Skip to content

feat(security): hardening de auth em webhook-dispatcher e connections-auto-test (Onda 1)#189

Merged
adm01-debug merged 5 commits into
mainfrom
cleanup/edge-functions-auth-hardening
May 14, 2026
Merged

feat(security): hardening de auth em webhook-dispatcher e connections-auto-test (Onda 1)#189
adm01-debug merged 5 commits into
mainfrom
cleanup/edge-functions-auth-hardening

Conversation

@adm01-debug
Copy link
Copy Markdown
Owner

@adm01-debug adm01-debug commented May 14, 2026

📋 Descrição

Hardening de autenticação em duas edge functions que estavam expostas anonimamente. Resolve N-1, N-2 (auditoria 13/05) e C2 como bônus.

🎯 Tipo de mudança

  • 🔒 security — segurança

🔐 Modelo de autorização

webhook-dispatcher — Modo A ou Modo B

  • Modo A: x-dispatcher-secret (triggers DB, RPCs, cron)
  • Modo B: Bearer <user JWT> + role ≥ supervisor (frontend admin)
  • test_mode e replay_delivery_id exigem Modo B (Modo A → 403)

connections-auto-test — Modo C

  • x-cron-secret (cron a cada 15min)

Retrocompat: se env não setada, aceita anônimo com warning log (rollback seguro).

🗄️ Aplicado no banco via MCP apply_migration (NÃO db push, ADR 0006)

  • vault.secrets: 2 secrets de 256 bits
  • public.get_edge_function_secret SECURITY DEFINER + whitelist + GRANT só service_role/postgres
  • Trigger dispatch_quote_webhook_event lê vault e envia x-dispatcher-secret
  • RPC retry_failed_webhook_deliveries lê vault e envia x-dispatcher-secret
  • Cron connections-auto-test re-agendado com x-cron-secret

Snapshots SQL em supabase/migrations/20260514112056* e ...57*.

✅ Validações

  • deno test _shared/dispatcher-auth.test.ts: 12/12 passed (28ms)
  • deno check em 78 edge functions: all clean
  • Helper SQL whitelist test: rejeita nomes não autorizados
  • node scripts/check-no-db-push.mjs: passa
  • SHA256 dos secrets do painel = SHA256 do vault ✅

🚀 Deploy

  1. Secrets já configurados no painel Edge Functions (sincronizados com vault — SHA256 confere)
  2. Triggers/cron/RPC já enviam o header (aplicado via MCP antes do merge — edge atual ignora headers desconhecidos)
  3. Após merge → workflow deploy-edge-functions.yml deploya automaticamente → validação 401/403 fica ativa

📋 Checklist pós-deploy

Detalhes em docs/hardening/ONDA-1-EDGE-AUTH.md:

  • Cron rodando a cada 15min com 200 OK
  • Criar quote via UI → webhook_deliveries ganha linha success=true
  • Frontend admin Playground → funciona (Modo B)
  • curl anônimo → 401
  • curl com secret errado → 401
  • curl test_mode com x-dispatcher-secret → 403

🔙 Rollback

Re-deploy do commit anterior. Triggers/cron continuam enviando o header (ignorado pela função antiga). Sem perda de funcionalidade.

📌 Notas

  • 5 commits no branch — use squash merge para histórico limpo
  • .tmp-write-probe.md criado durante diagnóstico de permissões — deletar manualmente após merge
  • Próxima onda do plano: Onda 3 (C2 já foi fechado neste PR como bônus)

Summary by CodeRabbit

Notas da Versão

  • Melhorias de Segurança

    • Autenticação obrigatória para webhook dispatcher usando secret compartilhado ou JWT com permissão de supervisor
    • Testes automáticos de conexão agora requerem autenticação via cron secret
    • Gerenciamento centralizado de secrets de Edge Functions através do Vault
  • Documentação

    • Novo guia de hardening com estratégias de autenticação, procedimentos operacionais e instruções de rollback

Review Change Stack

…-auto-test (Onda 1)

Resolve 3 bloqueadores das auditorias 2026-05-10 e 2026-05-13:
- N-1 (auditoria 13/05 §3.1): webhook-dispatcher sem auth real
- N-2 (auditoria 13/05 §3.2): connections-auto-test sem auth real
- C2 (auditoria 13/05): check-no-db-push allowlist atualizada

ALTERAÇÕES

