fix(core): usar token KC (valkey) para listar usuarios del tenant, no la sesión local
list_users/stats llamaban al Hub (users-with-info) con el Bearer de la app, que con el patrón SIWEB es la sesión local (HS256) → el Hub la rechaza (401) → 0 usuarios. Nuevo core.hub_token.get_hub_access_token: toma el token KC de la sesión (valkey vía crm_sid) y lo refresca si está por expirar. list_users y stats lo usan para el Hub. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -53,12 +53,17 @@ async def get_user_statistics(
|
||||
"""
|
||||
tenant_id = validate_access_to_resource(db, company_id, current_user, required_permissions=["user.view"])
|
||||
service = UserService(db, tenant_id, company_id, is_hub_admin=is_hub_admin(current_user))
|
||||
from core.hub_token import get_hub_access_token
|
||||
|
||||
auth_header = request.headers.get("Authorization") or ""
|
||||
token = (
|
||||
auth_header[7:].strip()
|
||||
if auth_header.lower().startswith("bearer ")
|
||||
else auth_header.strip()
|
||||
)
|
||||
kc_token = await get_hub_access_token(request)
|
||||
if kc_token:
|
||||
token = kc_token
|
||||
hub_tid = resolve_hub_tenant_id_for_api(
|
||||
tenant_id, request.headers.get("X-Tenant-Override")
|
||||
)
|
||||
@@ -84,12 +89,19 @@ async def list_users(
|
||||
"""
|
||||
tenant_id = validate_access_to_resource(db, company_id, current_user, required_permissions=["user.view"])
|
||||
service = UserService(db, tenant_id, company_id, is_hub_admin=is_hub_admin(current_user))
|
||||
# El Bearer de la app puede ser la sesión local (SIWEB), que el Hub no acepta.
|
||||
# Para listar usuarios del tenant se usa el token KC de la sesión (valkey), refrescado.
|
||||
from core.hub_token import get_hub_access_token
|
||||
|
||||
auth_header = request.headers.get("Authorization") or ""
|
||||
token = (
|
||||
auth_header[7:].strip()
|
||||
if auth_header.lower().startswith("bearer ")
|
||||
else auth_header.strip()
|
||||
)
|
||||
kc_token = await get_hub_access_token(request)
|
||||
if kc_token:
|
||||
token = kc_token
|
||||
hub_tid = resolve_hub_tenant_id_for_api(
|
||||
tenant_id, request.headers.get("X-Tenant-Override")
|
||||
)
|
||||
|
||||
67
backend/core/hub_token.py
Normal file
67
backend/core/hub_token.py
Normal file
@@ -0,0 +1,67 @@
|
||||
"""
|
||||
Obtención de un access token de Keycloak VÁLIDO para llamar a la API del Hub.
|
||||
|
||||
Con el patrón de sesión local (SIWEB) el Bearer de la app es un JWT propio (HS256)
|
||||
que el Hub NO entiende. Para las llamadas server→Hub se usa el token KC guardado en
|
||||
la sesión (valkey, vía cookie crm_sid), refrescándolo si está por expirar.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import time
|
||||
from typing import Optional
|
||||
|
||||
import httpx
|
||||
from jose import jwt
|
||||
|
||||
from core.config import settings
|
||||
from core import session_store
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _kc_exp_ok(token: str, leeway_seconds: int = 30) -> bool:
|
||||
"""True si el token KC no está expirado (con margen)."""
|
||||
try:
|
||||
claims = jwt.get_unverified_claims(token)
|
||||
exp = claims.get("exp")
|
||||
return isinstance(exp, (int, float)) and (int(exp) - int(time.time())) > leeway_seconds
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
async def get_hub_access_token(request) -> Optional[str]:
|
||||
"""
|
||||
Devuelve un access token KC válido tomado de la sesión (valkey vía crm_sid),
|
||||
refrescándolo contra el Hub si está por expirar. None si no hay sesión.
|
||||
Best-effort: si el refresh falla, devuelve el token guardado (puede estar vencido).
|
||||
"""
|
||||
sid = request.cookies.get("crm_sid") if request is not None else None
|
||||
if not sid:
|
||||
return None
|
||||
sess = session_store.get_session(sid)
|
||||
if not sess:
|
||||
return None
|
||||
|
||||
access = sess.get("access_token")
|
||||
refresh = sess.get("refresh_token")
|
||||
|
||||
if access and _kc_exp_ok(access):
|
||||
return access
|
||||
|
||||
if refresh:
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=8.0) as client:
|
||||
r = await client.post(
|
||||
f"{settings.HUB_URL}api/v1/auth/refresh",
|
||||
json={"refresh_token": refresh},
|
||||
)
|
||||
if r.status_code == 200:
|
||||
data = r.json()
|
||||
new_access = data.get("access_token") or access
|
||||
session_store.update_session_tokens(sid, new_access, data.get("refresh_token") or refresh)
|
||||
return new_access
|
||||
logger.info("get_hub_access_token: Hub refresh devolvió %s", r.status_code)
|
||||
except Exception as exc:
|
||||
logger.warning("get_hub_access_token: refresh falló: %s", exc)
|
||||
|
||||
return access
|
||||
Reference in New Issue
Block a user