Authorization

RBAC

O RBAC responde a uma pergunta por requisição: este principal tem permissão para fazer isso neste recurso? As roles agrupam permissões, e uma única verificação can() mantém a resposta consistente em todo o sistema.

intermediate15 min readUpdated 16 de set. de 2026
authorize.ts
ts
// authorize.ts
export type Action = "read" | "create" | "update" | "delete" | "manage";

export function can(
  user: Principal,
  action: Action,
  resource?: Resource
): boolean {
  const permission = `${resource?.type ?? "global"}:${action}`;

  // 1. Does any role grant this action at all?
  if (!user.permissions.has(permission)) return false;

  // 2. Tenant isolation: never cross the boundary.
  if (resource && resource.tenantId !== user.tenantId) return false;

  // 3. Writes on a resource require ownership or manage.
  if (resource && action !== "read" && resource.ownerId !== user.id) {
    return user.permissions.has(`${resource.type}:manage`);
  }

  return true;
}
Modelo
Usuários para roles para permissões
Chamada principal
can(user, action, resource)
Padrão
Negar, a menos que explicitamente permitido
Armazenamento
Tabelas de junção em SQL
Granularidade
Nível de rota e recurso
Falha comum
IDOR e falta de verificações de propriedade
Próximo passo
ABAC e policy engines
Status de falha
403 Forbidden

Por que importa

Por que o RBAC se sustenta em produção

Permissões, não strings de role

Conceda permissões nomeadas como posts:update e verifique-as. Os nomes das roles tornam-se rótulos para humanos em vez de strings espalhadas por seus handlers.

Roles agrupam permissões

Uma role é um conjunto reutilizável de concessões. Adicionar um moderador significa compor permissões existentes em vez de editar cada rota.

Um único lugar para decidir

Um único helper can() significa que a regra de quem pode fazer o quê reside em um único arquivo, evitando que os guards de rota e as verificações de recurso divirjam.

O panorama completo

As três camadas de uma verificação

Identifique o principal, expanda suas roles em permissões e, então, teste uma ação contra um recurso.

Principal

Identificar

A autenticação prova quem está chamando. O RBAC começa apenas depois que a requisição possui um usuário e um tenant confiáveis vinculados a ela.

Roles

Agrupar

As roles agrupam permissões em conjuntos baseados em funções, como viewer, editor ou admin, e os usuários recebem uma ou mais roles.

Permissões

Decidir

A concessão atômica de uma ação em um recurso. Cada verificação se resume a testar uma permissão contra um alvo.

HTML5 de uma olhada

As peças que você irá construir

Permissões

posts:update, billing:read, users:manage — os átomos do modelo.

Roles

Conjuntos nomeados como admin, editor e viewer que agrupam permissões.

Matriz

Uma grade legível de roles versus permissões, a fonte da verdade para revisão.

Multi-tenant

Um tenant id restringe cada concessão para que um cliente não possa ver outro.

Hierarquia

Roles seniores herdam as permissões de roles juniores em vez de duplicá-las.

Auditoria

Registre cada permissão e negação para que você possa explicar uma decisão a posteriori.

Modelo de dados

O schema por trás da verificação

Permissões são os átomos, roles são conjuntos nomeados, e duas tabelas de junção conectam usuários a roles e roles a permissões.

Roles, permissões e as junçõesPostgreSQL schema
  • userstableO principal autenticado, com uma coluna tenant_id
  • rolestableUm conjunto nomeado como admin, editor ou viewer
  • permissionstableUma única ação em um recurso, por exemplo posts:update
  • role_permissionsjoin tableMuitos-para-muitos entre roles e permissões
  • user_rolesjoin tableMuitos-para-muitos entre usuários e roles, escopado por tenant
  • scopetextTenant id opcional que restringe uma concessão a um único cliente

Permissões são os átomos, roles são conjuntos nomeados, e duas tabelas de junção conectam usuários a roles e roles a permissões.

Fluxo

Da requisição à permissão