1. Novo _shared/dispatcher-auth.ts (294L):
   - constantTimeEqual() para comparação segura
   - authorizeDispatcher(): Modo A (x-dispatcher-secret) + Modo B (Bearer JWT user)
   - authorizeCron(): Modo C (x-cron-secret)
   - Retrocompat: env nao setado → aceita anonimo com warning
   - Logs JSON single-line, nunca vaza secret

2. webhook-dispatcher/index.ts: chama authorizeDispatcher()
   - test_mode e replay_delivery_id exigem Modo B
   - Mantem HMAC, retries, circuit breaker

3. connections-auto-test/index.ts: chama authorizeCron()
   - Logica de auto-test inalterada

4. SQL aplicado via MCP apply_migration (NAO db push, conforme ADR 0006):
   - Vault: 2 secrets de 256 bits (WEBHOOK_DISPATCHER_SECRET, CONNECTIONS_AUTO_TEST_SECRET)
   - public.get_edge_function_secret() SECURITY DEFINER + whitelist + GRANT só service_role/postgres
   - dispatch_quote_webhook_event: envia x-dispatcher-secret do vault
   - retry_failed_webhook_deliveries: envia x-dispatcher-secret do vault
   - cron connections-auto-test: re-agendado com x-cron-secret
   - Snapshot em supabase/migrations/20260514112056* e ...57*

5. edge-authz-manifest.ts: rationale atualizado

6. scripts/check-no-db-push.mjs: allowlist expandida (resolve C2)

VALIDAÇÕES
- deno test _shared/dispatcher-auth.test.ts: 12/12 passed
- deno check em 78 edge functions: All clean
- Helper get_edge_function_secret validado no DB
- Cron + triggers + RPC confirmados no DB com header

DEPLOY NOTES
- Secrets ja configurados no painel Edge Functions pelo PO
- Cron + triggers ja enviam header (aplicado via MCP antes do deploy)
- Apos deploy auto da edge: validacao 401/403 ativa
- Checklist completo em docs/hardening/ONDA-1-EDGE-AUTH.md

ROLLBACK
- Re-deploy do commit anterior: triggers/cron continuam enviando header
  (ignorado pela funcao antiga). Sem perda de funcionalidade.
Copilot AI review requested due to automatic review settings May 14, 2026 12:13
@vercel
Copy link
Copy Markdown

vercel Bot commented May 14, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
promo-gifts Ready Ready Preview, Comment May 14, 2026 0:13am

@supabase
Copy link
Copy Markdown

supabase Bot commented May 14, 2026

This pull request has been ignored for the connected project doufsxqlfjyuvxuezpln due to reaching the limit of concurrent preview branches.
Go to Project Integrations Settings ↗︎ if you wish to update this limit.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 14, 2026

Walkthrough

Implementação de hardening de autenticação em duas edge functions públicas (webhook-dispatcher e connections-auto-test) via module centralizado de autorização com suporte a secrets, JWT com role enforcement, modo retrocompat com logs, callers de banco data atualizados para enviar secrets via vault, e documentação operacional completa.

Changes

Edge Function Auth Hardening

Layer / File(s) Summary
Shared auth module (types, helpers, unit tests)
supabase/functions/_shared/dispatcher-auth.ts, supabase/functions/_shared/dispatcher-auth.test.ts
Tipos AppRole, DispatcherAuthMode, CronAuthMode e resultados estruturados. constantTimeEqual() para comparação segura de secrets. authorizeDispatcher() suporta x-dispatcher-secret (constant-time), Bearer JWT com user_roles query e role enforcement, e retrocompat legacy_no_auth com warning log. authorizeCron() valida header de cron com secret configurable e retrocompat. Testes cobrem constant-time equality, legacy_no_auth, missing/correct/wrong/mismatched headers, e caracteres especiais.
Webhook dispatcher integration
supabase/functions/webhook-dispatcher/index.ts, supabase/functions/_shared/edge-authz-manifest.ts
Body parseado antes de auth para detectar test_mode/replay_delivery_id (requer user context). authorizeDispatcher chamado com minRole: "supervisor" e CORS headers; JSON malformado retorna 400 antes de auth. supabaseAdmin client retornado é usado para operações no banco. Manifesto atualizado documentando dois modos: x-dispatcher-secret ou JWT >= supervisor.
Connections auto-test integration
supabase/functions/connections-auto-test/index.ts, supabase/functions/_shared/edge-authz-manifest.ts
authorizeCron valida x-cron-secret header usando CONNECTIONS_AUTO_TEST_SECRET env var antes de qualquer operação. Docs atualizam cadência para 15 minutos. Manifesto documentado como "Modo C" com x-cron-secret header.
Database vault setup & caller hardening
supabase/migrations/20260514112056_edge_function_secrets_vault_setup.sql, supabase/migrations/20260514112057_edge_function_secrets_callers_hardening.sql
Migração 1: provisiona secrets no vault com placeholder + comentários. Migração 2: helper SECURITY DEFINER get_edge_function_secret() whitelist-valida nomes, execução restrita a service_role/postgres. dispatch_quote_webhook_event() trigger adiciona x-dispatcher-secret header (fallback empty string se falha). retry_failed_webhook_deliveries() RPC inclui secret no retry, retorna erro JSON se secret ausente. Cron connections-auto-test reschedule com x-cron-secret.
Documentation & validation tooling
docs/hardening/ONDA-1-EDGE-AUTH.md, scripts/check-no-db-push.mjs, .tmp-write-probe.md
Documento operacional: problema, arquitetura auth (multi-modo dispatcher, single-modo cron), retrocompat, rotação secrets (dashboard-first), pós-deploy checklist, validação automatizada, rollback. Script allowlist expandida para docs/hardening/. Arquivo probe sentinela.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes


Possibly related PRs

  • adm01-debug/Promo_Gifts#168: Ambas PRs tocam no mesmo script de guard CI (scripts/check-no-db-push.mjs)—PR principal estende allowlist de supabase db push.
  • adm01-debug/Promo_Gifts#146: PR principal atualiza public.dispatch_quote_webhook_event() para enviar x-dispatcher-secret ao dispatcher, que é a mesma função introduzida na PR #146.
  • adm01-debug/Promo_Gifts#70: Ambas tocam connections-auto-test—PR principal adiciona authorizeCron + vault-backed auth, PR #70 arruma/schedula o cron job.

🔍 Observações críticas de segurança

✅ Pontos fortes:

  • constantTimeEqual() implementada corretamente para evitar timing attacks em secret comparison
  • Helper SECURITY DEFINER com whitelist restrita de nomes de secrets (não aceita input direto do usuário)
  • Retrocompat com warning logs (não falha silenciosamente; permite dev/rollback seguro)
  • Role enforcement via query user_roles antes de autorizar (não confia apenas em JWT claims)
  • Body parseado antes de auth para rejeitar JSON malformado com 400 (correto)
  • Testes unitários covering cron auth com envvar missing/present/mismatch
  • x-dispatcher-secret gerado via vault, não hardcoded

⚠️ Itens para validar em execução:

  1. Rotação de secrets: Ordem critical—dashboard env antes de vault update (documentado, mas CI não força isso). Validar que trigger/retry não falha durante janela de desincronismo.
  2. Modo legacy_no_auth: Warning logs são estruturados e não vazam secrets? Conferir se logAuthEvent() está sendo usado em todos os paths.
  3. Cron schedule: Reschedule desativa job antigo? Confirmar que "unschedule any existing job" funciona antes de criar novo.
  4. Bearer = service role key: Path especial que cria supabaseAdmin sem validar JWT—comentário diz "unless user-context-only is required", validar que requireUserContext: true + bearer service_role_key resulta em 403.
  5. Fallback vazio no trigger: dispatch_quote_webhook_event() fallback x-dispatcher-secret para empty string se secret falha—edge function retorna 401, webhook_delivery fica pending. Esperado ou risco de queue stuck?

🐛 Bugs / regressões de risco:

  • Nenhum bug crítico detectado, mas edge cases de timing (moment entre deploy edge function e deploy migration) podem expor a função antes de secrets estarem prontos. Documentação de deploy order é importante.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 42.86% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed O título reflete fielmente o objetivo principal da PR: hardening de autenticação em duas edge functions (webhook-dispatcher e connections-auto-test) com a iniciativa Onda 1, específico e claro.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch cleanup/edge-functions-auth-hardening

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Hardens authentication for two Supabase Edge Functions that were previously callable anonymously (webhook-dispatcher and connections-auto-test), adding shared auth helpers and updating SQL callers (trigger/RPC/cron) to send required headers, with documentation for rollout/rollback.

Changes:

  • Added _shared/dispatcher-auth.ts (+ unit tests) to centralize auth for dispatcher/cron callers with retrocompat behavior when secrets aren’t configured.
  • Updated webhook-dispatcher and connections-auto-test edge functions to enforce the new auth modes.
  • Added snapshot migrations to provision vault secrets + update DB trigger/RPC/cron callers to include auth headers; updated docs and CI allowlist.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
