""" FastAPI Authentication Dependencies pentru ROA2WEB Acest modul oferă dependency functions pentru FastAPI care pot fi folosite pentru a proteja endpoint-urile și a obține informații despre utilizatorul curent. Dependencies disponibile: - get_current_user: Obține utilizatorul curent (obligatoriu) - get_optional_user: Obține utilizatorul curent (opțional) - require_company_access: Verifică accesul la o firmă specifică - require_permissions: Verifică permisiunile necesare - get_current_company: Obține firma curentă din context """ import logging from typing import Optional, List, Callable, Any from functools import wraps from fastapi import Depends, HTTPException, status, Request from fastapi.security import HTTPAuthorizationCredentials from .middleware import security_required, security_optional from .jwt_handler import jwt_handler, TokenData from .auth_service import auth_service from .models import CurrentUser, PermissionType, AuthError logger = logging.getLogger(__name__) class AuthenticationRequired(Exception): """Excepție pentru când autentificarea este obligatorie""" pass class InsufficientPermissions(Exception): """Excepție pentru permisiuni insuficiente""" pass class CompanyAccessDenied(Exception): """Excepție pentru acces refuzat la firmă""" pass async def get_current_user_from_token( credentials: HTTPAuthorizationCredentials = Depends(security_required) ) -> CurrentUser: """ Extrage și validează utilizatorul curent din token JWT Args: credentials: Credențialele HTTP de autentificare din header Returns: Utilizatorul curent autentificat Raises: HTTPException: Dacă token-ul este invalid sau utilizatorul nu există """ if not credentials: logger.warning("No credentials provided for protected endpoint") raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Authentication required", headers={"WWW-Authenticate": "Bearer"}, ) # Validează token-ul token_data = jwt_handler.verify_token(credentials.credentials) if not token_data: logger.warning("Invalid token provided") raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid authentication token", headers={"WWW-Authenticate": "Bearer"}, ) if token_data.token_type != "access": logger.warning(f"Invalid token type: {token_data.token_type}") raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token type", headers={"WWW-Authenticate": "Bearer"}, ) # Creează obiectul CurrentUser current_user = CurrentUser( username=token_data.username, user_id=token_data.user_id, companies=token_data.companies, permissions=token_data.permissions ) logger.debug(f"Successfully authenticated user: {current_user.username}") return current_user async def get_current_user_from_request(request: Request) -> CurrentUser: """ Obține utilizatorul curent din request state (setat de middleware) Args: request: Request-ul HTTP curent Returns: Utilizatorul curent autentificat Raises: HTTPException: Dacă utilizatorul nu este autentificat """ if not hasattr(request.state, 'is_authenticated') or not request.state.is_authenticated: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Authentication required", headers={"WWW-Authenticate": "Bearer"}, ) if not hasattr(request.state, 'user') or not request.state.user: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="User not found in request", headers={"WWW-Authenticate": "Bearer"}, ) return request.state.user async def get_optional_user_from_request(request: Request) -> Optional[CurrentUser]: """ Obține utilizatorul curent din request (opțional) Args: request: Request-ul HTTP curent Returns: Utilizatorul curent sau None dacă nu este autentificat """ if (hasattr(request.state, 'is_authenticated') and request.state.is_authenticated and hasattr(request.state, 'user')): return request.state.user return None async def get_optional_user_from_token( credentials: Optional[HTTPAuthorizationCredentials] = Depends(security_optional) ) -> Optional[CurrentUser]: """ Extrage utilizatorul curent din token (opțional) Args: credentials: Credențialele HTTP Bearer (opționale) Returns: Utilizatorul curent sau None """ if not credentials: return None try: return await get_current_user_from_token(credentials) except HTTPException: return None def require_company_access(company_code: str): """ Dependency factory care verifică accesul la o firmă specifică Args: company_code: Codul firmei la care se verifică accesul Returns: Dependency function pentru FastAPI """ async def check_company_access( current_user: CurrentUser = Depends(get_current_user_from_request) ) -> CurrentUser: """ Verifică dacă utilizatorul curent are acces la firma specificată Args: current_user: Utilizatorul curent autentificat Returns: Utilizatorul curent dacă are acces Raises: HTTPException: Dacă nu are acces la firmă """ if company_code not in current_user.companies: logger.warning( f"User {current_user.username} attempted to access " f"unauthorized company {company_code}" ) raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail=f"Access denied to company {company_code}" ) # Verifică și în baza de date pentru siguranță has_access = await auth_service.validate_user_company_access( current_user.username, company_code ) if not has_access: logger.error( f"Database access check failed for user {current_user.username} " f"and company {company_code}" ) raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail=f"Database access denied to company {company_code}" ) logger.debug(f"User {current_user.username} granted access to company {company_code}") return current_user return check_company_access def require_permissions(required_permissions: List[PermissionType]): """ Dependency factory care verifică permisiunile necesare Args: required_permissions: Lista permisiunilor necesare Returns: Dependency function pentru FastAPI """ async def check_permissions( current_user: CurrentUser = Depends(get_current_user_from_request) ) -> CurrentUser: """ Verifică dacă utilizatorul are permisiunile necesare Args: current_user: Utilizatorul curent autentificat Returns: Utilizatorul curent dacă are permisiunile Raises: HTTPException: Dacă nu are permisiunile necesare """ user_permissions = set(current_user.permissions) missing_permissions = [ perm for perm in required_permissions if perm not in user_permissions ] if missing_permissions: logger.warning( f"User {current_user.username} missing permissions: {missing_permissions}" ) raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail=f"Missing required permissions: {missing_permissions}" ) logger.debug(f"User {current_user.username} has required permissions") return current_user return check_permissions def require_company_and_permissions( company_code: str, required_permissions: List[PermissionType] ): """ Dependency factory care verifică atât accesul la firmă cât și permisiunile Args: company_code: Codul firmei required_permissions: Lista permisiunilor necesare Returns: Dependency function pentru FastAPI """ async def check_company_and_permissions( current_user: CurrentUser = Depends(get_current_user_from_request) ) -> CurrentUser: """ Verifică accesul la firmă și permisiunile Args: current_user: Utilizatorul curent Returns: Utilizatorul curent dacă are acces și permisiuni """ # Verifică accesul la firmă company_checker = require_company_access(company_code) await company_checker(current_user) # Verifică permisiunile permissions_checker = require_permissions(required_permissions) await permissions_checker(current_user) return current_user return check_company_and_permissions async def get_current_company_from_header( request: Request, current_user: CurrentUser = Depends(get_current_user_from_request) ) -> str: """ Obține codul firmei curente din header-ul X-Company-Code Args: request: Request-ul HTTP current_user: Utilizatorul curent Returns: Codul firmei curente Raises: HTTPException: Dacă header-ul lipsește sau utilizatorul nu are acces """ company_code = request.headers.get("X-Company-Code") if not company_code: # Folosește prima firmă ca default dacă nu este specificată if current_user.companies: company_code = current_user.companies[0] logger.debug(f"Using default company {company_code} for user {current_user.username}") else: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Company code required (X-Company-Code header or user default)" ) # Verifică accesul if company_code not in current_user.companies: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail=f"Access denied to company {company_code}" ) return company_code # Aliasuri pentru folosire mai ușoară get_current_user = get_current_user_from_request get_optional_user = get_optional_user_from_request # Dependency-uri predefinite pentru permisiuni comune require_read_permission = require_permissions([PermissionType.READ]) require_write_permission = require_permissions([PermissionType.WRITE]) require_admin_permission = require_permissions([PermissionType.ADMIN]) require_reports_permission = require_permissions([PermissionType.REPORTS]) # Decorator pentru validarea companiei în funcții def validate_company_access(company_param: str = "company"): """ Decorator pentru validarea automată a accesului la firmă Args: company_param: Numele parametrului care conține codul firmei Returns: Decorator function """ def decorator(func: Callable) -> Callable: @wraps(func) async def wrapper(*args, **kwargs): # Caută utilizatorul curent în argumentele funcției current_user = None for arg in args: if isinstance(arg, CurrentUser): current_user = arg break if not current_user: # Caută în kwargs current_user = kwargs.get('current_user') if not current_user: raise ValueError("CurrentUser not found in function arguments") # Obține codul firmei company_code = kwargs.get(company_param) if not company_code: raise ValueError(f"Company parameter '{company_param}' not found") # Validează accesul if company_code not in current_user.companies: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail=f"Access denied to company {company_code}" ) return await func(*args, **kwargs) return wrapper return decorator