Audit de mutações multi-tenant (DEV-934)¶
TL;DR¶
Toda mutação via admin client (service_role, que bypassa RLS) pode ser auditada trocando createAdminClient() por createAuditedAdmin(ctx) — grava em audit_log_tenant_writes e, se o tenant gravado divergir do tenant ativo da sessão, dispara Sentry.captureMessage → Central de Observabilidade (issue Linear + WhatsApp).
Por que existe¶
A investigação DEV-933 provou que o stack (Sentry/PostHog/Mixpanel) não detecta vazamento de contexto multi-tenant: super_admin/impersonate gravando no tenant errado não gera exception nem evento. Este audit é a rede de detecção; a RLS de WRITE (bloqueio na origem) é etapa 2, calibrada com os dados daqui.
Como funciona¶
src/lib/supabase/audit.ts—createAuditedAdmin(ctx)devolve um Proxy do admin client:- intercepta
insert/update/upsert/delete;selectpassa direto, sem custo; - tenant do payload (insert/upsert) OU do filtro (
.eq/.match/.in/.filteremtenant_id); - lote com tenants mistos (ou row sem tenant) vira
MULTIPLE— nunca mascara; - fail-open absoluto: erro do audit loga
[DEV-934]e nunca bloqueia a mutação; - gravação fire-and-forget (client singleton dedicado, sem await no hot path).
ctxé mutável de propósito: crie antes doresolveTenant, preenchactx.activeTenantdepois do guard.- Migration
20260704210000— tabela com RLS service-only + viewcross_tenant_writes_24h(meta = 0) comsecurity_invoker = true+ revokeanon/authenticated(sem isso o PostgREST exporia o audit — P1 pego no claude-review de 04/07).
Adoção (incremental, 1 linha por rota)¶
const auditCtx: AuditCtx = { route: "/api/app/x", userId: user.id, activeTenant: null,
requestId: request.headers.get("x-request-id") ?? randomUUID() };
const admin = createAuditedAdmin(auditCtx);
const role = await resolveTenant(admin, user.id);
// ...guard...
auditCtx.activeTenant = role.tenant_id;
/api/app/trigger-generation (cobre até as mutações internas do debitCredits) e /api/app/tickets. Pendente: demais rotas de mutação das tabelas críticas + adotar o tenant-guard.ts (CAD-599 — existe e está órfão, zero imports) nos jobs service_role. Don'ts¶
- NUNCA
awaitno insert de audit dentro do hot path. - NUNCA expor a tabela/view pra
anon/authenticated(revoke é parte da migration). - Rota nova de mutação SEM o wrapper nasce sem audit — cobrar na adoção.
Consultas úteis¶
select * from cross_tenant_writes_24h; -- meta = 0
select route, count(*) from audit_log_tenant_writes group by 1; -- cobertura por rota
Testes¶
src/lib/supabase/audit.test.ts — 13 casos (mismatch por payload e por filtro, MULTIPLE, select transparente, retorno preservado). npx vitest run src/lib/supabase/audit.test.ts.
Histórico¶
- 2026-07-04 — criado (PR #112, cascata crítica: openrouter + claude-review + runtime-fix-review). E2E produção: linha real com
request_idpropagado, view = 0.
Notas Relacionadas¶
suporte-botao-ajuda