erp-core-database-v2/ddl/01-auth-extensions.sql

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
-- =====================================================