A autenticação acontece uma vez; a autorização roda em cada requisição, em ordem, e falha por padrão (fail closed).

  1. 1

    Autenticar o principal

    Resolva a sessão ou token em um user id, um tenant id e um conjunto de roles.

  2. 2

    Carregar roles

    Busque as atribuições de role do usuário no banco de dados ou em um cache de curta duração.

  3. 3

    Expandir para permissões

    Achate cada role em suas permissões, incluindo as herdadas, para construir um único conjunto.

  4. 4

    Verificar a ação no recurso

    Chame can(user, action, resource) para o alvo específico, não apenas para a rota.

  5. 5

    Permitir ou retornar 403

    Prossiga para o handler quando permitido; caso contrário, responda com 403 e sem efeitos colaterais.

  6. 6

    Registrar a decisão

    Registre o principal, a ação, o recurso e o resultado para auditoria e depuração.

O guia completo

RBAC: Tudo que voce precisa saber

Autenticação não é autorização

As duas palavras são usadas como sinônimos, mas não são a mesma coisa. A Autenticação responde a “quem é você?” e resulta em um principal confiável: um ID de usuário, um tenant e uma forma de verificar que a requisição veio deles. A Autorização responde a “o que você tem permissão para fazer?” e é executada após a autenticação, em cada requisição, contra um alvo específico.

Confundi-las é a origem de alguns dos bugs de segurança mais comuns na web. Um login perfeitamente implementado não diz nada sobre se o usuário logado deve ser capaz de ler a fatura de outro cliente. Uma sessão válida prova a identidade; ela não concede permissões. Cada endpoint ainda precisa decidir, explicitamente, se este principal pode realizar esta ação neste recurso.

Este guia é sobre a segunda pergunta. Ele assume que você já resolveu a primeira — possuindo uma sessão ou token que identifica um usuário — e foca no modelo e nas verificações que transformam esse usuário em uma permissão ou negação. Se a autenticação ainda for um ponto em aberto, leia primeiro o guia de session auth.

O modelo RBAC

O Role-Based Access Control (Controle de Acesso Baseado em Papéis) é o modelo de autorização mais utilizado porque reflete a maneira como as organizações realmente funcionam. Existem três conceitos:

  • Principals são as entidades que agem: usuários, contas de serviço, API keys. Cada principal pertence a um tenant.
  • Roles são pacotes nomeados de capacidades: viewer, editor, admin, billing.
  • Permissions são os átomos: uma única ação em um único tipo de recurso, escrita como posts:update ou billing:read.

Um principal recebe um ou mais roles, e cada role é mapeado para um conjunto de permissions. O conjunto de permissões efetivas para uma requisição é a união de todas as permissions de todos os roles do principal. Uma verificação de autorização, então, resume-se a um único teste de pertinência ao conjunto, além de quaisquer regras específicas do recurso.

A elegância disso é que as permissions são estáveis, enquanto os roles são fluidos. Você pode adicionar um role moderator, mover posts:delete para dentro dele e nenhum handler precisará ser alterado. A regra de “quem pode deletar um post” reside nos dados, e não em uma cadeia de instruções if espalhadas por toda a base de código.

Usuários, papéis e permissões

A estrutura relacional consiste em quatro tabelas e duas junções many-to-many. Vale a pena internalizar esse conceito, pois quase toda implementação de RBAC é uma variação disso.

CREATE TABLE permissions (
  id     bigserial PRIMARY KEY,
  action text NOT NULL UNIQUE
);

CREATE TABLE roles (
  id   bigserial PRIMARY KEY,
  name text NOT NULL UNIQUE
);

CREATE TABLE role_permissions (
  role_id       bigint NOT NULL REFERENCES roles (id) ON DELETE CASCADE,
  permission_id bigint NOT NULL REFERENCES permissions (id) ON DELETE CASCADE,
  PRIMARY KEY (role_id, permission_id)
);

CREATE TABLE user_roles (
  user_id bigint NOT NULL REFERENCES users (id) ON DELETE CASCADE,
  role_id bigint NOT NULL REFERENCES roles (id) ON DELETE CASCADE,
  PRIMARY KEY (user_id, role_id)
);

