fix(pets): port owner-bypass into deployed tree (GRO-2013) #139

Merged
The Dogfather merged 4 commits from flea/gro-2013-owner-bypass-deployed-tree into dev 2026-06-01 20:06:25 +00:00

4 Commits

Author SHA1 Message Date
Flea Flicker c2f4bca720 Merge origin/dev into flea/gro-2013-deployed-tree-conflicts
CI / Test (pull_request) Successful in 12s
CI / Lint & Typecheck (pull_request) Successful in 15s
CI / Build & Push Docker Images (pull_request) Successful in 1m26s
# Conflicts:
#	UAT_PLAYBOOK.md
#	src/__tests__/petProfileSummary.test.ts
2026-06-01 19:54:22 +00:00
Paperclip a9ed681726 fix(pets): port owner-bypass into deployed tree (GRO-2013)
CI / Test (pull_request) Successful in 13s
CI / Lint & Typecheck (pull_request) Successful in 16s
CI / Build & Push Docker Images (pull_request) Successful in 1m20s
The previous fix for GRO-2013 (customer cannot view own pet profile
summary) landed in apps/api/src/routes/pets.ts, which is dead code
in the Docker build path. The Dockerfile does COPY src/ + pnpm build
from the repo root, so apps/api/ is never copied into the image and
is not a pnpm-workspace member.

Port the owner-bypass into the deployed-tree handler src/routes/pets.ts:
- Add resolveImpersonationClientId(db, c) helper that reads the
  X-Impersonation-Session-Id header, validates the session is active
  and not expired, and returns its clientId (or null).
- Gate the existing groomer 403 in GET /:id/profile-summary so an
  owner (session.clientId === pet.clientId) bypasses the
  appointment-linkage check. This mirrors the already-reviewed logic
  from apps/api/src/routes/pets.ts:318-364.
- Cross-tenant access remains blocked: the bypass requires
  session.clientId === pet.clientId, and groomers with no portal
  session still 403 as before.

Tests (src/__tests__/petProfileSummary.test.ts — new file, mirroring
the dead-tree test pattern but pointing at the deployed handler):
- Customer with valid active session for pet's client → 200
- Customer with no header → 403
- Customer with session for a different client → 403
- Customer with expired session → 403
- Customer with ended (status != active) session → 403
- Customer with unknown session id → 403
- Manager does not need the impersonation header (regression)
- Groomer with linkage to pet's client still works (regression)
- Customer cannot view another client's pet (cross-tenant block)

Full @groombook/api test suite: 560 passed (39 files).

Note (out of scope): the apps/api/ duplicate tree is dead code
producing false-green coverage — recommend filing a separate tech-debt
issue to delete apps/api/ or wire it into the workspace, but not
blocking this fix on it.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-06-01 19:08:28 +00:00
Paperclip 7fe578aeef fix(pets): customer can view own pet profile summary (GRO-2013)
CI / Test (pull_request) Successful in 13s
CI / Lint & Typecheck (pull_request) Successful in 15s
CI / Build & Push Docker Images (pull_request) Successful in 1m8s
When a customer (e.g. uat-customer@groombook.dev) signs in via Better Auth
and calls GET /api/pets/{ownPetId}/profile-summary with their portal
session header, the staff RBAC middleware auto-provisions a 'groomer'
staff row for them (rbac.ts) and the profile-summary route's
groomerLinkageCheck then denies the request with 403 Forbidden, because
the auto-provisioned customer-as-groomer has no appointment linkage.

This adds an owner-bypass: when a groomer-role staff row is making the
request with a valid X-Impersonation-Session-Id header, and the resolved
impersonation session's clientId matches the pet's clientId, we treat
the caller as the pet's owner and skip the groomerLinkageCheck.

The bypass is intentionally scoped to the profile-summary endpoint and
to the existing portal session mechanism (no new roles, no staff-row
shape changes). Cross-tenant access is still blocked because the
bypass requires session.clientId === pet.clientId.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-06-01 17:42:59 +00:00
Paperclip 337c0e2733 docs(UAT_PLAYBOOK): document canonical source-of-truth for UAT seed passwords (GRO-2000)
CI / Test (pull_request) Successful in 10s
CI / Lint & Typecheck (pull_request) Successful in 18s
CI / Build & Push Docker Images (pull_request) Successful in 36s
The 'Source of truth for UAT passwords' subsection under Pre-conditions
records:

- The seed-uat-passwords Secret in groombook-uat is the live source.
- The Bitnami SealedSecret apps/overlays/uat/ss-seed-uat-passwords.yaml
  in groombook/infra is the single upstream source of truth.
- A kubectl recipe to pull the current values for SUPER / GROOMER /
  TESTER / CUSTOMER at the start of every UAT run.
- The 'captured env var from a previous rotation produces 401' failure
  mode that GRO-2000 hit, and the manual-reseed escape hatch if the
  login still 401s after pulling the live value.

Refs: GRO-2000, GRO-1977 (idempotent re-hash), GRO-1999 (enum fix that
allowed the seed Job to run cleanly again).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 15:30:34 +00:00