From 662b9c8e268db74fef726fbde54cc49569277c46 Mon Sep 17 00:00:00 2001 From: luxferre Date: Tue, 16 Jun 2026 13:51:31 +0100 Subject: [PATCH] feat: permission permissions Orgs can only grant permissions to groups that they themselves have been granted access to. Super admin bypasses not added, flagged as todos. --- .../2026-06-16_org_perm_perms_join_table.py | 38 +++++++++++++++++++ src/iam/models.py | 14 +++++++ src/iam/router.py | 12 +++++- src/organisation/models.py | 4 ++ test/conftest.py | 5 ++- test/test_iam.py | 2 +- 6 files changed, 71 insertions(+), 4 deletions(-) create mode 100644 .alembic/versions/2026-06-16_org_perm_perms_join_table.py diff --git a/.alembic/versions/2026-06-16_org_perm_perms_join_table.py b/.alembic/versions/2026-06-16_org_perm_perms_join_table.py new file mode 100644 index 0000000..c0043c5 --- /dev/null +++ b/.alembic/versions/2026-06-16_org_perm_perms_join_table.py @@ -0,0 +1,38 @@ +"""org perm perms join table + +Revision ID: 85edbf9a176c +Revises: 98e20aae555c +Create Date: 2026-06-16 13:31:57.427953 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '85edbf9a176c' +down_revision: Union[str, Sequence[str], None] = '98e20aae555c' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('org_permissions', + sa.Column('org_id', sa.Integer(), nullable=False), + sa.Column('permission_id', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['org_id'], ['organisation.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['permission_id'], ['permission.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('org_id', 'permission_id') + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('org_permissions') + # ### end Alembic commands ### diff --git a/src/iam/models.py b/src/iam/models.py index 1387cca..a06ff79 100644 --- a/src/iam/models.py +++ b/src/iam/models.py @@ -52,6 +52,10 @@ class Permission(Base): "Group", secondary="group_permissions", back_populates="permission_rel" ) + org_rel = relationship( + "Organisation", secondary="org_permissions", back_populates="permission_rel" + ) + class Group(Base): __tablename__ = "group" @@ -95,3 +99,13 @@ class UserGroups(Base): group_id = Column( Integer, ForeignKey("group.id", ondelete="CASCADE"), primary_key=True ) + + +class OrgPermissions(Base): + __tablename__ = "org_permissions" + org_id = Column( + Integer, ForeignKey("organisation.id", ondelete="CASCADE"), primary_key=True + ) + permission_id = Column( + Integer, ForeignKey("permission.id", ondelete="CASCADE"), primary_key=True + ) diff --git a/src/iam/router.py b/src/iam/router.py index f688383..69073ea 100644 --- a/src/iam/router.py +++ b/src/iam/router.py @@ -325,6 +325,9 @@ async def add_group_permission( if perm_model in group_model.permission_rel: raise ConflictException("Group already has this permission") + if perm_model not in org_model.permission_rel: # TODO: and not su + raise ForbiddenException("You cannot grant this permission") + group_model.permission_rel.append(perm_model) db.flush() @@ -471,8 +474,10 @@ async def get_permissions( """ Returns a full list of permissions. """ - permission_models = db.query(Perm).all() - + # TODO: if su: + # permission_models = db.query(Perm).all() + # else + permission_models = db.query(Perm).filter(Perm.org_rel.any(id=org_model.id)).all() return {"permissions": permission_models} @@ -566,6 +571,9 @@ async def post_permissions( if not (request_model.action is None or request_model.action == ""): permission_query = permission_query.filter(Perm.action == request_model.action) + # TODO: if not su: + permission_query = permission_query.filter(Perm.org_rel.any(id=org_model.id)) + permission_models = permission_query.all() return {"permissions": permission_models} diff --git a/src/organisation/models.py b/src/organisation/models.py index 5879ab6..c333c40 100644 --- a/src/organisation/models.py +++ b/src/organisation/models.py @@ -55,6 +55,10 @@ class Organisation(Base): "Contact", foreign_keys="Organisation.owner_contact_id" ) + permission_rel = relationship( + "Permission", secondary="org_permissions", back_populates="org_rel" + ) + class OrgUsers(Base): __tablename__ = "orgusers" diff --git a/test/conftest.py b/test/conftest.py index a36e9a3..417cd68 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -10,7 +10,7 @@ from src.user.models import User from src.service.models import Service from src.organisation.models import Organisation as Org, OrgUsers from src.contact.models import Contact -from src.iam.models import Group, Permission +from src.iam.models import Group, Permission, OrgPermissions from src.auth.service import get_current_user, get_dev_user from src.auth.dependencies import empty_su_list, get_super_admin_list, testing_su_list from src.main import app # inited FastAPI app @@ -163,6 +163,9 @@ def _seed(db): db.add(Service(name="Test Service", api_key="123456789")) db.add(Permission(service_id=1, resource="test_resource", action="read")) db.add(Permission(service_id=1, resource="test_resource", action="move")) + db.add(Permission(service_id=1, resource="test_resource", action="delete")) + db.add(OrgPermissions(org_id=1, permission_id=1)) + db.add(OrgPermissions(org_id=1, permission_id=2)) db.add(Group(name="Org One Group", org_id=1)) db.add(Group(name="Org Two Group", org_id=2)) db.add(Group(name="Org One Group Two", org_id=1)) diff --git a/test/test_iam.py b/test/test_iam.py index b9e597c..e65b630 100644 --- a/test/test_iam.py +++ b/test/test_iam.py @@ -437,7 +437,7 @@ async def test_post_perm_success(default_client: AsyncClient): assert "permission" in data assert isinstance(data["permission"], dict) - assert data["permission"]["id"] == 3 + assert data["permission"]["id"] == 4 assert data["permission"]["service_name"] == "Test Service" assert data["permission"]["resource"] == "test_resource" assert data["permission"]["action"] == "create"