Fazer o seeding desses dados em uma migration é importante. Permissões e mapeamentos de papéis fazem parte do contrato da sua aplicação, e não algo que um administrador improvisa em produção. Mantenha o seed no controle de versão para que todos os ambientes concordem com o que editor significa, e trate qualquer alteração nele com o mesmo cuidado que trataria uma mudança de schema.

Carregar as permissões efetivas requer apenas uma query:

SELECT DISTINCT p.action
FROM user_roles ur
JOIN role_permissions rp ON rp.role_id = ur.role_id
JOIN permissions p       ON p.id = rp.permission_id
WHERE ur.user_id = $1;

Faça o cache do resultado por requisição. Carregá-lo uma única vez e anexá-lo ao req.user evita a repetição da query a cada verificação, e um cache com TTL curto indexado pelo ID do usuário mantém o banco de dados fora do caminho crítico (hot path).

Hierarquias de cargos

Organizações reais possuem níveis. Um cargo sênior geralmente faz tudo o que um cargo júnior faz, e mais. Modelar isso copiando cada permissão para cada cargo é uma armadilha de manutenção: altere posts:read e você precisará lembrar de todos os cinco cargos que a incluem.

Em vez disso, permita que os cargos herdem. Adicione uma tabela de junção parent_role_id ou role_inherits e expanda a hierarquia ao construir o conjunto de permissões. Um formato comum é viewer → editor → admin, onde cada nível adiciona novas capacidades.

CREATE TABLE role_inherits (
  role_id   bigint NOT NULL REFERENCES roles (id) ON DELETE CASCADE,
  parent_id bigint NOT NULL REFERENCES roles (id) ON DELETE CASCADE,
  PRIMARY KEY (role_id, parent_id)
);

A expansão é feita por meio de uma consulta recursiva ou, de forma mais simples, por uma closure table pré-computada que armazena cada par de ancestrais. A closure table troca um pouco de armazenamento por uma busca trivial e amigável a índices, o que geralmente é a escolha certa, pois as verificações de permissão são muito mais frequentes do que as edições de cargos.

Proteja-se contra ciclos. Um cargo que herda de si mesmo, direta ou indiretamente, fará com que a expansão entre em loop infinito. Valide na escrita: rejeite qualquer pai que possa criar um ciclo. Mantenha as hierarquias rasas — três ou quatro níveis são suficientes — porque árvores profundas são difíceis de serem compreendidas por humanos e fáceis de implementar incorretamente.

Por que permissões são melhores que strings de roles

O erro mais comum de RBAC é não construir um RBAC de fato. É espalhar verificações de roles por toda a base de código:

if (req.user.role !== "admin") return res.sendStatus(403);

Isso parece inofensivo e é uma decisão de política incorporada em um handler. Ela diz que apenas admin pode fazer isso, o que pode ter sido verdade quando foi escrito. Quando o produto adiciona uma role de support que também precisa de acesso, alguém terá que encontrar cada uma dessas verificações e editá-las — e eles vão esquecer alguma. A regra agora está espalhada por dezenas de arquivos sem uma única fonte de verdade.

Verificar uma permissão inverte a dependência. O handler pergunta “este principal pode atualizar um post?” e a resposta vem dos dados:

if (!can(req.user, "update", post)) return res.sendStatus(403);

Agora, conceder a support a capacidade de atualizar posts é apenas uma linha em role_permissions, e não uma alteração de código. A política torna-se revisável, testável e consistente. O handler descreve a intenção em vez de codificar uma role específica.

A regra de ouro: roles são para humanos, permissões são para o código. Uma UI pode dizer “Admins podem gerenciar o faturamento”, mas a verificação por baixo deve solicitar billing:manage.

O pipeline de autorização

