Olympus/backend/hermes/app/api/roles.py
2026-07-03 00:04:44 +02:00

208 lines
6.3 KiB
Python

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