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>
402 lines
13 KiB
Python
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 |