Files
service_manager/backend/app/middleware/tenant.py
icamarillo 517297e89a feat: Version 1.10.0 - Refactorizacion, optimizacion UI y mejoras de seguridad
- Extraccion de helpers en backend: audit_helpers.py, helpers.py
- Modularizacion de schemas en archivos individuales por dominio
- Reduccion de audit.py en 953 lineas (74% del archivo)
- Reduccion de tickets.py en 655 lineas (60% del archivo)
- Expansion de auth.py con recuperacion de contrasenia y tokens
- Nuevos modulos: core/email.py, core/cache.py
- Reorganizacion de scripts a backend/scripts/
- Frontend: refactorizacion de audit page con array-driven components
- Frontend: correccion de 11 errores ortograficos en tickets page
- Frontend: proxy Docker corregido en vite.config.js
- Frontend: nuevas rutas forgot-password, reset-password, organization, profile
- Nuevas utilidades TS: colorUtils.ts, dateFormats.ts
- 5 nuevos archivos de tests unitarios en backend/tests/unit/
- Eliminacion de 3 scripts temporales de prueba
- Documentacion tecnica: CAMBIOS_v1.10.0.md, OPTIMIZACIONES_RENDIMIENTO.md
2026-02-19 13:48:21 -07:00

145 lines
4.7 KiB
Python

"""
Tenant Middleware - ServiceManagerWeb
Middleware para manejo de multi-tenancy
"""
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.requests import Request
from starlette.responses import Response, JSONResponse
from sqlalchemy import select
import structlog
from app.core.database import AsyncSessionLocal
from app.core.config import get_settings
from app.models.tenant import Tenant, TenantStatus
logger = structlog.get_logger(__name__)
settings = get_settings()
class TenantMiddleware(BaseHTTPMiddleware):
"""
Middleware para extraer y validar información del tenant.
Extrae el tenant_id del header X-Tenant-ID o el slug del header
X-Tenant-Slug, valida que exista en la base de datos y que esté
activo, y almacena el objeto Tenant en request.state.tenant.
"""
# Rutas que no requieren tenant
EXCLUDED_PATHS = {
"/health",
"/",
"/api/v1/auth/login",
"/v1/auth/login",
"/api/v1/auth/refresh",
"/v1/auth/refresh",
"/api/v1/auth/forgot-password",
"/v1/auth/forgot-password",
"/api/v1/auth/reset-password",
"/v1/auth/reset-password",
"/docs",
"/api/v1/docs",
"/v1/docs",
"/openapi.json",
"/api/v1/openapi.json",
"/v1/openapi.json",
"/redoc",
"/api/v1/redoc",
"/v1/redoc",
}
async def dispatch(self, request: Request, call_next) -> Response:
"""Valida el tenant en cada request y lo almacena en request.state."""
# Inicializar state con valores por defecto
request.state.tenant = None
request.state.tenant_id = None
request.state.tenant_slug = None
# Saltar validación en rutas excluidas
if request.url.path in self.EXCLUDED_PATHS or request.url.path.startswith("/docs"):
return await call_next(request)
# Extraer headers de tenant
tenant_id = request.headers.get("X-Tenant-ID")
tenant_slug = request.headers.get("X-Tenant-Slug")
# Si no hay headers de tenant
if not tenant_id and not tenant_slug:
if settings.ENVIRONMENT == "production":
return JSONResponse(
status_code=400,
content={"detail": "Tenant information required (X-Tenant-ID or X-Tenant-Slug header)"}
)
# En desarrollo, continuar sin tenant con advertencia
logger.warning(
"Request without tenant information",
path=request.url.path,
method=request.method,
)
return await call_next(request)
# Validar tenant contra la base de datos
try:
async with AsyncSessionLocal() as session:
if tenant_id:
result = await session.execute(
select(Tenant).where(Tenant.id == tenant_id)
)
else:
result = await session.execute(
select(Tenant).where(Tenant.slug == tenant_slug)
)
tenant = result.scalars().first()
if tenant is None:
logger.warning(
"Tenant not found",
tenant_id=tenant_id,
tenant_slug=tenant_slug,
path=request.url.path,
)
return JSONResponse(
status_code=404,
content={"detail": "Tenant not found"}
)
if tenant.status != TenantStatus.ACTIVE:
logger.warning(
"Tenant is not active",
tenant_id=str(tenant.id),
tenant_slug=tenant.slug,
status=tenant.status,
path=request.url.path,
)
return JSONResponse(
status_code=403,
content={"detail": f"Tenant is {tenant.status.value}"}
)
# Almacenar tenant validado en el state
request.state.tenant = tenant
request.state.tenant_id = str(tenant.id)
request.state.tenant_slug = tenant.slug
logger.debug(
"Tenant validated",
tenant_id=str(tenant.id),
tenant_slug=tenant.slug,
path=request.url.path,
)
except Exception as exc:
logger.error(
"Error validating tenant",
error=str(exc),
path=request.url.path,
)
return JSONResponse(
status_code=503,
content={"detail": "Service temporarily unavailable"}
)
return await call_next(request)