Autorização por atributos e regras (ABAC) no back-end

Capítulo 13

Tempo estimado de leitura: 10 minutos

+ Exercício

O que é ABAC (Attribute-Based Access Control)

ABAC é um modelo de autorização em que a decisão de permitir ou negar uma ação é tomada avaliando atributos do contexto, em vez de depender apenas de papéis fixos. A regra (policy) recebe um conjunto de atributos e responde com uma decisão. Na prática, ABAC é útil quando as permissões dependem de condições como propriedade do recurso, status, tenant, horário, localização, risco ou tipo de operação.

Quatro categorias de atributos

  • Sujeito (subject): quem está tentando fazer algo. Ex.: user.id, user.roles, user.tenantId, user.department, user.supportLevel.
  • Recurso (resource): o objeto alvo. Ex.: order.id, order.ownerId, order.status, order.tenantId, order.amount.
  • Ação (action): o que se quer fazer. Ex.: order:read, order:update, order:refund.
  • Ambiente (environment): contexto externo. Ex.: time, ip, geo, deviceTrust, request.tenantId, mfaLevel.

Uma regra ABAC típica parece com: “Permitir order:read se user.tenantId == order.tenantId e (user.id == order.ownerId ou (user.roles contém support e order.status == OPEN))”.

Como escrever regras previsíveis e testáveis

Princípios práticos

  • Regras puras (sem I/O): a função de decisão deve depender apenas dos atributos recebidos. Buscas no banco e chamadas externas devem ocorrer antes, para montar o contexto.
  • Entradas explícitas: passe subject, resource, action e env como objetos bem definidos. Evite ler variáveis globais.
  • Sem “mágica”: prefira comparações diretas e nomes claros. Ex.: isOwner, isSupport, isSameTenant.
  • Negação explícita: trate casos de bloqueio com regras de deny claras (ex.: tenant diferente, recurso arquivado, conta suspensa).
  • Decisão padronizada: retorne ALLOW ou DENY (e opcionalmente um reason) para facilitar logs e testes.

Modelo de decisão e composição

Em ABAC, você normalmente tem várias regras e precisa combiná-las. Dois padrões comuns:

  • deny-overrides: se qualquer regra retornar DENY, a decisão final é DENY, mesmo que outras permitam. Útil para “guardrails” (ex.: tenant mismatch, recurso bloqueado).
  • allow-overrides: se qualquer regra retornar ALLOW, a decisão final é ALLOW, mesmo que outras neguem. Use com cuidado; pode ser útil para exceções muito controladas (ex.: break-glass, auditoria).

Na maioria dos sistemas, deny-overrides é mais seguro como padrão, porque evita que uma permissão “genérica” contorne uma restrição crítica.

Passo a passo: implementando ABAC no back-end

Passo 1: Defina o contrato do contexto

Crie um tipo/estrutura para o contexto de autorização. Exemplo em TypeScript (adaptável para outras linguagens):

Continue em nosso aplicativo e ...
  • Ouça o áudio com a tela desligada
  • Ganhe Certificado após a conclusão
  • + de 5000 cursos para você explorar!
ou continue lendo abaixo...
Download App

Baixar o aplicativo

type Action = 'order:read' | 'order:update' | 'order:refund';type Decision = { effect: 'ALLOW' | 'DENY'; reason?: string };type Subject = { id: string; tenantId: string; roles: string[]; suspended?: boolean };type ResourceOrder = { id: string; tenantId: string; ownerId: string; status: 'OPEN' | 'CLOSED' | 'CANCELED' };type Env = { now: Date; ip?: string; requestTenantId?: string };type AuthzContext = { subject: Subject; action: Action; resource: ResourceOrder; env: Env };

Passo 2: Escreva regras pequenas e nomeadas

Regras pequenas são mais fáceis de testar e combinar. Exemplo com regras para leitura de pedido:

