fix(rbac): RBAC dynamic grants + API ACL enforcement#998
Merged
Conversation
- admin-users.ts: replace static ROLES constant with fetchRoleOptions()
that queries RbacService.getRoles() so user-edit and user-new forms
always reflect live RBAC roles; validate submitted role against DB
- api.ts: wire isAllowed ACL check into all four GET content routes
(/:collection, /:collection/:id, /content, /collections/:c/content);
authenticated non-anonymous users filtered by DocumentRepository.isAllowed
against type settings; list routes return 403 when principal has no
type-level read grant; cache disabled for authenticated users
- document-permissions.ts: fix 'public' key collision — { type: 'role',
id: 'public' } no longer matches baseGrants['public'], which is reserved
for anonymous { type: 'public', id: '*' } principals only
- E2E specs 97 and 98 covering dynamic role dropdown and API ACL
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Admin can grant a role read access to a document type at runtime via the RBAC matrix UI. Previously the API only checked code-time baseGrants, so runtime grants had no effect — authenticated users with RBAC-only grants got 403/404 even after the grant was set. Now all four GET routes in api.ts consult both systems via typeReadAllowed (baseGrants first, RBAC fallback) and RbacService.isGrantedForRole. Per-doc deny overrides still apply on the baseGrants path; RBAC-only grants pass all rows (type-level grant implies row-level access). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… and grant→access E2E
- rbac-documents.test.ts: 6 new tests for isGrantedForRole — slug match, name
match, wildcard resource, grant cleared, verb mismatch, unknown role
- documents.sqlite.test.ts: 2 new tests for the baseGrantAllows collision fix —
authed { type:role, id:'public' } must not inherit baseGrants['public']
- rbac-enforcement.test.ts: update stale static role list tests to match the
dynamic role slugs (role-admin, role-editor) used by admin-users.ts
- tests/e2e/99-rbac-grant-api-access.spec.ts: regression test for the reported
bug — grant public role read via RBAC matrix → /api/example unblocks; revoke
→ 403 again
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Lock file was stale in this worktree — esbuild version mismatch and missing better-sqlite3 entry caused npm ci to fail in CI. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Branch added no new npm packages. Worktree had a stale/diverged lock file causing npm ci to fail on esbuild version mismatch and missing better-sqlite3/emnapi entries. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ease The v3.0.0-beta.24 release commit (801789c) stripped node_modules/better-sqlite3 from the lock file, breaking npm ci for all PRs and main since July 3. Regenerated the lock file fresh (esbuild now resolves to 0.28.1 as wrangler@4.105.0 requires) and patched back the better-sqlite3@12.11.1 and @emnapi/runtime|core@1.11.1 entries that were previously present. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The v3.0.0-beta.24 release commit stripped better-sqlite3 from the lock file and macOS-regenerated versions lacked Linux native binaries (@rollup/rollup-linux-x64-gnu, @esbuild/* Linux variants) needed for CI. Using lock file from 0235ba3 (last known-good state before the release stripped entries). All CI-required packages present: - better-sqlite3@12.11.1 - @rollup/rollup-linux-x64-gnu@4.62.2 - wrangler/node_modules/esbuild@0.28.1 (nested, avoids top-level conflict) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
typeReadAllowed now distinguishes "role not found in RBAC document store" (legacy names like viewer/author → default allow) from "role found but has no grants" (public role → deny). Adds hasRbacRole() to RbacService. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
bb9491e to
72163c5
Compare
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.
Summary
RbacService.getRoles()) instead of a hardcoded static list — custom roles created in the RBAC panel immediately appear in the user-edit dropdown.api.ts(/:collection,/collections/:collection/content,/content,/:collection/:id) now enforce document ACL for authenticated users — previously onlyis_publishedwas checked, so any logged-in user could read content regardless of role grants.baseGrants(registered at document-type boot) AND runtime RBAC grants (set via the RBAC matrix UI). Previously onlybaseGrantswas consulted, so changes made through the RBAC UI had no effect on API responses.baseGrants['public']collision fix: An authenticated user whoseauth_user.roleis'public'no longer inherits the anonymousbaseGrants.publicbucket — fixing a security gap where a logged-in public-role user bypassed ACL the same way an unauthenticated visitor does.Changes
routes/admin-users.tsROLESconstant withfetchRoleOptions(db)async helper backed byRbacService.getRoles()routes/api.tstypeReadAllowed()helper (baseGrants + RBAC fallback); enforce ACL on all 4 GET routes; disable cache for authenticated requestsservices/document-permissions.tsbaseGrants['public']lookup for{ type: 'role', id: 'public' }principalsservices/rbac.tsisGrantedForRole(roleSlugOrName, resource, verb)public methodisGrantedForRoleunit tests; 2 newbaseGrantscollision security tests; stale role-list tests updated; 3 new E2E specsTesting
rbac-documents.test.ts(15 tests),documents.sqlite.test.ts(29 tests),rbac-enforcement.test.ts(14 tests)97-user-edit-dynamic-roles,98-public-role-api-acl,99-rbac-grant-api-access(CI validates)Security notes
is_publishedfast-path is gone for non-anonymous callers.is_published = 1.🤖 Generated with Claude Code