-
Notifications
You must be signed in to change notification settings - Fork 78
Acl #1628
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Acl #1628
Changes from all commits
ac113b1
4fdac71
f7764dd
fdc6331
2450599
6a9bb9d
ee67abe
42f8e97
500d28b
6d9b7db
79c22e6
c40bc9d
982ef08
81d386a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,5 +3,3 @@ | |
|
|
||
| cd ui | ||
| npx lint-staged | ||
| ruff check | ||
| ruff format --check | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,91 @@ | ||
| import logging | ||
|
|
||
| from flask import abort | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
|
|
||
| class TRN: | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It would be nice to add a short docstring explain what TRN is, what it looks like. |
||
| def __init__(self, scope, type, name): | ||
| self.scope = scope | ||
| self.type = type | ||
| self.name = name | ||
|
|
||
| @staticmethod | ||
| def parse(trn): | ||
| elems = str.split(trn, ":") | ||
| if len(elems) < 5: | ||
| raise Exception("Malformed TRN") | ||
| return TRN(elems[2], elems[3], elems[4]) | ||
|
Comment on lines
+7
to
+12
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would use a class method here: @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])You avoid repeating the name of the class. It prevents errors if the class is renamed. And it works better with inheritance. |
||
|
|
||
| def __str__(self): | ||
| return f"trn:temboard:{self.scope}:{self.type}:{self.name}" | ||
|
|
||
| def parent(self): | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This would IMO be a good candidate for Then it would be called like this: |
||
| 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 | ||
|
|
||
| def expand(self): | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What about having a property called |
||
| trns = ["*"] | ||
| trn = TRN(self.scope, self.type, self.name) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| while str(trn) != "trn:temboard:*:*:*": | ||
| if str(trn) not in trns: | ||
| trns.append(str(trn)) | ||
| trn = trn.parent() | ||
| trns.append(str(trn)) | ||
| return trns | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,6 +8,7 @@ | |
| from sqlalchemy.orm import Query, relationship | ||
| from temboardtoolkit.utils import utcnow | ||
|
|
||
| from ..acl import TRN | ||
| from . import QUERIES | ||
|
|
||
| Model = declarative_base() | ||
|
|
@@ -62,6 +63,15 @@ def select_secret(cls, secret): | |
| def expired(self): | ||
| return self.edate < utcnow() | ||
|
|
||
| def trn(self): | ||
| return TRN("core", "apikey", str(self.id)) | ||
|
|
||
| def role_trns(self): | ||
| return self.trn().expand() | ||
|
|
||
| def resource_trns(self): | ||
| return self.trn().expand() | ||
|
Comment on lines
+66
to
+73
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We could possibly use |
||
|
|
||
|
|
||
| class Plugin(Model): | ||
| __tablename__ = "plugins" | ||
|
|
@@ -181,8 +191,8 @@ def asdict(self): | |
| phone=self.role_phone, | ||
| active=self.is_active, | ||
| admin=self.is_admin, | ||
| groups=[g.name for g in self.groups], | ||
| environments=[g.environment.name for g in self.groups], | ||
| groups=[g.name for g in self.groups if g.environment], | ||
| environments=[g.environment.name for g in self.groups if g.environment], | ||
|
Comment on lines
+194
to
+195
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it related to current commit? |
||
| ) | ||
|
|
||
| def select_environments(self): | ||
|
|
@@ -199,6 +209,19 @@ def select_instances(self): | |
| .columns(Instance.__mapper__.c.values()) | ||
| ) | ||
|
|
||
| def trn(self): | ||
| return TRN("core", "user", self.role_name) | ||
|
|
||
| def role_trns(self): | ||
| trns = set() | ||
| trns.update(self.trn().expand()) | ||
| for g in self.groups: | ||
| trns.update(g.trn().expand()) | ||
| return trns | ||
|
|
||
| def resource_trns(self): | ||
| return self.trn().expand() | ||
|
|
||
|
|
||
| class StubRole: | ||
| # Fake object for roles not in database. | ||
|
|
@@ -256,6 +279,12 @@ def delete_member(self, name, username): | |
| group=name, role=username | ||
| ) | ||
|
|
||
| def trn(self): | ||
| return TRN("core", "group", self.name) | ||
|
|
||
| def resource_trns(self): | ||
| return self.trn().expand() | ||
|
|
||
|
|
||
| class Environment(Model): | ||
| __tablename__ = "environments" | ||
|
|
@@ -327,6 +356,12 @@ def asdict(self): | |
| dba_group=self.dba_group.name, | ||
| ) | ||
|
|
||
| def trn(self): | ||
| return TRN("core", "environment", self.name) | ||
|
|
||
| def resource_trn(self): | ||
| return self.trn().expand() | ||
|
|
||
|
|
||
| class Instance(Model): | ||
| __tablename__ = "instances" | ||
|
|
@@ -522,3 +557,63 @@ def enable_plugin(self, plugin): | |
|
|
||
| def disable_plugin(self, plugin): | ||
| return Plugin.delete(self, plugin) | ||
|
|
||
| def trn(self): | ||
| return TRN( | ||
| "core", | ||
| "instance", | ||
| f"{self.environment.name}/{self.agent_address}:{self.agent_port}", | ||
| ) | ||
|
|
||
| def resource_trns(self): | ||
| return self.trn().expand() | ||
|
|
||
|
|
||
| class ACLRule(Model): | ||
| __tablename__ = "acl" | ||
| __table_args__ = {"schema": "application"} | ||
|
|
||
| id = Column(types.BigInteger, primary_key=True) | ||
| role = Column(types.UnicodeText) | ||
| action = Column(types.UnicodeText) | ||
| resource = Column(types.UnicodeText) | ||
| deny = Column(types.Boolean) | ||
| cdate = Column(types.TIMESTAMP(timezone=True)) | ||
| origin = Column(types.UnicodeText) | ||
|
|
||
| @classmethod | ||
| def insert(cls, role, action, resource, deny=False): | ||
| return Query(cls).from_statement( | ||
| text(QUERIES["acl-insert"]).bindparams( | ||
| role=role, action=action, resource=resource, deny=deny | ||
| ) | ||
| ) | ||
|
|
||
| @classmethod | ||
| def delete(cls, role, action, resource): | ||
| return Query(cls).from_statement( | ||
| text(QUERIES["acl-delete"]).bindparams( | ||
| role=role, action=action, resource=resource | ||
| ) | ||
| ) | ||
|
|
||
| @classmethod | ||
| def match(cls, roles, actions, resources): | ||
| return Query(cls).from_statement( | ||
| text(QUERIES["acl-get"]).bindparams( | ||
| roles=roles, actions=actions, resources=resources | ||
| ) | ||
| ) | ||
|
|
||
| def __repr__(self): | ||
| return f"<ACL stmt deny={self.deny} {self.role} for {self.action} on {self.resource}>" | ||
|
|
||
|
|
||
| class Anonymous: | ||
| @staticmethod | ||
| def trn(): | ||
| return TRN("*", "*", "*") | ||
|
|
||
| @staticmethod | ||
| def role_trns(): | ||
| return [Anonymous.trn()] | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| DELETE FROM | ||
| application.acl | ||
| WHERE | ||
| role = :role | ||
| AND action = :action | ||
| AND resource = :resource RETURNING *; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| SELECT | ||
| * | ||
| FROM | ||
| application.acl | ||
| WHERE | ||
| role = ANY(:roles) | ||
| AND ACTION = ANY(:actions) | ||
|
pirlgon marked this conversation as resolved.
|
||
| AND resource = ANY(:resources); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| INSERT INTO | ||
| application.acl(role, action, resource, deny) | ||
| VALUES | ||
| (:role, :action, :resource, :deny) | ||
| RETURNING *; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.