Cada requisição segue a mesma sequência, e cada etapa tem exatamente uma função.

  1. Autenticar. Resolve a sessão ou o token em um principal: um user id, um tenant id e uma lista de roles. Se isso falhar, a requisição é anônima e as rotas protegidas retornam 401.
  2. Carregar roles. Busca as atribuições de roles, geralmente do banco de dados ou de um cache populado no login.
  3. Expandir para permissões. Achata as roles, incluindo as herdadas, em um conjunto único de strings de permissão.
  4. Verificar a ação no recurso. Chama can(user, action, resource) para o alvo concreto, após carregá-lo.
  5. Permitir ou negar. Em caso de sucesso, executa o handler. Em caso de falha, retorna 403 sem efeitos colaterais.
  6. Logar a decisão. Registra o principal, a ação, o recurso e o resultado.

A ordem é importante por dois motivos. A autenticação deve vir primeiro porque todo o restante depende de um principal confiável. As verificações de recurso devem vir após o carregamento do recurso, pois você não pode avaliar a propriedade de um registro que ainda não foi buscado.

O “fail closed” (falha fechada) é inegociável. Se o carregamento de roles lançar um erro ou o cache estiver inacessível, o padrão é negar. Um sistema de autorização que retorna “permitir” em caso de erro é pior do que não ter sistema nenhum, pois gera uma falsa confiança.

Criando um helper can()

Centralizar a decisão em uma única função é o que impede que os route guards e as verificações de recursos fiquem dessincronizados. A assinatura é simples: um principal, uma ação e um recurso opcional.

export type Action = "read" | "create" | "update" | "delete" | "manage";

export function can(
  user: Principal,
  action: Action,
  resource?: Resource
): boolean {
  const permission = `${resource?.type ?? "global"}:${action}`;
  if (!user.permissions.has(permission)) return false;

  if (resource && resource.tenantId !== user.tenantId) return false;

  if (resource && action !== "read" && resource.ownerId !== user.id) {
    return user.permissions.has(`${resource.type}:manage`);
  }

  return true;
}

Três regras estão codificadas aqui, em ordem de importância. O conjunto de permissões é o filtro inicial: se nenhum papel (role) concede a ação, pare. O isolamento de tenant vem em seguida e é absoluto — um principal nunca deve agir fora de seu tenant, independentemente das permissões. Por fim, gravações em um recurso exigem a propriedade do mesmo ou uma concessão explícita de manage, que é o que permite que um editor edite seus próprios rascunhos enquanto um admin edita qualquer coisa.

O helper é puro. Ele recebe dados simples e retorna um booleano, sem chamadas ao banco de dados internamente. Isso torna trivial a criação de testes unitários com uma matriz de principals, ações e recursos, e significa que a mesma função pode ser executada em um route guard, em um serviço, em um background job ou em um componente de UI que decide se deve renderizar um botão.

Para a UI, exponha a mesma função ao cliente através de um endpoint ou de um objeto de permissões renderizado no servidor. O cliente deve ocultar controles que o usuário não pode usar, mas o servidor ainda deve aplicar cada verificação, pois um botão oculto não é um controle de segurança.

Aplicando no nível de rota

Um route guard é a primeira linha de defesa: ele decide se esse tipo de ação está disponível para esse principal. Ele é executado antes do handler e de qualquer operação no banco de dados, o que o torna uma maneira barata de rejeitar negações óbvias.

export function requirePermission(
  action: Action,
  type: string
): RequestHandler {
  return (req, res, next) => {
    if (!req.user) return res.status(401).json({ error: "unauthorized" });

    if (!can(req.user, action, { type, ownerId: req.user.id, tenantId: req.user.tenantId })) {
      return res.status(403).json({ error: "forbidden" });
    }
    next();
  };
}

Monte-o no router para que a regra fique visível onde as rotas são definidas:

router.get("/posts", requirePermission("read", "post"), listPosts);
router.post("/posts", requirePermission("create", "post"), createPost);

Retornar 401 para um principal ausente e 403 para um principal negado é fundamental. 401 significa “Eu não sei quem você é”; 403 significa “Eu sei quem você é e você não pode fazer isso”. Clientes e sistemas de monitoramento os tratam de forma diferente, e confundi-los torna a depuração mais difícil.

