import logging from fastapi import APIRouter, Depends, HTTPException, Response, status from sqlalchemy import select from sqlalchemy.orm import Session from starlette.requests import Request from app.core.rbac import require_permission from app.db.database import get_db from app.models.user import User from app.repositories.rbac_repository import RbacRepository from app.schemas.rbac import RoleCreate, RolePermissionUpdate, RoleResponse, RoleUpdate from app.services.audit_service import sanitize, write_audit_log logger = logging.getLogger(__name__) router = APIRouter( prefix="/roles", tags=["Roles"], ) def get_role_or_404(db: Session, role_id: int): role = RbacRepository.get_role_by_id(db, role_id) if role is None: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Rolle nicht gefunden", ) return role def ensure_unique_role_name(db: Session, name: str, role_id: int | None = None) -> None: role = RbacRepository.get_role_by_name(db, name) if role is not None and role.id != role_id: raise HTTPException( status_code=status.HTTP_409_CONFLICT, detail="Rollenname ist bereits vergeben", ) def ensure_permission_ids_exist(db: Session, permission_ids: list[int]) -> None: permissions = RbacRepository.get_permissions_by_ids(db, permission_ids) if len(permissions) != len(set(permission_ids)): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Mindestens eine Berechtigung existiert nicht", ) @router.get("", response_model=list[RoleResponse]) @router.get("/", response_model=list[RoleResponse], include_in_schema=False) def get_roles( db: Session = Depends(get_db), current_user: User = Depends(require_permission("roles.read")), ): logger.info("roles.list", extra={"actor_user_id": current_user.id}) return RbacRepository.get_roles(db) @router.get("/{role_id}", response_model=RoleResponse) def get_role( role_id: int, db: Session = Depends(get_db), current_user: User = Depends(require_permission("roles.read")), ): logger.info( "roles.detail", extra={"actor_user_id": current_user.id, "target_role_id": role_id}, ) return get_role_or_404(db, role_id) @router.post("", response_model=RoleResponse, status_code=status.HTTP_201_CREATED) @router.post( "/", response_model=RoleResponse, status_code=status.HTTP_201_CREATED, include_in_schema=False, ) def create_role( role: RoleCreate, request: Request, db: Session = Depends(get_db), current_user: User = Depends(require_permission("roles.create")), ): ensure_unique_role_name(db, role.name) ensure_permission_ids_exist(db, role.permission_ids) logger.info("roles.create", extra={"actor_user_id": current_user.id}) created_role = RbacRepository.create_role(db, role) write_audit_log( db, action="roles.create", entity_type="roles", entity_id=created_role.id, entity_label=created_role.name, actor=current_user, request=request, after_data=created_role, ) return created_role @router.put("/{role_id}", response_model=RoleResponse) def update_role( role_id: int, role: RoleUpdate, request: Request, db: Session = Depends(get_db), current_user: User = Depends(require_permission("roles.update")), ): db_role = get_role_or_404(db, role_id) before_data = sanitize(db_role) ensure_unique_role_name(db, role.name, role_id) ensure_permission_ids_exist(db, role.permission_ids) logger.info( "roles.update", extra={"actor_user_id": current_user.id, "target_role_id": role_id}, ) updated_role = RbacRepository.update_role(db, db_role, role) write_audit_log( db, action="roles.update", entity_type="roles", entity_id=updated_role.id, entity_label=updated_role.name, actor=current_user, request=request, before_data=before_data, after_data=updated_role, ) return updated_role @router.delete("/{role_id}", status_code=status.HTTP_204_NO_CONTENT) def delete_role( role_id: int, request: Request, db: Session = Depends(get_db), current_user: User = Depends(require_permission("roles.delete")), ): db_role = get_role_or_404(db, role_id) before_data = sanitize(db_role) entity_label = db_role.name if db_role.is_system: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Systemrollen dürfen nicht gelöscht werden", ) assigned_user_id = db.scalar(select(User.id).where(User.role_id == role_id).limit(1)) if assigned_user_id is not None: raise HTTPException( status_code=status.HTTP_409_CONFLICT, detail="Rolle ist Benutzern zugewiesen und kann nicht gelöscht werden", ) logger.info( "roles.delete", extra={"actor_user_id": current_user.id, "target_role_id": role_id}, ) RbacRepository.delete_role(db, db_role) write_audit_log( db, action="roles.delete", entity_type="roles", entity_id=role_id, entity_label=entity_label, actor=current_user, request=request, before_data=before_data, ) return Response(status_code=status.HTTP_204_NO_CONTENT) @router.put("/{role_id}/permissions", response_model=RoleResponse) def update_role_permissions( role_id: int, permission_update: RolePermissionUpdate, request: Request, db: Session = Depends(get_db), current_user: User = Depends(require_permission("roles.assign")), ): db_role = get_role_or_404(db, role_id) before_data = sanitize(db_role) ensure_permission_ids_exist(db, permission_update.permission_ids) logger.info( "roles.permissions_update", extra={"actor_user_id": current_user.id, "target_role_id": role_id}, ) updated_role = RbacRepository.update_role_permissions(db, db_role, permission_update) write_audit_log( db, action="roles.permissions_update", entity_type="roles", entity_id=updated_role.id, entity_label=updated_role.name, actor=current_user, request=request, before_data=before_data, after_data=updated_role, ) return updated_role