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