Route guards são necessários, mas não suficientes. Eles respondem a “este principal pode atualizar posts em geral?”, e não “ele pode atualizar o post 42?”. Essa segunda pergunta requer o recurso.

Aplicando a validação no nível do recurso

A verificação no nível do recurso é onde a maioria das vulnerabilidades reais é encontrada, pois é a etapa que as pessoas costumam esquecer. Um endpoint como PATCH /posts/:id recebe um id do cliente. Se ele confiar nesse id sem verificar a propriedade, qualquer usuário autenticado poderá modificar qualquer post apenas adivinhando ou enumerando ids. Isso é chamado de Insecure Direct Object Reference, ou IDOR.

A solução segue sempre o mesmo padrão: carregue o recurso e, em seguida, verifique-o.

router.patch("/posts/:id", requireAuth(), async (req, res) => {
  const post = await db.post.findById(req.params.id);
  if (!post) return res.status(404).json({ error: "not_found" });

  const allowed = can(req.user!, "update", {
    type: "post",
    ownerId: post.authorId,
    tenantId: post.tenantId,
  });

  if (!allowed) return res.status(403).json({ error: "forbidden" });

  const updated = await db.post.update(post.id, req.body);
  res.json(updated);
});

Existe uma escolha sutil de ordenação para recursos entre diferentes tenants. Se um usuário do tenant A solicitar um post do tenant B, retornar 403 confirma que o post existe, o que vaza informações entre tenants. Muitos sistemas retornam 404 nesse caso, para que o recurso seja indistinguível de um que não existe. Independentemente da sua escolha, seja consistente e documente-a.

O mesmo padrão se aplica a recursos aninhados. Antes de agir sobre /teams/:teamId/projects/:projectId, verifique se o principal pode acessar a equipe e se o projeto pertence a ela. Cada id no caminho é controlado pelo atacante e deve ser verificado.

A matriz de permissões

A matriz de permissões é uma tabela com as roles nas linhas e as permissões nas colunas, preenchida com as concessões (grants). É o artefato que torna um sistema de autorização revisável.

              posts:read  posts:create  posts:update  posts:delete  billing:read
viewer            x
editor            x            x             x
admin             x            x             x             x             x
billing           x                                                     x

Mantenha-a no controle de versão junto ao código e gere as migrations de seed a partir dela, para que a documentação e os dados não divirjam. Quando alguém propõe uma nova role, a primeira pergunta é quais colunas ela recebe — e a resposta é um diff nesta tabela, não uma busca exaustiva pelos handlers.

Dois hábitos tornam a matriz útil. Primeiro, nomeie as permissões de forma consistente como resource:action, para que a tabela seja lida com clareza e as strings sejam previsíveis. Segundo, revise a matriz sempre que uma role for alterada, pois uma única coluna extra é fácil de passar despercebida em uma migration e pode conceder muito mais do que o pretendido.

Papéis multi-tenant

Em uma aplicação multi-tenant, a mesma pessoa pode ter papéis diferentes em organizações distintas. O dono de uma agência é um admin do seu próprio tenant e um viewer no de um cliente. Uma única coluna global role não consegue expressar isso.

A solução é definir o escopo das atribuições de papéis por tenant. Adicione tenant_id a user_roles e torne-o parte da chave primária, para que um usuário possa ter papéis distintos por tenant. Quando você constrói o conjunto de permissões para uma requisição, você o constrói para um único tenant — aquele no qual a requisição está atuando.

SELECT DISTINCT p.action
FROM user_roles ur
JOIN role_permissions rp ON rp.role_id = ur.role_id
JOIN permissions p       ON p.id = rp.permission_id
WHERE ur.user_id = $1 AND ur.tenant_id = $2;

O tenant deve vir de uma fonte confiável: a sessão, um subdomínio que você controla ou o token. Nunca o aceite de um corpo de requisição ou query string sem verificar se o principal pertence a ele. Uma vez estabelecido, o isolamento de tenant é a primeira regra em can() e se aplica antes que qualquer permissão seja considerada, portanto, nenhuma concessão pode cruzar essa fronteira.

