fix(patches): persist all-orgs approval status on reload (#2597)#2601
Merged
Conversation
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>
Deploying breeze with
|
| 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 |
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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-approveupserts a partner-widepatch_approvalsrow (partner_id = auth.partnerId,ring_id NULL).patch_approvalsis a partner-scoped table.The bug is on the read path.
GET /patchescomputed each patch'sapprovalStatusonly inside anif (query.orgId) { … }block:?orgId=<uuid>(viaorgStore.currentOrgId), so the branch fires, resolves the org's partner, reads the approval, and showsapproved.currentOrgId === null, so noorgIdis sent. The branch is skipped,patch_approvalsis 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 noorgIdis supplied:This mirrors what the write path already does (
auth.partnerId). It's gated onpartner/systemscope so an org-scoped token — which can carry apartnerIdbut 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):orgId) surfacesapprovalStatus: 'approved', and the read is scoped to the caller's own token partner.orgIdstayspendingand 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.jsonis clean.Closes #2597
🤖 Generated with Claude Code