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