Trocar de tenant é uma mudança de privilégio. Se o tenant ativo estiver na sessão, atualize-o no lado do servidor e regenere qualquer conjunto de permissões em cache para que as concessões do tenant antigo não vazem para o novo contexto.

ABAC e policy engines

O RBAC resolve a maioria das questões, mas algumas regras dependem de mais do que apenas a role e a propriedade: hora do dia, a sensibilidade dos dados, o departamento do usuário ou a pontuação de risco da requisição. Estas são regras baseadas em atributos, e tentar codificá-las como roles gera uma explosão combinatória.

O ABAC (Attribute-Based Access Control) avalia políticas com base em atributos do principal, do recurso, da ação e do ambiente. Uma regra poderia ser: “um usuário pode ler um documento se o seu departamento coincidir com o departamento do documento e a classificação não for secreta”. Isso é expressável, testável e auditável de uma forma que uma matriz de roles não é.

Os policy engines tornam isso prático. O Open Policy Agent avalia políticas escritas em Rego e pode ser consultado como um sidecar ou uma biblioteca, permitindo que as mesmas regras sejam aplicadas em diferentes serviços e linguagens. O Casbin oferece uma abordagem de modelo e adaptador mais leve, com suporte para RBAC, ABAC e combinações, sendo popular em código de aplicação.

Adote um policy engine apenas quando as regras realmente superarem a capacidade do RBAC, não antes. Isso adiciona uma nova linguagem, uma superfície de deploy e uma curva de aprendizado. Um helper can() bem fatorado com regras claras resolve a maioria dos casos, e você sempre poderá encapsulá-lo em um policy engine mais tarde, quando uma decisão específica exigir mais contexto.

Testando a autorização

Bugs de autorização são bugs de segurança, portanto, os testes devem tratar os casos de negação como prioridade. Para cada ação protegida, escreva uma matriz de testes: um chamador anônimo, um principal sem a permissão, um proprietário, um não proprietário com a permissão e um principal de outro tenant.

describe("PATCH /posts/:id", () => {
  it("rejects anonymous users", async () => {
    await request(app).patch("/posts/1").send({ title: "x" }).expect(401);
  });

  it("rejects users without posts:update", async () => {
    await request(app).patch("/posts/1").set("Cookie", viewerCookie).expect(403);
  });

  it("allows the owner", async () => {
    await request(app).patch("/posts/1").set("Cookie", ownerCookie).expect(200);
  });

  it("rejects a non-owner editor", async () => {
    await request(app).patch("/posts/1").set("Cookie", editorCookie).expect(403);
  });

  it("rejects a user from another tenant", async () => {
    await request(app).patch("/posts/1").set("Cookie", otherTenantCookie).expect(404);
  });
});

Teste can() diretamente como uma função pura, com uma tabela de principals, ações e recursos. Isso cobre a lógica de forma exaustiva e barata, enquanto os testes de endpoint provam que a verificação está realmente implementada. Uma falha comum é ter um helper correto que o handler esqueceu de chamar, e apenas um teste de integração consegue detectar isso.

Populando permissões como migrations

Permissões e mapeamentos de roles fazem parte do contrato da sua aplicação, portanto, devem estar em migrations, e não em um painel administrativo que diverge entre os ambientes.

Escreva uma migration de seed que faça o upsert de permissões por nome e, em seguida, reconcilie as concessões de cada role com a matriz. O uso de upserts mantém a migration idempotente, o que é fundamental, pois ela pode ser executada em bancos de dados que já possuam algumas linhas.

INSERT INTO permissions (action) VALUES
  ('posts:read'), ('posts:create'), ('posts:update'), ('posts:delete'),
  ('billing:read'), ('billing:manage')
ON CONFLICT (action) DO NOTHING;

INSERT INTO role_permissions (role_id, permission_id)
SELECT r.id, p.id
FROM roles r
JOIN permissions p ON p.action IN ('posts:read', 'posts:create', 'posts:update')
WHERE r.name = 'editor'
ON CONFLICT DO NOTHING;

