Files
MABEA/backend/app/api/deps.py
T
patrickandClaude Sonnet 5 c56fc285de
CI / backend-tests (push) Failing after 1m55s
CI / frontend-build (push) Successful in 19s
feat(rollen): granulares Rechte-System (Roadmap Phase 6, letztes Modul)
Additiv neben dem bestehenden 4-Rollen-System (mitarbeiter/material-
verantwortlicher/leitungsverantwortlicher/administration bleiben unverändert,
kein Breaking Change): Admin kann jetzt eigene Rollen (z.B. "Materialwart",
"Helfer" aus der Ursprungs-Anforderung) mit frei wählbaren Einzelrechten aus
einem Berechtigungs-Katalog anlegen und Benutzern zuweisen - auch Benutzern
ganz ohne feste RolleTyp-Zuordnung.

Neue Tabellen: berechtigung (Katalog), rolle (custom, admin-anlegbar),
rolle_berechtigung (M:N), benutzer_rolle_zuordnung (M:N, eigene Tabelle statt
Wiederverwendung des ENUM-basierten benutzer_rolle).

require_roles_or_permission() kombiniert beide Systeme: bestehende feste
Rollen ODER eine passende granulare Berechtigung. Auf die vom Nutzer genannten
Beispiel-Endpunkte angewendet: Material anlegen/bearbeiten, Lagerbewegung
durchführen, Mangel melden/lesen/bearbeiten, Prüfung durchführen - weitere
Endpunkte folgen bei Bedarf nach demselben Muster (require_permission()/
require_roles_or_permission() stehen jetzt als Bausteine bereit).

Neue Endpunkte: GET /berechtigungen, CRUD /rollen, PUT/DELETE
/rollen/{id}/berechtigungen/{id}, PUT/DELETE /benutzer/{id}/rollen/{id}.

Frontend: neuer Admin-Tab "Rollen & Rechte" (Rolle anlegen, Rechte togglen,
Benutzer zuweisen/entfernen).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KC8HYvv6UkCVYheYiTw9DD
2026-09-05 14:07:03 +02:00

106 lines
4.1 KiB
Python

from collections.abc import Callable
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.security import InvalidTokenError, decode_access_token
from app.db.session import get_db
from app.models.auth import Benutzer, RolleTyp
from app.models.permission import Berechtigung, BenutzerRolleZuordnung, Rolle, RolleBerechtigung
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/login")
async def get_current_user(
token: str = Depends(oauth2_scheme),
db: AsyncSession = Depends(get_db),
) -> Benutzer:
credentials_error = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Ungültiger oder abgelaufener Token",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = decode_access_token(token)
except InvalidTokenError as exc:
raise credentials_error from exc
login = payload.get("sub")
if login is None:
raise credentials_error
result = await db.execute(select(Benutzer).where(Benutzer.login == login))
benutzer = result.scalar_one_or_none()
if benutzer is None or not benutzer.aktiv:
raise credentials_error
return benutzer
def require_roles(*erlaubte_rollen: RolleTyp) -> Callable:
"""Prompt 05 Berechtigungsmatrix: zentrale Rollenprüfung als FastAPI-Dependency."""
async def checker(current_user: Benutzer = Depends(get_current_user)) -> Benutzer:
besitzt = {RolleTyp(r) for r in current_user.rollen_namen}
if not besitzt.intersection(erlaubte_rollen):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Für diese Aktion fehlt die erforderliche Rolle",
)
return current_user
return checker
async def _hat_berechtigung(db: AsyncSession, benutzer_id: int, schluessel: tuple[str, ...]) -> bool:
result = await db.execute(
select(Berechtigung.schluessel)
.join(RolleBerechtigung, RolleBerechtigung.berechtigung_id == Berechtigung.id)
.join(Rolle, Rolle.id == RolleBerechtigung.rolle_id)
.join(BenutzerRolleZuordnung, BenutzerRolleZuordnung.rolle_id == Rolle.id)
.where(BenutzerRolleZuordnung.benutzer_id == benutzer_id, Berechtigung.schluessel.in_(schluessel))
)
return result.first() is not None
def require_permission(*erlaubte_berechtigungen: str) -> Callable:
"""Roadmap Phase 6 (granulares Rechte-System): prüft, ob eine der frei
konfigurierbaren Rollen des Benutzers (app.models.permission.Rolle) eine
der übergebenen Berechtigungen trägt - additiv zum festen 4-Rollen-System,
das über require_roles() unverändert weiterläuft."""
async def checker(
current_user: Benutzer = Depends(get_current_user), db: AsyncSession = Depends(get_db)
) -> Benutzer:
if not await _hat_berechtigung(db, current_user.id, erlaubte_berechtigungen):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Für diese Aktion fehlt die erforderliche Berechtigung",
)
return current_user
return checker
def require_roles_or_permission(*erlaubte_rollen: RolleTyp, berechtigung: str) -> Callable:
"""Kombinierte Prüfung für Endpunkte, die sowohl über eine der festen
Rollen (bestehende Admins/Materialverantwortliche/... - Rückwärts-
kompatibilität) als auch über eine neu zugewiesene granulare Berechtigung
(z.B. eine custom "Materialwart"-Rolle) erreichbar sein sollen."""
async def checker(
current_user: Benutzer = Depends(get_current_user), db: AsyncSession = Depends(get_db)
) -> Benutzer:
besitzt = {RolleTyp(r) for r in current_user.rollen_namen}
if besitzt.intersection(erlaubte_rollen):
return current_user
if await _hat_berechtigung(db, current_user.id, (berechtigung,)):
return current_user
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Für diese Aktion fehlt die erforderliche Rolle oder Berechtigung",
)
return checker