Skip to content
Draft

Acl #1628

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 16 additions & 6 deletions tests/test_25_environments.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import pytest
from fixtures.utils import retry_fast
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait


def test_create(prod, browser):
Expand Down Expand Up @@ -34,17 +35,26 @@ def test_update(prod, browser):
def test_add_member(alice_member, browse_prod_members):
browser = browse_prod_members

for attempt in retry_fast(AssertionError):
with attempt:
assert (
"alice" in browser.select("tbody tr:nth-child(1) td:nth-child(1)").text
wait = WebDriverWait(browser, 10)
element = wait.until(
EC.visibility_of_element_located(
(
By.CSS_SELECTOR,
"tbody tr:nth-child(1) td:nth-child(1)",
)
)
)
assert "alice" in element.text


def test_remove_member(alice_member, browse_prod_members):
browser = browse_prod_members
browser.select("tbody tr:nth-child(1) button").click()
username = browser.select(".modal-body strong").text
wait = WebDriverWait(browser, 10)
element = wait.until(
EC.visibility_of_element_located((By.CSS_SELECTOR, ".modal-body strong"))
)
username = element.text
assert username.startswith("a") # admin or alice
browser.select("#buttonDelete").click()
browser.absent("tbody tr:nth-child(2)")
Expand Down
2 changes: 0 additions & 2 deletions ui/.husky/pre-commit
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,3 @@

cd ui
npx lint-staged
ruff check
ruff format --check
6 changes: 5 additions & 1 deletion ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,11 @@
"vite": "^8.1.5"
},
"lint-staged": {
"*.{js,css,vue}": "prettier --write"
"*.{js,css,vue}": "prettier --write",
"*.py": [
"ruff check",
"ruff format --check"
]
},
"allowScripts": {
"vue-demi@0.14.8": true,
Expand Down
5 changes: 5 additions & 0 deletions ui/share/sql/dev-fixture.sql
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ VALUES
('admin', (SELECT id FROM application.groups WHERE name = 'stable/dba')),
('admin', (SELECT id FROM application.groups WHERE name = 'mass/dba'));

INSERT INTO application.acl (role, action, resource)
VALUES
('trn:temboard:core:group:mass/dba', '*', 'trn:temboard:core:instance:mass'),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
('trn:temboard:core:group:mass/dba', '*', 'trn:temboard:core:instance:mass'),
('trn:temboard:core:group:mass/dba', '*', 'trn:temboard:core:instance:mass/*'),

('trn:temboard:core:group:stable/dba', '*', 'trn:temboard:core:instance:stable');

-- Pre-register agents

INSERT INTO application.instances
Expand Down
111 changes: 111 additions & 0 deletions ui/temboardui/acl.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import logging

from flask import abort

logger = logging.getLogger(__name__)


class TRN:
Comment thread
pirlgon marked this conversation as resolved.
"""
TRN represent a temboard role/ressource name.
It's a kind of path separated by `:`.

Alice user:
trn:temboard:core:user:alice

pg001.bridoulou.fr instance from prod environment:
trn:temboard:core:instance:prod/pg001.bridoulou.fr:5432
"""

def __init__(self, scope, type, name):
self.scope = scope
self.type = type
self.name = name

def __eq__(self, value):
return str(self) == str(value)

def __hash__(self):
return hash(str(self))

@classmethod
def parse(cls, trn):
elems = str.split(trn, ":")
if len(elems) < 5:
raise Exception("Malformed TRN")
return cls(elems[2], elems[3], elems[4])

def __str__(self):
return f"trn:temboard:{self.scope}:{self.type}:{self.name}"

@property
def parent(self):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would IMO be a good candidate for @property.

Then it would be called like this:
parent = TRN.parse("trn:temboard:core:user:alice").parent

parent = TRN(self.scope, self.type, self.name)

if self.name != "*":
parent.name = "*"
if "/" in self.name:
names = str.split(self.name, "/")
parent.name = "/".join(names[:-1])
return parent
if self.type != "*":
parent.type = "*"
return parent
parent.scope = "*"
return parent

@property
def parents(self):
trns = []
trn = self
while str(trn) != "trn:temboard:*:*:*":
if trn not in trns:
trns.append(trn)
trn = trn.parent
trns.append(trn)
trns.append("*")
return trns

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of returning a list of strings, why not returning a list of TRN instances?



class ACLResult:
def __init__(self, role, action, resource, decision="allowed", statements=None):
self.role = role
self.action = action
self.resource = resource
self.decision = decision
self.statements = statements or []

def raise_for_decision(self):
log_prefix = "Access <%s %s on %s> "
log_args = (self.role, self.action, self.resource or "*")

if self.decision == "allowed":
logger.debug(
log_prefix + "allowed by %s",
*log_args,
", ".join(repr(s) for s in self.statements),
)
return True
else:
if self.decision == "implicitDeny":
logger.debug(log_prefix + "implicitly denied.", *log_args)
else:
logger.debug(
log_prefix + "denied by %s",
*log_args,
", ".join(repr(s) for s in self.statements if s.deny),
)
raise abort(403)


def expand_actions(action):
"""Returns the list of pattern relevant for this action."""
actions = ["*"]
if action != "*":
method, _, endpoint = action.partition(":")
if method != "*":
actions.append("*:" + endpoint)
elif endpoint != "*":
actions.append(method + ":*")
actions.append(action)
return actions
2 changes: 2 additions & 0 deletions ui/temboardui/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

from flask import current_app as app
from itsdangerous import URLSafeTimedSerializer
from sqlalchemy.orm import selectinload
from sqlalchemy.orm.exc import NoResultFound

from temboardui.errors import TemboardUIError
Expand Down Expand Up @@ -96,6 +97,7 @@ def get_role_by_cookie(session, content):
try:
role = (
session.query(Role)
.options(selectinload(Role.groups))
.filter(Role.role_name == str(c_role_name), Role.is_active.is_(True))
.one()
)
Expand Down
3 changes: 1 addition & 2 deletions ui/temboardui/handlers/settings/metadata.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import logging

from temboardui.web.tornado import admin_required, app, render_template
from temboardui.web.tornado import app, render_template

from ...version import inspect_versions

logger = logging.getLogger(__name__)


@app.route(r"/settings/metadata")
@admin_required
def metadata(request):
versions_info = inspect_versions()
infos = {
Expand Down
Loading