Sistema NEXUS v3.4 migrado con: Estructura principal: - core/orchestration: Sistema SIMCO + CAPVED (27 directivas, 28 perfiles) - core/catalog: Catalogo de funcionalidades reutilizables - shared/knowledge-base: Base de conocimiento compartida - devtools/scripts: Herramientas de desarrollo - control-plane/registries: Control de servicios y CI/CD - orchestration/: Configuracion de orquestacion de agentes Proyectos incluidos (11): - gamilit (submodule -> GitHub) - trading-platform (OrbiquanTIA) - erp-suite con 5 verticales: - erp-core, construccion, vidrio-templado - mecanicas-diesel, retail, clinicas - betting-analytics - inmobiliaria-analytics - platform_marketing_content - pos-micro, erp-basico Configuracion: - .gitignore completo para Node.js/Python/Docker - gamilit como submodule (git@github.com:rckrdmrd/gamilit-workspace.git) - Sistema de puertos estandarizado (3005-3199) Generated with NEXUS v3.4 Migration System EPIC-010: Configuracion Git y Repositorios
368 lines
13 KiB
PL/PgSQL
368 lines
13 KiB
PL/PgSQL
-- ============================================================================
|
|
-- ET-PROJ-001: Row-Level Security (RLS) Policies
|
|
-- Tabla: projects.projects
|
|
-- Fecha: 2025-11-17
|
|
-- Descripción: Políticas de seguridad para aislamiento multi-tenant (constructora)
|
|
-- ============================================================================
|
|
|
|
-- Habilitar RLS en la tabla
|
|
ALTER TABLE projects.projects ENABLE ROW LEVEL SECURITY;
|
|
|
|
-- ============================================================================
|
|
-- FUNCIONES HELPER
|
|
-- ============================================================================
|
|
|
|
-- Función: Obtener constructora_id del contexto actual
|
|
CREATE OR REPLACE FUNCTION public.get_current_constructora_id()
|
|
RETURNS UUID AS $$
|
|
BEGIN
|
|
RETURN current_setting('app.current_constructora_id', true)::UUID;
|
|
EXCEPTION
|
|
WHEN OTHERS THEN
|
|
RETURN NULL;
|
|
END;
|
|
$$ LANGUAGE plpgsql STABLE SECURITY DEFINER;
|
|
|
|
COMMENT ON FUNCTION public.get_current_constructora_id() IS
|
|
'Obtiene el UUID de la constructora del contexto RLS actual.
|
|
Retorna NULL si no está configurado.';
|
|
|
|
-- Función: Obtener user_id del contexto actual
|
|
CREATE OR REPLACE FUNCTION public.get_current_user_id()
|
|
RETURNS UUID AS $$
|
|
BEGIN
|
|
RETURN current_setting('app.current_user_id', true)::UUID;
|
|
EXCEPTION
|
|
WHEN OTHERS THEN
|
|
RETURN NULL;
|
|
END;
|
|
$$ LANGUAGE plpgsql STABLE SECURITY DEFINER;
|
|
|
|
COMMENT ON FUNCTION public.get_current_user_id() IS
|
|
'Obtiene el UUID del usuario autenticado del contexto RLS actual.';
|
|
|
|
-- Función: Obtener rol del usuario del contexto actual
|
|
CREATE OR REPLACE FUNCTION public.get_current_user_role()
|
|
RETURNS TEXT AS $$
|
|
BEGIN
|
|
RETURN current_setting('app.current_user_role', true);
|
|
EXCEPTION
|
|
WHEN OTHERS THEN
|
|
RETURN 'guest';
|
|
END;
|
|
$$ LANGUAGE plpgsql STABLE SECURITY DEFINER;
|
|
|
|
COMMENT ON FUNCTION public.get_current_user_role() IS
|
|
'Obtiene el rol del usuario autenticado (director, engineer, resident, etc.).';
|
|
|
|
-- ============================================================================
|
|
-- POLÍTICAS RLS: SELECT
|
|
-- ============================================================================
|
|
|
|
-- Política: Permitir SELECT solo de proyectos de la constructora actual
|
|
DROP POLICY IF EXISTS "projects_select_own_constructora" ON projects.projects;
|
|
|
|
CREATE POLICY "projects_select_own_constructora"
|
|
ON projects.projects
|
|
FOR SELECT
|
|
TO authenticated
|
|
USING (
|
|
constructora_id = public.get_current_constructora_id()
|
|
);
|
|
|
|
COMMENT ON POLICY "projects_select_own_constructora" ON projects.projects IS
|
|
'Permite a usuarios autenticados ver solo proyectos de su constructora.
|
|
Aislamiento: tenant (constructora) level.';
|
|
|
|
-- ============================================================================
|
|
-- POLÍTICAS RLS: INSERT
|
|
-- ============================================================================
|
|
|
|
-- Política: Permitir INSERT solo con constructora_id del contexto actual
|
|
DROP POLICY IF EXISTS "projects_insert_own_constructora" ON projects.projects;
|
|
|
|
CREATE POLICY "projects_insert_own_constructora"
|
|
ON projects.projects
|
|
FOR INSERT
|
|
TO authenticated
|
|
WITH CHECK (
|
|
constructora_id = public.get_current_constructora_id()
|
|
AND (
|
|
-- Solo directores y admins pueden crear proyectos
|
|
public.get_current_user_role() IN ('director', 'admin')
|
|
)
|
|
);
|
|
|
|
COMMENT ON POLICY "projects_insert_own_constructora" ON projects.projects IS
|
|
'Permite crear proyectos solo para la constructora actual.
|
|
Requiere rol: director o admin.
|
|
Previene: Inserción de proyectos en otras constructoras.';
|
|
|
|
-- ============================================================================
|
|
-- POLÍTICAS RLS: UPDATE
|
|
-- ============================================================================
|
|
|
|
-- Política: Permitir UPDATE solo de proyectos propios con roles autorizados
|
|
DROP POLICY IF EXISTS "projects_update_own_constructora" ON projects.projects;
|
|
|
|
CREATE POLICY "projects_update_own_constructora"
|
|
ON projects.projects
|
|
FOR UPDATE
|
|
TO authenticated
|
|
USING (
|
|
constructora_id = public.get_current_constructora_id()
|
|
AND (
|
|
-- Director puede editar todo
|
|
public.get_current_user_role() IN ('director', 'admin')
|
|
OR
|
|
-- Engineer e Resident pueden editar campos limitados (verificado en app)
|
|
public.get_current_user_role() IN ('engineer', 'resident')
|
|
)
|
|
)
|
|
WITH CHECK (
|
|
-- No permitir cambiar constructora_id
|
|
constructora_id = public.get_current_constructora_id()
|
|
);
|
|
|
|
COMMENT ON POLICY "projects_update_own_constructora" ON projects.projects IS
|
|
'Permite actualizar proyectos solo de la constructora actual.
|
|
Roles permitidos: director, admin (full), engineer, resident (limitado).
|
|
Protección: Previene cambio de constructora_id.';
|
|
|
|
-- ============================================================================
|
|
-- POLÍTICAS RLS: DELETE
|
|
-- ============================================================================
|
|
|
|
-- Política: Permitir DELETE solo a admin/director de proyectos propios
|
|
DROP POLICY IF EXISTS "projects_delete_own_constructora" ON projects.projects;
|
|
|
|
CREATE POLICY "projects_delete_own_constructora"
|
|
ON projects.projects
|
|
FOR DELETE
|
|
TO authenticated
|
|
USING (
|
|
constructora_id = public.get_current_constructora_id()
|
|
AND public.get_current_user_role() IN ('director', 'admin')
|
|
);
|
|
|
|
COMMENT ON POLICY "projects_delete_own_constructora" ON projects.projects IS
|
|
'Permite eliminar proyectos solo a director/admin de la constructora actual.
|
|
Prevención: Cross-tenant deletion.
|
|
Nota: Considerar soft-delete en lugar de hard-delete.';
|
|
|
|
-- ============================================================================
|
|
-- POLÍTICAS ADICIONALES: BYPASS PARA SUPER ADMIN (OPCIONAL)
|
|
-- ============================================================================
|
|
|
|
-- Política: Super Admin puede ver todos los proyectos (para soporte)
|
|
DROP POLICY IF EXISTS "projects_super_admin_all_access" ON projects.projects;
|
|
|
|
CREATE POLICY "projects_super_admin_all_access"
|
|
ON projects.projects
|
|
FOR ALL
|
|
TO authenticated
|
|
USING (
|
|
public.get_current_user_role() = 'super_admin'
|
|
)
|
|
WITH CHECK (
|
|
public.get_current_user_role() = 'super_admin'
|
|
);
|
|
|
|
COMMENT ON POLICY "projects_super_admin_all_access" ON projects.projects IS
|
|
'Permite acceso completo a super_admin (equipo interno de soporte).
|
|
Uso: Troubleshooting, migración de datos, auditoría.
|
|
Seguridad: Solo usuarios con rol super_admin (muy limitados).';
|
|
|
|
-- ============================================================================
|
|
-- ÍNDICES PARA PERFORMANCE
|
|
-- ============================================================================
|
|
|
|
-- Índice en constructora_id para queries filtrados
|
|
CREATE INDEX IF NOT EXISTS idx_projects_constructora_id
|
|
ON projects.projects(constructora_id);
|
|
|
|
-- Índice compuesto para queries comunes
|
|
CREATE INDEX IF NOT EXISTS idx_projects_constructora_status
|
|
ON projects.projects(constructora_id, status);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_projects_constructora_type
|
|
ON projects.projects(constructora_id, type);
|
|
|
|
-- Índice para búsquedas por código
|
|
CREATE INDEX IF NOT EXISTS idx_projects_code
|
|
ON projects.projects(code);
|
|
|
|
COMMENT ON INDEX projects.idx_projects_constructora_id IS
|
|
'Optimiza queries filtrados por constructora_id (usado por RLS).';
|
|
|
|
COMMENT ON INDEX projects.idx_projects_constructora_status IS
|
|
'Optimiza queries: listar proyectos activos de una constructora.';
|
|
|
|
-- ============================================================================
|
|
-- TRIGGERS DE AUDITORÍA
|
|
-- ============================================================================
|
|
|
|
-- Función: Registrar cambios en proyectos
|
|
CREATE OR REPLACE FUNCTION projects.audit_project_changes()
|
|
RETURNS TRIGGER AS $$
|
|
BEGIN
|
|
-- En UPDATE, registrar cambio de estado
|
|
IF TG_OP = 'UPDATE' AND OLD.status IS DISTINCT FROM NEW.status THEN
|
|
INSERT INTO projects.project_status_history (
|
|
project_id,
|
|
old_status,
|
|
new_status,
|
|
changed_by,
|
|
changed_at,
|
|
constructora_id
|
|
) VALUES (
|
|
NEW.id,
|
|
OLD.status,
|
|
NEW.status,
|
|
public.get_current_user_id(),
|
|
NOW(),
|
|
NEW.constructora_id
|
|
);
|
|
END IF;
|
|
|
|
RETURN NEW;
|
|
END;
|
|
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
|
|
|
-- Trigger: Auditar cambios de estado
|
|
DROP TRIGGER IF EXISTS trigger_audit_project_status ON projects.projects;
|
|
|
|
CREATE TRIGGER trigger_audit_project_status
|
|
AFTER UPDATE ON projects.projects
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION projects.audit_project_changes();
|
|
|
|
COMMENT ON TRIGGER trigger_audit_project_status ON projects.projects IS
|
|
'Registra cambios de estado en tabla de auditoría.
|
|
Tabla destino: projects.project_status_history.';
|
|
|
|
-- ============================================================================
|
|
-- VALIDACIONES Y RESTRICCIONES
|
|
-- ============================================================================
|
|
|
|
-- Constraint: constructora_id no puede ser NULL
|
|
ALTER TABLE projects.projects
|
|
ALTER COLUMN constructora_id SET NOT NULL;
|
|
|
|
-- Foreign Key: Validar que constructora existe y está activa
|
|
ALTER TABLE projects.projects
|
|
DROP CONSTRAINT IF EXISTS fk_projects_constructora;
|
|
|
|
ALTER TABLE projects.projects
|
|
ADD CONSTRAINT fk_projects_constructora
|
|
FOREIGN KEY (constructora_id)
|
|
REFERENCES constructoras.constructoras(id)
|
|
ON DELETE RESTRICT; -- Prevenir eliminación de constructora con proyectos
|
|
|
|
COMMENT ON CONSTRAINT fk_projects_constructora ON projects.projects IS
|
|
'Garantiza que constructora_id apunta a una constructora válida.
|
|
ON DELETE RESTRICT: No permitir eliminar constructora con proyectos activos.';
|
|
|
|
-- ============================================================================
|
|
-- TESTS DE RLS (Para ejecutar en ambiente de prueba)
|
|
-- ============================================================================
|
|
|
|
-- Test 1: Verificar aislamiento entre constructoras
|
|
DO $$
|
|
DECLARE
|
|
v_constructora_a UUID := 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa';
|
|
v_constructora_b UUID := 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb';
|
|
v_count INTEGER;
|
|
BEGIN
|
|
-- Configurar contexto para constructora A
|
|
PERFORM set_config('app.current_constructora_id', v_constructora_a::TEXT, true);
|
|
|
|
-- Contar proyectos visibles
|
|
SELECT COUNT(*) INTO v_count FROM projects.projects;
|
|
|
|
RAISE NOTICE 'Constructora A puede ver % proyectos', v_count;
|
|
|
|
-- Cambiar a constructora B
|
|
PERFORM set_config('app.current_constructora_id', v_constructora_b::TEXT, true);
|
|
|
|
SELECT COUNT(*) INTO v_count FROM projects.projects;
|
|
|
|
RAISE NOTICE 'Constructora B puede ver % proyectos', v_count;
|
|
|
|
-- Verificar que no se cruzan datos
|
|
IF v_count > 0 THEN
|
|
RAISE EXCEPTION 'RLS FAIL: Constructora B no debería ver proyectos de A';
|
|
END IF;
|
|
|
|
RAISE NOTICE 'Test RLS: PASSED - Aislamiento correcto';
|
|
END $$;
|
|
|
|
-- Test 2: Verificar que no se puede cambiar constructora_id
|
|
DO $$
|
|
DECLARE
|
|
v_project_id UUID;
|
|
v_original_constructora UUID := 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa';
|
|
v_malicious_constructora UUID := 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb';
|
|
BEGIN
|
|
-- Configurar contexto
|
|
PERFORM set_config('app.current_constructora_id', v_original_constructora::TEXT, true);
|
|
PERFORM set_config('app.current_user_role', 'director', true);
|
|
|
|
-- Crear proyecto de prueba
|
|
INSERT INTO projects.projects (
|
|
code, name, type, status, constructora_id
|
|
) VALUES (
|
|
'TEST-001', 'Test Project', 'fraccionamiento', 'adjudicado', v_original_constructora
|
|
) RETURNING id INTO v_project_id;
|
|
|
|
-- Intentar cambiar constructora_id (debe fallar)
|
|
BEGIN
|
|
UPDATE projects.projects
|
|
SET constructora_id = v_malicious_constructora
|
|
WHERE id = v_project_id;
|
|
|
|
RAISE EXCEPTION 'RLS FAIL: Se permitió cambiar constructora_id';
|
|
EXCEPTION
|
|
WHEN OTHERS THEN
|
|
RAISE NOTICE 'Test RLS: PASSED - No se puede cambiar constructora_id';
|
|
END;
|
|
|
|
-- Limpiar
|
|
DELETE FROM projects.projects WHERE id = v_project_id;
|
|
END $$;
|
|
|
|
-- ============================================================================
|
|
-- GRANTS DE PERMISOS
|
|
-- ============================================================================
|
|
|
|
-- Permisos para rol 'authenticated' (usuarios normales)
|
|
GRANT SELECT, INSERT, UPDATE, DELETE ON projects.projects TO authenticated;
|
|
|
|
-- Permisos para funciones helper
|
|
GRANT EXECUTE ON FUNCTION public.get_current_constructora_id() TO authenticated;
|
|
GRANT EXECUTE ON FUNCTION public.get_current_user_id() TO authenticated;
|
|
GRANT EXECUTE ON FUNCTION public.get_current_user_role() TO authenticated;
|
|
|
|
-- ============================================================================
|
|
-- FIN DE POLÍTICAS RLS
|
|
-- ============================================================================
|
|
|
|
-- Verificación final: RLS está habilitado
|
|
DO $$
|
|
BEGIN
|
|
IF NOT (SELECT relrowsecurity FROM pg_class WHERE relname = 'projects' AND relnamespace = 'projects'::regnamespace) THEN
|
|
RAISE EXCEPTION 'CRITICAL: RLS no está habilitado en projects.projects';
|
|
ELSE
|
|
RAISE NOTICE '✓ RLS habilitado correctamente en projects.projects';
|
|
END IF;
|
|
END $$;
|
|
|
|
-- Estadísticas
|
|
SELECT
|
|
schemaname,
|
|
tablename,
|
|
rowsecurity as rls_enabled,
|
|
(SELECT COUNT(*) FROM pg_policies WHERE schemaname = 'projects' AND tablename = 'projects') as policy_count
|
|
FROM pg_tables
|
|
WHERE schemaname = 'projects' AND tablename = 'projects';
|