fix telegram

This commit is contained in:
Claude Agent
2026-02-23 15:12:33 +00:00
parent 6c78fec8a7
commit 8bc567a9c5
426 changed files with 112478 additions and 1 deletions

View File

@@ -0,0 +1,649 @@
# 🔐 ROA2WEB Shared Authentication System
Sistem de autentificare JWT partajat între toate aplicațiile din ecosistemul ROA2WEB, integrat cu Oracle Database și optimizat pentru aplicații FastAPI.
## 📋 Table of Contents
- [Features](#features)
- [Architecture](#architecture)
- [Quick Start](#quick-start)
- [Components](#components)
- [Integration Guide](#integration-guide)
- [Security Features](#security-features)
- [API Reference](#api-reference)
- [Testing](#testing)
- [Deployment](#deployment)
- [Troubleshooting](#troubleshooting)
## ✨ Features
### Core Features
- **JWT Authentication**: Secure token-based authentication cu access și refresh tokens
- **Oracle Database Integration**: Folosește `pack_drepturi.verificautilizator` pentru autentificare
- **Multi-Company Support**: Acces controlat la multiple firme/schemas Oracle
- **Permission System**: Sistem granular de permisiuni (read, write, admin, reports)
- **FastAPI Integration**: Dependencies și middleware native pentru FastAPI
- **Rate Limiting**: Protecție împotriva brute force attacks
- **Caching**: Cache inteligent pentru performanță optimă
### Security Features
- **Token Expiration**: Configurabil pentru access și refresh tokens
- **SQL Injection Protection**: Parametri legați în toate query-urile
- **Rate Limiting**: Configurabil per IP și endpoint
- **CORS Protection**: Configurare flexibilă pentru origins
- **Header Security**: Security headers automate
- **Token Blacklisting**: Suport pentru invalidarea token-urilor (în dezvoltare)
## 🏗️ Architecture
```
ROA2WEB Authentication Flow:
┌─────────────┐ ┌──────────────┐ ┌─────────────┐ ┌──────────────┐
│ Client │───▶│ FastAPI │───▶│ JWT │───▶│ Oracle │
│ (Frontend) │ │ Application │ │ Handler │ │ Database │
└─────────────┘ └──────────────┘ └─────────────┘ └──────────────┘
│ │ │
▼ ▼ ▼
┌──────────────┐ ┌─────────────┐ ┌──────────────┐
│ Auth Service │ │ Middleware │ │ User Cache │
└──────────────┘ └─────────────┘ └──────────────┘
```
### Components Overview
```
shared/auth/
├── jwt_handler.py # JWT token creation și validation
├── auth_service.py # Oracle database integration
├── models.py # Pydantic models pentru data validation
├── middleware.py # FastAPI middleware pentru auto-authentication
├── dependencies.py # FastAPI dependencies pentru protected routes
├── routes.py # Pre-built authentication routes
├── test_auth.py # Comprehensive test suite
├── demo_app.py # Demo application cu examples
└── README.md # Această documentație
```
## 🚀 Quick Start
### 1. Environment Setup
```bash
# Copy și configurează environment variables
cp .env.example .env
# Edit .env cu configurările tale
JWT_SECRET_KEY=your-super-secret-jwt-key-change-in-production
ORACLE_USER=your_oracle_username
ORACLE_PASSWORD=your_oracle_password
ORACLE_DSN=your_oracle_connection_string
```
### 2. Basic Integration
```python
from fastapi import FastAPI, Depends
from roa2web.shared.auth import (
AuthenticationMiddleware, create_auth_router,
get_current_user, CurrentUser
)
from roa2web.shared.database import oracle_pool
app = FastAPI(title="My ROA2WEB App")
# Add authentication middleware
app.add_middleware(
AuthenticationMiddleware,
excluded_paths=["/", "/docs", "/health", "/auth/login"]
)
# Include authentication routes
auth_router = create_auth_router()
app.include_router(auth_router)
@app.on_event("startup")
async def startup():
await oracle_pool.initialize()
@app.get("/protected")
async def protected_endpoint(
current_user: CurrentUser = Depends(get_current_user)
):
return {"message": f"Hello {current_user.username}!"}
```
### 3. Test Authentication
```bash
# Start demo application
cd roa2web/shared/auth
python demo_app.py
# Open browser
open http://localhost:8000/docs
# Login prin Swagger UI cu credențialele Oracle
```
## 🧩 Components
### JWT Handler (`jwt_handler.py`)
Gestionează crearea, validarea și refresh-ul token-urilor JWT.
```python
from roa2web.shared.auth import jwt_handler
# Create access token
token = jwt_handler.create_access_token(
username="admin",
companies=["COMP1", "COMP2"],
permissions=["read", "write", "reports"]
)
# Verify token
token_data = jwt_handler.verify_token(token)
if token_data:
print(f"Valid token for user: {token_data.username}")
```
### Auth Service (`auth_service.py`)
Integrează cu Oracle Database pentru autentificare și management utilizatori.
```python
from roa2web.shared.auth import auth_service
# Authenticate user
success, token_response, error = await auth_service.authenticate_and_create_tokens(
"username", "password"
)
if success:
print(f"Access token: {token_response.access_token}")
else:
print(f"Authentication failed: {error}")
```
### FastAPI Dependencies (`dependencies.py`)
Oferă dependencies pentru protejarea endpoint-urilor.
```python
from fastapi import Depends
from roa2web.shared.auth import (
get_current_user, require_company_access,
require_permissions, PermissionType
)
@app.get("/admin-only")
async def admin_endpoint(
user: CurrentUser = Depends(require_permissions([PermissionType.ADMIN]))
):
return {"message": "Admin access granted"}
@app.get("/company/{company_code}/data")
async def company_data(
company_code: str,
user: CurrentUser = Depends(require_company_access(company_code))
):
return {"company": company_code, "data": "..."}
```
### Authentication Routes (`routes.py`)
Pre-built routes pentru operații de autentificare.
```python
from roa2web.shared.auth import create_auth_router
# Basic auth router
auth_router = create_auth_router()
app.include_router(auth_router)
# Auth router cu admin routes
auth_router_admin = create_auth_router(include_admin_routes=True)
app.include_router(auth_router_admin)
```
Available routes:
- `POST /auth/login` - User authentication
- `POST /auth/refresh` - Token refresh
- `POST /auth/logout` - User logout
- `GET /auth/me` - Current user info
- `GET /auth/companies` - User companies
- `GET /auth/status` - Authentication status
## 🔧 Integration Guide
### Full FastAPI Application
```python
from fastapi import FastAPI, Depends, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from contextlib import asynccontextmanager
from roa2web.shared.auth import (
AuthenticationMiddleware, create_auth_router,
get_current_user, require_company_access,
CurrentUser, PermissionType
)
from roa2web.shared.database import oracle_pool
@asynccontextmanager
async def lifespan(app: FastAPI):
# Startup
await oracle_pool.initialize()
yield
# Shutdown
await oracle_pool.close_pool()
app = FastAPI(
title="ROA2WEB Application",
lifespan=lifespan
)
# CORS
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:3000"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Authentication
app.add_middleware(
AuthenticationMiddleware,
excluded_paths=["/", "/docs", "/health"],
rate_limit_paths=["/auth/login"]
)
# Routes
auth_router = create_auth_router()
app.include_router(auth_router)
# Protected endpoints
@app.get("/")
async def public_endpoint():
return {"message": "Public endpoint"}
@app.get("/me")
async def my_info(current_user: CurrentUser = Depends(get_current_user)):
return current_user
@app.get("/company/{company_code}/invoices")
async def get_invoices(
company_code: str,
current_user: CurrentUser = Depends(require_company_access(company_code))
):
# Business logic here
return {"company": company_code, "invoices": []}
```
### Custom Permissions
```python
from roa2web.shared.auth import require_permissions, PermissionType
# Define custom permissions
class CustomPermissionType(str, Enum):
INVOICE_READ = "invoice_read"
INVOICE_WRITE = "invoice_write"
REPORT_EXPORT = "report_export"
# Use in endpoints
@app.get("/invoices")
async def get_invoices(
user: CurrentUser = Depends(require_permissions([CustomPermissionType.INVOICE_READ]))
):
return {"invoices": []}
```
### Company-Specific Endpoints
```python
from fastapi import Header
from roa2web.shared.auth import get_current_company_from_header
@app.get("/current-company-data")
async def get_current_company_data(
company_code: str = Depends(get_current_company_from_header),
current_user: CurrentUser = Depends(get_current_user)
):
# company_code is automatically extracted from X-Company-Code header
# and validated against user's accessible companies
return {"company": company_code, "data": "..."}
```
## 🔒 Security Features
### JWT Configuration
```python
# Environment variables
JWT_SECRET_KEY=your-super-secret-jwt-key-change-in-production
JWT_ALGORITHM=HS256
ACCESS_TOKEN_EXPIRE_MINUTES=30
REFRESH_TOKEN_EXPIRE_DAYS=7
```
### Rate Limiting
```python
from roa2web.shared.auth import RateLimiter, AuthenticationMiddleware
# Custom rate limiter
custom_rate_limiter = RateLimiter(
max_requests=10, # 10 requests
time_window=60 # per minute
)
app.add_middleware(
AuthenticationMiddleware,
rate_limit_paths=["/auth/login", "/auth/register"],
rate_limiter=custom_rate_limiter
)
```
### Security Headers
Middleware-ul adaugă automat header-e de securitate:
```
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 1; mode=block
```
## 📚 API Reference
### JWT Handler Methods
```python
class JWTHandler:
def create_access_token(username, companies, user_id=None, permissions=None) -> str
def create_refresh_token(username, user_id=None) -> str
def verify_token(token) -> Optional[TokenData]
def refresh_access_token(refresh_token, companies, permissions=None) -> Optional[str]
def create_token_response(username, companies, ...) -> TokenResponse
```
### Auth Service Methods
```python
class UserAuthService:
async def verify_user_credentials(username, password) -> bool
async def get_user_companies(username) -> List[str]
async def get_user_permissions(username, company) -> List[str]
async def authenticate_and_create_tokens(username, password) -> Tuple[bool, TokenResponse, str]
async def validate_user_company_access(username, company) -> bool
```
### FastAPI Dependencies
```python
# User dependencies
get_current_user() -> CurrentUser
get_optional_user() -> Optional[CurrentUser]
# Permission dependencies
require_permissions(permissions: List[PermissionType])
require_company_access(company_code: str)
require_company_and_permissions(company_code: str, permissions: List[PermissionType])
# Utility dependencies
get_current_company_from_header() -> str
```
## 🧪 Testing
### Running Tests
```bash
# Install test dependencies
pip install pytest pytest-asyncio httpx
# Run all tests
cd roa2web/shared/auth
python -m pytest test_auth.py -v
# Run specific test categories
python -m pytest test_auth.py::TestJWTHandler -v
python -m pytest test_auth.py::TestUserAuthService -v
python -m pytest test_auth.py::TestSecurityFeatures -v
# Run with coverage
python -m pytest test_auth.py --cov=. --cov-report=html
```
### Test Categories
- **Unit Tests**: JWT operations, auth service methods
- **Integration Tests**: Database integration, full auth flow
- **Security Tests**: Token tampering, SQL injection, rate limiting
- **Performance Tests**: Token creation/verification speed
### Demo Application
```bash
# Start demo app for manual testing
cd roa2web/shared/auth
python demo_app.py
# Available demo endpoints:
# http://localhost:8000/ - Home page cu documentație
# http://localhost:8000/docs - Swagger UI pentru testare
# http://localhost:8000/demo/* - Various demo endpoints
```
## 🚀 Deployment
### Production Configuration
```bash
# Strong JWT secret key
JWT_SECRET_KEY=$(python -c "import secrets; print(secrets.token_urlsafe(32))")
# Shorter token expiration
ACCESS_TOKEN_EXPIRE_MINUTES=15
REFRESH_TOKEN_EXPIRE_DAYS=1
# Strict rate limiting
RATE_LIMIT_MAX_REQUESTS=3
RATE_LIMIT_TIME_WINDOW=300
# Secure headers
SECURE_SSL_REDIRECT=true
SESSION_COOKIE_SECURE=true
```
### Docker Integration
```dockerfile
# În Dockerfile-ul aplicației
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
# Environment pentru container
ENV JWT_SECRET_KEY=${JWT_SECRET_KEY}
ENV ORACLE_USER=${ORACLE_USER}
ENV ORACLE_PASSWORD=${ORACLE_PASSWORD}
ENV ORACLE_DSN=${ORACLE_DSN}
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
```
### Health Checks
```python
@app.get("/health")
async def health_check():
# Test database connection
try:
async with oracle_pool.get_connection() as conn:
with conn.cursor() as cursor:
cursor.execute("SELECT 1 FROM DUAL")
db_status = "healthy"
except Exception as e:
db_status = f"error: {str(e)}"
return {
"status": "healthy" if db_status == "healthy" else "degraded",
"database": db_status,
"jwt": "functional",
"timestamp": datetime.now().isoformat()
}
```
## 🔧 Troubleshooting
### Common Issues
#### 1. "Invalid token" errors
```python
# Check JWT secret key consistency
print(f"JWT Secret: {os.getenv('JWT_SECRET_KEY')}")
# Verify token creation and validation
token = jwt_handler.create_access_token("test", ["COMP1"])
token_data = jwt_handler.verify_token(token)
print(f"Token valid: {token_data is not None}")
```
#### 2. Database connection errors
```python
# Test Oracle connection
try:
async with oracle_pool.get_connection() as conn:
with conn.cursor() as cursor:
cursor.execute("SELECT 1 FROM DUAL")
result = cursor.fetchone()
print("Database connection: OK")
except Exception as e:
print(f"Database error: {e}")
```
#### 3. Rate limiting issues
```python
# Check rate limiter stats
from roa2web.shared.auth import default_rate_limiter
client_ip = "192.168.1.1"
allowed = default_rate_limiter.is_allowed(client_ip)
reset_time = default_rate_limiter.get_reset_time(client_ip)
print(f"IP {client_ip} allowed: {allowed}, resets at: {reset_time}")
```
#### 4. Permission denied errors
```python
# Check user companies and permissions
companies = await auth_service.get_user_companies("username")
permissions = await auth_service.get_user_permissions("username", "COMP1")
print(f"User companies: {companies}")
print(f"User permissions: {permissions}")
```
### Debug Mode
```python
import logging
# Enable debug logging
logging.basicConfig(level=logging.DEBUG)
# Specific loggers
logging.getLogger("roa2web.shared.auth").setLevel(logging.DEBUG)
```
### Environment Validation
```python
from roa2web.shared.utils.config import shared_config
# Validate configuration
print(f"Oracle User: {shared_config.oracle_user}")
print(f"JWT Secret set: {'***' if shared_config.jwt_secret_key else 'NOT SET'}")
print(f"Token expiry: {shared_config.access_token_expire_minutes} minutes")
```
## 📈 Performance Optimization
### Caching
```python
# Cache configuration
AUTH_CACHE_TTL_MINUTES=15 # User data cache TTL
# Monitor cache performance
stats = auth_service.get_cache_stats()
print(f"Cache hit ratio: {stats['cache_hit_ratio']:.2%}")
```
### Connection Pooling
```python
# Oracle pool configuration
DB_MIN_CONNECTIONS=2
DB_MAX_CONNECTIONS=10
DB_CONNECTION_INCREMENT=1
```
### Token Optimization
```python
# Optimize token size by limiting payload
token = jwt_handler.create_access_token(
username="user",
companies=["COMP1"], # Limit companies in token
permissions=["read"] # Essential permissions only
)
```
## 🤝 Contributing
Pentru contribuții la sistemul de autentificare:
1. **Fork repository-ul** și creează o ramură pentru feature
2. **Implementează schimbările** cu tests comprehensive
3. **Rulează toate testele** pentru a verifica compatibilitatea
4. **Actualizează documentația** dacă este necesar
5. **Creează Pull Request** cu descriere detaliată
### Development Setup
```bash
# Clone repository
git clone [repository-url]
cd roa-flask
# Setup environment
python -m venv venv
source venv/bin/activate # Linux/Mac
# or
venv\Scripts\activate # Windows
pip install -r requirements.txt
# Run tests
cd roa2web/shared/auth
python -m pytest test_auth.py -v
```
## 📜 License
Acest sistem de autentificare face parte din proiectul ROA2WEB și este disponibil sub aceleași condiții de licențiere ca și proiectul principal.
---
**ROA2WEB Authentication System v1.0.0**
*Secure, scalable, Oracle-integrated authentication pentru aplicații moderne*

View File

@@ -0,0 +1,23 @@
"""
ROA2WEB Shared Authentication Module
This module provides JWT-based authentication functionality that can be shared
across all ROA2WEB microservices.
Components:
- jwt_handler: JWT token creation, validation, and refresh
- auth_service: Oracle database authentication integration
- middleware: FastAPI middleware for token validation
- dependencies: FastAPI dependencies for protected routes
- models: Pydantic models for authentication data
- routes: Template authentication routes for FastAPI apps
"""
from .jwt_handler import jwt_handler, JWTHandler, TokenData, TokenResponse
__all__ = [
'jwt_handler',
'JWTHandler',
'TokenData',
'TokenResponse'
]

View File

@@ -0,0 +1,476 @@
"""
Authentication Service - Oracle Database Integration pentru ROA2WEB
Acest modul integrează sistemul de autentificare JWT cu baza de date Oracle,
reutilizând funcționalitatea existentă din aplicația Flask originală.
Funcționalități:
- Verificare utilizatori prin pack_drepturi.verificautilizator
- Obținere lista firmelor din vdef_util_grup
- Gestionarea sesiunilor și permisiunilor utilizatorilor
- Caching pentru performanță optimă
"""
import logging
import hashlib
from typing import Optional, List, Dict, Any, Tuple
from datetime import datetime, timedelta
import asyncio
from contextlib import asynccontextmanager
# Import shared database pool
# IMPORTANT: Use shared.database.oracle_pool to ensure singleton consistency
# DO NOT use relative imports like 'database.oracle_pool'
from shared.database.oracle_pool import oracle_pool
from .jwt_handler import jwt_handler
from .models import TokenResponse, CurrentUser
logger = logging.getLogger(__name__)
class AuthenticationError(Exception):
"""Excepție pentru erorile de autentificare"""
pass
class UserAuthService:
"""
Serviciu pentru autentificarea utilizatorilor folosind Oracle Database
Acest serviciu integrează:
- Verificarea credențialelor prin pack_drepturi.verificautilizator
- Obținerea listei de firme prin vdef_util_grup
- Generarea token-urilor JWT
- Cache pentru performanță
"""
def __init__(self):
"""Inițializează serviciul de autentificare"""
self._user_cache: Dict[str, Dict[str, Any]] = {}
self._cache_ttl = timedelta(minutes=15) # Cache 15 minute
def _get_cache_key(self, username: str) -> str:
"""Generează cheia de cache pentru utilizator"""
return f"auth_user_{username.lower()}"
def _is_cache_valid(self, cache_entry: Dict[str, Any]) -> bool:
"""Verifică dacă entry-ul din cache este încă valid"""
if not cache_entry or 'timestamp' not in cache_entry:
return False
cache_time = cache_entry['timestamp']
return datetime.now() - cache_time < self._cache_ttl
def _get_cached_user_data(self, username: str) -> Optional[Dict[str, Any]]:
"""Obține datele utilizatorului din cache dacă sunt valide"""
cache_key = self._get_cache_key(username)
cache_entry = self._user_cache.get(cache_key)
if self._is_cache_valid(cache_entry):
logger.debug(f"Cache hit for user {username}")
return cache_entry['data']
return None
def _cache_user_data(self, username: str, data: Dict[str, Any]) -> None:
"""Salvează datele utilizatorului în cache"""
cache_key = self._get_cache_key(username)
self._user_cache[cache_key] = {
'data': data,
'timestamp': datetime.now()
}
logger.debug(f"Cached data for user {username}")
async def get_username_by_email(
self,
email: str,
server_id: Optional[str] = None
) -> Optional[str]:
"""
Obține username-ul Oracle corespunzător unui email.
Necesar pentru login cu email - convertește email-ul în username-ul
real din tabelul UTILIZATORI pentru autentificare cu pack_drepturi.
Args:
email: Email-ul utilizatorului
server_id: ID-ul serverului Oracle (pentru multi-server mode)
Returns:
Username-ul Oracle sau None dacă email-ul nu există
"""
try:
async with oracle_pool.get_connection(server_id) as connection:
with connection.cursor() as cursor:
cursor.execute("""
SELECT UTILIZATOR
FROM CONTAFIN_ORACLE.UTILIZATORI
WHERE LOWER(EMAIL) = :email
AND INACTIV = 0
AND STERS = 0
""", {'email': email.lower().strip()})
row = cursor.fetchone()
if row:
username = row[0]
logger.info(f"Resolved email '{email}' to username '{username}' on server '{server_id}'")
return username
else:
logger.warning(f"No username found for email '{email}' on server '{server_id}'")
return None
except Exception as e:
logger.error(f"Database error resolving email '{email}' to username: {str(e)}")
return None
async def verify_user_credentials(
self,
username: str,
password: str,
server_id: Optional[str] = None
) -> bool:
"""
Verifică credențialele utilizatorului folosind pack_drepturi.verificautilizator
Args:
username: Numele utilizatorului
password: Parola utilizatorului
server_id: ID-ul serverului Oracle (opțional, pentru multi-server mode)
Returns:
True dacă credențialele sunt corecte, False altfel
Raises:
AuthenticationError: Dacă apar erori în procesul de verificare
"""
try:
async with oracle_pool.get_connection(server_id) as connection:
with connection.cursor() as cursor:
# Apelarea procedurii pack_drepturi.verificautilizator
# Această procedură returnează ID-ul utilizatorului cu checksum pentru succes, -1 pentru eșec
cursor.execute("""
SELECT pack_drepturi.verificautilizator(:username, :password)
FROM DUAL
""", {
'username': username.upper(),
'password': password
})
result = cursor.fetchone()
verification_result = result[0] if result else -1
# DEBUG: Log the exact result from Oracle
logger.info(f"[DEBUG] verificautilizator('{username.upper()}', '***') on server '{server_id}' = {verification_result}")
# Interpretarea rezultatului conform logicii VFP:
# -1 = invalid credentials
# > 0 = valid user ID with checksum
# < -1000000 = admin/super user
is_valid = verification_result != -1
if is_valid:
# Extrage ID-ul real al utilizatorului conform logicii VFP
if verification_result < -1000000:
# Admin/Super user
user_id = verification_result + 1000000
logger.info(f"Admin/Super user {username} authenticated successfully (ID: {user_id})")
else:
# User normal - extrage ID-ul din checksum
user_id = int(verification_result / 100)
logger.info(f"User {username} authenticated successfully (ID: {user_id}, verification: {verification_result})")
else:
logger.warning(f"Authentication failed for user {username}")
return is_valid
except Exception as e:
logger.error(f"Database error during authentication for user {username}: {str(e)}")
raise AuthenticationError(f"Database authentication error: {str(e)}")
async def get_user_companies(
self,
username: str,
server_id: Optional[str] = None
) -> List[str]:
"""
Obține lista firmelor la care utilizatorul are acces din V_NOM_FIRME
folosind ID-ul utilizatorului din UTILIZATORI
Args:
username: Numele utilizatorului
server_id: ID-ul serverului Oracle (opțional, pentru multi-server mode)
Returns:
Lista codurilor firmelor la care utilizatorul are acces
Raises:
AuthenticationError: Dacă apar erori în procesul de obținere
"""
# Verifică cache-ul mai întâi (include server_id în cheie pentru multi-server)
cache_key_suffix = f"_{server_id}" if server_id else ""
cached_data = self._get_cached_user_data(f"{username}{cache_key_suffix}")
if cached_data and 'companies' in cached_data:
return cached_data['companies']
try:
async with oracle_pool.get_connection(server_id) as connection:
with connection.cursor() as cursor:
try:
# Debug: să vedem ce utilizatori există în tabela UTILIZATORI
cursor.execute("""
SELECT ID_UTIL, UTILIZATOR
FROM UTILIZATORI
WHERE UPPER(UTILIZATOR) LIKE '%MARIUS%'
ORDER BY UTILIZATOR
""")
debug_users = cursor.fetchall()
logger.info(f"DEBUG: Users with MARIUS in name: {debug_users}")
# Primul pas: obținem ID-ul utilizatorului din UTILIZATORI
cursor.execute("""
SELECT ID_UTIL, UTILIZATOR
FROM UTILIZATORI
WHERE UPPER(UTILIZATOR) = :username
""", {'username': username.upper()})
user_row = cursor.fetchone()
if not user_row:
logger.warning(f"User {username} not found in UTILIZATORI table")
# Să încercăm să găsim utilizatori similari
cursor.execute("""
SELECT ID_UTIL, UTILIZATOR
FROM UTILIZATORI
WHERE UPPER(UTILIZATOR) LIKE :username_pattern
ORDER BY UTILIZATOR
""", {'username_pattern': f'%{username.upper()}%'})
similar_users = cursor.fetchall()
logger.info(f"Similar users found: {similar_users}")
return []
user_id = user_row[0]
actual_name = user_row[1]
logger.info(f"Found user {username} with ID: {user_id}, actual name: {actual_name}")
# Al doilea pas: obținem firmele folosind query-ul corect (cu ID_FIRMA)
cursor.execute("""
SELECT A.ID_FIRMA, A.FIRMA
FROM V_NOM_FIRME A
WHERE A.ID_FIRMA IN (
SELECT ID_FIRMA
FROM VDEF_UTIL_FIRME
WHERE ID_PROGRAM = 2
AND ID_UTIL = :user_id
)
ORDER BY A.FIRMA
""", {'user_id': user_id})
companies_rows = cursor.fetchall()
companies = [str(row[0]) for row in companies_rows if row[0]]
if not companies:
logger.warning(f"No companies found for user {username} (ID: {user_id})")
return []
logger.info(f"User {username} has access to {len(companies)} companies: {companies}")
except Exception as e:
logger.error(f"Could not query companies for user {username}: {e}")
# În caz de eroare, returnăm listă goală în loc de TEST_COMPANY
return []
# Cache rezultatul (include server_id pentru multi-server)
cache_key = f"{username}{cache_key_suffix}"
self._cache_user_data(cache_key, {'companies': companies})
return companies
except Exception as e:
logger.error(f"Database error getting companies for user {username}: {str(e)}")
raise AuthenticationError(f"Error retrieving user companies: {str(e)}")
async def get_user_permissions(
self,
username: str,
company: str,
server_id: Optional[str] = None
) -> List[str]:
"""
Obține permisiunile utilizatorului pentru o anumită firmă
Args:
username: Numele utilizatorului
company: Codul firmei
server_id: ID-ul serverului Oracle (pentru multi-server mode)
Returns:
Lista permisiunilor pentru firma specificată
"""
# Implementare de bază - poate fi extinsă în viitor
companies = await self.get_user_companies(username, server_id)
# Dacă nu există companii sau compania nu este în listă, returnează permisiuni minime
if not companies or company not in companies:
return ["read"] if not companies else []
# Pentru moment, toți utilizatorii autentificați au permisiuni de citire
# Acest sistem poate fi extins cu permisiuni granulare în viitor
return ["read", "reports"]
async def authenticate_and_create_tokens(
self,
username: str,
password: str,
server_id: Optional[str] = None
) -> Tuple[bool, Optional[TokenResponse], Optional[str]]:
"""
Autentifică utilizatorul și creează token-urile JWT
Suportă atât username clasic cât și email pentru login.
Dacă input-ul conține '@', se tratează ca email și se convertește
în username-ul Oracle corespunzător.
Args:
username: Numele utilizatorului sau email-ul
password: Parola utilizatorului
server_id: ID-ul serverului Oracle (opțional, pentru multi-server mode)
Returns:
Tuple cu (success, token_response, error_message)
"""
try:
# Detectăm dacă input-ul este email sau username clasic
actual_username = username
if '@' in username:
# Este email - convertim în username Oracle
resolved_username = await self.get_username_by_email(username, server_id)
if not resolved_username:
logger.warning(f"Could not resolve email '{username}' to username on server '{server_id}'")
return False, None, "Invalid username or password"
actual_username = resolved_username
logger.info(f"Login with email '{username}' resolved to username '{actual_username}'")
# Verifică credențialele pe serverul specificat
is_valid = await self.verify_user_credentials(actual_username, password, server_id)
if not is_valid:
return False, None, "Invalid username or password"
# Obține firmele utilizatorului de pe serverul specificat
companies = await self.get_user_companies(actual_username, server_id)
# Nu blocăm login-ul dacă utilizatorul nu are firme - îl lăsăm să vadă mesajul în frontend
if not companies:
logger.info(f"User {actual_username} has no companies assigned - allowing login but with empty companies list")
# Obține permisiunile (pentru prima firmă ca default sau lista goală)
permissions = await self.get_user_permissions(actual_username, companies[0] if companies else "", server_id)
# Creează token-urile folosind jwt_handler
# Include server_id în JWT pentru ca request-urile ulterioare să știe pe care server să execute query-uri
jwt_tokens = jwt_handler.create_token_response(
username=actual_username,
companies=companies,
user_id=None, # Poate fi adăugat în viitor dacă avem user_id în DB
permissions=permissions,
server_id=server_id
)
# Creează obiectul CurrentUser
current_user = CurrentUser(
username=actual_username,
user_id=None,
companies=companies,
permissions=permissions
)
# Creează TokenResponse-ul complet cu user info
token_response = TokenResponse(
access_token=jwt_tokens.access_token,
refresh_token=jwt_tokens.refresh_token,
token_type=jwt_tokens.token_type,
expires_in=jwt_tokens.expires_in,
user=current_user
)
logger.info(f"Successfully created tokens for user {actual_username} on server {server_id or 'default'}")
return True, token_response, None
except AuthenticationError as e:
logger.error(f"Authentication error for user {username}: {str(e)}")
return False, None, str(e)
except Exception as e:
logger.error(f"Unexpected error during authentication for user {username}: {str(e)}")
return False, None, "Internal authentication error"
async def validate_user_company_access(self, username: str, company: str) -> bool:
"""
Validează dacă utilizatorul are acces la o anumită firmă
Args:
username: Numele utilizatorului
company: Codul firmei de verificat
Returns:
True dacă utilizatorul are acces, False altfel
"""
try:
companies = await self.get_user_companies(username)
has_access = company in companies
if not has_access:
logger.warning(f"User {username} attempted to access unauthorized company {company}")
return has_access
except Exception as e:
logger.error(f"Error validating company access for user {username}: {str(e)}")
return False
async def refresh_user_data(self, username: str) -> bool:
"""
Reîmprospătează datele utilizatorului din cache
Args:
username: Numele utilizatorului
Returns:
True dacă refresh-ul a fost cu succes
"""
try:
# Șterge din cache
cache_key = self._get_cache_key(username)
if cache_key in self._user_cache:
del self._user_cache[cache_key]
# Reîncarcă datele
await self.get_user_companies(username)
logger.info(f"Refreshed user data for {username}")
return True
except Exception as e:
logger.error(f"Error refreshing user data for {username}: {str(e)}")
return False
def clear_cache(self) -> None:
"""Șterge tot cache-ul utilizatorilor"""
self._user_cache.clear()
logger.info("User cache cleared")
def get_cache_stats(self) -> Dict[str, Any]:
"""Returnează statistici despre cache"""
total_entries = len(self._user_cache)
valid_entries = sum(
1 for entry in self._user_cache.values()
if self._is_cache_valid(entry)
)
return {
'total_entries': total_entries,
'valid_entries': valid_entries,
'cache_hit_ratio': valid_entries / total_entries if total_entries > 0 else 0
}
# Instance globală pentru folosire în toate aplicațiile
auth_service = UserAuthService()

View File

@@ -0,0 +1,540 @@
"""
FastAPI Demo App demonstrând sistemul de autentificare ROA2WEB
Această aplicație demonstrează integrarea completă a sistemului de autentificare:
- Login și logout cu Oracle database
- Protected routes cu JWT authentication
- Company-specific access control
- Permission-based authorization
- Rate limiting și security features
Funcționează ca:
1. Exemplu de integrare pentru dezvoltatori
2. Tool de testare pentru sistemul de autentificare
3. Demonstrație pentru managementul proiectului
Pentru a rula demo-ul:
1. Configurează variabilele de mediu în .env
2. Asigură-te că Oracle database este accesibil
3. Rulează: python demo_app.py
4. Acesează http://localhost:8000/docs pentru Swagger UI
"""
import asyncio
import logging
import sys
import os
from datetime import datetime
from typing import List, Optional
import uvicorn
from fastapi import FastAPI, Depends, HTTPException, Request, status
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import HTMLResponse, JSONResponse
from contextlib import asynccontextmanager
# Adaugă calea pentru modulele shared
sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
# Import modulele de autentificare
from .jwt_handler import jwt_handler
from .auth_service import auth_service
from .models import CurrentUser, LoginRequest, PermissionType
from .routes import create_auth_router
from .middleware import AuthenticationMiddleware, default_rate_limiter
from .dependencies import (
get_current_user, get_optional_user, require_company_access,
require_permissions, get_current_company_from_header
)
# Import componente shared
from database.oracle_pool import oracle_pool
from utils.config import shared_config
# Configurare logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
@asynccontextmanager
async def lifespan(app: FastAPI):
"""
Lifecycle events pentru demo app
"""
# Startup
logger.info("🚀 Starting ROA2WEB Authentication Demo")
try:
# Inițializează Oracle pool
await oracle_pool.initialize(
user=shared_config.oracle_user,
password=shared_config.oracle_password,
dsn=shared_config.oracle_dsn,
min_connections=2,
max_connections=5
)
logger.info("✅ Oracle connection pool initialized")
# Test database connection
async with oracle_pool.get_connection() as conn:
with conn.cursor() as cursor:
cursor.execute("SELECT 'Database connected successfully' FROM DUAL")
result = cursor.fetchone()
logger.info(f"✅ Database test: {result[0]}")
except Exception as e:
logger.error(f"❌ Startup error: {str(e)}")
logger.warning("Demo will continue but database features may not work")
yield
# Shutdown
logger.info("🛑 Shutting down ROA2WEB Authentication Demo")
try:
await oracle_pool.close_pool()
logger.info("✅ Oracle connection pool closed")
except Exception as e:
logger.error(f"❌ Shutdown error: {str(e)}")
# Crearea aplicației FastAPI
app = FastAPI(
title="ROA2WEB Authentication Demo",
description="""
Demonstrație completă a sistemului de autentificare ROA2WEB
Această aplicație demonstrează:
- JWT Authentication cu Oracle Database
- Protected routes și company access control
- Permission-based authorization
- Rate limiting și security features
- Integration patterns pentru aplicații ROA2WEB
""",
version="1.0.0",
docs_url="/docs",
redoc_url="/redoc",
lifespan=lifespan
)
# CORS pentru development
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:3000", "http://localhost:5173", "http://localhost:8080"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Authentication middleware
app.add_middleware(
AuthenticationMiddleware,
excluded_paths=["/", "/docs", "/redoc", "/openapi.json", "/health", "/demo", "/auth/login"],
rate_limit_paths=["/auth/login"],
rate_limiter=default_rate_limiter
)
# Include authentication router
auth_router = create_auth_router(include_admin_routes=True)
app.include_router(auth_router)
# =============================================================================
# DEMO ENDPOINTS
# =============================================================================
@app.get("/", response_class=HTMLResponse)
async def demo_home():
"""
Pagina principală cu informații despre demo
"""
html_content = """
<!DOCTYPE html>
<html>
<head>
<title>ROA2WEB Authentication Demo</title>
<style>
body { font-family: Arial, sans-serif; margin: 40px; background: #f5f5f5; }
.container { max-width: 800px; margin: 0 auto; background: white; padding: 30px; border-radius: 10px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
h1 { color: #2c3e50; border-bottom: 3px solid #3498db; padding-bottom: 10px; }
h2 { color: #34495e; margin-top: 30px; }
.endpoint { background: #ecf0f1; padding: 15px; margin: 10px 0; border-radius: 5px; border-left: 4px solid #3498db; }
.method { font-weight: bold; color: #e74c3c; }
.protected { border-left-color: #f39c12; }
.public { border-left-color: #27ae60; }
code { background: #34495e; color: white; padding: 2px 6px; border-radius: 3px; }
.status { padding: 10px; margin: 15px 0; border-radius: 5px; }
.success { background: #d5edda; border: 1px solid #c3e6cb; color: #155724; }
.info { background: #d1ecf1; border: 1px solid #bee5eb; color: #0c5460; }
</style>
</head>
<body>
<div class="container">
<h1>🔐 ROA2WEB Authentication Demo</h1>
<div class="status info">
<strong>Status:</strong> Demo aplicație ROA2WEB Authentication System<br>
<strong>Versiune:</strong> 1.0.0<br>
<strong>Timp:</strong> """ + datetime.now().strftime("%Y-%m-%d %H:%M:%S") + """
</div>
<h2>📋 Endpoints Disponibile</h2>
<div class="endpoint public">
<div class="method">GET</div>
<strong>/docs</strong> - Swagger UI pentru testarea API-ului
</div>
<div class="endpoint public">
<div class="method">GET</div>
<strong>/health</strong> - Health check pentru aplicație și database
</div>
<div class="endpoint public">
<div class="method">POST</div>
<strong>/auth/login</strong> - Autentificare utilizator cu username/password
</div>
<div class="endpoint protected">
<div class="method">GET</div>
<strong>/auth/me</strong> - Informații utilizator curent (protejat)
</div>
<div class="endpoint protected">
<div class="method">GET</div>
<strong>/demo/protected</strong> - Endpoint protejat simplu
</div>
<div class="endpoint protected">
<div class="method">GET</div>
<strong>/demo/company/{company_code}</strong> - Endpoint cu verificare acces firmă
</div>
<div class="endpoint protected">
<div class="method">GET</div>
<strong>/demo/admin</strong> - Endpoint cu verificare admin permissions
</div>
<h2>🧪 Cum să testezi</h2>
<ol>
<li>Accesează <a href="/docs">/docs</a> pentru Swagger UI</li>
<li>Folosește <code>POST /auth/login</code> cu credențiale valide</li>
<li>Copiază <code>access_token</code> din răspuns</li>
<li>Click pe "Authorize" în Swagger UI și introdu: <code>Bearer YOUR_TOKEN</code></li>
<li>Testează endpoint-urile protejate</li>
</ol>
<h2>🔧 Configurare</h2>
<p>Pentru a funcționa complet, demo-ul necesită:</p>
<ul>
<li>Variabile de mediu configurate în <code>.env</code></li>
<li>Conexiune la Oracle Database</li>
<li>Utilizatori valizi în sistemul Oracle</li>
</ul>
<div class="status success">
<strong>💡 Tip:</strong> Pentru dezvoltare rapidă, vezi <code>demo_app.py</code>
pentru exemple de integrare a autentificării în aplicațiile tale FastAPI.
</div>
</div>
</body>
</html>
"""
return HTMLResponse(content=html_content)
@app.get("/health")
async def health_check():
"""
Health check complet pentru demo
"""
health_status = {
"service": "ROA2WEB Authentication Demo",
"status": "healthy",
"timestamp": datetime.now().isoformat(),
"version": "1.0.0"
}
# Test database connection
try:
async with oracle_pool.get_connection() as conn:
with conn.cursor() as cursor:
cursor.execute("SELECT 1 FROM DUAL")
cursor.fetchone()
health_status["database"] = "connected"
except Exception as e:
health_status["database"] = f"error: {str(e)}"
health_status["status"] = "degraded"
# Test JWT handler
try:
test_token = jwt_handler.create_access_token("healthcheck", ["TEST"])
token_data = jwt_handler.verify_token(test_token)
if token_data and token_data.username == "healthcheck":
health_status["jwt"] = "functional"
else:
health_status["jwt"] = "error: token verification failed"
health_status["status"] = "degraded"
except Exception as e:
health_status["jwt"] = f"error: {str(e)}"
health_status["status"] = "degraded"
# Authentication service status
try:
cache_stats = auth_service.get_cache_stats()
health_status["auth_cache"] = {
"total_entries": cache_stats["total_entries"],
"cache_hit_ratio": cache_stats["cache_hit_ratio"]
}
except Exception as e:
health_status["auth_cache"] = f"error: {str(e)}"
status_code = 200 if health_status["status"] == "healthy" else 503
return JSONResponse(content=health_status, status_code=status_code)
@app.get("/demo/public")
async def demo_public_endpoint():
"""
Endpoint public - nu necesită autentificare
"""
return {
"message": "Acesta este un endpoint public",
"authenticated": False,
"timestamp": datetime.now().isoformat(),
"info": "Acest endpoint poate fi accesat fără autentificare"
}
@app.get("/demo/optional-auth")
async def demo_optional_auth(
current_user: Optional[CurrentUser] = Depends(get_optional_user)
):
"""
Endpoint cu autentificare opțională
"""
if current_user:
return {
"message": f"Salut, {current_user.username}!",
"authenticated": True,
"user": current_user.username,
"companies": current_user.companies,
"timestamp": datetime.now().isoformat()
}
else:
return {
"message": "Acesta este un endpoint cu autentificare opțională",
"authenticated": False,
"timestamp": datetime.now().isoformat(),
"info": "Poți accesa și fără autentificare, dar cu token obții mai multe informații"
}
@app.get("/demo/protected")
async def demo_protected_endpoint(
current_user: CurrentUser = Depends(get_current_user)
):
"""
Endpoint protejat - necesită autentificare
"""
return {
"message": f"Bună ziua, {current_user.username}!",
"authenticated": True,
"user_info": {
"username": current_user.username,
"companies": current_user.companies,
"permissions": current_user.permissions,
"companies_count": len(current_user.companies)
},
"timestamp": datetime.now().isoformat(),
"info": "Acest endpoint necesită JWT token valid pentru acces"
}
@app.get("/demo/company/{company_code}")
async def demo_company_specific_endpoint(
company_code: str,
current_user: CurrentUser = Depends(require_company_access("")) # Will be overridden
):
"""
Endpoint cu verificare acces la firmă specifică
Demonstrează cum să verifici dacă utilizatorul are acces la o anumită firmă
"""
# Verificare manuală pentru demonstrație (în practică folosești dependency)
if company_code not in current_user.companies:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=f"Nu aveți acces la firma {company_code}"
)
return {
"message": f"Acces permis la firma {company_code}",
"company_code": company_code,
"user": current_user.username,
"user_companies": current_user.companies,
"timestamp": datetime.now().isoformat(),
"info": "Utilizatorul are acces la această firmă"
}
@app.get("/demo/admin")
async def demo_admin_endpoint(
current_user: CurrentUser = Depends(require_permissions([PermissionType.ADMIN]))
):
"""
Endpoint cu verificare permisiuni admin
"""
return {
"message": f"Bună ziua, admin {current_user.username}!",
"admin_info": {
"username": current_user.username,
"permissions": current_user.permissions,
"companies": current_user.companies,
"admin_since": datetime.now().isoformat()
},
"system_stats": {
"total_companies": len(current_user.companies),
"demo_version": "1.0.0",
"auth_system": "ROA2WEB JWT"
},
"timestamp": datetime.now().isoformat(),
"info": "Acest endpoint necesită permisiuni de administrator"
}
@app.get("/demo/reports")
async def demo_reports_endpoint(
request: Request,
current_user: CurrentUser = Depends(require_permissions([PermissionType.REPORTS]))
):
"""
Endpoint pentru rapoarte - demonstrează integrarea cu header-ul Company
"""
# Obține company din header (X-Company-Code) sau folosește prima disponibilă
company_code = request.headers.get("X-Company-Code")
if not company_code:
company_code = current_user.companies[0] if current_user.companies else None
if not company_code:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Nu s-a specificat codul firmei (X-Company-Code header)"
)
if company_code not in current_user.companies:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=f"Nu aveți acces la rapoartele firmei {company_code}"
)
# Simulează generarea unui raport
mock_report_data = {
"company_code": company_code,
"report_type": "demo_report",
"generated_by": current_user.username,
"generated_at": datetime.now().isoformat(),
"data": {
"total_invoices": 150,
"total_amount": 125000.50,
"paid_invoices": 120,
"outstanding_amount": 25000.00
}
}
return {
"message": f"Raport generat pentru firma {company_code}",
"report": mock_report_data,
"user_info": {
"username": current_user.username,
"permissions": current_user.permissions
},
"info": "Acesta este un exemplu de endpoint pentru rapoarte cu verificare company access"
}
@app.get("/demo/rate-limited")
async def demo_rate_limited_endpoint():
"""
Endpoint cu rate limiting pentru demonstrație
"""
return {
"message": "Acest endpoint are rate limiting aplicat",
"timestamp": datetime.now().isoformat(),
"info": "Încercați să faceți mai multe request-uri rapid pentru a vedea rate limiting-ul"
}
# =============================================================================
# DEMO UTILITIES
# =============================================================================
@app.get("/demo/token-info")
async def demo_token_info(
request: Request,
current_user: CurrentUser = Depends(get_current_user)
):
"""
Endpoint pentru afișarea informațiilor despre token-ul curent
"""
# Extrage token-ul din header
auth_header = request.headers.get("Authorization", "")
if auth_header.startswith("Bearer "):
token = auth_header[7:]
# Decodează token-ul pentru informații (fără verificare pentru demo)
payload = jwt_handler.decode_token_payload(token)
return {
"message": "Informații despre token-ul curent",
"token_info": {
"user": current_user.username,
"companies": current_user.companies,
"permissions": current_user.permissions,
"token_type": payload.get("type") if payload else "unknown",
"issued_at": payload.get("iat") if payload else None,
"expires_at": payload.get("exp") if payload else None
},
"timestamp": datetime.now().isoformat()
}
else:
return {
"error": "Nu s-a găsit token în header-ul Authorization"
}
# =============================================================================
# MAIN EXECUTION
# =============================================================================
def main():
"""
Funcția principală pentru rularea demo-ului
"""
print("🚀 Starting ROA2WEB Authentication Demo")
print("📋 Available endpoints:")
print(" • http://localhost:8000/ - Demo home page")
print(" • http://localhost:8000/docs - Swagger UI")
print(" • http://localhost:8000/health - Health check")
print(" • http://localhost:8000/demo/* - Demo endpoints")
print("")
print("💡 Pentru testare completă:")
print(" 1. Configurează .env cu credențialele Oracle")
print(" 2. Asigură-te că database-ul este accesibil")
print(" 3. Folosește /docs pentru testarea interactivă")
print("")
uvicorn.run(
"demo_app:app",
host="0.0.0.0",
port=8000,
reload=True,
log_level="info"
)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,402 @@
"""
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

View File

@@ -0,0 +1,362 @@
"""
Email-Server Cache for Multi-Oracle Auto-Discovery
Builds and maintains a cache mapping emails to server IDs:
- At startup, connects to each Oracle server and extracts emails from CONTAFIN_ORACLE.UTILIZATORI
- Cache structure: {email: [server_ids]}
- Auto-refresh every 15 minutes (configurable)
- Thread-safe with asyncio.Lock
US-003: Auto-Discovery Email-Server Cache
US-013: Added username lookup support (direct query, no caching)
"""
import asyncio
import logging
from datetime import datetime, timedelta
from typing import Dict, List, Optional, Set
logger = logging.getLogger(__name__)
class EmailServerCache:
"""
Cache for email-to-server mapping.
Builds a dictionary {email: [server_ids]} by querying CONTAFIN_ORACLE.UTILIZATORI
on each configured Oracle server.
Features:
- Lazy initialization (build on first access or explicit call)
- Auto-refresh at configurable intervals
- Thread-safe operations
- Graceful handling of server connection failures
"""
_instance: Optional['EmailServerCache'] = None
def __new__(cls):
if cls._instance is None:
cls._instance = super(EmailServerCache, cls).__new__(cls)
cls._instance._cache: Dict[str, List[str]] = {}
cls._instance._last_refresh: Optional[datetime] = None
cls._instance._refresh_interval = timedelta(minutes=15)
cls._instance._lock = asyncio.Lock()
cls._instance._initialized = False
cls._instance._refresh_task: Optional[asyncio.Task] = None
return cls._instance
def set_refresh_interval(self, minutes: int) -> None:
"""
Set the cache refresh interval.
Args:
minutes: Refresh interval in minutes (default: 15)
"""
self._refresh_interval = timedelta(minutes=minutes)
logger.info(f"Email cache refresh interval set to {minutes} minutes")
async def build_cache(self) -> None:
"""
Build the email-server cache by querying all configured Oracle servers.
Connects to each server and extracts active user emails from
CONTAFIN_ORACLE.UTILIZATORI table.
"""
from shared.database.oracle_pool import oracle_pool
from backend.config import settings
async with self._lock:
logger.info("[EMAIL-CACHE] Building email-server cache...")
new_cache: Dict[str, Set[str]] = {} # Use set to avoid duplicates
servers = settings.get_oracle_servers()
if not servers:
logger.warning("[EMAIL-CACHE] No Oracle servers configured")
self._cache = {}
self._last_refresh = datetime.now()
self._initialized = True
return
for server in servers:
try:
logger.info(f"[EMAIL-CACHE] Querying server '{server.id}' ({server.name})...")
# Get connection from the multi-pool
async with oracle_pool.get_connection(server.id) as connection:
with connection.cursor() as cursor:
# Query emails from UTILIZATORI table
# Only active users (INACTIV=0, STERS=0) with valid emails
cursor.execute("""
SELECT LOWER(EMAIL) as email
FROM CONTAFIN_ORACLE.UTILIZATORI
WHERE EMAIL IS NOT NULL
AND TRIM(EMAIL) IS NOT NULL
AND INACTIV = 0
AND STERS = 0
""")
rows = cursor.fetchall()
email_count = 0
for row in rows:
email = row[0].strip().lower() if row[0] else None
if email and '@' in email: # Basic email validation
if email not in new_cache:
new_cache[email] = set()
new_cache[email].add(server.id)
email_count += 1
logger.info(f"[EMAIL-CACHE] Found {email_count} valid emails on server '{server.id}'")
except Exception as e:
# Log error but continue with other servers
logger.error(f"[EMAIL-CACHE] Failed to query server '{server.id}': {e}")
continue
# Convert sets to sorted lists for consistent ordering
self._cache = {email: sorted(list(server_ids)) for email, server_ids in new_cache.items()}
self._last_refresh = datetime.now()
self._initialized = True
total_emails = len(self._cache)
multi_server_emails = sum(1 for servers in self._cache.values() if len(servers) > 1)
logger.info(f"[EMAIL-CACHE] ✅ Cache built: {total_emails} unique emails")
logger.info(f"[EMAIL-CACHE] {multi_server_emails} emails exist on multiple servers")
async def refresh_if_needed(self) -> bool:
"""
Refresh cache if the refresh interval has passed.
Returns:
True if cache was refreshed, False otherwise
"""
if not self._initialized:
await self.build_cache()
return True
if self._last_refresh is None:
await self.build_cache()
return True
time_since_refresh = datetime.now() - self._last_refresh
if time_since_refresh >= self._refresh_interval:
await self.build_cache()
return True
return False
def get_servers_for_email(self, email: str) -> List[str]:
"""
Get list of server IDs where the email exists.
Args:
email: User email address
Returns:
List of server_ids where this email exists.
Empty list if email not found (NOT an error).
"""
if not email:
return []
normalized_email = email.strip().lower()
servers = self._cache.get(normalized_email, [])
if servers:
logger.debug(f"[EMAIL-CACHE] Email '{normalized_email}' found on servers: {servers}")
else:
logger.debug(f"[EMAIL-CACHE] Email '{normalized_email}' not found in cache")
return servers.copy() # Return a copy to prevent external modification
def is_initialized(self) -> bool:
"""Check if cache has been built at least once."""
return self._initialized
def get_cache_stats(self) -> Dict:
"""
Get cache statistics.
Returns:
Dict with cache stats (total_emails, multi_server_count, last_refresh, etc.)
"""
if not self._initialized:
return {
'initialized': False,
'total_emails': 0,
'last_refresh': None,
'refresh_interval_minutes': self._refresh_interval.total_seconds() / 60
}
multi_server = sum(1 for servers in self._cache.values() if len(servers) > 1)
server_distribution = {}
for servers in self._cache.values():
count = len(servers)
server_distribution[count] = server_distribution.get(count, 0) + 1
return {
'initialized': True,
'total_emails': len(self._cache),
'multi_server_count': multi_server,
'server_distribution': server_distribution,
'last_refresh': self._last_refresh.isoformat() if self._last_refresh else None,
'refresh_interval_minutes': self._refresh_interval.total_seconds() / 60
}
async def start_auto_refresh(self) -> None:
"""
Start background task for automatic cache refresh.
Runs refresh at the configured interval (default: 15 minutes).
"""
if self._refresh_task and not self._refresh_task.done():
logger.warning("[EMAIL-CACHE] Auto-refresh task already running")
return
async def refresh_loop():
while True:
try:
await asyncio.sleep(self._refresh_interval.total_seconds())
logger.info("[EMAIL-CACHE] Auto-refresh triggered")
await self.build_cache()
except asyncio.CancelledError:
logger.info("[EMAIL-CACHE] Auto-refresh task cancelled")
break
except Exception as e:
logger.error(f"[EMAIL-CACHE] Auto-refresh error: {e}")
# Continue running, will retry on next interval
self._refresh_task = asyncio.create_task(refresh_loop())
logger.info(f"[EMAIL-CACHE] Auto-refresh started (every {self._refresh_interval.total_seconds() / 60:.0f} minutes)")
async def stop_auto_refresh(self) -> None:
"""Stop the auto-refresh background task."""
if self._refresh_task and not self._refresh_task.done():
self._refresh_task.cancel()
try:
await self._refresh_task
except asyncio.CancelledError:
pass
self._refresh_task = None
logger.info("[EMAIL-CACHE] Auto-refresh stopped")
def clear_cache(self) -> None:
"""Clear the cache (useful for testing)."""
self._cache = {}
self._initialized = False
self._last_refresh = None
logger.info("[EMAIL-CACHE] Cache cleared")
async def get_servers_for_username(self, username: str) -> List[str]:
"""
Get list of server IDs where the username exists (US-013).
Unlike email lookup which uses the cache, username lookup queries
Oracle directly on each server. This is because:
- Usernames are less commonly used for login
- Direct query ensures fresh data
- Avoids bloating the cache with both email and username mappings
Args:
username: Username to look up (case-insensitive, converted to uppercase)
Returns:
List of server_ids where this username exists.
Empty list if username not found (NOT an error).
"""
if not username:
return []
from shared.database.oracle_pool import oracle_pool
from backend.config import settings
normalized_username = username.strip().upper()
found_servers: List[str] = []
servers = settings.get_oracle_servers()
if not servers:
logger.warning("[EMAIL-CACHE] No Oracle servers configured for username lookup")
return []
for server in servers:
try:
async with oracle_pool.get_connection(server.id) as connection:
with connection.cursor() as cursor:
# Query for username in UTILIZATORI table
# Only active users (INACTIV=0, STERS=0)
cursor.execute("""
SELECT 1
FROM CONTAFIN_ORACLE.UTILIZATORI
WHERE UPPER(UTILIZATOR) = :username
AND INACTIV = 0
AND STERS = 0
AND ROWNUM = 1
""", {"username": normalized_username})
row = cursor.fetchone()
if row:
found_servers.append(server.id)
logger.debug(f"[EMAIL-CACHE] Username '{normalized_username}' found on server '{server.id}'")
except Exception as e:
logger.error(f"[EMAIL-CACHE] Failed to query username on server '{server.id}': {e}")
continue
if found_servers:
logger.info(f"[EMAIL-CACHE] Username '{normalized_username}' found on {len(found_servers)} server(s): {found_servers}")
else:
logger.debug(f"[EMAIL-CACHE] Username '{normalized_username}' not found on any server")
return sorted(found_servers)
# Global singleton instance
email_server_cache = EmailServerCache()
# Convenience functions for external use
def get_servers_for_email(email: str) -> List[str]:
"""
Get list of server IDs where the email exists.
This is a convenience function that wraps the singleton instance.
Args:
email: User email address
Returns:
List of server_ids. Empty list if email not found (NOT an error).
"""
return email_server_cache.get_servers_for_email(email)
async def build_email_cache() -> None:
"""Build/refresh the email-server cache."""
await email_server_cache.build_cache()
async def start_email_cache_refresh() -> None:
"""Start automatic cache refresh."""
await email_server_cache.start_auto_refresh()
async def stop_email_cache_refresh() -> None:
"""Stop automatic cache refresh."""
await email_server_cache.stop_auto_refresh()
async def get_servers_for_username(username: str) -> List[str]:
"""
Get list of server IDs where the username exists (US-013).
This is a convenience function that wraps the singleton instance.
Args:
username: Username to look up (case-insensitive)
Returns:
List of server_ids. Empty list if username not found (NOT an error).
"""
return await email_server_cache.get_servers_for_username(username)

View File

@@ -0,0 +1,264 @@
"""
JWT Authentication Handler - Shared între toate aplicațiile ROA2WEB
Acest modul gestionează crearea, validarea și refresh-ul token-urilor JWT
pentru autentificarea utilizatorilor în ecosistemul ROA2WEB.
Payload structure:
{
"username": "string",
"user_id": "integer",
"companies": ["schema1", "schema2"],
"permissions": ["read", "write", "admin"],
"server_id": "string|null", // ID-ul serverului Oracle (multi-server mode)
"exp": "timestamp",
"iat": "timestamp",
"type": "access|refresh"
}
"""
from jose import jwt
import os
from datetime import datetime, timedelta
from typing import Optional, Dict, Any, List
from pydantic import BaseModel, Field
import logging
logger = logging.getLogger(__name__)
class TokenData(BaseModel):
"""Date conținute în token"""
username: str = Field(description="Numele utilizatorului")
user_id: Optional[int] = Field(default=None, description="ID-ul utilizatorului")
companies: List[str] = Field(default_factory=list, description="Lista firmelor accesibile")
permissions: List[str] = Field(default_factory=list, description="Lista permisiunilor")
server_id: Optional[str] = Field(default=None, description="ID-ul serverului Oracle (pentru multi-server mode)")
exp: datetime = Field(description="Data expirării")
iat: datetime = Field(description="Data creării")
token_type: str = Field(alias="type", description="Tipul token-ului (access/refresh)")
class TokenResponse(BaseModel):
"""Răspuns pentru token-uri"""
access_token: str = Field(description="JWT access token")
refresh_token: Optional[str] = Field(default=None, description="JWT refresh token")
token_type: str = Field(default="bearer", description="Tipul token-ului")
expires_in: int = Field(description="Timpul de expirare în secunde")
class JWTHandler:
"""
Gestionarea JWT tokens pentru autentificare
Această clasă oferă funcționalități pentru:
- Crearea token-urilor access și refresh
- Validarea și decodificarea token-urilor
- Gestionarea expirării token-urilor
"""
def __init__(self, secret_key: Optional[str] = None, algorithm: str = "HS256"):
"""
Inițializează JWT handler
Args:
secret_key: Cheia secretă pentru semnarea token-urilor
algorithm: Algoritmul de criptare (default: HS256)
"""
self.secret_key = secret_key or os.getenv('JWT_SECRET_KEY', 'your-secret-key-change-in-production')
self.algorithm = algorithm
self.access_token_expire_minutes = int(os.getenv('ACCESS_TOKEN_EXPIRE_MINUTES', 30))
self.refresh_token_expire_days = int(os.getenv('REFRESH_TOKEN_EXPIRE_DAYS', 7))
# Warning pentru development
if self.secret_key == 'your-secret-key-change-in-production':
logger.warning("Using default JWT secret key! Change JWT_SECRET_KEY in production!")
def create_access_token(
self,
username: str,
companies: List[str],
user_id: Optional[int] = None,
permissions: Optional[List[str]] = None,
server_id: Optional[str] = None
) -> str:
"""
Creează un JWT access token
Args:
username: Numele utilizatorului
companies: Lista firmelor la care utilizatorul are acces
user_id: ID-ul utilizatorului în baza de date
permissions: Lista permisiunilor utilizatorului
server_id: ID-ul serverului Oracle (pentru multi-server mode)
Returns:
Token JWT ca string
"""
now = datetime.utcnow()
expire = now + timedelta(minutes=self.access_token_expire_minutes)
payload = {
"username": username,
"user_id": user_id,
"companies": companies or [],
"permissions": permissions or ["read"],
"server_id": server_id,
"exp": expire,
"iat": now,
"type": "access"
}
token = jwt.encode(payload, self.secret_key, algorithm=self.algorithm)
logger.debug(f"Created access token for user {username} on server {server_id or 'default'} with companies: {companies}")
return token
def create_refresh_token(
self,
username: str,
user_id: Optional[int] = None,
server_id: Optional[str] = None
) -> str:
"""
Creează un refresh token cu durată mai mare
Args:
username: Numele utilizatorului
user_id: ID-ul utilizatorului
server_id: ID-ul serverului Oracle (pentru multi-server mode)
Returns:
Refresh token JWT ca string
"""
now = datetime.utcnow()
expire = now + timedelta(days=self.refresh_token_expire_days)
payload = {
"username": username,
"user_id": user_id,
"server_id": server_id,
"exp": expire,
"iat": now,
"type": "refresh"
}
token = jwt.encode(payload, self.secret_key, algorithm=self.algorithm)
logger.debug(f"Created refresh token for user {username} on server {server_id or 'default'}")
return token
def verify_token(self, token: str) -> Optional[TokenData]:
"""
Verifică și decodează un JWT token
Args:
token: Token-ul JWT de verificat
Returns:
TokenData cu informațiile din token sau None dacă token-ul e invalid
"""
try:
logger.debug(f"Using JWT secret key (first 10 chars): {self.secret_key[:10]}...")
payload = jwt.decode(token, self.secret_key, algorithms=[self.algorithm])
token_data = TokenData(**payload)
logger.debug(f"Token verified successfully for user {token_data.username}")
return token_data
except jwt.ExpiredSignatureError:
logger.warning("Token has expired")
return None
except jwt.JWTError as e:
logger.warning(f"Invalid token: {str(e)}")
logger.debug(f"Token that failed verification: {token[:50]}...")
return None
def refresh_access_token(
self,
refresh_token: str,
companies: List[str],
permissions: Optional[List[str]] = None
) -> Optional[str]:
"""
Creează un nou access token folosind refresh token-ul
Args:
refresh_token: Refresh token-ul valid
companies: Lista actualizată a firmelor (poate fi modificată între refresh-uri)
permissions: Lista actualizată a permisiunilor
Returns:
Noul access token sau None dacă refresh token-ul e invalid
"""
token_data = self.verify_token(refresh_token)
if not token_data or token_data.token_type != "refresh":
logger.warning("Invalid refresh token")
return None
# Creează nou access token cu datele din refresh token
# Păstrează server_id din refresh token pentru consistență multi-server
return self.create_access_token(
username=token_data.username,
companies=companies,
user_id=token_data.user_id,
permissions=permissions,
server_id=token_data.server_id
)
def create_token_response(
self,
username: str,
companies: List[str],
user_id: Optional[int] = None,
permissions: Optional[List[str]] = None,
include_refresh: bool = True,
server_id: Optional[str] = None
) -> TokenResponse:
"""
Creează un răspuns complet cu access și refresh token
Args:
username: Numele utilizatorului
companies: Lista firmelor accesibile
user_id: ID-ul utilizatorului
permissions: Lista permisiunilor
include_refresh: Dacă să includă și refresh token
server_id: ID-ul serverului Oracle (pentru multi-server mode)
Returns:
TokenResponse cu toate token-urile
"""
access_token = self.create_access_token(
username, companies, user_id, permissions, server_id
)
refresh_token = self.create_refresh_token(
username, user_id, server_id
) if include_refresh else None
return TokenResponse(
access_token=access_token,
refresh_token=refresh_token,
token_type="bearer",
expires_in=self.access_token_expire_minutes * 60
)
def decode_token_payload(self, token: str) -> Optional[Dict[str, Any]]:
"""
Decodează token-ul fără verificare (pentru debugging)
Args:
token: Token-ul de decodat
Returns:
Payload-ul token-ului sau None
"""
try:
# Decodare fără verificare - doar pentru debugging
payload = jwt.decode(token, key="", algorithms=[self.algorithm], options={"verify_signature": False})
return payload
except Exception as e:
logger.error(f"Error decoding token payload: {str(e)}")
return None
# Instance globală pentru folosire în toate aplicațiile
jwt_handler = JWTHandler()

View File

@@ -0,0 +1,375 @@
"""
FastAPI Authentication Middleware pentru ROA2WEB
Acest modul oferă middleware pentru autentificarea automată în aplicațiile FastAPI,
incluzând extragerea token-urilor, validarea și injectarea datelor utilizatorului
în contextul request-ului.
Funcționalități:
- Extragere automată token JWT din header Authorization
- Validare token și user data injection
- Rate limiting pentru endpoint-urile de autentificare
- Logging pentru securitate și monitoring
"""
import logging
import time
from typing import Optional, Callable, Dict, Any, List, Set
from collections import defaultdict, deque
from datetime import datetime, timedelta
from fastapi import Request, Response, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.responses import JSONResponse
from .jwt_handler import jwt_handler, TokenData
from .auth_service import auth_service
from .models import CurrentUser, AuthError
logger = logging.getLogger(__name__)
class RateLimiter:
"""
Rate limiter pentru protejarea endpoint-urilor de autentificare
"""
def __init__(self, max_requests: int = 5, time_window: int = 300):
"""
Inițializează rate limiter
Args:
max_requests: Numărul maxim de request-uri permise
time_window: Fereastra de timp în secunde
"""
self.max_requests = max_requests
self.time_window = time_window
self.requests: Dict[str, deque] = defaultdict(deque)
def is_allowed(self, client_ip: str) -> bool:
"""
Verifică dacă request-ul este permis pentru acest IP
Args:
client_ip: Adresa IP a clientului
Returns:
True dacă request-ul este permis
"""
now = time.time()
client_requests = self.requests[client_ip]
# Șterge request-urile vechi
while client_requests and client_requests[0] < now - self.time_window:
client_requests.popleft()
# Verifică dacă putem accepta încă un request
if len(client_requests) >= self.max_requests:
return False
# Adaugă request-ul curent
client_requests.append(now)
return True
def get_reset_time(self, client_ip: str) -> int:
"""
Returnează timpul când rate limiting se resetează pentru acest IP
Args:
client_ip: Adresa IP a clientului
Returns:
Timestamp când se resetează
"""
client_requests = self.requests[client_ip]
if not client_requests:
return int(time.time())
return int(client_requests[0] + self.time_window)
class AuthenticationMiddleware(BaseHTTPMiddleware):
"""
Middleware pentru autentificarea automată în FastAPI
Acest middleware:
- Extrage token-ul JWT din header-ul Authorization
- Validează token-ul și obține datele utilizatorului
- Injectează utilizatorul curent în request.state
- Aplică rate limiting pentru endpoint-urile sensibile
"""
def __init__(
self,
app,
excluded_paths: Optional[List[str]] = None,
rate_limit_paths: Optional[List[str]] = None,
rate_limiter: Optional[RateLimiter] = None
):
"""
Inițializează middleware-ul
Args:
app: Aplicația FastAPI
excluded_paths: Căile care nu necesită autentificare
rate_limit_paths: Căile cu rate limiting
rate_limiter: Instance de rate limiter personalizat
"""
super().__init__(app)
self.excluded_paths = excluded_paths or [
"/docs", "/redoc", "/openapi.json", "/health", "/",
"/auth/login", "/auth/register"
]
self.rate_limit_paths = rate_limit_paths or [
"/auth/login", "/auth/register", "/auth/forgot-password"
]
self.rate_limiter = rate_limiter or RateLimiter(max_requests=5, time_window=300)
logger.info(f"Authentication middleware initialized with {len(self.excluded_paths)} excluded paths")
def _get_client_ip(self, request: Request) -> str:
"""Obține adresa IP a clientului"""
# Verifică header-ele proxy
forwarded_for = request.headers.get("X-Forwarded-For")
if forwarded_for:
return forwarded_for.split(",")[0].strip()
real_ip = request.headers.get("X-Real-IP")
if real_ip:
return real_ip
# Fallback la client IP direct
return request.client.host if request.client else "unknown"
def _should_exclude_path(self, path: str) -> bool:
"""Verifică dacă path-ul trebuie exclus de la autentificare"""
# Special case for root path to avoid excluding all paths that start with "/"
if "/" in self.excluded_paths and path == "/":
return True
# Check other excluded paths (excluding "/" to avoid matching all paths)
excluded_paths_no_root = [p for p in self.excluded_paths if p != "/"]
return any(path.startswith(excluded) for excluded in excluded_paths_no_root)
def _should_rate_limit_path(self, path: str) -> bool:
"""Verifică dacă path-ul necesită rate limiting"""
return any(path.startswith(limited) for limited in self.rate_limit_paths)
def _extract_token_from_header(self, request: Request) -> Optional[str]:
"""
Extrage token-ul JWT în header-ul Authorization
Args:
request: Request-ul HTTP
Returns:
Token-ul JWT sau None
"""
authorization = request.headers.get("Authorization")
if not authorization:
return None
if not authorization.startswith("Bearer "):
return None
return authorization[7:] # Elimină "Bearer "
async def _create_current_user(self, token_data: TokenData) -> CurrentUser:
"""
Creează obiectul CurrentUser din token data
Args:
token_data: Datele din token
Returns:
Obiectul CurrentUser
"""
return CurrentUser(
username=token_data.username,
user_id=token_data.user_id,
companies=token_data.companies,
permissions=token_data.permissions,
last_login=datetime.now()
)
async def _handle_rate_limiting(self, request: Request, path: str) -> Optional[Response]:
"""
Gestionează rate limiting pentru căile sensibile
Args:
request: Request-ul HTTP
path: Calea request-ului
Returns:
Response cu eroare dacă este rate limited, None altfel
"""
if not self._should_rate_limit_path(path):
return None
client_ip = self._get_client_ip(request)
if not self.rate_limiter.is_allowed(client_ip):
reset_time = self.rate_limiter.get_reset_time(client_ip)
logger.warning(f"Rate limit exceeded for IP {client_ip} on path {path}")
error = AuthError(
error="rate_limit_exceeded",
error_description="Too many requests. Please try again later.",
error_code="RATE_LIMIT_001"
)
return JSONResponse(
status_code=status.HTTP_429_TOO_MANY_REQUESTS,
content=error.dict(),
headers={
"X-RateLimit-Limit": str(self.rate_limiter.max_requests),
"X-RateLimit-Remaining": "0",
"X-RateLimit-Reset": str(reset_time),
"Retry-After": str(reset_time - int(time.time()))
}
)
return None
async def dispatch(self, request: Request, call_next: Callable) -> Response:
"""
Procesează request-ul prin middleware
Args:
request: Request-ul HTTP
call_next: Următorul handler din pipeline
Returns:
Response-ul HTTP
"""
start_time = time.time()
path = request.url.path
# IMPORTANT: Allow OPTIONS requests (CORS preflight) to pass through
if request.method == "OPTIONS":
response = await call_next(request)
return response
# Rate limiting pentru căile sensibile
rate_limit_response = await self._handle_rate_limiting(request, path)
if rate_limit_response:
return rate_limit_response
# Skip autentificare pentru căile excluse
if self._should_exclude_path(path):
request.state.user = None
request.state.is_authenticated = False
response = await call_next(request)
return response
# Extrage token-ul
token = self._extract_token_from_header(request)
if not token:
# Nu există token - pentru endpoint-urile protejate returnează 401
logger.warning(f"No token provided for protected path {path}")
error = AuthError(
error="authentication_required",
error_description="Authentication required",
error_code="AUTH_003"
)
return JSONResponse(
status_code=status.HTTP_401_UNAUTHORIZED,
content=error.dict(),
headers={"WWW-Authenticate": "Bearer"}
)
# Validează token-ul
token_data = jwt_handler.verify_token(token)
if not token_data:
# Token invalid
logger.warning(f"Invalid token used for path {path}")
error = AuthError(
error="invalid_token",
error_description="The provided token is invalid or expired.",
error_code="AUTH_001"
)
return JSONResponse(
status_code=status.HTTP_401_UNAUTHORIZED,
content=error.dict(),
headers={"WWW-Authenticate": "Bearer"}
)
# Token valid - creează utilizatorul curent
try:
current_user = await self._create_current_user(token_data)
request.state.user = current_user
request.state.is_authenticated = True
request.state.token_data = token_data
# Extrage server_id din token pentru a fi folosit în query-uri Oracle
request.state.server_id = token_data.server_id
logger.debug(f"User {current_user.username} authenticated successfully for path {path} (server: {token_data.server_id or 'default'})")
except Exception as e:
logger.error(f"Error creating current user: {str(e)}")
error = AuthError(
error="authentication_error",
error_description="Authentication processing error.",
error_code="AUTH_002"
)
return JSONResponse(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
content=error.dict()
)
# Procesează request-ul
response = await call_next(request)
# Adaugă header-e de securitate
response.headers["X-Content-Type-Options"] = "nosniff"
response.headers["X-Frame-Options"] = "DENY"
response.headers["X-XSS-Protection"] = "1; mode=block"
# Log timpul de procesare
process_time = time.time() - start_time
response.headers["X-Process-Time"] = str(process_time)
return response
class HTTPBearerOptional(HTTPBearer):
"""
Versiune opțională pentru autentificare care nu aruncă excepții
dacă token-ul lipsește - utile pentru endpoint-urile care
pot funcționa atât cu cât și fără autentificare
"""
async def __call__(self, request: Request) -> Optional[HTTPAuthorizationCredentials]:
"""
Extrage credențialele de autentificare fără să arunce excepții
Args:
request: Request-ul HTTP
Returns:
Credențialele sau None
"""
try:
return await super().__call__(request)
except HTTPException:
return None
# Instance predefinite pentru folosire rapidă
security_optional = HTTPBearerOptional(auto_error=False)
security_required = HTTPBearer()
# Rate limiter default
default_rate_limiter = RateLimiter(max_requests=5, time_window=300)

View File

@@ -0,0 +1,344 @@
"""
Authentication Pydantic Models pentru ROA2WEB
Acest modul definește toate modelele de date folosite în sistemul de autentificare,
incluzând request/response models și modele pentru user data.
Modelele acoperă:
- Login request și response
- Token data și management
- User information și permisiuni
- Company access control
"""
from pydantic import BaseModel, Field, validator, EmailStr
from typing import List, Optional, Dict, Any
from datetime import datetime
from enum import Enum
class PermissionType(str, Enum):
"""Tipurile de permisiuni disponibile în sistem"""
READ = "read"
WRITE = "write"
DELETE = "delete"
ADMIN = "admin"
REPORTS = "reports"
EXPORT = "export"
class TokenType(str, Enum):
"""Tipurile de token-uri JWT"""
ACCESS = "access"
REFRESH = "refresh"
class LoginRequest(BaseModel):
"""Model pentru request-ul de login"""
username: str = Field(
...,
min_length=3,
max_length=50,
description="Numele utilizatorului",
example="admin"
)
password: str = Field(
...,
min_length=1,
description="Parola utilizatorului"
)
remember_me: bool = Field(
default=False,
description="Dacă să păstreze utilizatorul autentificat mai mult timp"
)
server_id: Optional[str] = Field(
default=None,
description="ID-ul serverului Oracle pentru autentificare (opțional în modul single-server)",
example="romfast"
)
@validator('username')
def username_alphanumeric(cls, v):
"""Validează că username-ul conține doar caractere permise (inclusiv email-uri)
Pentru backward compatibility:
- Permite username-uri clasice: litere, cifre, spații, _, -
- Permite email-uri pentru noul flow multi-server: @, .
"""
# Permitem litere, cifre, spații, _, -, @, și . (pentru email-uri)
allowed_chars = v.replace(' ', '').replace('_', '').replace('-', '').replace('@', '').replace('.', '')
if not allowed_chars.isalnum():
raise ValueError('Username-ul poate conține doar litere, cifre, spații, _, -, @ și .')
# Detectăm dacă este email sau username clasic
if '@' in v:
# Email: păstrăm lowercase pentru consistență cu email-urile
return v.lower().strip()
else:
# Username clasic: uppercase pentru consistență cu Oracle
return v.upper().strip()
class TokenResponse(BaseModel):
"""Model pentru răspunsul de autentificare cu token-uri"""
access_token: str = Field(description="JWT access token")
refresh_token: Optional[str] = Field(
default=None,
description="JWT refresh token (opțional)"
)
token_type: str = Field(
default="bearer",
description="Tipul token-ului (întotdeauna 'bearer')"
)
expires_in: int = Field(
description="Timpul de expirare al access token-ului în secunde"
)
user: 'CurrentUser' = Field(description="Informațiile utilizatorului autentificat")
class RefreshTokenRequest(BaseModel):
"""Model pentru request-ul de refresh token"""
refresh_token: str = Field(description="Refresh token-ul valid")
class LogoutRequest(BaseModel):
"""Model pentru request-ul de logout"""
refresh_token: Optional[str] = Field(
default=None,
description="Refresh token de invalidat (opțional)"
)
class CurrentUser(BaseModel):
"""Model pentru utilizatorul curent autentificat"""
username: str = Field(description="Numele utilizatorului")
user_id: Optional[int] = Field(
default=None,
description="ID-ul utilizatorului în baza de date"
)
email: Optional[EmailStr] = Field(
default=None,
description="Email-ul utilizatorului"
)
companies: List[str] = Field(
default_factory=list,
description="Lista codurilor firmelor la care utilizatorul are acces"
)
permissions: List[PermissionType] = Field(
default_factory=lambda: [PermissionType.READ],
description="Lista permisiunilor utilizatorului"
)
is_active: bool = Field(
default=True,
description="Dacă utilizatorul este activ"
)
last_login: Optional[datetime] = Field(
default=None,
description="Data ultimei autentificări"
)
@validator('companies')
def companies_not_empty_if_active(cls, v, values):
"""Validează că utilizatorii activi au cel puțin o firmă"""
if values.get('is_active', True) and not v:
raise ValueError('Utilizatorii activi trebuie să aibă acces la cel puțin o firmă')
return v
class UserCompany(BaseModel):
"""Model pentru o firmă la care utilizatorul are acces"""
code: str = Field(description="Codul firmei (schema Oracle)")
name: Optional[str] = Field(
default=None,
description="Numele firmei (dacă este disponibil)"
)
permissions: List[PermissionType] = Field(
default_factory=lambda: [PermissionType.READ],
description="Permisiunile utilizatorului pentru această firmă"
)
is_default: bool = Field(
default=False,
description="Dacă aceasta este firma implicită pentru utilizator"
)
class CompanyAccessRequest(BaseModel):
"""Model pentru verificarea accesului la o firmă"""
company_code: str = Field(description="Codul firmei de verificat")
required_permissions: Optional[List[PermissionType]] = Field(
default=None,
description="Permisiunile necesare (opțional)"
)
class CompanyAccessResponse(BaseModel):
"""Model pentru răspunsul de verificare acces firmă"""
has_access: bool = Field(description="Dacă utilizatorul are acces")
company: Optional[UserCompany] = Field(
default=None,
description="Detaliile firmei dacă utilizatorul are acces"
)
missing_permissions: Optional[List[PermissionType]] = Field(
default=None,
description="Permisiunile lipsă (dacă aplicabil)"
)
class AuthError(BaseModel):
"""Model pentru erorile de autentificare"""
error: str = Field(description="Tipul erorii")
error_description: str = Field(description="Descrierea detaliată a erorii")
error_code: Optional[str] = Field(
default=None,
description="Codul de eroare pentru procesare automată"
)
class AuthStats(BaseModel):
"""Model pentru statisticile de autentificare"""
total_users: int = Field(description="Numărul total de utilizatori")
active_sessions: int = Field(description="Sesiuni active curente")
cache_hit_ratio: float = Field(
description="Rata de hit a cache-ului pentru date utilizatori"
)
last_cleanup: Optional[datetime] = Field(
default=None,
description="Ultima curățare a cache-ului"
)
class PasswordChangeRequest(BaseModel):
"""Model pentru schimbarea parolei (pentru viitor)"""
current_password: str = Field(description="Parola curentă")
new_password: str = Field(
min_length=8,
description="Noua parolă (minim 8 caractere)"
)
confirm_password: str = Field(description="Confirmarea noii parole")
@validator('confirm_password')
def passwords_match(cls, v, values):
"""Validează că parolele coincid"""
if 'new_password' in values and v != values['new_password']:
raise ValueError('Parolele nu coincid')
return v
class SessionInfo(BaseModel):
"""Model pentru informațiile despre sesiune"""
session_id: str = Field(description="ID-ul sesiunii")
username: str = Field(description="Numele utilizatorului")
created_at: datetime = Field(description="Data creării sesiunii")
last_activity: datetime = Field(description="Ultima activitate")
ip_address: Optional[str] = Field(
default=None,
description="Adresa IP a utilizatorului"
)
user_agent: Optional[str] = Field(
default=None,
description="User agent-ul browserului"
)
is_active: bool = Field(
default=True,
description="Dacă sesiunea este încă activă"
)
# ============================================================================
# MULTI-ORACLE IDENTITY CHECK MODELS (US-004, US-013)
# ============================================================================
class CheckIdentityRequest(BaseModel):
"""
Model pentru verificarea identității în sistemul multi-Oracle (US-013)
Suportă atât email cât și username:
- Cu '@': tratează ca email și caută în EmailServerCache
- Fără '@': tratează ca username și caută în Oracle pe toate serverele
"""
identity: str = Field(
...,
min_length=2,
max_length=100,
description="Email sau username de verificat",
example="user@example.com sau MARIUS"
)
@validator('identity')
def validate_identity(cls, v):
"""Validează și normalizează identitatea"""
stripped = v.strip()
if not stripped:
raise ValueError('Identitatea nu poate fi goală')
# Pentru email-uri, normalizăm la lowercase
if '@' in stripped:
return stripped.lower()
# Pentru username-uri, normalizăm la uppercase (convenție Oracle)
return stripped.upper()
class CheckEmailRequest(BaseModel):
"""
Model pentru verificarea email-ului în sistemul multi-Oracle (US-004)
DEPRECATED: Folosește CheckIdentityRequest pentru suport dual email/username
Păstrat pentru backward compatibility.
"""
email: EmailStr = Field(
...,
description="Adresa email a utilizatorului de verificat",
example="user@example.com"
)
class ServerInfo(BaseModel):
"""Informații despre un server Oracle disponibil pentru utilizator"""
id: str = Field(description="ID-ul serverului (ex: 'romfast')")
name: str = Field(description="Numele human-readable al serverului (ex: 'Romfast - Producție')")
class CheckIdentityResponse(BaseModel):
"""
Răspunsul pentru verificarea identității (email sau username) (US-013).
SECURITATE:
- Pentru identitate validă: returnează exists=True și lista serverelor
- Pentru identitate invalidă: returnează exists=False și listă goală de servere
(NU expunem serverele disponibile pentru a preveni enumerarea!)
"""
exists: bool = Field(
description="True dacă identitatea există în sistem pe cel puțin un server"
)
servers: List[ServerInfo] = Field(
default_factory=list,
description="Lista serverelor pe care există identitatea (goală pentru identitate invalidă)"
)
identity_type: str = Field(
default="unknown",
description="Tipul identității: 'email' sau 'username'"
)
class CheckEmailResponse(BaseModel):
"""
Răspunsul pentru verificarea email-ului (US-004).
DEPRECATED: Folosește CheckIdentityResponse pentru suport dual email/username
Păstrat pentru backward compatibility.
SECURITATE:
- Pentru email valid: returnează exists=True și lista serverelor
- Pentru email invalid: returnează exists=False și listă goală de servere
(NU expunem serverele disponibile pentru a preveni enumerarea!)
"""
exists: bool = Field(
description="True dacă email-ul există în sistem pe cel puțin un server"
)
servers: List[ServerInfo] = Field(
default_factory=list,
description="Lista serverelor pe care există email-ul (goală pentru email invalid)"
)
# Update la forward references pentru TokenResponse
TokenResponse.model_rebuild()

View File

@@ -0,0 +1,681 @@
"""
Authentication Routes Template pentru ROA2WEB FastAPI Applications
Acest modul oferă rute predefinite pentru autentificare care pot fi integrate
în orice aplicație FastAPI din ecosistemul ROA2WEB.
Endpoints disponibile:
- POST /auth/login - Autentificare utilizator
- POST /auth/refresh - Refresh access token
- POST /auth/logout - Deconectare utilizator
- GET /auth/me - Informații utilizator curent
- GET /auth/companies - Firmele utilizatorului
- GET /auth/status - Status autentificare
"""
import logging
from typing import List, Optional
from datetime import datetime
from fastapi import APIRouter, Depends, HTTPException, status, Request, Response
from fastapi.security import HTTPAuthorizationCredentials
from .models import (
LoginRequest, TokenResponse, RefreshTokenRequest, LogoutRequest,
CurrentUser, UserCompany, CompanyAccessRequest, CompanyAccessResponse,
AuthError, AuthStats, CheckEmailRequest, CheckEmailResponse, ServerInfo,
CheckIdentityRequest, CheckIdentityResponse
)
from .auth_service import auth_service, AuthenticationError
from .jwt_handler import jwt_handler
from .dependencies import (
get_current_user, get_optional_user,
security_required, security_optional
)
from .middleware import default_rate_limiter, RateLimiter
logger = logging.getLogger(__name__)
def create_auth_router(
prefix: str = "/auth",
tags: Optional[List[str]] = None,
include_admin_routes: bool = False
) -> APIRouter:
"""
Creează un router FastAPI cu toate rutele de autentificare
Args:
prefix: Prefix-ul pentru toate rutele
tags: Tag-urile pentru documentația OpenAPI
include_admin_routes: Dacă să includă rutele de administrare
Returns:
Router-ul FastAPI configurat
"""
router = APIRouter(prefix=prefix, tags=tags or ["authentication"])
# Rate limiter pentru check-identity/check-email: 5 requests per minut per IP
check_identity_rate_limiter = RateLimiter(max_requests=5, time_window=60)
@router.post("/check-identity", response_model=CheckIdentityResponse, status_code=status.HTTP_200_OK)
async def check_identity(
check_data: CheckIdentityRequest,
request: Request
) -> CheckIdentityResponse:
"""
Verifică dacă un email sau username există în sistem și pe câte servere Oracle (US-013).
Acest endpoint suportă dual login:
- Input cu '@': tratează ca email și caută în EmailServerCache
- Input fără '@': tratează ca username și caută direct în Oracle
SECURITATE:
- Rate limited: max 5 requests/minut per IP
- NU expune serverele disponibile pentru identități invalide
- Identități invalide returnează {exists: false, servers: []}
Args:
check_data: Identitatea de verificat (email sau username)
request: Request-ul HTTP (pentru rate limiting)
Returns:
CheckIdentityResponse cu exists, servers[] și identity_type
Raises:
HTTPException 429: Rate limit exceeded
"""
# Rate limiting - 5 req/min per IP
client_ip = request.client.host if request.client else "unknown"
if not check_identity_rate_limiter.is_allowed(client_ip):
reset_time = check_identity_rate_limiter.get_reset_time(client_ip)
logger.warning(f"Rate limit exceeded for check-identity from IP {client_ip}")
raise HTTPException(
status_code=status.HTTP_429_TOO_MANY_REQUESTS,
detail="Too many requests. Please try again later.",
headers={
"X-RateLimit-Limit": "5",
"X-RateLimit-Remaining": "0",
"X-RateLimit-Reset": str(reset_time),
"Retry-After": str(max(1, reset_time - int(__import__('time').time())))
}
)
try:
from .email_server_cache import email_server_cache
from backend.config import settings
identity = check_data.identity # Already normalized by validator
is_email = '@' in identity
identity_type = "email" if is_email else "username"
logger.info(f"Check-identity request for '{identity}' (type: {identity_type}) from IP {client_ip}")
# Get server IDs based on identity type
if is_email:
# Email lookup from cache
server_ids = email_server_cache.get_servers_for_email(identity)
else:
# Username lookup directly from Oracle (async)
server_ids = await email_server_cache.get_servers_for_username(identity)
if not server_ids:
# Identity not found - return empty response (don't expose available servers!)
logger.info(f"Identity '{identity}' not found in any server")
return CheckIdentityResponse(exists=False, servers=[], identity_type=identity_type)
# Build server info list with human-readable names
servers: List[ServerInfo] = []
for server_id in server_ids:
server_config = settings.get_oracle_server(server_id)
if server_config:
servers.append(ServerInfo(
id=server_config.id,
name=server_config.name
))
else:
# Fallback if server config not found (shouldn't happen)
logger.warning(f"Server '{server_id}' not found in config")
servers.append(ServerInfo(id=server_id, name=server_id))
logger.info(f"Identity '{identity}' found on {len(servers)} server(s): {[s.id for s in servers]}")
return CheckIdentityResponse(exists=True, servers=servers, identity_type=identity_type)
except Exception as e:
logger.error(f"Error checking identity '{check_data.identity}': {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Error checking identity"
)
@router.post("/check-email", response_model=CheckEmailResponse, status_code=status.HTTP_200_OK)
async def check_email(
check_data: CheckEmailRequest,
request: Request
) -> CheckEmailResponse:
"""
Verifică dacă un email există în sistem și pe câte servere Oracle.
DEPRECATED: Folosește /check-identity pentru suport dual email/username.
Păstrat pentru backward compatibility.
Args:
check_data: Email-ul de verificat
request: Request-ul HTTP (pentru rate limiting)
Returns:
CheckEmailResponse cu exists și servers[]
"""
# Rate limiting - shared with check-identity
client_ip = request.client.host if request.client else "unknown"
if not check_identity_rate_limiter.is_allowed(client_ip):
reset_time = check_identity_rate_limiter.get_reset_time(client_ip)
logger.warning(f"Rate limit exceeded for check-email from IP {client_ip}")
raise HTTPException(
status_code=status.HTTP_429_TOO_MANY_REQUESTS,
detail="Too many requests. Please try again later.",
headers={
"X-RateLimit-Limit": "5",
"X-RateLimit-Remaining": "0",
"X-RateLimit-Reset": str(reset_time),
"Retry-After": str(max(1, reset_time - int(__import__('time').time())))
}
)
try:
from .email_server_cache import email_server_cache
from backend.config import settings
email = check_data.email.lower().strip()
logger.info(f"Check-email request for '{email}' from IP {client_ip}")
# Get server IDs from cache
server_ids = email_server_cache.get_servers_for_email(email)
if not server_ids:
# Email not found - return empty response (don't expose available servers!)
logger.info(f"Email '{email}' not found in any server")
return CheckEmailResponse(exists=False, servers=[])
# Build server info list with human-readable names
servers: List[ServerInfo] = []
for server_id in server_ids:
server_config = settings.get_oracle_server(server_id)
if server_config:
servers.append(ServerInfo(
id=server_config.id,
name=server_config.name
))
else:
# Fallback if server config not found (shouldn't happen)
logger.warning(f"Server '{server_id}' not found in config")
servers.append(ServerInfo(id=server_id, name=server_id))
logger.info(f"Email '{email}' found on {len(servers)} server(s): {[s.id for s in servers]}")
return CheckEmailResponse(exists=True, servers=servers)
except Exception as e:
logger.error(f"Error checking email '{check_data.email}': {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Error checking email"
)
@router.post("/login", response_model=TokenResponse, status_code=status.HTTP_200_OK)
async def login(
login_data: LoginRequest,
request: Request,
response: Response
) -> TokenResponse:
"""
Autentifică un utilizator și returnează token-urile JWT
Acest endpoint:
- Validează credențialele utilizatorului în Oracle
- Obține firmele la care utilizatorul are acces
- Generează access și refresh token-uri JWT
- Aplică rate limiting pentru securitate
- Suportă modul multi-server (server_id opțional)
Args:
login_data: Datele de autentificare (username, password, server_id opțional)
request: Request-ul HTTP (pentru rate limiting)
response: Response-ul HTTP (pentru header-e)
Returns:
Token-urile JWT și informațiile utilizatorului
Raises:
HTTPException 400: Pentru server_id invalid
HTTPException 401: Pentru credențiale invalide
HTTPException 500: Pentru erori de sistem
"""
try:
# Log tentativa de autentificare
client_ip = request.client.host if request.client else "unknown"
server_info = f" on server {login_data.server_id}" if login_data.server_id else ""
logger.info(f"Login attempt for user {login_data.username}{server_info} from IP {client_ip}")
# Validare server_id dacă specificat (multi-server mode)
if login_data.server_id:
from backend.config import settings
from shared.database.oracle_pool import oracle_pool
# Verifică dacă serverul există în configurație
server_config = settings.get_oracle_server(login_data.server_id)
if not server_config:
logger.warning(f"Invalid server_id '{login_data.server_id}' in login request")
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Invalid server_id: '{login_data.server_id}'. Server not found in configuration."
)
# Verifică dacă serverul este înregistrat în pool
if not oracle_pool.is_server_registered(login_data.server_id):
logger.warning(f"Server '{login_data.server_id}' not registered in pool")
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Server '{login_data.server_id}' is not available."
)
# Autentifică și creează token-urile
success, token_response, error_message = await auth_service.authenticate_and_create_tokens(
login_data.username,
login_data.password,
login_data.server_id
)
if not success:
logger.warning(f"Failed login attempt for user {login_data.username}{server_info}: {error_message}")
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=error_message or "Authentication failed"
)
# token_response.user este deja populat corect de auth_service.authenticate_and_create_tokens
# cu username-ul Oracle rezolvat (nu email-ul) și lista de firme
# Header-e de securitate
response.headers["X-Content-Type-Options"] = "nosniff"
response.headers["X-Frame-Options"] = "DENY"
logger.info(f"Successful login for user {login_data.username}{server_info}")
return token_response
except HTTPException:
# Re-raise HTTP exceptions as-is (e.g., 401 for invalid credentials)
raise
except AuthenticationError as e:
logger.error(f"Authentication error for user {login_data.username}: {str(e)}")
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=str(e)
)
except Exception as e:
logger.error(f"Unexpected error during login for user {login_data.username}: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Internal authentication error"
)
@router.post("/refresh", response_model=TokenResponse, status_code=status.HTTP_200_OK)
async def refresh_token(refresh_data: RefreshTokenRequest) -> TokenResponse:
"""
Reîmprospătează access token-ul folosind refresh token-ul
Args:
refresh_data: Refresh token-ul valid
Returns:
Noul access token și informațiile utilizatorului
Raises:
HTTPException: Pentru refresh token-uri invalide
"""
try:
# Validează refresh token-ul
token_data = jwt_handler.verify_token(refresh_data.refresh_token)
if not token_data or token_data.token_type != "refresh":
logger.warning("Invalid refresh token provided")
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid refresh token"
)
# Obține datele actualizate ale utilizatorului
companies = await auth_service.get_user_companies(token_data.username)
permissions = ["read", "reports"] # Poate fi extins în viitor
# Creează noul access token
new_access_token = jwt_handler.create_access_token(
username=token_data.username,
companies=companies,
user_id=token_data.user_id,
permissions=permissions
)
# Informațiile utilizatorului
current_user = CurrentUser(
username=token_data.username,
user_id=token_data.user_id,
companies=companies,
permissions=permissions
)
token_response = TokenResponse(
access_token=new_access_token,
token_type="bearer",
expires_in=jwt_handler.access_token_expire_minutes * 60,
user=current_user
)
logger.info(f"Token refreshed for user {token_data.username}")
return token_response
except Exception as e:
logger.error(f"Error refreshing token: {str(e)}")
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Token refresh failed"
)
@router.post("/logout", status_code=status.HTTP_200_OK)
async def logout(
logout_data: Optional[LogoutRequest] = None,
current_user: CurrentUser = Depends(get_current_user)
) -> dict:
"""
Deconectează utilizatorul (invalidează token-urile)
Note: În implementarea curentă, token-urile JWT sunt stateless,
deci nu pot fi invalidate direct. În viitor poate fi implementat
un blacklist pentru token-uri.
Args:
logout_data: Date pentru logout (opțional)
current_user: Utilizatorul curent autentificat
Returns:
Confirmarea deconectării
"""
logger.info(f"User {current_user.username} logged out")
# În viitor, aici se poate implementa:
# - Adăugarea token-ului într-un blacklist
# - Invalidarea tuturor sesiunilor utilizatorului
# - Notificări de securitate
return {
"message": "Successfully logged out",
"username": current_user.username,
"logout_time": datetime.now().isoformat()
}
@router.get("/me", response_model=CurrentUser)
async def get_current_user_info(
current_user: CurrentUser = Depends(get_current_user)
) -> CurrentUser:
"""
Returnează informațiile despre utilizatorul curent
Args:
current_user: Utilizatorul curent autentificat
Returns:
Informațiile complete ale utilizatorului
"""
logger.debug(f"User info requested for {current_user.username}")
return current_user
@router.get("/companies", response_model=List[UserCompany])
async def get_user_companies(
current_user: CurrentUser = Depends(get_current_user)
) -> List[UserCompany]:
"""
Returnează lista firmelor la care utilizatorul are acces
Args:
current_user: Utilizatorul curent autentificat
Returns:
Lista firmelor cu permisiunile asociate
"""
try:
# Obține firmele actualizate din baza de date
companies = await auth_service.get_user_companies(current_user.username)
user_companies = []
for i, company_code in enumerate(companies):
# Obține permisiunile pentru fiecare firmă
permissions = await auth_service.get_user_permissions(
current_user.username,
company_code
)
user_company = UserCompany(
code=company_code,
permissions=permissions,
is_default=(i == 0) # Prima firmă ca default
)
user_companies.append(user_company)
logger.debug(f"Returned {len(user_companies)} companies for user {current_user.username}")
return user_companies
except Exception as e:
logger.error(f"Error getting companies for user {current_user.username}: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Error retrieving user companies"
)
@router.post("/check-company-access", response_model=CompanyAccessResponse)
async def check_company_access(
access_request: CompanyAccessRequest,
current_user: CurrentUser = Depends(get_current_user)
) -> CompanyAccessResponse:
"""
Verifică dacă utilizatorul are acces la o firmă specifică
Args:
access_request: Request-ul de verificare acces
current_user: Utilizatorul curent autentificat
Returns:
Răspunsul cu informații despre acces
"""
try:
has_access = await auth_service.validate_user_company_access(
current_user.username,
access_request.company_code
)
if not has_access:
return CompanyAccessResponse(
has_access=False,
company=None,
missing_permissions=None
)
# Obține permisiunile pentru firmă
permissions = await auth_service.get_user_permissions(
current_user.username,
access_request.company_code
)
# Verifică permisiunile cerute
missing_permissions = []
if access_request.required_permissions:
missing_permissions = [
perm for perm in access_request.required_permissions
if perm not in permissions
]
user_company = UserCompany(
code=access_request.company_code,
permissions=permissions
)
return CompanyAccessResponse(
has_access=True,
company=user_company,
missing_permissions=missing_permissions if missing_permissions else None
)
except Exception as e:
logger.error(f"Error checking company access: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Error checking company access"
)
@router.get("/my-servers", response_model=dict)
async def get_my_servers(
current_user: CurrentUser = Depends(get_current_user)
) -> dict:
"""
Returnează lista serverelor la care utilizatorul autentificat are acces (US-006).
Acest endpoint este folosit de frontend pentru a popula dropdown-ul de server switch.
Lookup-ul se face pe baza email-ului sau username-ului utilizatorului curent.
Args:
current_user: Utilizatorul curent autentificat
Returns:
Dict cu lista de servere: {servers: [{id: string, name: string}, ...]}
"""
try:
from .email_server_cache import email_server_cache
from backend.config import settings
logger.info(f"Get my-servers request for user '{current_user.username}'")
# Try email lookup first (faster, from cache)
server_ids: List[str] = []
if current_user.email:
server_ids = email_server_cache.get_servers_for_email(current_user.email)
logger.debug(f"Email lookup for '{current_user.email}': {server_ids}")
# If no email or no results, try username lookup (queries Oracle directly)
if not server_ids:
server_ids = await email_server_cache.get_servers_for_username(current_user.username)
logger.debug(f"Username lookup for '{current_user.username}': {server_ids}")
# Build server info list with human-readable names
servers: List[ServerInfo] = []
for server_id in server_ids:
server_config = settings.get_oracle_server(server_id)
if server_config:
servers.append(ServerInfo(
id=server_config.id,
name=server_config.name
))
else:
# Fallback if server config not found
logger.warning(f"Server '{server_id}' not found in config")
servers.append(ServerInfo(id=server_id, name=server_id))
logger.info(f"User '{current_user.username}' has access to {len(servers)} server(s)")
return {"servers": [s.model_dump() for s in servers]}
except Exception as e:
logger.error(f"Error getting servers for user '{current_user.username}': {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Error retrieving user servers"
)
@router.get("/status")
async def get_auth_status(
current_user: Optional[CurrentUser] = Depends(get_optional_user)
) -> dict:
"""
Returnează statusul de autentificare (endpoint public)
Args:
current_user: Utilizatorul curent (opțional)
Returns:
Statusul de autentificare
"""
if current_user:
return {
"authenticated": True,
"username": current_user.username,
"companies_count": len(current_user.companies),
"permissions": current_user.permissions
}
else:
return {
"authenticated": False,
"username": None,
"companies_count": 0,
"permissions": []
}
# Rute de administrare (opționale)
if include_admin_routes:
@router.get("/admin/stats", response_model=AuthStats)
async def get_auth_stats(
current_user: CurrentUser = Depends(get_current_user)
) -> AuthStats:
"""
Returnează statistici despre sistemul de autentificare
Necesită permisiuni de admin.
"""
# Verifică permisiuni admin
if "admin" not in current_user.permissions:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Admin permissions required"
)
cache_stats = auth_service.get_cache_stats()
return AuthStats(
total_users=1, # Placeholder - poate fi implementat
active_sessions=1, # Placeholder - poate fi implementat
cache_hit_ratio=cache_stats.get('cache_hit_ratio', 0),
last_cleanup=datetime.now()
)
@router.post("/admin/refresh-cache")
async def refresh_user_cache(
username: Optional[str] = None,
current_user: CurrentUser = Depends(get_current_user)
) -> dict:
"""
Reîmprospătează cache-ul utilizatorilor
Necesită permisiuni de admin.
"""
if "admin" not in current_user.permissions:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Admin permissions required"
)
if username:
success = await auth_service.refresh_user_data(username)
return {
"message": f"Cache refreshed for user {username}",
"success": success
}
else:
auth_service.clear_cache()
return {"message": "All user cache cleared"}
return router
# Router implicit pentru folosire rapidă
auth_router = create_auth_router()
# Router cu rute de admin incluse
auth_router_with_admin = create_auth_router(include_admin_routes=True)

View File

@@ -0,0 +1,559 @@
"""
Comprehensive Authentication Tests pentru ROA2WEB
Acest modul conține teste pentru toate componentele sistemului de autentificare:
- JWT Handler functionality
- Oracle authentication service
- FastAPI dependencies și middleware
- Rate limiting și security features
Testele acoperă:
- Unit tests pentru funcționalitatea de bază
- Integration tests cu Oracle database (mock)
- Security tests pentru vulnerabilități comune
- Performance tests pentru scalabilitate
"""
import pytest
import asyncio
import os
import time
from datetime import datetime, timedelta
from unittest.mock import Mock, AsyncMock, patch, MagicMock
from typing import List, Dict, Any, Optional
import jwt as pyjwt
from fastapi import FastAPI, HTTPException, status
from fastapi.testclient import TestClient
from httpx import AsyncClient
# Import modulele de testat
from .jwt_handler import JWTHandler, TokenData, TokenResponse
from .auth_service import UserAuthService, AuthenticationError
from .models import (
LoginRequest, CurrentUser, PermissionType,
CompanyAccessRequest, CompanyAccessResponse
)
from .middleware import AuthenticationMiddleware, RateLimiter
from .dependencies import (
get_current_user_from_token, require_company_access,
require_permissions, get_current_company_from_header
)
from .routes import create_auth_router
class TestJWTHandler:
"""Test suite pentru JWT Handler"""
@pytest.fixture
def jwt_handler(self):
"""Fixture pentru JWT handler cu configurare de test"""
return JWTHandler(
secret_key="test-secret-key",
algorithm="HS256"
)
def test_create_access_token(self, jwt_handler):
"""Test pentru crearea access token-urilor"""
username = "testuser"
companies = ["COMP1", "COMP2"]
permissions = ["read", "write"]
token = jwt_handler.create_access_token(
username=username,
companies=companies,
user_id=123,
permissions=permissions
)
assert isinstance(token, str)
assert len(token) > 0
# Verifică că token-ul poate fi decodat
payload = pyjwt.decode(token, "test-secret-key", algorithms=["HS256"])
assert payload["username"] == username
assert payload["companies"] == companies
assert payload["permissions"] == permissions
assert payload["user_id"] == 123
assert payload["type"] == "access"
def test_create_refresh_token(self, jwt_handler):
"""Test pentru crearea refresh token-urilor"""
username = "testuser"
user_id = 123
token = jwt_handler.create_refresh_token(username, user_id)
assert isinstance(token, str)
assert len(token) > 0
# Verifică payload-ul
payload = pyjwt.decode(token, "test-secret-key", algorithms=["HS256"])
assert payload["username"] == username
assert payload["user_id"] == user_id
assert payload["type"] == "refresh"
def test_verify_valid_token(self, jwt_handler):
"""Test pentru verificarea token-urilor valide"""
username = "testuser"
companies = ["COMP1"]
token = jwt_handler.create_access_token(username, companies)
token_data = jwt_handler.verify_token(token)
assert token_data is not None
assert isinstance(token_data, TokenData)
assert token_data.username == username
assert token_data.companies == companies
assert token_data.token_type == "access"
def test_verify_expired_token(self, jwt_handler):
"""Test pentru token-uri expirate"""
# Creează token cu expirare în trecut
past_time = datetime.utcnow() - timedelta(minutes=10)
payload = {
"username": "testuser",
"companies": ["COMP1"],
"permissions": ["read"],
"exp": past_time,
"iat": past_time - timedelta(minutes=5),
"type": "access"
}
expired_token = pyjwt.encode(payload, "test-secret-key", algorithm="HS256")
token_data = jwt_handler.verify_token(expired_token)
assert token_data is None
def test_verify_invalid_token(self, jwt_handler):
"""Test pentru token-uri invalide"""
invalid_tokens = [
"invalid.token.here",
"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.invalid",
"",
None
]
for invalid_token in invalid_tokens:
if invalid_token is not None:
token_data = jwt_handler.verify_token(invalid_token)
assert token_data is None
def test_create_token_response(self, jwt_handler):
"""Test pentru crearea răspunsului complet cu token-uri"""
username = "testuser"
companies = ["COMP1", "COMP2"]
permissions = ["read", "reports"]
response = jwt_handler.create_token_response(
username=username,
companies=companies,
permissions=permissions,
include_refresh=True
)
assert isinstance(response, TokenResponse)
assert response.access_token is not None
assert response.refresh_token is not None
assert response.token_type == "bearer"
assert response.expires_in > 0
def test_refresh_access_token(self, jwt_handler):
"""Test pentru refresh-ul access token-urilor"""
username = "testuser"
refresh_token = jwt_handler.create_refresh_token(username, 123)
companies = ["COMP1", "COMP2"]
new_access_token = jwt_handler.refresh_access_token(
refresh_token, companies, ["read", "write"]
)
assert new_access_token is not None
# Verifică noul token
token_data = jwt_handler.verify_token(new_access_token)
assert token_data.username == username
assert token_data.companies == companies
assert token_data.token_type == "access"
class TestUserAuthService:
"""Test suite pentru User Authentication Service"""
@pytest.fixture
def auth_service(self):
"""Fixture pentru auth service cu mock database"""
return UserAuthService()
@pytest.fixture
def mock_oracle_pool(self):
"""Mock pentru Oracle connection pool"""
with patch('roa2web.shared.auth.auth_service.oracle_pool') as mock_pool:
yield mock_pool
@pytest.mark.asyncio
async def test_verify_user_credentials_success(self, auth_service, mock_oracle_pool):
"""Test pentru verificarea cu succes a credențialelor"""
# Mock pentru conexiunea Oracle
mock_connection = AsyncMock()
mock_cursor = MagicMock()
mock_cursor.fetchone.return_value = [1] # Success
mock_connection.__aenter__.return_value = mock_connection
mock_connection.cursor.return_value.__enter__.return_value = mock_cursor
mock_oracle_pool.get_connection.return_value = mock_connection
result = await auth_service.verify_user_credentials("testuser", "password")
assert result is True
mock_cursor.execute.assert_called_once()
@pytest.mark.asyncio
async def test_verify_user_credentials_failure(self, auth_service, mock_oracle_pool):
"""Test pentru verificarea eșuată a credențialelor"""
# Mock pentru conexiunea Oracle
mock_connection = AsyncMock()
mock_cursor = MagicMock()
mock_cursor.fetchone.return_value = [0] # Failure
mock_connection.__aenter__.return_value = mock_connection
mock_connection.cursor.return_value.__enter__.return_value = mock_cursor
mock_oracle_pool.get_connection.return_value = mock_connection
result = await auth_service.verify_user_credentials("testuser", "wrongpassword")
assert result is False
@pytest.mark.asyncio
async def test_get_user_companies(self, auth_service, mock_oracle_pool):
"""Test pentru obținerea firmelor utilizatorului"""
# Mock pentru conexiunea Oracle
mock_connection = AsyncMock()
mock_cursor = MagicMock()
mock_cursor.fetchall.return_value = [["COMP1"], ["COMP2"], ["COMP3"]]
mock_connection.__aenter__.return_value = mock_connection
mock_connection.cursor.return_value.__enter__.return_value = mock_cursor
mock_oracle_pool.get_connection.return_value = mock_connection
companies = await auth_service.get_user_companies("testuser")
assert companies == ["COMP1", "COMP2", "COMP3"]
mock_cursor.execute.assert_called_once()
@pytest.mark.asyncio
async def test_authenticate_and_create_tokens_success(self, auth_service, mock_oracle_pool):
"""Test pentru autentificare completă cu succes"""
# Mock pentru conexiunea Oracle
mock_connection = AsyncMock()
mock_cursor = MagicMock()
# Prima chiamată pentru verificare credențiale (succes)
# A doua chiamată pentru obținerea firmelor
mock_cursor.fetchone.return_value = [1]
mock_cursor.fetchall.return_value = [["COMP1"], ["COMP2"]]
mock_connection.__aenter__.return_value = mock_connection
mock_connection.cursor.return_value.__enter__.return_value = mock_cursor
mock_oracle_pool.get_connection.return_value = mock_connection
success, token_response, error = await auth_service.authenticate_and_create_tokens(
"testuser", "password"
)
assert success is True
assert token_response is not None
assert isinstance(token_response, TokenResponse)
assert error is None
@pytest.mark.asyncio
async def test_authenticate_and_create_tokens_failure(self, auth_service, mock_oracle_pool):
"""Test pentru autentificare eșuată"""
# Mock pentru conexiunea Oracle
mock_connection = AsyncMock()
mock_cursor = MagicMock()
mock_cursor.fetchone.return_value = [0] # Credențiale invalide
mock_connection.__aenter__.return_value = mock_connection
mock_connection.cursor.return_value.__enter__.return_value = mock_cursor
mock_oracle_pool.get_connection.return_value = mock_connection
success, token_response, error = await auth_service.authenticate_and_create_tokens(
"testuser", "wrongpassword"
)
assert success is False
assert token_response is None
assert error is not None
@pytest.mark.asyncio
async def test_validate_user_company_access(self, auth_service, mock_oracle_pool):
"""Test pentru validarea accesului la firmă"""
# Mock pentru conexiunea Oracle
mock_connection = AsyncMock()
mock_cursor = MagicMock()
mock_cursor.fetchall.return_value = [["COMP1"], ["COMP2"]]
mock_connection.__aenter__.return_value = mock_connection
mock_connection.cursor.return_value.__enter__.return_value = mock_cursor
mock_oracle_pool.get_connection.return_value = mock_connection
# Test acces valid
has_access = await auth_service.validate_user_company_access("testuser", "COMP1")
assert has_access is True
# Test acces invalid
has_access = await auth_service.validate_user_company_access("testuser", "COMP3")
assert has_access is False
def test_cache_functionality(self, auth_service):
"""Test pentru funcționalitatea de cache"""
# Test cache stats
stats = auth_service.get_cache_stats()
assert isinstance(stats, dict)
assert 'total_entries' in stats
assert 'valid_entries' in stats
assert 'cache_hit_ratio' in stats
# Test clear cache
auth_service.clear_cache()
stats_after_clear = auth_service.get_cache_stats()
assert stats_after_clear['total_entries'] == 0
class TestRateLimiter:
"""Test suite pentru Rate Limiter"""
@pytest.fixture
def rate_limiter(self):
"""Fixture pentru rate limiter"""
return RateLimiter(max_requests=3, time_window=5)
def test_rate_limiting_within_limit(self, rate_limiter):
"""Test pentru request-uri în limita permisă"""
client_ip = "192.168.1.1"
# Primele 3 request-uri trebuie să fie permise
for i in range(3):
assert rate_limiter.is_allowed(client_ip) is True
# Al 4-lea request trebuie refuzat
assert rate_limiter.is_allowed(client_ip) is False
def test_rate_limiting_reset_after_time(self, rate_limiter):
"""Test pentru resetarea rate limiting după expirarea ferestrei"""
client_ip = "192.168.1.2"
# Consumă toate request-urile
for i in range(3):
assert rate_limiter.is_allowed(client_ip) is True
# Request-ul următor trebuie refuzat
assert rate_limiter.is_allowed(client_ip) is False
# Simulează trecerea timpului
time.sleep(6)
# Acum ar trebui să funcționeze din nou
assert rate_limiter.is_allowed(client_ip) is True
def test_rate_limiting_different_ips(self, rate_limiter):
"""Test pentru rate limiting pe IP-uri diferite"""
ip1 = "192.168.1.1"
ip2 = "192.168.1.2"
# Consumă toate request-urile pentru primul IP
for i in range(3):
assert rate_limiter.is_allowed(ip1) is True
assert rate_limiter.is_allowed(ip1) is False
# Al doilea IP ar trebui să funcționeze normal
for i in range(3):
assert rate_limiter.is_allowed(ip2) is True
class TestAuthenticationRoutes:
"""Test suite pentru rutele de autentificare"""
@pytest.fixture
def app(self):
"""Fixture pentru aplicația FastAPI de test"""
app = FastAPI()
auth_router = create_auth_router()
app.include_router(auth_router)
return app
@pytest.fixture
def client(self, app):
"""Fixture pentru client de test"""
return TestClient(app)
@pytest.fixture
def mock_auth_service(self):
"""Mock pentru auth service"""
with patch('roa2web.shared.auth.routes.auth_service') as mock_service:
yield mock_service
def test_login_success(self, client, mock_auth_service):
"""Test pentru login cu succes"""
# Mock pentru autentificare cu succes
mock_token_response = TokenResponse(
access_token="test-access-token",
refresh_token="test-refresh-token",
token_type="bearer",
expires_in=1800,
user=CurrentUser(
username="testuser",
companies=["COMP1", "COMP2"],
permissions=[PermissionType.READ, PermissionType.REPORTS]
)
)
mock_auth_service.authenticate_and_create_tokens.return_value = (
True, mock_token_response, None
)
mock_auth_service.get_user_companies.return_value = ["COMP1", "COMP2"]
response = client.post("/auth/login", json={
"username": "testuser",
"password": "password"
})
assert response.status_code == 200
data = response.json()
assert data["access_token"] == "test-access-token"
assert data["token_type"] == "bearer"
assert data["user"]["username"] == "testuser"
def test_login_failure(self, client, mock_auth_service):
"""Test pentru login eșuat"""
mock_auth_service.authenticate_and_create_tokens.return_value = (
False, None, "Invalid credentials"
)
response = client.post("/auth/login", json={
"username": "testuser",
"password": "wrongpassword"
})
assert response.status_code == 401
assert "Invalid credentials" in response.json()["detail"]
def test_protected_endpoint_without_token(self, client):
"""Test pentru endpoint protejat fără token"""
response = client.get("/auth/me")
assert response.status_code == 401
def test_protected_endpoint_with_valid_token(self, client):
"""Test pentru endpoint protejat cu token valid"""
# Creează un token de test
jwt_handler = JWTHandler(secret_key="test-secret-key")
token = jwt_handler.create_access_token(
username="testuser",
companies=["COMP1"],
permissions=["read"]
)
with patch('roa2web.shared.auth.dependencies.jwt_handler', jwt_handler):
response = client.get("/auth/me", headers={
"Authorization": f"Bearer {token}"
})
assert response.status_code == 200
data = response.json()
assert data["username"] == "testuser"
class TestSecurityFeatures:
"""Test suite pentru funcții de securitate"""
def test_jwt_token_tampering(self):
"""Test pentru detectarea modificării token-urilor JWT"""
jwt_handler = JWTHandler(secret_key="test-secret-key")
token = jwt_handler.create_access_token("testuser", ["COMP1"])
# Modifică token-ul
tampered_token = token[:-5] + "XXXXX"
# Token-ul modificat trebuie să fie invalid
token_data = jwt_handler.verify_token(tampered_token)
assert token_data is None
def test_jwt_secret_key_different(self):
"""Test pentru token-uri semnate cu chei diferite"""
jwt_handler1 = JWTHandler(secret_key="secret1")
jwt_handler2 = JWTHandler(secret_key="secret2")
token = jwt_handler1.create_access_token("testuser", ["COMP1"])
# Token-ul nu trebuie să fie valid cu o cheie diferită
token_data = jwt_handler2.verify_token(token)
assert token_data is None
@pytest.mark.asyncio
async def test_sql_injection_prevention(self):
"""Test pentru prevenirea SQL injection"""
auth_service = UserAuthService()
with patch('roa2web.shared.auth.auth_service.oracle_pool') as mock_pool:
mock_connection = AsyncMock()
mock_cursor = MagicMock()
mock_connection.__aenter__.return_value = mock_connection
mock_connection.cursor.return_value.__enter__.return_value = mock_cursor
mock_oracle_pool.get_connection.return_value = mock_connection
# Încearcă SQL injection în username
malicious_username = "admin'; DROP TABLE users; --"
await auth_service.verify_user_credentials(malicious_username, "password")
# Verifică că query-ul folosește parametri legați
mock_cursor.execute.assert_called_once()
call_args = mock_cursor.execute.call_args
assert ':username' in call_args[0][0] # Query cu parametri
assert malicious_username.upper() == call_args[1]['username'] # Parametri legați
@pytest.mark.performance
class TestPerformance:
"""Test suite pentru performanță"""
def test_jwt_token_creation_performance(self):
"""Test pentru performanța creării token-urilor"""
jwt_handler = JWTHandler(secret_key="test-secret-key")
start_time = time.time()
# Creează 1000 de token-uri
for i in range(1000):
jwt_handler.create_access_token(f"user{i}", ["COMP1"])
end_time = time.time()
total_time = end_time - start_time
# Ar trebui să dureze mai puțin de 1 secundă
assert total_time < 1.0
print(f"Created 1000 tokens in {total_time:.4f} seconds")
def test_jwt_token_verification_performance(self):
"""Test pentru performanța verificării token-urilor"""
jwt_handler = JWTHandler(secret_key="test-secret-key")
# Creează 100 de token-uri
tokens = []
for i in range(100):
token = jwt_handler.create_access_token(f"user{i}", ["COMP1"])
tokens.append(token)
start_time = time.time()
# Verifică toate token-urile
for token in tokens:
jwt_handler.verify_token(token)
end_time = time.time()
total_time = end_time - start_time
# Ar trebui să dureze mai puțin de 0.5 secunde
assert total_time < 0.5
print(f"Verified 100 tokens in {total_time:.4f} seconds")
if __name__ == "__main__":
# Rulează testele
pytest.main([__file__, "-v", "--tb=short"])