Skip to content

feat(rbac): expose admin scopes over the API - #775

Merged
philmerrell merged 1 commit into
developfrom
feature/admin-scopes-api
Jul 27, 2026
Merged

feat(rbac): expose admin scopes over the API#775
philmerrell merged 1 commit into
developfrom
feature/admin-scopes-api

Conversation

@philmerrell

Copy link
Copy Markdown
Contributor

PR-3 of docs/specs/granular-admin-permissions.md. Follows #770 (data + registry) and #774 (enforcement).

Serves the scope registry and the caller's own scopes, so PR-4 has something to build the SPA against. No behavior change — nothing consumes these yet. Full backend suite green: 5406 passed, 3 skipped.

What this adds

GET /admin/roles/admin-scopes — the closed registry that will feed the role form's scope picker. It lives on the roles router, which is non-delegable: granting admin scopes is a system_admin-only act.

Non-delegable scopes are returned with delegable: false rather than filtered out. Otherwise an admin sees 13 areas and has no way to know why Roles and Auth Providers aren't among them. The client renders them disabled; the server rejects them regardless.

adminScopes on /users/me/permissions — plus skills, which UserEffectivePermissions has carried for months without it ever reaching the response. That's the field-added-to-model, forgotten-in-the-response-shape bug the spec called out; fixing it here rather than leaving a second instance of the same class. Both default to [], so a client on an older build is unaffected.

The trap worth a second look

The registry handler must be declared above /{role_id}. FastAPI matches in declaration order and a single-segment literal loses to a single-segment path parameter declared first — so /roles/admin-scopes would be read as a request for a role named "admin-scopes".

The failure mode is a 404 at runtime, not an error at import, which is exactly the kind of thing that survives review. test_registry_route_is_not_shadowed_by_the_role_id_route asserts declaration order; I verified it fails when the handler is moved (and that 4 tests go red, so the shadowing genuinely breaks the endpoint rather than just tripping the assertion).

Note /cache/stats further down the same router is safe only because it has two segments — worth knowing before adding another single-segment literal route there.

Frontend

UserPermissions gains both fields, since CLAUDE.md requires the TS interfaces to match the API shape. Inert — the admin guard and nav still gate on system_admin; wiring them up is PR-4. Typechecked clean, and version.ts was not stamped.

Spec

Corrects §6.2, which described the dependency as _require across 15 files — it shipped as require_<area>_admin across 13 packages. Also records what PR-2 and PR-3 added beyond the original plan (the RoleMutationForbidden → 403 handler, the reload-proof override_admin_auth test helper, and this route-ordering constraint), so the spec reads as-built rather than as-designed.

Next

PR-4 is the last one: adminScopes signal + hasAdminScope/canAccessAdmin, per-route adminScopeGuard, nav filtering, the landing resolver (/admin currently redirects to costs, which a skills-only admin can't reach), and the scope picker on the role form.

🤖 Generated with Claude Code

PR-3 of docs/specs/granular-admin-permissions.md. Serves the scope
registry and the caller's own scopes, so PR-4 has something to build the
SPA against. No behavior change: nothing consumes these yet.

- GET /admin/roles/admin-scopes returns the closed registry that feeds
  the role form's scope picker. Non-delegable scopes are included with
  `delegable: false` rather than filtered out, so the picker can show
  them as unavailable instead of leaving an admin wondering why two
  areas are missing. Lives on the roles router, which is non-delegable
  — granting scopes is a system_admin-only act.

- /users/me/permissions gains `adminScopes`, and `skills`, which
  UserEffectivePermissions has carried for months without it ever
  reaching the response — the field-added-to-model,
  forgotten-in-the-response-shape bug the spec flagged. Both default to
  [] so an older client is unaffected.

The registry handler must be declared above `/{role_id}`: FastAPI
matches in declaration order and a single-segment literal loses to a
single-segment path parameter declared first. The symptom is a 404, not
an import-time error, so a declaration-order test guards it — verified
to fail when the handler is moved.

Frontend `UserPermissions` gains both fields to keep the cross-package
contract honest. The guard and nav still gate on system_admin; wiring
them up is PR-4.

Also corrects spec §6.2, which described the dependency as `_require`
across 15 files — it shipped as `require_<area>_admin` across 13 — and
records what PR-2 and PR-3 added beyond the plan.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@philmerrell
philmerrell merged commit 03a6a1d into develop Jul 27, 2026
4 checks passed
@philmerrell
philmerrell deleted the feature/admin-scopes-api branch July 27, 2026 21:24
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