Saltar a contenido

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). Como accion_id ya pertenece a una página, menu_id es 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:

  1. ver es implícito (ADR 0014): basta con rel_usuario_menu para ver la página; no se emite código.
  2. Las acciones se otorgan solo sobre páginas del usuario: la API valida que exista rel_usuario_menu de (usuario, página, empresa) antes de insertar una acción (regla blanda; no es FK cruzada).
  3. 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.
  4. Otorgamiento en cascada: si se da rrhh.* vía rel_usuario_permiso, cubre todas las acciones; rel_usuario_menu_accion solo agrega detalle cuando no hay wildcard.
  5. 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_permiso con 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 permisos crece 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.