fix telegram
This commit is contained in:
649
deploy-package-20260223-151231/shared/auth/README.md
Normal file
649
deploy-package-20260223-151231/shared/auth/README.md
Normal 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*
|
||||
23
deploy-package-20260223-151231/shared/auth/__init__.py
Normal file
23
deploy-package-20260223-151231/shared/auth/__init__.py
Normal 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'
|
||||
]
|
||||
476
deploy-package-20260223-151231/shared/auth/auth_service.py
Normal file
476
deploy-package-20260223-151231/shared/auth/auth_service.py
Normal 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()
|
||||
540
deploy-package-20260223-151231/shared/auth/demo_app.py
Normal file
540
deploy-package-20260223-151231/shared/auth/demo_app.py
Normal 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()
|
||||
402
deploy-package-20260223-151231/shared/auth/dependencies.py
Normal file
402
deploy-package-20260223-151231/shared/auth/dependencies.py
Normal 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
|
||||
362
deploy-package-20260223-151231/shared/auth/email_server_cache.py
Normal file
362
deploy-package-20260223-151231/shared/auth/email_server_cache.py
Normal 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)
|
||||
264
deploy-package-20260223-151231/shared/auth/jwt_handler.py
Normal file
264
deploy-package-20260223-151231/shared/auth/jwt_handler.py
Normal 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()
|
||||
375
deploy-package-20260223-151231/shared/auth/middleware.py
Normal file
375
deploy-package-20260223-151231/shared/auth/middleware.py
Normal 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)
|
||||
344
deploy-package-20260223-151231/shared/auth/models.py
Normal file
344
deploy-package-20260223-151231/shared/auth/models.py
Normal 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()
|
||||
681
deploy-package-20260223-151231/shared/auth/routes.py
Normal file
681
deploy-package-20260223-151231/shared/auth/routes.py
Normal 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)
|
||||
559
deploy-package-20260223-151231/shared/auth/test_auth.py
Normal file
559
deploy-package-20260223-151231/shared/auth/test_auth.py
Normal 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"])
|
||||
Reference in New Issue
Block a user