feat(rbac): expose admin scopes over the API - #775
Merged
Conversation
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>
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.
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 asystem_admin-only act.Non-delegable scopes are returned with
delegable: falserather 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.adminScopeson/users/me/permissions— plusskills, whichUserEffectivePermissionshas 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-scopeswould 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_routeasserts 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/statsfurther down the same router is safe only because it has two segments — worth knowing before adding another single-segment literal route there.Frontend
UserPermissionsgains both fields, since CLAUDE.md requires the TS interfaces to match the API shape. Inert — the admin guard and nav still gate onsystem_admin; wiring them up is PR-4. Typechecked clean, andversion.tswas not stamped.Spec
Corrects §6.2, which described the dependency as
_requireacross 15 files — it shipped asrequire_<area>_adminacross 13 packages. Also records what PR-2 and PR-3 added beyond the original plan (theRoleMutationForbidden→ 403 handler, the reload-proofoverride_admin_authtest helper, and this route-ordering constraint), so the spec reads as-built rather than as-designed.Next
PR-4 is the last one:
adminScopessignal +hasAdminScope/canAccessAdmin, per-routeadminScopeGuard, nav filtering, the landing resolver (/admincurrently redirects tocosts, which a skills-only admin can't reach), and the scope picker on the role form.🤖 Generated with Claude Code