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"])
|
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))
|
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 ""
|
auth_header = request.headers.get("Authorization") or ""
|
||||||
token = (
|
token = (
|
||||||
auth_header[7:].strip()
|
auth_header[7:].strip()
|
||||||
if auth_header.lower().startswith("bearer ")
|
if auth_header.lower().startswith("bearer ")
|
||||||
else auth_header.strip()
|
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(
|
hub_tid = resolve_hub_tenant_id_for_api(
|
||||||
tenant_id, request.headers.get("X-Tenant-Override")
|
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"])
|
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))
|
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 ""
|
auth_header = request.headers.get("Authorization") or ""
|
||||||
token = (
|
token = (
|
||||||
auth_header[7:].strip()
|
auth_header[7:].strip()
|
||||||
if auth_header.lower().startswith("bearer ")
|
if auth_header.lower().startswith("bearer ")
|
||||||
else auth_header.strip()
|
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(
|
hub_tid = resolve_hub_tenant_id_for_api(
|
||||||
tenant_id, request.headers.get("X-Tenant-Override")
|
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