RBAC na prática: setores e hierarquias
O que fazer quando duas pessoas com o mesmo cargo não deveriam enxergar os mesmos dados.
No artigo anterior, onde falei tudo sobre RBAC, usei Lucas e João: dois colaboradores com cargos iguais, mas atividades diferentes. Hoje vou avançar pra um cenário mais complexo, com setores e níveis hierárquicos bem definidos.
Antes de seguir, dois conceitos de modelagem que vão aparecer o tempo todo aqui.
Formas normais são as regras que evitam você guardar a mesma informação em mais de um lugar, ou pior, guardar uma informação que depende de outra que já está guardada em outro canto. Cada fato mora numa tabela só.
Relacionamentos definem como as tabelas se referenciam entre si: 1:1 quando um registro de uma tabela se liga a no máximo um da outra, 1:N quando um registro se liga a vários (um cliente tem vários pedidos), e N:N quando dos dois lados pode haver vários (um aluno tem várias matérias, uma matéria tem vários alunos). N:N sempre precisa de uma tabela no meio pra existir, porque banco relacional não guarda lista dentro de coluna.
Guarda esses dois, porque vou usar os dois o tempo todo daqui pra frente.
O que acontece quando a empresa cresce e passa a ter Financeiro, RH, TI, Vendas… e cada setor tem seus próprios Estagiários, Analistas e Gerentes?
Se modelarmos somente a função numa única tabela plana, sobram duas saídas:
- Usar o mesmo cargo “Gerente” pros dois setores e remendar as permissões de cada usuário na mão.
- Criar um cargo por combinação: gerente_financeiro, gerente_rh, analista_financeiro…
A primeira te faz perder o ponto do RBAC, que é não tratar cada pessoa como caso especial. A segunda cresce combinatorialmente a cada setor ou cargo novo.
As duas funcionam, mas não escalam. Considere isso ao tomar a decisão técnica de adotar ou não.
Por quê? Porque a função não é uma coisa só, são duas:
- Setor, onde a pessoa atua (Financeiro, RH, TI…)
- Hierarquia, o nível dela dentro da estrutura (Estagiário, Analista, Gerente…)
E a função que atribuíamos direto ao usuário, no artigo anterior, é na verdade o cruzamento desses dois: um Gerente do Financeiro.
Pega esse exemplo: existe um Gerente do Financeiro e um Gerente do RH. Cargo idêntico. Só que o Gerente do Financeiro aprova pagamento e vê folha salarial, o Gerente do RH aprova férias e vê dado de contratação. Praticamente nenhuma permissão é compartilhada entre os dois, apesar do cargo ser o mesmo.
Abaixo está a modelagem que fiz pra isso:

sectors e hierarchies viram tabelas próprias, cada setor e cada cargo cadastrado uma única vez, seguindo formas normais: nada de repetir “Financeiro” em quinhentas linhas quando ele pode morar numa linha só e ser referenciado.
Assim como fixamos uma convenção pro código de permissão no artigo anterior, vale fixar uma pro slug de sectors e hierarchies: minúsculo, sem acento, sem espaço, e definido uma vez só. slug é o que seu código usa pra tomar decisão (if hierarchy.slug == "gerente"), name é só o que aparece na tela. Renomear o name de “RH” pra “Recursos Humanos” não deveria quebrar nada; renomear o slug sim, porque ele vira o identificador de fato. E nada de prefixo redundante: financeiro, não setor_financeiro; gerente, não hierarquia_gerente, o nome da tabela já diz o que é.
Repare que hierarchies não guarda nenhuma ordem ou nível numérico entre os cargos. Proposital: cargo muda, a empresa cria um cargo novo no meio da estrutura amanhã (um “Coordenador” entre Analista e Gerente, por exemplo), e se a ordem estivesse fixada como número, toda inserção no meio virava migração pra renumerar todo mundo abaixo. hierarchies é só catálogo. Quem define o que aquele nível pode fazer são as permissões atribuídas a ele, não a posição dele numa régua.
O cruzamento setor + hierarquia vira uma terceira tabela, role_levels, que referencia as duas num relacionamento 1:N em cada lado (um setor tem vários role_levels, uma hierarquia tem vários role_levels). Cada linha dela é uma combinação específica: Gerente do Financeiro, Analista do RH, Estagiário de TI… e é essa linha, não o setor sozinho, não a hierarquia sozinha, que carrega as permissões, através da mesma relação N:N que já usávamos entre função e permissão no artigo anterior. Só que agora ela liga permissions a role_levels.
O vínculo do usuário também muda de lugar: em vez de apontar direto pra uma função genérica, o usuário se conecta a uma ou mais linhas de role_levels, porque, na prática, alguém pode acumular cargos diferentes, e cada vínculo carrega seu próprio conjunto de permissões, sem misturar um com o outro.
Isso é fundamentalmente uma decisão de negócio, não uma regra do modelo: dá pra restringir um usuário a no máximo uma conexão com role_levels, se a sua empresa não permitir acúmulo de cargo.
Dá pra pensar: “mas Gerente é Gerente, não dá pra ter uma permissão base pra todo Gerente e só as extras por setor?” Na teoria dá, bastaria ligar permissão direto em hierarchies também, além de role_levels, e unir os dois conjuntos na hora de checar acesso. Só que isso descreve um cenário que raramente existe: gerentes de setores diferentes quase nunca compartilham a mesma superfície de permissão. Aprovar pagamento não tem nada a ver com aprovar férias. Procurar uma “base comum” entre Financeiro e RH vira exercício artificial, e manter dois lugares de atribuição de permissão (hierarquia e setor) custa mais do que resolve. Por isso a atribuição fica concentrada só em role_levels: uma única origem de verdade pro que aquele vínculo pode fazer, sem ambiguidade se a permissão veio do cargo, do setor, ou dos dois.
Isso é a teoria. Na prática, essa é uma consulta buscando as permissões de um role_level específico, com a sintaxe do PostgreSQL:
SELECT DISTINCT "permissions"."code"
FROM "permissions"
INNER JOIN "permissions__roles_levels"
ON "permissions"."id" = "permissions__roles_levels"."permissions_id"
INNER JOIN "users__roles_levels"
ON "permissions__roles_levels"."role_level_id" = "users__roles_levels"."role_id"
WHERE "users__roles_levels"."user_id" = :user_id;
Repare no DISTINCT: como um usuário pode ter mais de um role_level, e dois role_levels podem compartilhar uma permissão, sem ele o mesmo código de permissão pode voltar duplicado. É essa lista de códigos que vira a checagem de acesso na aplicação, o que não estiver nela, o usuário não pode fazer.
O padrão de fundo não mudou desde o artigo anterior: permissão é ausência por padrão, o princípio do menor privilégio continua valendo, e N:N continua sendo o relacionamento certo entre usuário e aquilo que define o que ele pode fazer. O que mudou foi entender que função pode ser composta, e modelar isso como uma tabela própria de cruzamento, em vez de espalhar lógica de setor e cargo pelo resto do sistema, é o que evita que seu RBAC vire um emaranhado de exceções.