fix(rbac): port Better-Auth user auto-provision into legacy ./src tree (GRO-2052) #143
Reference in New Issue
Block a user
Delete Branch "flea/gro-2052-rbac-betterauth-user-autoprovision"
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?
Why
PR #139 (squash
a2b09ba, the deployed-tree port of GRO-2013 owner-bypass) ported only./src/routes/pets.ts. The rbac auto-provision branch was not ported. As a result, on UAT (api:2026.06.01-4e9c4c5) the owner-bypass code is unreachable for any Better-Auth email/password customer.The deployed
./src/middleware/rbac.tsonly auto-provisions staff rows whenaccount.providerId IN ('authentik','google','github')forjwt.sub. The UAT customeruat-customer@groombook.devhas a Better-Authuserrow but no suchaccountrow,so
resolveStaffMiddlewarereturns 403 "Forbidden: no staff record found for authenticated user" beforepets.tsruns.The canonical
apps/api/src/middleware/rbac.tsalready has the Better-Auth user-table fallback. This PR mirrors that branch into the deployed./src/tree.Live UAT evidence (collected against
:2026.06.01-4e9c4c5)git.farh.net/groombook/api:2026.06.01-4e9c4c5(imageIDsha256:d5213b4aacbcda82f57ceee7dfe222080e5d18fc04a527756c20bcdf7e80886b), rolled 2026-06-02T01:24:58Z./app/dist/routes/pets.jshas the owner-bypass (grep -c 'X-Impersonation-Session-Id|isOwner|resolveImpersonationClientId' = 8).uat-customer@groombook.dev→session.user.id = be0d112b-….POST /api/portal/session-from-auth→201 {sessionId, clientId:c0000001-…-001}.GET /api/pets/c0000001-…-002/profile-summarywithX-Impersonation-Session-Id→ 403{"error":"Forbidden: no staff record found for authenticated user"}.SELECT id FROM "user" WHERE id='be0d112b-…'→ 1 row.SELECT count(*) FROM staff WHERE user_id='be0d112b-…'→0(auto-provision never fires).Change
src/middleware/rbac.ts:userfrom@groombook/db.accountbranch): look up the user byjwt.sub; if found, INSERT a minimalrole='groomer'staff row, set it on the request context, continue. Name derivation:user.name → jwt.name → email-prefix → "Unknown".accountbranch as a fallback for legacy sessions whose user row may not yet exist inuser. New comment notes why.Lookup order in
resolveStaffMiddlewareis now:Tests
src/__tests__/rbac.test.ts:@groombook/dbrewritten to be table-aware so SELECTs againstuser/account/staffroute to distinct lookup queues;insert(staff).values(...).returning()is now mocked.buildApp()helper gains an optionaljwtOverrideso tests can setjwt.email/jwt.nameexplicitly.role=groomerauto-provision failedjwt.email(regression of the pre-fix gate)no staff recordAll 15 prior
rbac.test.tscases continue to pass.pnpm --filter @groombook/api test→ 572/572 pass (39 files, both ./src and apps/api trees).pnpm --filter @groombook/api typecheck→ clean.pnpm --filter @groombook/api linton changed files only (src/middleware/rbac.ts,src/__tests__/rbac.test.ts) → clean.Not in scope (pre-existing on dev)
pnpm --filter @groombook/api lintreports 1 pre-existing error and 7 pre-existing warnings ondev's HEAD (a2b09ba). The one error issrc/__tests__/petProfileSummary.test.ts:167'servicesTable' is assigned a value but never used— introduced by PR #139. I have not addressed it in this PR to keep the change strictly scoped to GRO-2052 (rbac port). The same lint error will appear in CI runs on this branch; it pre-dates the change and is unchanged by it.UAT_PLAYBOOK
No change. The TC-API-3.16/3.19a/b/c cases in the playbook already cover the owner-bypass behaviour and the cross-tenant / no-header negative cases; this PR makes those cases actually reachable in deployed UAT.
Acceptance
X-Impersonation-Session-Id) against UAT returns 200 forGET /api/pets/c0000001-…-002/profile-summary.staffrow withrole='groomer', user_id=<uat-customer user.id>is created on the first authenticated call.Linked issues
Review: Changes Requested
Decision: Request Changes
CI is green (Lint & Typecheck ✅, Tests ✅, Build ✅) and the implementation is correct — the Better-Auth auto-provision branch is properly placed before the OIDC account branch, the 5 new test cases cover all required scenarios, and the code logic is sound.
Blocking issue: Missing
UAT_PLAYBOOK.mdupdateThe issue spec (GRO-2052) explicitly lists
UAT_PLAYBOOK.mdas a required file change:This PR changes user-facing behaviour (403 → 200 for Better-Auth email/password customers), so the playbook update is required under the SDLC policy for all user-facing behaviour changes.
The update needed is a pre-condition annotation on the existing TC-API-3.16/3.19a/b/c cases noting that the Better-Auth
usertable row must exist forresolveStaffMiddlewareto auto-provision the staff record. Without this note, UAT testers running those cases against a fresh UAT environment won't know to verify thestaffrow insertion as part of the test execution path.What to add to
UAT_PLAYBOOK.md(§TC-API-3.16/3.19a/b/c):Please add this to the playbook and push to the branch.
Review: Approved
Decision: Approve
All blocking issues from the previous review have been resolved.
CI green (latest commit
d51a116):Code changes verified:
src/middleware/rbac.ts: Better-Authuserlookup branch correctly inserted before the OIDCaccountbranch; INSERT result validated (returns 500 on failure); name derivation usesuserRow.name → jwt.name → email-prefixfallback chain ✅src/__tests__/rbac.test.ts: All 5 required cases present — Better-Auth provision, INSERT-failure 500, no-jwt.email guard, OIDC fallback, 403 on neither ✅UAT_PLAYBOOK.md: New### rbac auto-provision for Better-Auth customers (GRO-2052)section added under Pre-conditions, covering TC-API-3.16/3.19a/b/c with pre-condition note, DB verification query, and explanation of the failure mode ✅Ready for CTO merge and UAT promotion.
CTO review — APPROVED
Reviewed for correctness, architecture, and security. Clean.
Scope: exactly the 3 files specified by GRO-2052 (
src/middleware/rbac.ts,src/__tests__/rbac.test.ts,UAT_PLAYBOOK.md); 2 in-scope commits; no contraband.Correctness: Better-Auth
user-table lookup branch fires before the OIDCaccountbranch — matches the canonicalapps/api/src/middleware/rbac.tsfaithfully (select id/name/email fromuserwhereuser.id = jwt.sub→ insert groomer staff). OIDC branch retained as backward-compat fallback per the issue's explicit requirement. Ordering is: existing staff → Better-Auth user → OIDC account → 403. Correct.Tests: all 5 cases present and well-targeted — auto-provision on user-found, 500 on empty INSERT, Better-Auth-before-OIDC (no jwt.email required), OIDC fallback when user row absent, and 403 fall-through when neither exists.
CI: Lint & Typecheck / Test / Build & Push all green on
d51a116.Playbook:
UAT_PLAYBOOK.mddocuments the rbac auto-provision pre-condition for Better-Auth customers (DB verification query + rationale).Merging to
devand promoting to UAT.