Files
PecHub/backend/app/dependencies.py
T
mgiustini 58a233236c feat: Fase 1 – Fondamenta complete (backend FastAPI + auth + permessi)
- docker-compose.yml: PostgreSQL 16, Redis 7, MinIO, Nginx
- backend FastAPI: struttura monorepo, config pydantic-settings
- modelli SQLAlchemy: tutti i modelli (tenants, users, mailboxes, messages, archival, permissions, labels, audit_log)
- migrazione Alembic 0001: schema completo in pure SQL
- auth API: login JWT, refresh token rotation, logout, 2FA TOTP (setup/verify/disable)
- CRUD utenti: lista, crea, modifica, reset password, soft delete
- permessi granulari (Fase 1-A): mailbox_permissions, assegna/revoca/lista
- CRUD tenant: gestione super-admin
- sicurezza: AES-256-GCM cifratura credenziali IMAP/SMTP, bcrypt password
- RLS PostgreSQL: isolamento multi-tenant per request
- seed sviluppo: tenant demo + admin + operator
- test unit: security (bcrypt, JWT, AES), auth_service
- test integration: auth endpoints, users endpoints
- CI GitHub Actions: lint (ruff), test (pytest), build Docker, security scan
- infra: nginx.conf, redis.conf
- Makefile con comandi make dev/test/migrate/seed

Definition of Done:
 Login, refresh token e TOTP funzionanti
 make dev porta in piedi tutto lo stack locale
 CI configurata
2026-03-18 16:42:01 +01:00

118 lines
3.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Dependency FastAPI get_db, get_current_user, require_admin, RLS middleware.
"""
import uuid
from typing import Annotated
from fastapi import Depends, Request
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from jose import JWTError
from sqlalchemy import text
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.exceptions import ForbiddenError, TokenInvalidError
from app.core.security import decode_token
from app.database import get_db
from app.models.user import User
from sqlalchemy import select
security = HTTPBearer()
# ─── Database con RLS ─────────────────────────────────────────────────────────
async def get_db_with_rls(
tenant_id: uuid.UUID,
db: AsyncSession = Depends(get_db),
) -> AsyncSession:
"""
Imposta la variabile di sessione PostgreSQL per RLS.
Da usare dopo aver estratto il tenant_id dall'utente autenticato.
"""
await db.execute(
text("SET LOCAL app.current_tenant_id = :tenant_id"),
{"tenant_id": str(tenant_id)},
)
return db
# ─── Utente corrente ──────────────────────────────────────────────────────────
async def get_current_user(
credentials: Annotated[HTTPAuthorizationCredentials, Depends(security)],
db: AsyncSession = Depends(get_db),
) -> User:
"""
Estrae e valida il JWT dall'header Authorization: Bearer <token>.
Carica l'utente dal DB e imposta RLS.
"""
token = credentials.credentials
try:
payload = decode_token(token)
except JWTError:
raise TokenInvalidError()
if payload.get("type") != "access":
raise TokenInvalidError()
user_id_str = payload.get("sub")
tenant_id_str = payload.get("tid")
if not user_id_str or not tenant_id_str:
raise TokenInvalidError()
try:
user_id = uuid.UUID(user_id_str)
tenant_id = uuid.UUID(tenant_id_str)
except ValueError:
raise TokenInvalidError()
# Imposta RLS per questo tenant
# SET LOCAL non supporta parametri $1, usiamo text() con valore inline
await db.execute(
text(f"SET LOCAL app.current_tenant_id = '{tenant_id!s}'")
)
# Carica utente
result = await db.execute(
select(User).where(User.id == user_id, User.tenant_id == tenant_id)
)
user = result.scalar_one_or_none()
if not user:
raise TokenInvalidError()
if not user.is_active:
from app.core.exceptions import AccountDisabledError
raise AccountDisabledError()
return user
# ─── Role guards ──────────────────────────────────────────────────────────────
async def require_admin(
current_user: Annotated[User, Depends(get_current_user)],
) -> User:
"""Richiede ruolo admin o super_admin."""
if not current_user.is_admin:
raise ForbiddenError("Richiesto ruolo amministratore")
return current_user
async def require_super_admin(
current_user: Annotated[User, Depends(get_current_user)],
) -> User:
"""Richiede ruolo super_admin."""
if not current_user.is_super_admin:
raise ForbiddenError("Richiesto ruolo super_admin")
return current_user
# ─── Tipo annotato per ridurre boilerplate negli endpoint ─────────────────────
CurrentUser = Annotated[User, Depends(get_current_user)]
AdminUser = Annotated[User, Depends(require_admin)]
SuperAdminUser = Annotated[User, Depends(require_super_admin)]
DB = Annotated[AsyncSession, Depends(get_db)]