DevGuideTK
← Trilha Backend

Autorização (RBAC e ABAC)

Modelos para decidir o que cada usuário pode fazer, por papel ou por atributos.

As cinco versões desta página foram executadas e receberam os mesmos dez pedidos. A política em OPA e a segurança por linha no Postgres também foram testadas.

A política, em cada linguagem

Cada versão tem as mesmas peças:

  1. Uma tabela de alcances: para cada papel e ação, "os próprios" ou "todos". O que não está na tabela é negado.
  2. Uma função pode(usuario, acao, pedido), o único lugar onde a regra mora.
  3. Nas rotas, a conferência depois de carregar o pedido: 404 se o usuário não pode vê-lo, 403 se pode ver, mas não pode fazer a ação.
import http from "node:http";

type Usuario = { id: number; papel: string };
type Pedido = { id: number; cliente_id: number; total: string };
type Acao = "pedido:ler" | "pedido:reembolsar";

// Cada papel diz o alcance de cada ação; o que não está aqui é negado
const alcances: Record<string, Partial<Record<Acao, "proprios" | "todos">>> = {
  cliente: { "pedido:ler": "proprios" },
  suporte: { "pedido:ler": "todos" },
  financeiro: { "pedido:ler": "todos", "pedido:reembolsar": "todos" },
};

function pode(usuario: Usuario, acao: Acao, pedido: Pedido): boolean {
  const alcance = alcances[usuario.papel]?.[acao];
  if (alcance === "todos") return true;
  if (alcance === "proprios") return pedido.cliente_id === usuario.id;
  return false;
}

// No exemplo, o usuário vem de um cabeçalho; num sistema real, da sessão ou do token
const usuarios = new Map<string, Usuario>([
  ["ana", { id: 1, papel: "cliente" }],
  ["bruno", { id: 2, papel: "cliente" }],
  ["carla", { id: 3, papel: "suporte" }],
  ["davi", { id: 4, papel: "financeiro" }],
  ["eva", { id: 5, papel: "estagiario" }],
]);
const pedidos = new Map<number, Pedido>([
  [1, { id: 1, cliente_id: 1, total: "120.00" }],
  [2, { id: 2, cliente_id: 2, total: "80.00" }],
]);

function responder(res: http.ServerResponse, status: number, corpo?: object) {
  res.writeHead(status, { "content-type": "application/json" });
  res.end(corpo ? JSON.stringify(corpo) : undefined);
}

http.createServer((req, res) => {
  const usuario = usuarios.get(String(req.headers["x-usuario"]));
  if (!usuario) return responder(res, 401);

  const rota = req.url?.match(/^\/pedidos\/(\d+)(\/reembolso)?$/);
  if (!rota) return responder(res, 404);
  const pedido = pedidos.get(Number(rota[1]));

  // Quem não pode ver o pedido recebe 404, igual a um pedido que não existe
  if (!pedido || !pode(usuario, "pedido:ler", pedido)) return responder(res, 404);

  if (req.method === "GET" && !rota[2]) return responder(res, 200, pedido);
  if (req.method === "POST" && rota[2]) {
    if (!pode(usuario, "pedido:reembolsar", pedido)) return responder(res, 403);
    return responder(res, 200, { reembolsado: pedido.id });
  }
  responder(res, 405);
}).listen(3000);

O resultado, nas cinco linguagens

O pedido 1 é da Ana; o 2, do Bruno.

PedidoEsperadoResultado nas cinco
GET /pedidos/1 sem usuário401401
Ana lê o pedido 1, dela200200
Ana lê o pedido 2, do Bruno404404
Ana lê o pedido 999, que não existe404404
Carla, do suporte, lê o pedido 2200200
Ana pede o reembolso do pedido 1, dela403403
Bruno pede o reembolso do pedido 1, da Ana404404
Carla, do suporte, pede o reembolso do pedido 2403403
Davi, do financeiro, pede o reembolso do pedido 2200200
Eva, com o papel "estagiario", que não está na tabela, lê o pedido 1404404

