0015. Permisos finos por acción (rel_usuario_menu_accion)¶
Estado: Propuesto
Fecha: 2026-09-08
Autor: Duval Alcivar
Módulo(s) afectado(s): svc-identidad (core_sigfa), frontend (React 18), servicios que validan acciones
Contexto¶
El ADR 0007 define el permiso de navegación user → página
(rel_usuario_menu) y deja como pendiente 0015 la tabla de acciones
finas: poder crear/editar/eliminar (y acciones propias del dominio) en una
página. El menú del módulo y su árbol están en el ADR 0012; las acciones
disponibles por página en cfg_menu_acciones (ADR 0013); el namespace de
códigos en cat_permisos (ADR 0014).
Este ADR define: (1) el DDL de rel_usuario_menu_accion, (2) sus reglas de
resolución (unión con rel_usuario_permiso para armar el claim permisos),
y (3) el endpoint de asignación.
Decisión¶
1. DDL rel_usuario_menu_accion¶
Asignación user → acción de una página. accion_id referencia
cfg_menu_acciones (acción de la página); el menu_id se guarda también para
consultas legibles (es el mismo que el de la acción).
| Campo | Tipo | Notas |
|---|---|---|
id |
UUID PK | |
empresa_id |
BIGINT NOT NULL | RLS |
usuario_id |
UUID NOT NULL | FK → mae_usuarios |
menu_id |
UUID NOT NULL | FK → cfg_menu (página) |
accion_id |
UUID NOT NULL | FK → cfg_menu_acciones |
is_activo |
BOOLEAN DEFAULT TRUE | |
| auditoría estándar | created_at, updated_at, deleted_at, created_by, updated_by |
UNIQUE
(usuario_id, accion_id, empresa_id). Comoaccion_idya pertenece a una página,menu_ides redundante pero se conserva para leer "las acciones que le di en esta página" sin joins.
CREATE TABLE core.identidad.rel_usuario_menu_accion (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
empresa_id BIGINT NOT NULL,
usuario_id UUID NOT NULL,
menu_id UUID NOT NULL,
accion_id UUID NOT NULL,
is_activo BOOLEAN DEFAULT TRUE,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now(),
deleted_at TIMESTAMPTZ NULL,
created_by UUID,
updated_by UUID,
CONSTRAINT uq_rel_usuario_menu_accion UNIQUE (usuario_id, accion_id, empresa_id),
CONSTRAINT fk_rel_usuario_menu_accion_usuario
FOREIGN KEY (usuario_id) REFERENCES core.identidad.mae_usuarios(id),
CONSTRAINT fk_rel_usuario_menu_accion_menu
FOREIGN KEY (menu_id) REFERENCES core.identidad.cfg_menu(id),
CONSTRAINT fk_rel_usuario_menu_accion_accion
FOREIGN KEY (accion_id) REFERENCES core.identidad.cfg_menu_acciones(id)
);
ALTER TABLE core.identidad.rel_usuario_menu_accion ENABLE ROW LEVEL SECURITY;
CREATE POLICY aislamiento_empresa ON core.identidad.rel_usuario_menu_accion
USING (empresa_id = NULLIF(current_setting('app.current_empresa_id', true), '')::BIGINT);
class RelUsuarioMenuAccion(Base):
__tablename__ = "rel_usuario_menu_accion"
__table_args__ = (
UniqueConstraint("usuario_id", "accion_id", "empresa_id", name="uq_rel_usuario_menu_accion"),
{"schema": "core.identidad"},
)
id: Mapped[uuid.UUID] = mapped_column(primary_key=True, default=uuid.uuid4)
empresa_id: Mapped[int] = mapped_column(BigInteger, nullable=False)
usuario_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("core.identidad.mae_usuarios.id"), nullable=False)
menu_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("core.identidad.cfg_menu.id"), nullable=False)
accion_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("core.identidad.cfg_menu_acciones.id"), nullable=False)
is_activo: Mapped[bool] = mapped_column(Boolean, default=True)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
deleted_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
created_by: Mapped[uuid.UUID | None] = mapped_column(as_uuid=True)
updated_by: Mapped[uuid.UUID | None] = mapped_column(as_uuid=True)
2. Reglas de resolución (cómo se arma el claim permisos)¶
En el login (ADR 0011) y al renovar, svc-identidad calcula el claim
permisos uniendo dos fuentes:
permisos del token = códigos de rel_usuario_permiso (ADR 0007)
∪ códigos de rel_usuario_menu_accion (esta tabla)
∪ (si root → vacío/omitido: todo implícito)
Reglas de negocio:
veres implícito (ADR 0014): basta conrel_usuario_menupara ver la página; no se emite código.- Las acciones se otorgan solo sobre páginas del usuario: la API valida
que exista
rel_usuario_menude (usuario, página, empresa) antes de insertar una acción (regla blanda; no es FK cruzada). - Un otorgamiento de acción produce el código
<modulo>.<pagina>.<accion>en el claim; el servicio destino valida con el wildcard.*del ADR 0014. - Otorgamiento en cascada: si se da
rrhh.*víarel_usuario_permiso, cubre todas las acciones;rel_usuario_menu_accionsolo agrega detalle cuando no hay wildcard. - Caducidad inmediata: quitar la fila (soft-delete) quita la acción en el próximo login/recarga del menú (no se revocan tokens ya firmados; ADR 0011).
3. Endpoint¶
| Endpoint | Método | Qué hace |
|---|---|---|
PUT /api/v1/usuarios/{id}/menu/{menu_id}/acciones |
PUT | reemplaza las acciones del usuario en una página (declarativo: envía accion_ids completos) |
GET /api/v1/usuarios/{id}/menu/{menu_id}/acciones |
GET | acciones otorgadas en la página |
Ejemplo (mismo estilo declarativo que el ADR 0009):
PUT /api/v1/usuarios/{id}/menu/{menu_id}/acciones
{ "accion_ids": ["<cfg_menu_acciones.nomina.crear>", "<...editar>"] }
// respuesta: las acciones efectivas de la página (persistidas en
// rel_usuario_menu_accion + is_activo)
Alternativas descartadas¶
- Solo
rel_usuario_permisocon códigos sueltos (sin tabla por página) — la asignación por página es lo que la UI de gestión pide ("qué puede hacer este usuario en nómina"); modelarla suelta obliga al admin a conocer códigos, no páginas. - Permisos por rol/paquete de acciones — descartado por regla de negocio (ADR 0007): no existen perfiles; la asignación es individual.
- Acciones evaluadas con consulta por petición — rompe "validar sin consultar BD"; se resuelven en el claim en el login.
Consecuencias¶
- Un usuario puede ver (página otorgada, ADR 0007) sin poder ejecutar
nada más; cada acción debe otorgarse con esta tabla o con
rel_usuario_permiso. - El claim
permisoscrece en ~1 código por acción otorgada; los resúmenes.*(ADR 0014) lo mantienen chico para jefes de módulo. - La UI de permisos marca casillas por página y acción; el guardado es declarativo e idempotente (compatible con el ADR 0009).
- Resuelto: pendiente 0015 del README (acciones finas) y el "deuda" registrado en los ADR 0007 y 0012.
La definición del permiso como concepto y de rel_usuario_menu está en el
ADR 0007; la navegación en ejecución en el ADR 0012; la firma del claim en el
ADR 0011; el seed de acciones en el ADR 0013.