Skip to content

Reader role: an administrator made on the user page or through POST /admin/users stays a reader after a demotion - #852

Merged
schmitch merged 1 commit into
mainfrom
claude/812-admin-keeps-reader-role
Nov 3, 2026
Merged

schmitch merged 1 commit into
mainfrom
claude/812-admin-keeps-reader-role

Conversation

@schmitch

@schmitch schmitch commented Nov 3, 2026

Copy link
Copy Markdown
Member

Fixes #812

Problem

#792 (#810) gave the built-in reader role to every administrator migration 0013 finds. docs/changes/2.2.0/733.upgrade.md promises the same for accounts made after the upgrade: an admin "stays a reader, as an account created after the upgrade does". Admins made on 2.2 through the admin UI or API did not get it:

  • The user page's switch to Admin (UserAccess, web/src/routes/admin/users/Edit.tsx) assigned admin, then unassigned every other level role, reader included.
  • New user with access level Admin, or POST /admin/users with "role": "admin" (insert_with_role), assigned admin only.

An OIDC realm with admin groups or claim, or a credential-forwarding realm with admin_group, demotes through sync_admin_role(false), which removes admin and nothing else. So the account was left Roles only and lost all read access.

Change

I picked the first option from the issue: keep reader next to admin.

  • insert_with_role (crates/server/src/admin/users.rs) also assigns reader when role is admin, in the same transaction, for user accounts. Service accounts are refused admin already. They get no reader either way and stay Roles only.
  • UserAccess treats the Admin level as admin + reader. Switching to Admin assigns both, skipping roles the user already holds, then unassigns only writer. Switching from Admin to Reader takes admin alone, and to Roles only takes both. Assign-before-unassign is unchanged, so the last-admin 409 still leaves the user with nothing lost.
  • The create screen (Create.tsx) already sends role: admin, so the server change covers it. The user list needed no change: accessLevel shows the highest built-in role (admin), and RoleBadges leaves reader out like the other level roles. A test now checks both.

Why not the second option (sync_admin_role(false) assigns the realm's new_account_access):

  • It is what migration 0013 and the upgrade note already do: an administrator holds reader. With this change every admin matches that, however they became one.
  • It keeps sync_admin_role a pure admin toggle. It would not give reader back to an account an administrator deliberately set to Roles only, which a demotion path cannot tell apart.
  • A realm whose new accounts are Roles only would still leave a demoted admin with nothing, which the issue's Expected section rules out.

The admin API itself is unchanged (POST /admin/users gives one extra role on the existing role: admin value), and its screens are updated in this PR. Changelog fragment: docs/changes/2.2.0/812.fixed.md.

Tests

  • crates/server/tests/auth_realms.rs, an_admin_demoted_by_the_upstream_stays_a_reader: a credential-forwarding realm with groups_path/admin_group whose mock upstream lists no admin group.
    • An account created with "role": "admin" holds admin + reader.
    • A writer switched to Admin through the page's API calls (PUT admin, PUT reader, DELETE writer) holds admin + reader.
    • After one upstream login each, both are reader only.
    • A service account with role: admin is 400, and one without a role holds no role.
    • With the old insert_with_role this test fails (["admin"] instead of ["admin", "reader"]).
  • crates/server/tests/oidc_routes.rs, a_user_made_admin_on_the_user_page_stays_a_reader_after_an_idp_demotion: an IdP-provisioned reader is switched to Admin through the page's call (PUT admin; reader is kept). The next login without the admin group leaves reader.
  • web/src/routes/admin/users/userRoles.test.tsx:
    • The switch test now expects writer → admin to PUT admin and reader and DELETE writer, and admin → reader to DELETE only admin.
    • New: a reader made admin keeps reader, and the list shows them as admin with no extra role badge.
    • Both fail against the old Edit.tsx.
  • Ran:
    • cargo fmt --all -- --check: ok
    • cargo clippy -p forge-server --all-targets -- -D warnings: ok
    • cargo test -p forge-server --test auth_realms --test oidc_routes --test admin_rbac --test service_accounts --test admin --test invitations --test trusted_header: all pass (Postgres from mirror.gcr.io/library/postgres)
    • cargo xtask changelog --check: ok
    • pnpm run tsc: ok
    • pnpm exec vitest run src/routes/admin/users src/routes/admin/service-accounts: 49 passed
    • web/package.json has no lint script.

Generated by Claude Code

…admin/users stays a reader after a demotion

An administrator made after the upgrade held `admin` alone: the user page's
switch to Admin unassigned `reader`, and `POST /admin/users` with
`"role": "admin"` assigned `admin` only. An OIDC or credential-forwarding
realm that later took `admin` away (`sync_admin_role(false)`) left the
account with no built-in role, unlike the administrators migration 0013
found, who keep `reader` (#792).

- `insert_with_role` assigns `reader` next to `admin` for user accounts. A
  service account never holds `admin` and gets no `reader`.
- The user page's Admin level is `admin` plus `reader`: switching to it
  assigns both and unassigns only writer; switching back to Reader takes
  `admin` alone.

Fixes #812

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012e6DJXgdmBmbEhT2HcZAv5
@schmitch
schmitch added this pull request to the merge queue Nov 3, 2026
Merged via the queue into main with commit 403feea Nov 3, 2026
20 checks passed
@schmitch
schmitch deleted the claude/812-admin-keeps-reader-role branch November 3, 2026 18: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

2 participants