Teto de escopo das permissões
Triggers que impedem um perfil ou template de receber permissão acima do seu escopo, inclusive sob concorrência.
Toda permissão tem um escopo (platform, tenant ou broker), e todo titular de permissões também. O banco
garante que um titular nunca recebe permissão acima do seu escopo:
| Escopo do titular | Aceita permissões de escopo |
|---|---|
platform | platform |
tenant | tenant, broker |
broker | broker |
Os titulares e suas tabelas de ligação:
| Titular | Escopo vem de | Tabela de ligação |
|---|---|---|
access_profiles | coluna gerada scope (tenant sem broker_id, broker com) | access_profile_permissions |
access_profile_templates | coluna scope | access_profile_template_permissions |
platform_access_profiles | sempre platform | platform_access_profile_permissions |
platform_access_profile_templates | coluna scope (tenant ou broker: o template é copiado para um tenant) | platform_access_profile_template_permissions |
A matriz tem uma única fonte no banco, a função permission_scope_ceiling(holder_scope). O código de domínio tem
um espelho dela para devolver o erro antes de chegar ao banco, e um teste prende os dois juntos.
Três direções
| Mudança | Trigger | Quando |
|---|---|---|
| Inserir ou alterar uma ligação | *_permissions_scope_ceiling nas 4 tabelas de ligação | BEFORE INSERT OR UPDATE |
| Mudar o escopo de um titular | access_profiles_scope_ceiling, access_profile_templates_scope_ceiling, platform_access_profile_templates_scope_ceiling | AFTER UPDATE, só quando scope muda |
| Mudar o escopo de uma permissão | permissions_scope_ceiling | AFTER UPDATE OF scope |
O trigger de access_profiles é AFTER UPDATE (e não UPDATE OF scope) porque scope é uma coluna gerada
STORED: ela só é calculada depois dos triggers BEFORE e não pode ser nomeada em UPDATE OF.
A recusa tem uma forma só, que quem chama pode verificar: SQLSTATE 23514 (check_violation), constraint
permission_scope_ceiling e uma mensagem com o código da permissão, o escopo dela e o escopo esperado. Uma
permissão ou um titular inexistente não é erro do teto: fica para a FK reportar.
Concorrência: os locks
As primeiras versões das checagens liam o titular e a permissão sem lock. Em READ COMMITTED, duas transações podiam
passar cada uma sozinha e ambas fazerem commit: uma ligação inserida enquanto o escopo do titular ou da permissão
mudava (NOVA-112). A migration 0017_authorization-scope-ceiling-locks redefine as duas leituras com
SELECT … FOR SHARE, que conflita com qualquer UPDATE dessas linhas:
- uma mudança de escopo que chega em segundo lugar espera a transação que segura o lock terminar; o guard
AFTERdela roda com um snapshot novo, vê a ligação já commitada e recusa; - uma leitura com lock que chega em segundo lugar espera o commit da mudança de escopo, relê o escopo novo e recusa.
A ordem dos locks é sempre a mesma: primeiro a linha da permissão, depois a do titular. Uma transação que altera um titular e depois liga uma permissão que outra transação está reescopando ainda pode entrar em deadlock; o PostgreSQL aborta uma delas, o que recusa a operação e nunca a deixa passar.
A convenção de SQL congelado
O teto foi construído em três arquivos, cada um carregado por uma migration própria, porque SQL mergeado não se edita:
| Migration | Arquivo | O que faz |
|---|---|---|
0012_authorization-scope-ceiling | scope-ceiling.sql | Matriz, recusa e os triggers das ligações e dos titulares |
0013_authorization-permission-scope-guard | permission-scope-guard.sql | O lado da permissão: o seeder atualiza permissions.scope a cada deploy |
0017_authorization-scope-ceiling-locks | scope-ceiling-locks.sql | Redefine as leituras com FOR SHARE (mesmas assinaturas, os triggers seguem chamando) |
A correção de concorrência não editou scope-ceiling.sql: ela é um arquivo novo que faz CREATE OR REPLACE das
funções. Os três arquivos estão fixados por sha256 (ver SQL congelado).
O que fica fora do banco
O teto compara escopos. "Quem concede só concede o que tem", "ninguém concede a si mesmo" e a posição de quem concede são regras das rotas de concessão (NOVA-43, NOVA-59), não do banco.