Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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$;
159 changes: 87 additions & 72 deletions supabase/migrations/20260515030000_onda19_numeric_precision.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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 (
Comment on lines 116 to +120
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$;
54 changes: 27 additions & 27 deletions supabase/migrations/20260524210100_harden_anon_graphql_exposure.sql
Original file line number Diff line number Diff line change
Expand Up @@ -18,30 +18,30 @@
--
-- Rollback: GRANT SELECT ON public.<tabela> 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$;
Loading