fix(auth): hornear is_hub_admin (autoritativo del Hub) en la sesión local

create_company (y otros checks) usan is_hub_admin, pero la sesión local se emitía
desde el token KC crudo, que no trae ese claim → el hub_admin sin tenant recibía 403
al crear compañía. Ahora la sesión se emite con los claims de /auth/me del Hub
(is_hub_admin, roles), con fallback al decode del token KC si el Hub no responde.
Se mantiene intacto el control de autorización (solo hub_admin crea fuera de su tenant).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ernesto Herrera
2026-07-17 12:27:27 -06:00
parent 63ad2e2ecd
commit ce8b042e84
2 changed files with 31 additions and 2 deletions

View File

@@ -559,6 +559,8 @@ async def create_company(
if not tenant: if not tenant:
raise HTTPException(status_code=404, detail="Tenant no encontrado.") raise HTTPException(status_code=404, detail="Tenant no encontrado.")
# Autorización: hub_admin (atestado por el Hub) puede crear en cualquier tenant;
# un usuario ligado a un tenant, solo en el suyo.
if not is_hub_admin(current_user): if not is_hub_admin(current_user):
own = resolve_effective_tenant_id_from_user(current_user) own = resolve_effective_tenant_id_from_user(current_user)
if own is None or int(own) != tid: if own is None or int(own) != tid:

View File

@@ -235,7 +235,7 @@ class AuthService:
return claims return claims
def _session_claims_from_kc(self, data: Dict[str, Any]) -> Dict[str, Any]: def _session_claims_from_kc(self, data: Dict[str, Any]) -> Dict[str, Any]:
"""Construye los claims de la sesión local a partir de la respuesta KC del Hub.""" """Construye los claims de la sesión local a partir del token KC (decode)."""
kc_claims = self._decode_kc_user_from_token(data.get("access_token", "")) kc_claims = self._decode_kc_user_from_token(data.get("access_token", ""))
claims: Dict[str, Any] = dict(kc_claims) claims: Dict[str, Any] = dict(kc_claims)
# tenant_id/tenant_slug explícitos del Hub tienen precedencia sobre el token # tenant_id/tenant_slug explícitos del Hub tienen precedencia sobre el token
@@ -245,6 +245,33 @@ class AuthService:
claims["tenant_slug"] = data.get("tenant_slug") claims["tenant_slug"] = data.get("tenant_slug")
return claims return claims
async def _session_claims(self, data: Dict[str, Any]) -> Dict[str, Any]:
"""
Claims AUTORITATIVOS para la sesión local: se prefiere /auth/me del Hub (trae
is_hub_admin, roles, etc. que el token KC crudo no incluye). Si el Hub no
responde, se cae al decode del token KC. Así la sesión local sabe si el
usuario es hub_admin sin volver a consultar al Hub en cada request.
"""
from core.security import verify_token
claims: Dict[str, Any] = {}
try:
info = await verify_token(data.get("access_token", ""))
if isinstance(info, dict):
claims = dict(info)
except Exception as exc:
logger.warning("session_claims: /auth/me no disponible, uso decode KC: %s", exc)
if not claims:
return self._session_claims_from_kc(data)
# tenant_id/tenant_slug explícitos del Hub tienen precedencia.
if data.get("tenant_id") is not None:
claims["tenant_id"] = data.get("tenant_id")
if data.get("tenant_slug") is not None:
claims["tenant_slug"] = data.get("tenant_slug")
return claims
async def refresh_token(self, refresh_data: RefreshTokenRequestDTO) -> TokenResponseDTO: async def refresh_token(self, refresh_data: RefreshTokenRequestDTO) -> TokenResponseDTO:
""" """
Refresca la sesión. Refresca la sesión.
@@ -321,7 +348,7 @@ class AuthService:
from core import local_session, session_store from core import local_session, session_store
start = int(prev_sst) if prev_sst else int(datetime.now(timezone.utc).timestamp()) start = int(prev_sst) if prev_sst else int(datetime.now(timezone.utc).timestamp())
claims = self._session_claims_from_kc(data) claims = await self._session_claims(data)
new_access = data.get("access_token", "") new_access = data.get("access_token", "")
new_refresh = data.get("refresh_token", "") new_refresh = data.get("refresh_token", "")
# Reutiliza la sesión de valkey si ya existía; si no, la crea. # Reutiliza la sesión de valkey si ya existía; si no, la crea.