# #region AdminApi [C:5] [TYPE Module] [SEMANTICS fastapi, admin, api, rbac, user] # # @BRIEF Admin API endpoints for user and role management. # @LAYER: API # @RELATION DEPENDS_ON -> [AuthRepository:Class] # @RELATION DEPENDS_ON -> [get_auth_db:Function] # @RELATION DEPENDS_ON -> [has_permission:Function] # # @INVARIANT: All endpoints in this module require 'Admin' role or 'admin' scope. from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy.orm import Session from ...core.auth.repository import AuthRepository from ...core.auth.security import get_password_hash from ...core.database import get_auth_db from ...core.logger import belief_scope, logger from ...dependencies import get_plugin_loader, has_permission from ...models.auth import ADGroupMapping, Role, User from ...schemas.auth import ( ADGroupMappingCreate, ADGroupMappingSchema, PermissionSchema, RoleCreate, RoleSchema, RoleUpdate, User as UserSchema, UserCreate, UserUpdate, ) from ...services.rbac_permission_catalog import ( discover_declared_permissions, sync_permission_catalog, ) # #region router [TYPE Variable] # @RELATION DEPENDS_ON -> fastapi.APIRouter # @BRIEF APIRouter instance for admin routes. router = APIRouter(prefix="/api/admin", tags=["admin"]) # #endregion router # #region list_users [C:3] [TYPE Function] # @BRIEF Lists all registered users. # @PRE: Current user has 'Admin' role. # @POST: Returns a list of UserSchema objects. # @RELATION CALLS -> User @router.get("/users", response_model=list[UserSchema]) async def list_users( db: Session = Depends(get_auth_db), _=Depends(has_permission("admin:users", "READ")) ): with belief_scope("api.admin.list_users"): users = db.query(User).all() return users # #endregion list_users # #region create_user [C:3] [TYPE Function] # @BRIEF Creates a new local user. # @PRE: Current user has 'Admin' role. # @POST: New user is created in the database. # @RELATION CALLS -> [AuthRepository:Class] @router.post("/users", response_model=UserSchema, status_code=status.HTTP_201_CREATED) async def create_user( user_in: UserCreate, db: Session = Depends(get_auth_db), _=Depends(has_permission("admin:users", "WRITE")), ): with belief_scope("api.admin.create_user"): repo = AuthRepository(db) if repo.get_user_by_username(user_in.username): raise HTTPException(status_code=400, detail="Username already exists") new_user = User( username=user_in.username, email=user_in.email, password_hash=get_password_hash(user_in.password), auth_source="LOCAL", is_active=user_in.is_active, ) for role_name in user_in.roles: role = repo.get_role_by_name(role_name) if role: new_user.roles.append(role) db.add(new_user) db.commit() db.refresh(new_user) return new_user # #endregion create_user # #region update_user [C:3] [TYPE Function] # @BRIEF Updates an existing user. # @PRE: Current user has 'Admin' role. # @POST: User record is updated in the database. # @RELATION CALLS -> AuthRepository @router.put("/users/{user_id}", response_model=UserSchema) async def update_user( user_id: str, user_in: UserUpdate, db: Session = Depends(get_auth_db), _=Depends(has_permission("admin:users", "WRITE")), ): with belief_scope("api.admin.update_user"): repo = AuthRepository(db) user = repo.get_user_by_id(user_id) if not user: raise HTTPException(status_code=404, detail="User not found") if user_in.email is not None: user.email = user_in.email if user_in.is_active is not None: user.is_active = user_in.is_active if user_in.password is not None: user.password_hash = get_password_hash(user_in.password) if user_in.roles is not None: user.roles = [] for role_name in user_in.roles: role = repo.get_role_by_name(role_name) if role: user.roles.append(role) db.commit() db.refresh(user) return user # #endregion update_user # #region delete_user [C:3] [TYPE Function] # @BRIEF Deletes a user. # @PRE: Current user has 'Admin' role. # @POST: User record is removed from the database. # @RELATION CALLS -> AuthRepository @router.delete("/users/{user_id}", status_code=status.HTTP_204_NO_CONTENT) async def delete_user( user_id: str, db: Session = Depends(get_auth_db), _=Depends(has_permission("admin:users", "WRITE")), ): with belief_scope("api.admin.delete_user"): logger.info( f"[DEBUG] Attempting to delete user context={{'user_id': '{user_id}'}}" ) repo = AuthRepository(db) user = repo.get_user_by_id(user_id) if not user: logger.warning( f"[DEBUG] User not found for deletion context={{'user_id': '{user_id}'}}" ) raise HTTPException(status_code=404, detail="User not found") logger.info( f"[DEBUG] Found user to delete context={{'username': '{user.username}'}}" ) db.delete(user) db.commit() logger.info( f"[DEBUG] Successfully deleted user context={{'user_id': '{user_id}'}}" ) return None # #endregion delete_user # #region list_roles [C:3] [TYPE Function] # @BRIEF Lists all available roles. # @RELATION CALLS -> [Role:Class] @router.get("/roles", response_model=list[RoleSchema]) async def list_roles( db: Session = Depends(get_auth_db), _=Depends(has_permission("admin:roles", "READ")) ): with belief_scope("api.admin.list_roles"): return db.query(Role).all() # #endregion list_roles # #region create_role [C:3] [TYPE Function] # @BRIEF Creates a new system role with associated permissions. # @PRE: Role name must be unique. # @POST: New Role record is created in auth.db. # @SIDE_EFFECT: Commits new role and associations to auth.db. # @RELATION CALLS -> [get_permission_by_id:Function] @router.post("/roles", response_model=RoleSchema, status_code=status.HTTP_201_CREATED) async def create_role( role_in: RoleCreate, db: Session = Depends(get_auth_db), _=Depends(has_permission("admin:roles", "WRITE")), ): with belief_scope("api.admin.create_role"): if db.query(Role).filter(Role.name == role_in.name).first(): raise HTTPException(status_code=400, detail="Role already exists") new_role = Role(name=role_in.name, description=role_in.description) repo = AuthRepository(db) for perm_id_or_str in role_in.permissions: perm = repo.get_permission_by_id(perm_id_or_str) if not perm and ":" in perm_id_or_str: res, act = perm_id_or_str.split(":", 1) perm = repo.get_permission_by_resource_action(res, act) if perm: new_role.permissions.append(perm) db.add(new_role) db.commit() db.refresh(new_role) return new_role # #endregion create_role # #region update_role [C:3] [TYPE Function] # @BRIEF Updates an existing role's metadata and permissions. # @PRE: role_id must be a valid existing role UUID. # @POST: Role record is updated in auth.db. # @SIDE_EFFECT: Commits updates to auth.db. # @RELATION CALLS -> [get_role_by_id:Function] @router.put("/roles/{role_id}", response_model=RoleSchema) async def update_role( role_id: str, role_in: RoleUpdate, db: Session = Depends(get_auth_db), _=Depends(has_permission("admin:roles", "WRITE")), ): with belief_scope("api.admin.update_role"): repo = AuthRepository(db) role = repo.get_role_by_id(role_id) if not role: raise HTTPException(status_code=404, detail="Role not found") if role_in.name is not None: role.name = role_in.name if role_in.description is not None: role.description = role_in.description if role_in.permissions is not None: role.permissions = [] for perm_id_or_str in role_in.permissions: perm = repo.get_permission_by_id(perm_id_or_str) if not perm and ":" in perm_id_or_str: res, act = perm_id_or_str.split(":", 1) perm = repo.get_permission_by_resource_action(res, act) if perm: role.permissions.append(perm) db.commit() db.refresh(role) return role # #endregion update_role # #region delete_role [C:3] [TYPE Function] # @BRIEF Removes a role from the system. # @PRE: role_id must be a valid existing role UUID. # @POST: Role record is removed from auth.db. # @SIDE_EFFECT: Deletes record from auth.db and commits. # @RELATION CALLS -> [get_role_by_id:Function] @router.delete("/roles/{role_id}", status_code=status.HTTP_204_NO_CONTENT) async def delete_role( role_id: str, db: Session = Depends(get_auth_db), _=Depends(has_permission("admin:roles", "WRITE")), ): with belief_scope("api.admin.delete_role"): repo = AuthRepository(db) role = repo.get_role_by_id(role_id) if not role: raise HTTPException(status_code=404, detail="Role not found") db.delete(role) db.commit() return None # #endregion delete_role # #region list_permissions [C:3] [TYPE Function] # @BRIEF Lists all available system permissions for assignment. # @POST: Returns a list of all PermissionSchema objects. # @RELATION CALLS -> backend.src.core.auth.repository.AuthRepository.list_permissions @router.get("/permissions", response_model=list[PermissionSchema]) async def list_permissions( db: Session = Depends(get_auth_db), plugin_loader=Depends(get_plugin_loader), _=Depends(has_permission("admin:roles", "READ")), ): with belief_scope("api.admin.list_permissions"): declared_permissions = discover_declared_permissions( plugin_loader=plugin_loader ) inserted_count = sync_permission_catalog( db=db, declared_permissions=declared_permissions ) if inserted_count > 0: logger.reason( f"Synchronized {inserted_count} missing RBAC permissions into auth catalog", extra={"src": "api.admin.list_permissions"}, ) repo = AuthRepository(db) return repo.list_permissions() # #endregion list_permissions # #region list_ad_mappings [C:3] [TYPE Function] # @BRIEF Lists all AD Group to Role mappings. # @RELATION CALLS -> ADGroupMapping @router.get("/ad-mappings", response_model=list[ADGroupMappingSchema]) async def list_ad_mappings( db: Session = Depends(get_auth_db), _=Depends(has_permission("admin:settings", "READ")), ): with belief_scope("api.admin.list_ad_mappings"): return db.query(ADGroupMapping).all() # #endregion list_ad_mappings # #region create_ad_mapping [C:2] [TYPE Function] # @RELATION DEPENDS_ON -> [ADGroupMapping:Class] # @RELATION DEPENDS_ON -> [get_auth_db:Function] # @RELATION DEPENDS_ON -> [has_permission:Function] # @BRIEF Creates a new AD Group mapping. @router.post("/ad-mappings", response_model=ADGroupMappingSchema) async def create_ad_mapping( mapping_in: ADGroupMappingCreate, db: Session = Depends(get_auth_db), _=Depends(has_permission("admin:settings", "WRITE")), ): with belief_scope("api.admin.create_ad_mapping"): new_mapping = ADGroupMapping( ad_group=mapping_in.ad_group, role_id=mapping_in.role_id ) db.add(new_mapping) db.commit() db.refresh(new_mapping) return new_mapping # #endregion create_ad_mapping # #endregion AdminApi