Controle de Permissões Granular (RBAC/ABAC) no Node.js
Da coluna isAdmin a um sistema RBAC completo: modelagem Users/Roles/Permissions, middleware encadeável, permissões no payload JWT para performance, e ABAC para regras baseadas em atributos do recurso.
Autorização é diferente de autenticação. Autenticação responde "quem é você?". Autorização responde "o que você pode fazer?". A maioria dos sistemas começa com um campo isAdmin boolean no banco — funciona para sistemas simples, mas quebra quando surgem requisitos como: "gerentes podem editar usuários, mas não deletar", "usuários premium têm acesso ao relatório X", "um usuário só pode editar seu próprio post".
Neste artigo construímos um sistema de autorização em camadas: RBAC (Role-Based Access Control) para permissões baseadas em cargo, e uma introdução ao ABAC (Attribute-Based Access Control) para regras baseadas nos atributos do recurso acessado.
RBAC vs ABAC: Quando Usar Cada Um
- RBAC — permissões atribuídas a roles (cargos), roles atribuídas a usuários. Simples e escalável para a maioria dos casos. Exemplo: role
MANAGERtem permissõesusers.read,users.update. - ABAC — permissões baseadas em atributos do usuário, do recurso e do contexto. Mais expressivo mas mais complexo. Exemplo: "usuário pode editar o post apenas se
post.authorId === user.id".
Modelagem do Banco: Users, Roles, Permissions
A modelagem Users → Roles → Permissions cria um sistema de autorização flexível: você não atribui permissões diretamente a usuários (isso escala mal), mas a roles (cargos). Um usuário pode ter múltiplas roles. Uma role pode ter múltiplas permissões. Permissões são strings como users.read, users.delete, reports.export — o objeto e a ação. Quando um novo cargo surge na empresa, cria-se uma nova role com as permissões adequadas, sem mudar o código dos middlewares.
import { Entity, PrimaryColumn, Column, ManyToMany } from 'typeorm';
import { RoleModel } from './RoleModel';
// Convenção de nomes: 'recurso.acao'
// Exemplos: 'users.create', 'users.update', 'users.delete', 'reports.read'
@Entity('permissions')
export class PermissionModel {
@PrimaryColumn('uuid')
id: string;
@Column({ unique: true })
name: string; // 'users.delete'
@Column()
description: string; // 'Permite excluir usuários do sistema'
@ManyToMany(() => RoleModel, (role) => role.permissions)
roles: Array<RoleModel>;
}import { Entity, PrimaryColumn, Column, ManyToMany, JoinTable } from 'typeorm';
import { UserModel } from './UserModel';
import { PermissionModel } from './PermissionModel';
@Entity('roles')
export class RoleModel {
@PrimaryColumn('uuid')
id: string;
@Column({ unique: true })
name: string; // 'ADMIN', 'MANAGER', 'EDITOR', 'VIEWER'
@ManyToMany(() => UserModel, (user) => user.roles)
users: Array<UserModel>;
@ManyToMany(() => PermissionModel)
@JoinTable({ name: 'role_permissions' })
permissions: Array<PermissionModel>;
}// Adicione ao UserModel existente:
@ManyToMany(() => RoleModel)
@JoinTable({ name: 'user_roles' })
roles: Array<RoleModel>;
// Schema resultante:
// users (id, name, email, ...)
// roles (id, name)
// permissions (id, name, description)
// user_roles (user_id, role_id)
// role_permissions (role_id, permission_id)Seeding das Permissões e Roles
Crie uma migration de seed para pré-popular as permissões e roles do sistema. Permissões devem ser definidas pelo time de produto/engenharia e inseridas no banco, não criadas dinamicamente:
import { DataSource } from 'typeorm';
import { PermissionModel } from '../models/PermissionModel';
import { RoleModel } from '../models/RoleModel';
export async function seedRolesAndPermissions(dataSource: DataSource): Promise<void> {
const permRepo = dataSource.getRepository(PermissionModel);
const roleRepo = dataSource.getRepository(RoleModel);
// Define todas as permissões do sistema
const permissions = await permRepo.save([
{ id: crypto.randomUUID(), name: 'users.read', description: 'Listar e visualizar usuários' },
{ id: crypto.randomUUID(), name: 'users.create', description: 'Criar novos usuários' },
{ id: crypto.randomUUID(), name: 'users.update', description: 'Editar usuários' },
{ id: crypto.randomUUID(), name: 'users.delete', description: 'Excluir usuários' },
{ id: crypto.randomUUID(), name: 'reports.read', description: 'Acessar relatórios' },
{ id: crypto.randomUUID(), name: 'reports.export', description: 'Exportar relatórios' },
{ id: crypto.randomUUID(), name: 'settings.manage', description: 'Gerenciar configurações do sistema' },
]);
const permsMap = Object.fromEntries(permissions.map((p) => [p.name, p]));
// Define as roles e suas permissões
await roleRepo.save([
{
id: crypto.randomUUID(),
name: 'ADMIN',
permissions: Object.values(permsMap), // Acesso total
},
{
id: crypto.randomUUID(),
name: 'MANAGER',
permissions: [
permsMap['users.read'],
permsMap['users.update'],
permsMap['reports.read'],
permsMap['reports.export'],
],
},
{
id: crypto.randomUUID(),
name: 'EDITOR',
permissions: [
permsMap['users.read'],
permsMap['reports.read'],
],
},
]);
}Middleware de Autorização
import type { Request, Response, NextFunction } from 'express';
import { AppError } from '../errors/AppError';
import { getRedisClient } from '../infra/redis/RedisClient';
import { dataSource } from '../infra/typeorm/dataSource';
import { UserModel } from '../infra/typeorm/models/UserModel';
// Retorna as permissões do usuário com cache Redis para evitar
// uma query pesada (com joins de roles e permissions) em cada requisição
async function getUserPermissions(userId: string): Promise<Array<string>> {
const cacheKey = `user:${userId}:permissions`;
const redis = getRedisClient();
// Tenta o cache primeiro
const cached = await redis.get(cacheKey);
if (cached) return JSON.parse(cached) as Array<string>;
// Cache miss: busca no banco com todas as relações
const user = await dataSource
.getRepository(UserModel)
.findOne({
where: { id: userId },
relations: ['roles', 'roles.permissions'],
});
if (!user) return [];
const permissions = user.roles.flatMap((role) =>
role.permissions.map((p) => p.name)
);
// Remove duplicatas (um usuário pode ter duas roles com a mesma permissão)
const uniquePermissions = [...new Set(permissions)];
// Cacheia por 5 minutos — invalide quando as roles do usuário mudam
await redis.set(cacheKey, JSON.stringify(uniquePermissions), 'EX', 300);
return uniquePermissions;
}
// Middleware factory: retorna um middleware que exige as permissões especificadas
export function requirePermissions(...required: Array<string>) {
return async (req: Request, res: Response, next: NextFunction): Promise<void> => {
const userId = req.user.id; // Populado pelo ensureAuthenticated
const userPermissions = await getUserPermissions(userId);
// Verifica se o usuário tem TODAS as permissões requeridas
const hasAll = required.every((perm) => userPermissions.includes(perm));
if (!hasAll) {
res.status(403).json({
error: 'Acesso negado.',
required,
// Não mostre quais permissões o usuário tem — exposição de informação
});
return;
}
next();
};
}Aplicando nas Rotas
import { Router } from 'express';
import { ensureAuthenticated } from '../middlewares/ensureAuthenticated';
import { requirePermissions } from '../middlewares/ensurePermission';
import { UsersController } from '../controllers/UsersController';
const router = Router();
const controller = new UsersController();
// Todos os endpoints requerem autenticação
router.use(ensureAuthenticated);
// GET / — qualquer usuário autenticado pode listar
router.get('/', requirePermissions('users.read'), controller.index);
// POST / — apenas quem pode criar
router.post('/', requirePermissions('users.create'), controller.create);
// PUT /:id — requer update
router.put('/:id', requirePermissions('users.update'), controller.update);
// DELETE /:id — apenas admins (que têm users.delete)
router.delete('/:id', requirePermissions('users.delete'), controller.destroy);
// Rota que exige MÚLTIPLAS permissões simultaneamente
router.post(
'/bulk-report',
requirePermissions('users.read', 'reports.export'),
controller.bulkReport
);
export { router as usersRoutes };ABAC: Permissões Baseadas no Recurso
RBAC não resolve todos os casos. Um usuário EDITOR pode editar qualquer post ou apenas o seu? Para isso precisamos de ABAC — verificar atributos do recurso no contexto da requisição:
// Middleware ABAC: garante que o usuário só acessa seus próprios recursos
export function ensureOwnership(
getResourceOwnerId: (req: Request) => Promise<string | null>
) {
return async (req: Request, res: Response, next: NextFunction): Promise<void> => {
const { id: userId, roles } = req.user;
// Admins passam direto — RBAC tem precedência sobre ABAC
if (roles.includes('ADMIN')) {
next();
return;
}
const ownerId = await getResourceOwnerId(req);
if (!ownerId || ownerId !== userId) {
res.status(403).json({ error: 'Você só pode modificar seus próprios recursos.' });
return;
}
next();
};
}
// Uso nas rotas:
const getPostOwner = async (req: Request): Promise<string | null> => {
const post = await postsRepo.findById(req.params.id);
return post?.authorId ?? null;
};
router.put(
'/posts/:id',
ensureAuthenticated,
requirePermissions('posts.update'),
ensureOwnership(getPostOwner), // Verifica autoria apenas se passou no RBAC
controller.update
);Invalide o cache de permissões quando as roles do usuário mudam. Se você promoveu um usuário para ADMIN mas não invalidou user:{id}:permissions no Redis, ele ainda terá as permissões antigas por até 5 minutos. Implemente um método invalidateUserPermissionsCache(userId) e chame-o sempre que alterar as roles de um usuário.
Conclusão
Um sistema RBAC bem implementado separa claramente quem pode fazer o quê da lógica de negócio — as regras de acesso ficam nos middlewares e na configuração de rotas, não espalhadas pelos Use Cases. Com cache Redis nas permissões, o overhead de autorização é sub-milissegundo. E quando RBAC não é suficiente, ABAC complementa verificando atributos do recurso específico sendo acessado.