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),rootyexterno(usuario externo al grupo, ADR 0022). El tipo es general a la cuenta, no depende de la empresa. roottiene 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. Elcodigoes ú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 unrootes redundante (no hace falta): elrootya 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 enrel_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¶
- Login (ADR 0010) →
svc-identidadlee el código del tipo desdecat_tipos_usuario(víamae_usuarios.tipo_usuario_id). root→ el JWT se firma contipo_usuario: "root"; los servicios validan "root ⇒ todo" sin consultar ninguna tabla de permisos.usuario→ el JWT se firma contipo_usuario: "usuario"ypermisos: ["rrhh.nomina.ver", ...](los códigos derel_usuario_permisode la empresa activa; ADR 0011).- El menú/panel se construye con
rel_usuario_menu(ADR 0012); la autorización de cada operación la valida el servicio destino con lospermisosdel 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 | sí | 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álogocat_tipos_usuario. - Mantener el
CHECKdetipo_usuario— agregar un tipo (p. ej.externo) exigiríaALTER TABLErevalidando 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). rootno 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.