Ajolote y roles
Ajolote es el servicio de autenticación/sesión + roles de Aura. Los paquetes
(@cuatro-quinas/ajolote-*) son ESM-only; como el backend compila a CommonJS, Aura los
carga con import() dinámico y usa guards propios (SessionGuard, AppRoleGuard).
Modelo de roles
role (global) | role_tenant (por-tenant) | |
|---|---|---|
| Valores | superadmin | admin | user | slug libre del tenant (ej. supervisor) |
| Alcance | todo el ecosistema Ajolote | solo dentro del tenant aura |
| Para qué | operaciones del SSO | roles de negocio de Aura |
Aura usa role_tenant (ej. supervisor). superadmin global bypassa cualquier
chequeo de role_tenant. Los roles viven en Ajolote; el backend los hace cumplir; el
front solo los lee para mostrar/ocultar UI.
Flujo de sesión
WebSocket del copiloto: los browsers no pueden setear headers
Authorizationen WS, así que la conexión usa un ticket UUID de un solo uso (TTL 30s); el JWT nunca va en la URL.
Bearer y no cookie: la cookie la setea ajolote en su dominio; el browser nunca la
manda al backend de Aura (dominio distinto). Por eso toda llamada autenticada usa
fetchAuthed (Bearer), que sí cruza dominios.
Gating en el frontend
useRole()(src/lib/use-role.ts) →{ role, globalRole, hasRole, hasAnyRole }. LeeroleTenant;superadminglobal pasa siempre.- Nav (
app-nav.tsx): cada item puede tenerroles?: string[]; se muestra solo si el user cumple. Un no-supervisor no ve Analíticas / Operación / Logs. - Rutas (
<RequireRole roles={[...]}>): oculta/redirige la vista si no cumple. - Catálogo (
src/lib/roles.ts,AURA_ROLES): espejo de los keys de Ajolote.
El front solo oculta UI. La autoridad real es el backend.
Gating en el backend
@RequireAppRole({ any: [...] }) combina SessionGuard + AppRoleGuard.
@Get('operational')
@RequireAppRole({ any: ['supervisor'] })
getOperational(...) { ... }
superadminglobal pasa siempre; el resto se valida contrarole_tenant.- Sin el rol → 403. Solo autenticación (sin rol) →
@UseGuards(SessionGuard).
AppRoleGuard resuelve los roles en vivo contra ajolote (HttpAppRoleSource →
GET /api/me/app-roles, cacheado 30s) usando el Bearer del request. Consecuencias:
- Si le pegás al endpoint directo en el browser (sin
Authorization: Bearer) → "App roles [ninguno]" → 403. Probá siempre desde la app. - Puede haber desajuste con el claim del JWT: el front muestra un rol (del claim) pero el endpoint de ajolote devuelve otra cosa. Si pasa, verificá que el rol esté creado en el catálogo del tenant y asignado al usuario (abajo).
Endpoints gateados a supervisor hoy: GET /metrics/operational, GET /feedback/stats,
GET /audit/logs. POST /feedback NO — los agentes normales deben poder marcar
sugerencias (solo autenticación).
Asignar el rol supervisor (Ajolote)
Se administra contra ajolote con un JWT de superadmin (no desde Aura):
$AUTH = "http://100.64.188.115:3010"
# 1. Crear el rol en el catálogo del tenant (una vez)
POST $AUTH/api/admin/roles Body: {"key":"supervisor","label":"Supervisor"}
# 2. Asignarlo a un usuario
PATCH $AUTH/api/admin/users/USER_ID/role-tenant Body: {"roleKey":"supervisor"}
# 3. Verificar
GET $AUTH/api/admin/users/USER_ID/role-tenant?appId=aura
- Backend: al resolver en vivo (cache 30s), toma el rol en ≤30s sin redeploy.
- Frontend: la nav lee el claim del JWT → necesita re-login para verlo.
Notas de sesión
El botón Salir hace signOut() y un full reload (window.location.href = '/').
Sin el reload, el token en memoria del refresher, la caché de React Query y el WebSocket
sobreviven → al re-loguear con otro usuario, fetchAuthed sigue mandando el JWT viejo
(otro usuario, role_tenant: null) → 403. Si ves un token de una sesión anterior, es esto:
logout limpio + re-login.
AUTH_SERVICE_URL == ajolote del frontEl AUTH_SERVICE_URL del backend y el ajolote al que apunta el front tienen que ser el
mismo, o los roles no matchean.