365 lines
12 KiB
Python
365 lines
12 KiB
Python
# #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
|