O paper que fundou tudo
RBAC não é uma ideia da Microsoft nem do AWS IAM. É um modelo formal publicado em 1996 por Ravi Sandhu et al. no IEEE Computer ("Role-Based Access Control Models") e padronizado pelo NIST em 2004 (ANSI INCITS 359-2004). A contribuição central é uma observação simples:
Atribuir permissões diretamente a usuários não escala. Atribuir papéis (roles) a usuários, e permissões a papéis, sim — porque o conjunto de papéis numa organização é pequeno, mas o de usuários é grande e rotativo.
O paper define quatro níveis incrementais — conhecidos como família RBAC:
Modelagem em SQL (3 tabelas, não 4)
O esquema canônico em Postgres — apenas três tabelas associativas além das entidades. Atenção ao índice composto na tabela de associação:
-- Entidades base
CREATE TABLE users (
id UUID PRIMARY KEY,
email TEXT UNIQUE NOT NULL,
created_at TIMESTAMPTZ DEFAULT now()
);
CREATE TABLE roles (
id UUID PRIMARY KEY,
name TEXT UNIQUE NOT NULL, -- 'admin', 'editor', 'viewer'
parent_role_id UUID REFERENCES roles(id) -- RBAC1 hierarchy
);
CREATE TABLE permissions (
id UUID PRIMARY KEY,
resource TEXT NOT NULL, -- 'invoices', 'users'
action TEXT NOT NULL, -- 'read', 'write', 'delete'
UNIQUE (resource, action)
);
-- Associações N:N
CREATE TABLE user_roles (
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
role_id UUID REFERENCES roles(id) ON DELETE CASCADE,
assigned_at TIMESTAMPTZ DEFAULT now(),
PRIMARY KEY (user_id, role_id)
);
CREATE INDEX idx_user_roles_user ON user_roles(user_id);
CREATE TABLE role_permissions (
role_id UUID REFERENCES roles(id) ON DELETE CASCADE,
permission_id UUID REFERENCES permissions(id) ON DELETE CASCADE,
PRIMARY KEY (role_id, permission_id)
);Erro comum: criar tabela direta "para casos especiais". Em 2 anos você tem 30% das perms via role e 70% via overrides — RBAC virou bagunça. Se precisa de override, modele como uma role pessoal por user (anti-pattern, mas pelo menos consistente) ou migre para ABAC/ReBAC.
Resolver permissões efetivas com hierarquia
A query "quais permissões esse user tem?" precisa atravessar a hierarquia. Em Postgres, CTE recursiva resolve elegantemente:
-- Dado um user_id, retornar todas as perms efetivas (incluindo herdadas)
WITH RECURSIVE role_tree AS (
-- base: roles diretas do user
SELECT r.id, r.parent_role_id
FROM roles r
JOIN user_roles ur ON ur.role_id = r.id
WHERE ur.user_id = $1
UNION
-- step: sobe na hierarquia, mas espera, herança é DESCENDENTE
-- se editor herda de viewer, queremos perms de viewer ao olhar editor
SELECT child.id, child.parent_role_id
FROM roles child
JOIN role_tree rt ON child.parent_role_id = rt.id
)
SELECT DISTINCT p.resource, p.action
FROM role_tree rt
JOIN role_permissions rp ON rp.role_id = rt.id
JOIN permissions p ON p.id = rp.permission_id;Em produção, a query acima é resolvida no path de cada request — cache obrigatório. Padrão: redis com TTL curto (60s) chave , invalidação em writes de /.
Anatomia de uma decisão de acesso
Separation of Duties (RBAC2)
SoD vem de auditoria financeira: a pessoa que cria uma transação não pode ser a mesma que aprova. Em SOX/PCI-DSS isso é requisito legal, não best practice.
| Tipo | Quando se verifica | Exemplo |
|---|---|---|
| Static SoD (SSD) | Na atribuição da role ao user | User com role "submitter_payment" não pode receber "approver_payment" |
| Dynamic SoD (DSD) | Na ativação da role na sessão | User tem ambas, mas só pode ativar uma por sessão |
| Cardinality constraint | Quando role é atribuída | Apenas 2 usuários podem ter role "ceo" |
| Prerequisite role | Antes de atribuir | Só pode receber "team_lead" se já tiver "senior_eng" |
-- SoD estática modelada em SQL via tabela de pares conflitantes
CREATE TABLE role_conflicts (
role_a UUID REFERENCES roles(id),
role_b UUID REFERENCES roles(id),
PRIMARY KEY (role_a, role_b)
);
-- Trigger que bloqueia INSERT em user_roles violando conflito
CREATE FUNCTION check_sod() RETURNS TRIGGER AS $$
BEGIN
IF EXISTS (
SELECT 1 FROM user_roles ur
JOIN role_conflicts rc
ON (rc.role_a = NEW.role_id AND rc.role_b = ur.role_id)
OR (rc.role_b = NEW.role_id AND rc.role_a = ur.role_id)
WHERE ur.user_id = NEW.user_id
) THEN
RAISE EXCEPTION 'SoD violation: conflicting role';
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER enforce_sod BEFORE INSERT ON user_roles
FOR EACH ROW EXECUTE FUNCTION check_sod();Onde RBAC vence: arquitetura típica
Onde RBAC quebra (e o porquê)
📋 Notion/Drive-like sharing: 'compartilhar este doc com fulano@externo como editor'
RBAC exigiria uma role por documento ('editor_doc_xyz') — role explosion. ABAC tentaria condição sobre atributo, mas o atributo 'shared_with_users' vira lista que cresce sem bound. ReBAC modela a relação diretamente.
Alt: RBAC puro —
Alt: ABAC —
Alt: ReBAC —
Os três sinais de que você precisa sair do RBAC puro:
- Role explosion: mais de ~50 roles ativas, com nomes contendo IDs de recurso
- Conditional perms: "editor SE recurso for do mesmo departamento" — isso é ABAC
- Resource-level sharing: usuário externo precisa de acesso a UM recurso — isso é ReBAC
Implementação enxuta em TypeScript
// Decorator-style authz middleware
import type { Request, Response, NextFunction } from 'express';
type Permission = `${string}:${string}`; // 'invoices:read'
export function requires(perm: Permission) {
const [resource, action] = perm.split(':');
return async (req: Request, res: Response, next: NextFunction) => {
const user = req.user; // garantido por authn middleware anterior
if (!user) return res.status(401).json({ error: 'unauthenticated' });
const perms = await getEffectivePermissions(user.id); // cache redis
const allowed = perms.has(`${resource}:${action}`);
// Audit log — sempre, permitido ou não
audit.log({
user_id: user.id,
resource, action,
decision: allowed ? 'permit' : 'deny',
ts: new Date(),
});
if (!allowed) return res.status(403).json({ error: 'forbidden' });
return next();
};
}
// Uso
app.get('/invoices/:id', requires('invoices:read'), handleGetInvoice);
app.delete('/users/:id', requires('users:delete'), handleDeleteUser);ArchFlow: integração Postgres + Redis + audit
Resumo executivo
- RBAC = padrão NIST formal desde 1996 (Sandhu). RBAC0/1/2/3 são camadas, não "versões".
- 3 tabelas associativas em SQL resolvem 90% dos casos. Não invente uma quarta.
- Role hierarchy (RBAC1) via CTE recursiva, sempre com cache redis na frente.
- SoD (RBAC2) é requisito de compliance financeira — modele com trigger SQL ou policy no app.
- RBAC quebra em: sharing resource-level, conditional permissions e role explosion. A saída é ABAC + ReBAC — próximos módulos.