> ## Documentation Index
> Fetch the complete documentation index at: https://internal.softcrum.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Registro de permisos y RBAC

> Cuando esto se entregue, "el call center puede leer contactos pero nunca sus documentos de identidad" es una configuración que un administrador hace en un minuto, y la impone el mismo camino de código para una persona y para una API key.

> Traducción. Autoritativo: [`fs-idn-0002-permission-registry-rbac.md`](/modules/identity/features/fs-idn-0002-permission-registry-rbac).

## Contexto

DEC-D5 fijó el modelo: cada endpoint declara **exactamente un** permiso
`{module}.{resource}.{action}`; los permisos se agrupan en roles; los roles se asignan a usuarios **y
a máquinas**. Esta feature es ese modelo.

La decisión que lo hace confiable es que el registro se **genera, no se mantiene**. Las rutas
declaran su permiso en código; el registro se construye desde esas declaraciones; CI falla si la base
y el union generado discrepan. Una lista de permisos mantenida a mano se separa de lo que el código
verifica en semanas, y una lista divergente es peor que ninguna: le dice al administrador algo falso
sobre lo que acaba de otorgar.

Que las máquinas lleven roles es la segunda decisión de carga. Una API key no es un caso especial con
su propio lenguaje de scopes; es un sujeto con asignaciones de rol, verificado idénticamente. Eso es
lo que permite que una pantalla de consentimiento OAuth2 (FS-IDN-0007) muestre permisos reales en vez
de scopes inventados.

## Alcance *(normativo)*

* `identity.permissions`: el registro generado, sembrado desde las declaraciones de ruta.
* `identity.roles`: roles de sistema sembrados por organización, más roles definidos por el tenant.
* `identity.role_permissions` y `identity.role_assignments` con sujeto polimórfico.
* Resolución de autorización: sujeto → roles → permisos, cacheada.
* Chequeos de CI: cada ruta declara exactamente un permiso; cero divergencia entre código y base.

## Fuera de alcance *(normativo)*

* **Permisos por registro.** Son por acción, nunca por fila. "Puede editar esta campaña pero no
  aquella" queda fuera deliberadamente: es donde los modelos de autorización dejan de ser explicables
  para quien tiene que configurarlos.
* Verificaciones de permiso dentro de los módulos. La capa de API resuelve el contexto y lo pasa
  hacia abajo (ADR-022).
* Scopes OAuth — FS-IDN-0007 renderiza agrupaciones de estos permisos; no define un segundo modelo.

## Comportamiento *(normativo)*

1. Un permiso es `{module}.{resource}.{action}`, en minúsculas, separado por puntos. Lo crea una
   **ruta declarándolo**, nunca a mano y nunca un tenant.
2. Cada endpoint de la plataforma declara **exactamente uno**. Cero es un defecto; dos también. CI
   impone ambos.
3. El registro se genera en build y se siembra. **La divergencia entre las declaraciones del código y
   la base falla CI** — la misma disciplina que los catálogos paramétricos.
4. Los roles de sistema se siembran por organización: `owner`, `admin`, `member`, `analyst`,
   `support`. Son `is_system` y el tenant no puede editarlos ni borrarlos, solo copiarlos como punto
   de partida.
5. Los roles definidos por el tenant se gatean por plan.
6. Una `role_assignment` tiene **sujeto polimórfico**: una membresía o una credencial de máquina. La
   resolución y la verificación son el mismo camino de código para ambos.
7. La resolución es `sujeto → roles → permisos`, cacheada por sujeto con invalidación al cambiar una
   asignación. Una verificación de permiso hace **cero consultas** en el camino caliente.
8. Los permisos son **solo aditivos**. No hay reglas de denegación: la unión de los roles del sujeto
   es lo que puede hacer. Las reglas de denegación hacen imposible razonar sobre el permiso efectivo,
   y todo administrador que haya depurado una sabe por qué.
9. El rol `owner` siempre tiene todos los permisos, incluidos los futuros.
10. Los permisos sensibles —`core.contacts.read_national_id`, `identity.impersonation.start`,
    `identity.roles.update`— **nunca están en un rol sembrado por debajo de `admin`**, y otorgar uno
    queda auditado con el actor que lo otorgó.

## Datos *(normativo)*

| Tabla                       | Invariantes clave                                                                                                                                          |
| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `identity.permissions`      | `code` TEXT PK con patrón `^[a-z_]+\.[a-z_]+\.[a-z_]+$`; `description`; `is_sensitive`; sembrado desde declaraciones generadas, nunca creado por el tenant |
| `identity.roles`            | `organization_id` nullable (null = plantilla de sistema); `code` único por organización; `is_system`; `is_active`                                          |
| `identity.role_permissions` | (`role_id`, `permission_code`); las filas de un rol de sistema son inmutables                                                                              |
| `identity.role_assignments` | `subject_type` membership\|api\_key\|oauth\_client + `subject_id`; `role_id`; `assigned_by`, `assigned_at`, `revoked_at`; append-only con revocación       |

## API *(normativo)*

| Endpoint                                    | Clase      | Permiso                                 | Presupuesto |
| ------------------------------------------- | ---------- | --------------------------------------- | ----------- |
| `GET /v1/identity/permissions`              | Management | `identity.permissions.read`             | p95 \<1 s   |
| `GET/POST/PATCH /v1/identity/roles`         | Management | `identity.roles.{read\|create\|update}` | p95 \<1 s   |
| `POST /v1/identity/role-assignments`        | Management | `identity.role_assignments.create`      | p95 \<1 s   |
| `DELETE /v1/identity/role-assignments/{id}` | Management | `identity.role_assignments.revoke`      | p95 \<1 s   |

## Eventos *(normativo)*

`identity.role.assigned` y `identity.role.revoked`, con sujeto, rol y actor. No se ofrecen como
webhooks salientes en F1.

## Criterios de aceptación *(normativo)*

1. Una ruta que declara cero o dos permisos falla CI, cada caso con su fixture.
2. Quitar un permiso de las declaraciones dejándolo sembrado falla el chequeo de divergencia
   nombrando el código.
3. Una verificación de permiso hace cero consultas con la caché tibia.
4. Una API key y un usuario con el mismo rol se autorizan idénticamente en todo el conjunto de
   endpoints.
5. Revocar una asignación invalida la caché y surte efecto en el siguiente request, no al expirar.
6. Un tenant no puede editar ni borrar un rol de sistema.
7. Otorgar un permiso sensible escribe una fila de auditoría con el actor.
8. **Negativo:** no existe regla de denegación en el modelo, y ningún endpoint resuelve permisos
   consultando tablas de `identity` desde otro módulo.

## Ejecución

Un solo slice, comando síncrono. El generador y la función de resolución viven en `packages/core`;
los chequeos de CI llegan con TS-005. Las declaraciones de ruta son la fuente: el registro está
aguas abajo del código, nunca arriba.

## Preguntas abiertas

| # | Pregunta                                                                           | Decide | Para                            |
| - | ---------------------------------------------------------------------------------- | ------ | ------------------------------- |
| 1 | ¿Los cinco roles sembrados cubren los casos comunes, o falta uno de "facturación"? | Daniel | antes de aprobar                |
| 2 | ¿Los roles personalizados se gatean por plan o están disponibles para todos?       | Daniel | antes del lanzamiento comercial |

## Changelog

| Versión | Fecha      | Cambio           | Por qué | Autor                  |
| ------- | ---------- | ---------------- | ------- | ---------------------- |
| 0.1.0   | 2026-08-17 | Borrador inicial | —       | daniel + claude-opus-5 |

## Registro de entrega

*Aún no implementado.*
