Monalisa
Garantias do banco

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 titularAceita permissões de escopo
platformplatform
tenanttenant, broker
brokerbroker

Os titulares e suas tabelas de ligação:

TitularEscopo vem deTabela de ligação
access_profilescoluna gerada scope (tenant sem broker_id, broker com)access_profile_permissions
access_profile_templatescoluna scopeaccess_profile_template_permissions
platform_access_profilessempre platformplatform_access_profile_permissions
platform_access_profile_templatescoluna 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çaTriggerQuando
Inserir ou alterar uma ligação*_permissions_scope_ceiling nas 4 tabelas de ligaçãoBEFORE INSERT OR UPDATE
Mudar o escopo de um titularaccess_profiles_scope_ceiling, access_profile_templates_scope_ceiling, platform_access_profile_templates_scope_ceilingAFTER UPDATE, só quando scope muda
Mudar o escopo de uma permissãopermissions_scope_ceilingAFTER 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 AFTER dela 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:

MigrationArquivoO que faz
0012_authorization-scope-ceilingscope-ceiling.sqlMatriz, recusa e os triggers das ligações e dos titulares
0013_authorization-permission-scope-guardpermission-scope-guard.sqlO lado da permissão: o seeder atualiza permissions.scope a cada deploy
0017_authorization-scope-ceiling-locksscope-ceiling-locks.sqlRedefine 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.

Nesta página