diff --git a/supabase/migrations/20260514220543_onda13_rls_audit_logs_admin_only.sql b/supabase/migrations/20260514220543_onda13_rls_audit_logs_admin_only.sql index d56270f1a..b4ae60c0d 100644 --- a/supabase/migrations/20260514220543_onda13_rls_audit_logs_admin_only.sql +++ b/supabase/migrations/20260514220543_onda13_rls_audit_logs_admin_only.sql @@ -7,57 +7,49 @@ -- tabela_preco_gravacao_oficial — vaza historico de precos e quem alterou. -- - `seo_audit_log`: historico de auditorias SEO — info interna de melhorias. -- --- INVESTIGACAO: --- 1. Frontend (src/) NAO consulta nenhuma das 2 (code_search confirmou — zero matches). --- 2. Edge functions usam service_role que bypassa RLS, entao a mudanca eh transparente. --- 3. Sao auditadas apenas via Supabase Dashboard pelo admin. --- 4. Convencao: is_supervisor_or_above (alinhada com Ondas 5-10). --- -- MUDANCA: -- SELECT em ambas restrito a is_supervisor_or_above (dev, admin, supervisor, manager). -- INSERT continua via triggers SECURITY DEFINER que bypassam RLS. -- --- ESTADO DA B-3 APOS ESTA MIGRATION: --- Auditoria pre-prod listou 7 tabelas overly-permissive. Estado final: --- - audit_log: ja correta (supervisor_or_above) — fechada antes --- - analytics_events: ja correta (self ou coord) — fechada antes --- - product_views: ja correta (admin ou seller_id) — fechada antes --- - search_queries: ja correta (self ou coord) — fechada antes --- - quote_templates: ja correta (por created_by) — fechada antes --- - sync_jobs: tabela nao existe mais — N/A --- - notification_templates: fechada em Onda 8 (PR #199) --- - audit_log_gravacao (esta migration): supervisor_or_above --- - seo_audit_log (esta migration): supervisor_or_above --- B-3 ENCERRADA. --- -- APLICADA EM PROD via MCP apply_migration (ADR 0006). +-- +-- REPLAY: audit_log_gravacao e seo_audit_log foram criadas fora de migration +-- (out-of-band). Num replay limpo elas nao existem, entao cada bloco e guardado +-- por to_regclass(...) IS NOT NULL — no-op quando a tabela esta ausente, sem +-- alterar o comportamento em producao (onde as tabelas existem). -- ─── audit_log_gravacao ─── -DROP POLICY IF EXISTS audit_log_gravacao_read_authenticated ON public.audit_log_gravacao; -DROP POLICY IF EXISTS audit_log_gravacao_select_supervisor_or_above ON public.audit_log_gravacao; +DO $g$ +BEGIN + IF to_regclass('public.audit_log_gravacao') IS NOT NULL THEN + DROP POLICY IF EXISTS audit_log_gravacao_read_authenticated ON public.audit_log_gravacao; + DROP POLICY IF EXISTS audit_log_gravacao_select_supervisor_or_above ON public.audit_log_gravacao; -CREATE POLICY audit_log_gravacao_select_supervisor_or_above -ON public.audit_log_gravacao -FOR SELECT -TO authenticated -USING ( - is_supervisor_or_above((SELECT auth.uid())) -); + CREATE POLICY audit_log_gravacao_select_supervisor_or_above + ON public.audit_log_gravacao + FOR SELECT + TO authenticated + USING ( is_supervisor_or_above((SELECT auth.uid())) ); -COMMENT ON POLICY audit_log_gravacao_select_supervisor_or_above ON public.audit_log_gravacao IS -'Onda 13 (B-3): SELECT restrito a supervisor_or_above. Audit log de gravacao expoe usuario/valor_antes/depois e nao deve vazar para vendedores. Service_role (triggers SECDEF) continua bypassando.'; + COMMENT ON POLICY audit_log_gravacao_select_supervisor_or_above ON public.audit_log_gravacao IS + 'Onda 13 (B-3): SELECT restrito a supervisor_or_above. Audit log de gravacao expoe usuario/valor_antes/depois e nao deve vazar para vendedores. Service_role (triggers SECDEF) continua bypassando.'; + END IF; +END $g$; -- ─── seo_audit_log ─── -DROP POLICY IF EXISTS auth_read_seo_audit_log ON public.seo_audit_log; -DROP POLICY IF EXISTS seo_audit_log_select_supervisor_or_above ON public.seo_audit_log; +DO $g$ +BEGIN + IF to_regclass('public.seo_audit_log') IS NOT NULL THEN + DROP POLICY IF EXISTS auth_read_seo_audit_log ON public.seo_audit_log; + DROP POLICY IF EXISTS seo_audit_log_select_supervisor_or_above ON public.seo_audit_log; -CREATE POLICY seo_audit_log_select_supervisor_or_above -ON public.seo_audit_log -FOR SELECT -TO authenticated -USING ( - is_supervisor_or_above((SELECT auth.uid())) -); + CREATE POLICY seo_audit_log_select_supervisor_or_above + ON public.seo_audit_log + FOR SELECT + TO authenticated + USING ( is_supervisor_or_above((SELECT auth.uid())) ); -COMMENT ON POLICY seo_audit_log_select_supervisor_or_above ON public.seo_audit_log IS -'Onda 13 (B-3): SELECT restrito a supervisor_or_above. Historico de auditoria SEO eh info operacional interna, nao deve ser legivel por vendedores.'; + COMMENT ON POLICY seo_audit_log_select_supervisor_or_above ON public.seo_audit_log IS + 'Onda 13 (B-3): SELECT restrito a supervisor_or_above. Historico de auditoria SEO eh info operacional interna, nao deve ser legivel por vendedores.'; + END IF; +END $g$; diff --git a/supabase/migrations/20260515030000_onda19_numeric_precision.sql b/supabase/migrations/20260515030000_onda19_numeric_precision.sql index 378abd0a4..ac5c45838 100644 --- a/supabase/migrations/20260515030000_onda19_numeric_precision.sql +++ b/supabase/migrations/20260515030000_onda19_numeric_precision.sql @@ -25,17 +25,19 @@ -- - TRIGGER trg_quotes_calc_real_values (UPDATE OF negotiation_markup_percent) -- - TRIGGER trg_kit_print_area_normalizar_eixos (UPDATE OF max_width, max_height) -- --- NÃO MEXIDO (decisão pré-flight): --- - orders.total (GENERATED ALWAYS AS total_amount — herda precisão) --- - app_vitals.metric_value (telemetria livre) --- - _asia_api_staging.peso, color_analysis_staging.match_distance (staging) --- - tabela_preco_gravacao_oficial.{preco_max,preco_min}_unitario (placeholder) +-- REPLAY: tabela_preco_gravacao_oficial(_faixa), supplier_technique_mappings, +-- variant_supplier_sources e kit_component_print_areas foram criadas fora de +-- migration (out-of-band). Num replay limpo elas não existem; os statements que +-- dependem delas são guardados por EXCEPTION undefined_table (no-op quando +-- ausentes). Em produção (onde existem) o comportamento é idêntico ao original. -- ================================================================= -- 0. DROP dependências bloqueadoras DROP VIEW IF EXISTS public.v_audit_paradoxos_gravacao; DROP TRIGGER IF EXISTS trg_quotes_calc_real_values ON public.quotes; -DROP TRIGGER IF EXISTS trg_kit_print_area_normalizar_eixos ON public.kit_component_print_areas; +DO $g$ BEGIN + DROP TRIGGER IF EXISTS trg_kit_print_area_normalizar_eixos ON public.kit_component_print_areas; +EXCEPTION WHEN undefined_table THEN NULL; END $g$; -- 1. PERCENTUAIS (8 colunas) ALTER TABLE public.quotes @@ -47,14 +49,20 @@ ALTER TABLE public.seller_discount_limits ALTER COLUMN max_discount_percent TYPE numeric(5,2), ALTER COLUMN approval_required_above TYPE numeric(5,2); -ALTER TABLE public.tabela_preco_gravacao_oficial - ALTER COLUMN markup_percent TYPE numeric(5,2); +DO $g$ BEGIN + ALTER TABLE public.tabela_preco_gravacao_oficial + ALTER COLUMN markup_percent TYPE numeric(5,2); +EXCEPTION WHEN undefined_table THEN NULL; END $g$; -ALTER TABLE public.supplier_technique_mappings - ALTER COLUMN confidence TYPE numeric(3,2); +DO $g$ BEGIN + ALTER TABLE public.supplier_technique_mappings + ALTER COLUMN confidence TYPE numeric(3,2); +EXCEPTION WHEN undefined_table THEN NULL; END $g$; -ALTER TABLE public.variant_supplier_sources - ALTER COLUMN supplier_ipi_rate TYPE numeric(5,2); +DO $g$ BEGIN + ALTER TABLE public.variant_supplier_sources + ALTER COLUMN supplier_ipi_rate TYPE numeric(5,2); +EXCEPTION WHEN undefined_table THEN NULL; END $g$; -- 2. DINHEIRO (8 colunas) ALTER TABLE public.quotes @@ -84,9 +92,11 @@ ALTER TABLE public.quote_item_personalizations ALTER COLUMN height_cm TYPE numeric(8,2), ALTER COLUMN width_cm TYPE numeric(8,2); -ALTER TABLE public.kit_component_print_areas - ALTER COLUMN max_height TYPE numeric(8,2), - ALTER COLUMN max_width TYPE numeric(8,2); +DO $g$ BEGIN + ALTER TABLE public.kit_component_print_areas + ALTER COLUMN max_height TYPE numeric(8,2), + ALTER COLUMN max_width TYPE numeric(8,2); +EXCEPTION WHEN undefined_table THEN NULL; END $g$; -- 4. RECREATE triggers (definições idênticas às originais) CREATE TRIGGER trg_quotes_calc_real_values @@ -95,62 +105,67 @@ CREATE TRIGGER trg_quotes_calc_real_values FOR EACH ROW EXECUTE FUNCTION public.fn_quotes_calc_real_values(); -CREATE TRIGGER trg_kit_print_area_normalizar_eixos - BEFORE INSERT OR UPDATE OF max_width, max_height - ON public.kit_component_print_areas - FOR EACH ROW - EXECUTE FUNCTION public.fn_kit_print_area_normalizar_eixos(); +DO $g$ BEGIN + CREATE TRIGGER trg_kit_print_area_normalizar_eixos + BEFORE INSERT OR UPDATE OF max_width, max_height + ON public.kit_component_print_areas + FOR EACH ROW + EXECUTE FUNCTION public.fn_kit_print_area_normalizar_eixos(); +EXCEPTION WHEN undefined_table THEN NULL; END $g$; -- 5. RECREATE view (definição idêntica à original — só rebuild para usar novo tipo) -CREATE OR REPLACE VIEW public.v_audit_paradoxos_gravacao AS -WITH faixas_ord AS ( - SELECT t.id, - t.codigo_tabela, - t.nome, - t.grupo_tecnica, - t.custo_setup, - t.markup_percent, - f.quantidade_minima, - f.quantidade_maxima, - f.preco_unitario, - lag(f.preco_unitario) OVER ( - PARTITION BY t.id, - (COALESCE(f.largura_min, 0::numeric)), - (COALESCE(f.largura_max, 0::numeric)), - (COALESCE(f.altura_min, 0::numeric)), - (COALESCE(f.altura_max, 0::numeric)) - ORDER BY f.quantidade_minima - ) AS preco_anterior, - lag(f.quantidade_maxima) OVER ( - PARTITION BY t.id, - (COALESCE(f.largura_min, 0::numeric)), - (COALESCE(f.largura_max, 0::numeric)), - (COALESCE(f.altura_min, 0::numeric)), - (COALESCE(f.altura_max, 0::numeric)) - ORDER BY f.quantidade_minima - ) AS qty_max_anterior - FROM public.tabela_preco_gravacao_oficial t - JOIN public.tabela_preco_gravacao_oficial_faixa f - ON f.tabela_preco_gravacao_id = t.id - WHERE t.ativo = true -) -SELECT - codigo_tabela, - nome, - grupo_tecnica, - qty_max_anterior AS qty_pico_ant, - preco_anterior, - round(GREATEST(qty_max_anterior::numeric * preco_anterior, custo_setup) * (1::numeric + markup_percent / 100::numeric), 2) AS venda_no_pico, - quantidade_minima AS qty_inicio, - preco_unitario AS preco_atual, - round(GREATEST(quantidade_minima::numeric * preco_unitario, custo_setup) * (1::numeric + markup_percent / 100::numeric), 2) AS venda_inicio, - round(GREATEST(quantidade_minima::numeric * preco_unitario, custo_setup) - GREATEST(qty_max_anterior::numeric * preco_anterior, custo_setup), 2) AS economia_cliente_se_subir_faixa, - CASE - WHEN preco_anterior IS NULL THEN 'primeira_faixa'::text - WHEN qty_max_anterior IS NULL THEN 'sem_pico_anterior'::text - WHEN GREATEST(quantidade_minima::numeric * preco_unitario, custo_setup) < GREATEST(qty_max_anterior::numeric * preco_anterior, custo_setup) THEN 'PARADOXO_NATURAL'::text - ELSE 'OK'::text - END AS status_natural -FROM faixas_ord -WHERE preco_anterior IS NOT NULL AND qty_max_anterior IS NOT NULL -ORDER BY codigo_tabela, quantidade_minima; +-- Guardada: referencia tabela_preco_gravacao_oficial(_faixa) (out-of-band). +DO $g$ BEGIN + CREATE OR REPLACE VIEW public.v_audit_paradoxos_gravacao AS + WITH faixas_ord AS ( + SELECT t.id, + t.codigo_tabela, + t.nome, + t.grupo_tecnica, + t.custo_setup, + t.markup_percent, + f.quantidade_minima, + f.quantidade_maxima, + f.preco_unitario, + lag(f.preco_unitario) OVER ( + PARTITION BY t.id, + (COALESCE(f.largura_min, 0::numeric)), + (COALESCE(f.largura_max, 0::numeric)), + (COALESCE(f.altura_min, 0::numeric)), + (COALESCE(f.altura_max, 0::numeric)) + ORDER BY f.quantidade_minima + ) AS preco_anterior, + lag(f.quantidade_maxima) OVER ( + PARTITION BY t.id, + (COALESCE(f.largura_min, 0::numeric)), + (COALESCE(f.largura_max, 0::numeric)), + (COALESCE(f.altura_min, 0::numeric)), + (COALESCE(f.altura_max, 0::numeric)) + ORDER BY f.quantidade_minima + ) AS qty_max_anterior + FROM public.tabela_preco_gravacao_oficial t + JOIN public.tabela_preco_gravacao_oficial_faixa f + ON f.tabela_preco_gravacao_id = t.id + WHERE t.ativo = true + ) + SELECT + codigo_tabela, + nome, + grupo_tecnica, + qty_max_anterior AS qty_pico_ant, + preco_anterior, + round(GREATEST(qty_max_anterior::numeric * preco_anterior, custo_setup) * (1::numeric + markup_percent / 100::numeric), 2) AS venda_no_pico, + quantidade_minima AS qty_inicio, + preco_unitario AS preco_atual, + round(GREATEST(quantidade_minima::numeric * preco_unitario, custo_setup) * (1::numeric + markup_percent / 100::numeric), 2) AS venda_inicio, + round(GREATEST(quantidade_minima::numeric * preco_unitario, custo_setup) - GREATEST(qty_max_anterior::numeric * preco_anterior, custo_setup), 2) AS economia_cliente_se_subir_faixa, + CASE + WHEN preco_anterior IS NULL THEN 'primeira_faixa'::text + WHEN qty_max_anterior IS NULL THEN 'sem_pico_anterior'::text + WHEN GREATEST(quantidade_minima::numeric * preco_unitario, custo_setup) < GREATEST(qty_max_anterior::numeric * preco_anterior, custo_setup) THEN 'PARADOXO_NATURAL'::text + ELSE 'OK'::text + END AS status_natural + FROM faixas_ord + WHERE preco_anterior IS NOT NULL AND qty_max_anterior IS NOT NULL + ORDER BY codigo_tabela, quantidade_minima; +EXCEPTION WHEN undefined_table THEN NULL; END $g$; diff --git a/supabase/migrations/20260524210100_harden_anon_graphql_exposure.sql b/supabase/migrations/20260524210100_harden_anon_graphql_exposure.sql index 214048bed..ba7785428 100644 --- a/supabase/migrations/20260524210100_harden_anon_graphql_exposure.sql +++ b/supabase/migrations/20260524210100_harden_anon_graphql_exposure.sql @@ -18,30 +18,30 @@ -- -- Rollback: GRANT SELECT ON public. TO anon; -REVOKE SELECT ON public._unif_pending_log FROM anon; -REVOKE SELECT ON public.ai_usage_logs FROM anon; -REVOKE SELECT ON public.api_usage FROM anon; -REVOKE SELECT ON public.audit_log_gravacao FROM anon; -REVOKE SELECT ON public.edge_rate_limits FROM anon; -REVOKE SELECT ON public.enrichment_log FROM anon; -REVOKE SELECT ON public.external_connections_sync_log FROM anon; -REVOKE SELECT ON public.file_scan_logs FROM anon; -REVOKE SELECT ON public.frontend_telemetry FROM anon; -REVOKE SELECT ON public.image_import_log FROM anon; -REVOKE SELECT ON public.image_validation_log FROM anon; -REVOKE SELECT ON public.inbound_webhook_endpoints FROM anon; -REVOKE SELECT ON public.inbound_webhook_events FROM anon; -REVOKE SELECT ON public.media_sync_log FROM anon; -REVOKE SELECT ON public.outbound_webhooks FROM anon; -REVOKE SELECT ON public.ownership_audit_reports FROM anon; -REVOKE SELECT ON public.ownership_repair_logs FROM anon; -REVOKE SELECT ON public.product_search_logs FROM anon; -REVOKE SELECT ON public.product_sync_logs FROM anon; -REVOKE SELECT ON public.query_telemetry FROM anon; -REVOKE SELECT ON public.request_rate_limits FROM anon; -REVOKE SELECT ON public.rls_denial_log FROM anon; -REVOKE SELECT ON public.schema_drift_log FROM anon; -REVOKE SELECT ON public.user_known_devices FROM anon; -REVOKE SELECT ON public.video_validation_log FROM anon; -REVOKE SELECT ON public.voice_command_logs FROM anon; -REVOKE SELECT ON public.webhook_deliveries FROM anon; +DO $g$ BEGIN REVOKE SELECT ON public._unif_pending_log FROM anon; EXCEPTION WHEN undefined_table THEN NULL; END $g$; +DO $g$ BEGIN REVOKE SELECT ON public.ai_usage_logs FROM anon; EXCEPTION WHEN undefined_table THEN NULL; END $g$; +DO $g$ BEGIN REVOKE SELECT ON public.api_usage FROM anon; EXCEPTION WHEN undefined_table THEN NULL; END $g$; +DO $g$ BEGIN REVOKE SELECT ON public.audit_log_gravacao FROM anon; EXCEPTION WHEN undefined_table THEN NULL; END $g$; +DO $g$ BEGIN REVOKE SELECT ON public.edge_rate_limits FROM anon; EXCEPTION WHEN undefined_table THEN NULL; END $g$; +DO $g$ BEGIN REVOKE SELECT ON public.enrichment_log FROM anon; EXCEPTION WHEN undefined_table THEN NULL; END $g$; +DO $g$ BEGIN REVOKE SELECT ON public.external_connections_sync_log FROM anon; EXCEPTION WHEN undefined_table THEN NULL; END $g$; +DO $g$ BEGIN REVOKE SELECT ON public.file_scan_logs FROM anon; EXCEPTION WHEN undefined_table THEN NULL; END $g$; +DO $g$ BEGIN REVOKE SELECT ON public.frontend_telemetry FROM anon; EXCEPTION WHEN undefined_table THEN NULL; END $g$; +DO $g$ BEGIN REVOKE SELECT ON public.image_import_log FROM anon; EXCEPTION WHEN undefined_table THEN NULL; END $g$; +DO $g$ BEGIN REVOKE SELECT ON public.image_validation_log FROM anon; EXCEPTION WHEN undefined_table THEN NULL; END $g$; +DO $g$ BEGIN REVOKE SELECT ON public.inbound_webhook_endpoints FROM anon; EXCEPTION WHEN undefined_table THEN NULL; END $g$; +DO $g$ BEGIN REVOKE SELECT ON public.inbound_webhook_events FROM anon; EXCEPTION WHEN undefined_table THEN NULL; END $g$; +DO $g$ BEGIN REVOKE SELECT ON public.media_sync_log FROM anon; EXCEPTION WHEN undefined_table THEN NULL; END $g$; +DO $g$ BEGIN REVOKE SELECT ON public.outbound_webhooks FROM anon; EXCEPTION WHEN undefined_table THEN NULL; END $g$; +DO $g$ BEGIN REVOKE SELECT ON public.ownership_audit_reports FROM anon; EXCEPTION WHEN undefined_table THEN NULL; END $g$; +DO $g$ BEGIN REVOKE SELECT ON public.ownership_repair_logs FROM anon; EXCEPTION WHEN undefined_table THEN NULL; END $g$; +DO $g$ BEGIN REVOKE SELECT ON public.product_search_logs FROM anon; EXCEPTION WHEN undefined_table THEN NULL; END $g$; +DO $g$ BEGIN REVOKE SELECT ON public.product_sync_logs FROM anon; EXCEPTION WHEN undefined_table THEN NULL; END $g$; +DO $g$ BEGIN REVOKE SELECT ON public.query_telemetry FROM anon; EXCEPTION WHEN undefined_table THEN NULL; END $g$; +DO $g$ BEGIN REVOKE SELECT ON public.request_rate_limits FROM anon; EXCEPTION WHEN undefined_table THEN NULL; END $g$; +DO $g$ BEGIN REVOKE SELECT ON public.rls_denial_log FROM anon; EXCEPTION WHEN undefined_table THEN NULL; END $g$; +DO $g$ BEGIN REVOKE SELECT ON public.schema_drift_log FROM anon; EXCEPTION WHEN undefined_table THEN NULL; END $g$; +DO $g$ BEGIN REVOKE SELECT ON public.user_known_devices FROM anon; EXCEPTION WHEN undefined_table THEN NULL; END $g$; +DO $g$ BEGIN REVOKE SELECT ON public.video_validation_log FROM anon; EXCEPTION WHEN undefined_table THEN NULL; END $g$; +DO $g$ BEGIN REVOKE SELECT ON public.voice_command_logs FROM anon; EXCEPTION WHEN undefined_table THEN NULL; END $g$; +DO $g$ BEGIN REVOKE SELECT ON public.webhook_deliveries FROM anon; EXCEPTION WHEN undefined_table THEN NULL; END $g$;