Saltar a contenido

0007. Tipos de usuario y permisos individuales

Estado: Propuesto Fecha: 2026-09-08 Autor: Duval Alcivar Módulo(s) afectado(s): svc-identidad (core_sigfa), todos los servicios que validan permisos en el JWT

Contexto

svc-identidad es el dueño de la identidad y de los permisos de toda la plataforma. El objetivo: que cada microservicio identifique al usuario y, con su token, sepa qué permisos tiene, sin consultar su propia tabla de seguridad (opción centralizada).

Reglas de negocio:

  • No existen perfiles ni roles dinámicos que agrupen permisos. El tipo de cuenta vive en el catálogo cat_tipos_usuario: usuario (por defecto), root y externo (usuario externo al grupo, ADR 0022). El tipo es general a la cuenta, no depende de la empresa.
  • root tiene permiso implícito a todo: no requiere asignaciones.
  • Los permisos se asignan individualmente a cada usuario, referenciando módulos/páginas. Como los permisos se definen por módulo, y un módulo vale igual sin importar la empresa (además se pueden crear módulos específicos para cada empresa, ADR 0006), el tipo no necesita ligarse a una empresa.

Decisión

1. cat_tipos_usuario y mae_usuarios.tipo_usuario_id — tipo de usuario

El tipo de usuario es un catálogo (cat_tipos_usuario, global y cacheable). mae_usuarios.tipo_usuario_id lo referencia (FK); ya no es un CHECK. Vive en mae_usuarios (DDL en el ADR 0005); aquí se definen su semántica y sus valores. Reemplaza al CHECK (tipo_usuario IN ('usuario','root')) del ADR 0005 original: agregar un tipo solo inserta una fila, sin ALTER TABLE.

Código Efecto
usuario (por defecto) Solo lo que le fue asignado individualmente
root Acceso a todo, implícito (no consulta permisos)
externo Usuario externo al grupo (persona natural o jurídica, ADR 0022); permisos individuales
CREATE TABLE core.identidad.cat_tipos_usuario (
    id     UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    codigo VARCHAR(20) NOT NULL,           -- usuario | root | externo
    nombre VARCHAR(100) NOT NULL,
    -- auditoría estándar
    CONSTRAINT uq_cat_tipos_usuario_codigo UNIQUE (codigo)
);

-- columna definida en el ADR 0005 (mae_usuarios)
tipo_usuario_id UUID NOT NULL
    REFERENCES core.identidad.cat_tipos_usuario(id)

2. cat_permisos — catálogo de permisos (generales y por módulo)

Un permiso puede ser general (operación global, ej. reportes.exportar) o por módulo/página (ej. rrhh.nomina.ver, inventario.bajas.crear).

Campo Tipo Notas
id UUID PK
empresa_id BIGINT NULL = permiso de plataforma/global; NOT NULL = permiso de empresa (RLS)
aplicacion_id UUID FK → cat_aplicaciones (ADR 0006), opcional
modulo_id UUID FK → cfg_modulos (ADR 0006), opcional
menu_id UUID FK → cfg_menu (ADR 0006), opcional
codigo VARCHAR(100) NOT NULL ej. rrhh.nomina.ver
nombre VARCHAR(150) NOT NULL
descripcion TEXT
is_activo BOOLEAN DEFAULT TRUE
auditoría estándar created_at, updated_at, deleted_at, created_by, updated_by

El alcance del permiso se deduce de sus FKs: sin FK → global; con modulo_id/menu_id → por módulo/página. El codigo es único por (empresa nullable).

CREATE TABLE core.identidad.cat_permisos (
    id             UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    empresa_id     BIGINT,
    aplicacion_id  UUID,
    modulo_id      UUID,
    menu_id        UUID,
    codigo         VARCHAR(100) NOT NULL,
    nombre         VARCHAR(150) NOT NULL,
    descripcion    TEXT,
    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_cat_permisos_empresa_codigo UNIQUE (empresa_id, codigo),
    CONSTRAINT fk_cat_permisos_aplicacion
        FOREIGN KEY (aplicacion_id) REFERENCES core.identidad.cat_aplicaciones(id),
    CONSTRAINT fk_cat_permisos_modulo
        FOREIGN KEY (modulo_id) REFERENCES core.identidad.cfg_modulos(id),
    CONSTRAINT fk_cat_permisos_menu
        FOREIGN KEY (menu_id) REFERENCES core.identidad.cfg_menu(id)
);

3. rel_usuario_permiso — permiso asignado individualmente

Asignación directa usuario ↔ permiso (sin roles intermedios). El campo empresa_id acompaña el alcance del permiso: NULL si el permiso es global, NOT NULL (RLS) si el permiso es de un módulo/página de una empresa.

Campo Tipo Notas
id UUID PK
usuario_id UUID NOT NULL FK → mae_usuarios (ADR 0005)
permiso_id UUID NOT NULL FK → cat_permisos
empresa_id BIGINT NULL = global; NOT NULL = por empresa (RLS)
is_activo BOOLEAN DEFAULT TRUE
auditoría estándar

UNIQUE (usuario_id, permiso_id, empresa_id). Asignar un permiso a un root es redundante (no hace falta): el root ya tiene todo.

