feat: define the group-scope primitive for Editors (#990 slice 0) - #1045
Conversation
Resolve Editor memberships per request and fail closed so group revocation takes effect immediately. Centralize spec, user, assignment, and merge decisions without changing any handler behavior.
Segunda passada da revisãoEste é o primitivo em que as outras quatro fatias se apoiam, então fui atrás de buraco de verdade. O código passou; o achado é sobre a fatia 1, e já reforcei o prompt do agente com ele. Armadilha encontrada em Como Verificações que passaram nesta passada:
CI verde nos 5 checks; gate local reexecutado por mim antes do push. |
Fatia 0 de 5 do plano da #990. Não muda o comportamento de nenhuma tela — entrega só o primitivo, para as fatias seguintes optarem handler por handler sem cada uma inventar sua própria regra de acesso.
Implementada por agente
codex exec(gpt-5.6-sol, reasoning high); diff e gate revisados e reexecutados por mim.O que entra
crates/ruscker-admin/src/scope.rs: o extratorEditorScoperesolve{role, actor, groups, unscoped}e expõe os predicados que as fatias 1–3 vão consumir —may_touch_spec,may_touch_user,may_assign_groups,may_assign_roleemerge_preserving_out_of_scope.O modelo reusa o que já roda em produção: o escopo do Editor são os grupos dele, mesma ideia do
Spec::access_allowsque o proxy e a landing usam. Nada de ownership por app — o #517 rejeitou isso explicitamente.Decisões que ficaram documentadas no código
unscoped. O Editor nesse estado vê apenas apps abertos, e o erro vai para o log.actor: None, papel Admin) segue irrestrita: acesso de emergência tem de funcionar mesmo com o banco de contas danificado.set_groupsnão revoga sessão, então cachear grupos na sessão manteria vivo um acesso já revogado — tirar alguém de um grupo tem de valer no próximo clique. Há precedente do mesmo formato nomust_change_password_guard.access-usersfica Admin-only.is_open()exigeaccess-groupsEaccess-usersvazios, então uma ACL de usuário nomeado não oferece fronteira de grupo de onde derivar autoridade de Editor. Documentado como decisão deliberada, não deixado como acidente.Testes
Dez testes de unidade, um por invariante, nomeados pelo que protegem: Admin e token irrestritos; Editor alcança app aberto e app com grupo em comum, mas não o de outro time nem o restrito só por
access-users; Editor nunca toca conta Admin nem usuário sem grupo em comum; não concede grupo que não tem nem papel Admin;merge_preserving_out_of_scopepreserva o grupo de fora do escopo tanto ao adicionar quanto ao remover; escopo vazio só alcança apps abertos; e a filiação é relida a cada resolução.Duas observações da minha revisão (não bloqueantes, registradas para as próximas fatias)
merge_preserving_out_of_scopeordena a lista também no caminho Admin, então a ordem gravada dos grupos passa de "ordem do formulário" para alfabética quando isso for ligado na fatia 2. Determinístico, bom para teste, mas é mudança sutil e melhor estar escrita do que descoberta depois.must_change_password_guard— aquele tem o cache do Perf: must_change_password_guard faz SELECT + valida sessão 2x por request /admin/* #903, este não. Irrelevante aqui (nada ligado), mas é o momento de anotar: nas fatias 1–3 toda página escopada vai usá-lo, e se aparecer pressão de latência o cache existente é o padrão a copiar.Gate reexecutado por mim na branch:
cargo testlimpo,clippy --all-targets -- -D warningssem warning,i18n-checkOK.🤖 Generated with Claude Code