Skip to content

feat: scope Editor user administration by group (#990 slice 2) - #1047

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

feat: scope Editor user administration by group (#990 slice 2)#1047
milkway merged 2 commits into
mainfrom
feat/990-slice2-scope-users

Conversation

@milkway

@milkway milkway commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Fatia 2 de 5 da #990. Implementada por agente codex exec (gpt-5.6-sol, high); diff e gate revisados e reexecutados por mim, com uma correção minha de segurança (bb2fcfc).

O que muda

A seção Usuários abre para Editor, escopada pelos grupos dele. Ele nunca vê conta Admin. Cria usuário restrito a Viewer/Editor e aos grupos dele, edita perfil e reseta senha de quem está no escopo. Seguem Admin-only: apagar conta, importar CSV e resetar MFA. Rota com username fora do escopo responde 404.

O agente acertou a armadilha da paginação

Foi o risco que eu tinha destacado no prompt: list_page/count_filtered paginam no SQL, então filtrar em Rust depois da página faria a contagem mentir e a página 2 vir vazia — bug que passa em teste unitário e aparece em produção com 120 contas, que é o volume do hugo.

Ele empurrou o filtro para dentro do SQL, nos dois dialetos, e o teste é da forma certa: semeia 49 usuários visíveis e 60 estrangeiros mais novos, de modo que filtragem pós-página deixaria a página 1 vazia.

O casamento de grupo também está correto onde é fácil errar: usa instr/strpos sobre ',' || groups || ',' com o padrão ,time-a,, então time-a não casa com time-ab, um % ou _ no nome do grupo não vira curinga, e a comparação segue case-sensitive como o Spec::access_allows que já governa landing e proxy. Conferi que a coluna é de fato separada por vírgula (join_groups/parse_groups), e escopo vazio virou 1=0 — Editor sem grupo não vê ninguém.

Minha correção: exclusão de Admin era sensível a caixa

role <> 'admin' só funciona porque users.role é TEXT sem CHECK e todo escritor passa por Role::as_str, que emite minúsculas. Uma linha gravada como Admin — à mão, ou por um escritor futuro: o #934 (OIDC/LDAP) adiciona um — passaria pela única fronteira que mantém Editor longe de conta Admin. E só se manifestaria quando esse Admin compartilhasse grupo com o Editor, o que torna a falha rara e silenciosa.

Agora é lower(role) <> 'admin' nos dois braços. Não há índice em role a perder e a tabela é pequena.

O teste prende o buraco, não apenas o descreve: verifiquei que ele falha com o predicado antigo e passa com o novo.

Outras verificações da revisão

  • Enumeração de rotas conferida contra o roteador: são 7 rotas com username (edit, groups, role, password, profile, delete, mfa/reset) e o teste cobre as 7 para alvo de outro time.
  • Auto-edição bloqueada (papel, grupos e senha própria), com o teste relendo o banco para confirmar que papel e grupos não mudaram — não só o status HTTP.
  • Invariante 6: grupo de fora do escopo sobrevive à edição do Editor, e o formulário mostra esses grupos como somente-leitura, para ele entender por que continuam ali.
  • Auditoria: o teste confirma que o actor gravado é o Editor.
  • Mudanças no código compartilhado (auth.rs +28, scope.rs +13): só "users" entrando no nível Editor do can_access_section, com o teste existente estendido, e o helper user_filter que mapeia irrestrito→None e escopado→Some(groups) — com a distinção documentada de que Some([]) não casa nada, para um lookup falho nunca virar a consulta irrestrita. Nenhum predicado 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 20:08
…ew of #990 slice 2)

The scope filter excluded admin accounts with `role <> 'admin'`. That
holds today only because `users.role` is plain TEXT with no CHECK and
every writer goes through `Role::as_str`, which emits lowercase. A row
stored as `Admin` — by hand, or by a future writer; #934 (OIDC/LDAP) adds
one — would slip past the single boundary that keeps an Editor away from
admin accounts, and only when that admin happened to share the Editor's
group.

Now `lower(role) <> 'admin'` in both dialect arms. There is no index on
`role` to lose, and the users table is small (hugo runs ~120 accounts).

The test seeds an admin row with `Admin` casing sharing the Editor's group
and asserts it stays out of the list. I verified it FAILS with the old
predicate and passes with the new one, so it pins the hole rather than
merely describing it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@milkway
milkway merged commit c900f59 into main Jul 29, 2026
5 checks passed
@milkway
milkway deleted the feat/990-slice2-scope-users branch July 29, 2026 23:17
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