function denyIfSuspended(ctx: AuthzContext): Decision | null {  if (ctx.subject.suspended) return { effect: 'DENY', reason: 'subject_suspended' };  return null;}function denyIfCrossTenant(ctx: AuthzContext): Decision | null {  if (ctx.subject.tenantId !== ctx.resource.tenantId) {    return { effect: 'DENY', reason: 'cross_tenant' };  }  return null;}function allowIfOwner(ctx: AuthzContext): Decision | null {  if (ctx.action === 'order:read' && ctx.subject.id === ctx.resource.ownerId) {    return { effect: 'ALLOW', reason: 'owner' };  }  return null;}function allowIfSupportAndOpen(ctx: AuthzContext): Decision | null {  const isSupport = ctx.subject.roles.includes('support');  if (ctx.action === 'order:read' && isSupport && ctx.resource.status === 'OPEN') {    return { effect: 'ALLOW', reason: 'support_open_order' };  }  return null;}

Note que as regras retornam null quando não se aplicam. Isso ajuda a compor sem “negar por padrão” em cada regra individual.

Passo 3: Combine regras com um algoritmo (deny-overrides)

type Rule = (ctx: AuthzContext) => Decision | null;function evaluateDenyOverrides(ctx: AuthzContext, rules: Rule[]): Decision {  // 1) qualquer DENY vence  for (const rule of rules) {    const res = rule(ctx);    if (res?.effect === 'DENY') return res;  }  // 2) se não houve DENY, qualquer ALLOW permite  for (const rule of rules) {    const res = rule(ctx);    if (res?.effect === 'ALLOW') return res;  }  // 3) default deny  return { effect: 'DENY', reason: 'no_matching_allow' };}

Você pode manter uma lista de regras por ação/recurso, por exemplo:

const orderReadRules: Rule[] = [  denyIfSuspended,  denyIfCrossTenant,  allowIfOwner,  allowIfSupportAndOpen];

Passo 4: Aplique ABAC no endpoint (camada HTTP)

No endpoint, o fluxo típico é: (1) carregar o recurso, (2) montar o contexto, (3) avaliar, (4) responder. Exemplo:

// GET /orders/:idasync function getOrder(req, res) {  const subject = req.auth.user; // já autenticado  const order = await db.orders.findById(req.params.id);  if (!order) return res.status(404).json({ error: 'not_found' });  const ctx: AuthzContext = {    subject: {      id: subject.id,      tenantId: subject.tenantId,      roles: subject.roles,      suspended: subject.suspended    },    action: 'order:read',    resource: {      id: order.id,      tenantId: order.tenantId,      ownerId: order.ownerId,      status: order.status    },    env: { now: new Date(), requestTenantId: req.headers['x-tenant-id'] }  };  const decision = evaluateDenyOverrides(ctx, orderReadRules);  if (decision.effect === 'DENY') {    return res.status(403).json({ error: 'forbidden', reason: decision.reason });  }  return res.json(order);}

Esse padrão deixa a autorização explícita e auditável. Também facilita testes unitários porque a lógica está em funções puras.

ABAC em consultas ao banco: evitando retornar dados indevidos

Aplicar ABAC apenas no endpoint pode ser insuficiente quando você lista dados (ex.: GET /orders) ou quando o endpoint retorna dados agregados. O ideal é empurrar as restrições para a consulta, garantindo que o banco só devolva o que o usuário pode ver.

Estratégia 1: Filtro por tenant e propriedade (mais comum)

Exemplo: listar pedidos visíveis para um usuário. Regra: “usuário vê pedidos do próprio tenant e (é dono ou é suporte e pedido está aberto)”. Em SQL:

-- subject: :userId, :tenantId, :isSupportSELECT o.*FROM orders oWHERE o.tenant_id = :tenantId  AND (    o.owner_id = :userId    OR (:isSupport = true AND o.status = 'OPEN')  );

Isso evita o erro clássico de buscar todos os pedidos do tenant e filtrar em memória (o que pode vazar dados via logs, paginação incorreta, cache ou bugs).

Estratégia 2: “Policy as filter” (construir predicados)

Quando as regras são expressáveis como predicados de banco, você pode manter uma função que gera o WHERE (ou equivalente) a partir do sujeito e da ação. Exemplo conceitual:

function ordersVisibilityWhere(subject): any {  const base = { tenantId: subject.tenantId };  const isSupport = subject.roles.includes('support');  if (isSupport) {    return {      ...base,      OR: [        { ownerId: subject.id },        { status: 'OPEN' }      ]    };  }  return { ...base, ownerId: subject.id };}

O ponto-chave é: a mesma regra que decide “pode ver?” deve ter uma forma equivalente de restringir “quais linhas podem aparecer?”. Quando não for possível expressar no banco (ex.: regras dependentes de serviço externo), considere materializar atributos (ex.: risk_score) ou usar uma abordagem híbrida (pré-filtrar no banco e validar no código).

Cuidados comuns

  • Paginação: sempre pagine após aplicar o filtro de autorização, para não inferir existência de dados indevidos.
  • Campos sensíveis: ABAC pode decidir não apenas “ver o recurso”, mas “ver campos”. Ex.: suporte vê pedido, mas não vê dados de pagamento. Isso pode ser implementado com projeções condicionais.
  • Joins: ao juntar tabelas, aplique o filtro no recurso principal e nos relacionados quando necessário (ex.: itens do pedido só se o pedido for visível).

Exemplo completo de regra: “dono OU suporte e pedido aberto”

Regra em linguagem natural

  • Pré-condição: sujeito e pedido devem ser do mesmo tenant.
  • Permitir leitura se: (a) sujeito é dono do pedido, ou (b) sujeito tem papel suporte e o pedido está aberto.
  • Negar se: sujeito suspenso, tenant diferente, ou nenhuma condição de allow for atendida.

Tabela de casos para tornar a regra previsível

CasoMesmo tenant?Suspenso?É dono?É suporte?StatusDecisão
1SimNãoSimNãoCLOSEDALLOW (owner)
2SimNãoNãoSimOPENALLOW (support_open_order)
3SimNãoNãoSimCLOSEDDENY (no_matching_allow)
4NãoNãoSimSimOPENDENY (cross_tenant)
5SimSimSimSimOPENDENY (subject_suspended)

Como testar regras ABAC

Como as regras são funções puras, você pode testá-las com testes unitários rápidos, cobrindo combinações de atributos. A ideia é testar: (1) denies prioritários, (2) allows esperados, (3) default deny.

Passo a passo para testes unitários

  • 1) Crie builders para montar contexto com defaults.
  • 2) Escreva testes por cenário (tabela de casos acima).
  • 3) Asserte effect e reason para garantir previsibilidade.
