feat: scope Editor group membership by group (#990 slice 3) - #1048
Merged
Conversation
…990 slice 3) The slice made `Role::parse` case-insensitive, which is the right call for consistency: slice 2's SQL filter already hides `Admin`-cased rows from Editors via `lower(role)`, and having Rust believe the same row is a Viewer would leave the two layers disagreeing about an account's role — how confused-deputy bugs start. Chasing that turned up the actual latent defect, which was never the casing: `Role::parse(&role).unwrap_or(Role::Viewer)` swallowed the failure. Falling back to the LEAST privilege is correct, but doing it silently means an account that suddenly reads as Viewer is an undebuggable mystery from the UI — and `users.role` is plain TEXT with no CHECK, so a hand-edited row or an external provisioner (#934) can put anything there. Now it warns with the username and the stored value. The fallback is unchanged; only the silence is gone. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fatia 3 de 5 da #990, a mais contida. Implementada por agente
codex exec(gpt-5.6-sol, high); diff e gate revisados e reexecutados por mim, com uma correção minha (60f6505).O que muda
A seção Grupos abre para Editor, limitada a filiação: ele vê só os grupos dele, com membros não-Admin e apps que ele pode tocar, e adiciona/remove membros nesses grupos. Criar, renomear e apagar seguem
RequireAdmin—rewrite_groupreescreveusers.groupse oaccess-groupsde todos os specs, efeito muito além do escopo de quem chamou.Detalhe sutil que o agente acertou
As rotas de filiação recebem o grupo no corpo do formulário, não no path — então a proteção não pode vir de um extractor de path: está no valor do form, com 404 para grupo fora do escopo.
E o caso do "adicionar membro" é mais fino do que parece: um membro recém-inscrito ainda não compartilha grupo nenhum com o Editor, então avaliar
may_touch_usersobre a filiação atual recusaria a própria operação que se quer permitir. Ele avalia sobre a linha proposta (existente + grupo novo), o que mantém a exclusão de Admin intacta — um Admin proposto continua Admin e é recusado. Está documentado no código.Verifiquei que remover de um grupo preserva as outras filiações (
merge_preserving_out_of_scope), e que um usuário inexistente cai em 404.Efeito colateral que exigiu julgamento, e a correção que fiz
O agente generalizou a lição da fatia 2 e tornou
Role::parsecase-insensitive — mudança num primitivo de autorização que eu não havia pedido. Avaliei a direção: antes, uma linha gravada comoAdminviravaNonee, porunwrap_or(Role::Viewer), a conta era rebaixada a Viewer; depois, é reconhecida como Admin.Conceder privilégio a partir de valor não-canônico anda na direção fail-open, o que me fez hesitar. O que decidiu a favor: consistência entre camadas. A correção da fatia 2 já esconde essas linhas do Editor via
lower(role)no SQL; se o Rust acreditasse que a mesma linha é um Viewer, SQL e código discordariam sobre o papel de uma conta — que é como nascem bugs de confused deputy.Mas isso descobriu o defeito de verdade, que nunca foi a caixa: o
unwrap_or(Role::Viewer)era silencioso. O fallback para o menor privilégio está certo; engoli-lo não. Uma conta que "de repente virou Viewer" é mistério impossível de depurar pela interface, e a coluna éTEXTsem CHECK. Agora emitewarncom o username e o valor armazenado — só a silêncio saiu.Outras verificações
may_touch_groupé o predicado novo no primitivo: irrestrito para Admin/token, exato e case-sensitive para Editor, consistente comSpec::access_allows. Teste inclui"Blue"≠"blue".can_access_sectionganhou só"groups"no nível Editor; o restante segue Admin-only e o Viewer continua sem alcançar seção alguma.may_touch_spec, então um app fora do escopo não aparece no card.Gate reexecutado por mim:
cargo testlimpo,clippy --all-targets -- -D warningssem warning,i18n-checkOK.🤖 Generated with Claude Code