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,actioneenvcomo 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
denyclaras (ex.: tenant diferente, recurso arquivado, conta suspensa). - Decisão padronizada: retorne
ALLOWouDENY(e opcionalmente umreason) 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):
- Ouça o áudio com a tela desligada
- Ganhe Certificado após a conclusão
- + de 5000 cursos para você explorar!
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
| Caso | Mesmo tenant? | Suspenso? | É dono? | É suporte? | Status | Decisão |
|---|---|---|---|---|---|---|
| 1 | Sim | Não | Sim | Não | CLOSED | ALLOW (owner) |
| 2 | Sim | Não | Não | Sim | OPEN | ALLOW (support_open_order) |
| 3 | Sim | Não | Não | Sim | CLOSED | DENY (no_matching_allow) |
| 4 | Não | Não | Sim | Sim | OPEN | DENY (cross_tenant) |
| 5 | Sim | Sim | Sim | Sim | OPEN | DENY (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.