Por que admin = pode ver tudo quebra em sistemas de denúncia
Quando você olha de fora, um sistema de denúncias anônimas parece um CRUD relativamente comum.
Alguém envia um relato.
Um administrador recebe.
Alguns arquivos são anexados.
O caso passa por alguns status.
No final, alguém encerra.
A primeira modelagem provavelmente fica parecida com isso:
denunciante
investigador
admin
E as permissões:
denunciante -> cria denúncia, acompanha o próprio caso
investigador -> visualiza casos, adiciona informações
admin -> visualiza tudo, gerencia usuários
Até aqui, tranquilo.
O problema aparece quando chega um caso realmente sensível.
Imagine que um funcionário denuncie o próprio gestor.
Esse gestor também possui perfil administrativo no sistema.
A regra continua sendo:
admin = pode_ver_tudo
?
Obviamente não deveria.
E é aí que um modelo simples de RBAC começa a não ser suficiente.
O papel do usuário não conta a história inteira
RBAC funciona muito bem quando as permissões acompanham funções relativamente estáveis.
Financeiro acessa informações financeiras.
Suporte acessa tickets.
Administrador configura o sistema.
Só que uma investigação sensível introduz uma variável nova:
o contexto daquele caso específico.
Uma pessoa pode ter permissão para investigar denúncias no sistema inteiro e, ao mesmo tempo, não poder acessar uma determinada denúncia.
Algo conceitualmente parecido com:
pode_acessar_caso =
possui_permissao_de_investigacao
&& esta_atribuido_ao_caso
&& !esta_envolvido_no_caso
&& !possui_conflito_de_interesse
O papel não mudou.
O contexto mudou.
Isso começa a empurrar a autorização para uma combinação de:
RBAC
+
permissões por caso
+
atribuições
+
exclusões
+
conflitos de interesse
+
overrides auditáveis
O famoso admin começa a perder muito do sentido.
"Pode ver o caso" também é uma permissão ampla demais
Outro atalho tentador é tratar a denúncia inteira como um único objeto protegido.
Mas um caso pode conter coisas bem diferentes:
Caso
├── relato inicial
├── identidade do denunciante
├── anexos
├── notas internas
├── entrevistas
├── análise
└── conclusão
A pessoa que pode visualizar o relato necessariamente precisa saber quem denunciou?
Não.
Quem analisa um documento precisa visualizar todas as entrevistas?
Também não necessariamente.
Quem aprova uma decisão final precisa ter acesso irrestrito a todos os dados pessoais coletados?
De novo: não.
Então uma permissão genérica como:
can_view_case
começa a virar algo mais granular:
can_view_report
can_view_reporter_identity
can_view_evidence
can_view_internal_notes
can_add_evidence
can_assign_case
can_close_case
E isso deixa de ser somente um problema de autorização.
Começa a afetar o próprio modelo de dados.
Se a identidade do denunciante é muito mais sensível que o restante do relato, talvez ela não devesse ser simplesmente mais uma propriedade retornada por:
GET /cases/:id
Talvez precise de:
- política de acesso diferente;
- trilha de auditoria diferente;
- armazenamento separado;
- criptografia ou proteção adicional.
O modelo de dados deveria refletir o nível de sensibilidade dos dados.
Anonimato não é um booleano
É fácil imaginar algo assim:
{
"anonymous": true
}
Mas anonimato não é uma propriedade isolada da denúncia.
É uma propriedade do sistema inteiro.
Você pode remover o campo "nome" e ainda coletar:
- endereço IP;
- identificadores de analytics;
- cookies;
- sessão;
- dados de autenticação;
- logs de proxy;
- fingerprint de dispositivo;
- informações enviadas para serviços de terceiros.
A interface diz:
Sua denúncia é anônima.
Mas a infraestrutura consegue correlacionar o envio com uma pessoa.
Isso nem sempre acontece por má-fé.
Muitas vezes acontece porque ninguém tratou anonimato como requisito de arquitetura.
Uma pergunta melhor seria:
Quais informações deste fluxo poderiam identificar o denunciante e quais delas realmente precisamos armazenar?
Essa pergunta muda bastante coisa.
Como conversar com alguém que você não identifica?
Outro problema aparece logo depois.
Se o denunciante é anônimo, como o investigador pede mais informações?
Exemplo:
Você lembra aproximadamente quando isso aconteceu?
ou:
Você ainda possui o arquivo original?
Um sistema tradicional resolve comunicação usando uma conta.
E-mail.
Login.
Usuário.
Só que isso pode destruir o anonimato que você estava tentando preservar.
Uma abordagem possível é gerar uma credencial opaca no momento da denúncia:
report_id
access_token
Algo conceitualmente assim:
POST /reports
Retornando:
{
"report_id": "RPT-8FH2K",
"access_token": "<opaque-secret>"
}
Aquele token não representa uma identidade.
Ele apenas comprova que quem possui a credencial pode acessar o lado do denunciante daquele caso.
Você ganha comunicação bidirecional sem exigir uma conta.
Mas aparecem outros problemas:
- o token é armazenado em plaintext?
- deveria ser armazenado como hash?
- pode ser rotacionado?
- expira?
- um administrador consegue visualizá-lo?
- o que acontece se ele vazar?
- como funciona "recuperação de acesso" quando você deliberadamente não sabe quem é o usuário?
Um requisito aparentemente simples — "permitir acompanhamento anônimo" — vira um problema de autenticação.
Evidência não deveria ser apenas um attachment
Outro ponto interessante é upload de arquivos.
A implementação inicial pode ser:
attachments
Subiu o arquivo, salva no storage e acabou.
Só que, se esse arquivo pode influenciar uma decisão séria, começam outras perguntas:
- Quem enviou?
- Quando chegou?
- Qual era o arquivo original?
- Ele foi substituído?
- Quem acessou?
- Quem fez download?
- Ele foi descartado da investigação?
- Por quê?
Um registro pode começar a ficar mais próximo disso:
Evidence
--------
id
case_id
original_filename
storage_key
content_hash
submitted_at
submitted_by
source_type
status
E algumas operações importantes podem virar eventos:
EVIDENCE_UPLOADED
EVIDENCE_VIEWED
EVIDENCE_DOWNLOADED
EVIDENCE_RESTRICTED
EVIDENCE_MARKED_IRRELEVANT
Tem um evento que provavelmente não deveria existir:
EVIDENCE_SILENTLY_DISAPPEARED
Esse é um daqueles domínios em que DELETE merece um pouco de desconfiança.
Em vez de:
DELETE FROM evidence
WHERE id = ?;
talvez o domínio precise de algo como:
status = discarded
reason = duplicate
changed_by = investigator_42
changed_at = ...
O arquivo pode deixar de ser considerado sem que a história da investigação seja apagada.
Isso não significa que todo sistema de denúncias precisa de blockchain, WORM storage ou uma implementação forense completa.
Significa apenas que auditabilidade precisa ser pensada antes do dia em que alguém perguntar "o que aconteceu com esse arquivo?".
Log de aplicação não é audit log
Esse registro:
GET /api/cases/381 200
pode ser ótimo para observabilidade.
Mas diz pouco sobre uma investigação.
Para uma ação sensível, eu prefiro conseguir reconstruir algo parecido com:
actor: user_827
action: viewed_reporter_identity
case: case_381
timestamp: ...
reason: assigned_investigator
O ponto não é registrar tudo indiscriminadamente.
É registrar as ações que mudam ou expõem informações importantes.
Por exemplo:
- quem visualizou a identidade;
- quem adicionou uma evidência;
- quem alterou a conclusão;
- quem transferiu o caso;
- quem usou uma permissão excepcional.
E existe outra questão:
quem pode alterar o próprio audit log?
Se o usuário que realizou uma ação consegue editar silenciosamente o registro daquela ação, a trilha perde boa parte do valor.
open e closed também começam a não servir
Outro problema que parece pequeno é status.
Primeira versão:
open
closed
Depois:
new
triage
investigating
resolved
closed
Só que existe uma confusão aí.
Uma coisa é em qual etapa o caso está.
Outra coisa é qual foi a conclusão da investigação.
Um caso pode estar:
workflow_status = closed
e ter como resultado:
outcome = inconclusive
Talvez faça mais sentido separar:
workflow_status:
new
triage
investigating
awaiting_information
decision_pending
closed
outcome:
substantiated
partially_substantiated
unsubstantiated
inconclusive
Por que isso importa?
Porque software também cria comportamento.
Se a interface obriga alguém a escolher apenas:
true
false
a aplicação está impondo uma interpretação do mundo real.
Nem todo caso consegue ser reduzido a "aconteceu" ou "não aconteceu".
Às vezes simplesmente não existem elementos suficientes para chegar a uma conclusão.
Um enum aparentemente inocente pode acabar virando política interna.
Conflito de interesse deveria existir no domínio
Em muitas empresas existe uma regra semelhante a:
Se você estiver envolvido no caso, não pode investigá-lo.
Ótimo.
Mas se isso existe somente em um PDF de política interna, o sistema depende de alguém lembrar da regra.
Podemos representar a relação:
CaseParticipant
---------------
case_id
user_id
relationship
Com relações como:
reporter
subject
witness
investigator
reviewer
conflicted
E a autorização começa a consultar essa informação:
def can_access_case(user, case):
if not user.has_permission("investigate"):
return False
if case.has_conflict(user):
return False
return case.is_assigned_to(user)
Claro que uma implementação real provavelmente terá mais regras.
Mas a ideia é interessante:
uma regra de governança deixa de depender exclusivamente de comportamento humano e passa a ser reforçada pelo software.
E quando alguém realmente precisar quebrar a regra?
Existe o outro extremo.
Algum dia vai aparecer uma exceção.
Todos os investigadores disponíveis podem estar envolvidos.
Pode existir uma solicitação jurídica.
Pode haver uma situação emergencial.
Bloquear tudo rigidamente pode tornar o sistema impossível de operar.
Mas isso não significa precisar de:
super_admin = god_mode
Uma alternativa é tratar isso como um break-glass access.
Algo como:
override requires:
- permissão elevada
- justificativa explícita
- reautenticação
- evento no audit log
- segunda aprovação, quando necessário
A ação continua possível.
Só que passa a ser:
intencional, excepcional e rastreável.
Nesse tipo de domínio, um pouco de fricção pode ser uma feature.
Admin da empresa não significa dono de todos os dados
Esse talvez seja um dos pontos mais estranhos para quem está acostumado com SaaS B2B.
Normalmente pensamos:
customer_admin = everything_inside_tenant
Faz sentido para diversos sistemas.
Só que imagine:
O administrador da conta é justamente a pessoa denunciada.
Ou trabalha na área investigada.
Ou comprou o software, mas não deveria ter qualquer participação em investigações.
Nesse cenário:
tenant_owner != unrestricted_case_access
Configurar a organização e acessar dados sensíveis são permissões diferentes.
Esse detalhe muda bastante a forma como um painel administrativo precisa ser desenhado.
A complexidade começa depois do POST /reports
Quando você olha para o produto pela primeira vez, é fácil pensar que a parte difícil é o formulário.
Validação.
Upload.
Anonimato.
Talvez CAPTCHA.
Mas boa parte dos problemas interessantes começa depois:
submission
↓
triage
↓
conflict detection
↓
assignment
↓
evidence preservation
↓
interviews
↓
analysis
↓
decision
↓
anonymous follow-up
↓
retention
Cada seta esconde alguma decisão de autorização.
Cada etapa cria ou manipula informação sensível.
E vários atalhos que seriam aceitáveis em um CRUD comum começam a ficar perigosos.
Hoje eu acho mais útil pensar nesse tipo de sistema não como:
um formulário anônimo com um painel administrativo
mas como:
um sistema de autorização contextual, evidências e auditoria construído ao redor de um workflow de investigação.
O formulário é só onde os dados entram.
Algumas perguntas que eu faria antes de criar o schema
Se tivesse que começar um sistema sensível desse tipo hoje, tentaria responder cedo:
- Um usuário pode ter permissão global e ainda ser bloqueado em um caso específico?
- A identidade do denunciante possui uma permissão própria?
- Uma evidência pode desaparecer sem deixar histórico?
- É possível conversar com alguém sem descobrir sua identidade?
- O sistema consegue representar conflitos de interesse?
- Existe um mecanismo controlado para exceções?
- Administrar o tenant significa necessariamente poder acessar os casos?
- Status operacional e conclusão são coisas diferentes no modelo?
- Daqui seis meses conseguiremos reconstruir por que determinada decisão foi tomada?
Se essas perguntas aparecem depois que tudo já está em produção, normalmente a mudança dói bastante.
Se aparecem antes, provavelmente influenciam schema, autorização, logging e até a forma como o produto é dividido.
No fim, a parte mais simples de um sistema de denúncias pode ser justamente o formulário.
A arquitetura começa a ficar interessante depois que alguém aperta Enviar.
Trabalho como CTO e cofundador da Sigilo, onde lidamos com sistemas de denúncias e feedback organizacional. Boa parte dessas questões surgiu ao pensar nos edge cases que aparecem depois que um relato sensível entra no sistema.
Tenho curiosidade de saber como vocês modelariam isso:
vocês manteriam RBAC + regras de domínio na aplicação ou levariam esse tipo de autorização contextual para uma camada/policy engine separada?