Yago Marinho
Voltar para os artigos
  • Auth
  • Arquitetura
  • Python
  • Functional Programming

Autorização que também filtra os dados: os ganhos de um motor embarcado

Levei a decisão de autorização pra dentro da aplicação e ganhei mais do que disponibilidade: as concessões passaram a dizer quais dados cada pessoa vê.

Foto de Yago MarinhoPor: Yago MarinhoÚltima atualização realizada em:
💡
Este é o segundo de três artigos sobre um motor de autorização que criei num contrato privado. O primeiro conta o problema e a ideia; este conta o que o motor entregou. O código original e os detalhes do cliente ficam de fora, e os exemplos foram escritos do zero, num domínio inventado.
“Pode ou não pode?” — essa é a pergunta que praticamente todo sistema de autorização responde. Ela parece suficiente, e na maioria dos tutoriais ela é. Mas, quando a aplicação passa a lidar com várias organizações, com pessoas que têm papéis diferentes em cada uma e com telas que listam dados de mais de uma delas ao mesmo tempo, um simples sim ou não deixa de responder o que realmente importa.
No artigo anterior, mostrei como resolvi a autorização de uma aplicação sem poder tocar na API central de permissões: levei as concessões para o banco da própria aplicação e a decisão para dentro do processo, com um motor que compara regras declarativas. Agora quero falar do outro lado dessa decisão — o que ela entregou.
E, para mim, o ganho mais importante não foi o mais óbvio.

Antes de tudo: o que foi entregue

Para quem chegou agora, um resumo rápido do que foi construído:
  • As concessões moram no banco da aplicação: cada concessão é um papel num escopo, como “administradora na organização A”.
  • Cada rota declara o que exige: numa linha, com o privilégio mínimo, o recurso e a ação.
  • Um motor compara a exigência com as concessões: campo a campo, segundo um esquema, e devolve as concessões que autorizaram — e não um simples verdadeiro ou falso.
Vale também lembrar o contexto: eu era um dos desenvolvedores da aplicação, recebi a tarefa com a condição de não mexer na API central, e a solução foi minha. Tudo o que conto aqui vale para essa aplicação e para o momento em que ela foi entregue.
Vamos aos ganhos.

Ganho 1: a aplicação deixou de depender de outro serviço para responder

O primeiro ganho é o mais direto, e foi justamente o motivo da tarefa. Antes, cada pedido precisava esperar a resposta de outro serviço antes de fazer qualquer coisa. Agora, decidir é uma função comparando dados que já estão dentro da aplicação.
Em outras palavras: se a API central ficar lenta ou fora do ar, a aplicação continua respondendo. A disponibilidade dela deixa de ser multiplicada pela de outro serviço, e cada pedido economiza uma ida e volta pela rede.
Vale destacar um cuidado: esse ganho vem do desenho, e não de uma medição. Não tenho números de produção para mostrar aqui — e prefiro não inventar nenhum. O que posso afirmar é o que a arquitetura garante por construção: nenhuma decisão de autorização dessa aplicação passa pela rede.

Ganho 2: a exigência de cada rota ficou legível

Com as regras espalhadas em ifs, saber quem pode fazer o quê exige ler o código inteiro. Com a exigência declarada na própria rota, a resposta está numa linha:
@requer("usuario", "documento", "ler") async def listar_documentos(pedido): ... @requer("admin", "documento", "remover") async def remover_documento(pedido): ...
Mas por que isso importa? Porque, quando a regra é um dado, ela pode ser listada, revisada e auditada. Qualquer pessoa do time consegue ler o que cada rota exige sem entender a implementação do motor, e uma mudança de regra acontece num lugar só, e não em todos os lugares onde ela foi copiada.

Ganho 3: a autorização passou a dizer “sobre o quê”

Esse é, para mim, o ganho mais importante — e o menos óbvio.
Pense numa tela que lista documentos. A pergunta “essa pessoa pode ler documentos?” não basta, porque ela pode ler os documentos de algumas organizações e não de outras. Num motor que só responde sim ou não, a autorização diz “pode”, e o filtro de quais documentos mostrar fica escrito em outro lugar — geralmente numa consulta, às vezes por outra pessoa, com outra interpretação da mesma regra.
Como o motor que construí devolve as concessões que autorizaram, a mesma decisão já entrega o filtro. Veja abaixo, usando o motor do artigo anterior:
concessoes_da_ana = [ {"privilegio": "admin", "recurso": "*", "acao": "*", "organizacao": "org-a"}, {"privilegio": "usuario", "recurso": "documento", "acao": "ler", "organizacao": "org-b"}, {"privilegio": "usuario", "recurso": "relatorio", "acao": "ler", "organizacao": "org-c"}, ] # a listagem pede "ler documentos" sem fixar uma organização ESQUEMA_DA_LISTAGEM = { campo: comparador for campo, comparador in ESQUEMA.items() if campo != "organizacao" } exigencia = {"privilegio": "usuario", "recurso": "documento", "acao": "ler"} autorizaram = autorizar(exigencia, concessoes_da_ana, ESQUEMA_DA_LISTAGEM) def organizacoes_visiveis(concessoes): organizacoes = {concessao["organizacao"] for concessao in concessoes} # uma concessão com curinga na organização libera todas return None if "*" in organizacoes else organizacoes organizacoes_visiveis(autorizaram) # {"org-a", "org-b"}
O que está acontecendo aqui?
  • A concessão da org-a cobre a leitura de documentos, porque o curinga de recurso e de ação vale para tudo naquela organização.
  • A concessão da org-b cobre exatamente o que foi pedido.
  • A concessão da org-c fica de fora: ela permite ler relatórios, e não documentos.
