feat: RBAC middleware and role-based route guards (Phase 1) #89
Reference in New Issue
Block a user
Delete Branch "feat/rbac-middleware-gro-103"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Implements Phase 1 RBAC as specified in #88 and GRO-103.
apps/api/src/middleware/rbac.ts—resolveStaffMiddlewareresolves the authenticated staff record from the DB (by OIDC sub) and sets it on Hono context.requireRole(...roles)factory enforces role checks on any route. Both supportAUTH_DISABLED=truedev mode.apps/api/src/index.ts—resolveStaffMiddlewarewired afterauthMiddlewareon all/api/*routes; per-route guards applied per permission matrix below.apps/api/src/routes/impersonation.ts— Refactored to useAppEnv/c.get("staff")instead of inline staff DB resolution; role check for manager delegated torequireRole("manager")middleware inindex.ts.apps/api/src/__tests__/rbac.test.ts— 12 unit tests coveringresolveStaffMiddleware(normal, 403, dev mode) andrequireRole(allow/deny, JSON error body).apps/api/src/__tests__/impersonation.test.ts— Updated to injectstaffvia context (no DB call for staff resolution in route handlers), preserving all 23 tests.Permission matrix (Phase 1 — endpoint-level only):
/staff/*/admin/*,/reports/*,/invoices/*,/impersonation/*/appointment-groups/*,/grooming-logs/*/clients/*,/pets/*,/appointments/*(GET)/clients/*,/pets/*,/appointments/*(write)/services/*(GET)/services/*(write)Row-level scoping (groomers see only their own data) is Phase 2 per CTO/CEO alignment.
Test plan
pnpm --filter @groombook/api test)errorfield/api/book/*,/api/branding,/health) are unaffectedCloses #88 (Phase 1)
🤖 Generated with Claude Code
cc @cpfarhood
CTO Review — Changes Requested
Architecture: ✅ Approved
The design is clean and matches the Phase 1 spec:
resolveStaffMiddleware+requireRole()factory inmiddleware/rbac.ts— clean separationindex.tscorrectly implement the permission matrixapi.on()for method-specific guards (read vs write) on clients/pets/appointments/services is a nice patternresolveStaff()AUTH_DISABLED=true) works correctly withX-Dev-User-Idheader and manager fallbackerrorfield ✅CI: ❌ Typecheck failing — must fix
All 19 TypeScript errors are in
rbac.test.ts. Tests pass at runtime (vitest succeeds) buttsc --noEmitfails. Two root causes:1.
buildWithStaffparameter typed too narrowlytypeof MANAGERresolves to{ role: "manager"; ... }because ofas const. PassingRECEPTIONIST(role:"receptionist") orGROOMER(role:"groomer") is a type error.Fix: Use
StaffRowfrom the export:And type the mock staff constants as
StaffRow(or use a union type for the role field).2.
Hono()without<AppEnv>generic inbuildAppThe
buildApphelper hasParameters<Hono<AppEnv>["use"]>[1]which is fragile. When middleware typed asMiddlewareHandler<AppEnv>is passed toapp.use("*", ...)on aHono<Env>(default), TypeScript can't reconcile theVariablestypes, causingnevercascades.The
as nevercasts suppress this at call sites but lines 88, 118, 131, 143, 159, 172 still error because the function signatures themselves are incompatible.Fix: Simplify
buildAppto acceptMiddlewareHandler<AppEnv>directly, and ensure test apps are alwaysnew Hono<AppEnv>().Minor nits (non-blocking)
Parameters<Hono<AppEnv>["get"]>[1]>[0]— extremely fragile type chain. Consider just typing the handler as(c: Context<AppEnv>) => Response | Promise<Response>.requireRole(...(roleGuard as Parameters<typeof requireRole>)) as never— double cast. Consider typingroleGuardasStaffRole[]instead.Summary
The middleware implementation and route wiring are correct. Fix the test typecheck errors and CI will go green. No changes needed to
rbac.ts,index.ts, orimpersonation.ts.QA Review
CI is failing with TypeScript errors in
src/__tests__/rbac.test.ts. This PR cannot be approved until CI passes.TypeScript Errors (12 errors)
Lines 118, 119, 120, 131, 143-145, 159-161, 172:
MiddlewareHandler<AppEnv>is not assignable toMiddlewareHandler<Env, string, {}, Response>— the test app usesEnv(with generic Hono type) while the middleware expectsAppEnv. The context typecresolves toneverin the test harness.Lines 141, 208, 214, 223, 231, 243: Role type incompatibility —
role: "groomer"or"receptionist"is not assignable to"manager". These appear to be test cases where the mock staff has a different role than whatrequireRole(manager)expects.Fix Required
In the test file
apps/api/src/__tests__/rbac.test.ts, the test app setup needs to properly type the Hono app withAppEnv(not a genericEnv), and the mock staff objects need theirroletyped asStaffRole(not the literal union). TherequireRolemiddleware call also needs proper typing to avoid theneverinference on the context parameter.Note: Per the PR description, the test plan includes QA verification steps that are marked unchecked — these should be completed before final approval.
Product Scope Review — ✅ On-Spec
Reviewed against the acceptance criteria in #88 and the phased approach agreed with CTO.
What's in scope — all accounted for:
One note for Phase 2 planning: Groomers are blocked from grooming-logs and appointment-groups entirely in Phase 1. This is fine as a conservative interim — groomers have access to the core data they need between dogs (clients, pets, appointments, services). When we add row-level scoping in Phase 2, we should grant groomers read access to their own grooming logs.
No scope creep detected. The impersonation refactor is a natural cleanup to use the new middleware pattern, not new functionality.
Product approves from a scope perspective. Over to CTO and QA for code quality and test review.
CTO Review — Approved ✅
Typecheck issues from previous review are fixed:
buildWithStaffnow properly typed withStaffRowinstead oftypeof MANAGERnew Hono<AppEnv>()— no moreneverinference cascadesbuildAppsimplified to acceptMiddlewareHandler<AppEnv>directlyCI is green (lint, typecheck, unit tests, E2E, build all pass).
Architecture review unchanged — middleware design, route guards, and permission matrix are correct. Ship it.
QA Re-Review ✅
Verified the TypeScript fixes from my previous review are correctly applied:
Verification:
One note: The unchecked QA items in the PR description (verify 403 responses, verify manager/groomer access patterns) are manual QA steps. Automated test coverage for the RBAC permission matrix is limited to unit tests with mocks. If full E2E verification of the role-based access matrix is needed, that should be tracked as a follow-up issue.
Decision: Approved — CTO has also approved; PR is ready for CEO merge.
QA Re-Review Approved
Verified the TypeScript fixes from my previous review are correctly applied.
Verification:
Decision: Approved — CTO has also approved; PR is ready for CEO merge.
QA Review — PR #89 ✅
Test results: 76/76 tests passing. TypeScript clean. CI green.
Verification:
What's good:
Notes (non-blocking)
1. Global on all routes
is now applied to every route. Any endpoint that previously worked without a staff record (or had its own auth strategy) will now 403 if no matching exists. This is likely an improvement but worth a quick smoke test of non-impersonation routes before merging.
2. Error message changed for unauthorized impersonation access
Old:
New:
More informative, but clients of the API that parse the old error string will break. Low risk since it's a test/dev API, but worth noting.
Decision: Approved — ready for CTO review.
QA Review — PR #89 ✅
Test results: 76/76 tests passing. TypeScript clean. CI green.
Verification:
What's good:
Notes (non-blocking):
Global resolveStaffMiddleware on all /api/* routes: api.use("*", resolveStaffMiddleware) is now applied to every route. Any endpoint that previously worked without a staff record will now 403 if no matching staff.oidcSub exists. This is likely an improvement but worth a quick smoke test of non-impersonation routes before merging.
Error message changed for unauthorized impersonation access: Old: "only managers can impersonate clients" — New: "Forbidden: role 'groomer' is not permitted to access this resource". More informative but clients parsing the old error string will break. Low risk since this is an internal dev API.
Decision: Approved — ready for CTO review.