Deletar uma permissão é mais arriscado do que adicionar uma. Verifique os usos no código e, se ela for referenciada em qualquer lugar, renomeie-a ou descontinue-a gradualmente. Uma migration que remove posts:update enquanto um handler ainda a verifica transformará cada requisição em um “deny”, o que é seguro, mas confuso até que alguém analise o diff do seed.

Cacheando o conjunto de permissões

Uma verificação de permissão nunca deve acessar o banco de dados. Monte o conjunto efetivo uma vez por requisição, anexe-o ao principal e reutilize-o para cada verificação naquela requisição.

Para sistemas de alto tráfego, faça o cache do conjunto por usuário por um curto período, utilizando o usuário e o tenant como chaves. Um TTL de 30 a 60 segundos geralmente é suficiente para remover a query do caminho crítico (hot path), mantendo as alterações de função visíveis rapidamente. Quando uma função for alterada, invalide o cache explicitamente em vez de esperar pelo TTL, para que uma permissão revogada pare de funcionar imediatamente.

async function permissionsFor(userId: string, tenantId: string) {
  const key = `perm:${tenantId}:${userId}`;
  const cached = await redis.get(key);
  if (cached) return new Set(JSON.parse(cached));

  const rows = await db.query(permissionQuery, [userId, tenantId]);
  const set = new Set(rows.map((r) => r.action));
  await redis.set(key, JSON.stringify([...set]), "EX", 60);
  return set;
}

Existe um trade-off de segurança no TTL. Quanto maior o cache, maior a janela de tempo em que uma função revogada ainda funciona. Prefira a invalidação explícita a cada alteração de atribuição de função e mantenha o TTL curto como uma rede de segurança para invalidações perdidas.

Melhores práticas

  • Verifique permissões, e não nomes de roles, no código da aplicação; mantenha as roles como pacotes para humanos.
  • Centralize a decisão em uma única can(user, action, resource) pura.
  • Negue por padrão e falhe no modo fechado (fail closed) caso as roles ou permissões não possam ser carregadas.
  • Aplique o isolamento de tenant antes de qualquer verificação de permissão, e obtenha o tenant de uma fonte confiável.
  • Carregue o recurso antes de autorizar uma ação sobre ele, para evitar IDOR.
  • Retorne 401 para requisições não autenticadas e 403 para requisições negadas.
  • Faça cache do conjunto de permissões efetivas por requisição e invalide-o quando as roles mudarem.
  • Mantenha a matriz de permissões no controle de versão e gere seeds a partir dela.
  • Teste os casos negativos: anônimo, permissão incorreta, não proprietário, outro tenant.
  • Registre (log) as decisões de permissão e negação com contexto suficiente para explicá-las posteriormente.

Erros comuns

  • Tratar uma sessão ou token válido como prova de autorização.
  • Criar ramificações baseadas em user.role === "admin" por todo o código.
  • Verificar a rota, mas nunca o recurso, deixando uma vulnerabilidade de IDOR.
  • Confiar em um tenant id vindo do corpo da requisição ou da query string.
  • Conceder permissões amplas de manage para evitar a modelagem de uma regra real.
  • Construir hierarquias de roles profundas que ninguém consegue compreender.
  • Fazer cache de permissões indefinidamente, mantendo concessões obsoletas após a alteração de uma role.
  • Retornar 403 quando um 404 evitaria vazar a existência de um recurso de outro tenant.
  • Deixar que botões ocultos na UI substituam a validação no servidor.
  • Esquecer de verificar cada id em um caminho de rota aninhada.

Próximos passos

O RBAC é o modelo de autorização que você mais utilizará e ele se integra perfeitamente a tudo o que você já construiu. Se os seus principals chegam via tokens, o guia de JWT mostra onde claims como roles se encaixam e por que você ainda deve verificá-las no server-side. O principal em si vem da autenticação de sessão ou, no caso de máquinas, de API keys. E como a autorização sempre envolve um alvo, o guia de REST é o companheiro ideal para modelar recursos e seus ids. Quando suas regras começarem a depender do contexto em vez de roles, volte à seção de ABAC e utilize um policy engine.