// Exemplo com Jest (conceitual)const baseCtx = (): AuthzContext => ({  subject: { id: 'u1', tenantId: 't1', roles: [] },  action: 'order:read',  resource: { id: 'o1', tenantId: 't1', ownerId: 'u1', status: 'CLOSED' },  env: { now: new Date('2026-01-01T10:00:00Z') }});test('ALLOW quando usuário é dono', () => {  const ctx = baseCtx();  ctx.subject.id = 'u1';  ctx.resource.ownerId = 'u1';  const decision = evaluateDenyOverrides(ctx, orderReadRules);  expect(decision.effect).toBe('ALLOW');  expect(decision.reason).toBe('owner');});test('ALLOW quando suporte e pedido OPEN', () => {  const ctx = baseCtx();  ctx.subject.id = 'u2';  ctx.resource.ownerId = 'u1';  ctx.subject.roles = ['support'];  ctx.resource.status = 'OPEN';  const decision = evaluateDenyOverrides(ctx, orderReadRules);  expect(decision.effect).toBe('ALLOW');  expect(decision.reason).toBe('support_open_order');});test('DENY quando suporte mas pedido CLOSED', () => {  const ctx = baseCtx();  ctx.subject.id = 'u2';  ctx.subject.roles = ['support'];  ctx.resource.ownerId = 'u1';  ctx.resource.status = 'CLOSED';  const decision = evaluateDenyOverrides(ctx, orderReadRules);  expect(decision.effect).toBe('DENY');  expect(decision.reason).toBe('no_matching_allow');});test('DENY cross-tenant tem prioridade (deny-overrides)', () => {  const ctx = baseCtx();  ctx.subject.roles = ['support'];  ctx.resource.status = 'OPEN';  ctx.subject.tenantId = 't2';  ctx.resource.tenantId = 't1';  const decision = evaluateDenyOverrides(ctx, orderReadRules);  expect(decision.effect).toBe('DENY');  expect(decision.reason).toBe('cross_tenant');});test('DENY se sujeito suspenso (deny-overrides)', () => {  const ctx = baseCtx();  ctx.subject.suspended = true;  const decision = evaluateDenyOverrides(ctx, orderReadRules);  expect(decision.effect).toBe('DENY');  expect(decision.reason).toBe('subject_suspended');});