A consulta recebe esse conjunto e devolve só os documentos das organizações A e B. Ou seja: a autorização e o filtro de dados deixam de poder divergir, porque os dois saem da mesma decisão.
O mesmo raciocínio vale fora da API. Num dos painéis da aplicação, os relatórios vinham de uma ferramenta de BI embutida na página. Ferramentas como o Power BI aceitam, no momento de embutir o relatório, identidades para a segurança em nível de linha (row-level security) — o relatório só mostra as linhas que aquelas identidades podem ver. As concessões que autorizaram viravam essas identidades, e cada pessoa via no relatório exatamente o que a autorização permitia, sem uma segunda regra escrita dentro do BI.

Ganho 4: o mesmo papel, em escopos diferentes

Outro ganho que apareceu cedo foi poder dar papéis diferentes para a mesma pessoa, cada um no seu escopo, sem criar tabelas ou colunas novas para isso. Observe o caso do Bruno, administrador na organização A e atendente na organização B:
concessoes_do_bruno = [ { "papel": "admin", "privilegio": "admin", "recurso": "*", "acao": "*", "organizacao": "org-a", }, { "papel": "atendente", "privilegio": "usuario", "recurso": "documento", "acao": "ler", "organizacao": "org-b", }, ] remover_na_org_b = { "privilegio": "admin", "recurso": "documento", "acao": "remover", "organizacao": "org-b", } autorizar(remover_na_org_b, concessoes_do_bruno) # []
Ser administrador na organização A não empresta nada na organização B. Para o motor, cada concessão é independente, e o isolamento entre organizações é só mais um campo comparado. Para quem administra os acessos, dar ou tirar um papel numa organização é criar ou apagar uma linha — e nada além disso.

Ganho 5: regras novas sem mexer no motor

No artigo anterior, mostrei como um eixo novo entra como um campo e um comparador. Para mim, isso mudou a forma de olhar para um requisito novo. Quando surgia uma regra de contexto — um grupo dentro da organização, por exemplo —, a pergunta deixava de ser “quanto do código eu vou ter que mexer?” e passava a ser “qual comparador essa regra precisa?”.
O motor fica fechado para modificação e aberto para extensão. É o princípio aberto/fechado aplicado onde ele mais ajuda: na parte do sistema que mais muda com o negócio.

Ganho 6: testes rápidos, sem banco e sem rede

Como o motor é uma função pura, cada regra ganhou um teste que só escreve dados e confere o resultado. Não há banco para subir, nem serviço para simular. Além de rápidos, esses testes funcionam como documentação viva: para saber se um administrador de uma organização alcança outra, basta procurar o teste com esse nome.

E o que isso custou?

Óbvio que não é uma abordagem que só apresenta vantagens. Três custos apareceram, e prefiro deixá-los claros:
  • Generalidade a mais: o motor ficou mais flexível do que a aplicação precisava naquele momento. Um esquema com campos e comparadores plugáveis exige mais cuidado para ler e manter do que meia dúzia de ifs. Acredito que vale a pena quando as regras crescem, mas é um investimento, e não um ganho gratuito.
  • Uma segunda fonte da verdade: a aplicação passou a guardar os próprios usuários e concessões, enquanto as outras continuavam com a API central.
  • Dados perto pedem cuidado: ou se leem as concessões a cada pedido, ou elas vão para um cache — e, no cache, uma revogação precisa valer na hora.
Os dois últimos pontos merecem um artigo só para eles, e é exatamente o próximo.

Roadmap e próximos passos

Na entrega, alguns passos ficaram registrados como evolução natural do motor:
  • Cache das concessões, com a revogação apagando o cache da pessoa na hora.
  • Validação das regras na subida da aplicação, para que um erro de configuração apareça antes do primeiro pedido, e não durante ele.
  • O terceiro artigo da série, sobre os desafios: o legado, o cache e os riscos de um motor genérico.

Conclusão

Comecei este artigo com a pergunta “pode ou não pode?”. Depois de construir esse motor, acredito que ela está incompleta. A pergunta que uma aplicação com várias organizações realmente precisa responder é “pode, sobre o quê?” — e, quando a autorização responde as duas ao mesmo tempo, o filtro de dados deixa de ser uma regra paralela e passa a ser consequência da própria decisão.
Os outros ganhos — a independência da API central, as regras legíveis, os testes sem infraestrutura — também importam, mas todos nascem da mesma escolha: tratar a regra como dado e a decisão como uma função. Foi essa escolha que permitiu entregar mais do que a tarefa pedia, sem tocar no que não era meu para mudar.
No fim, a lição que fica para mim é esta:
uma boa decisão de desenho não resolve só o problema que a motivou; ela abre espaço para resolver os próximos.
Foto de Yago Marinho
Yago MarinhoEngenheiro de Software Sênior. Construo produtos com IA, do design ao deploy.
Ver portfólio ↗