892 lines
30 KiB
PL/PgSQL
892 lines
30 KiB
PL/PgSQL
-- =====================================================
|
|
-- SCHEMA: auth (Extensiones)
|
|
-- PROPÓSITO: 2FA, API Keys, OAuth2, Grupos, ACL, Record Rules
|
|
-- MÓDULOS: MGN-001 (Fundamentos), MGN-002 (Usuarios), MGN-003 (Roles)
|
|
-- FECHA: 2025-12-08
|
|
-- VERSION: 1.0.0
|
|
-- DEPENDENCIAS: 01-auth.sql
|
|
-- SPECS RELACIONADAS:
|
|
-- - SPEC-TWO-FACTOR-AUTHENTICATION.md
|
|
-- - SPEC-SEGURIDAD-API-KEYS-PERMISOS.md
|
|
-- - SPEC-OAUTH2-SOCIAL-LOGIN.md
|
|
-- =====================================================
|
|
|
|
-- =====================================================
|
|
-- PARTE 1: GROUPS Y HERENCIA
|
|
-- =====================================================
|
|
|
|
-- Tabla: groups (Grupos de usuarios con herencia)
|
|
CREATE TABLE auth.groups (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
|
|
code VARCHAR(100) NOT NULL,
|
|
name VARCHAR(255) NOT NULL,
|
|
description TEXT,
|
|
|
|
-- Configuración
|
|
is_system BOOLEAN NOT NULL DEFAULT FALSE, -- Grupos del sistema no editables
|
|
category VARCHAR(100), -- Categoría para agrupación (ventas, compras, etc.)
|
|
color VARCHAR(20),
|
|
|
|
-- API Keys
|
|
api_key_max_duration_days INTEGER DEFAULT 30
|
|
CHECK (api_key_max_duration_days >= 0), -- 0 = sin expiración (solo grupos system)
|
|
|
|
-- Auditoría
|
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
created_by UUID REFERENCES auth.users(id),
|
|
updated_at TIMESTAMP,
|
|
updated_by UUID REFERENCES auth.users(id),
|
|
deleted_at TIMESTAMP,
|
|
deleted_by UUID REFERENCES auth.users(id),
|
|
|
|
CONSTRAINT uq_groups_code_tenant UNIQUE (tenant_id, code)
|
|
);
|
|
|
|
-- Tabla: group_implied (Herencia de grupos)
|
|
CREATE TABLE auth.group_implied (
|
|
group_id UUID NOT NULL REFERENCES auth.groups(id) ON DELETE CASCADE,
|
|
implied_group_id UUID NOT NULL REFERENCES auth.groups(id) ON DELETE CASCADE,
|
|
|
|
PRIMARY KEY (group_id, implied_group_id),
|
|
CONSTRAINT chk_group_no_self_imply CHECK (group_id != implied_group_id)
|
|
);
|
|
|
|
-- Tabla: user_groups (Many-to-Many usuarios-grupos)
|
|
CREATE TABLE auth.user_groups (
|
|
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
|
group_id UUID NOT NULL REFERENCES auth.groups(id) ON DELETE CASCADE,
|
|
assigned_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
assigned_by UUID REFERENCES auth.users(id),
|
|
|
|
PRIMARY KEY (user_id, group_id)
|
|
);
|
|
|
|
-- Índices para groups
|
|
CREATE INDEX idx_groups_tenant_id ON auth.groups(tenant_id);
|
|
CREATE INDEX idx_groups_code ON auth.groups(code);
|
|
CREATE INDEX idx_groups_category ON auth.groups(category);
|
|
CREATE INDEX idx_groups_is_system ON auth.groups(is_system);
|
|
|
|
-- Índices para user_groups
|
|
CREATE INDEX idx_user_groups_user_id ON auth.user_groups(user_id);
|
|
CREATE INDEX idx_user_groups_group_id ON auth.user_groups(group_id);
|
|
|
|
-- =====================================================
|
|
-- PARTE 2: MODELS Y ACL (Access Control Lists)
|
|
-- =====================================================
|
|
|
|
-- Tabla: models (Definición de modelos del sistema)
|
|
CREATE TABLE auth.models (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
|
|
name VARCHAR(128) NOT NULL, -- Nombre técnico (ej: 'sale.order')
|
|
description VARCHAR(255), -- Descripción legible
|
|
module VARCHAR(64), -- Módulo al que pertenece
|
|
is_transient BOOLEAN NOT NULL DEFAULT FALSE, -- Modelo temporal
|
|
|
|
-- Auditoría
|
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMP,
|
|
|
|
CONSTRAINT uq_models_name_tenant UNIQUE (tenant_id, name)
|
|
);
|
|
|
|
-- Tabla: model_access (Permisos CRUD por modelo y grupo)
|
|
CREATE TABLE auth.model_access (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
|
|
name VARCHAR(255) NOT NULL, -- Identificador legible
|
|
|
|
model_id UUID NOT NULL REFERENCES auth.models(id) ON DELETE CASCADE,
|
|
group_id UUID REFERENCES auth.groups(id) ON DELETE RESTRICT, -- NULL = global
|
|
|
|
-- Permisos CRUD
|
|
perm_read BOOLEAN NOT NULL DEFAULT FALSE,
|
|
perm_create BOOLEAN NOT NULL DEFAULT FALSE,
|
|
perm_write BOOLEAN NOT NULL DEFAULT FALSE,
|
|
perm_delete BOOLEAN NOT NULL DEFAULT FALSE,
|
|
|
|
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
|
|
|
-- Auditoría
|
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMP,
|
|
|
|
-- Un grupo solo puede tener un registro por modelo
|
|
CONSTRAINT uq_model_access_model_group UNIQUE (model_id, group_id, tenant_id)
|
|
);
|
|
|
|
-- Índices para models
|
|
CREATE INDEX idx_models_name ON auth.models(name);
|
|
CREATE INDEX idx_models_tenant ON auth.models(tenant_id);
|
|
CREATE INDEX idx_models_module ON auth.models(module);
|
|
|
|
-- Índices para model_access
|
|
CREATE INDEX idx_model_access_model ON auth.model_access(model_id);
|
|
CREATE INDEX idx_model_access_group ON auth.model_access(group_id);
|
|
CREATE INDEX idx_model_access_active ON auth.model_access(is_active) WHERE is_active = TRUE;
|
|
|
|
-- =====================================================
|
|
-- PARTE 3: RECORD RULES (Row-Level Security)
|
|
-- =====================================================
|
|
|
|
-- Tabla: record_rules (Reglas de acceso a nivel de registro)
|
|
CREATE TABLE auth.record_rules (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
|
|
name VARCHAR(255) NOT NULL,
|
|
|
|
model_id UUID NOT NULL REFERENCES auth.models(id) ON DELETE CASCADE,
|
|
|
|
-- Dominio como expresión JSON
|
|
domain_expression JSONB NOT NULL, -- [["company_id", "in", "user.company_ids"]]
|
|
|
|
-- Permisos afectados
|
|
perm_read BOOLEAN NOT NULL DEFAULT TRUE,
|
|
perm_create BOOLEAN NOT NULL DEFAULT TRUE,
|
|
perm_write BOOLEAN NOT NULL DEFAULT TRUE,
|
|
perm_delete BOOLEAN NOT NULL DEFAULT TRUE,
|
|
|
|
-- Regla global (sin grupos = aplica a todos)
|
|
is_global BOOLEAN NOT NULL DEFAULT FALSE,
|
|
|
|
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
|
|
|
-- Auditoría
|
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMP
|
|
);
|
|
|
|
-- Tabla: rule_groups (Relación M:N entre rules y groups)
|
|
CREATE TABLE auth.rule_groups (
|
|
rule_id UUID NOT NULL REFERENCES auth.record_rules(id) ON DELETE CASCADE,
|
|
group_id UUID NOT NULL REFERENCES auth.groups(id) ON DELETE CASCADE,
|
|
|
|
PRIMARY KEY (rule_id, group_id)
|
|
);
|
|
|
|
-- Índices para record_rules
|
|
CREATE INDEX idx_record_rules_model ON auth.record_rules(model_id);
|
|
CREATE INDEX idx_record_rules_global ON auth.record_rules(is_global) WHERE is_global = TRUE;
|
|
CREATE INDEX idx_record_rules_active ON auth.record_rules(is_active) WHERE is_active = TRUE;
|
|
|
|
-- Índices para rule_groups
|
|
CREATE INDEX idx_rule_groups_rule ON auth.rule_groups(rule_id);
|
|
CREATE INDEX idx_rule_groups_group ON auth.rule_groups(group_id);
|
|
|
|
-- =====================================================
|
|
-- PARTE 4: FIELD PERMISSIONS
|
|
-- =====================================================
|
|
|
|
-- Tabla: model_fields (Campos del modelo con metadatos de seguridad)
|
|
CREATE TABLE auth.model_fields (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
|
|
model_id UUID NOT NULL REFERENCES auth.models(id) ON DELETE CASCADE,
|
|
|
|
name VARCHAR(128) NOT NULL, -- Nombre técnico del campo
|
|
field_type VARCHAR(64) NOT NULL, -- Tipo: char, int, many2one, etc.
|
|
description VARCHAR(255), -- Etiqueta legible
|
|
|
|
-- Seguridad por defecto
|
|
is_readonly BOOLEAN NOT NULL DEFAULT FALSE,
|
|
is_required BOOLEAN NOT NULL DEFAULT FALSE,
|
|
|
|
-- Auditoría
|
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
|
|
CONSTRAINT uq_model_field UNIQUE (model_id, name, tenant_id)
|
|
);
|
|
|
|
-- Tabla: field_permissions (Permisos de campo por grupo)
|
|
CREATE TABLE auth.field_permissions (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
|
|
field_id UUID NOT NULL REFERENCES auth.model_fields(id) ON DELETE CASCADE,
|
|
group_id UUID NOT NULL REFERENCES auth.groups(id) ON DELETE CASCADE,
|
|
|
|
-- Permisos
|
|
can_read BOOLEAN NOT NULL DEFAULT TRUE,
|
|
can_write BOOLEAN NOT NULL DEFAULT FALSE,
|
|
|
|
CONSTRAINT uq_field_permission UNIQUE (field_id, group_id, tenant_id)
|
|
);
|
|
|
|
-- Índices para model_fields
|
|
CREATE INDEX idx_model_fields_model ON auth.model_fields(model_id);
|
|
CREATE INDEX idx_model_fields_name ON auth.model_fields(name);
|
|
|
|
-- Índices para field_permissions
|
|
CREATE INDEX idx_field_permissions_field ON auth.field_permissions(field_id);
|
|
CREATE INDEX idx_field_permissions_group ON auth.field_permissions(group_id);
|
|
|
|
-- =====================================================
|
|
-- PARTE 5: API KEYS
|
|
-- =====================================================
|
|
|
|
-- Tabla: api_keys (Autenticación para integraciones)
|
|
CREATE TABLE auth.api_keys (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
|
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
|
|
|
|
-- Descripción
|
|
name VARCHAR(255) NOT NULL, -- Descripción del propósito
|
|
|
|
-- Seguridad
|
|
key_index VARCHAR(16) NOT NULL, -- Primeros 8 bytes del key (para lookup rápido)
|
|
key_hash VARCHAR(255) NOT NULL, -- Hash PBKDF2-SHA512 del key completo
|
|
|
|
-- Scope y restricciones
|
|
scope VARCHAR(100), -- NULL = acceso completo, 'rpc' = solo API
|
|
allowed_ips INET[], -- IPs permitidas (opcional)
|
|
|
|
-- Expiración
|
|
expiration_date TIMESTAMPTZ, -- NULL = sin expiración (solo system users)
|
|
last_used_at TIMESTAMPTZ, -- Último uso
|
|
|
|
-- Estado
|
|
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
|
|
|
-- Auditoría
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
revoked_at TIMESTAMPTZ,
|
|
revoked_by UUID REFERENCES auth.users(id),
|
|
|
|
-- Constraints
|
|
CONSTRAINT chk_key_index_length CHECK (LENGTH(key_index) = 16)
|
|
);
|
|
|
|
-- Índices para API Keys
|
|
CREATE INDEX idx_api_keys_lookup ON auth.api_keys (key_index, is_active)
|
|
WHERE is_active = TRUE;
|
|
CREATE INDEX idx_api_keys_expiration ON auth.api_keys (expiration_date)
|
|
WHERE expiration_date IS NOT NULL;
|
|
CREATE INDEX idx_api_keys_user ON auth.api_keys (user_id);
|
|
CREATE INDEX idx_api_keys_tenant ON auth.api_keys (tenant_id);
|
|
|
|
-- =====================================================
|
|
-- PARTE 6: TWO-FACTOR AUTHENTICATION (2FA)
|
|
-- =====================================================
|
|
|
|
-- Extensión de users para MFA
|
|
ALTER TABLE auth.users ADD COLUMN IF NOT EXISTS
|
|
mfa_enabled BOOLEAN NOT NULL DEFAULT FALSE;
|
|
|
|
ALTER TABLE auth.users ADD COLUMN IF NOT EXISTS
|
|
mfa_method VARCHAR(16) DEFAULT 'none'
|
|
CHECK (mfa_method IN ('none', 'totp', 'sms', 'email'));
|
|
|
|
ALTER TABLE auth.users ADD COLUMN IF NOT EXISTS
|
|
mfa_secret BYTEA; -- Secreto TOTP encriptado con AES-256-GCM
|
|
|
|
ALTER TABLE auth.users ADD COLUMN IF NOT EXISTS
|
|
backup_codes JSONB DEFAULT '[]'; -- Códigos de respaldo (array de hashes SHA-256)
|
|
|
|
ALTER TABLE auth.users ADD COLUMN IF NOT EXISTS
|
|
backup_codes_count INTEGER NOT NULL DEFAULT 0;
|
|
|
|
ALTER TABLE auth.users ADD COLUMN IF NOT EXISTS
|
|
mfa_setup_at TIMESTAMPTZ;
|
|
|
|
ALTER TABLE auth.users ADD COLUMN IF NOT EXISTS
|
|
last_2fa_verification TIMESTAMPTZ;
|
|
|
|
-- Constraint de consistencia MFA
|
|
ALTER TABLE auth.users ADD CONSTRAINT chk_mfa_consistency CHECK (
|
|
(mfa_enabled = TRUE AND mfa_secret IS NOT NULL AND mfa_method != 'none') OR
|
|
(mfa_enabled = FALSE)
|
|
);
|
|
|
|
-- Índice para usuarios con MFA
|
|
CREATE INDEX idx_users_mfa_enabled ON auth.users(mfa_enabled) WHERE mfa_enabled = TRUE;
|
|
|
|
-- Tabla: trusted_devices (Dispositivos de confianza)
|
|
CREATE TABLE auth.trusted_devices (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
|
|
-- Relación con usuario
|
|
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
|
|
|
-- Identificación del dispositivo
|
|
device_fingerprint VARCHAR(128) NOT NULL,
|
|
device_name VARCHAR(128), -- "iPhone de Juan", "Chrome en MacBook"
|
|
device_type VARCHAR(32), -- 'mobile', 'desktop', 'tablet'
|
|
|
|
-- Información del dispositivo
|
|
user_agent TEXT,
|
|
browser_name VARCHAR(64),
|
|
browser_version VARCHAR(32),
|
|
os_name VARCHAR(64),
|
|
os_version VARCHAR(32),
|
|
|
|
-- Ubicación del registro
|
|
registered_ip INET NOT NULL,
|
|
registered_location JSONB, -- {country, city, lat, lng}
|
|
|
|
-- Estado de confianza
|
|
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
|
trust_level VARCHAR(16) NOT NULL DEFAULT 'standard'
|
|
CHECK (trust_level IN ('standard', 'high', 'temporary')),
|
|
trust_expires_at TIMESTAMPTZ, -- NULL = no expira
|
|
|
|
-- Uso
|
|
last_used_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
last_used_ip INET,
|
|
use_count INTEGER NOT NULL DEFAULT 1,
|
|
|
|
-- Auditoría
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
revoked_at TIMESTAMPTZ,
|
|
revoked_reason VARCHAR(128),
|
|
|
|
-- Constraints
|
|
CONSTRAINT uk_trusted_device_user_fingerprint UNIQUE (user_id, device_fingerprint)
|
|
);
|
|
|
|
-- Índices para trusted_devices
|
|
CREATE INDEX idx_trusted_devices_user ON auth.trusted_devices(user_id) WHERE is_active;
|
|
CREATE INDEX idx_trusted_devices_fingerprint ON auth.trusted_devices(device_fingerprint);
|
|
CREATE INDEX idx_trusted_devices_expires ON auth.trusted_devices(trust_expires_at)
|
|
WHERE trust_expires_at IS NOT NULL AND is_active;
|
|
|
|
-- Tabla: verification_codes (Códigos de verificación temporales)
|
|
CREATE TABLE auth.verification_codes (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
|
|
-- Relaciones
|
|
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
|
session_id UUID REFERENCES auth.sessions(id) ON DELETE CASCADE,
|
|
|
|
-- Tipo de código
|
|
code_type VARCHAR(16) NOT NULL
|
|
CHECK (code_type IN ('totp_setup', 'sms', 'email', 'backup')),
|
|
|
|
-- Código (hash SHA-256)
|
|
code_hash VARCHAR(64) NOT NULL,
|
|
code_length INTEGER NOT NULL DEFAULT 6,
|
|
|
|
-- Destino (para SMS/Email)
|
|
destination VARCHAR(256), -- Teléfono o email
|
|
|
|
-- Intentos
|
|
attempts INTEGER NOT NULL DEFAULT 0,
|
|
max_attempts INTEGER NOT NULL DEFAULT 5,
|
|
|
|
-- Validez
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
expires_at TIMESTAMPTZ NOT NULL,
|
|
used_at TIMESTAMPTZ,
|
|
|
|
-- Metadata
|
|
ip_address INET,
|
|
user_agent TEXT,
|
|
|
|
-- Constraint
|
|
CONSTRAINT chk_code_not_expired CHECK (used_at IS NULL OR used_at <= expires_at)
|
|
);
|
|
|
|
-- Índices para verification_codes
|
|
CREATE INDEX idx_verification_codes_user ON auth.verification_codes(user_id, code_type)
|
|
WHERE used_at IS NULL;
|
|
CREATE INDEX idx_verification_codes_expires ON auth.verification_codes(expires_at)
|
|
WHERE used_at IS NULL;
|
|
|
|
-- Tabla: mfa_audit_log (Log de auditoría MFA)
|
|
CREATE TABLE auth.mfa_audit_log (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
|
|
-- Usuario
|
|
user_id UUID NOT NULL REFERENCES auth.users(id),
|
|
|
|
-- Evento
|
|
event_type VARCHAR(32) NOT NULL
|
|
CHECK (event_type IN (
|
|
'mfa_setup_initiated',
|
|
'mfa_setup_completed',
|
|
'mfa_disabled',
|
|
'totp_verified',
|
|
'totp_failed',
|
|
'backup_code_used',
|
|
'backup_codes_regenerated',
|
|
'device_trusted',
|
|
'device_revoked',
|
|
'anomaly_detected',
|
|
'account_locked',
|
|
'account_unlocked'
|
|
)),
|
|
|
|
-- Resultado
|
|
success BOOLEAN NOT NULL,
|
|
failure_reason VARCHAR(128),
|
|
|
|
-- Contexto
|
|
ip_address INET,
|
|
user_agent TEXT,
|
|
device_fingerprint VARCHAR(128),
|
|
location JSONB,
|
|
|
|
-- Metadata adicional
|
|
metadata JSONB DEFAULT '{}',
|
|
|
|
-- Timestamp
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
|
|
-- Índices para mfa_audit_log
|
|
CREATE INDEX idx_mfa_audit_user ON auth.mfa_audit_log(user_id, created_at DESC);
|
|
CREATE INDEX idx_mfa_audit_event ON auth.mfa_audit_log(event_type, created_at DESC);
|
|
CREATE INDEX idx_mfa_audit_failures ON auth.mfa_audit_log(user_id, created_at DESC)
|
|
WHERE success = FALSE;
|
|
|
|
-- =====================================================
|
|
-- PARTE 7: OAUTH2 PROVIDERS
|
|
-- =====================================================
|
|
|
|
-- Tabla: oauth_providers (Proveedores OAuth2)
|
|
CREATE TABLE auth.oauth_providers (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
tenant_id UUID REFERENCES auth.tenants(id) ON DELETE CASCADE, -- NULL = global
|
|
|
|
code VARCHAR(50) NOT NULL,
|
|
name VARCHAR(100) NOT NULL,
|
|
|
|
-- Configuración OAuth2
|
|
client_id VARCHAR(255) NOT NULL,
|
|
client_secret VARCHAR(500), -- Encriptado con AES-256
|
|
|
|
-- Endpoints OAuth2
|
|
authorization_endpoint VARCHAR(500) NOT NULL,
|
|
token_endpoint VARCHAR(500) NOT NULL,
|
|
userinfo_endpoint VARCHAR(500) NOT NULL,
|
|
jwks_uri VARCHAR(500), -- Para validación de ID tokens
|
|
|
|
-- Scopes y parámetros
|
|
scope VARCHAR(500) NOT NULL DEFAULT 'openid profile email',
|
|
response_type VARCHAR(50) NOT NULL DEFAULT 'code',
|
|
|
|
-- PKCE Configuration
|
|
pkce_enabled BOOLEAN NOT NULL DEFAULT TRUE,
|
|
code_challenge_method VARCHAR(10) DEFAULT 'S256',
|
|
|
|
-- Mapeo de claims
|
|
claim_mapping JSONB NOT NULL DEFAULT '{
|
|
"sub": "oauth_uid",
|
|
"email": "email",
|
|
"name": "name",
|
|
"picture": "avatar_url"
|
|
}'::jsonb,
|
|
|
|
-- UI
|
|
icon_class VARCHAR(100), -- fa-google, fa-microsoft, etc.
|
|
button_text VARCHAR(100),
|
|
button_color VARCHAR(20),
|
|
display_order INTEGER NOT NULL DEFAULT 10,
|
|
|
|
-- Estado
|
|
is_enabled BOOLEAN NOT NULL DEFAULT FALSE,
|
|
is_visible BOOLEAN NOT NULL DEFAULT TRUE,
|
|
|
|
-- Restricciones
|
|
allowed_domains TEXT[], -- NULL = todos permitidos
|
|
auto_create_users BOOLEAN NOT NULL DEFAULT FALSE,
|
|
default_role_id UUID REFERENCES auth.roles(id),
|
|
|
|
-- Auditoría
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
created_by UUID REFERENCES auth.users(id),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
updated_by UUID REFERENCES auth.users(id),
|
|
|
|
-- Constraints
|
|
CONSTRAINT uq_oauth_provider_code UNIQUE (code),
|
|
CONSTRAINT chk_response_type CHECK (response_type IN ('code', 'token')),
|
|
CONSTRAINT chk_pkce_method CHECK (code_challenge_method IN ('S256', 'plain'))
|
|
);
|
|
|
|
-- Índices para oauth_providers
|
|
CREATE INDEX idx_oauth_providers_enabled ON auth.oauth_providers(is_enabled);
|
|
CREATE INDEX idx_oauth_providers_tenant ON auth.oauth_providers(tenant_id);
|
|
CREATE INDEX idx_oauth_providers_code ON auth.oauth_providers(code);
|
|
|
|
-- Tabla: oauth_user_links (Vinculación usuario-proveedor)
|
|
CREATE TABLE auth.oauth_user_links (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
|
provider_id UUID NOT NULL REFERENCES auth.oauth_providers(id) ON DELETE CASCADE,
|
|
|
|
-- Identificación OAuth
|
|
oauth_uid VARCHAR(255) NOT NULL, -- Subject ID del proveedor
|
|
oauth_email VARCHAR(255),
|
|
|
|
-- Tokens (encriptados)
|
|
access_token TEXT,
|
|
refresh_token TEXT,
|
|
id_token TEXT,
|
|
token_expires_at TIMESTAMPTZ,
|
|
|
|
-- Metadata
|
|
raw_userinfo JSONB, -- Datos completos del proveedor
|
|
last_login_at TIMESTAMPTZ,
|
|
login_count INTEGER NOT NULL DEFAULT 0,
|
|
|
|
-- Auditoría
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
|
|
-- Constraints
|
|
CONSTRAINT uq_provider_oauth_uid UNIQUE (provider_id, oauth_uid),
|
|
CONSTRAINT uq_user_provider UNIQUE (user_id, provider_id)
|
|
);
|
|
|
|
-- Índices para oauth_user_links
|
|
CREATE INDEX idx_oauth_links_user ON auth.oauth_user_links(user_id);
|
|
CREATE INDEX idx_oauth_links_provider ON auth.oauth_user_links(provider_id);
|
|
CREATE INDEX idx_oauth_links_oauth_uid ON auth.oauth_user_links(oauth_uid);
|
|
|
|
-- Tabla: oauth_states (Estados OAuth2 temporales para CSRF)
|
|
CREATE TABLE auth.oauth_states (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
state VARCHAR(64) NOT NULL UNIQUE,
|
|
|
|
-- PKCE
|
|
code_verifier VARCHAR(128),
|
|
|
|
-- Contexto
|
|
provider_id UUID NOT NULL REFERENCES auth.oauth_providers(id),
|
|
redirect_uri VARCHAR(500) NOT NULL,
|
|
return_url VARCHAR(500),
|
|
|
|
-- Vinculación con usuario existente (para linking)
|
|
link_user_id UUID REFERENCES auth.users(id),
|
|
|
|
-- Metadata
|
|
ip_address INET,
|
|
user_agent TEXT,
|
|
|
|
-- Tiempo de vida
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
expires_at TIMESTAMPTZ NOT NULL DEFAULT (CURRENT_TIMESTAMP + INTERVAL '10 minutes'),
|
|
used_at TIMESTAMPTZ,
|
|
|
|
-- Constraints
|
|
CONSTRAINT chk_state_not_expired CHECK (expires_at > created_at)
|
|
);
|
|
|
|
-- Índices para oauth_states
|
|
CREATE INDEX idx_oauth_states_state ON auth.oauth_states(state);
|
|
CREATE INDEX idx_oauth_states_expires ON auth.oauth_states(expires_at);
|
|
|
|
-- Extensión de users para OAuth
|
|
ALTER TABLE auth.users ADD COLUMN IF NOT EXISTS
|
|
oauth_only BOOLEAN NOT NULL DEFAULT FALSE;
|
|
|
|
ALTER TABLE auth.users ADD COLUMN IF NOT EXISTS
|
|
primary_oauth_provider_id UUID REFERENCES auth.oauth_providers(id);
|
|
|
|
-- =====================================================
|
|
-- PARTE 8: FUNCIONES DE UTILIDAD
|
|
-- =====================================================
|
|
|
|
-- Función: Obtener grupos efectivos de un usuario (incluyendo herencia)
|
|
CREATE OR REPLACE FUNCTION auth.get_user_effective_groups(p_user_id UUID)
|
|
RETURNS TABLE(group_id UUID) AS $$
|
|
WITH RECURSIVE effective_groups AS (
|
|
-- Grupos asignados directamente
|
|
SELECT ug.group_id
|
|
FROM auth.user_groups ug
|
|
WHERE ug.user_id = p_user_id
|
|
|
|
UNION
|
|
|
|
-- Grupos heredados
|
|
SELECT gi.implied_group_id
|
|
FROM auth.group_implied gi
|
|
JOIN effective_groups eg ON gi.group_id = eg.group_id
|
|
)
|
|
SELECT DISTINCT group_id FROM effective_groups;
|
|
$$ LANGUAGE SQL STABLE;
|
|
|
|
COMMENT ON FUNCTION auth.get_user_effective_groups IS 'Obtiene todos los grupos de un usuario incluyendo herencia';
|
|
|
|
-- Función: Verificar permiso ACL
|
|
CREATE OR REPLACE FUNCTION auth.check_model_access(
|
|
p_user_id UUID,
|
|
p_model_name VARCHAR,
|
|
p_mode VARCHAR -- 'read', 'create', 'write', 'delete'
|
|
)
|
|
RETURNS BOOLEAN AS $$
|
|
DECLARE
|
|
v_has_access BOOLEAN;
|
|
BEGIN
|
|
-- Superusers tienen todos los permisos
|
|
IF EXISTS (
|
|
SELECT 1 FROM auth.users
|
|
WHERE id = p_user_id AND is_superuser = TRUE AND deleted_at IS NULL
|
|
) THEN
|
|
RETURN TRUE;
|
|
END IF;
|
|
|
|
-- Verificar ACL
|
|
SELECT EXISTS (
|
|
SELECT 1
|
|
FROM auth.model_access ma
|
|
JOIN auth.models m ON ma.model_id = m.id
|
|
WHERE m.name = p_model_name
|
|
AND ma.is_active = TRUE
|
|
AND (
|
|
ma.group_id IS NULL -- Permiso global
|
|
OR ma.group_id IN (SELECT auth.get_user_effective_groups(p_user_id))
|
|
)
|
|
AND CASE p_mode
|
|
WHEN 'read' THEN ma.perm_read
|
|
WHEN 'create' THEN ma.perm_create
|
|
WHEN 'write' THEN ma.perm_write
|
|
WHEN 'delete' THEN ma.perm_delete
|
|
ELSE FALSE
|
|
END
|
|
) INTO v_has_access;
|
|
|
|
RETURN COALESCE(v_has_access, FALSE);
|
|
END;
|
|
$$ LANGUAGE plpgsql STABLE SECURITY DEFINER;
|
|
|
|
COMMENT ON FUNCTION auth.check_model_access IS 'Verifica si un usuario tiene permiso CRUD en un modelo';
|
|
|
|
-- Función: Limpiar estados OAuth expirados
|
|
CREATE OR REPLACE FUNCTION auth.cleanup_expired_oauth_states()
|
|
RETURNS INTEGER AS $$
|
|
DECLARE
|
|
v_deleted INTEGER;
|
|
BEGIN
|
|
WITH deleted AS (
|
|
DELETE FROM auth.oauth_states
|
|
WHERE expires_at < CURRENT_TIMESTAMP
|
|
OR used_at IS NOT NULL
|
|
RETURNING id
|
|
)
|
|
SELECT COUNT(*) INTO v_deleted FROM deleted;
|
|
|
|
RETURN v_deleted;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
COMMENT ON FUNCTION auth.cleanup_expired_oauth_states IS 'Limpia estados OAuth expirados (ejecutar periódicamente)';
|
|
|
|
-- Función: Limpiar códigos de verificación expirados
|
|
CREATE OR REPLACE FUNCTION auth.cleanup_expired_verification_codes()
|
|
RETURNS INTEGER AS $$
|
|
DECLARE
|
|
v_deleted INTEGER;
|
|
BEGIN
|
|
WITH deleted AS (
|
|
DELETE FROM auth.verification_codes
|
|
WHERE expires_at < NOW() - INTERVAL '1 day'
|
|
RETURNING id
|
|
)
|
|
SELECT COUNT(*) INTO v_deleted FROM deleted;
|
|
|
|
RETURN v_deleted;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
COMMENT ON FUNCTION auth.cleanup_expired_verification_codes IS 'Limpia códigos de verificación expirados';
|
|
|
|
-- Función: Limpiar dispositivos de confianza expirados
|
|
CREATE OR REPLACE FUNCTION auth.cleanup_expired_trusted_devices()
|
|
RETURNS INTEGER AS $$
|
|
DECLARE
|
|
v_deleted INTEGER;
|
|
BEGIN
|
|
WITH updated AS (
|
|
UPDATE auth.trusted_devices
|
|
SET is_active = FALSE,
|
|
revoked_at = NOW(),
|
|
revoked_reason = 'expired'
|
|
WHERE trust_expires_at < NOW() - INTERVAL '7 days'
|
|
AND trust_expires_at IS NOT NULL
|
|
AND is_active = TRUE
|
|
RETURNING id
|
|
)
|
|
SELECT COUNT(*) INTO v_deleted FROM updated;
|
|
|
|
RETURN v_deleted;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
COMMENT ON FUNCTION auth.cleanup_expired_trusted_devices IS 'Desactiva dispositivos de confianza expirados';
|
|
|
|
-- Función: Limpiar API keys expiradas
|
|
CREATE OR REPLACE FUNCTION auth.cleanup_expired_api_keys()
|
|
RETURNS INTEGER AS $$
|
|
DECLARE
|
|
v_deleted INTEGER;
|
|
BEGIN
|
|
WITH deleted AS (
|
|
DELETE FROM auth.api_keys
|
|
WHERE expiration_date IS NOT NULL
|
|
AND expiration_date < NOW()
|
|
RETURNING id
|
|
)
|
|
SELECT COUNT(*) INTO v_deleted FROM deleted;
|
|
|
|
RETURN v_deleted;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
COMMENT ON FUNCTION auth.cleanup_expired_api_keys IS 'Limpia API keys expiradas';
|
|
|
|
-- =====================================================
|
|
-- PARTE 9: TRIGGERS
|
|
-- =====================================================
|
|
|
|
-- Trigger: Actualizar updated_at para grupos
|
|
CREATE TRIGGER trg_groups_updated_at
|
|
BEFORE UPDATE ON auth.groups
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION auth.update_updated_at_column();
|
|
|
|
-- Trigger: Actualizar updated_at para oauth_providers
|
|
CREATE TRIGGER trg_oauth_providers_updated_at
|
|
BEFORE UPDATE ON auth.oauth_providers
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION auth.update_updated_at_column();
|
|
|
|
-- Trigger: Actualizar updated_at para oauth_user_links
|
|
CREATE OR REPLACE FUNCTION auth.update_oauth_link_updated_at()
|
|
RETURNS TRIGGER AS $$
|
|
BEGIN
|
|
NEW.updated_at = CURRENT_TIMESTAMP;
|
|
RETURN NEW;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
CREATE TRIGGER trg_oauth_user_links_updated_at
|
|
BEFORE UPDATE ON auth.oauth_user_links
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION auth.update_oauth_link_updated_at();
|
|
|
|
-- =====================================================
|
|
-- PARTE 10: VISTAS
|
|
-- =====================================================
|
|
|
|
-- Vista: Usuarios con sus proveedores OAuth vinculados
|
|
CREATE OR REPLACE VIEW auth.users_oauth_summary AS
|
|
SELECT
|
|
u.id,
|
|
u.email,
|
|
u.full_name,
|
|
u.oauth_only,
|
|
COUNT(ol.id) as linked_providers_count,
|
|
ARRAY_AGG(op.name) FILTER (WHERE op.id IS NOT NULL) as linked_provider_names,
|
|
MAX(ol.last_login_at) as last_oauth_login
|
|
FROM auth.users u
|
|
LEFT JOIN auth.oauth_user_links ol ON ol.user_id = u.id
|
|
LEFT JOIN auth.oauth_providers op ON op.id = ol.provider_id
|
|
WHERE u.deleted_at IS NULL
|
|
GROUP BY u.id;
|
|
|
|
COMMENT ON VIEW auth.users_oauth_summary IS 'Vista de usuarios con sus proveedores OAuth vinculados';
|
|
|
|
-- Vista: Permisos efectivos por usuario y modelo
|
|
CREATE OR REPLACE VIEW auth.user_model_access_view AS
|
|
SELECT DISTINCT
|
|
u.id as user_id,
|
|
u.email,
|
|
m.name as model_name,
|
|
BOOL_OR(ma.perm_read) as can_read,
|
|
BOOL_OR(ma.perm_create) as can_create,
|
|
BOOL_OR(ma.perm_write) as can_write,
|
|
BOOL_OR(ma.perm_delete) as can_delete
|
|
FROM auth.users u
|
|
CROSS JOIN auth.models m
|
|
LEFT JOIN auth.user_groups ug ON ug.user_id = u.id
|
|
LEFT JOIN auth.model_access ma ON ma.model_id = m.id
|
|
AND (ma.group_id IS NULL OR ma.group_id = ug.group_id)
|
|
AND ma.is_active = TRUE
|
|
WHERE u.deleted_at IS NULL
|
|
GROUP BY u.id, u.email, m.name;
|
|
|
|
COMMENT ON VIEW auth.user_model_access_view IS 'Vista de permisos ACL efectivos por usuario y modelo';
|
|
|
|
-- =====================================================
|
|
-- PARTE 11: DATOS INICIALES
|
|
-- =====================================================
|
|
|
|
-- Proveedores OAuth2 preconfigurados (template)
|
|
-- NOTA: Solo se insertan como template, requieren client_id y client_secret
|
|
INSERT INTO auth.oauth_providers (
|
|
code, name,
|
|
authorization_endpoint, token_endpoint, userinfo_endpoint, jwks_uri,
|
|
scope, icon_class, button_text, button_color,
|
|
claim_mapping, display_order, is_enabled, client_id
|
|
) VALUES
|
|
-- Google
|
|
(
|
|
'google', 'Google',
|
|
'https://accounts.google.com/o/oauth2/v2/auth',
|
|
'https://oauth2.googleapis.com/token',
|
|
'https://openidconnect.googleapis.com/v1/userinfo',
|
|
'https://www.googleapis.com/oauth2/v3/certs',
|
|
'openid profile email',
|
|
'fa-google', 'Continuar con Google', '#4285F4',
|
|
'{"sub": "oauth_uid", "email": "email", "name": "name", "picture": "avatar_url"}',
|
|
1, FALSE, 'CONFIGURE_ME'
|
|
),
|
|
-- Microsoft Azure AD
|
|
(
|
|
'microsoft', 'Microsoft',
|
|
'https://login.microsoftonline.com/common/oauth2/v2.0/authorize',
|
|
'https://login.microsoftonline.com/common/oauth2/v2.0/token',
|
|
'https://graph.microsoft.com/v1.0/me',
|
|
'https://login.microsoftonline.com/common/discovery/v2.0/keys',
|
|
'openid profile email User.Read',
|
|
'fa-microsoft', 'Continuar con Microsoft', '#00A4EF',
|
|
'{"id": "oauth_uid", "mail": "email", "displayName": "name"}',
|
|
2, FALSE, 'CONFIGURE_ME'
|
|
),
|
|
-- GitHub
|
|
(
|
|
'github', 'GitHub',
|
|
'https://github.com/login/oauth/authorize',
|
|
'https://github.com/login/oauth/access_token',
|
|
'https://api.github.com/user',
|
|
NULL,
|
|
'read:user user:email',
|
|
'fa-github', 'Continuar con GitHub', '#333333',
|
|
'{"id": "oauth_uid", "email": "email", "name": "name", "avatar_url": "avatar_url"}',
|
|
3, FALSE, 'CONFIGURE_ME'
|
|
)
|
|
ON CONFLICT (code) DO NOTHING;
|
|
|
|
-- =====================================================
|
|
-- COMENTARIOS EN TABLAS
|
|
-- =====================================================
|
|
|
|
COMMENT ON TABLE auth.groups IS 'Grupos de usuarios con herencia para control de acceso';
|
|
COMMENT ON TABLE auth.group_implied IS 'Herencia entre grupos (A implica B)';
|
|
COMMENT ON TABLE auth.user_groups IS 'Asignación de usuarios a grupos (many-to-many)';
|
|
COMMENT ON TABLE auth.models IS 'Definición de modelos del sistema para ACL';
|
|
COMMENT ON TABLE auth.model_access IS 'Permisos CRUD a nivel de modelo por grupo (ACL)';
|
|
COMMENT ON TABLE auth.record_rules IS 'Reglas de acceso a nivel de registro (row-level security)';
|
|
COMMENT ON TABLE auth.rule_groups IS 'Relación entre record rules y grupos';
|
|
COMMENT ON TABLE auth.model_fields IS 'Campos de modelo con metadatos de seguridad';
|
|
COMMENT ON TABLE auth.field_permissions IS 'Permisos de lectura/escritura por campo y grupo';
|
|
COMMENT ON TABLE auth.api_keys IS 'API Keys para autenticación de integraciones externas';
|
|
COMMENT ON TABLE auth.trusted_devices IS 'Dispositivos de confianza para bypass de 2FA';
|
|
COMMENT ON TABLE auth.verification_codes IS 'Códigos de verificación temporales para 2FA';
|
|
COMMENT ON TABLE auth.mfa_audit_log IS 'Log de auditoría de eventos MFA';
|
|
COMMENT ON TABLE auth.oauth_providers IS 'Proveedores OAuth2 configurados';
|
|
COMMENT ON TABLE auth.oauth_user_links IS 'Vinculación de usuarios con proveedores OAuth';
|
|
COMMENT ON TABLE auth.oauth_states IS 'Estados OAuth2 temporales para protección CSRF';
|
|
|
|
COMMENT ON COLUMN auth.api_keys.key_index IS 'Primeros 16 hex chars del key para lookup O(1)';
|
|
COMMENT ON COLUMN auth.api_keys.key_hash IS 'Hash PBKDF2-SHA512 del key completo';
|
|
COMMENT ON COLUMN auth.api_keys.scope IS 'Scope del API key (NULL=full, rpc=API only)';
|
|
COMMENT ON COLUMN auth.groups.api_key_max_duration_days IS 'Máxima duración en días para API keys de usuarios de este grupo (0=ilimitado)';
|
|
|
|
-- =====================================================
|
|
-- FIN DE EXTENSIONES AUTH
|
|
-- =====================================================
|