Repare em duas linhas. Ana lendo o pedido 2 e lendo o 999 recebem a mesma resposta: de fora, não dá para saber se o pedido do Bruno existe. E o Bruno, que não vê o pedido da Ana, recebe 404 também no reembolso, e não 403: a conferência de leitura vem antes da conferência da ação.

O cabeçalho X-Usuario só existe para o exemplo rodar sozinho. Num sistema real, o usuário vem da sessão ou do token validado, como mostra o tópico de autenticação, e nunca de um valor que o cliente escolhe.

O que muda de uma linguagem para outra

A tabela de alcancesPapel ou ação ausente
TypeScriptRecord com Partial, e tipos para as ações?. devolve undefined, que nega
PythonDicionários.get(..., {}) devolve vazio, que nega
Gomap[string]map[string]stringLer um mapa ausente devolve "", que nega
JavaMap.of, imutávelgetOrDefault com mapa vazio
C#Dictionary e um switch de expressãoGetValueOrDefault e o caso _

Em todas, negar é o caminho que não exige código: o valor ausente cai no return false. Com frameworks, a mesma função vira uma política registrada: IAuthorizationHandler no ASP.NET Core, @PreAuthorize com um bean no Spring, uma dependência no FastAPI. O importante é a política continuar num só lugar.

A mesma política num motor de políticas

A política em Rego, a linguagem do OPA (versão 1.22 no teste):

package pedidos

default permitir := false

alcances := {
	"cliente": {"pedido:ler": "proprios"},
	"suporte": {"pedido:ler": "todos"},
	"financeiro": {"pedido:ler": "todos", "pedido:reembolsar": "todos"},
}

permitir if alcances[input.usuario.papel][input.acao] == "todos"

permitir if {
	alcances[input.usuario.papel][input.acao] == "proprios"
	input.pedido.cliente_id == input.usuario.id
}

Avaliada com opa eval para os casos principais:

UsuárioAçãoDono do pedidoResultado
Ana, cliente 1lercliente 1permitido
Ana, cliente 1lercliente 2negado
Carla, suportelercliente 2permitido
Ana, cliente 1reembolsarcliente 1negado
Carla, suportereembolsarcliente 2negado
Davi, financeiroreembolsarcliente 2permitido
Eva, estagiariolercliente 1negado

Os mesmos resultados das cinco linguagens. A diferença é onde a regra mora: num arquivo que várias aplicações consultam, com testes e revisão próprios. O preço é que cada pergunta precisa levar os dados do pedido, aqui o cliente_id, e alguém precisa buscá-los antes.

Isolar empresas com segurança por linha

A configuração testada no nível Conceito, com as correções que o teste exigiu:

-- A aplicação conecta com este usuário, que não é dono das tabelas
CREATE ROLE app LOGIN PASSWORD '...';

ALTER TABLE pedidos ENABLE ROW LEVEL SECURITY;
ALTER TABLE pedidos FORCE ROW LEVEL SECURITY; -- vale também para o dono

CREATE POLICY por_empresa ON pedidos
  USING (empresa_id = nullif(current_setting('app.empresa_id', true), '')::int);

GRANT SELECT ON pedidos TO app;

E, na aplicação, a empresa definida dentro da transação de cada pedido:

const conexao = await pool.connect();
try {
  await conexao.query("BEGIN");
  // true = só nesta transação; some no COMMIT, sem vazar para o próximo pedido
  await conexao.query("SELECT set_config('app.empresa_id', $1, true)", [String(usuario.empresaId)]);
  const { rows } = await conexao.query("SELECT * FROM pedidos"); // o banco filtra
  await conexao.query("COMMIT");
  return rows;
} catch (erro) {
  await conexao.query("ROLLBACK");
  throw erro;
} finally {
  conexao.release();
}

No teste, com um pool de uma conexão, essa forma devolveu as 100 linhas da empresa e, no pedido seguinte sem empresa definida, 0. Com SET no lugar de set_config(..., true), o pedido seguinte recebeu as 100 linhas da empresa anterior.

Testar a autorização

