-- ============================================================================ -- SCHEMA: admin -- TABLE: admin_roles -- DESCRIPTION: Roles administrativos de la plataforma -- VERSION: 1.0.0 -- CREATED: 2026-01-16 -- SPRINT: Sprint 1 - DDL Implementation Roadmap Q1-2026 -- ============================================================================ -- Crear schema si no existe CREATE SCHEMA IF NOT EXISTS admin; -- Grant usage GRANT USAGE ON SCHEMA admin TO trading_app; GRANT USAGE ON SCHEMA admin TO trading_readonly; -- Enum para nivel de acceso administrativo DO $$ BEGIN CREATE TYPE admin.admin_level AS ENUM ( 'super_admin', -- Acceso total a la plataforma 'platform_admin', -- Administracion de plataforma (sin acceso a codigo) 'tenant_admin', -- Administrador de tenant 'support', -- Soporte al cliente 'analyst', -- Solo lectura para analisis 'auditor' -- Solo lectura para auditorias ); EXCEPTION WHEN duplicate_object THEN null; END $$; -- Tabla de Roles Administrativos CREATE TABLE IF NOT EXISTS admin.admin_roles ( -- Identificadores id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES users.users(id) ON DELETE CASCADE, -- Nivel administrativo level admin.admin_level NOT NULL, -- Scope del rol is_global BOOLEAN NOT NULL DEFAULT FALSE, -- Aplica a todos los tenants tenant_id UUID REFERENCES tenants.tenants(id), -- Tenant especifico (NULL si global) -- Permisos especificos (override del nivel) permissions JSONB DEFAULT '{}'::JSONB, denied_permissions JSONB DEFAULT '[]'::JSONB, -- Permisos explicitamente denegados -- Configuracion can_impersonate BOOLEAN NOT NULL DEFAULT FALSE, -- Puede actuar como otro usuario can_view_pii BOOLEAN NOT NULL DEFAULT FALSE, -- Puede ver datos personales can_export_data BOOLEAN NOT NULL DEFAULT FALSE, -- Puede exportar datos masivos can_modify_config BOOLEAN NOT NULL DEFAULT FALSE, -- Puede modificar configuracion can_manage_admins BOOLEAN NOT NULL DEFAULT FALSE, -- Puede gestionar otros admins -- Restricciones de IP allowed_ips INET[], -- IPs permitidas (NULL = todas) -- Restricciones de horario allowed_hours_start TIME, -- Hora inicio permitida allowed_hours_end TIME, -- Hora fin permitida allowed_days INTEGER[], -- Dias permitidos (0=Dom, 6=Sab) -- Activacion is_active BOOLEAN NOT NULL DEFAULT TRUE, activated_at TIMESTAMPTZ, deactivated_at TIMESTAMPTZ, deactivation_reason TEXT, -- Expiracion expires_at TIMESTAMPTZ, -- Rol temporal -- Auditoria de asignacion assigned_by UUID REFERENCES users.users(id), assigned_at TIMESTAMPTZ DEFAULT NOW(), assignment_reason TEXT, -- Ultima actividad administrativa last_admin_action_at TIMESTAMPTZ, total_admin_actions INTEGER NOT NULL DEFAULT 0, -- Metadata metadata JSONB DEFAULT '{}'::JSONB, -- Timestamps created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), -- Constraints CONSTRAINT admin_roles_unique_user_tenant UNIQUE (user_id, tenant_id), CONSTRAINT admin_roles_tenant_check CHECK ( (is_global = TRUE AND tenant_id IS NULL) OR (is_global = FALSE AND tenant_id IS NOT NULL) ) ); COMMENT ON TABLE admin.admin_roles IS 'Roles administrativos asignados a usuarios de la plataforma'; COMMENT ON COLUMN admin.admin_roles.level IS 'Nivel de acceso: super_admin, platform_admin, tenant_admin, support, analyst, auditor'; COMMENT ON COLUMN admin.admin_roles.is_global IS 'TRUE si el rol aplica a todos los tenants (solo para super_admin/platform_admin)'; -- Indices CREATE INDEX IF NOT EXISTS idx_admin_roles_user_id ON admin.admin_roles(user_id); CREATE INDEX IF NOT EXISTS idx_admin_roles_tenant_id ON admin.admin_roles(tenant_id) WHERE tenant_id IS NOT NULL; CREATE INDEX IF NOT EXISTS idx_admin_roles_level ON admin.admin_roles(level); CREATE INDEX IF NOT EXISTS idx_admin_roles_active ON admin.admin_roles(is_active) WHERE is_active = TRUE; CREATE INDEX IF NOT EXISTS idx_admin_roles_global ON admin.admin_roles(is_global) WHERE is_global = TRUE; CREATE INDEX IF NOT EXISTS idx_admin_roles_expires ON admin.admin_roles(expires_at) WHERE expires_at IS NOT NULL; -- Trigger para updated_at CREATE OR REPLACE FUNCTION admin.update_admin_timestamp() RETURNS TRIGGER AS $$ BEGIN NEW.updated_at = NOW(); RETURN NEW; END; $$ LANGUAGE plpgsql; DROP TRIGGER IF EXISTS admin_role_updated_at ON admin.admin_roles; CREATE TRIGGER admin_role_updated_at BEFORE UPDATE ON admin.admin_roles FOR EACH ROW EXECUTE FUNCTION admin.update_admin_timestamp(); -- Funcion para verificar acceso admin CREATE OR REPLACE FUNCTION admin.check_admin_access( p_user_id UUID, p_tenant_id UUID DEFAULT NULL, p_required_level admin.admin_level DEFAULT 'support' ) RETURNS BOOLEAN AS $$ DECLARE v_role RECORD; v_level_order INTEGER; v_required_order INTEGER; v_current_time TIME; v_current_day INTEGER; BEGIN -- Orden de niveles (mayor = mas permisos) SELECT CASE p_required_level WHEN 'super_admin' THEN 6 WHEN 'platform_admin' THEN 5 WHEN 'tenant_admin' THEN 4 WHEN 'support' THEN 3 WHEN 'analyst' THEN 2 WHEN 'auditor' THEN 1 END INTO v_required_order; -- Buscar rol activo SELECT * INTO v_role FROM admin.admin_roles WHERE user_id = p_user_id AND is_active = TRUE AND (expires_at IS NULL OR expires_at > NOW()) AND (is_global = TRUE OR tenant_id = p_tenant_id) ORDER BY is_global DESC -- Preferir roles globales LIMIT 1; IF v_role IS NULL THEN RETURN FALSE; END IF; -- Verificar nivel SELECT CASE v_role.level WHEN 'super_admin' THEN 6 WHEN 'platform_admin' THEN 5 WHEN 'tenant_admin' THEN 4 WHEN 'support' THEN 3 WHEN 'analyst' THEN 2 WHEN 'auditor' THEN 1 END INTO v_level_order; IF v_level_order < v_required_order THEN RETURN FALSE; END IF; -- Verificar restricciones de horario si existen IF v_role.allowed_hours_start IS NOT NULL AND v_role.allowed_hours_end IS NOT NULL THEN v_current_time := CURRENT_TIME; IF v_current_time < v_role.allowed_hours_start OR v_current_time > v_role.allowed_hours_end THEN RETURN FALSE; END IF; END IF; -- Verificar restricciones de dias si existen IF v_role.allowed_days IS NOT NULL AND array_length(v_role.allowed_days, 1) > 0 THEN v_current_day := EXTRACT(DOW FROM CURRENT_DATE)::INTEGER; IF NOT (v_current_day = ANY(v_role.allowed_days)) THEN RETURN FALSE; END IF; END IF; RETURN TRUE; END; $$ LANGUAGE plpgsql; -- Vista de administradores activos CREATE OR REPLACE VIEW admin.v_active_admins AS SELECT ar.id, ar.user_id, u.email, u.first_name, u.last_name, ar.level, ar.is_global, ar.tenant_id, t.name AS tenant_name, ar.can_impersonate, ar.can_view_pii, ar.expires_at, ar.last_admin_action_at, ar.total_admin_actions FROM admin.admin_roles ar JOIN users.users u ON ar.user_id = u.id LEFT JOIN tenants.tenants t ON ar.tenant_id = t.id WHERE ar.is_active = TRUE AND (ar.expires_at IS NULL OR ar.expires_at > NOW()) ORDER BY ar.level, ar.created_at; -- Grants GRANT SELECT, INSERT, UPDATE, DELETE ON admin.admin_roles TO trading_app; GRANT SELECT ON admin.admin_roles TO trading_readonly; GRANT SELECT ON admin.v_active_admins TO trading_app; GRANT EXECUTE ON FUNCTION admin.check_admin_access TO trading_app;