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:
@@ -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:
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
Reference in New Issue
Block a user