Fix .gitignore and add missing authentication source files

This commit fixes overly broad .gitignore patterns that were excluding
important source code files from version control. Previously, wildcard
patterns like *auth*, *token*, *secret*, *connection*, and *credential*
were excluding ALL files containing these words, including critical
application code.

Changes:
- Updated .gitignore with specific patterns for sensitive config files
  (*.json, *.txt, *.yml, *.yaml extensions only)
- Removed broad wildcards that excluded source code files

Added missing source files:
- shared/auth/ (9 files): Complete authentication system
  - JWT handler, middleware, auth service, models, routes
- reports-app/backend/app/routers/auth.py: Authentication API router
- reports-app/backend/app/auth_middleware_wrapper.py: Middleware wrapper
- reports-app/frontend/src/stores/auth.js: Vue.js auth store
- reports-app/frontend/tests/: E2E tests and fixtures for auth
- reports-app/telegram-bot/app/auth/: Telegram auth linking module
- deployment/windows/scripts/Setup-ClaudeAuth.ps1: Windows deployment script
- security/secrets_scanner.py: Security scanning utility

These files are essential for the application to function and should
have been included in the initial commit.

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-10-25 15:02:28 +03:00
parent 6b13ffa183
commit f42eff71a6
19 changed files with 5035 additions and 21 deletions

649
shared/auth/README.md Normal file
View File

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

23
shared/auth/__init__.py Normal file
View File

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

395
shared/auth/auth_service.py Normal file
View File

