Skip to content

fix(patches): persist all-orgs approval status on reload (#2597)#2601

Merged
ToddHebebrand merged 2 commits into
mainfrom
ToddHebebrand/issue-2597-all-orgs-patching
Jul 17, 2026
Merged

fix(patches): persist all-orgs approval status on reload (#2597)#2601
ToddHebebrand merged 2 commits into
mainfrom
ToddHebebrand/issue-2597-all-orgs-patching

Conversation

@ToddHebebrand

Copy link
Copy Markdown
Collaborator

Problem

Reported in #2597: in the All orgs view → Patching → Microsoft, approving a patch shows it as approved, but navigating away and back reverts it to pending. Single-org view works fine.

Root cause

The approve write is durable and correct in both views — POST /patches/bulk-approve upserts a partner-wide patch_approvals row (partner_id = auth.partnerId, ring_id NULL). patch_approvals is a partner-scoped table.

The bug is on the read path. GET /patches computed each patch's approvalStatus only inside an if (query.orgId) { … } block:

  • Single-org view injects ?orgId=<uuid> (via orgStore.currentOrgId), so the branch fires, resolves the org's partner, reads the approval, and shows approved.
  • All orgs view has currentOrgId === null, so no orgId is sent. The branch is skipped, patch_approvals is never queried, and every patch falls back to 'pending' — even though the approval row exists.

Fix

apps/api/src/routes/patches/list.ts — resolve the approval partner from the caller's own token when no orgId is supplied:

const approvalPartnerId = query.orgId
  ? await resolvePartnerIdForOrg(query.orgId)
  : (auth.scope !== 'organization' ? auth.partnerId : null);

This mirrors what the write path already does (auth.partnerId). It's gated on partner/system scope so an org-scoped token — which can carry a partnerId but must never read partner-axis rows — still skips the read. The partner is always server-derived, so the existing system-context read never widens tenant visibility.

Ring-scoped reads use a different endpoint (/update-rings/:id/patches) and are unaffected; this only fixes the blanket partner-wide (ring_id NULL) case that the all-orgs view uses.

Tests

apps/api/src/routes/patches/index.test.ts (+2):

  • All-orgs partner caller (no orgId) surfaces approvalStatus: 'approved', and the read is scoped to the caller's own token partner.
  • Org-scoped caller with no orgId stays pending and never issues the partner-scoped approvals read (tenant-isolation guard — exactly 3 base selects).

All 31 tests in the suite pass; tsc --noEmit -p apps/api/tsconfig.json is clean.

Closes #2597

🤖 Generated with Claude Code

The "All orgs" patching view sends no ?orgId to GET /patches. The approval
read gated the entire patch_approvals lookup on query.orgId, so with no
orgId every patch fell back to 'pending' on reload — even though the
partner-wide approval row (ring_id NULL) had been written durably by
/patches/bulk-approve. Single-org view worked only because it injects
?orgId and trips the gated branch.

Read approvals for the caller's own token partner when no orgId is
supplied, gated on partner/system scope so org-scoped tokens (which can
carry a partnerId) still never read partner-axis approvals. Partner is
always server-derived, so the system-context read never widens tenant
visibility.

Adds two route tests: the all-orgs partner read surfaces 'approved', and
an org-scoped caller with no orgId skips the approvals read entirely.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 17, 2026

Copy link
Copy Markdown

Deploying breeze with  Cloudflare Pages  Cloudflare Pages

Latest commit: ab1de1d
Status: ✅  Deploy successful!
Preview URL: https://d5fe56fa.breeze-9te.pages.dev
Branch Preview URL: https://toddhebebrand-issue-2597-all.breeze-9te.pages.dev

View logs

Addresses PR review feedback:
- Add real-Postgres integration coverage in patch-approval-org-scope
  integration test: partner-scope no-orgId (all-orgs view) surfaces the
  partner-wide approval, plus a cross-partner isolation case proving one
  partner's approval never leaks into another's all-orgs view. This
  exercises the RLS + system-context escape that the mocked unit tests
  cannot reach and closes a pre-existing cross-partner isolation gap.
  Verified: 5/5 pass against a clean migrated DB.
- Log the "partner-scope token with null partnerId" invariant violation
  instead of silently rendering every patch as 'pending' (a different
  cause of the same #2597 symptom).
- Strengthen the mocked tests: assert the WHERE binds the caller's partner
  to the partnerId column (not just appears anywhere), and assert the
  system-context escape is / isn't entered per scope rather than counting
  db.select calls.
- Trim the now-redundant null-case comment.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@ToddHebebrand
ToddHebebrand merged commit 66fd8a4 into main Jul 17, 2026
49 checks passed
@ToddHebebrand
ToddHebebrand deleted the ToddHebebrand/issue-2597-all-orgs-patching branch July 17, 2026 21:25
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.

All orgs patching approval not saved

1 participant