Na pratica

Schema, helper, guard, propriedade

Quatro arquivos que levam uma requisição de um usuário autenticado a uma ação autorizada.

migrations/rbac.sql
CREATE TABLE roles (
  id   bigserial PRIMARY KEY,
  name text NOT NULL UNIQUE
);

CREATE TABLE permissions (
  id     bigserial PRIMARY KEY,
  action text NOT NULL UNIQUE -- e.g. "posts:update"
);

CREATE TABLE role_permissions (
  role_id       bigint NOT NULL REFERENCES roles (id) ON DELETE CASCADE,
  permission_id bigint NOT NULL REFERENCES permissions (id) ON DELETE CASCADE,
  PRIMARY KEY (role_id, permission_id)
);

CREATE TABLE user_roles (
  user_id  bigint NOT NULL REFERENCES users (id) ON DELETE CASCADE,
  role_id  bigint NOT NULL REFERENCES roles (id) ON DELETE CASCADE,
  tenant_id bigint NOT NULL REFERENCES tenants (id) ON DELETE CASCADE,
  PRIMARY KEY (user_id, role_id, tenant_id)
);

Verifique permissões, não nomes de role

Uma verificação de role codifica a suposição de que apenas admins podem editar. Uma verificação de permissão expressa a regra diretamente, permitindo que novas roles funcionem sem tocar no handler.

Preferir
if (!can(user, "update", post)) {
  return res.status(403).json({ error: "forbidden" });
}
Evitar
if (user.role !== "admin") {
  return res.status(403).json({ error: "forbidden" });
}
// An "editor" role can never be granted
// this action without editing the code.

Autorize o recurso, não apenas a rota

Um guard de rota prova que o usuário pode editar algum post. Apenas uma verificação de recurso prova que ele pode editar este post. Pular isso é a vulnerabilidade clássica de IDOR.

Preferir
const post = await db.post.findById(req.params.id);
if (!post) return res.sendStatus(404);

if (!can(req.user!, "update", {
  type: "post",
  ownerId: post.authorId,
  tenantId: post.tenantId,
})) {
  return res.sendStatus(403);
}
Evitar
router.patch("/posts/:id", requirePermission("update", "post"),
  async (req, res) => {
    // Any user who can update any post
    // can now update every post by id.
    await db.post.update(req.params.id, req.body);
    res.sendStatus(204);
  });

Trade-offs

O RBAC é o modelo certo?

O RBAC é o padrão pragmático para a maioria das aplicações. Saiba onde ele deixa de ser suficiente.

Strengths

  • Fácil de explicar e revisar

    Uma matriz de permissões é algo que alguém que não é engenheiro consegue ler. Revisões e auditorias tornam-se uma questão de checar uma tabela em vez de rastrear caminhos de código.

  • Reutilizável e composível

    As roles empacotam permissões uma vez e as aplicam em todo lugar. Novas funcionalidades reutilizam átomos existentes em vez de inventar novas verificações.

  • Mapeia como as equipes trabalham

    Funções de trabalho como suporte, editor e admin alinham-se naturalmente com roles, tornando o modelo intuitivo para as pessoas que o utilizam.

Trade-offs

  • A explosão de roles é real

    Sem um vocabulário de permissões disciplinado, as equipes criam uma nova role para cada caso extremo até que ninguém saiba o que cada uma significa.

  • O contexto é difícil de expressar

    Regras como "editores podem publicar seus próprios rascunhos durante o horário comercial" não se encaixam bem em roles. É aí que o ABAC ou um policy engine se tornam necessários.

  • O cache deve ser invalidado

    Permissões geralmente são cacheadas por requisição ou por token. Uma mudança de role deve invalidar esse cache, ou a concessão antiga persistirá até a expiração.

Perguntas frequentes

Perguntas frequentes

Keep learning

Related topics from the roadmap.

$ comecar a aprender

Pronto para aprender RBAC & Permissions?

Nosso tutorial interativo te guia por RBAC & Permissions passo a passo — com quizzes e codigo real que voce pode executar no navegador.