@@ -0,0 +1,395 @@
"""
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
import sys
import os
sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
from 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 verify_user_credentials(self, username: str, password: str) -> bool:
"""
Verifică credențialele utilizatorului folosind pack_drepturi.verificautilizator
Args:
username: Numele utilizatorului
password: Parola utilizatorului
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() 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
# 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) -> 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
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
cached_data = self._get_cached_user_data(username)
if cached_data and 'companies' in cached_data:
return cached_data['companies']
try:
async with oracle_pool.get_connection() 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
self._cache_user_data(username, {'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) -> List[str]:
"""
Obține permisiunile utilizatorului pentru o anumită firmă
Args:
username: Numele utilizatorului
company: Codul firmei
Returns:
Lista permisiunilor pentru firma specificată
"""
# Implementare de bază - poate fi extinsă în viitor
companies = await self.get_user_companies(username)
# 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
) -> Tuple[bool, Optional[TokenResponse], Optional[str]]:
"""
Autentifică utilizatorul și creează token-urile JWT
Args:
username: Numele utilizatorului
password: Parola utilizatorului
Returns:
Tuple cu (success, token_response, error_message)
"""
try:
# Verifică credențialele
is_valid = await self.verify_user_credentials(username, password)
if not is_valid:
return False, None, "Invalid username or password"
# Obține firmele utilizatorului
companies = await self.get_user_companies(username)
# 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 {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(username, companies[0] if companies else "")
# Creează token-urile folosind jwt_handler
jwt_tokens = jwt_handler.create_token_response(
username=username,
companies=companies,
user_id=None, # Poate fi adăugat în viitor dacă avem user_id în DB
permissions=permissions
)
# Creează obiectul CurrentUser
current_user = CurrentUser(
username=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 {username}")
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
shared/auth/demo_app.py Normal file
View File

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

412
shared/auth/dependencies.py Normal file
View File

@@ -0,0 +1,412 @@
"""
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
"""
print(f"[DEPENDENCY DEBUG] get_current_user_from_request called")
print(f"[DEPENDENCY DEBUG] request.state attributes: {dir(request.state)}")
print(f"[DEPENDENCY DEBUG] has is_authenticated: {hasattr(request.state, 'is_authenticated')}")
print(f"[DEPENDENCY DEBUG] is_authenticated value: {getattr(request.state, 'is_authenticated', 'NOT_SET')}")
print(f"[DEPENDENCY DEBUG] has user: {hasattr(request.state, 'user')}")
print(f"[DEPENDENCY DEBUG] user value: {getattr(request.state, 'user', 'NOT_SET')}")
if not hasattr(request.state, 'is_authenticated') or not request.state.is_authenticated:
print(f"[DEPENDENCY DEBUG] Returning 401: Authentication required")
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:
print(f"[DEPENDENCY DEBUG] Returning 401: User not found in request")
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="User not found in request",
headers={"WWW-Authenticate": "Bearer"},
)
print(f"[DEPENDENCY DEBUG] Returning user: {request.state.user}")
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

239
shared/auth/jwt_handler.py Normal file
View File

@@ -0,0 +1,239 @@
"""
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"],
"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")
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
) -> 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
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"],
"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} with companies: {companies}")
return token
def create_refresh_token(self, username: str, user_id: Optional[int] = None) -> str:
"""
Creează un refresh token cu durată mai mare
Args:
username: Numele utilizatorului
user_id: ID-ul utilizatorului
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,
"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}")
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
return self.create_access_token(
username=token_data.username,
companies=companies,
user_id=token_data.user_id,
permissions=permissions
)
def create_token_response(
self,
username: str,
companies: List[str],
user_id: Optional[int] = None,
permissions: Optional[List[str]] = None,
include_refresh: bool = True
) -> 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
Returns:
TokenResponse cu toate token-urile
"""
access_token = self.create_access_token(username, companies, user_id, permissions)
refresh_token = self.create_refresh_token(username, user_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()

373
shared/auth/middleware.py Normal file
View File

@@ -0,0 +1,373 @@
"""
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
"""
print(f"[ORIGINAL MIDDLEWARE] dispatch called for path: {request.url.path}")
start_time = time.time()
path = request.url.path
# 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
print(f"[MIDDLEWARE DEBUG] Extracting token for path: {path}")
token = self._extract_token_from_header(request)
print(f"[MIDDLEWARE DEBUG] Extracted token: {token[:30] if token else 'None'}...")
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
print(f"[MIDDLEWARE DEBUG] Validating token: {token[:30]}...")
token_data = jwt_handler.verify_token(token)
print(f"[MIDDLEWARE DEBUG] Token validation result: {token_data}")
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
logger.debug(f"User {current_user.username} authenticated successfully for path {path}")
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)

231
shared/auth/models.py Normal file
View File

@@ -0,0 +1,231 @@
"""
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"
)
@validator('username')
def username_alphanumeric(cls, v):
"""Validează că username-ul conține doar caractere permise (inclusiv spații)"""
# Permitem litere, cifre, spații, _, și -
allowed_chars = v.replace(' ', '').replace('_', '').replace('-', '')
if not allowed_chars.isalnum():
raise ValueError('Username-ul poate conține doar litere, cifre, spații, _ și -')
return v.upper() # Convertim la uppercase pentru consistență cu Oracle
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ă"
)
# Update la forward references pentru TokenResponse
TokenResponse.model_rebuild()

433
shared/auth/routes.py Normal file
View File

@@ -0,0 +1,433 @@
"""
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
)
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
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"])
@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
Args:
login_data: Datele de autentificare (username, password)
request: Request-ul HTTP (pentru rate limiting)
response: Response-ul HTTP (pentru header-e)
Returns:
Token-urile JWT și informațiile utilizatorului
Raises:
HTTPException: Pentru credențiale invalide sau erori de sistem
"""
try:
# Log tentativa de autentificare
client_ip = request.client.host if request.client else "unknown"
logger.info(f"Login attempt for user {login_data.username} from IP {client_ip}")
# Autentifică și creează token-urile
success, token_response, error_message = await auth_service.authenticate_and_create_tokens(
login_data.username,
login_data.password
)
if not success:
logger.warning(f"Failed login attempt for user {login_data.username}: {error_message}")
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=error_message or "Authentication failed"
)
# Adaugă informațiile utilizatorului în răspuns
companies = await auth_service.get_user_companies(login_data.username)
current_user = CurrentUser(
username=login_data.username,
companies=companies,
permissions=["read", "reports"], # Permisiuni de bază
last_login=datetime.now()
)
token_response.user = current_user
# 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}")
return token_response
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("/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)