From 0ce6d25359e889fc1fa1e36704ae715f24f38dfc Mon Sep 17 00:00:00 2001 From: Tiago Garcia Date: Thu, 12 Dec 2024 01:24:20 +0000 Subject: [PATCH] Implement perms system Signed-off-by: Tiago Garcia --- delivery2/server/models/org.py | 4 +-- delivery2/server/models/user.py | 2 ++ delivery2/server/routes/user.py | 7 ---- delivery2/server/services/orgs.py | 58 ++++++++++++++++++++++++++++-- delivery2/server/services/users.py | 37 +++++++++++++++++++ delivery2/server/utils/perms.py | 33 +++++++++++++++++ 6 files changed, 130 insertions(+), 11 deletions(-) create mode 100644 delivery2/server/utils/perms.py diff --git a/delivery2/server/models/org.py b/delivery2/server/models/org.py index 2374eec..dd4e783 100644 --- a/delivery2/server/models/org.py +++ b/delivery2/server/models/org.py @@ -5,15 +5,15 @@ class Organization(db_connection.Model): id = db_connection.Column(db_connection.Integer, primary_key=True, index=True) name = db_connection.Column(db_connection.String, unique=True, index=True, nullable=False) + roles = db_connection.Column(db_connection.JSON, nullable=False, default=dict) users = db_connection.Column(db_connection.JSON, nullable=False, default=dict) users_id = db_connection.Column(db_connection.Integer, db_connection.ForeignKey('users.id')) - manager = db_connection.relationship('User', backref=db_connection.backref('owned_organization', uselist=False)) def to_dict(self): return { "id": self.id, "name": self.name, - "manager": self.manager.to_dict(), + "roles": [{"role": role, "permissions": permissions} for role, permissions in self.roles.items()], "users": [{"id": user_id, "user_data": { "username": user_data["username"], "full_name": user_data["full_name"], diff --git a/delivery2/server/models/user.py b/delivery2/server/models/user.py index 3325839..5c2eeae 100644 --- a/delivery2/server/models/user.py +++ b/delivery2/server/models/user.py @@ -9,6 +9,7 @@ class User(db_connection.Model): username = db_connection.Column(db_connection.String, unique=True, index=True, nullable=False) full_name = db_connection.Column(db_connection.String, nullable=False) email = db_connection.Column(db_connection.String, unique=True, index=True, nullable=False) + roles = db_connection.Column(db_connection.JSON, nullable=False, default=dict) public_keys = db_connection.Column(db_connection.JSON, nullable=False, default=dict) orgs = db_connection.Column(db_connection.JSON, nullable=False, default=dict) @@ -18,5 +19,6 @@ class User(db_connection.Model): "username": self.username, "full_name": self.full_name, "email": self.email, + "role": self.role, "orgs": [{"id": org_id, "name": org_data["name"], "status": org_data["status"]} for org_id, org_data in self.orgs.items()], } \ No newline at end of file diff --git a/delivery2/server/routes/user.py b/delivery2/server/routes/user.py index 4efea2b..cbda045 100644 --- a/delivery2/server/routes/user.py +++ b/delivery2/server/routes/user.py @@ -89,9 +89,6 @@ def user_create(): if not org: return jsonify({"error": "Organization not found"}), 404 - if org.manager.id != session.user_id: - return jsonify({"error": "Not authorized to create users"}), 403 - user = UserService.get_user_by_username(data["username"]) if not user: user = UserService.create_user( @@ -118,8 +115,6 @@ def user_suspend(username): org = OrganizationService.get_organization(session.org_id) if not org: return jsonify({"error": "Organization not found"}), 404 - if org.manager.id != session.user_id: - return jsonify({"error": "Not authorized to suspend users"}), 403 user = UserService.get_user_by_username(username) if not user: @@ -141,8 +136,6 @@ def user_unsuspend(username): org = OrganizationService.get_organization(session.org_id) if not org: return jsonify({"error": "Organization not found"}), 404 - if org.manager.id != session.user_id: - return jsonify({"error": "Not authorized to unsuspend users"}), 403 user = UserService.get_user_by_username(username) if not user: diff --git a/delivery2/server/services/orgs.py b/delivery2/server/services/orgs.py index a5e7388..67fbf17 100644 --- a/delivery2/server/services/orgs.py +++ b/delivery2/server/services/orgs.py @@ -3,6 +3,7 @@ import os.path from database import db from models import Organization, User from sqlalchemy.orm.attributes import flag_modified +from utils.perms import Perm class OrganizationService: @@ -18,9 +19,33 @@ class OrganizationService: if not os.path.exists(os.path.join(repos, name)): os.mkdir(os.path.join(repos, name)) + roles = { + "manager": { + "permissions": Perm.get_int([ + Perm.DOC_ACL, + Perm.DOC_READ, + Perm.DOC_DELETE, + Perm.ROLE_ACL, + Perm.SUBJECT_NEW, + Perm.SUBJECT_DOWN, + Perm.SUBJECT_UP, + Perm.DOC_NEW, + Perm.ROLE_NEW, + Perm.ROLE_DOWN, + Perm.ROLE_UP, + Perm.ROLE_MOD + ]), + "users": [user.id] + }, + "default": { + "permissions": Perm.get_int([]), + "users": [] + } + } + organization = Organization( name=name, - manager=user, + roles=roles, users={user.id: { "username": user.username, "full_name": user.full_name, @@ -34,6 +59,7 @@ class OrganizationService: db.refresh(organization) UserService().add_org_to_user(user, organization) + UserService().add_role_to_user(user, organization, "manager") UserService().add_public_key_to_user(user, organization, public_key) return organization @@ -71,12 +97,40 @@ class OrganizationService: db.refresh(org) return org + @staticmethod + def create_role(org: Organization, role: str, perms: list[Perm]) -> Organization: + roles = org.roles.copy() + roles[role] = { + "permissions": Perm.get_int(perms), + "users": [] + } + org.roles = roles + flag_modified(org, "roles") + db.commit() + db.refresh(org) + return org + + @staticmethod + def delete_role(org: Organization, role: str) -> Organization: + roles = org.roles.copy() + del roles[role] + org.roles = roles + flag_modified(org, "roles") + db.commit() + db.refresh(org) + return org + + @staticmethod + def check_role_permission(org: Organization, role: str, perm: Perm) -> bool: + role_perms = org.roles[role]["permissions"] + return Perm.check_perm(role_perms, perm.value) + @staticmethod def suspend_user(org: Organization, user: User) -> tuple: if OrganizationService.get_user_status(org, user.id) != "active": return {"error": "User already suspended"}, 400 - if org.manager.id == user.id: + if user.roles[org.id] == "manager": return {"error": "Cannot suspend manager"}, 400 org.users[str(user.id)]["status"] = "suspended" diff --git a/delivery2/server/services/users.py b/delivery2/server/services/users.py index 2f11bca..4b1f3e8 100644 --- a/delivery2/server/services/users.py +++ b/delivery2/server/services/users.py @@ -1,3 +1,5 @@ +from sqlalchemy.orm.attributes import flag_modified + from database import db from models import User, Organization @@ -10,6 +12,7 @@ class UserService: username=username, full_name=full_name, email=email, + roles={}, public_keys={org.id: public_key} if org else {}, orgs={org.id: { "name": org.name, @@ -43,6 +46,40 @@ class UserService: db.refresh(user) return user + @staticmethod + def add_role_to_user(user: User, org: Organization, role: str) -> User: + roles = user.roles.copy() + roles[org.id] = role + user.roles = roles + flag_modified(user, "roles") + db.commit() + db.refresh(user) + + roles = org.roles.copy() + roles[role]["users"].append(user.id) + org.roles = roles + flag_modified(org, "roles") + db.commit() + db.refresh(org) + return user + + @staticmethod + def remove_role_from_user(user: User, org: Organization, role: str) -> User: + roles = user.roles.copy() + roles.pop(org.id) + user.roles = roles + flag_modified(user, "roles") + db.commit() + db.refresh(user) + + roles = org.roles.copy() + roles[role]["users"].remove(user.id) + org.roles = roles + flag_modified(org, "roles") + db.commit() + db.refresh(org) + return user + @staticmethod def add_public_key_to_user(user: User, org: Organization, public_key: str) -> User: public_keys = user.public_keys.copy() diff --git a/delivery2/server/utils/perms.py b/delivery2/server/utils/perms.py new file mode 100644 index 0000000..eb666d5 --- /dev/null +++ b/delivery2/server/utils/perms.py @@ -0,0 +1,33 @@ +from enum import Enum + + +class Perm(Enum): + DOC_ACL = 0b000000000001 + DOC_READ = 0b000000000010 + DOC_DELETE = 0b000000000100 + ROLE_ACL = 0b000000001000 + SUBJECT_NEW = 0b000000010000 + SUBJECT_DOWN = 0b000000100000 + SUBJECT_UP = 0b000001000000 + DOC_NEW = 0b000010000000 + ROLE_NEW = 0b000100000000 + ROLE_DOWN = 0b001000000000 + ROLE_UP = 0b010000000000 + ROLE_MOD = 0b100000000000 + + @staticmethod + def get_perm(bit_array: int): + for perm in Perm: + if bit_array == perm.value: + return perm + return None + + @staticmethod + def get_int(perms): + if isinstance(perms, list): + return sum([p.value for p in perms if isinstance(p, Perm)]) + return perms.value if isinstance(perms, Perm) else 0 + + @staticmethod + def check_perm(perm, bit_array: int): + return perm.value & bit_array == perm.value