Testes de integração: endpoint + consulta

Além do unitário, valide que a consulta ao banco aplica o filtro correto. Um padrão é criar dados de dois tenants e garantir que o endpoint de listagem nunca retorna pedidos do outro tenant, mesmo que existam e mesmo que o usuário seja suporte.

// Pseudocódigo de integraçãoArrange:  - Tenant t1: order A (owner u1, OPEN), order B (owner u3, CLOSED)  - Tenant t2: order C (owner u2, OPEN)  - Usuário u2 do tenant t1 com role supportAct:  - GET /orders (como u2)Assert:  - retorna order A (OPEN, t1)  - não retorna order B (CLOSED, t1)  - não retorna order C (t2)

Combinando regras: quando usar allow-overrides

Se você precisar de exceções fortes (por exemplo, um modo “break-glass” para incidentes), pode usar allow-overrides em um conjunto específico de regras, mantendo deny-overrides como padrão global. Exemplo: permitir leitura para um usuário auditor apenas quando env.mfaLevel é alto e a requisição vem de uma rede corporativa, mas ainda negar se cross-tenant.

function evaluateAllowOverrides(ctx: AuthzContext, rules: Rule[]): Decision {  // 1) qualquer ALLOW vence  for (const rule of rules) {    const res = rule(ctx);    if (res?.effect === 'ALLOW') return res;  }  // 2) se não houve ALLOW, qualquer DENY nega  for (const rule of rules) {    const res = rule(ctx);    if (res?.effect === 'DENY') return res;  }  return { effect: 'DENY', reason: 'no_matching_allow' };}

Mesmo usando allow-overrides em um bloco, é comum manter “guardrails” fora desse bloco (ex.: tenant mismatch) e aplicá-los antes, para evitar bypass acidental.

Agora responda o exercício sobre o conteúdo:

Em uma autorização ABAC com composição deny-overrides, qual é o comportamento esperado quando uma regra retorna DENY e outra regra retorna ALLOW para o mesmo contexto?

Você acertou! Parabéns, agora siga para a próxima página

Você errou! Tente novamente.

No padrão deny-overrides, qualquer regra que resulte em DENY encerra a avaliação e define a decisão final como negação, funcionando como “guardrail”. Só na ausência de DENY um ALLOW pode permitir.

Próximo capitúlo

Aplicando autorização corretamente em camadas: controller, serviço e banco de dados

Arrow Right Icon
Capa do Ebook gratuito Autenticação e Autorização no Back-end: Sessões, JWT e Boas Práticas
72%

Autenticação e Autorização no Back-end: Sessões, JWT e Boas Práticas

Novo curso

18 páginas

Baixe o app para ganhar Certificação grátis e ouvir os cursos em background, mesmo com a tela desligada.