CREATE TABLE core.identidad.rel_usuario_permiso (
    id         UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    usuario_id UUID NOT NULL,
    permiso_id UUID NOT NULL,
    empresa_id BIGINT,
    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_permiso UNIQUE (usuario_id, permiso_id, empresa_id),
    CONSTRAINT fk_rel_usuario_permiso_usuario
        FOREIGN KEY (usuario_id) REFERENCES core.identidad.mae_usuarios(id),
    CONSTRAINT fk_rel_usuario_permiso_permiso
        FOREIGN KEY (permiso_id) REFERENCES core.identidad.cat_permisos(id)
);

ALTER TABLE core.identidad.rel_usuario_permiso ENABLE ROW LEVEL SECURITY;
CREATE POLICY aislamiento_empresa ON core.identidad.rel_usuario_permiso
    USING (empresa_id IS NULL
        OR empresa_id = NULLIF(current_setting('app.current_empresa_id', true), '')::BIGINT);

4. rel_usuario_menu — qué páginas del menú ve cada usuario

Conecta directamente user ↔ página del menú (cfg_menu, ADR 0006). Es la tabla que hace que "permiso → pantalla" sea dato, no código. La resolución del menú en ejecución está en el ADR 0012.

Campo Tipo Notas
id UUID PK
empresa_id BIGINT NOT NULL RLS — el menú se configura por empresa
usuario_id UUID NOT NULL FK → mae_usuarios
menu_id UUID NOT NULL FK → cfg_menu (página permitida)
created_at / updated_at TIMESTAMPTZ default now()
deleted_at TIMESTAMPTZ NULL
created_by / updated_by UUID

UNIQUE (usuario_id, menu_id, empresa_id). Las acciones finas por página (crear/editar/eliminar) se definen en rel_usuario_menu_accion (ADR 0015).

CREATE TABLE core.identidad.rel_usuario_menu (
    id          UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    empresa_id  BIGINT NOT NULL,
    usuario_id  UUID NOT NULL,
    menu_id     UUID NOT NULL,
    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 UNIQUE (usuario_id, menu_id, empresa_id),
    CONSTRAINT fk_rel_usuario_menu_usuario
        FOREIGN KEY (usuario_id) REFERENCES core.identidad.mae_usuarios(id),
    CONSTRAINT fk_rel_usuario_menu_menu
        FOREIGN KEY (menu_id) REFERENCES core.identidad.cfg_menu(id)
);

ALTER TABLE core.identidad.rel_usuario_menu ENABLE ROW LEVEL SECURITY;
CREATE POLICY aislamiento_empresa ON core.identidad.rel_usuario_menu
    USING (empresa_id = NULLIF(current_setting('app.current_empresa_id', true), '')::BIGINT);

5. Cómo se resuelve en el token

  1. Login (ADR 0010) → svc-identidad lee el código del tipo desde cat_tipos_usuario (vía mae_usuarios.tipo_usuario_id).
  2. root → el JWT se firma con tipo_usuario: "root"; los servicios validan "root ⇒ todo" sin consultar ninguna tabla de permisos.
  3. usuario → el JWT se firma con tipo_usuario: "usuario" y permisos: ["rrhh.nomina.ver", ...] (los códigos de rel_usuario_permiso de la empresa activa; ADR 0011).
  4. El menú/panel se construye con rel_usuario_menu (ADR 0012); la autorización de cada operación la valida el servicio destino con los permisos del token.

Resumen de tablas

Tabla empresa_id RLS Descripción
cat_tipos_usuario no Catálogo de tipos (usuario/root/externo)
mae_usuarios.tipo_usuario_id no FK al catálogo, columna en ADR 0005
cat_permisos BIGINT/null mixto Catálogo de permisos generales y por módulo
rel_usuario_permiso BIGINT/null mixto Permiso asignado individualmente a un usuario
rel_usuario_menu BIGINT Páginas del menú que ve el usuario

Alternativas descartadas

  • Perfiles/roles dinámicos (cat_roles, rel_rol_usuario, rel_rol_permiso, rel_rol_menu) — descartado por regla de negocio: no existen perfiles; solo los tipos del catálogo cat_tipos_usuario.
  • Mantener el CHECK de tipo_usuario — agregar un tipo (p. ej. externo) exigiría ALTER TABLE revalidando la tabla; el catálogo + FK solo inserta una fila.
  • Permisos distribuidos por producto (ERP y CRM aparte) — duplica administración, impide SSO real y da dos fuentes de verdad.
  • Filtrar el menú solo en frontend — inseguro: cualquiera con la URL directa accedería; el filtro nace en el backend.

Consecuencias

  • El JWT trae tipo_usuario + permisos (códigos) → todos los servicios validan sin consultar BD (ADR 0011).
  • root no necesita configuración: acceso a todo por su tipo.
  • Los permisos viven en un solo lugar (svc-identidad) y se asignan individualmente por usuario, siempre referenciando un módulo/página.
  • Como el exigente del backend es el token, si la lista de permisos de un usuario crece demasiado (> ~40 códigos) se recomienda asignar el resumen .<módulo>.* (ADR 0014); la resolución es local con wildcard.
  • Resuelto: rel_usuario_menu_accion (acciones finas) → ADR 0015; el catálogo inicial de permisos por módulo/acción → ADR 0014.