Files
ss-tools/backend/src/api/routes/admin.py
2026-05-20 23:54:53 +03:00

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