Skip to main content

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)
Valoressuperadmin | admin | userslug libre del tenant (ej. supervisor)
Alcancetodo el ecosistema Ajolotesolo dentro del tenant aura
Para quéoperaciones del SSOroles 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 Authorization en 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 }. Lee roleTenant; superadmin global pasa siempre.
  • Nav (app-nav.tsx): cada item puede tener roles?: 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(...) { ... }
  • superadmin global pasa siempre; el resto se valida contra role_tenant.
  • Sin el rol → 403. Solo autenticación (sin rol) → @UseGuards(SessionGuard).
El backend NO lee el rol del JWT

AppRoleGuard resuelve los roles en vivo contra ajolote (HttpAppRoleSourceGET /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

Logout debe recargar

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 front

El AUTH_SERVICE_URL del backend y el ajolote al que apunta el front tienen que ser el mismo, o los roles no matchean.