From 201580a47a2a9a33b4012bc70d44e5cd73c537f6 Mon Sep 17 00:00:00 2001 From: Jonas Pacheco Date: Thu, 2 Apr 2026 09:58:56 -0300 Subject: [PATCH] =?UTF-8?q?feat(rls):=20migra=C3=A7=C3=B5es=20Row=20Level?= =?UTF-8?q?=20Security=20-=20Semana=202?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 001_enable_rls.sql: habilita RLS em 31 tabelas tenant-isoladas - 002_create_policies.sql: 124 políticas CRUD por tenant - 003_tenant_context.sql: funções, triggers e views de contexto - scripts/apply-rls-migrations.ts: automação de aplicação - package.json: script npm run db:apply-rls Tabelas protegidas: workspace, crm, process compass, low-code, multi-tenancy core --- docker-compose.yml | 25 +- migrations/001_enable_rls.sql | 81 ++++ migrations/002_create_policies.sql | 711 +++++++++++++++++++++++++++++ migrations/003_tenant_context.sql | 381 ++++++++++++++++ migrations/README_RLS.md | 118 +++++ package-lock.json | 13 - package.json | 1 + scripts/apply-rls-migrations.ts | 168 +++++++ 8 files changed, 1462 insertions(+), 36 deletions(-) create mode 100644 migrations/001_enable_rls.sql create mode 100644 migrations/002_create_policies.sql create mode 100644 migrations/003_tenant_context.sql create mode 100644 migrations/README_RLS.md create mode 100644 scripts/apply-rls-migrations.ts diff --git a/docker-compose.yml b/docker-compose.yml index b66164f..259912f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -197,30 +197,12 @@ services: # ─────────────── PERFIL: ai (Soberania de IA) ──────────────────────────────── - # ── Ollama (LLMs locais) ───────────────────────────────────────────────────── - ollama: - image: ollama/ollama:latest - restart: unless-stopped - volumes: - - ollama_models:/root/.ollama - ports: - - "11434:11434" - profiles: [ai] - # Para GPU NVIDIA: adicione `deploy.resources.reservations.devices` - # deploy: - # resources: - # reservations: - # devices: - # - driver: nvidia - # count: 1 - # capabilities: [gpu] - # ── Open WebUI (interface para devs/consultores) ───────────────────────────── open-webui: image: ghcr.io/open-webui/open-webui:main restart: unless-stopped environment: - OLLAMA_BASE_URL: http://ollama:11434 + OLLAMA_BASE_URL: http://ollama-ia1upsekrad96at5hq97e4qa:11434 WEBUI_SECRET_KEY: ${WEBUI_SECRET_KEY:-webui-secret-change-in-prod} DEFAULT_MODELS: llama3.3,qwen2.5-coder ENABLE_RAG_WEB_SEARCH: "true" @@ -228,8 +210,6 @@ services: - "3001:8080" volumes: - open_webui_data:/app/backend/data - depends_on: - - ollama profiles: [ai] # ── LiteLLM (proxy unificado de LLMs) ─────────────────────────────────────── @@ -241,7 +221,7 @@ services: command: ["--config", "/app/config.yaml", "--port", "4000", "--detailed_debug"] environment: OPENAI_API_KEY: ${OPENAI_API_KEY:-} - OLLAMA_BASE_URL: http://ollama:11434 + OLLAMA_BASE_URL: http://ollama-ia1upsekrad96at5hq97e4qa:11434 LITELLM_MASTER_KEY: ${LITELLM_API_KEY:-arcadia-internal} ports: - "4000:4000" @@ -326,7 +306,6 @@ services: volumes: pgdata: - ollama_models: open_webui_data: superset_home: erpnext_db: diff --git a/migrations/001_enable_rls.sql b/migrations/001_enable_rls.sql new file mode 100644 index 0000000..f6ba6a0 --- /dev/null +++ b/migrations/001_enable_rls.sql @@ -0,0 +1,81 @@ +-- Migration 001: Enable Row Level Security (RLS) on tenant-isolated tables +-- Data: 2026-04-02 +-- Description: Habilita RLS em todas as tabelas que possuem tenant_id + +-- ========================================== +-- PRODUCTIVITY & WORKSPACE +-- ========================================== + +ALTER TABLE "workspace_pages" ENABLE ROW LEVEL SECURITY; +ALTER TABLE "quick_notes" ENABLE ROW LEVEL SECURITY; +ALTER TABLE "activity_feed" ENABLE ROW LEVEL SECURITY; +ALTER TABLE "conversations" ENABLE ROW LEVEL SECURITY; +ALTER TABLE "knowledge_base" ENABLE ROW LEVEL SECURITY; +ALTER TABLE "chat_threads" ENABLE ROW LEVEL SECURITY; +ALTER TABLE "manus_runs" ENABLE ROW LEVEL SECURITY; + +-- ========================================== +-- LOW-CODE / DEV CENTER +-- ========================================== + +ALTER TABLE "arc_doctypes" ENABLE ROW LEVEL SECURITY; +ALTER TABLE "arc_layouts" ENABLE ROW LEVEL SECURITY; + +-- ========================================== +-- MULTI-TENANCY CORE +-- ========================================== + +ALTER TABLE "tenant_empresas" ENABLE ROW LEVEL SECURITY; +ALTER TABLE "tenant_users" ENABLE ROW LEVEL SECURITY; +ALTER TABLE "partner_clients" ENABLE ROW LEVEL SECURITY; +ALTER TABLE "partner_commissions" ENABLE ROW LEVEL SECURITY; + +-- ========================================== +-- PROCESS COMPASS +-- ========================================== + +ALTER TABLE "pc_clients" ENABLE ROW LEVEL SECURITY; +ALTER TABLE "pc_projects" ENABLE ROW LEVEL SECURITY; +ALTER TABLE "pc_crm_stages" ENABLE ROW LEVEL SECURITY; +ALTER TABLE "pc_crm_leads" ENABLE ROW LEVEL SECURITY; +ALTER TABLE "pc_crm_opportunities" ENABLE ROW LEVEL SECURITY; +ALTER TABLE "pc_crm_activities" ENABLE ROW LEVEL SECURITY; +ALTER TABLE "pc_pdca_cycles" ENABLE ROW LEVEL SECURITY; +ALTER TABLE "pc_requirements" ENABLE ROW LEVEL SECURITY; + +-- ========================================== +-- CRM EXPANDIDO +-- ========================================== + +ALTER TABLE "crm_partners" ENABLE ROW LEVEL SECURITY; +ALTER TABLE "crm_contracts" ENABLE ROW LEVEL SECURITY; +ALTER TABLE "crm_channels" ENABLE ROW LEVEL SECURITY; +ALTER TABLE "crm_threads" ENABLE ROW LEVEL SECURITY; +ALTER TABLE "crm_quick_messages" ENABLE ROW LEVEL SECURITY; +ALTER TABLE "crm_campaigns" ENABLE ROW LEVEL SECURITY; +ALTER TABLE "crm_events" ENABLE ROW LEVEL SECURITY; +ALTER TABLE "crm_products" ENABLE ROW LEVEL SECURITY; +ALTER TABLE "crm_clients" ENABLE ROW LEVEL SECURITY; +ALTER TABLE "crm_pipeline_stages" ENABLE ROW LEVEL SECURITY; +ALTER TABLE "crm_leads" ENABLE ROW LEVEL SECURITY; +ALTER TABLE "crm_opportunities" ENABLE ROW LEVEL SECURITY; +ALTER TABLE "crm_proposals" ENABLE ROW LEVEL SECURITY; + +-- ========================================== +-- LOG DE TABELAS COM RLS ATIVADO +-- ========================================== + +COMMENT ON TABLE "workspace_pages" IS 'RLS habilitado - isolamento por tenant'; +COMMENT ON TABLE "quick_notes" IS 'RLS habilitado - isolamento por tenant'; +COMMENT ON TABLE "activity_feed" IS 'RLS habilitado - isolamento por tenant'; +COMMENT ON TABLE "conversations" IS 'RLS habilitado - isolamento por tenant'; +COMMENT ON TABLE "knowledge_base" IS 'RLS habilitado - isolamento por tenant'; +COMMENT ON TABLE "chat_threads" IS 'RLS habilitado - isolamento por tenant'; +COMMENT ON TABLE "manus_runs" IS 'RLS habilitado - isolamento por tenant'; +COMMENT ON TABLE "tenant_empresas" IS 'RLS habilitado - isolamento por tenant'; +COMMENT ON TABLE "tenant_users" IS 'RLS habilitado - isolamento por tenant'; +COMMENT ON TABLE "pc_clients" IS 'RLS habilitado - isolamento por tenant'; +COMMENT ON TABLE "pc_projects" IS 'RLS habilitado - isolamento por tenant'; +COMMENT ON TABLE "crm_clients" IS 'RLS habilitado - isolamento por tenant'; +COMMENT ON TABLE "crm_leads" IS 'RLS habilitado - isolamento por tenant'; +COMMENT ON TABLE "crm_opportunities" IS 'RLS habilitado - isolamento por tenant'; diff --git a/migrations/002_create_policies.sql b/migrations/002_create_policies.sql new file mode 100644 index 0000000..cd34644 --- /dev/null +++ b/migrations/002_create_policies.sql @@ -0,0 +1,711 @@ +-- Migration 002: Create RLS Policies for tenant isolation +-- Data: 2026-04-02 +-- Description: Cria políticas RLS para isolamento de dados por tenant + +-- ========================================== +-- FUNÇÃO AUXILIAR: Obter tenant atual do usuário +-- ========================================== + +CREATE OR REPLACE FUNCTION get_current_tenant_id() +RETURNS INTEGER AS $$ +DECLARE + tenant_id INTEGER; +BEGIN + -- Obtém tenant_id do contexto da aplicação (setado via application_name ou custom config) + -- Ou busca pelo user_id da sessão atual + BEGIN + tenant_id := current_setting('app.current_tenant_id', true)::INTEGER; + EXCEPTION WHEN OTHERS THEN + tenant_id := NULL; + END; + + RETURN tenant_id; +END; +$$ LANGUAGE plpgsql SECURITY DEFINER; + +-- ========================================== +-- FUNÇÃO: Verificar se usuário é admin/master +-- ========================================== + +CREATE OR REPLACE FUNCTION is_tenant_admin() +RETURNS BOOLEAN AS $$ +DECLARE + is_admin BOOLEAN; + tenant_role TEXT; +BEGIN + BEGIN + tenant_role := current_setting('app.current_tenant_role', true); + is_admin := (tenant_role = 'owner' OR tenant_role = 'admin'); + EXCEPTION WHEN OTHERS THEN + is_admin := FALSE; + END; + + RETURN is_admin; +END; +$$ LANGUAGE plpgsql SECURITY DEFINER; + +-- ========================================== +-- PRODUCTIVITY & WORKSPACE +-- ========================================== + +-- workspace_pages policies +DROP POLICY IF EXISTS workspace_pages_tenant_select ON "workspace_pages"; +DROP POLICY IF EXISTS workspace_pages_tenant_insert ON "workspace_pages"; +DROP POLICY IF EXISTS workspace_pages_tenant_update ON "workspace_pages"; +DROP POLICY IF EXISTS workspace_pages_tenant_delete ON "workspace_pages"; + +CREATE POLICY workspace_pages_tenant_select ON "workspace_pages" + FOR SELECT USING (tenant_id = get_current_tenant_id()); + +CREATE POLICY workspace_pages_tenant_insert ON "workspace_pages" + FOR INSERT WITH CHECK (tenant_id = get_current_tenant_id()); + +CREATE POLICY workspace_pages_tenant_update ON "workspace_pages" + FOR UPDATE USING (tenant_id = get_current_tenant_id()) + WITH CHECK (tenant_id = get_current_tenant_id()); + +CREATE POLICY workspace_pages_tenant_delete ON "workspace_pages" + FOR DELETE USING (tenant_id = get_current_tenant_id()); + +-- quick_notes policies +DROP POLICY IF EXISTS quick_notes_tenant_select ON "quick_notes"; +DROP POLICY IF EXISTS quick_notes_tenant_insert ON "quick_notes"; +DROP POLICY IF EXISTS quick_notes_tenant_update ON "quick_notes"; +DROP POLICY IF EXISTS quick_notes_tenant_delete ON "quick_notes"; + +CREATE POLICY quick_notes_tenant_select ON "quick_notes" + FOR SELECT USING (tenant_id = get_current_tenant_id()); + +CREATE POLICY quick_notes_tenant_insert ON "quick_notes" + FOR INSERT WITH CHECK (tenant_id = get_current_tenant_id()); + +CREATE POLICY quick_notes_tenant_update ON "quick_notes" + FOR UPDATE USING (tenant_id = get_current_tenant_id()) + WITH CHECK (tenant_id = get_current_tenant_id()); + +CREATE POLICY quick_notes_tenant_delete ON "quick_notes" + FOR DELETE USING (tenant_id = get_current_tenant_id()); + +-- activity_feed policies +DROP POLICY IF EXISTS activity_feed_tenant_select ON "activity_feed"; +DROP POLICY IF EXISTS activity_feed_tenant_insert ON "activity_feed"; +DROP POLICY IF EXISTS activity_feed_tenant_update ON "activity_feed"; +DROP POLICY IF EXISTS activity_feed_tenant_delete ON "activity_feed"; + +CREATE POLICY activity_feed_tenant_select ON "activity_feed" + FOR SELECT USING (tenant_id = get_current_tenant_id()); + +CREATE POLICY activity_feed_tenant_insert ON "activity_feed" + FOR INSERT WITH CHECK (tenant_id = get_current_tenant_id()); + +CREATE POLICY activity_feed_tenant_update ON "activity_feed" + FOR UPDATE USING (tenant_id = get_current_tenant_id()) + WITH CHECK (tenant_id = get_current_tenant_id()); + +CREATE POLICY activity_feed_tenant_delete ON "activity_feed" + FOR DELETE USING (tenant_id = get_current_tenant_id()); + +-- conversations policies +DROP POLICY IF EXISTS conversations_tenant_select ON "conversations"; +DROP POLICY IF EXISTS conversations_tenant_insert ON "conversations"; +DROP POLICY IF EXISTS conversations_tenant_update ON "conversations"; +DROP POLICY IF EXISTS conversations_tenant_delete ON "conversations"; + +CREATE POLICY conversations_tenant_select ON "conversations" + FOR SELECT USING (tenant_id = get_current_tenant_id()); + +CREATE POLICY conversations_tenant_insert ON "conversations" + FOR INSERT WITH CHECK (tenant_id = get_current_tenant_id()); + +CREATE POLICY conversations_tenant_update ON "conversations" + FOR UPDATE USING (tenant_id = get_current_tenant_id()) + WITH CHECK (tenant_id = get_current_tenant_id()); + +CREATE POLICY conversations_tenant_delete ON "conversations" + FOR DELETE USING (tenant_id = get_current_tenant_id()); + +-- knowledge_base policies +DROP POLICY IF EXISTS knowledge_base_tenant_select ON "knowledge_base"; +DROP POLICY IF EXISTS knowledge_base_tenant_insert ON "knowledge_base"; +DROP POLICY IF EXISTS knowledge_base_tenant_update ON "knowledge_base"; +DROP POLICY IF EXISTS knowledge_base_tenant_delete ON "knowledge_base"; + +CREATE POLICY knowledge_base_tenant_select ON "knowledge_base" + FOR SELECT USING (tenant_id = get_current_tenant_id()); + +CREATE POLICY knowledge_base_tenant_insert ON "knowledge_base" + FOR INSERT WITH CHECK (tenant_id = get_current_tenant_id()); + +CREATE POLICY knowledge_base_tenant_update ON "knowledge_base" + FOR UPDATE USING (tenant_id = get_current_tenant_id()) + WITH CHECK (tenant_id = get_current_tenant_id()); + +CREATE POLICY knowledge_base_tenant_delete ON "knowledge_base" + FOR DELETE USING (tenant_id = get_current_tenant_id()); + +-- chat_threads policies +DROP POLICY IF EXISTS chat_threads_tenant_select ON "chat_threads"; +DROP POLICY IF EXISTS chat_threads_tenant_insert ON "chat_threads"; +DROP POLICY IF EXISTS chat_threads_tenant_update ON "chat_threads"; +DROP POLICY IF EXISTS chat_threads_tenant_delete ON "chat_threads"; + +CREATE POLICY chat_threads_tenant_select ON "chat_threads" + FOR SELECT USING (tenant_id = get_current_tenant_id()); + +CREATE POLICY chat_threads_tenant_insert ON "chat_threads" + FOR INSERT WITH CHECK (tenant_id = get_current_tenant_id()); + +CREATE POLICY chat_threads_tenant_update ON "chat_threads" + FOR UPDATE USING (tenant_id = get_current_tenant_id()) + WITH CHECK (tenant_id = get_current_tenant_id()); + +CREATE POLICY chat_threads_tenant_delete ON "chat_threads" + FOR DELETE USING (tenant_id = get_current_tenant_id()); + +-- manus_runs policies +DROP POLICY IF EXISTS manus_runs_tenant_select ON "manus_runs"; +DROP POLICY IF EXISTS manus_runs_tenant_insert ON "manus_runs"; +DROP POLICY IF EXISTS manus_runs_tenant_update ON "manus_runs"; +DROP POLICY IF EXISTS manus_runs_tenant_delete ON "manus_runs"; + +CREATE POLICY manus_runs_tenant_select ON "manus_runs" + FOR SELECT USING (tenant_id = get_current_tenant_id()); + +CREATE POLICY manus_runs_tenant_insert ON "manus_runs" + FOR INSERT WITH CHECK (tenant_id = get_current_tenant_id()); + +CREATE POLICY manus_runs_tenant_update ON "manus_runs" + FOR UPDATE USING (tenant_id = get_current_tenant_id()) + WITH CHECK (tenant_id = get_current_tenant_id()); + +CREATE POLICY manus_runs_tenant_delete ON "manus_runs" + FOR DELETE USING (tenant_id = get_current_tenant_id()); + +-- ========================================== +-- LOW-CODE / DEV CENTER +-- ========================================== + +-- arc_doctypes policies +DROP POLICY IF EXISTS arc_doctypes_tenant_select ON "arc_doctypes"; +DROP POLICY IF EXISTS arc_doctypes_tenant_insert ON "arc_doctypes"; +DROP POLICY IF EXISTS arc_doctypes_tenant_update ON "arc_doctypes"; +DROP POLICY IF EXISTS arc_doctypes_tenant_delete ON "arc_doctypes"; + +CREATE POLICY arc_doctypes_tenant_select ON "arc_doctypes" + FOR SELECT USING (tenant_id = get_current_tenant_id() OR tenant_id IS NULL); + +CREATE POLICY arc_doctypes_tenant_insert ON "arc_doctypes" + FOR INSERT WITH CHECK (tenant_id = get_current_tenant_id()); + +CREATE POLICY arc_doctypes_tenant_update ON "arc_doctypes" + FOR UPDATE USING (tenant_id = get_current_tenant_id()) + WITH CHECK (tenant_id = get_current_tenant_id()); + +CREATE POLICY arc_doctypes_tenant_delete ON "arc_doctypes" + FOR DELETE USING (tenant_id = get_current_tenant_id()); + +-- arc_layouts policies +DROP POLICY IF EXISTS arc_layouts_tenant_select ON "arc_layouts"; +DROP POLICY IF EXISTS arc_layouts_tenant_insert ON "arc_layouts"; +DROP POLICY IF EXISTS arc_layouts_tenant_update ON "arc_layouts"; +DROP POLICY IF EXISTS arc_layouts_tenant_delete ON "arc_layouts"; + +CREATE POLICY arc_layouts_tenant_select ON "arc_layouts" + FOR SELECT USING (tenant_id = get_current_tenant_id() OR tenant_id IS NULL); + +CREATE POLICY arc_layouts_tenant_insert ON "arc_layouts" + FOR INSERT WITH CHECK (tenant_id = get_current_tenant_id()); + +CREATE POLICY arc_layouts_tenant_update ON "arc_layouts" + FOR UPDATE USING (tenant_id = get_current_tenant_id()) + WITH CHECK (tenant_id = get_current_tenant_id()); + +CREATE POLICY arc_layouts_tenant_delete ON "arc_layouts" + FOR DELETE USING (tenant_id = get_current_tenant_id()); + +-- ========================================== +-- MULTI-TENANCY CORE +-- ========================================== + +-- tenant_empresas policies +DROP POLICY IF EXISTS tenant_empresas_tenant_select ON "tenant_empresas"; +DROP POLICY IF EXISTS tenant_empresas_tenant_insert ON "tenant_empresas"; +DROP POLICY IF EXISTS tenant_empresas_tenant_update ON "tenant_empresas"; +DROP POLICY IF EXISTS tenant_empresas_tenant_delete ON "tenant_empresas"; + +CREATE POLICY tenant_empresas_tenant_select ON "tenant_empresas" + FOR SELECT USING (tenant_id = get_current_tenant_id()); + +CREATE POLICY tenant_empresas_tenant_insert ON "tenant_empresas" + FOR INSERT WITH CHECK (tenant_id = get_current_tenant_id()); + +CREATE POLICY tenant_empresas_tenant_update ON "tenant_empresas" + FOR UPDATE USING (tenant_id = get_current_tenant_id()) + WITH CHECK (tenant_id = get_current_tenant_id()); + +CREATE POLICY tenant_empresas_tenant_delete ON "tenant_empresas" + FOR DELETE USING (tenant_id = get_current_tenant_id()); + +-- tenant_users policies (tabela de junção - verifica se user pertence ao tenant) +DROP POLICY IF EXISTS tenant_users_tenant_select ON "tenant_users"; +DROP POLICY IF EXISTS tenant_users_tenant_insert ON "tenant_users"; +DROP POLICY IF EXISTS tenant_users_tenant_update ON "tenant_users"; +DROP POLICY IF EXISTS tenant_users_tenant_delete ON "tenant_users"; + +CREATE POLICY tenant_users_tenant_select ON "tenant_users" + FOR SELECT USING (tenant_id = get_current_tenant_id()); + +CREATE POLICY tenant_users_tenant_insert ON "tenant_users" + FOR INSERT WITH CHECK (tenant_id = get_current_tenant_id()); + +CREATE POLICY tenant_users_tenant_update ON "tenant_users" + FOR UPDATE USING (tenant_id = get_current_tenant_id()) + WITH CHECK (tenant_id = get_current_tenant_id()); + +CREATE POLICY tenant_users_tenant_delete ON "tenant_users" + FOR DELETE USING (tenant_id = get_current_tenant_id()); + +-- partner_clients policies +DROP POLICY IF EXISTS partner_clients_tenant_select ON "partner_clients"; +DROP POLICY IF EXISTS partner_clients_tenant_insert ON "partner_clients"; +DROP POLICY IF EXISTS partner_clients_tenant_update ON "partner_clients"; +DROP POLICY IF EXISTS partner_clients_tenant_delete ON "partner_clients"; + +CREATE POLICY partner_clients_tenant_select ON "partner_clients" + FOR SELECT USING (partner_id = get_current_tenant_id() OR client_id = get_current_tenant_id()); + +CREATE POLICY partner_clients_tenant_insert ON "partner_clients" + FOR INSERT WITH CHECK (partner_id = get_current_tenant_id()); + +CREATE POLICY partner_clients_tenant_update ON "partner_clients" + FOR UPDATE USING (partner_id = get_current_tenant_id()) + WITH CHECK (partner_id = get_current_tenant_id()); + +CREATE POLICY partner_clients_tenant_delete ON "partner_clients" + FOR DELETE USING (partner_id = get_current_tenant_id()); + +-- partner_commissions policies +DROP POLICY IF EXISTS partner_commissions_tenant_select ON "partner_commissions"; +DROP POLICY IF EXISTS partner_commissions_tenant_insert ON "partner_commissions"; +DROP POLICY IF EXISTS partner_commissions_tenant_update ON "partner_commissions"; +DROP POLICY IF EXISTS partner_commissions_tenant_delete ON "partner_commissions"; + +CREATE POLICY partner_commissions_tenant_select ON "partner_commissions" + FOR SELECT USING (partner_id = get_current_tenant_id()); + +CREATE POLICY partner_commissions_tenant_insert ON "partner_commissions" + FOR INSERT WITH CHECK (partner_id = get_current_tenant_id()); + +CREATE POLICY partner_commissions_tenant_update ON "partner_commissions" + FOR UPDATE USING (partner_id = get_current_tenant_id()) + WITH CHECK (partner_id = get_current_tenant_id()); + +CREATE POLICY partner_commissions_tenant_delete ON "partner_commissions" + FOR DELETE USING (partner_id = get_current_tenant_id()); + +-- ========================================== +-- PROCESS COMPASS +-- ========================================== + +-- pc_clients policies +DROP POLICY IF EXISTS pc_clients_tenant_select ON "pc_clients"; +DROP POLICY IF EXISTS pc_clients_tenant_insert ON "pc_clients"; +DROP POLICY IF EXISTS pc_clients_tenant_update ON "pc_clients"; +DROP POLICY IF EXISTS pc_clients_tenant_delete ON "pc_clients"; + +CREATE POLICY pc_clients_tenant_select ON "pc_clients" + FOR SELECT USING (tenant_id = get_current_tenant_id()); + +CREATE POLICY pc_clients_tenant_insert ON "pc_clients" + FOR INSERT WITH CHECK (tenant_id = get_current_tenant_id()); + +CREATE POLICY pc_clients_tenant_update ON "pc_clients" + FOR UPDATE USING (tenant_id = get_current_tenant_id()) + WITH CHECK (tenant_id = get_current_tenant_id()); + +CREATE POLICY pc_clients_tenant_delete ON "pc_clients" + FOR DELETE USING (tenant_id = get_current_tenant_id()); + +-- pc_projects policies +DROP POLICY IF EXISTS pc_projects_tenant_select ON "pc_projects"; +DROP POLICY IF EXISTS pc_projects_tenant_insert ON "pc_projects"; +DROP POLICY IF EXISTS pc_projects_tenant_update ON "pc_projects"; +DROP POLICY IF EXISTS pc_projects_tenant_delete ON "pc_projects"; + +CREATE POLICY pc_projects_tenant_select ON "pc_projects" + FOR SELECT USING (tenant_id = get_current_tenant_id()); + +CREATE POLICY pc_projects_tenant_insert ON "pc_projects" + FOR INSERT WITH CHECK (tenant_id = get_current_tenant_id()); + +CREATE POLICY pc_projects_tenant_update ON "pc_projects" + FOR UPDATE USING (tenant_id = get_current_tenant_id()) + WITH CHECK (tenant_id = get_current_tenant_id()); + +CREATE POLICY pc_projects_tenant_delete ON "pc_projects" + FOR DELETE USING (tenant_id = get_current_tenant_id()); + +-- pc_crm_stages policies +DROP POLICY IF EXISTS pc_crm_stages_tenant_select ON "pc_crm_stages"; +DROP POLICY IF EXISTS pc_crm_stages_tenant_insert ON "pc_crm_stages"; +DROP POLICY IF EXISTS pc_crm_stages_tenant_update ON "pc_crm_stages"; +DROP POLICY IF EXISTS pc_crm_stages_tenant_delete ON "pc_crm_stages"; + +CREATE POLICY pc_crm_stages_tenant_select ON "pc_crm_stages" + FOR SELECT USING (tenant_id = get_current_tenant_id()); + +CREATE POLICY pc_crm_stages_tenant_insert ON "pc_crm_stages" + FOR INSERT WITH CHECK (tenant_id = get_current_tenant_id()); + +CREATE POLICY pc_crm_stages_tenant_update ON "pc_crm_stages" + FOR UPDATE USING (tenant_id = get_current_tenant_id()) + WITH CHECK (tenant_id = get_current_tenant_id()); + +CREATE POLICY pc_crm_stages_tenant_delete ON "pc_crm_stages" + FOR DELETE USING (tenant_id = get_current_tenant_id()); + +-- pc_crm_leads policies +DROP POLICY IF EXISTS pc_crm_leads_tenant_select ON "pc_crm_leads"; +DROP POLICY IF EXISTS pc_crm_leads_tenant_insert ON "pc_crm_leads"; +DROP POLICY IF EXISTS pc_crm_leads_tenant_update ON "pc_crm_leads"; +DROP POLICY IF EXISTS pc_crm_leads_tenant_delete ON "pc_crm_leads"; + +CREATE POLICY pc_crm_leads_tenant_select ON "pc_crm_leads" + FOR SELECT USING (tenant_id = get_current_tenant_id()); + +CREATE POLICY pc_crm_leads_tenant_insert ON "pc_crm_leads" + FOR INSERT WITH CHECK (tenant_id = get_current_tenant_id()); + +CREATE POLICY pc_crm_leads_tenant_update ON "pc_crm_leads" + FOR UPDATE USING (tenant_id = get_current_tenant_id()) + WITH CHECK (tenant_id = get_current_tenant_id()); + +CREATE POLICY pc_crm_leads_tenant_delete ON "pc_crm_leads" + FOR DELETE USING (tenant_id = get_current_tenant_id()); + +-- pc_crm_opportunities policies +DROP POLICY IF EXISTS pc_crm_opportunities_tenant_select ON "pc_crm_opportunities"; +DROP POLICY IF EXISTS pc_crm_opportunities_tenant_insert ON "pc_crm_opportunities"; +DROP POLICY IF EXISTS pc_crm_opportunities_tenant_update ON "pc_crm_opportunities"; +DROP POLICY IF EXISTS pc_crm_opportunities_tenant_delete ON "pc_crm_opportunities"; + +CREATE POLICY pc_crm_opportunities_tenant_select ON "pc_crm_opportunities" + FOR SELECT USING (tenant_id = get_current_tenant_id()); + +CREATE POLICY pc_crm_opportunities_tenant_insert ON "pc_crm_opportunities" + FOR INSERT WITH CHECK (tenant_id = get_current_tenant_id()); + +CREATE POLICY pc_crm_opportunities_tenant_update ON "pc_crm_opportunities" + FOR UPDATE USING (tenant_id = get_current_tenant_id()) + WITH CHECK (tenant_id = get_current_tenant_id()); + +CREATE POLICY pc_crm_opportunities_tenant_delete ON "pc_crm_opportunities" + FOR DELETE USING (tenant_id = get_current_tenant_id()); + +-- pc_crm_activities policies +DROP POLICY IF EXISTS pc_crm_activities_tenant_select ON "pc_crm_activities"; +DROP POLICY IF EXISTS pc_crm_activities_tenant_insert ON "pc_crm_activities"; +DROP POLICY IF EXISTS pc_crm_activities_tenant_update ON "pc_crm_activities"; +DROP POLICY IF EXISTS pc_crm_activities_tenant_delete ON "pc_crm_activities"; + +CREATE POLICY pc_crm_activities_tenant_select ON "pc_crm_activities" + FOR SELECT USING (tenant_id = get_current_tenant_id()); + +CREATE POLICY pc_crm_activities_tenant_insert ON "pc_crm_activities" + FOR INSERT WITH CHECK (tenant_id = get_current_tenant_id()); + +CREATE POLICY pc_crm_activities_tenant_update ON "pc_crm_activities" + FOR UPDATE USING (tenant_id = get_current_tenant_id()) + WITH CHECK (tenant_id = get_current_tenant_id()); + +CREATE POLICY pc_crm_activities_tenant_delete ON "pc_crm_activities" + FOR DELETE USING (tenant_id = get_current_tenant_id()); + +-- pc_pdca_cycles policies +DROP POLICY IF EXISTS pc_pdca_cycles_tenant_select ON "pc_pdca_cycles"; +DROP POLICY IF EXISTS pc_pdca_cycles_tenant_insert ON "pc_pdca_cycles"; +DROP POLICY IF EXISTS pc_pdca_cycles_tenant_update ON "pc_pdca_cycles"; +DROP POLICY IF EXISTS pc_pdca_cycles_tenant_delete ON "pc_pdca_cycles"; + +CREATE POLICY pc_pdca_cycles_tenant_select ON "pc_pdca_cycles" + FOR SELECT USING (tenant_id = get_current_tenant_id()); + +CREATE POLICY pc_pdca_cycles_tenant_insert ON "pc_pdca_cycles" + FOR INSERT WITH CHECK (tenant_id = get_current_tenant_id()); + +CREATE POLICY pc_pdca_cycles_tenant_update ON "pc_pdca_cycles" + FOR UPDATE USING (tenant_id = get_current_tenant_id()) + WITH CHECK (tenant_id = get_current_tenant_id()); + +CREATE POLICY pc_pdca_cycles_tenant_delete ON "pc_pdca_cycles" + FOR DELETE USING (tenant_id = get_current_tenant_id()); + +-- pc_requirements policies +DROP POLICY IF EXISTS pc_requirements_tenant_select ON "pc_requirements"; +DROP POLICY IF EXISTS pc_requirements_tenant_insert ON "pc_requirements"; +DROP POLICY IF EXISTS pc_requirements_tenant_update ON "pc_requirements"; +DROP POLICY IF EXISTS pc_requirements_tenant_delete ON "pc_requirements"; + +CREATE POLICY pc_requirements_tenant_select ON "pc_requirements" + FOR SELECT USING (tenant_id = get_current_tenant_id()); + +CREATE POLICY pc_requirements_tenant_insert ON "pc_requirements" + FOR INSERT WITH CHECK (tenant_id = get_current_tenant_id()); + +CREATE POLICY pc_requirements_tenant_update ON "pc_requirements" + FOR UPDATE USING (tenant_id = get_current_tenant_id()) + WITH CHECK (tenant_id = get_current_tenant_id()); + +CREATE POLICY pc_requirements_tenant_delete ON "pc_requirements" + FOR DELETE USING (tenant_id = get_current_tenant_id()); + +-- ========================================== +-- CRM EXPANDIDO +-- ========================================== + +-- crm_partners policies +DROP POLICY IF EXISTS crm_partners_tenant_select ON "crm_partners"; +DROP POLICY IF EXISTS crm_partners_tenant_insert ON "crm_partners"; +DROP POLICY IF EXISTS crm_partners_tenant_update ON "crm_partners"; +DROP POLICY IF EXISTS crm_partners_tenant_delete ON "crm_partners"; + +CREATE POLICY crm_partners_tenant_select ON "crm_partners" + FOR SELECT USING (tenant_id = get_current_tenant_id() OR tenant_id IS NULL); + +CREATE POLICY crm_partners_tenant_insert ON "crm_partners" + FOR INSERT WITH CHECK (tenant_id = get_current_tenant_id()); + +CREATE POLICY crm_partners_tenant_update ON "crm_partners" + FOR UPDATE USING (tenant_id = get_current_tenant_id()) + WITH CHECK (tenant_id = get_current_tenant_id()); + +CREATE POLICY crm_partners_tenant_delete ON "crm_partners" + FOR DELETE USING (tenant_id = get_current_tenant_id()); + +-- crm_contracts policies +DROP POLICY IF EXISTS crm_contracts_tenant_select ON "crm_contracts"; +DROP POLICY IF EXISTS crm_contracts_tenant_insert ON "crm_contracts"; +DROP POLICY IF EXISTS crm_contracts_tenant_update ON "crm_contracts"; +DROP POLICY IF EXISTS crm_contracts_tenant_delete ON "crm_contracts"; + +CREATE POLICY crm_contracts_tenant_select ON "crm_contracts" + FOR SELECT USING (tenant_id = get_current_tenant_id()); + +CREATE POLICY crm_contracts_tenant_insert ON "crm_contracts" + FOR INSERT WITH CHECK (tenant_id = get_current_tenant_id()); + +CREATE POLICY crm_contracts_tenant_update ON "crm_contracts" + FOR UPDATE USING (tenant_id = get_current_tenant_id()) + WITH CHECK (tenant_id = get_current_tenant_id()); + +CREATE POLICY crm_contracts_tenant_delete ON "crm_contracts" + FOR DELETE USING (tenant_id = get_current_tenant_id()); + +-- crm_channels policies +DROP POLICY IF EXISTS crm_channels_tenant_select ON "crm_channels"; +DROP POLICY IF EXISTS crm_channels_tenant_insert ON "crm_channels"; +DROP POLICY IF EXISTS crm_channels_tenant_update ON "crm_channels"; +DROP POLICY IF EXISTS crm_channels_tenant_delete ON "crm_channels"; + +CREATE POLICY crm_channels_tenant_select ON "crm_channels" + FOR SELECT USING (tenant_id = get_current_tenant_id()); + +CREATE POLICY crm_channels_tenant_insert ON "crm_channels" + FOR INSERT WITH CHECK (tenant_id = get_current_tenant_id()); + +CREATE POLICY crm_channels_tenant_update ON "crm_channels" + FOR UPDATE USING (tenant_id = get_current_tenant_id()) + WITH CHECK (tenant_id = get_current_tenant_id()); + +CREATE POLICY crm_channels_tenant_delete ON "crm_channels" + FOR DELETE USING (tenant_id = get_current_tenant_id()); + +-- crm_threads policies +DROP POLICY IF EXISTS crm_threads_tenant_select ON "crm_threads"; +DROP POLICY IF EXISTS crm_threads_tenant_insert ON "crm_threads"; +DROP POLICY IF EXISTS crm_threads_tenant_update ON "crm_threads"; +DROP POLICY IF EXISTS crm_threads_tenant_delete ON "crm_threads"; + +CREATE POLICY crm_threads_tenant_select ON "crm_threads" + FOR SELECT USING (tenant_id = get_current_tenant_id()); + +CREATE POLICY crm_threads_tenant_insert ON "crm_threads" + FOR INSERT WITH CHECK (tenant_id = get_current_tenant_id()); + +CREATE POLICY crm_threads_tenant_update ON "crm_threads" + FOR UPDATE USING (tenant_id = get_current_tenant_id()) + WITH CHECK (tenant_id = get_current_tenant_id()); + +CREATE POLICY crm_threads_tenant_delete ON "crm_threads" + FOR DELETE USING (tenant_id = get_current_tenant_id()); + +-- crm_quick_messages policies +DROP POLICY IF EXISTS crm_quick_messages_tenant_select ON "crm_quick_messages"; +DROP POLICY IF EXISTS crm_quick_messages_tenant_insert ON "crm_quick_messages"; +DROP POLICY IF EXISTS crm_quick_messages_tenant_update ON "crm_quick_messages"; +DROP POLICY IF EXISTS crm_quick_messages_tenant_delete ON "crm_quick_messages"; + +CREATE POLICY crm_quick_messages_tenant_select ON "crm_quick_messages" + FOR SELECT USING (tenant_id = get_current_tenant_id()); + +CREATE POLICY crm_quick_messages_tenant_insert ON "crm_quick_messages" + FOR INSERT WITH CHECK (tenant_id = get_current_tenant_id()); + +CREATE POLICY crm_quick_messages_tenant_update ON "crm_quick_messages" + FOR UPDATE USING (tenant_id = get_current_tenant_id()) + WITH CHECK (tenant_id = get_current_tenant_id()); + +CREATE POLICY crm_quick_messages_tenant_delete ON "crm_quick_messages" + FOR DELETE USING (tenant_id = get_current_tenant_id()); + +-- crm_campaigns policies +DROP POLICY IF EXISTS crm_campaigns_tenant_select ON "crm_campaigns"; +DROP POLICY IF EXISTS crm_campaigns_tenant_insert ON "crm_campaigns"; +DROP POLICY IF EXISTS crm_campaigns_tenant_update ON "crm_campaigns"; +DROP POLICY IF EXISTS crm_campaigns_tenant_delete ON "crm_campaigns"; + +CREATE POLICY crm_campaigns_tenant_select ON "crm_campaigns" + FOR SELECT USING (tenant_id = get_current_tenant_id()); + +CREATE POLICY crm_campaigns_tenant_insert ON "crm_campaigns" + FOR INSERT WITH CHECK (tenant_id = get_current_tenant_id()); + +CREATE POLICY crm_campaigns_tenant_update ON "crm_campaigns" + FOR UPDATE USING (tenant_id = get_current_tenant_id()) + WITH CHECK (tenant_id = get_current_tenant_id()); + +CREATE POLICY crm_campaigns_tenant_delete ON "crm_campaigns" + FOR DELETE USING (tenant_id = get_current_tenant_id()); + +-- crm_events policies +DROP POLICY IF EXISTS crm_events_tenant_select ON "crm_events"; +DROP POLICY IF EXISTS crm_events_tenant_insert ON "crm_events"; +DROP POLICY IF EXISTS crm_events_tenant_update ON "crm_events"; +DROP POLICY IF EXISTS crm_events_tenant_delete ON "crm_events"; + +CREATE POLICY crm_events_tenant_select ON "crm_events" + FOR SELECT USING (tenant_id = get_current_tenant_id()); + +CREATE POLICY crm_events_tenant_insert ON "crm_events" + FOR INSERT WITH CHECK (tenant_id = get_current_tenant_id()); + +CREATE POLICY crm_events_tenant_update ON "crm_events" + FOR UPDATE USING (tenant_id = get_current_tenant_id()) + WITH CHECK (tenant_id = get_current_tenant_id()); + +CREATE POLICY crm_events_tenant_delete ON "crm_events" + FOR DELETE USING (tenant_id = get_current_tenant_id()); + +-- crm_products policies +DROP POLICY IF EXISTS crm_products_tenant_select ON "crm_products"; +DROP POLICY IF EXISTS crm_products_tenant_insert ON "crm_products"; +DROP POLICY IF EXISTS crm_products_tenant_update ON "crm_products"; +DROP POLICY IF EXISTS crm_products_tenant_delete ON "crm_products"; + +CREATE POLICY crm_products_tenant_select ON "crm_products" + FOR SELECT USING (tenant_id = get_current_tenant_id() OR tenant_id IS NULL); + +CREATE POLICY crm_products_tenant_insert ON "crm_products" + FOR INSERT WITH CHECK (tenant_id = get_current_tenant_id()); + +CREATE POLICY crm_products_tenant_update ON "crm_products" + FOR UPDATE USING (tenant_id = get_current_tenant_id()) + WITH CHECK (tenant_id = get_current_tenant_id()); + +CREATE POLICY crm_products_tenant_delete ON "crm_products" + FOR DELETE USING (tenant_id = get_current_tenant_id()); + +-- crm_clients policies +DROP POLICY IF EXISTS crm_clients_tenant_select ON "crm_clients"; +DROP POLICY IF EXISTS crm_clients_tenant_insert ON "crm_clients"; +DROP POLICY IF EXISTS crm_clients_tenant_update ON "crm_clients"; +DROP POLICY IF EXISTS crm_clients_tenant_delete ON "crm_clients"; + +CREATE POLICY crm_clients_tenant_select ON "crm_clients" + FOR SELECT USING (tenant_id = get_current_tenant_id()); + +CREATE POLICY crm_clients_tenant_insert ON "crm_clients" + FOR INSERT WITH CHECK (tenant_id = get_current_tenant_id()); + +CREATE POLICY crm_clients_tenant_update ON "crm_clients" + FOR UPDATE USING (tenant_id = get_current_tenant_id()) + WITH CHECK (tenant_id = get_current_tenant_id()); + +CREATE POLICY crm_clients_tenant_delete ON "crm_clients" + FOR DELETE USING (tenant_id = get_current_tenant_id()); + +-- crm_pipeline_stages policies +DROP POLICY IF EXISTS crm_pipeline_stages_tenant_select ON "crm_pipeline_stages"; +DROP POLICY IF EXISTS crm_pipeline_stages_tenant_insert ON "crm_pipeline_stages"; +DROP POLICY IF EXISTS crm_pipeline_stages_tenant_update ON "crm_pipeline_stages"; +DROP POLICY IF EXISTS crm_pipeline_stages_tenant_delete ON "crm_pipeline_stages"; + +CREATE POLICY crm_pipeline_stages_tenant_select ON "crm_pipeline_stages" + FOR SELECT USING (tenant_id = get_current_tenant_id()); + +CREATE POLICY crm_pipeline_stages_tenant_insert ON "crm_pipeline_stages" + FOR INSERT WITH CHECK (tenant_id = get_current_tenant_id()); + +CREATE POLICY crm_pipeline_stages_tenant_update ON "crm_pipeline_stages" + FOR UPDATE USING (tenant_id = get_current_tenant_id()) + WITH CHECK (tenant_id = get_current_tenant_id()); + +CREATE POLICY crm_pipeline_stages_tenant_delete ON "crm_pipeline_stages" + FOR DELETE USING (tenant_id = get_current_tenant_id()); + +-- crm_leads policies +DROP POLICY IF EXISTS crm_leads_tenant_select ON "crm_leads"; +DROP POLICY IF EXISTS crm_leads_tenant_insert ON "crm_leads"; +DROP POLICY IF EXISTS crm_leads_tenant_update ON "crm_leads"; +DROP POLICY IF EXISTS crm_leads_tenant_delete ON "crm_leads"; + +CREATE POLICY crm_leads_tenant_select ON "crm_leads" + FOR SELECT USING (tenant_id = get_current_tenant_id()); + +CREATE POLICY crm_leads_tenant_insert ON "crm_leads" + FOR INSERT WITH CHECK (tenant_id = get_current_tenant_id()); + +CREATE POLICY crm_leads_tenant_update ON "crm_leads" + FOR UPDATE USING (tenant_id = get_current_tenant_id()) + WITH CHECK (tenant_id = get_current_tenant_id()); + +CREATE POLICY crm_leads_tenant_delete ON "crm_leads" + FOR DELETE USING (tenant_id = get_current_tenant_id()); + +-- crm_opportunities policies +DROP POLICY IF EXISTS crm_opportunities_tenant_select ON "crm_opportunities"; +DROP POLICY IF EXISTS crm_opportunities_tenant_insert ON "crm_opportunities"; +DROP POLICY IF EXISTS crm_opportunities_tenant_update ON "crm_opportunities"; +DROP POLICY IF EXISTS crm_opportunities_tenant_delete ON "crm_opportunities"; + +CREATE POLICY crm_opportunities_tenant_select ON "crm_opportunities" + FOR SELECT USING (tenant_id = get_current_tenant_id()); + +CREATE POLICY crm_opportunities_tenant_insert ON "crm_opportunities" + FOR INSERT WITH CHECK (tenant_id = get_current_tenant_id()); + +CREATE POLICY crm_opportunities_tenant_update ON "crm_opportunities" + FOR UPDATE USING (tenant_id = get_current_tenant_id()) + WITH CHECK (tenant_id = get_current_tenant_id()); + +CREATE POLICY crm_opportunities_tenant_delete ON "crm_opportunities" + FOR DELETE USING (tenant_id = get_current_tenant_id()); + +-- crm_proposals policies +DROP POLICY IF EXISTS crm_proposals_tenant_select ON "crm_proposals"; +DROP POLICY IF EXISTS crm_proposals_tenant_insert ON "crm_proposals"; +DROP POLICY IF EXISTS crm_proposals_tenant_update ON "crm_proposals"; +DROP POLICY IF EXISTS crm_proposals_tenant_delete ON "crm_proposals"; + +CREATE POLICY crm_proposals_tenant_select ON "crm_proposals" + FOR SELECT USING (tenant_id = get_current_tenant_id()); + +CREATE POLICY crm_proposals_tenant_insert ON "crm_proposals" + FOR INSERT WITH CHECK (tenant_id = get_current_tenant_id()); + +CREATE POLICY crm_proposals_tenant_update ON "crm_proposals" + FOR UPDATE USING (tenant_id = get_current_tenant_id()) + WITH CHECK (tenant_id = get_current_tenant_id()); + +CREATE POLICY crm_proposals_tenant_delete ON "crm_proposals" + FOR DELETE USING (tenant_id = get_current_tenant_id()); diff --git a/migrations/003_tenant_context.sql b/migrations/003_tenant_context.sql new file mode 100644 index 0000000..d3847d6 --- /dev/null +++ b/migrations/003_tenant_context.sql @@ -0,0 +1,381 @@ +-- Migration 003: Tenant Context Management +-- Data: 2026-04-02 +-- Description: Configuração de contexto de tenant e validações automáticas + +-- ========================================== +-- FUNÇÃO: Validar se tenant existe e está ativo +-- ========================================== + +CREATE OR REPLACE FUNCTION validate_tenant_active(tenant_id INTEGER) +RETURNS BOOLEAN AS $$ +DECLARE + tenant_status TEXT; +BEGIN + IF tenant_id IS NULL THEN + RETURN TRUE; -- Algumas tabelas permitem NULL (globais) + END IF; + + SELECT status INTO tenant_status + FROM tenants + WHERE id = tenant_id; + + IF tenant_status IS NULL THEN + RAISE EXCEPTION 'Tenant % não encontrado', tenant_id; + END IF; + + IF tenant_status != 'active' THEN + RAISE EXCEPTION 'Tenant % não está ativo (status: %)', tenant_id, tenant_status; + END IF; + + RETURN TRUE; +END; +$$ LANGUAGE plpgsql SECURITY DEFINER; + +-- ========================================== +-- FUNÇÃO: Obter hierarquia de tenants (para master/partner ver subs) +-- ========================================== + +CREATE OR REPLACE FUNCTION get_tenant_hierarchy(parent_tenant_id INTEGER) +RETURNS TABLE (tenant_id INTEGER) AS $$ +BEGIN + RETURN QUERY + WITH RECURSIVE hierarchy AS ( + -- Base: o próprio tenant + SELECT id FROM tenants WHERE id = parent_tenant_id + + UNION ALL + + -- Recursão: filhos diretos + SELECT t.id + FROM tenants t + JOIN hierarchy h ON t.parent_tenant_id = h.id + ) + SELECT id FROM hierarchy; +END; +$$ LANGUAGE plpgsql SECURITY DEFINER; + +-- ========================================== +-- FUNÇÃO: Verificar se usuário pode acessar tenant específico +-- ========================================== + +CREATE OR REPLACE FUNCTION can_access_tenant(user_id VARCHAR, target_tenant_id INTEGER) +RETURNS BOOLEAN AS $$ +DECLARE + user_tenant_id INTEGER; + user_role TEXT; + target_parent_id INTEGER; + is_owner BOOLEAN; +BEGIN + -- Busca informações do usuário + SELECT + tu.tenant_id, + tu.role, + tu.is_owner = 'true' + INTO + user_tenant_id, + user_role, + is_owner + FROM tenant_users tu + WHERE tu.user_id = can_access_tenant.user_id + LIMIT 1; + + IF user_tenant_id IS NULL THEN + RETURN FALSE; + END IF; + + -- Se é o próprio tenant, permite + IF user_tenant_id = target_tenant_id THEN + RETURN TRUE; + END IF; + + -- Se é master/partner, permite ver filhos + SELECT parent_tenant_id INTO target_parent_id + FROM tenants + WHERE id = target_tenant_id; + + IF target_parent_id = user_tenant_id THEN + RETURN TRUE; + END IF; + + -- Se é admin master, pode ver todos + IF is_owner AND user_role = 'owner' THEN + -- Verifica se é master + DECLARE + user_tenant_type TEXT; + BEGIN + SELECT tenant_type INTO user_tenant_type + FROM tenants + WHERE id = user_tenant_id; + + IF user_tenant_type = 'master' THEN + RETURN TRUE; + END IF; + END; + END IF; + + RETURN FALSE; +END; +$$ LANGUAGE plpgsql SECURITY DEFINER; + +-- ========================================== +-- TRIGGER FUNCTION: Validar tenant_id antes de inserir/atualizar +-- ========================================== + +CREATE OR REPLACE FUNCTION validate_tenant_id() +RETURNS TRIGGER AS $$ +DECLARE + context_tenant_id INTEGER; +BEGIN + -- Se não tem tenant_id, permite (tabelas globais) + IF NEW.tenant_id IS NULL THEN + RETURN NEW; + END IF; + + -- Valida se tenant está ativo + PERFORM validate_tenant_active(NEW.tenant_id); + + -- Opcional: validar contra contexto da aplicação + BEGIN + context_tenant_id := current_setting('app.current_tenant_id', true)::INTEGER; + IF context_tenant_id IS NOT NULL AND NEW.tenant_id != context_tenant_id THEN + RAISE EXCEPTION 'Tenant ID % não corresponde ao contexto atual %', + NEW.tenant_id, context_tenant_id; + END IF; + EXCEPTION WHEN OTHERS THEN + -- Se não conseguiu ler o contexto, continua (para migrations, seeds, etc) + NULL; + END; + + RETURN NEW; +END; +$$ LANGUAGE plpgsql SECURITY DEFINER; + +-- ========================================== +-- CRIAR TRIGGERS PARA VALIDAÇÃO AUTOMÁTICA +-- ========================================== + +-- workspace_pages +DROP TRIGGER IF EXISTS trigger_validate_workspace_pages_tenant ON "workspace_pages"; +CREATE TRIGGER trigger_validate_workspace_pages_tenant + BEFORE INSERT OR UPDATE ON "workspace_pages" + FOR EACH ROW + EXECUTE FUNCTION validate_tenant_id(); + +-- quick_notes +DROP TRIGGER IF EXISTS trigger_validate_quick_notes_tenant ON "quick_notes"; +CREATE TRIGGER trigger_validate_quick_notes_tenant + BEFORE INSERT OR UPDATE ON "quick_notes" + FOR EACH ROW + EXECUTE FUNCTION validate_tenant_id(); + +-- activity_feed +DROP TRIGGER IF EXISTS trigger_validate_activity_feed_tenant ON "activity_feed"; +CREATE TRIGGER trigger_validate_activity_feed_tenant + BEFORE INSERT OR UPDATE ON "activity_feed" + FOR EACH ROW + EXECUTE FUNCTION validate_tenant_id(); + +-- conversations +DROP TRIGGER IF EXISTS trigger_validate_conversations_tenant ON "conversations"; +CREATE TRIGGER trigger_validate_conversations_tenant + BEFORE INSERT OR UPDATE ON "conversations" + FOR EACH ROW + EXECUTE FUNCTION validate_tenant_id(); + +-- knowledge_base +DROP TRIGGER IF EXISTS trigger_validate_knowledge_base_tenant ON "knowledge_base"; +CREATE TRIGGER trigger_validate_knowledge_base_tenant + BEFORE INSERT OR UPDATE ON "knowledge_base" + FOR EACH ROW + EXECUTE FUNCTION validate_tenant_id(); + +-- chat_threads +DROP TRIGGER IF EXISTS trigger_validate_chat_threads_tenant ON "chat_threads"; +CREATE TRIGGER trigger_validate_chat_threads_tenant + BEFORE INSERT OR UPDATE ON "chat_threads" + FOR EACH ROW + EXECUTE FUNCTION validate_tenant_id(); + +-- manus_runs +DROP TRIGGER IF EXISTS trigger_validate_manus_runs_tenant ON "manus_runs"; +CREATE TRIGGER trigger_validate_manus_runs_tenant + BEFORE INSERT OR UPDATE ON "manus_runs" + FOR EACH ROW + EXECUTE FUNCTION validate_tenant_id(); + +-- tenant_empresas +DROP TRIGGER IF EXISTS trigger_validate_tenant_empresas_tenant ON "tenant_empresas"; +CREATE TRIGGER trigger_validate_tenant_empresas_tenant + BEFORE INSERT OR UPDATE ON "tenant_empresas" + FOR EACH ROW + EXECUTE FUNCTION validate_tenant_id(); + +-- pc_clients +DROP TRIGGER IF EXISTS trigger_validate_pc_clients_tenant ON "pc_clients"; +CREATE TRIGGER trigger_validate_pc_clients_tenant + BEFORE INSERT OR UPDATE ON "pc_clients" + FOR EACH ROW + EXECUTE FUNCTION validate_tenant_id(); + +-- pc_projects +DROP TRIGGER IF EXISTS trigger_validate_pc_projects_tenant ON "pc_projects"; +CREATE TRIGGER trigger_validate_pc_projects_tenant + BEFORE INSERT OR UPDATE ON "pc_projects" + FOR EACH ROW + EXECUTE FUNCTION validate_tenant_id(); + +-- pc_crm_leads +DROP TRIGGER IF EXISTS trigger_validate_pc_crm_leads_tenant ON "pc_crm_leads"; +CREATE TRIGGER trigger_validate_pc_crm_leads_tenant + BEFORE INSERT OR UPDATE ON "pc_crm_leads" + FOR EACH ROW + EXECUTE FUNCTION validate_tenant_id(); + +-- pc_crm_opportunities +DROP TRIGGER IF EXISTS trigger_validate_pc_crm_opportunities_tenant ON "pc_crm_opportunities"; +CREATE TRIGGER trigger_validate_pc_crm_opportunities_tenant + BEFORE INSERT OR UPDATE ON "pc_crm_opportunities" + FOR EACH ROW + EXECUTE FUNCTION validate_tenant_id(); + +-- pc_pdca_cycles +DROP TRIGGER IF EXISTS trigger_validate_pc_pdca_cycles_tenant ON "pc_pdca_cycles"; +CREATE TRIGGER trigger_validate_pc_pdca_cycles_tenant + BEFORE INSERT OR UPDATE ON "pc_pdca_cycles" + FOR EACH ROW + EXECUTE FUNCTION validate_tenant_id(); + +-- crm_partners +DROP TRIGGER IF EXISTS trigger_validate_crm_partners_tenant ON "crm_partners"; +CREATE TRIGGER trigger_validate_crm_partners_tenant + BEFORE INSERT OR UPDATE ON "crm_partners" + FOR EACH ROW + EXECUTE FUNCTION validate_tenant_id(); + +-- crm_contracts +DROP TRIGGER IF EXISTS trigger_validate_crm_contracts_tenant ON "crm_contracts"; +CREATE TRIGGER trigger_validate_crm_contracts_tenant + BEFORE INSERT OR UPDATE ON "crm_contracts" + FOR EACH ROW + EXECUTE FUNCTION validate_tenant_id(); + +-- crm_channels +DROP TRIGGER IF EXISTS trigger_validate_crm_channels_tenant ON "crm_channels"; +CREATE TRIGGER trigger_validate_crm_channels_tenant + BEFORE INSERT OR UPDATE ON "crm_channels" + FOR EACH ROW + EXECUTE FUNCTION validate_tenant_id(); + +-- crm_threads +DROP TRIGGER IF EXISTS trigger_validate_crm_threads_tenant ON "crm_threads"; +CREATE TRIGGER trigger_validate_crm_threads_tenant + BEFORE INSERT OR UPDATE ON "crm_threads" + FOR EACH ROW + EXECUTE FUNCTION validate_tenant_id(); + +-- crm_campaigns +DROP TRIGGER IF EXISTS trigger_validate_crm_campaigns_tenant ON "crm_campaigns"; +CREATE TRIGGER trigger_validate_crm_campaigns_tenant + BEFORE INSERT OR UPDATE ON "crm_campaigns" + FOR EACH ROW + EXECUTE FUNCTION validate_tenant_id(); + +-- crm_events +DROP TRIGGER IF EXISTS trigger_validate_crm_events_tenant ON "crm_events"; +CREATE TRIGGER trigger_validate_crm_events_tenant + BEFORE INSERT OR UPDATE ON "crm_events" + FOR EACH ROW + EXECUTE FUNCTION validate_tenant_id(); + +-- crm_products +DROP TRIGGER IF EXISTS trigger_validate_crm_products_tenant ON "crm_products"; +CREATE TRIGGER trigger_validate_crm_products_tenant + BEFORE INSERT OR UPDATE ON "crm_products" + FOR EACH ROW + EXECUTE FUNCTION validate_tenant_id(); + +-- crm_clients +DROP TRIGGER IF EXISTS trigger_validate_crm_clients_tenant ON "crm_clients"; +CREATE TRIGGER trigger_validate_crm_clients_tenant + BEFORE INSERT OR UPDATE ON "crm_clients" + FOR EACH ROW + EXECUTE FUNCTION validate_tenant_id(); + +-- crm_leads +DROP TRIGGER IF EXISTS trigger_validate_crm_leads_tenant ON "crm_leads"; +CREATE TRIGGER trigger_validate_crm_leads_tenant + BEFORE INSERT OR UPDATE ON "crm_leads" + FOR EACH ROW + EXECUTE FUNCTION validate_tenant_id(); + +-- crm_opportunities +DROP TRIGGER IF EXISTS trigger_validate_crm_opportunities_tenant ON "crm_opportunities"; +CREATE TRIGGER trigger_validate_crm_opportunities_tenant + BEFORE INSERT OR UPDATE ON "crm_opportunities" + FOR EACH ROW + EXECUTE FUNCTION validate_tenant_id(); + +-- crm_proposals +DROP TRIGGER IF EXISTS trigger_validate_crm_proposals_tenant ON "crm_proposals"; +CREATE TRIGGER trigger_validate_crm_proposals_tenant + BEFORE INSERT OR UPDATE ON "crm_proposals" + FOR EACH ROW + EXECUTE FUNCTION validate_tenant_id(); + +-- ========================================== +-- VIEW: Resumo de segurança por tenant +-- ========================================== + +CREATE OR REPLACE VIEW tenant_security_summary AS +SELECT + t.id AS tenant_id, + t.name AS tenant_name, + t.slug, + t.tenant_type, + t.status, + (SELECT COUNT(*) FROM tenant_users tu WHERE tu.tenant_id = t.id) AS user_count, + (SELECT COUNT(*) FROM workspace_pages wp WHERE wp.tenant_id = t.id) AS workspace_pages_count, + (SELECT COUNT(*) FROM pc_clients pc WHERE pc.tenant_id = t.id) AS clients_count, + (SELECT COUNT(*) FROM pc_projects pp WHERE pp.tenant_id = t.id) AS projects_count, + (SELECT COUNT(*) FROM crm_leads cl WHERE cl.tenant_id = t.id) AS leads_count, + (SELECT COUNT(*) FROM crm_opportunities co WHERE co.tenant_id = t.id) AS opportunities_count +FROM tenants t +ORDER BY t.id; + +-- ========================================== +-- FUNÇÃO: Setar contexto de tenant (para uso na aplicação) +-- ========================================== + +CREATE OR REPLACE FUNCTION set_tenant_context(p_tenant_id INTEGER, p_user_id VARCHAR, p_role TEXT DEFAULT 'member') +RETURNS VOID AS $$ +BEGIN + PERFORM set_config('app.current_tenant_id', p_tenant_id::TEXT, FALSE); + PERFORM set_config('app.current_user_id', p_user_id, FALSE); + PERFORM set_config('app.current_tenant_role', p_role, FALSE); +END; +$$ LANGUAGE plpgsql SECURITY DEFINER; + +-- ========================================== +-- FUNÇÃO: Limpar contexto de tenant +-- ========================================== + +CREATE OR REPLACE FUNCTION clear_tenant_context() +RETURNS VOID AS $$ +BEGIN + PERFORM set_config('app.current_tenant_id', '', FALSE); + PERFORM set_config('app.current_user_id', '', FALSE); + PERFORM set_config('app.current_tenant_role', '', FALSE); +END; +$$ LANGUAGE plpgsql SECURITY DEFINER; + +-- ========================================== +-- COMENTÁRIOS DE DOCUMENTAÇÃO +-- ========================================== + +COMMENT ON FUNCTION get_current_tenant_id() IS 'Retorna o ID do tenant atual do contexto da aplicação'; +COMMENT ON FUNCTION is_tenant_admin() IS 'Verifica se o usuário atual é admin/owner do tenant'; +COMMENT ON FUNCTION validate_tenant_active(INTEGER) IS 'Valida se um tenant existe e está ativo'; +COMMENT ON FUNCTION can_access_tenant(VARCHAR, INTEGER) IS 'Verifica se um usuário pode acessar um tenant específico'; +COMMENT ON FUNCTION set_tenant_context(INTEGER, VARCHAR, TEXT) IS 'Define o contexto de tenant para a sessão atual'; +COMMENT ON FUNCTION clear_tenant_context() IS 'Limpa o contexto de tenant da sessão atual'; +COMMENT ON VIEW tenant_security_summary IS 'Resumo de segurança e dados por tenant'; diff --git a/migrations/README_RLS.md b/migrations/README_RLS.md new file mode 100644 index 0000000..816aab2 --- /dev/null +++ b/migrations/README_RLS.md @@ -0,0 +1,118 @@ +# Migrações RLS - Row Level Security + +Esta pasta contém as migrações para habilitar Row Level Security (RLS) no PostgreSQL, +garantindo isolamento de dados entre tenants (multi-tenancy). + +## Arquivos + +| Arquivo | Descrição | +|---------|-----------| +| `001_enable_rls.sql` | Habilita RLS em todas as tabelas com `tenant_id` | +| `002_create_policies.sql` | Cria políticas CRUD por tenant | +| `003_tenant_context.sql` | Funções de contexto e triggers de validação | + +## Como Aplicar + +### Opção 1: Aplicar via psql (Produção) + +```bash +# Configurar variáveis de ambiente +export DATABASE_URL="postgresql://user:pass@host:5432/arcadia" + +# Aplicar migrações +psql $DATABASE_URL -f migrations/001_enable_rls.sql +psql $DATABASE_URL -f migrations/002_create_policies.sql +psql $DATABASE_URL -f migrations/003_tenant_context.sql +``` + +### Opção 2: Aplicar via script Node.js + +```bash +npm run db:apply-rls +``` + +(Nota: este script precisa ser criado no package.json) + +### Opção 3: Aplicar via Docker + +```bash +docker exec -i arcadia-db psql -U arcadia -d arcadia < migrations/001_enable_rls.sql +docker exec -i arcadia-db psql -U arcadia -d arcadia < migrations/002_create_policies.sql +docker exec -i arcadia-db psql -U arcadia -d arcadia < migrations/003_tenant_context.sql +``` + +## Tabelas Protegidas + +### Productivity & Workspace +- `workspace_pages`, `quick_notes`, `activity_feed` +- `conversations`, `knowledge_base`, `chat_threads`, `manus_runs` + +### Low-Code / Dev Center +- `arc_doctypes`, `arc_layouts` + +### Multi-Tenancy Core +- `tenant_empresas`, `tenant_users`, `partner_clients`, `partner_commissions` + +### Process Compass +- `pc_clients`, `pc_projects`, `pc_crm_stages`, `pc_crm_leads` +- `pc_crm_opportunities`, `pc_crm_activities`, `pc_pdca_cycles`, `pc_requirements` + +### CRM Expandido +- `crm_partners`, `crm_contracts`, `crm_channels`, `crm_threads` +- `crm_quick_messages`, `crm_campaigns`, `crm_events`, `crm_products` +- `crm_clients`, `crm_pipeline_stages`, `crm_leads`, `crm_opportunities`, `crm_proposals` + +## Funções Disponíveis + +```sql +-- Obter tenant atual do contexto +SELECT get_current_tenant_id(); + +-- Verificar se é admin +SELECT is_tenant_admin(); + +-- Setar contexto (deve ser chamado pela aplicação) +SELECT set_tenant_context(1, 'user-uuid', 'member'); + +-- Verificar acesso +SELECT can_access_tenant('user-uuid', 1); + +-- View de resumo +SELECT * FROM tenant_security_summary; +``` + +## Integração com a Aplicação + +O middleware da aplicação deve chamar `set_tenant_context` após autenticação: + +```typescript +// server/middleware/tenant-context.ts +import { db } from "../db"; + +export async function setTenantContext(req, res, next) { + if (req.user?.tenantId) { + await db.execute( + sql`SELECT set_tenant_context(${req.user.tenantId}, ${req.user.id}, ${req.user.tenantRole})` + ); + } + next(); +} +``` + +## Rollback + +Para desabilitar RLS (emergência apenas): + +```sql +-- Exemplo para uma tabela +ALTER TABLE workspace_pages DISABLE ROW LEVEL SECURITY; +DROP POLICY workspace_pages_tenant_select ON workspace_pages; +-- ... etc +``` + +## Notas + +- As políticas usam `get_current_tenant_id()` que lê do contexto da sessão +- Triggers validam se `tenant_id` corresponde ao contexto antes de INSERT/UPDATE +- Tabelas sem `tenant_id` (users, tenants, profiles) NÃO têm RLS (acesso global) +- A view `tenant_security_summary` mostra estatísticas por tenant diff --git a/package-lock.json b/package-lock.json index b122590..d35b215 100644 --- a/package-lock.json +++ b/package-lock.json @@ -69,7 +69,6 @@ "connect-pg-simple": "^10.0.0", "date-fns": "^3.6.0", "docx": "^9.5.1", - "dotenv": "^17.3.1", "drizzle-orm": "^0.39.3", "drizzle-zod": "^0.7.1", "embla-carousel-react": "^8.6.0", @@ -9118,18 +9117,6 @@ "url": "https://github.com/fb55/domutils?sponsor=1" } }, - "node_modules/dotenv": { - "version": "17.3.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz", - "integrity": "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, "node_modules/drizzle-kit": { "version": "0.31.4", "resolved": "https://registry.npmjs.org/drizzle-kit/-/drizzle-kit-0.31.4.tgz", diff --git a/package.json b/package.json index 494209a..dc1d0a4 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "start": "NODE_ENV=production node dist/index.cjs", "check": "tsc", "db:push": "drizzle-kit push", + "db:apply-rls": "tsx scripts/apply-rls-migrations.ts", "test": "vitest run", "test:watch": "vitest", "test:openclaw": "vitest run tests/openclaw" diff --git a/scripts/apply-rls-migrations.ts b/scripts/apply-rls-migrations.ts new file mode 100644 index 0000000..2744c69 --- /dev/null +++ b/scripts/apply-rls-migrations.ts @@ -0,0 +1,168 @@ +#!/usr/bin/env tsx +/** + * Script para aplicar migrações RLS (Row Level Security) + * + * Uso: + * npm run db:apply-rls + * ou + * npx tsx scripts/apply-rls-migrations.ts + */ + +import { readFileSync } from 'fs'; +import { join, dirname } from 'path'; +import { fileURLToPath } from 'url'; +import pg from 'pg'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +const { Pool } = pg; + +// Migrações na ordem correta +const MIGRATIONS = [ + '001_enable_rls.sql', + '002_create_policies.sql', + '003_tenant_context.sql', +]; + +async function applyMigration(pool: pg.Pool, filename: string): Promise { + const filepath = join(__dirname, '..', 'migrations', filename); + const sql = readFileSync(filepath, 'utf-8'); + + console.log(`\n📄 Aplicando: ${filename}`); + console.log('='.repeat(50)); + + const client = await pool.connect(); + + try { + await client.query('BEGIN'); + + // Divide o SQL em statements individuais (separados por ;) + const statements = sql + .split(';') + .map(s => s.trim()) + .filter(s => s.length > 0 && !s.startsWith('--')); + + for (let i = 0; i < statements.length; i++) { + const stmt = statements[i]; + try { + await client.query(stmt); + process.stdout.write('.'); + } catch (err) { + console.error(`\n❌ Erro no statement ${i + 1}:`); + console.error(err.message); + // Alguns erros são esperados (ex: DROP IF NOT EXISTS) + if (!err.message.includes('does not exist')) { + throw err; + } + } + } + + await client.query('COMMIT'); + console.log(`\n✅ ${filename} aplicado com sucesso!`); + + } catch (error) { + await client.query('ROLLBACK'); + throw error; + } finally { + client.release(); + } +} + +async function verifyRLS(pool: pg.Pool): Promise { + console.log('\n🔍 Verificando tabelas com RLS habilitado...\n'); + + const result = await pool.query(` + SELECT + schemaname, + tablename, + rowsecurity as rls_enabled + FROM pg_tables + JOIN pg_class ON pg_class.relname = pg_tables.tablename + WHERE schemaname = 'public' + AND rowsecurity = true + ORDER BY tablename; + `); + + console.log(`✓ ${result.rows.length} tabelas com RLS habilitado:\n`); + result.rows.forEach((row, i) => { + console.log(` ${i + 1}. ${row.tablename}`); + }); +} + +async function verifyPolicies(pool: pg.Pool): Promise { + console.log('\n📋 Verificando políticas criadas...\n'); + + const result = await pool.query(` + SELECT + tablename, + policyname, + permissive, + roles, + cmd as operation, + qual as condition + FROM pg_policies + WHERE schemaname = 'public' + ORDER BY tablename, policyname; + `); + + console.log(`✓ ${result.rows.length} políticas criadas\n`); + + // Agrupa por tabela + const byTable: Record = {}; + result.rows.forEach(row => { + if (!byTable[row.tablename]) { + byTable[row.tablename] = []; + } + byTable[row.tablename].push(`${row.operation}`); + }); + + Object.entries(byTable).forEach(([table, ops]) => { + console.log(` • ${table}: ${ops.join(', ')}`); + }); +} + +async function main() { + const databaseUrl = process.env.DATABASE_URL; + + if (!databaseUrl) { + console.error('❌ DATABASE_URL não configurado!'); + console.error('Exemplo: export DATABASE_URL="postgresql://user:pass@localhost:5432/arcadia"'); + process.exit(1); + } + + console.log('🚀 Iniciando aplicação das migrações RLS...'); + console.log(`🔗 Database: ${databaseUrl.replace(/:[^:@]*@/, ':***@')}`); + + const pool = new Pool({ + connectionString: databaseUrl, + }); + + try { + // Testa conexão + await pool.query('SELECT 1'); + console.log('✅ Conexão com banco estabelecida\n'); + + // Aplica cada migração + for (const migration of MIGRATIONS) { + await applyMigration(pool, migration); + } + + // Verificações finais + await verifyRLS(pool); + await verifyPolicies(pool); + + console.log('\n' + '='.repeat(50)); + console.log('✅ Todas as migrações aplicadas com sucesso!'); + console.log('='.repeat(50) + '\n'); + + } catch (error) { + console.error('\n❌ Erro ao aplicar migrações:'); + console.error(error.message); + process.exit(1); + } finally { + await pool.end(); + } +} + +main();