supabase/migrations/20260514112057_edge_function_secrets_callers_hardening.sql Updates DB trigger/RPC/cron callers to send dispatcher/cron auth headers; includes helper function for vault secret lookup.
supabase/migrations/20260514112056_edge_function_secrets_vault_setup.sql Snapshot for creating the required Vault secrets (placeholders) if absent.
supabase/functions/webhook-dispatcher/index.ts Enforces auth via new shared authorizeDispatcher with user-context requirement for test/replay operations.
supabase/functions/connections-auto-test/index.ts Enforces cron secret auth via new shared authorizeCron.
supabase/functions/_shared/edge-authz-manifest.ts Updates auth rationale entries for the two hardened functions.
supabase/functions/_shared/dispatcher-auth.ts New shared authorization helper (modes A/B/C + retrocompat) and constant-time compare helper.
supabase/functions/_shared/dispatcher-auth.test.ts Unit tests for constantTimeEqual and authorizeCron.
scripts/check-no-db-push.mjs Expands allowlist to permit “supabase db push” mentions in new audit/hardening docs.
docs/hardening/ONDA-1-EDGE-AUTH.md Operational documentation for the auth hardening rollout, rotation, and validation checklist.
.tmp-write-probe.md Temporary diagnostic file added to repo.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread .tmp-write-probe.md
Comment on lines +1 to +2
// supabase/functions/_shared/dispatcher-auth.ts — sentinel test write
// (will be overwritten below if push works)
Comment on lines +53 to +61
* retornar 401. Sempre processa todos os caracteres antes de retornar.
*/
export function constantTimeEqual(a: string, b: string): boolean {
if (typeof a !== "string" || typeof b !== "string") return false;
if (a.length !== b.length) return false;
let result = 0;
for (let i = 0; i < a.length; i++) {
result |= a.charCodeAt(i) ^ b.charCodeAt(i);
}
Comment on lines +97 to +106
_dispatcher_secret := NULL;
END;

PERFORM extensions.http_post(
url := _project_url || '/functions/v1/webhook-dispatcher',
body := jsonb_build_object('event', _event, 'payload', _payload)::text,
params := '{}'::jsonb,
headers := jsonb_build_object(
'Content-Type', 'application/json',
'x-dispatcher-secret', COALESCE(_dispatcher_secret, '')
DECLARE
_event text;
_payload jsonb;
_project_url text := 'https://doufsxqlfjyuvxuezpln.supabase.co';
Comment on lines +125 to +128
DECLARE
v_supabase_url text := 'https://doufsxqlfjyuvxuezpln.supabase.co';
v_service_key text;
v_dispatcher_secret text;
headers := jsonb_build_object(
'Content-Type', 'application/json',
'x-dispatcher-secret', v_dispatcher_secret,
'Authorization', COALESCE('Bearer ' || v_service_key, '')
Comment on lines +211 to +216
SELECT net.http_post(
url := 'https://doufsxqlfjyuvxuezpln.supabase.co/functions/v1/connections-auto-test',
headers := jsonb_build_object(
'Content-Type', 'application/json',
'x-cron-secret', public.get_edge_function_secret('CONNECTIONS_AUTO_TEST_SECRET')
),
Comment on lines +22 to +25
const SUPABASE_URL = Deno.env.get("SUPABASE_URL") ?? "";
const ANON_KEY = Deno.env.get("SUPABASE_ANON_KEY") ?? "";
const SERVICE_KEY = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY") ?? "";

Comment on lines +21 to +28

const SUPABASE_URL = Deno.env.get("SUPABASE_URL") ?? "";
const ANON_KEY = Deno.env.get("SUPABASE_ANON_KEY") ?? "";
const SERVICE_KEY = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY") ?? "";

export type AppRole = "dev" | "supervisor" | "agente";
const ROLE_RANK: Record<AppRole, number> = { agente: 1, supervisor: 2, dev: 3 };

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.tmp-write-probe.md:
- Around line 1-2: Remova o artefato temporário ".tmp-write-probe.md" que contém
a linha de probe ("supabase/functions/_shared/dispatcher-auth.ts — sentinel test
write") antes do merge: delete o arquivo do commit atual (ou reverta o commit
que o adicionou) e force um novo push; opcionalmente adicione uma entrada
apropriada ao .gitignore para evitar re-commits acidentais do probe no futuro.

In `@supabase/functions/_shared/dispatcher-auth.ts`:
- Around line 55-62: A implementação de constantTimeEqual está vazando tempo ao
retornar cedo quando os comprimentos diferem; ajuste a função
(constantTimeEqual) para iterar até Math.max(a.length, b.length) em vez de
retornar quando a.length !== b.length e inclua a diferença de tamanho no
acumulador (por exemplo XOR com 0 para índices fora do range ou incorporar
a.length ^ b.length) para que o result agregue tanto diferenças de bytes quanto
de comprimento, preservando comparação constant-time; mantenha as checagens de
tipo e retorne result === 0 no final.
- Around line 26-27: AppRole and ROLE_RANK are missing the "admin" role which
causes valid admin users to be filtered out by the .filter((r) => r in
ROLE_RANK) check; update the AppRole type to include "admin" and add an "admin"
entry to ROLE_RANK with the appropriate numeric rank (e.g., between agente and
dev or as your access model requires), and ensure any header/module
documentation about supported roles matches these values (also confirm whether
"agente" is intended or should be renamed and adjust both AppRole and ROLE_RANK
accordingly).

In
`@supabase/migrations/20260514112057_edge_function_secrets_callers_hardening.sql`:
- Around line 94-112: The code currently falls back to an empty
x-dispatcher-secret when
public.get_edge_function_secret('WEBHOOK_DISPATCHER_SECRET') fails, causing
silent 401s; change the flow in the block around _dispatcher_secret and the call
to extensions.http_post so that if get_edge_function_secret throws or returns
NULL you do not call extensions.http_post with an empty header: capture the
exception (or check COALESCE result) and instead log the failure and abort early
(e.g., RAISE NOTICE/RAISE EXCEPTION or RETURN with an error) before reaching the
extensions.http_post call; update the block that assigns _dispatcher_secret (and
the subsequent PERFORM extensions.http_post call) to verify _dispatcher_secret
is present and non-empty and fail/log immediately if not.
- Around line 119-195: Add a restrictive ACL for the SECURITY DEFINER function
retry_failed_webhook_deliveries(): after the CREATE OR REPLACE FUNCTION ... END;
block, explicitly revoke all privileges from PUBLIC (REVOKE ALL ON FUNCTION
public.retry_failed_webhook_deliveries() FROM PUBLIC) and then grant execute
only to the privileged roles (GRANT EXECUTE ON FUNCTION
public.retry_failed_webhook_deliveries() TO postgres, service_role) so only
those roles can invoke the RPC that reads the vault secret and uses the service
role key.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 0a655a2f-a7f5-44e4-b1d2-b640461fafe0

📥 Commits

Reviewing files that changed from the base of the PR and between e44e1c2 and ede1dea.

📒 Files selected for processing (10)
  • .tmp-write-probe.md
  • docs/hardening/ONDA-1-EDGE-AUTH.md
  • scripts/check-no-db-push.mjs
  • supabase/functions/_shared/dispatcher-auth.test.ts
  • supabase/functions/_shared/dispatcher-auth.ts
  • supabase/functions/_shared/edge-authz-manifest.ts
  • supabase/functions/connections-auto-test/index.ts
  • supabase/functions/webhook-dispatcher/index.ts
  • supabase/migrations/20260514112056_edge_function_secrets_vault_setup.sql
  • supabase/migrations/20260514112057_edge_function_secrets_callers_hardening.sql

Comment thread .tmp-write-probe.md
Comment on lines +1 to +2
// supabase/functions/_shared/dispatcher-auth.ts — sentinel test write
// (will be overwritten below if push works)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Remover artefato temporário antes do merge.

Em Line [1] e Line [2] há um probe de escrita temporário. Esse arquivo de diagnóstico não deve entrar no main.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.tmp-write-probe.md around lines 1 - 2, Remova o artefato temporário
".tmp-write-probe.md" que contém a linha de probe
("supabase/functions/_shared/dispatcher-auth.ts — sentinel test write") antes do
merge: delete o arquivo do commit atual (ou reverta o commit que o adicionou) e
force um novo push; opcionalmente adicione uma entrada apropriada ao .gitignore
para evitar re-commits acidentais do probe no futuro.

Comment on lines +26 to +27
export type AppRole = "dev" | "supervisor" | "agente";
const ROLE_RANK: Record<AppRole, number> = { agente: 1, supervisor: 2, dev: 3 };
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== ocorrências de roles e enum/constraint de user_roles =="
rg -n -C2 "\"admin\"|'admin'|\\badmin\\b|\\bsupervisor\\b|\\bagente\\b|\\bdev\\b|user_roles" supabase

Repository: adm01-debug/Promo_Gifts

Length of output: 50380


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== app_role enum definition ==="
rg -n "CREATE TYPE.*app_role|DROP TYPE.*app_role|enum.*app_role" supabase/migrations --max-count=5

echo ""
echo "=== dispatcher-auth.ts header and full context ==="
head -40 supabase/functions/_shared/dispatcher-auth.ts

echo ""
echo "=== Lines 191-193 context in dispatcher-auth.ts ==="
sed -n '185,200p' supabase/functions/_shared/dispatcher-auth.ts

Repository: adm01-debug/Promo_Gifts

Length of output: 3143


AppRole type e ROLE_RANK não incluem admin, causando 403 para usuários com esse papel.

O cabeçalho do módulo (linhas 8–11) documenta suporte a admin|supervisor|dev, mas AppRole (linha 26) define apenas "dev" | "supervisor" | "agente" e ROLE_RANK (linha 27) cobre só { agente: 1, supervisor: 2, dev: 3 }.

Na linha 191, o filtro .filter((r) => r in ROLE_RANK) descarta qualquer papel não listado em ROLE_RANK. Usuários com user_roles.role = 'admin' (válido no enum app_role do banco) são silenciosamente filtrados, deixando userRoles vazio e causando falha de autorização (403).

Alinha AppRole com os papéis reais que o sistema deve reconhecer (remova admin do cabeçalho se deletado, ou restaure no código) e confirme se agente é intencional ou deveria ser admin.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@supabase/functions/_shared/dispatcher-auth.ts` around lines 26 - 27, AppRole
and ROLE_RANK are missing the "admin" role which causes valid admin users to be
filtered out by the .filter((r) => r in ROLE_RANK) check; update the AppRole
type to include "admin" and add an "admin" entry to ROLE_RANK with the
appropriate numeric rank (e.g., between agente and dev or as your access model
requires), and ensure any header/module documentation about supported roles
matches these values (also confirm whether "agente" is intended or should be
renamed and adjust both AppRole and ROLE_RANK accordingly).

Comment on lines +55 to +62
export function constantTimeEqual(a: string, b: string): boolean {
if (typeof a !== "string" || typeof b !== "string") return false;
if (a.length !== b.length) return false;
let result = 0;
for (let i = 0; i < a.length; i++) {
result |= a.charCodeAt(i) ^ b.charCodeAt(i);
}
return result === 0;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

A comparação ainda não é constant-time em mismatch de tamanho.

A Line 57 retorna antes do loop quando os comprimentos diferem. Isso vaza timing por length e contradiz o contrato da função justamente no caminho de autenticação por secret. Faça o loop até Math.max(a.length, b.length) e incorpore a diferença de tamanho no acumulador.

💡 Ajuste sugerido
 export function constantTimeEqual(a: string, b: string): boolean {
   if (typeof a !== "string" || typeof b !== "string") return false;
-  if (a.length !== b.length) return false;
-  let result = 0;
-  for (let i = 0; i < a.length; i++) {
-    result |= a.charCodeAt(i) ^ b.charCodeAt(i);
+  const maxLength = Math.max(a.length, b.length);
+  let result = a.length ^ b.length;
+  for (let i = 0; i < maxLength; i++) {
+    result |= (a.charCodeAt(i) || 0) ^ (b.charCodeAt(i) || 0);
   }
   return result === 0;
 }

As per coding guidelines, supabase/functions/**/*.ts: Validação de payload em webhooks (shared secret, assinatura HMAC quando aplicável).

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function constantTimeEqual(a: string, b: string): boolean {
if (typeof a !== "string" || typeof b !== "string") return false;
if (a.length !== b.length) return false;
let result = 0;
for (let i = 0; i < a.length; i++) {
result |= a.charCodeAt(i) ^ b.charCodeAt(i);
}
return result === 0;
export function constantTimeEqual(a: string, b: string): boolean {
if (typeof a !== "string" || typeof b !== "string") return false;
const maxLength = Math.max(a.length, b.length);
let result = a.length ^ b.length;
for (let i = 0; i < maxLength; i++) {
result |= (a.charCodeAt(i) || 0) ^ (b.charCodeAt(i) || 0);
}
return result === 0;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@supabase/functions/_shared/dispatcher-auth.ts` around lines 55 - 62, A
implementação de constantTimeEqual está vazando tempo ao retornar cedo quando os
comprimentos diferem; ajuste a função (constantTimeEqual) para iterar até
Math.max(a.length, b.length) em vez de retornar quando a.length !== b.length e
inclua a diferença de tamanho no acumulador (por exemplo XOR com 0 para índices
fora do range ou incorporar a.length ^ b.length) para que o result agregue tanto
diferenças de bytes quanto de comprimento, preservando comparação constant-time;
mantenha as checagens de tipo e retorne result === 0 no final.

Comment on lines +94 to +112
BEGIN
_dispatcher_secret := public.get_edge_function_secret('WEBHOOK_DISPATCHER_SECRET');
EXCEPTION WHEN OTHERS THEN
_dispatcher_secret := NULL;
END;

PERFORM extensions.http_post(
url := _project_url || '/functions/v1/webhook-dispatcher',
body := jsonb_build_object('event', _event, 'payload', _payload)::text,
params := '{}'::jsonb,
headers := jsonb_build_object(
'Content-Type', 'application/json',
'x-dispatcher-secret', COALESCE(_dispatcher_secret, '')
),
timeout_milliseconds := 5000
);
RETURN NEW;
EXCEPTION WHEN OTHERS THEN
RETURN NEW;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Não degrade para x-dispatcher-secret: '' quando o vault falhar.

Se get_edge_function_secret() quebrar, esse bloco ainda faz o http_post com header vazio. Depois que o hardening entrar, isso vira 401 em toda entrega enquanto o EXCEPTION externo continua retornando NEW, então a quebra fica silenciosa. Melhor falhar cedo aqui, com log, em vez de enviar uma chamada garantidamente não autorizada.

💡 Ajuste sugerido
   BEGIN
     _dispatcher_secret := public.get_edge_function_secret('WEBHOOK_DISPATCHER_SECRET');
   EXCEPTION WHEN OTHERS THEN
-    _dispatcher_secret := NULL;
+    RAISE LOG 'dispatch_quote_webhook_event: WEBHOOK_DISPATCHER_SECRET unavailable: %', SQLERRM;
+    RETURN NEW;
   END;
+
+  IF _dispatcher_secret IS NULL OR _dispatcher_secret = '' THEN
+    RAISE LOG 'dispatch_quote_webhook_event: empty WEBHOOK_DISPATCHER_SECRET; skipping dispatch';
+    RETURN NEW;
+  END IF;
 
   PERFORM extensions.http_post(
     url := _project_url || '/functions/v1/webhook-dispatcher',
     body := jsonb_build_object('event', _event, 'payload', _payload)::text,
     params := '{}'::jsonb,
     headers := jsonb_build_object(
       'Content-Type', 'application/json',
-      'x-dispatcher-secret', COALESCE(_dispatcher_secret, '')
+      'x-dispatcher-secret', _dispatcher_secret
     ),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@supabase/migrations/20260514112057_edge_function_secrets_callers_hardening.sql`
around lines 94 - 112, The code currently falls back to an empty
x-dispatcher-secret when
public.get_edge_function_secret('WEBHOOK_DISPATCHER_SECRET') fails, causing
silent 401s; change the flow in the block around _dispatcher_secret and the call
to extensions.http_post so that if get_edge_function_secret throws or returns
NULL you do not call extensions.http_post with an empty header: capture the
exception (or check COALESCE result) and instead log the failure and abort early
(e.g., RAISE NOTICE/RAISE EXCEPTION or RETURN with an error) before reaching the
extensions.http_post call; update the block that assigns _dispatcher_secret (and
the subsequent PERFORM extensions.http_post call) to verify _dispatcher_secret
is present and non-empty and fail/log immediately if not.

Comment on lines +119 to +195
CREATE OR REPLACE FUNCTION public.retry_failed_webhook_deliveries()
RETURNS jsonb
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $function$
DECLARE
v_supabase_url text := 'https://doufsxqlfjyuvxuezpln.supabase.co';
v_service_key text;
v_dispatcher_secret text;
v_retried int := 0;
v_skipped int := 0;
rec record;
v_max_attempts int;
BEGIN
BEGIN
v_service_key := current_setting('app.supabase_service_role_key', true);
EXCEPTION WHEN OTHERS THEN
v_service_key := NULL;
END;

BEGIN
v_dispatcher_secret := public.get_edge_function_secret('WEBHOOK_DISPATCHER_SECRET');
EXCEPTION WHEN OTHERS THEN
v_dispatcher_secret := NULL;
END;

IF v_dispatcher_secret IS NULL THEN
RETURN jsonb_build_object(
'ok', false,
'error', 'WEBHOOK_DISPATCHER_SECRET not configured in vault'
);
END IF;

FOR rec IN
WITH latest AS (
SELECT DISTINCT ON (d.webhook_id, d.event, d.payload_hash)
d.id, d.webhook_id, d.event, d.payload, d.attempt, d.success
FROM public.webhook_deliveries d
WHERE d.delivered_at > now() - interval '1 hour'
ORDER BY d.webhook_id, d.event, d.payload_hash, d.attempt DESC
)
SELECT l.*, w.active, w.retry_policy
FROM latest l
JOIN public.outbound_webhooks w ON w.id = l.webhook_id
WHERE l.success = false AND w.active = true
LOOP
v_max_attempts := COALESCE((rec.retry_policy->>'max_attempts')::int, 3);

IF rec.attempt >= v_max_attempts THEN
v_skipped := v_skipped + 1;
CONTINUE;
END IF;

PERFORM net.http_post(
url := v_supabase_url || '/functions/v1/webhook-dispatcher',
headers := jsonb_build_object(
'Content-Type', 'application/json',
'x-dispatcher-secret', v_dispatcher_secret,
'Authorization', COALESCE('Bearer ' || v_service_key, '')
),
body := jsonb_build_object(
'event', rec.event,
'payload', rec.payload
)
);
v_retried := v_retried + 1;
END LOOP;

RETURN jsonb_build_object(
'ok', true,
'retried', v_retried,
'skipped_max_attempts', v_skipped,
'ran_at', now()
);
END;
$function$;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== grants/revokes para retry_failed_webhook_deliveries =="
rg -n -C2 'retry_failed_webhook_deliveries|GRANT EXECUTE ON FUNCTION public\.retry_failed_webhook_deliveries|REVOKE ALL ON FUNCTION public\.retry_failed_webhook_deliveries' supabase/migrations

Repository: adm01-debug/Promo_Gifts

Length of output: 3737


🏁 Script executed:

sed -n '195,220p' supabase/migrations/20260514112057_edge_function_secrets_callers_hardening.sql

Repository: adm01-debug/Promo_Gifts

Length of output: 905


🏁 Script executed:

rg 'REVOKE ALL|GRANT EXECUTE' supabase/migrations/20260514112057_edge_function_secrets_callers_hardening.sql

Repository: adm01-debug/Promo_Gifts

Length of output: 386


Adicione ACL restritiva para retry_failed_webhook_deliveries().

Função criada com SECURITY DEFINER (correto) + SET search_path (correto), mas faltam REVOKE ALL e GRANT EXECUTE. Sem ACL explícito, qualquer role (anon/authenticated/public) consegue executar um RPC privilegiado que:

  • Acessa Vault (WEBHOOK_DISPATCHER_SECRET)
  • Dispara HTTP POST para edge function com service_role key
  • Reprocessa webhooks arbitrários

Use o padrão da migration anterior:

REVOKE ALL ON FUNCTION public.retry_failed_webhook_deliveries() FROM PUBLIC;
GRANT EXECUTE ON FUNCTION public.retry_failed_webhook_deliveries() TO postgres, service_role;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@supabase/migrations/20260514112057_edge_function_secrets_callers_hardening.sql`
around lines 119 - 195, Add a restrictive ACL for the SECURITY DEFINER function
retry_failed_webhook_deliveries(): after the CREATE OR REPLACE FUNCTION ... END;
block, explicitly revoke all privileges from PUBLIC (REVOKE ALL ON FUNCTION
public.retry_failed_webhook_deliveries() FROM PUBLIC) and then grant execute
only to the privileged roles (GRANT EXECUTE ON FUNCTION
public.retry_failed_webhook_deliveries() TO postgres, service_role) so only
those roles can invoke the RPC that reads the vault secret and uses the service
role key.

@adm01-debug adm01-debug merged commit 91f3c08 into main May 14, 2026
24 of 28 checks passed
@adm01-debug adm01-debug deleted the cleanup/edge-functions-auth-hardening branch May 14, 2026 12:59
adm01-debug pushed a commit that referenced this pull request May 14, 2026
Resolve conflitos causados pelo merge de PR #189 em main:
- scripts/check-no-db-push.mjs: mantida versão main (allowlist mais completa)
- connections-auto-test/index.ts: mantida versão main (authorizeCron() do módulo compartilhado)
- webhook-dispatcher/index.ts: auto-merged sem conflito

https://claude.ai/code/session_01S3r3Qup4JAPdsvmFsDWNwq
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants