Skip to content

RBAC: roles, permissions, admin UI, privacy page, and MCP context - #596

Merged
kentcdodds merged 23 commits into
mainfrom
cursor/rbac-integration-66a6
Jul 3, 2026
Merged

kentcdodds merged 23 commits into
mainfrom
cursor/rbac-integration-66a6

Conversation

@kentcdodds

@kentcdodds kentcdodds commented Jul 3, 2026 •

Copy link
Copy Markdown
Owner

Summary

Complete RBAC support, Epic Stack-style: users have roles, roles have permissions (action:entity:access strings), and a user's permissions are the union across their roles. Implemented by parallel subagents and merged into this single PR.

  • Schema — migration 0043-rbac.sql: roles, permissions, role_permissions, user_roles (integer users.id FKs, cascade deletes), with user/admin roles and their permissions seeded via INSERT OR IGNORE so every environment gets them. Migration 0044-rbac-backfill-user-role.sql backfills the user role for pre-RBAC accounts. Signup assigns the user role (and rolls the account back if that fails); user_roles joins the account-deletion cascade.
  • Typed registry + guards — permissions.ts (PermissionString template-literal type, so an unregistered permission is a compile error), permissions-db.ts (getUserRolesAndPermissions single-join query), permissions-server.ts (requireUserWithPermission, requireUserWithRole; login redirect when unauthenticated, 403 when unauthorized). Roles load fresh per request — never stored in cookies or OAuth grant props, so revocation is immediate; transient role-query failures fail closed to empty permissions instead of breaking auth. /session exposes roles/permissions for client-side nav gating.
  • Admin UI — /admin/users (paginated list, assign/remove roles, audit-logged) and /admin/roles (read-only roles + permissions), following the /account/secrets server-shell + JSON API pattern. The last-admin guardrail runs inside the DELETE statement itself (removeAdminRolePreservingLastAdmin), so concurrent removals cannot race the deployment down to zero admins. Admin nav link renders only for admins.
  • Privacy boundary — admins see account metadata only (id, username, email, created_at, updated_at, roles); never secret values/metadata, tokens, values, memories, packages, jobs, email, chat, storage, or connectors. Enforced structurally: the permission vocabulary has no content entities, admin queries touch identity tables only, and a unit shape test pins the payload. Public /privacy page + docs/use/privacy.md document the boundary, including the infrastructure-operator caveat.
  • MCP — optional roles/permissions on mcpUserContextSchema, resolved fresh per request in mcp-auth (with graceful fallback to the base grant identity on D1 errors); new requireMcpUserWithPermission helper. No existing capability is guarded — everything stays own-scoped.
  • Seed fixtures — node tools/seed-test-data.ts --local now seeds kody@example.com with the admin role (opt out with --no-admin) plus a regular companion account jane@example.com (user role only, local-only), so both sides of RBAC are testable out of the box. Both fixtures use password ilikecode. Custom --email accounts stay non-admin unless --admin is passed. The script also resolves the worker Wrangler config automatically, fixing the long-standing "cannot resolve APP_DB" gotcha.
  • Docs — docs/contributing/architecture/authorization.md is the single reference for the RBAC model, guards (with a copy-pasteable handler example), extension steps, privacy boundary, and first-admin bootstrap. primitives.yaml gains the rbac primitive with an amended per-user-isolation invariant, and project-intent.md names the account-administration exception. No separate agent skill — the docs carry the guidance.

First-admin bootstrap (manual, once):

INSERT OR IGNORE INTO user_roles (user_id, role_id)
SELECT u.id, r.id FROM users u, roles r
WHERE u.email = 'you@example.com' AND r.name = 'admin';

Review loop

All Bugbot and CodeRabbit findings were addressed (each has a threaded reply with the fixing commit):

  • Atomic last-admin guard (count check inside the DELETE; verified against real SQLite both blocked and allowed branches)
  • Signup fails loudly (500 + audit event) and rolls back the user row when the default role cannot be assigned
  • user role backfill migration for accounts that predate RBAC
  • Session and MCP role lookups fail closed/gracefully on transient D1 errors instead of taking auth down
  • Admin role POSTs carry the pagination query string so the UI stays on the viewed page
  • Companion seed fixture gated to local-only so the fixed password never reaches remote environments
  • Docs wording fix; seed script no longer logs the email address

Walkthrough

rbac_admin_ui_full_walkthrough.mp4
Full GUI demo: admin login → Admin nav → metadata-only user list → assign/remove admin role → last-admin guardrail error → read-only roles page → public privacy page → logout → non-admin login → no Admin link → 403 on direct /admin/users access. (Recorded before the fixture rename, so the regular account appears as twix; it is now jane@example.com / ilikecode.)

Admin users page (metadata only)
Last-admin guardrail error
Privacy page
Non-admin gets 403

System recap — adds the rbac primitive (high risk)

Mode: recap · Base: main · Head: da0c7e2

Classification: adds — new RBAC primitive (registry, guards, tables); primitives.yaml updated in this PR.

Primitives touched

Primitive Group Impact
rbac auth adds — typed permission registry, DB helpers, request guards
d1-app-db storage extends — migrations 0043-rbac.sql + 0044 backfill
app-sessions auth extends — roles/permissions on authenticated user + /session
app-ui surfaces extends — /admin/users, /admin/roles, public /privacy
mcp-oauth auth extends — fresh roles on McpCallerContext

System map

flowchart LR
	appUi["app-ui"]:::extended
	appSessions["app-sessions"]:::extended
	mcpOauth["mcp-oauth"]:::extended
	rbac["rbac"]:::added
	d1AppDb["d1-app-db"]:::extended
	capabilityRegistry["capability-registry"]:::untouched
	appUi --> appSessions --> rbac --> d1AppDb
	mcpOauth --> rbac
	capabilityRegistry -.-> rbac
	classDef touched fill:#1a7f37,color:#fff
	classDef extended fill:#9a6700,color:#fff
	classDef added fill:#cf222e,color:#fff
	classDef untouched fill:#57606a,color:#fff
Loading

Invariants

per-user-isolation amended in primitives.yaml: access='any' is the single, explicitly-guarded exception, limited to account administration (users, roles), never user content. Admin endpoints never touch content tables; a unit shape test pins the admin payload to metadata fields. Respects compact-mcp-surface: no new MCP tools.

Plan vs actual

Shipped per the RBAC proposal with three small drifts: permissions-db.ts split out to break a circular import; mcp-auth-user-context.ts extracted for node-testability; E2E gained seedE2eUser to avoid signup rate limits during parallel test runs. Review loop added the atomic last-admin guard, signup rollback, role backfill migration, and fail-closed role lookups. Follow-ups made the default seed an admin + regular fixture pair, renamed fixtures to jane@example.com / ilikecode, and dropped the agent skill in favor of the authorization doc.

Testing

  • ✅ npm run validate green on this tree (format, lint, typecheck, 370+ unit tests, Playwright E2E incl. admin-rbac.spec.ts, MCP E2E) — CI runs the same gate and is green
  • ✅ Manual curl verification: /session roles for admin vs non-admin, metadata-only admin payload, 403 for non-admin, 302 unauthenticated, last-admin guardrail, assign/remove round-trip, pagination-preserving POST
  • ✅ Atomic last-admin DELETE verified against real SQLite (local D1): blocked with one admin, allowed with two
  • ✅ npm run migrate:local applies the 0044 backfill; all pre-existing accounts gain the user role
  • ✅ Seed script verified end-to-end from a clean role state: node tools/seed-test-data.ts --local seeds kody (admin) + jane (regular); both logins verified via /session with the new ilikecode password, and jane gets 403 on /admin/users.json
  • ✅ Full GUI walkthrough via browser (video above)
Open in Web Open in Cursor 

Summary by CodeRabbit

  • New Features
    • Added RBAC-driven admin pages for viewing roles/permissions and managing user roles.
    • Added a Privacy page, linked from login and account screens.
    • Signed-in session info now includes roles and permissions (server-validated for access).
  • Bug Fixes
    • Enforced admin-only access to admin interfaces and admin role APIs.
    • Prevented removing the last remaining admin role (returns a safe error when attempted).
    • Improved signup reliability by rolling back failed default role assignment.
  • Documentation
    • Added/updated authorization (RBAC) architecture docs and privacy guidance.
  • Tests
    • Added end-to-end coverage for admin RBAC access and privacy boundaries.

Ship /privacy as an unauthenticated page describing per-account data storage,
admin visibility boundaries, and deployment operator caveats per the RBAC
proposal. Link from login, signup, and account pages; add docs/use/privacy.md;
extend smoke E2E to assert the page renders without authentication.
- Add D1 migration 0043-rbac.sql with roles, permissions, and seed data
- Add typed permission registry and DB/server guard utilities
- Load roles/permissions on authenticated user and /session payload
- Assign user role at signup; seed script --admin flag
- Include user_roles in account deletion cascade
Extend mcpUserContextSchema with optional roles/permissions arrays.
Load fresh RBAC data in mcp-auth when building McpCallerContext via
users email lookup and getUserRolesAndPermissions. Add
requireMcpUserWithPermission helper for future admin capabilities.
Includes unit tests for schema, context construction, and permission guard.
Implement /admin/users and /admin/roles with server shells, JSON APIs,
client routes, role assignment/removal with last-admin guardrail, audit
logging, privacy-boundary shape tests, and Playwright E2E coverage.
Avoid /auth signup rate limits in shared E2E runs by seeding fixture
users through the wrangler D1 wrapper and using login-only auth.
Add present-tense authorization.md covering the shipped RBAC model, guards,
admin routes, MCP context, privacy boundary, and first-admin bootstrap.
Add .agents/skills/rbac/SKILL.md for copy-paste guard patterns.

Update project-intent.md and primitives.yaml with the account-administration
exception to per-user isolation. Link from architecture index and
authentication.md. Delete the RBAC proposal now that behavior is documented.
The default page size of 20 hides newly seeded users when the E2E database
accumulates accounts from prior runs. Request pageSize=100 so the spec
remains stable on shared local E2E state.
@coderabbitai

coderabbitai Bot commented Jul 3, 2026 •

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR adds RBAC roles and permissions across worker logic, client routes, MCP context handling, migrations, tests, docs, and seed tooling. It also adds admin user/role management, privacy pages, last-admin protection, and session propagation of roles and permissions.

Changes

RBAC Feature

