panels-origin/db/iam/changesets/001-baseline.sql
Cursor Agent b87b0205a8
db: migrar infraestructura de BD a Postgres (fase 0-1)
- Fase 0: scripts de aprovisionamiento (db/provision/) para roles, dos
  bases de datos separadas (panels_platform / panels_product con
  esquemas iam+core) y ACLs de Redis por modulo, con verificacion
  automatizada de aislamiento (verify-isolation.sh) y setup local
  reproducible (dev-local.sh).
- Fase 1: changelogs de Liquibase reescritos para Postgres
  (db/platform, db/iam, db/core reemplazan db/app + los changesets
  SQLite de platform). Baseline como estado final (no replay literal),
  tipos traducidos (IDENTITY, TIMESTAMPTZ/DATE, NUMERIC, BOOLEAN,
  CITEXT), contexts dev vs. schema/catalogos, RLS por tenant_id como
  defensa en profundidad, uploaded_by/created_by como snapshot
  desnormalizado (sin FK hacia iam).
- Migraciones ya no corren en el arranque de la API: paso explicito de
  deploy via db/update.sh con credenciales _owner.

Verificado end-to-end contra Postgres 16 + Redis local.

Co-authored-by: alberto.martinez <alberto.martinez@mrdev.mx>
2026-09-02 20:00:49 +00:00

92 lines
4.3 KiB
SQL

--liquibase formatted sql
-- PANELS · panels_product.iam (Postgres) · baseline
--
-- Identidad y RBAC DE CADA TENANT (ej. empleados de ARCTEC que usan
-- web-panel). No confundir con panels_platform.platform_users (identidad
-- de PANELS como operador SaaS) -- son dos sistemas de identidad distintos
-- que no comparten tabla ni base de datos.
--
-- Este esquema NO tiene FK hacia `core` (ver reglas del monolito modular
-- en el plan): documents.uploaded_by/badge_jobs.created_by pasan a
-- snapshot desnormalizado en `core`, y users.company_id es una referencia
-- lógica a core.companies(id) sin FK real.
--changeset panel:iam-001a-roles endDelimiter:; splitStatements:true
--preconditions onFail:MARK_RAN
--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='iam' AND table_name='roles'
CREATE TABLE iam.roles (
code TEXT PRIMARY KEY,
label TEXT NOT NULL
);
INSERT INTO iam.roles (code, label) VALUES
('tenant_admin', 'Administrador del tenant'),
('user', 'Usuario operativo');
--changeset panel:iam-001b-permissions endDelimiter:; splitStatements:true
--preconditions onFail:MARK_RAN
--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='iam' AND table_name='permissions'
--comment: Catálogo base de permisos. El código de la app hoy sigue gateando
-- por role_code (igual que el string "role" anterior); esta tabla es la
-- base para pasar a chequeos granulares por permiso de forma incremental,
-- sin bloquear la migración a Postgres en ese rediseño más grande.
CREATE TABLE iam.permissions (
code TEXT PRIMARY KEY,
label TEXT NOT NULL
);
INSERT INTO iam.permissions (code, label) VALUES
('manage_workers', 'Alta, edición y expediente de personal'),
('manage_projects', 'Alta y edición de obras'),
('manage_companies', 'Alta y edición de empresas'),
('manage_budget', 'Presupuesto de obra'),
('manage_payroll', 'Nómina y préstamos'),
('manage_documents', 'Documentos de personal/obra/empresa'),
('manage_users', 'Alta y edición de usuarios del tenant'),
('manage_settings', 'Configuración del tenant (zona horaria, etc.)'),
('view_reports', 'Reportes y tableros');
--changeset panel:iam-001c-role-permissions endDelimiter:; splitStatements:true
--preconditions onFail:MARK_RAN
--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='iam' AND table_name='role_permissions'
CREATE TABLE iam.role_permissions (
role_code TEXT NOT NULL REFERENCES iam.roles(code) ON DELETE CASCADE,
permission_code TEXT NOT NULL REFERENCES iam.permissions(code) ON DELETE CASCADE,
PRIMARY KEY (role_code, permission_code)
);
INSERT INTO iam.role_permissions (role_code, permission_code)
SELECT 'tenant_admin', code FROM iam.permissions;
INSERT INTO iam.role_permissions (role_code, permission_code) VALUES
('user', 'manage_workers'),
('user', 'manage_documents'),
('user', 'view_reports');
--changeset panel:iam-001d-users endDelimiter:; splitStatements:true
--preconditions onFail:MARK_RAN
--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='iam' AND table_name='users'
CREATE TABLE iam.users (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
username TEXT NOT NULL UNIQUE,
password_hash TEXT NOT NULL,
display_name TEXT NOT NULL,
-- Referencia lógica a core.companies(id) -- SIN FK real (iam y core están
-- aislados a propósito; ver reglas del monolito modular).
company_id INTEGER,
-- Referencia lógica a panels_platform.tenants(id) -- SIN FK real (bases
-- de datos distintas, Postgres no permite FK entre databases).
tenant_id INTEGER,
role_code TEXT NOT NULL DEFAULT 'user' REFERENCES iam.roles(code),
must_change_password BOOLEAN NOT NULL DEFAULT false,
email TEXT NOT NULL DEFAULT '',
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_iam_users_tenant ON iam.users(tenant_id);
-- Nota: el primer usuario (platform_admin y el tenant_admin de cada tenant
-- nuevo) NO se siembra aquí. Un password_hash real requiere PBKDF2 (mismo
-- algoritmo que api/crypto.ts), que Liquibase/SQL no puede calcular. Ese
-- bootstrap es un comando explícito y one-shot: scripts/bootstrap-admin.ts
-- (Fase 4), no lógica de seed en el arranque de la app ni en un changeset.