RBAC em Node.js: controle de acesso por papel
RBAC (Role-Based Access Control) organiza permissões em papéis — admin, editor, viewer — e atribui papéis a usuários. É o modelo mais adotado por APIs de SaaS: mais simples que permissões individuais por usuário, mais flexível que hard-coded checks de "é admin?". Implementar RBAC correto desde o início evita refatorações dolorosas no futuro.
Schema de banco para RBAC
Estrutura de tabelas para papéis e permissões:
-- Schema RBAC no PostgreSQL:
-- Permissões granulares (recurso:ação):
CREATE TABLE permissoes (
id SERIAL PRIMARY KEY,
nome TEXT UNIQUE NOT NULL, -- ex: "pedidos:read", "usuarios:write", "relatorios:export"
descricao TEXT
);
-- Papéis (roles):
CREATE TABLE roles (
id SERIAL PRIMARY KEY,
nome TEXT UNIQUE NOT NULL, -- "admin", "editor", "viewer", "billing"
descricao TEXT
);
-- Permissões por papel (N:N):
CREATE TABLE role_permissoes (
role_id INT REFERENCES roles(id) ON DELETE CASCADE,
permissao_id INT REFERENCES permissoes(id) ON DELETE CASCADE,
PRIMARY KEY (role_id, permissao_id)
);
-- Papéis por usuário (um usuário pode ter múltiplos papéis):
CREATE TABLE usuario_roles (
usuario_id UUID REFERENCES usuarios(id) ON DELETE CASCADE,
role_id INT REFERENCES roles(id) ON DELETE CASCADE,
-- Opcional: escopo (para multi-tenant):
org_id UUID REFERENCES organizacoes(id),
PRIMARY KEY (usuario_id, role_id, org_id)
);
-- Seed de permissões e roles:
INSERT INTO permissoes (nome) VALUES
('pedidos:read'), ('pedidos:write'), ('pedidos:delete'),
('usuarios:read'), ('usuarios:write'),
('relatorios:view'), ('relatorios:export'),
('admin:*');
INSERT INTO roles (nome) VALUES ('admin'), ('editor'), ('viewer'), ('billing');
-- Atribuir permissões ao admin (todas):
INSERT INTO role_permissoes (role_id, permissao_id)
SELECT r.id, p.id FROM roles r, permissoes p WHERE r.nome = 'admin';Carregar permissões do usuário
Query eficiente para obter todas as permissões de um usuário:
// lib/rbac.ts
import { db } from './db'
import { redis } from './redis'
// Carregar permissões com cache no Redis (5 minutos):
export async function carregarPermissoes(userId: string, orgId?: string): Promise<Set<string>> {
const cacheKey = `permissoes:${userId}:${orgId ?? 'global'}`
const cached = await redis.get(cacheKey)
if (cached) return new Set(JSON.parse(cached))
const { rows } = await db.query(
`SELECT DISTINCT p.nome
FROM usuario_roles ur
JOIN role_permissoes rp ON rp.role_id = ur.role_id
JOIN permissoes p ON p.id = rp.permissao_id
WHERE ur.usuario_id = $1
AND (ur.org_id = $2 OR ur.org_id IS NULL)`,
[userId, orgId ?? null]
)
const permissoes = rows.map(r => r.nome)
await redis.setex(cacheKey, 300, JSON.stringify(permissoes))
return new Set(permissoes)
}
// Verificar permissão (suporta wildcard "admin:*"):
export function temPermissao(permissoes: Set<string>, recurso: string): boolean {
if (permissoes.has('admin:*')) return true // admin tem tudo
if (permissoes.has(recurso)) return true
// Verificar wildcard por recurso: "pedidos:*" cobre "pedidos:read":
const [namespace] = recurso.split(':')
return permissoes.has(`${namespace}:*`)
}
// Limpar cache ao alterar roles:
export async function invalidarCachePermissoes(userId: string) {
const keys = await redis.keys(`permissoes:${userId}:*`)
if (keys.length) await redis.del(...keys)
}Middleware de autorização RBAC
Verificar permissões em cada rota:
// middleware/authorize.ts
import { carregarPermissoes, temPermissao } from '../lib/rbac'
// Middleware factory — exige permissão específica:
export function pode(permissao: string) {
return async (req: Request, res: Response, next: NextFunction) => {
const userId = req.usuario?.userId
if (!userId) return res.status(401).json({ error: 'Não autenticado' })
const orgId = req.headers['x-org-id'] as string | undefined
const permissoes = await carregarPermissoes(userId, orgId)
if (!temPermissao(permissoes, permissao)) {
return res.status(403).json({
error: 'Permissão negada',
required: permissao,
})
}
// Adicionar permissões ao request para uso nas rotas:
req.permissoes = permissoes
next()
}
}
// Verificar múltiplas permissões (ANY ou ALL):
export function podeQualquer(...permissoesList: string[]) {
return async (req: Request, res: Response, next: NextFunction) => {
const permissoes = await carregarPermissoes(req.usuario!.userId)
const temAlguma = permissoesList.some(p => temPermissao(permissoes, p))
if (!temAlguma) return res.status(403).json({ error: 'Permissão negada' })
next()
}
}
// Usar nas rotas:
app.get('/api/pedidos',
autenticar,
pode('pedidos:read'),
listarPedidos
)
app.delete('/api/pedidos/:id',
autenticar,
pode('pedidos:delete'),
deletarPedido
)
app.get('/api/admin',
autenticar,
pode('admin:*'),
adminPanel
)Gerenciar roles via API
Endpoints para atribuir e revogar papéis:
// Atribuir papel a usuário (apenas admin):
app.post('/api/admin/usuarios/:userId/roles',
autenticar,
pode('admin:*'),
async (req, res) => {
const { userId } = req.params
const { roleNome, orgId } = req.body
const { rows: [role] } = await db.query(
'SELECT id FROM roles WHERE nome = $1', [roleNome]
)
if (!role) return res.status(404).json({ error: 'Role não encontrado' })
await db.query(
`INSERT INTO usuario_roles (usuario_id, role_id, org_id)
VALUES ($1, $2, $3) ON CONFLICT DO NOTHING`,
[userId, role.id, orgId ?? null]
)
// Invalidar cache de permissões do usuário:
await invalidarCachePermissoes(userId)
res.json({ message: `Role ${roleNome} atribuído` })
}
)
// Ver permissões efetivas de um usuário:
app.get('/api/usuarios/:userId/permissoes',
autenticar,
pode('usuarios:read'),
async (req, res) => {
const permissoes = await carregarPermissoes(req.params.userId)
const roles = await db.query(
`SELECT r.nome FROM usuario_roles ur
JOIN roles r ON r.id = ur.role_id
WHERE ur.usuario_id = $1`,
[req.params.userId]
)
res.json({
roles: roles.rows.map(r => r.nome),
permissoes: [...permissoes].sort(),
})
}
)RBAC no frontend: mostrar/ocultar elementos
Expor permissões no JWT e usar no React:
// Incluir roles no JWT (não as permissões — são muitas):
const accessToken = await gerarAccessToken({
userId: usuario.id,
email: usuario.email,
roles: usuario.roles, // ['editor', 'billing']
})
// Endpoint para o frontend verificar permissões sem round-trip:
app.get('/api/auth/me', autenticar, async (req, res) => {
const permissoes = await carregarPermissoes(req.usuario!.userId)
res.json({
usuario: req.usuario,
permissoes: [...permissoes],
})
})
// React — hook de permissões:
// hooks/usePermissoes.ts
import { useQuery } from '@tanstack/react-query'
export function usePermissoes() {
return useQuery({
queryKey: ['me'],
queryFn: () => fetch('/api/auth/me').then(r => r.json()),
staleTime: 5 * 60 * 1000, // 5 minutos
})
}
export function usePode(permissao: string) {
const { data } = usePermissoes()
return data?.permissoes?.includes(permissao) ?? false
}
// Componente React:
function BotaoDeletar({ pedidoId }: { pedidoId: string }) {
const podeDeletar = usePode('pedidos:delete')
if (!podeDeletar) return null
return <button onClick={() => deletar(pedidoId)}>Deletar</button>
}$ runstack deploy --plan starter
Não quer configurar manualmente?
Não quer configurar manualmente? Implante o VPS para APIs em menos de 3 minutos com a Runstack. Infraestrutura da OPEN DATACENTER, com servidores no Brasil.
Perguntas frequentes
Conteúdos relacionados