Layer / File(s) Summary
Permission model and guards
packages/worker/migrations/0043-rbac.sql, 0044-rbac-backfill-user-role.sql, packages/worker/src/app/permissions.ts, permissions-db.ts, permissions-server.ts, permissions.node.test.ts, permissions-server.node.test.ts, packages/worker/tsconfig-client.json
Adds the RBAC schema and seed data, typed permission and role helpers, database access for assignments and permission lookup, request guards, and supporting tests and tsconfig wiring.
Authenticated user and signup wiring
authenticated-user.ts, session.ts, auth.ts, account-deletion.ts, audit-log.ts, related tests
Extends authenticated-user and session payloads with roles and permissions, assigns the default role during signup with rollback on failure, deletes user_roles during account deletion, and adds an 'admin' audit category.
Admin handlers and route wiring
handlers/admin-users.ts, handlers/admin-roles.ts, router.ts, routes.ts, related tests
Adds admin UI/API handlers for listing users, assigning and removing roles with last-admin protection and audit logging, plus the admin roles listing handler and backend route wiring.
Client admin and privacy surfaces
client/session.ts, client/app.tsx, client/routes/admin-users.tsx, admin-roles.tsx, privacy.tsx, account.tsx, login.tsx, index.tsx, handlers/privacy.ts
Parses roles and permissions client-side, shows an Admin nav link, adds admin users and roles routes, adds a privacy page, and links privacy from account and login pages.
MCP user context RBAC enrichment
chat.ts, mcp-auth-user-context.ts, mcp-auth.ts, require-permission.ts, related tests
Extends MCP context schemas with optional roles and permissions, builds MCP user context from grant props with fresh RBAC lookups, updates MCP auth request handling, and adds an MCP capability permission guard.
E2E seeding and RBAC coverage
e2e/d1-utils.ts, e2e/playwright-utils.ts, e2e/admin-rbac.spec.ts, e2e/smoke.spec.ts
Adds D1 seeding and role-assignment helpers, Playwright fixtures for role assignment and login modes, and end-to-end admin RBAC and privacy smoke coverage.
RBAC documentation and seed tooling
.agents/skills/rbac/SKILL.md, docs/contributing/architecture/*, docs/use/privacy.md, tools/seed-test-data.ts
Adds the RBAC skill guide, authorization architecture docs, privacy documentation, contributing index updates, and seed CLI admin flag support.

Estimated code review effort: 4 (Complex) | ~75 minutes

Sequence Diagram(s)

sequenceDiagram
  participant AdminUI as AdminUsersRoute
  participant Handler as createAdminUsersApiHandler
  participant Permissions as permissions-server
  participant Db as permissions-db
  participant Audit as audit-log

  AdminUI->>Handler: POST /admin/users.json {action, userId, role}
  Handler->>Permissions: requireUserWithPermission('update:user:any')
  Handler->>Db: assignUserRole/removeUserRole
  Db-->>Handler: updated role rows
  Handler->>Audit: logAuditEvent(category:'admin')
  Handler-->>AdminUI: refreshed user list payload
Loading
sequenceDiagram
  participant Client
  participant McpAuth as mcp-auth.ts
  participant ContextBuilder as buildMcpUserContextFromGrantProps
  participant Db as permissions-db

  Client->>McpAuth: MCP request with bearer token
  McpAuth->>ContextBuilder: buildMcpUserContextFromGrantProps(env, grantProps)
  ContextBuilder->>Db: getUserRolesAndPermissions(userId)
  Db-->>ContextBuilder: roles, permissions
  ContextBuilder-->>McpAuth: McpUserContext
  McpAuth->>Client: createMcpCallerContext(mcpUser)
Loading

Possibly related PRs

  • kentcdodds/kody#39: Both PRs modify packages/worker/src/app/handlers/auth.ts, so they touch the same signup/auth handler path.
  • kentcdodds/kody#418: This PR extends the account-deletion cascade to include user_roles, which builds on the same deletion path.
  • kentcdodds/kody#453: Both PRs extend packages/shared/src/chat.ts and the MCP user context shape.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main RBAC, admin UI, privacy, and MCP context changes in the PR.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch cursor/rbac-integration-66a6

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@kody-bot
kody-bot marked this pull request as ready for review July 3, 2026 05:24
@cursor cursor Bot changed the title RBAC integration: docs, agent skill, and merged stack RBAC: roles, permissions, admin UI, privacy page, and MCP context Jul 3, 2026
@cursor
cursor Bot changed the base branch from cursor/rbac-admin-ui-66a6 to main July 3, 2026 05:31
Comment thread packages/worker/src/app/handlers/auth.ts
Comment thread packages/worker/src/app/handlers/admin-users.ts Outdated
@github-actions

github-actions Bot commented Jul 3, 2026 •

Copy link
Copy Markdown
Contributor

🔎 Preview deployed: https://kody-pr-596.kentcdodds.workers.dev

Worker: kody-pr-596
D1: kody-pr-596-db
KV: kody-pr-596-oauth-kv

Mocks:

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (6)
packages/worker/src/app/handlers/privacy.ts (1)

6-11: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Set a page title for the Privacy route.

Other new handlers in this PR (e.g. admin users) pass a contextual title to Layout, but this one leaves the default 'kody' title, so the browser tab won't reflect the Privacy page.

✏️ Proposed fix
 export const privacy = {
 	middleware: [],
 	async handler() {
-		return render(Layout({}))
+		return render(Layout({ title: 'Privacy' }))
 	},
 } satisfies Action<typeof routes.privacy>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/worker/src/app/handlers/privacy.ts` around lines 6 - 11, The Privacy
route handler currently renders Layout without a contextual title, so it falls
back to the default app title. Update the privacy handler’s render call to pass
an appropriate Privacy page title into Layout, following the same pattern used
by the other handlers in this PR (for example the admin users handler).
e2e/playwright-utils.ts (1)

90-140: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate signup/login-fallback logic across fixtures.

The signup-with-409-fallback-to-login flow here largely duplicates the logic already in insertNewUser (Lines 45-65). Consider extracting a shared helper (e.g. signupOrLogin(request, { email, username, password, mode })) used by both fixtures to avoid drift between the two copies.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@e2e/playwright-utils.ts` around lines 90 - 140, The
signup-with-409-fallback-to-login logic in the `login` fixture duplicates the
existing flow in `insertNewUser`, which risks the two paths drifting apart.
Extract the shared request handling into a common helper such as `signupOrLogin`
that encapsulates the `/auth` POST, 409 check, fallback login, and error
handling, then have both `login` and `insertNewUser` call that helper with the
appropriate options.
packages/worker/src/app/handlers/session.ts (1)

33-61: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

Duplicate userId/roles-loading logic vs. readAuthenticatedAppUser.

This handler re-implements the same session→userId→user-record→roles/permissions pipeline that authenticated-user.ts's readAuthenticatedAppUser already centralizes. Consider reusing that helper here (adjusting for the cookie-destroy-on-missing-user branch) to keep the RBAC payload contract in one place.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/worker/src/app/handlers/session.ts` around lines 33 - 61, The
session handler is duplicating the session-to-user lookup and RBAC loading flow
already centralized in readAuthenticatedAppUser. Refactor the logic in the
session handler to reuse readAuthenticatedAppUser for resolving the user, roles,
and permissions, and keep only the missing-user branch that destroys the auth
cookie before returning jsonResponse. Use the existing symbols
readAuthenticatedAppUser, getUserRolesAndPermissions, and destroyAuthCookie to
consolidate the payload contract in one place.
packages/worker/src/app/permissions-db.ts (1)

30-37: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Hardcoded role-name filter duplicates the RoleName vocabulary.

roleSet is filtered against literal 'user'/'admin' strings while permissionSet accepts any row unconditionally. If a role is ever added without updating this check, its permissions would still be granted but its name silently dropped from roles, creating an inconsistent {roles, permissions} payload used downstream by session/MCP context.

♻️ Suggested fix using the shared vocabulary
-import { type PermissionString, type RoleName } from '`#app/permissions.ts`'
+import {
+	roleNames,
+	type PermissionString,
+	type RoleName,
+} from '`#app/permissions.ts`'
 	for (const row of result.results ?? []) {
-		if (row.role_name === 'user' || row.role_name === 'admin') {
+		if ((roleNames as ReadonlyArray<string>).includes(row.role_name)) {
 			roleSet.add(row.role_name)
 		}
 		permissionSet.add(formatPermissionString(row))
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/worker/src/app/permissions-db.ts` around lines 30 - 37, The role
filtering in the permissions mapping is hardcoded to literal user/admin strings,
which can drift from the shared RoleName vocabulary. Update the logic in the
result loop to use the RoleName source of truth already used by the type system
or shared constants instead of inline string checks, so any valid role is
consistently added to roleSet while permissionSet remains unchanged.
packages/worker/src/app/authenticated-user.ts (1)

53-65: 🚀 Performance & Scalability | 🔵 Trivial

Per-request roles/permissions fetch adds a second DB query to every authenticated request.

This is consistent with the PR's "fresh per-request role/permission loading" design, but since readAuthenticatedAppUser backs most guards/admin routes/MCP context, it's worth keeping an eye on latency/DB load as traffic grows (e.g. via caching with short TTL or batching the user lookup and roles/permissions query into one round trip).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/worker/src/app/authenticated-user.ts` around lines 53 - 65, The
authenticated-user path is doing a separate roles/permissions lookup for every
request, which adds extra DB load in the readAuthenticatedAppUser flow. Update
readAuthenticatedAppUser/getUserRolesAndPermissions to avoid the extra round
trip by either batching the user and role/permission fetch into one query or
adding a short-lived cache for roles/permissions keyed by user/session. Keep the
existing return shape from readAuthenticatedAppUser intact.
packages/worker/src/app/handlers/admin-roles.ts (1)

24-47: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate admin-page guard/render boilerplate.

This handler's body (role check → session read → redirect-to-login → render Layout → set-cookie) is near-identical to createAdminUsersHandler in admin-users.ts (lines 65-88). Consider extracting a shared renderAdminPage({ request, env, title }) helper to avoid drift between the two as more admin pages are added.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/worker/src/app/handlers/admin-roles.ts` around lines 24 - 47, The
admin page handler logic is duplicated between createAdminRolesHandler and
createAdminUsersHandler, including the role check, auth session lookup, login
redirect, Layout rendering, and Set-Cookie handling. Extract that shared flow
into a reusable helper such as renderAdminPage({ request, env, title }) and have
createAdminRolesHandler call it so the admin page handlers stay consistent as
more pages are added.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/contributing/architecture/authorization.md`:
- Around line 56-65: Clarify the RBAC table ownership wording in the
authorization docs so it does not imply all four D1 tables are keyed on
users.id. Update the table description near the
roles/permissions/role_permissions/user_roles section to state that only
user_roles is account-scoped to the session identifier, while roles,
permissions, and role_permissions are global/shared tables. Reference the
existing RBAC table names and the users.id mention to keep the wording precise.

In `@packages/worker/src/app/handlers/admin-users.ts`:
- Around line 321-355: The last-admin guard in admin-users is not atomic because
countUsersWithRole, loadRolesByUserIds, and removeUserRole run as separate DB
calls, so concurrent remove_role requests can bypass the check. Update the
remove flow in the relevant handler to enforce the invariant inside one
transactional write or a single conditional database operation, using the
existing removeUserRole path and the admin guard logic together so only one
request can succeed when the target is the final admin.

In `@packages/worker/src/app/handlers/auth.ts`:
- Line 7: The signup handler in auth.ts leaves a partial account if
assignUserRole fails because it is awaited without the same defensive handling
used around db.create. Update the auth flow around assignUserRole so failures
are caught, logged/audited, and handled before proceeding to cookie issuance or
success return; use the existing handler context and symbols like assignUserRole
and the user record id to either retry/self-heal or surface a hard failure. If
the role assignment cannot succeed, ensure the created user is not left as a
permission-less orphan, and emit an audit event for the failure path.

In `@packages/worker/src/mcp-auth.ts`:
- Around line 115-126: handleMcpRequest currently lets
buildMcpUserContextFromGrantProps throw during D1 lookup, which can fail the
entire MCP auth flow. Wrap the mcp user-context lookup in a try/catch around
buildMcpUserContextFromGrantProps and decide on a safe fallback path. If lookup
fails, either fall back to the grant-props user when possible or surface a clear
5xx response, and keep the downstream remote connector lookup and
createMcpCallerContext flow working off the resolved mcpUser.

In `@tools/seed-test-data.ts`:
- Around line 238-240: The seeding log in the test-data script is printing the
account email address, which exposes PII in stdout. Update the logging around
the seeded account message in the seed logic to stop including options.email,
while keeping the local/remote and admin context if needed. Use the existing
seed flow in tools/seed-test-data.ts and the console.log statement that reports
the seeded account to locate the change.

---

Nitpick comments:
In `@e2e/playwright-utils.ts`:
- Around line 90-140: The signup-with-409-fallback-to-login logic in the `login`
fixture duplicates the existing flow in `insertNewUser`, which risks the two
paths drifting apart. Extract the shared request handling into a common helper
such as `signupOrLogin` that encapsulates the `/auth` POST, 409 check, fallback
login, and error handling, then have both `login` and `insertNewUser` call that
helper with the appropriate options.

In `@packages/worker/src/app/authenticated-user.ts`:
- Around line 53-65: The authenticated-user path is doing a separate
roles/permissions lookup for every request, which adds extra DB load in the
readAuthenticatedAppUser flow. Update
readAuthenticatedAppUser/getUserRolesAndPermissions to avoid the extra round
trip by either batching the user and role/permission fetch into one query or
adding a short-lived cache for roles/permissions keyed by user/session. Keep the
existing return shape from readAuthenticatedAppUser intact.

In `@packages/worker/src/app/handlers/admin-roles.ts`:
- Around line 24-47: The admin page handler logic is duplicated between
createAdminRolesHandler and createAdminUsersHandler, including the role check,
auth session lookup, login redirect, Layout rendering, and Set-Cookie handling.
Extract that shared flow into a reusable helper such as renderAdminPage({
request, env, title }) and have createAdminRolesHandler call it so the admin
page handlers stay consistent as more pages are added.

In `@packages/worker/src/app/handlers/privacy.ts`:
- Around line 6-11: The Privacy route handler currently renders Layout without a
contextual title, so it falls back to the default app title. Update the privacy
handler’s render call to pass an appropriate Privacy page title into Layout,
following the same pattern used by the other handlers in this PR (for example
the admin users handler).

In `@packages/worker/src/app/handlers/session.ts`:
- Around line 33-61: The session handler is duplicating the session-to-user
lookup and RBAC loading flow already centralized in readAuthenticatedAppUser.
Refactor the logic in the session handler to reuse readAuthenticatedAppUser for
resolving the user, roles, and permissions, and keep only the missing-user
branch that destroys the auth cookie before returning jsonResponse. Use the
existing symbols readAuthenticatedAppUser, getUserRolesAndPermissions, and
destroyAuthCookie to consolidate the payload contract in one place.

In `@packages/worker/src/app/permissions-db.ts`:
- Around line 30-37: The role filtering in the permissions mapping is hardcoded
to literal user/admin strings, which can drift from the shared RoleName
vocabulary. Update the logic in the result loop to use the RoleName source of
truth already used by the type system or shared constants instead of inline
string checks, so any valid role is consistently added to roleSet while
permissionSet remains unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 0d941bd4-b9ac-4c92-9676-603af35dc6ac

📥 Commits

Reviewing files that changed from the base of the PR and between 2818195 and 50c383a.

📒 Files selected for processing (52)
  • .agents/skills/rbac/SKILL.md
  • docs/contributing/architecture/authentication.md
  • docs/contributing/architecture/authorization.md
  • docs/contributing/architecture/index.md
  • docs/contributing/architecture/primitives.yaml
  • docs/contributing/index.md
  • docs/contributing/project-intent.md
  • docs/use/index.md
  • docs/use/privacy.md
  • e2e/admin-rbac.spec.ts
  • e2e/d1-utils.ts
  • e2e/playwright-utils.ts
  • e2e/smoke.spec.ts
  • packages/shared/src/chat.node.test.ts
  • packages/shared/src/chat.ts
  • packages/worker/client/app.tsx
  • packages/worker/client/routes/account.tsx
  • packages/worker/client/routes/admin-roles.tsx
  • packages/worker/client/routes/admin-users.tsx
  • packages/worker/client/routes/index.tsx
  • packages/worker/client/routes/login.tsx
  • packages/worker/client/routes/privacy.tsx
  • packages/worker/client/session.ts
  • packages/worker/migrations/0043-rbac.sql
  • packages/worker/src/app/account-deletion.node.test.ts
  • packages/worker/src/app/account-deletion.ts
  • packages/worker/src/app/audit-log.ts
  • packages/worker/src/app/authenticated-user.ts
  • packages/worker/src/app/handlers/admin-roles.node.test.ts
  • packages/worker/src/app/handlers/admin-roles.ts
  • packages/worker/src/app/handlers/admin-users.node.test.ts
  • packages/worker/src/app/handlers/admin-users.ts
  • packages/worker/src/app/handlers/auth-handler.node.test.ts
  • packages/worker/src/app/handlers/auth.ts
  • packages/worker/src/app/handlers/privacy.ts
  • packages/worker/src/app/handlers/session-handler.node.test.ts
  • packages/worker/src/app/handlers/session.ts
  • packages/worker/src/app/permissions-db.ts
  • packages/worker/src/app/permissions-server.node.test.ts
  • packages/worker/src/app/permissions-server.ts
  • packages/worker/src/app/permissions.node.test.ts
  • packages/worker/src/app/permissions.ts
  • packages/worker/src/app/router.ts
  • packages/worker/src/app/routes.ts
  • packages/worker/src/mcp-auth-user-context.node.test.ts
  • packages/worker/src/mcp-auth-user-context.ts
  • packages/worker/src/mcp-auth.ts
  • packages/worker/src/mcp/capabilities/meta/require-permission.node.test.ts
  • packages/worker/src/mcp/capabilities/meta/require-permission.ts
  • packages/worker/tsconfig-client.json
  • tools/seed-test-data.node.test.ts
  • tools/seed-test-data.ts

Comment thread docs/contributing/architecture/authorization.md Outdated
Comment thread packages/worker/src/app/handlers/admin-users.ts Outdated
Comment thread packages/worker/src/app/handlers/auth.ts
Comment thread packages/worker/src/mcp-auth.ts
Comment thread tools/seed-test-data.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/worker/src/app/handlers/auth-handler.node.test.ts (1)

345-360: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Assert the failed signup does not persist a roleless account.

This failure happens after user creation but before default-role assignment, so the test should also cover cleanup/rollback; otherwise a 500 can still reserve the email with no role.

Proposed test coverage addition
 	expect(response.status).toBe(500)
 	expect(await response.json()).toEqual({ error: 'Unable to create account.' })
 	expect(response.headers.get('Set-Cookie')).toBeNull()
+	expect(context.testDb.users.has('roleless@example.com')).toBe(false)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/worker/src/app/handlers/auth-handler.node.test.ts` around lines 345
- 360, The signup failure test in auth-handler.node.test.ts only checks the 500
response and cookie state, but it should also verify cleanup when default role
assignment fails after user creation. Update the test around
createAuthTestContext and the signup request to assert the account is rolled
back or removed (for example by confirming no user record exists for
roleless@example.com after the failure), using the existing signup fails when
the default user role cannot be assigned case to cover the rollback path.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/worker/public/mcp-apps/kody-ui-utils.css`:
- Around line 4-14: The monospace font stack in the CSS custom property is still
violating the Stylelint rule because the literal family names are unquoted.
Update the --font-mono declaration in kody-ui-utils.css so the font-family
tokens SFMono-Regular, Menlo, Monaco, and Consolas are quoted while keeping the
same fallback order, and leave the surrounding font variables unchanged.

---

Nitpick comments:
In `@packages/worker/src/app/handlers/auth-handler.node.test.ts`:
- Around line 345-360: The signup failure test in auth-handler.node.test.ts only
checks the 500 response and cookie state, but it should also verify cleanup when
default role assignment fails after user creation. Update the test around
createAuthTestContext and the signup request to assert the account is rolled
back or removed (for example by confirming no user record exists for
roleless@example.com after the failure), using the existing signup fails when
the default user role cannot be assigned case to cover the rollback path.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e6fb6631-d22b-478f-aca0-1dcabed91390

📥 Commits

Reviewing files that changed from the base of the PR and between 50c383a and c5e4b14.

📒 Files selected for processing (9)
  • .agents/skills/rbac/SKILL.md
  • docs/contributing/architecture/authorization.md
  • packages/worker/public/mcp-apps/kody-ui-utils.css
  • packages/worker/src/app/handlers/admin-users.node.test.ts
  • packages/worker/src/app/handlers/admin-users.ts
  • packages/worker/src/app/handlers/auth-handler.node.test.ts
  • packages/worker/src/app/handlers/auth.ts
  • packages/worker/src/app/permissions-db.ts
  • packages/worker/src/app/permissions-server.ts
✅ Files skipped from review due to trivial changes (1)
  • docs/contributing/architecture/authorization.md
🚧 Files skipped from review as they are similar to previous changes (4)
  • packages/worker/src/app/handlers/auth.ts
  • packages/worker/src/app/permissions-server.ts
  • packages/worker/src/app/handlers/admin-users.node.test.ts
  • packages/worker/src/app/handlers/admin-users.ts

Comment thread packages/worker/public/mcp-apps/kody-ui-utils.css Outdated
…ited signup role failure, doc wording, seed log PII
Comment thread packages/worker/src/app/handlers/auth.ts
Comment thread packages/worker/client/routes/admin-users.tsx

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 6e843e0. Configure here.

Comment thread packages/worker/migrations/0043-rbac.sql
Comment thread packages/worker/src/app/handlers/session.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tools/seed-test-data.ts (1)

196-222: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

--no-admin doesn't revoke an existing admin grant.

buildAccountSeedSql only ever adds the admin role row (INSERT OR IGNORE) when admin is true; when admin is false there's no corresponding removal of a pre-existing admin role for that user. Re-running the seed script with --no-admin (or without --admin) against an account that was previously granted admin leaves it admin, contradicting the documented behavior ("seed the default account without the admin role").

🛠️ Proposed fix to revoke admin when not requested
 	const adminRoleSql = admin
 		? `
 INSERT OR IGNORE INTO user_roles (user_id, role_id)
 SELECT u.id, r.id
 FROM users u, roles r
 WHERE u.email = ${quoteSql(email)} AND r.name = 'admin';`
-		: ''
+		: `
+DELETE FROM user_roles
+WHERE user_id = (SELECT id FROM users WHERE email = ${quoteSql(email)})
+  AND role_id = (SELECT id FROM roles WHERE name = 'admin');`
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tools/seed-test-data.ts` around lines 196 - 222, `buildAccountSeedSql` only
adds the admin role and never removes it, so reseeding with `--no-admin` leaves
a previously granted admin intact. Update the SQL generation in
`buildAccountSeedSql` to explicitly revoke the `admin` role when `admin` is
false, while preserving the existing `user_roles` insert logic for the default
user role and the current upsert behavior for `users`.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tools/seed-test-data.ts`:
- Around line 267-274: The fixture insertion in seed-test-data.ts currently runs
for every non-matching email, which seeds the companion test account in remote
environments too. Update the conditional around the accounts.push block in the
seed logic so it only adds the `regularTestEmail`/`regularTestUsername` fixture
when `--local` is enabled, keeping the fixed credential out of remote runs.

---

Outside diff comments:
In `@tools/seed-test-data.ts`:
- Around line 196-222: `buildAccountSeedSql` only adds the admin role and never
removes it, so reseeding with `--no-admin` leaves a previously granted admin
intact. Update the SQL generation in `buildAccountSeedSql` to explicitly revoke
the `admin` role when `admin` is false, while preserving the existing
`user_roles` insert logic for the default user role and the current upsert
behavior for `users`.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b83e22f5-a023-4921-9249-0f98154ed98f

📥 Commits

Reviewing files that changed from the base of the PR and between 9ae04b1 and 01d2f4d.

📒 Files selected for processing (6)
  • AGENTS.md
  • docs/contributing/architecture/authorization.md
  • docs/contributing/getting-started.md
  • docs/contributing/setup.md
  • tools/seed-test-data.node.test.ts
  • tools/seed-test-data.ts
✅ Files skipped from review due to trivial changes (2)
  • docs/contributing/getting-started.md
  • docs/contributing/architecture/authorization.md

Comment thread tools/seed-test-data.ts Outdated
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.

3 participants