perf(auth): extender TTL de token_cache y cachear licencia válida por tenant

Cada navegación al dashboard disparaba verify-license y auth/me contra el Hub sin
ningún caché, sumando 1-6s por llamada del lado del Hub a cada request.

- security.py: token_cache TTL 60s→300s (el access token vive más que el TTL
  anterior; cachear su verificación es seguro).
- middleware.py: nuevo _license_ok_cache (TTLCache, 300s) en
  LicenseValidationMiddleware — SOLO cachea el camino "licencia válida" por
  tenant_override; inválida/expirada/error nunca se cachea y siempre revalida
  contra el Hub (fail-closed: el peor caso es que un tenant recién revocado siga
  pasando hasta 5 min más, nunca al revés).
This commit is contained in:
2026-07-21 16:13:02 -05:00
parent bdd089954b
commit a1bbb6b1b2
2 changed files with 19 additions and 2 deletions

View File

@@ -6,12 +6,20 @@ from typing import Callable, Optional
from fastapi import Request, Response from fastapi import Request, Response
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
from starlette.middleware.base import BaseHTTPMiddleware from starlette.middleware.base import BaseHTTPMiddleware
from cachetools import TTLCache
from .config import settings from .config import settings
from .security import get_tenant_from_token, verify_token, get_active_system from .security import get_tenant_from_token, verify_token, get_active_system
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# Cache de licencia VÁLIDA por tenant (evita golpear verify-license del Hub en cada
# request). Solo el camino "válida" se cachea — inválida/expirada/error nunca se
# cachean y siempre revalidan contra el Hub (fail-closed: lo peor que puede pasar es
# que un tenant recién revocado siga pasando hasta 5 min más, nunca al revés).
_LICENSE_OK_TTL_SECONDS = 300
_license_ok_cache: TTLCache = TTLCache(maxsize=1000, ttl=_LICENSE_OK_TTL_SECONDS)
def _normalize_text(value: str | None) -> str: def _normalize_text(value: str | None) -> str:
if not value: if not value:
@@ -160,6 +168,11 @@ class LicenseValidationMiddleware(BaseHTTPMiddleware):
if tid is not None and str(tid).strip() != "": if tid is not None and str(tid).strip() != "":
tenant_override = str(tid) tenant_override = str(tid)
# Licencia ya confirmada válida hace poco para este tenant — no volver a
# golpear al Hub (ver definición de _license_ok_cache arriba).
if tenant_override and tenant_override in _license_ok_cache:
return await call_next(request)
hub_headers = {"Authorization": f"Bearer {token}"} hub_headers = {"Authorization": f"Bearer {token}"}
if tenant_override: if tenant_override:
hub_headers["X-Tenant-Override"] = str(tenant_override) hub_headers["X-Tenant-Override"] = str(tenant_override)
@@ -274,6 +287,8 @@ class LicenseValidationMiddleware(BaseHTTPMiddleware):
pass # Si no se puede parsear, dejamos pasar — el Hub es la fuente de verdad pass # Si no se puede parsear, dejamos pasar — el Hub es la fuente de verdad
request.state.license_info = data request.state.license_info = data
if tenant_override:
_license_ok_cache[tenant_override] = True
return await call_next(request) return await call_next(request)
if response.status_code == 401: if response.status_code == 401:

View File

@@ -18,8 +18,10 @@ from .database import get_core_db
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# Cache para tokens verificados (1 minuto de TTL, máximo 1000 tokens) # Cache para tokens verificados (5 minutos de TTL, máximo 1000 tokens).
token_cache = TTLCache(maxsize=1000, ttl=60) # El access token vive más que este TTL, así que cachear su verificación es seguro
# y evita golpear /auth/me del Hub en cada request de navegación.
token_cache = TTLCache(maxsize=1000, ttl=300)
# IDs de tenants ya sincronizados en este proceso (evita consultas repetidas) # IDs de tenants ya sincronizados en este proceso (evita consultas repetidas)
_synced_tenant_ids: Set[int] = set() _synced_tenant_ids: Set[int] = set()