A tabela de resultados acima é o formato de teste que mais pega falhas de autorização: uma matriz de usuários, ações e recursos, com o status esperado em cada célula. Ela precisa de pelo menos:

  • dois usuários com o mesmo papel, para pegar a falta de conferência do dono;
  • um usuário de cada papel;
  • um papel que não está na política, para conferir a negação por padrão;
  • todas as rotas que recebem um identificador: detalhe, edição, exclusão e as ações, e não só a listagem.

Um teste extra que vale a pena: percorrer todas as rotas registradas na aplicação e falhar se alguma não passar pela função de autorização.

Lista de revisão

Modelo

  • As permissões são conferidas pela ação (pedido:reembolsar), e não pelo nome do papel?
  • O alcance de cada permissão (os próprios, todos) está explícito?
  • O que não está na política é negado?

Conferência

  • Toda rota que recebe um identificador confere se o usuário pode usar aquele recurso?
  • A conferência acontece depois de carregar o recurso, ou dentro da consulta?
  • As listagens filtram pelo que o usuário pode ver, no banco?
  • O usuário vem da sessão ou do token, e nunca de um parâmetro enviado pelo cliente?
  • Recursos invisíveis devolvem 404, e ações proibidas em recursos visíveis, 403?

Política

  • A regra mora num só lugar: uma função, uma biblioteca ou um motor?
  • Papéis e permissões que mudam são consultados, e não presos no token?

Empresas

  • Há um isolamento que não depende de cada consulta lembrar o filtro?
  • Com segurança por linha: a aplicação não é dona das tabelas, usa SET LOCAL e a política trata o valor ausente?

Testes

  • Há uma matriz de usuários, ações e recursos, com dois usuários do mesmo papel?
  • Um teste garante que toda rota passa pela autorização?

Prompts para aprofundar

Ao abrir no ChatGPT ou no Claude, o prompt é enviado ao serviço escolhido pela URL e também fica copiado na sua área de transferência.

  • Implementar a autorização por objeto

    Quero implementar a autorização na minha API, com papéis e conferência do dono de cada recurso.
    
    Pergunte qual linguagem e framework eu uso e como os usuários são autenticados.
    
    Vamos em etapas:
    1. A tabela de alcances por papel e ação, negando o que não estiver nela.
    2. A função central que decide, recebendo o usuário, a ação e o recurso.
    3. As rotas que recebem um identificador, com 404 para o que o usuário não vê e 403 para a ação proibida.
    4. As listagens, filtradas no banco pelo que o usuário pode ver.
    5. A matriz de testes com dois usuários do mesmo papel, um de cada papel e um papel desconhecido.
    
    Em cada etapa, me passe os comandos para testar.
  • Configurar a segurança por linha no Postgres

    Quero isolar os dados de cada empresa com a segurança por linha do Postgres.
    
    Pergunte qual linguagem, driver e pool de conexões eu uso, e com qual usuário a aplicação conecta hoje.
    
    Explique, com código:
    1. O usuário da aplicação, que não é dono das tabelas.
    2. ENABLE e FORCE ROW LEVEL SECURITY e a política, tratando o valor ausente com nullif.
    3. A definição da empresa com set_config dentro da transação de cada pedido.
    4. Um teste que confere que uma consulta sem empresa definida devolve zero linhas.
    5. Um teste que confere que o valor não vaza para o pedido seguinte na mesma conexão.
  • Revisar a autorização de uma API

    Vou colar abaixo o código das rotas de uma API e da parte que decide permissões. Revise a autorização.
    
    Verifique, e para cada item diga se está correto, incorreto ou ausente, citando o trecho:
    1. Toda rota que recebe um identificador confere se o usuário pode usar aquele recurso?
    2. O usuário vem da sessão ou do token, e não de um parâmetro do cliente?
    3. As permissões são conferidas por ação, num só lugar, negando por padrão?
    4. As listagens filtram no banco?
    5. Recursos invisíveis devolvem 404, e ações proibidas, 403?
    6. Há isolamento entre empresas que não depende de cada consulta?
    7. Há testes com dois usuários do mesmo papel?
    
    Para cada problema, descreva como explorá-lo e mostre a correção.
    
    Código:
    [cole aqui]