ADR-013: Autorización resuelta en la BD por request (Keycloak autentica, la BD autoriza)¶
Estado: Aceptado (decisión R4 de la Fase 1) Fecha: 2026-06-17 Autores: Giampiero (mantenedor principal)
Contexto¶
Keycloak autentica al usuario y emite un JWT (firma RS256, validado contra JWKS por el
api-gateway, [ADR sobre Keycloak]). Pero la autorización en Orfeo es por tenant y por
dependencia: los permisos granulares (PERM_RADI, USUA_PERM_EXPEDIENTE, …) y la
dependencia activa del usuario viven en el schema tenant_{slug} (tablas auth_*, urd_*),
no en Keycloak. El RBAC ya está implementado (E08): resolve_permissions(user_id) calcula
MAX(crud) por permiso desde la BD del tenant.
Faltaba decidir cómo llega esa autorización al punto de decisión (el gate
require_permission de cada endpoint). Tres caminos posibles:
- Resolver en la BD por request — el token de Keycloak solo identifica al usuario
(
sub) y su tenant; cada servicio resuelve permisos/dependencia desde la BD del tenant en cada petición. - Firma propia — auth-service valida el token de Keycloak y emite un JWT propio con
permissions{}/deptembebidos; auth-service pasa a ser emisor de tokens. - Token-exchange de Keycloak — Keycloak inyecta los permisos vía protocol mappers / token-exchange.
Decisión¶
Se adopta la opción 1: la autorización se resuelve en la BD del tenant en cada request. Keycloak sigue siendo el único emisor de tokens (solo autentica).
- El api-gateway valida el JWT de Keycloak e inyecta hacia los servicios los headers de
identidad ya confiables:
X-User-Id(=subde Keycloak),X-Username,X-Tenant-Slug,X-User-Roles. - Cada servicio que necesite autorizar resuelve, dentro del schema del tenant:
keycloak_sub→auth_users.id→resolve_effective_permissions()→ comprobación dehas_permission(name, min_crud)(ROOT primero). - El gate vive en
app/core/authz.pycomo dependenciarequire_permission(name, min_crud)reutilizable; comparte la conexión conget_tenant_conn(FastAPI cachea la dependencia por request). - El cambio de dependencia activa (
/auth/context/switch) no reemite token: cambia la fila de contexto en la BD; la siguiente petición resuelve sobre ella.
Justificación¶
- Coherencia con el principio ya fijado ("Keycloak autentica, la BD autoriza", reflejo del legado plan-argoik/plan-icetex). La verdad de los permisos es la BD del tenant.
- Sin tokens obsoletos: un cambio de permiso o de dependencia surte efecto en la
siguiente petición, sin esperar al refresh del token. Crítico para revocaciones (RN de
seguridad) y para el flujo de
context/switch. - Keycloak no modela permisos por tenant/dependencia: mapearlos vía protocol mappers (opción 3) obligaría a duplicar en Keycloak datos que viven en la BD del tenant y a configurar token-exchange en el realm. Frágil y con doble fuente de verdad.
- auth-service no se vuelve emisor de tokens (opción 2): evita gestionar firma, rotación de claves y expiración propias, y la obsolescencia de permisos embebidos.
Consecuencias¶
Positivas
- Autorización siempre fresca; revocación inmediata.
- Punto de decisión único y testeable (require_permission), replicable en todos los servicios.
- El token de Keycloak queda mínimo y estable.
Negativas / mitigaciones
- Una consulta de permisos por request. Mitigación: la resolución es una sola query
indexada (auth_memberships ⨝ auth_group_permissions ⨝ auth_permissions); es cacheable
por (tenant, user) con TTL corto e invalidación en cambios de RBAC si el perfil de carga lo
exige. No se cachea en v1 (correctitud primero).
- Lazy-provisioning: un usuario válido en Keycloak puede no existir aún en auth_users
del tenant. v1: si no existe, no tiene permisos → 403. La autocreación se decide aparte.
- Cada servicio que autorice necesita acceso al schema del tenant (get_tenant_conn) y a la
resolución de permisos (hoy en auth-service; se generalizará a orpycamcp_common si más de
un servicio la necesita, ADR-010).
Alternativas descartadas¶
- Firma propia (opción 2): convierte a auth-service en IdP secundario; permisos embebidos
→ obsolescencia hasta el refresh;
context/switchobliga a reemitir. - Token-exchange Keycloak (opción 3): doble fuente de verdad de permisos; configuración de realm pesada; no encaja con permisos por dependencia.