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 deVERBOS_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, combefore/afternulos.
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=
¤t_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_typeleva 401. Até alguém receber o perfilAuditor, só orootlê.
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.