RBAC com Claude Code: autenticacao, autorizacao, tenants, testes e auditoria
RBAC seguro com Claude Code: permissoes, tenants, middleware Express, testes negativos, logs de auditoria e limite para ABAC.
RBAC não fica seguro apenas porque a aplicação tem roles chamados admin, editor e viewer. A maioria das falhas nasce em detalhes menores: tratar qualquer usuário logado como autorizado, esquecer o filtro tenant_id em uma consulta, esconder um botão na interface mas deixar a API aberta, ou permitir que um editor altere um objeto de outra pessoa usando um ID adivinhado.
Este guia transforma RBAC em uma tarefa revisável para Claude Code. Vamos separar autenticação e autorização, modelar role / permission / resource / action, aplicar deny by default, proteger limites de tenant, adicionar autorização por objeto, middleware Express, schema de banco, testes, logs de auditoria e o momento em que RBAC deve evoluir para ABAC. Para a camada de login, veja o guia de autenticação JWT com Claude Code. Para proteção operacional, combine com boas práticas de segurança no Claude Code.
As referências externas usadas aqui são OWASP Authorization Cheat Sheet, OWASP Access Control, Casbin RBAC e Auth0 Core RBAC. A OWASP reforça privilégio mínimo, rejeição por padrão, validação em toda request, logging e testes de autorização.
Autenticacao nao e autorizacao
Autenticação responde “quem é este usuário”. Senha, SSO, JWT, cookie de sessão e MFA pertencem a essa camada. Autorização responde “este ator pode executar esta ação sobre este recurso?”. Estar logado não deve permitir apagar faturas, convidar administradores ou ler dados de outro cliente.
Em RBAC, mantenha quatro conceitos visíveis:
| Elemento | Exemplo | Pergunta de revisão |
|---|---|---|
| role | viewer, editor, billing_admin, owner | Está baseado em responsabilidade real |
| permission | article:update, invoice:read | Está definido em um único lugar |
| resource | article, invoice, user | Qual objeto é protegido |
| action | read, create, update, delete | É mais preciso que o método HTTP |
O erro comum é criar um admin enorme e depois somar exceções. Na revisão, fica difícil saber se uma exceção é requisito de produto ou atalho temporário. É melhor definir permissions primeiro e tratar roles como pacotes de permissions.
flowchart TD
A["Authenticated actor"] --> B["Tenant boundary"]
B --> C["Role to permission map"]
C --> D["Resource and action check"]
D --> E["Object-level rule"]
E --> F["Allow"]
B --> X["Deny and audit"]
C --> X
D --> X
E --> X
Comece com deny by default
Deny by default significa rejeitar toda operação que não foi explicitamente permitida pelo código. Ao pedir para Claude Code mexer em autorização, escreva primeiro as regras de rejeição: permission desconhecida, lista de roles vazia, tenant diferente, recurso ausente e objeto sem propriedade compatível devem falhar de forma fechada.
O exemplo Express abaixo pode ser copiado para um novo projeto. A demo lê identidade de headers para facilitar os testes. Em produção, substitua essa parte por JWT ou sessão já verificados.
{
"name": "rbac-express-demo",
"type": "module",
"scripts": {
"dev": "tsx src/server.ts",
"test": "vitest run"
},
"dependencies": {
"express": "^5.1.0"
},
"devDependencies": {
"@types/express": "^5.0.3",
"@types/node": "^22.15.0",
"@types/supertest": "^6.0.3",
"supertest": "^7.1.1",
"tsx": "^4.19.4",
"typescript": "^5.8.3",
"vitest": "^3.1.4"
}
}
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["src/**/*.ts", "test/**/*.ts"]
}
Nucleo RBAC em TypeScript
Não espalhe verificações de role pelos controllers. Centralize a decisão em authorize e deixe as rotas passarem o nome da permission. Assim a superfície de revisão é menor e Claude Code sabe onde alterar quando um novo recurso aparece.
// src/rbac.ts
export const permissions = [
"project:read",
"article:read",
"article:create",
"article:update",
"article:delete",
"invoice:read",
"invoice:refund",
"user:manage"
] as const;
export type Permission = (typeof permissions)[number];
export type Role = "viewer" | "editor" | "billing_admin" | "owner";
export type Actor = {
id: string;
tenantId: string;
roles: Role[];
};
export type ResourceRecord = {
id: string;
tenantId: string;
ownerId?: string;
};
export type AuthorizationDecision = {
allow: boolean;
reason: string;
};
export const rolePermissions = {
viewer: ["project:read", "article:read", "invoice:read"],
editor: ["project:read", "article:read", "article:create", "article:update"],
billing_admin: ["project:read", "invoice:read", "invoice:refund"],
owner: [...permissions]
} as const satisfies Record<Role, readonly Permission[]>;
const knownPermissions = new Set<Permission>(permissions);
export function authorize(
actor: Actor,
permission: Permission,
record?: ResourceRecord
): AuthorizationDecision {
if (!knownPermissions.has(permission)) {
return { allow: false, reason: "unknown_permission" };
}
if (actor.roles.length === 0) {
return { allow: false, reason: "no_role" };
}
if (record && record.tenantId !== actor.tenantId) {
return { allow: false, reason: "tenant_mismatch" };
}
const roleAllows = actor.roles.some((role) =>
rolePermissions[role].includes(permission)
);
if (!roleAllows) {
return { allow: false, reason: "role_missing_permission" };
}
if (
permission === "article:update" &&
!actor.roles.includes("owner") &&
record?.ownerId !== actor.id
) {
return { allow: false, reason: "not_resource_owner" };
}
return { allow: true, reason: "allowed" };
}
export function auditAuthorization(input: {
actor?: Actor;
permission: Permission;
resourceId?: string;
decision: AuthorizationDecision;
}) {
console.info(
JSON.stringify({
type: "authorization",
actorId: input.actor?.id ?? "anonymous",
tenantId: input.actor?.tenantId ?? "unknown",
permission: input.permission,
resourceId: input.resourceId ?? null,
allow: input.decision.allow,
reason: input.decision.reason,
at: new Date().toISOString()
})
);
}
A rota pede article:update, não pergunta apenas “o usuário é editor?”. O role resolve permissions; depois o código ainda valida tenant e propriedade do objeto.
Middleware Express em todas as requests
A autorização deve rodar antes de o handler alterar dados. Middleware torna visível quais rotas estão protegidas e evita verificações esquecidas.
// src/server.ts
import express, { type NextFunction, type Request, type Response } from "express";
import {
type Actor,
type Permission,
type ResourceRecord,
type Role,
auditAuthorization,
authorize,
rolePermissions
} from "./rbac.js";
declare global {
namespace Express {
interface Request {
actor?: Actor;
}
}
}
type Article = ResourceRecord & {
title: string;
body: string;
};
const articles: Article[] = [
{ id: "a1", tenantId: "tenant-a", ownerId: "user-1", title: "Roadmap", body: "Draft" },
{ id: "a2", tenantId: "tenant-a", ownerId: "user-2", title: "Release", body: "Ready" },
{ id: "b1", tenantId: "tenant-b", ownerId: "user-9", title: "Private", body: "Secret" }
];
function parseRoles(value: string | undefined): Role[] {
return (value ?? "")
.split(",")
.map((role) => role.trim())
.filter((role): role is Role => role in rolePermissions);
}
function authenticateForDemo(req: Request, _res: Response, next: NextFunction) {
const userId = req.header("x-user-id");
const tenantId = req.header("x-tenant-id");
if (userId && tenantId) {
req.actor = {
id: userId,
tenantId,
roles: parseRoles(req.header("x-roles"))
};
}
next();
}
function findArticle(req: Request): Article | undefined {
return articles.find((article) => article.id === req.params.articleId);
}
function requirePermission(
permission: Permission,
loadResource?: (req: Request) => ResourceRecord | undefined
) {
return (req: Request, res: Response, next: NextFunction) => {
if (!req.actor) {
return res.status(401).json({ error: "unauthenticated" });
}
const record = loadResource?.(req);
if (loadResource && !record) {
return res.status(404).json({ error: "not_found" });
}
const decision = authorize(req.actor, permission, record);
auditAuthorization({
actor: req.actor,
permission,
resourceId: record?.id,
decision
});
if (!decision.allow) {
return res.status(403).json({ error: "forbidden", reason: decision.reason });
}
return next();
};
}
export const app = express();
app.use(express.json());
app.use(authenticateForDemo);
app.get("/health", (_req, res) => {
res.json({ ok: true });
});
app.get(
"/articles/:articleId",
requirePermission("article:read", findArticle),
(req, res) => {
res.json(findArticle(req));
}
);
app.patch(
"/articles/:articleId",
requirePermission("article:update", findArticle),
(req, res) => {
const article = findArticle(req);
if (!article) return res.status(404).json({ error: "not_found" });
article.title = String(req.body.title ?? article.title);
article.body = String(req.body.body ?? article.body);
return res.json(article);
}
);
app.delete(
"/articles/:articleId",
requirePermission("article:delete", findArticle),
(req, res) => {
const index = articles.findIndex((article) => article.id === req.params.articleId);
if (index >= 0) articles.splice(index, 1);
return res.status(204).send();
}
);
app.get("/admin/users", requirePermission("user:manage"), (_req, res) => {
res.json([{ id: "user-1" }, { id: "user-2" }]);
});
if (process.env.NODE_ENV !== "test") {
app.listen(3000, () => {
console.log("RBAC demo listening on http://localhost:3000");
});
}
Em produção, authenticateForDemo deve virar uma origem de identidade verificada. Mesmo quando um provedor de identidade envia roles ou permissions, a API precisa conferir tenant e regras por objeto no servidor.
Schema de banco com limite de tenant
Falhas de autorização podem acontecer antes do middleware: uma consulta busca a linha errada e o handler assume que ela é segura. Por isso tenant_id, constraints compostas e logs de auditoria precisam aparecer no schema.
CREATE TABLE tenants (
id TEXT PRIMARY KEY,
name TEXT NOT NULL
);
CREATE TABLE users (
id TEXT PRIMARY KEY,
tenant_id TEXT NOT NULL REFERENCES tenants(id),
email TEXT NOT NULL,
UNIQUE (tenant_id, email)
);
CREATE TABLE roles (
id TEXT PRIMARY KEY,
tenant_id TEXT NOT NULL REFERENCES tenants(id),
name TEXT NOT NULL,
UNIQUE (tenant_id, name)
);
CREATE TABLE permissions (
id TEXT PRIMARY KEY,
resource TEXT NOT NULL,
action TEXT NOT NULL,
UNIQUE (resource, action)
);
CREATE TABLE user_roles (
user_id TEXT NOT NULL REFERENCES users(id),
role_id TEXT NOT NULL REFERENCES roles(id),
tenant_id TEXT NOT NULL REFERENCES tenants(id),
PRIMARY KEY (user_id, role_id)
);
CREATE TABLE role_permissions (
role_id TEXT NOT NULL REFERENCES roles(id),
permission_id TEXT NOT NULL REFERENCES permissions(id),
PRIMARY KEY (role_id, permission_id)
);
CREATE TABLE articles (
id TEXT PRIMARY KEY,
tenant_id TEXT NOT NULL REFERENCES tenants(id),
owner_id TEXT NOT NULL REFERENCES users(id),
title TEXT NOT NULL,
body TEXT NOT NULL
);
CREATE TABLE authorization_audit_logs (
id TEXT PRIMARY KEY,
tenant_id TEXT,
actor_id TEXT,
permission TEXT NOT NULL,
resource_id TEXT,
allowed BOOLEAN NOT NULL,
reason TEXT NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_articles_tenant_owner ON articles(tenant_id, owner_id);
CREATE INDEX idx_audit_tenant_created ON authorization_audit_logs(tenant_id, created_at);
Em SaaS B2B, roles personalizados por tenant são comuns; por isso tenant_id + name costuma ser o unique certo. Em ambientes regulados, roles globais fixos com atribuições locais podem ser mais fáceis de auditar.
Teste mais recusas do que sucessos
Os testes precisam provar os casos permitidos, mas devem cobrir principalmente recusas: mesmo role em outro tenant, mesmo tenant com outro owner, usuário logado sem permission, recurso inexistente e request sem autenticação.
// test/rbac.test.ts
import request from "supertest";
import { describe, expect, it } from "vitest";
import { app } from "../src/server.js";
const editorUser1 = {
"x-user-id": "user-1",
"x-tenant-id": "tenant-a",
"x-roles": "editor"
};
const editorTenantB = {
"x-user-id": "user-9",
"x-tenant-id": "tenant-b",
"x-roles": "editor"
};
const owner = {
"x-user-id": "owner-1",
"x-tenant-id": "tenant-a",
"x-roles": "owner"
};
describe("RBAC middleware", () => {
it("rejects unauthenticated requests", async () => {
const res = await request(app).get("/articles/a1");
expect(res.status).toBe(401);
});
it("allows an editor to update an owned article in the same tenant", async () => {
const res = await request(app)
.patch("/articles/a1")
.set(editorUser1)
.send({ title: "Updated roadmap" });
expect(res.status).toBe(200);
expect(res.body.title).toBe("Updated roadmap");
});
it("blocks cross-tenant access even when the role matches", async () => {
const res = await request(app).get("/articles/a1").set(editorTenantB);
expect(res.status).toBe(403);
expect(res.body.reason).toBe("tenant_mismatch");
});
it("blocks editors from managing users", async () => {
const res = await request(app).get("/admin/users").set(editorUser1);
expect(res.status).toBe(403);
expect(res.body.reason).toBe("role_missing_permission");
});
it("allows owners to delete tenant resources", async () => {
const res = await request(app).delete("/articles/a2").set(owner);
expect(res.status).toBe(204);
});
});
npm install
npm test
npm run dev
Quando Claude Code adicionar testes, cite tenant_mismatch, not_resource_owner, role_missing_permission e unauthenticated. O reason retornado ao cliente deve ser igual ao reason do log de auditoria.
Quatro casos de uso reais
O primeiro caso é gestão de projetos B2B. O owner convida membros e altera cobrança; o editor só modifica conteúdo. O limite de tenant é mais importante que o nome do role: um projectId adivinhado deve falhar na consulta e no middleware.
O segundo caso é um CMS interno. Um editor altera seus próprios rascunhos, mas excluir artigo publicado ou editar artigo de outro autor exige role mais forte. Isso é autorização por objeto sobre RBAC, pois article:update também depende de ownerId e status.
O terceiro caso é cobrança e reembolso. billing_admin pode ler faturas, mas um reembolso pode depender de valor, status de aprovação ou dupla revisão. RBAC abre a porta; atributos de negócio decidem se a ação específica é segura.
O quarto caso é impersonação por suporte. Suporte pode ver o workspace para diagnosticar um ticket, mas não deve alterar e-mail, pagamento ou role de owner. Toda ação impersonada deve ser auditada separadamente da ação do cliente.
Falhas concretas e correcoes
| Falha | Risco | Correção |
|---|---|---|
| Retornar todos os registros a qualquer usuário logado | Escalada horizontal | Filtrar sempre por tenant_id e resource ID |
| Esconder botões apenas na UI | Chamada direta à API ainda funciona | Rejeitar no middleware do servidor |
Um admin gigante | Revisão de permissão perde valor | Dividir permissions e redesenhar roles |
| Logar só ações permitidas | Tentativas de abuso somem | Logar allow e deny com reason |
| Testar só sucesso | Regressões abrem brechas silenciosas | Automatizar matriz de recusas |
A interface não é fronteira de segurança. Esconder um botão melhora a experiência, mas a proteção real precisa estar no servidor.
O que delegar a Claude Code
Claude Code é útil para transformar uma política clara em código, testes e diffs revisáveis. Ele não deve inventar a política de segurança. Informe arquivos, regras de recusa e critérios de revisão.
Help implement RBAC.
Only change src/rbac.ts, src/server.ts, and test/rbac.test.ts.
Requirements:
- An authenticated actor has userId, tenantId, and roles
- Permissions use the resource:action format
- Unknown permission, empty role list, and tenant mismatch deny by default
- article:update is allowed only for owner role or the article owner
- 403 responses include a reason, and audit logs use the same reason
- Add more denial tests than success tests
Do not:
- Add an admin bypass
- Treat UI visibility as authorization
- Modify the existing authentication flow
Na revisão, verifique se authorize é o único ponto de decisão, se toda rota protegida usa middleware, se consultas de objeto incluem tenant, se logs não expõem tokens ou segredos e se os testes cobrem recusas. Se Claude Code gerar um diff grande, separe autorização de refactors não relacionados.
Quando RBAC deve virar ABAC
RBAC funciona bem para responsabilidades de trabalho. Ele fica pesado quando regras dependem de atributos: reembolso acima de um limite precisa de dupla aprovação, dados pessoais da UE só podem ser vistos de uma região europeia, exportar auditoria só é permitido no plano Enterprise, ou uma permission vale apenas no horário comercial.
ABAC significa Attribute-Based Access Control, controle de acesso baseado em atributos. Ele avalia atributos do ator, recurso, ambiente e request. Na prática, mantenha RBAC como porta grossa e adicione regras ABAC onde o negócio exigir. Mesmo com motores de política, deixe claros os nomes de permission, limites de tenant, logs e testes.
Resumo
Um bom RBAC é definido por recusas claras. Separe autenticação de autorização, use permissions resource:action, aplique deny by default, valide tenants, adicione regras por objeto e comprove tudo com testes e logs. Essa estrutura também dá a Claude Code um escopo seguro: implementar a política, adicionar testes negativos e manter a mudança revisável.
Para um fluxo completo, combine este artigo com o guia de desenvolvimento de APIs com Claude Code, o guia JWT e as boas práticas de segurança. Se precisar de consultoria, treinamento ou apoio de implementação, traga a tabela atual de roles, a lista de APIs protegidas e as operações mais perigosas se fossem expostas.
Ao testar esta abordagem, verifique quatro pontos: testes negativos passam, IDs de outro tenant retornam 403, logs registram allow e deny, e Claude Code não adicionou um atalho que contorne authorize. Comece por uma operação perigosa e escreva role, permission, resource, action, tenantId e ownerId antes de levar para o código.
PDF grátis: cheatsheet do Claude Code
Informe seu e-mail e baixe uma página com comandos, hábitos de revisão e workflows seguros.
Cuidamos dos seus dados e não enviamos spam.
Sobre o autor
Masa
Engenheiro focado em workflows práticos com Claude Code.
Artigos relacionados
Permission receipt no Claude Code: escopo, prova e rollback
Padrão de permission receipt para Claude Code: ações permitidas, limites de aprovação, comandos de prova, rollback e CTA de receita.
Agent Harness seguro para Claude Code e Codex: permissoes, verificacao e rollback
Monte uma base segura para agentes com Claude Code e Codex usando politicas, plano, verificacao e recuperacao.
Subagentes no Claude Code: guia prático para delegar trabalho com segurança
Guia prático de subagentes no Claude Code para dividir artigos e código: regras, prompts, riscos e checklist.