Files
roa2web-service-auto/shared/auth/dependencies.py
Marius Mutu 495790411f feat(ocr): Add docTR OCR engine with metrics infrastructure
Add docTR as primary OCR engine with 2-tier sequential processing,
OCR metrics tracking, and simplified engine selection.

Features:
- docTR OCR engine with light+medium preprocessing tiers
- doctr_plus mode with early exit optimization (~65% fast path)
- OCR metrics dashboard with per-engine statistics
- User OCR preference persistence
- Parallel worker pool for OCR processing
- Cross-validation for extraction quality

Engine options: tesseract, doctr, doctr_plus (recommended), paddleocr

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-02 05:37:16 +02:00

402 lines
13 KiB
Python

"""
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