diff --git a/backend/api/v1/modules/core/auth/routes.py b/backend/api/v1/modules/core/auth/routes.py index e313a7e..3dfe7eb 100644 --- a/backend/api/v1/modules/core/auth/routes.py +++ b/backend/api/v1/modules/core/auth/routes.py @@ -559,6 +559,8 @@ async def create_company( if not tenant: 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): own = resolve_effective_tenant_id_from_user(current_user) if own is None or int(own) != tid: diff --git a/backend/api/v1/modules/core/auth/service.py b/backend/api/v1/modules/core/auth/service.py index 040e220..8c707be 100644 --- a/backend/api/v1/modules/core/auth/service.py +++ b/backend/api/v1/modules/core/auth/service.py @@ -235,7 +235,7 @@ class AuthService: return claims 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", "")) claims: Dict[str, Any] = dict(kc_claims) # 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") 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: """ Refresca la sesión. @@ -321,7 +348,7 @@ class AuthService: from core import local_session, session_store 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_refresh = data.get("refresh_token", "") # Reutiliza la sesión de valkey si ya existía; si no, la crea.