Skip to content

feat: scope Editor app administration by group (#990 slice 1) - #1046

Merged
milkway merged 2 commits into
mainfrom
feat/990-slice1-scope-apps
Jul 29, 2026
Merged

feat: scope Editor app administration by group (#990 slice 1)#1046
milkway merged 2 commits into
mainfrom
feat/990-slice1-scope-apps

Conversation

@milkway

@milkway milkway commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Fatia 1 de 5 da #990, a maior delas. Implementada por agente codex exec (gpt-5.6-sol, high); diff e gate revisados e reexecutados por mim, com uma correção minha no caminho do cache (e92c2ad).

O que muda

O escopo por grupo do Editor passa a valer nas Aplicações: specs.rs, spec_form.rs e as réplicas do dashboard.rs. Apps abertos seguem Editor-globais, como você decidiu. Admin e sessão de token seguem irrestritos. images.rs ficou de fora de propósito — mídia é biblioteca compartilhada, não tem dono.

Rota fora do escopo responde 404, não 403, para não vazar a existência do app.

O buraco que a revisão da fatia 0 tinha antecipado

specs.rs::index não itera Spec: faz o SELECT enxuto do #588 e depois enriquece as linhas com metadados do catálogo efetivo. Um id ausente desse mapa deixa access_groups vazio, e o app restrito pareceria aberto — que, sendo app aberto Editor-global, seria bypass silencioso com o teste feliz verde.

Tratado nos dois caminhos da listagem (linhas do banco e linhas "config-defined" do YAML): sem resolver no catálogo efetivo, o Editor escopado não vê. Nas mutações a autorização usa db::specs::fetch_one, a fonte autoritativa do spec no banco.

Vazamento que o agente achou, e a correção que eu fiz por cima

Achado dele: a chave do cache do snapshot do dashboard era só o locale — reaproveitá-la para Editor escopado serviria as linhas de um time para outro. Vazamento entre times via cache.

A correção dele foi desligar o cache para escopados. Correta, mas joga fora a razão de o cache existir: o #291 existe porque construir um snapshot lê o registry, consulta o catálogo efetivo e monta as linhas, e cada aba faz polling no seu próprio intervalo de 5 s. Sem cache, toda aba de Editor reconstrói a cada tick — exatamente a pressão de múltiplas abas que travou a admin atrás de front HTTP/1.1 no #1039/#1040.

Minha correção (e92c2ad): chave (locale, escopo). Admin/break-glass compartilham uma entrada, dois Editores do mesmo conjunto de grupos compartilham a deles, e ninguém lê a de outro escopo. A chave de escopo ordena e deduplica (["a","b"] e ["b","a"] são o mesmo escopo e não devem partir o cache) e junta pelo separador de unidade, para um grupo chamado a,b não forjar a chave de ["a","b"]. Teste prende os três casos.

Enumeração de rotas: conferida, não aceita de boa-fé

O teste scoped_editor_gets_404_on_every_foreign_app_or_replica_id_route lista 7 rotas de app e 4 de réplica. Comparei com o roteador: são exatamente 11 as rotas com id nesses três arquivos (edit, duplicate, POST /admin/specs/{id}, delete, repull, state/toggle, featured/toggle, logs, logs/stream, stop, restart). Cobertura exaustiva.

Outras verificações da revisão

  • Criação: app restrito tem de nascer com grupo do Editor (may_assign_groups + may_touch_spec sobre o candidato); app aberto passa por is_open.
  • Edição: valida o pedido, faz o merge preservando grupo de fora do escopo, e revalida o spec resultante — o Editor não consegue se expulsar do próprio app.
  • Dashboard: o handler constrói o escopo a partir do AdminSession para preservar o redirect do Viewer para / (Viewer role: drop admin-panel access — only change-password + logout #857) em vez de trocá-lo por 403.
  • Réplica: scoped_replica_spec resolve o spec dono antes de qualquer acesso ao backend e falha fechado quando o metadado não resolve.
  • Alteração no primitivo compartilhado (scope.rs, +10 linhas): só from_editor virando pub(crate) e um helper actor(). Nenhum predicado foi afrouxado.

Gate reexecutado por mim: cargo test limpo, clippy --all-targets -- -D warnings sem warning, i18n-check OK.

🤖 Generated with Claude Code

milkway and others added 2 commits July 29, 2026 19:33
…#990 slice 1)

The slice caught a real leak: the snapshot cache key was locale-only, so
a scoped Editor could be served another team's container rows. Its fix
was to skip the cache entirely for scoped Editors — correct, but it
throws away the reason the cache exists.

That cache is #291: building a snapshot reads the registry, queries the
effective catalog and assembles the rows, and N dashboard tabs each poll
on their own 5 s interval. Without it, every Editor tab rebuilds every
tick — the same many-tabs pressure that made the admin hang behind an
HTTP/1.1 front end in #1039/#1040.

Keyed by `(locale, scope)` instead: the unscoped Admin/break-glass view
shares one entry, two Editors with the same group set share theirs, and
no one can read another scope's. The scope key sorts and dedups the
group list, since `["a","b"]` and `["b","a"]` are the same scope and must
not split the cache, and joins on the unit separator so a group literally
named `a,b` cannot forge the key of `["a","b"]`. A test pins all of that.

The cache map's type is factored into `SnapshotCacheKey` /
`CachedSnapshot` — clippy's `type_complexity` fails the gate otherwise.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@milkway
milkway merged commit adb29e9 into main Jul 29, 2026
5 checks passed
@milkway
milkway deleted the feat/990-slice1-scope-apps branch July 29, 2026 22:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant