2026-05-25 09:05:17 +01:00
|
|
|
"""
|
2026-05-28 13:22:24 +01:00
|
|
|
Router endpoints for IAM
|
2026-05-25 09:05:17 +01:00
|
|
|
|
|
|
|
|
Endpoints:
|
2026-05-28 13:22:24 +01:00
|
|
|
- [POST](/iam/can_act_on_resource): [API key & user claim]: Service access point to verify user permissions
|
|
|
|
|
- [GET](/iam/group/permissions): [root user]: Gets list of perms(service, resource, action) the given group(id) has
|
|
|
|
|
- [DELETE](/iam/group/permissions): [root user]: Removes a given perm(id) from the given group(id)
|
|
|
|
|
- [GET](/iam/group/users): [root user]: Gets a list of users(id, name, email) that are assigned to the given group(id)
|
|
|
|
|
- [POST](/iam/group): [root user]: Creates a new group for the given org(id)
|
|
|
|
|
- [PUT](/iam/group/permission): [root user]: Assigns a perm(id) to the given group(id)
|
|
|
|
|
- [PUT](/iam/group/user): [root user]: Assigns a user(id) to a group(id)
|
|
|
|
|
- [DELETE](/iam/group/user): [root user]: Removes a user(id) from the given group(id)
|
|
|
|
|
- [GET](/iam/permissions): [root user]: Gets a list of all permissions
|
|
|
|
|
- [POST](/iam/permission): [super admin]: Creates a new permission
|
|
|
|
|
- [DELETE](/iam/permission): [super admin]: Removes a permission
|
|
|
|
|
- [GET](/iam/permissions/search): [root user]: Returns a list of permissions matching a filter(service|resource|action)
|
2026-05-25 09:05:17 +01:00
|
|
|
"""
|
2026-05-27 14:58:10 +01:00
|
|
|
from fastapi import APIRouter, status
|
2026-05-27 16:26:34 +01:00
|
|
|
from sqlalchemy.exc import IntegrityError
|
|
|
|
|
from psycopg import errors
|
2026-05-25 09:05:17 +01:00
|
|
|
|
2026-05-29 09:50:09 +01:00
|
|
|
from src.exceptions import ConflictException
|
2026-05-25 09:05:17 +01:00
|
|
|
from src.database import db_dependency
|
|
|
|
|
from src.schemas import ResourceName
|
2026-05-27 15:35:06 +01:00
|
|
|
from src.auth.exceptions import UnauthorizedException
|
2026-05-25 09:05:17 +01:00
|
|
|
from src.auth.service import claims_dependency
|
2026-05-27 15:35:06 +01:00
|
|
|
from src.auth.dependencies import org_model_root_claim_query_dependency, org_model_root_claim_body_dependency, \
|
|
|
|
|
super_admin_dependency
|
2026-05-25 09:05:17 +01:00
|
|
|
from src.user.models import User
|
2026-05-27 14:58:10 +01:00
|
|
|
from src.user.dependencies import user_model_body_dependency
|
2026-05-25 09:05:17 +01:00
|
|
|
from src.organisation.models import Organisation as Org
|
|
|
|
|
from src.service.models import Service
|
|
|
|
|
|
|
|
|
|
from src.iam.service import service_key_dependency
|
|
|
|
|
from src.iam.models import Permission as Perm, GroupPermissions as GPerms, Group, UserGroups
|
2026-05-27 11:11:19 +01:00
|
|
|
from src.iam.dependencies import group_model_query_dependency, group_model_body_dependency, perm_model_body_dependency
|
2026-05-27 15:35:06 +01:00
|
|
|
from src.iam.schemas import IAMGetGroupPermissionsResponse, IAMGetGroupUsersResponse, IAMPostGroupRequest, \
|
2026-05-28 13:37:32 +01:00
|
|
|
GroupSchema, IAMPostGroupResponse, IAMPutGroupPermissionRequest, IAMPutGroupPermissionResponse, \
|
2026-05-27 15:35:06 +01:00
|
|
|
IAMPutGroupUserRequest, IAMPutGroupUserResponse, IAMDeleteGroupPermissionRequest, IAMDeleteGroupPermissionResponse, \
|
|
|
|
|
IAMDeleteGroupUserRequest, IAMDeleteGroupUserResponse, IAMGetPermissionsResponse, IAMPostPermissionRequest, \
|
2026-05-28 13:37:32 +01:00
|
|
|
IAMPostPermissionResponse, PermissionSchema, IAMDeletePermissionRequest, IAMGetPermissionsSearchRequest, IAMGetPermissionsSearchResponse
|
2026-05-25 09:05:17 +01:00
|
|
|
|
|
|
|
|
router = APIRouter(
|
|
|
|
|
tags=["IAM"],
|
|
|
|
|
prefix="/iam",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/can_act_on_resource")
|
|
|
|
|
async def can_act_on_resource(valid_key: service_key_dependency, db: db_dependency, user_claims: claims_dependency,
|
|
|
|
|
rn: ResourceName, action: str) -> bool:
|
|
|
|
|
try:
|
|
|
|
|
user_id = user_claims["db_id"]
|
|
|
|
|
rn_org = rn.organisation
|
|
|
|
|
rn_service = rn.service
|
|
|
|
|
rn_resource = rn.resource
|
|
|
|
|
|
|
|
|
|
result = (db.query(Perm)
|
|
|
|
|
.join(Service, Service.id == Perm.service_id)
|
|
|
|
|
.join(GPerms, GPerms.permission_id == Perm.id)
|
|
|
|
|
.join(Group, Group.id == GPerms.group_id)
|
|
|
|
|
.join(Org, Org.id == Group.org_id)
|
|
|
|
|
.join(UserGroups, UserGroups.group_id == Group.id)
|
|
|
|
|
.join(User, User.id == UserGroups.user_id)
|
|
|
|
|
.filter(User.id == user_id)
|
|
|
|
|
.filter(Org.name == rn_org)
|
|
|
|
|
.filter(Service.name == rn_service)
|
|
|
|
|
.filter(Perm.resource == rn_resource)
|
|
|
|
|
.filter(Perm.action == action)
|
|
|
|
|
).first()
|
|
|
|
|
|
|
|
|
|
if result:
|
|
|
|
|
return True
|
|
|
|
|
else:
|
|
|
|
|
return False
|
2026-05-27 14:58:10 +01:00
|
|
|
except Exception:
|
|
|
|
|
raise UnauthorizedException()
|
2026-05-25 09:05:17 +01:00
|
|
|
|
|
|
|
|
|
2026-05-26 16:25:14 +01:00
|
|
|
@router.get("/group/permissions", response_model=IAMGetGroupPermissionsResponse)
|
2026-05-27 15:35:06 +01:00
|
|
|
async def get_group_permissions(group_model: group_model_query_dependency, org_model: org_model_root_claim_query_dependency):
|
|
|
|
|
if group_model.org_id != org_model.id:
|
|
|
|
|
raise UnauthorizedException()
|
2026-05-26 16:25:14 +01:00
|
|
|
return {"permissions": group_model.permission_rel}
|
2026-05-25 09:05:17 +01:00
|
|
|
|
|
|
|
|
|
2026-05-26 16:25:14 +01:00
|
|
|
@router.get("/group/users", response_model=IAMGetGroupUsersResponse)
|
2026-05-27 15:35:06 +01:00
|
|
|
async def get_group_users(group_model: group_model_query_dependency, org_model: org_model_root_claim_query_dependency):
|
2026-06-02 12:22:36 +01:00
|
|
|
if group_model.org_id != org_model.id:
|
2026-05-27 15:35:06 +01:00
|
|
|
raise UnauthorizedException()
|
2026-05-26 16:25:14 +01:00
|
|
|
return {"users": group_model.user_rel}
|
2026-05-25 09:05:17 +01:00
|
|
|
|
|
|
|
|
|
2026-05-26 16:25:14 +01:00
|
|
|
@router.post("/group", response_model=IAMPostGroupResponse)
|
2026-05-27 15:35:06 +01:00
|
|
|
async def create_group(db: db_dependency, org_model: org_model_root_claim_body_dependency, request_model: IAMPostGroupRequest):
|
2026-05-27 12:21:03 +01:00
|
|
|
group_model = Group(name=request_model.name, org_id=org_model.id)
|
2026-05-25 09:05:17 +01:00
|
|
|
|
|
|
|
|
db.add(group_model)
|
2026-05-27 16:26:34 +01:00
|
|
|
try:
|
|
|
|
|
db.flush()
|
|
|
|
|
except IntegrityError as e:
|
|
|
|
|
if isinstance(e.orig, errors.UniqueViolation):
|
2026-05-29 09:50:09 +01:00
|
|
|
raise ConflictException("Group with this name already exists")
|
2026-05-28 13:37:32 +01:00
|
|
|
response = GroupSchema(**group_model.__dict__)
|
2026-05-25 09:05:17 +01:00
|
|
|
db.commit()
|
2026-05-26 16:25:14 +01:00
|
|
|
return {"group": response}
|
2026-05-25 09:05:17 +01:00
|
|
|
|
|
|
|
|
|
2026-05-26 16:25:14 +01:00
|
|
|
@router.put("/group/permission", response_model=IAMPutGroupPermissionResponse)
|
2026-05-27 15:35:06 +01:00
|
|
|
async def add_group_permission(db: db_dependency, group_model: group_model_body_dependency, perm_model: perm_model_body_dependency, org_model: org_model_root_claim_body_dependency, request_model: IAMPutGroupPermissionRequest):
|
|
|
|
|
if group_model.org_id == org_model.id:
|
|
|
|
|
raise UnauthorizedException()
|
|
|
|
|
|
2026-05-27 16:26:34 +01:00
|
|
|
if perm_model in group_model.permission_rel:
|
2026-05-29 09:50:09 +01:00
|
|
|
raise ConflictException("Group already has this permission")
|
2026-05-27 16:26:34 +01:00
|
|
|
|
2026-05-26 16:25:14 +01:00
|
|
|
group_model.permission_rel.append(perm_model)
|
2026-05-25 09:05:17 +01:00
|
|
|
|
2026-05-26 16:25:14 +01:00
|
|
|
db.flush()
|
2026-05-28 13:37:32 +01:00
|
|
|
response = IAMPutGroupPermissionResponse(group=GroupSchema(**group_model.__dict__), permissions=group_model.permission_rel)
|
2026-05-26 16:25:14 +01:00
|
|
|
db.commit()
|
|
|
|
|
return response
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.put("/group/user")
|
2026-05-27 15:35:06 +01:00
|
|
|
async def add_group_user(db: db_dependency, group_model: group_model_body_dependency, user_model: user_model_body_dependency, org_model: org_model_root_claim_body_dependency, request_model: IAMPutGroupUserRequest):
|
|
|
|
|
if group_model.org_id == org_model.id:
|
|
|
|
|
raise UnauthorizedException()
|
|
|
|
|
|
2026-05-27 16:26:34 +01:00
|
|
|
if user_model in group_model.user_rel:
|
2026-05-29 09:50:09 +01:00
|
|
|
raise ConflictException("User already in group")
|
2026-05-27 16:26:34 +01:00
|
|
|
|
2026-05-26 16:25:14 +01:00
|
|
|
group_model.user_rel.append(user_model)
|
|
|
|
|
db.flush()
|
2026-05-28 13:37:32 +01:00
|
|
|
response = IAMPutGroupUserResponse(group=GroupSchema(**group_model.__dict__), users=group_model.user_rel)
|
2026-05-25 09:05:17 +01:00
|
|
|
db.commit()
|
2026-05-26 16:25:14 +01:00
|
|
|
return response
|
2026-05-25 09:05:17 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.delete("/group/permissions")
|
2026-05-27 15:35:06 +01:00
|
|
|
async def remove_group_permissions(db: db_dependency, group_model: group_model_body_dependency, perm_model: perm_model_body_dependency, org_model: org_model_root_claim_body_dependency, request_model: IAMDeleteGroupPermissionRequest):
|
|
|
|
|
if group_model.org_id == org_model.id:
|
|
|
|
|
raise UnauthorizedException()
|
|
|
|
|
|
2026-05-26 16:25:14 +01:00
|
|
|
group_model.permission_rel.remove(perm_model)
|
|
|
|
|
db.flush()
|
2026-05-28 13:37:32 +01:00
|
|
|
response = IAMDeleteGroupPermissionResponse(group=GroupSchema(**group_model.__dict__),
|
|
|
|
|
permissions=group_model.permission_rel)
|
2026-05-25 09:05:17 +01:00
|
|
|
db.commit()
|
2026-05-26 16:25:14 +01:00
|
|
|
return response
|
2026-05-25 09:05:17 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.delete("/group/user")
|
2026-05-27 15:35:06 +01:00
|
|
|
async def remove_group_user(db: db_dependency, group_model: group_model_body_dependency, user_model: user_model_body_dependency, org_model: org_model_root_claim_body_dependency, request_model: IAMDeleteGroupUserRequest):
|
|
|
|
|
if group_model.org_id == org_model.id:
|
|
|
|
|
raise UnauthorizedException()
|
|
|
|
|
|
2026-05-26 16:25:14 +01:00
|
|
|
user_model.group_rel.remove(group_model)
|
|
|
|
|
db.flush()
|
2026-05-28 13:37:32 +01:00
|
|
|
response = IAMDeleteGroupUserResponse(group=GroupSchema(**group_model.__dict__), users=group_model.user_rel)
|
2026-05-25 09:05:17 +01:00
|
|
|
db.commit()
|
2026-05-26 16:25:14 +01:00
|
|
|
|
|
|
|
|
return response
|
2026-05-25 09:05:17 +01:00
|
|
|
|
|
|
|
|
|
2026-05-26 16:25:14 +01:00
|
|
|
@router.get("/permissions", response_model=IAMGetPermissionsResponse)
|
2026-05-27 15:35:06 +01:00
|
|
|
async def get_permissions(db: db_dependency, org_model: org_model_root_claim_body_dependency):
|
2026-05-25 09:05:17 +01:00
|
|
|
permission_models = db.query(Perm).all()
|
|
|
|
|
|
2026-05-26 16:25:14 +01:00
|
|
|
return {"permissions": permission_models}
|
2026-05-25 09:05:17 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/permission")
|
2026-05-27 15:35:06 +01:00
|
|
|
async def create_new_permission(db: db_dependency, su: super_admin_dependency, request_mode: IAMPostPermissionRequest):
|
2026-05-26 16:25:14 +01:00
|
|
|
perm_model = Perm(**request_mode.__dict__)
|
2026-05-27 16:26:34 +01:00
|
|
|
try:
|
|
|
|
|
db.add(perm_model)
|
|
|
|
|
except IntegrityError as e:
|
|
|
|
|
if isinstance(e.orig, errors.UniqueViolation):
|
2026-05-29 09:50:09 +01:00
|
|
|
raise ConflictException(message="Permission already exists")
|
2026-05-26 16:25:14 +01:00
|
|
|
db.flush()
|
2026-05-28 13:37:32 +01:00
|
|
|
response = IAMPostPermissionResponse(permission=PermissionSchema(**perm_model.__dict__))
|
2026-05-25 09:05:17 +01:00
|
|
|
db.commit()
|
2026-05-26 16:25:14 +01:00
|
|
|
return response
|
2026-05-25 09:05:17 +01:00
|
|
|
|
|
|
|
|
|
2026-05-26 16:25:14 +01:00
|
|
|
@router.delete("/permission", status_code=status.HTTP_204_NO_CONTENT)
|
2026-05-27 15:35:06 +01:00
|
|
|
async def delete_permission(db: db_dependency, su: super_admin_dependency, perm_model: perm_model_body_dependency, request_model: IAMDeletePermissionRequest):
|
2026-05-25 09:05:17 +01:00
|
|
|
db.delete(perm_model)
|
|
|
|
|
db.commit()
|
|
|
|
|
|
|
|
|
|
|
2026-05-26 16:25:14 +01:00
|
|
|
@router.get("/permissions/search", response_model=IAMGetPermissionsSearchResponse)
|
2026-05-27 15:35:06 +01:00
|
|
|
async def get_permissions(db: db_dependency, org_model: org_model_root_claim_body_dependency, search: IAMGetPermissionsSearchRequest):
|
2026-05-25 09:05:17 +01:00
|
|
|
permission_query = db.query(Perm)
|
|
|
|
|
|
2026-05-26 16:25:14 +01:00
|
|
|
if search.service_id is not None:
|
|
|
|
|
permission_query = permission_query.filter(Perm.service_id == search.service_id)
|
2026-05-25 09:05:17 +01:00
|
|
|
|
2026-05-26 16:25:14 +01:00
|
|
|
if search.resource is not None:
|
|
|
|
|
permission_query = permission_query.filter(Perm.resource == search.resource)
|
2026-05-25 09:05:17 +01:00
|
|
|
|
2026-05-26 16:25:14 +01:00
|
|
|
if search.action is not None:
|
|
|
|
|
permission_query = permission_query.filter(Perm.action == search. action)
|
2026-05-25 09:05:17 +01:00
|
|
|
|
|
|
|
|
permission_models = permission_query.all()
|
|
|
|
|
|
2026-05-26 16:25:14 +01:00
|
|
|
return {"permissions": permission_models}
|