"""FastAPI-Dependencies fuer Authentifizierung und Autorisierung.""" from __future__ import annotations from dataclasses import dataclass from fastapi import Cookie, Depends, HTTPException, Request, Response, WebSocket, status from app.db import get_db from app.rbac import user_has_role, user_has_role_for_host from app.security.sessions import ( SESSION_COOKIE_NAME, decode_session_token, is_expired, refresh_session_token, ) @dataclass class CurrentUser: id: int username: str is_admin: bool async def get_current_user( response: Response, jh_session: str | None = Cookie(default=None, alias=SESSION_COOKIE_NAME), ) -> CurrentUser: if jh_session is None: raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Nicht angemeldet") payload = decode_session_token(jh_session) if payload is None or is_expired(payload): response.delete_cookie(SESSION_COOKIE_NAME) raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Session abgelaufen oder ungueltig") conn = get_db() cursor = await conn.execute( "SELECT id, username, is_admin, is_active, session_version FROM users WHERE id = ?", (payload.user_id,), ) row = await cursor.fetchone() if row is None or not row[3] or row[4] != payload.session_version: response.delete_cookie(SESSION_COOKIE_NAME) raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Session ungueltig") # Gleitenden Idle-Timeout verlaengern (gleiche session_version/login_ts). new_token = refresh_session_token(payload) response.set_cookie( SESSION_COOKIE_NAME, new_token, httponly=True, secure=True, samesite="strict", max_age=None, # Session-Cookie; Ablauf wird serverseitig durchgesetzt path="/", ) return CurrentUser(id=row[0], username=row[1], is_admin=bool(row[2])) async def get_current_user_ws(websocket: WebSocket) -> CurrentUser | None: """Wie get_current_user(), aber fuer WebSocket-Handshakes: kein Cookie-Refresh (WebSockets erlauben nach dem Handshake kein Set-Cookie mehr), stattdessen wird der Idle-Timeout beim naechsten regulaeren HTTP-Request durchgesetzt.""" token = websocket.cookies.get(SESSION_COOKIE_NAME) if token is None: return None payload = decode_session_token(token) if payload is None or is_expired(payload): return None conn = get_db() cursor = await conn.execute( "SELECT id, username, is_admin, is_active, session_version FROM users WHERE id = ?", (payload.user_id,), ) row = await cursor.fetchone() if row is None or not row[3] or row[4] != payload.session_version: return None return CurrentUser(id=row[0], username=row[1], is_admin=bool(row[2])) async def require_global_admin(user: CurrentUser = Depends(get_current_user)) -> CurrentUser: if not user.is_admin: raise HTTPException(status.HTTP_403_FORBIDDEN, "Admin-Rechte erforderlich") return user def require_host_group_role(role_name: str): """Dependency-Factory: prueft Rolle des Users fuer eine per Pfad-/Query-Param uebergebene host_group_id. Globale Admins duerfen immer.""" async def _dep(host_group_id: int, user: CurrentUser = Depends(get_current_user)) -> CurrentUser: if user.is_admin: return user conn = get_db() allowed = await user_has_role( conn, user_id=user.id, host_group_id=host_group_id, role_name=role_name ) if not allowed: raise HTTPException(status.HTTP_403_FORBIDDEN, "Keine Berechtigung fuer diese Hostgruppe") return user return _dep def require_host_role(role_name: str): """Dependency-Factory: prueft Rolle des Users fuer einen konkreten host_id.""" async def _dep(host_id: int, user: CurrentUser = Depends(get_current_user)) -> CurrentUser: if user.is_admin: return user conn = get_db() allowed = await user_has_role_for_host( conn, user_id=user.id, host_id=host_id, role_name=role_name ) if not allowed: raise HTTPException(status.HTTP_403_FORBIDDEN, "Keine Berechtigung fuer diesen Host") return user return _dep