Skip to content

Trilha de auditoria

ADR-0149 / ADR-0150 · desde 2026-09-20

Um registro por ação administrativa que altera estado. Responde as cinco perguntas — quem, o quê, quando, de onde, com qual resultado — e é append-only.


Cobertura

Entra Não entra
84 rotas administrativas de escrita 30 rotas self-service do cliente (assinar, comprar, trocar cartão)
Eventos de auth, inclusive login recusado Webhooks de gateway (já têm webhook_logs)
5 exportações (GET, mas levam base embora — art. 37 da LGPD) Leitura comum

O cliente fica fora porque já tem registro próprio onde importa (subscriptions, ticket_orders, account_deletions), e porque 11.852 clientes diluiriam a trilha de 13 administradores — que é a que responde "quem emitiu este reembolso?".

Nenhuma ação silenciosa: rota administrativa de escrita que nasça sem verbo canônico quebra o teste test_toda_rota_administrativa_tem_verbo. A cobertura não depende de alguém lembrar.

⚠️ Duas rotas admin (/admin/{stripe,asaas}/subscriptions/{id}/cancel) compartilham o nome de função com as self-service. O verbo delas vem de VERBOS_POR_ROTA, pelo caminho — resolver pelo nome auditaria a do cliente e deixaria a do admin de fora (ADR-0151).

O registro

{
  "occurred_at": "2026-09-20T14:02:11.482000+00:00",
  "actor_id": "6a0a1116...", "actor_name": "angelasirino",
  "actor_email": "ana@fdplay.com", "actor_type_user": "6a0a...",
  "action": "customer.toggle_active", "resource": "users",
  "resource_id": "6aad368b...", "resource_label": "eleni-jesus@hotmail.com",
  "outcome": "success", "status_code": 200,
  "before": { "enable": true }, "after": { "enable": false },
  "request_id": "b3f1...", "ip": "203.0.113.9", "user_agent": "painel/1.0",
  "route": "POST /api/v1/admin/customers/{customer_id}/toggle-active",
  "retention_until": "2031-09-20T14:02:11.482000+00:00",
  "prev_hash": "9c1f…", "hash": "4ab2…"
}

occurred_at é sempre UTC, com milissegundos. A conversão para Brasília é do painel — e sem exceção, ao contrário de /admin/finance/*, que corta o dia no fuso de São Paulo.

action é verbo canônico recurso.acao, não a rota. Rota muda de caminho, o significado não. O painel agrupa por prefixo (refund.*, customer.*).

before/after só com o que mudou. Copiar o documento inteiro faria da trilha uma segunda base — mesmo dado pessoal, mesma exposição, sem o mesmo controle.

🔒 Segredo nunca entra. password, hashes, token_version, signature, Authorization, número de cartão — conferidos por lista, recursivamente, e substituídos por [omitido]. Troca de senha registra a ocorrência, com before/after nulos.

outcome

Valor Quando
success 2xx/3xx
denied 401/403 — tentativa recusada
error 4xx restante, 5xx, ou exceção

denied costuma ser o registro mais importante: é ele que mostra alguém tentando o que não pode. Sem ele, varredura é indistinguível de silêncio. Ação que falhou e sofreu rollback permanece na trilha, com error — é o registro da tentativa.

Consulta

GET /admin/audit-logs?start=&end=&actor_id=&action=&resource=&resource_id=&outcome=
                     &current_page=&qty_docs_page=
GET /admin/audit-logs/{_id}
GET /admin/audit-logs/verificar?limite=500
GET /admin/audit-logs/export?format=csv   (mesmos filtros; autentica por header ou ?token=)

A exportação aceita os mesmos filtros da listagem e é ela mesma registrada, com o recorte pedido e a contagem de linhas. before/after saem como texto: são dicionários de forma variável, e uma coluna por campo alterado não existiria — ali o auditor precisa ler, não cruzar.

Envelope docs + pagination.qty_total_docs das demais rotas. Dois recortes que o índice cobre:

  • por pessoa — actor_id + datas: tudo que o administrador X fez no período;
  • por registro — resource_id: tudo que aconteceu com o cliente Y. É este que responde "por que esta conta foi suspensa?".

⚠️ A leitura é registrada (audit.read). Quem consultou o histórico de quem também é informação de auditoria.

Imutabilidade

Não existe PUT nem DELETE em /admin/audit-logs, para perfil nenhum. O gate é a ausência da rota, não a boa vontade de quem opera.

Cada registro carrega prev_hash (o hash do anterior) e hash (SHA-256 de si mesmo, já incluindo prev_hash). Apagar ou alterar um registro quebra a cadeia:

GET /admin/audit-logs/verificar
→ { "conferidos": 500, "elos_quebrados": [], "sem_encadeamento": [], "integra": true }

sem_encadeamento lista registros gravados com chain_broken — a cabeça da cadeia estava indisponível no momento, ou o registro foi reduzido pelo expurgo. Não é adulteração, é a cadeia dizendo que ali houve um corte legítimo. elos_quebrados é que pede investigação.

Quem lê

Perfil Auditor, criado por scripts/setup_auditoria.py. Não Admin — é quem mais aparece na trilha — e nunca Global Viewer, onde estão os 13 administradores.

🔒 As rotas sobem inacessíveis. O RBAC é fail-closed (ADR-061): handler fora de users_type leva 401. Até alguém receber o perfil Auditor, só o root lê.

Retenção

Fase Quando O que acontece
Redução occurred_at + 12 meses before/after saem; quem/o quê/quando ficam
Expurgo retention_until (5 anos) o registro sai

before/after são o que a trilha tem de dado pessoal de cliente. Passado o prazo operacional, a pergunta que sobra — "quem fez o quê, e quando" — não precisa do conteúdo. Rotina diária no worker; prazo adotado do sugerido no pedido, e mudá-lo é uma constante.