Repository navigation
fix(api): enforce four-eyes on RI exchange approval - #501
Merged
Merged
Conversation
The 4-eyes check only needs the subject ID and the creator's user ID, not a whole PurchaseExecution. Taking those two values lets the RI exchange approval path reuse the same policy instead of duplicating it. No behaviour change for purchase approvals; log lines now use a neutral "4-eyes[<id>]" prefix. Refs #221
With RequireDifferentApprover on, the user who created an RI exchange could approve and execute it themselves: neither the session branch nor the email-token branch of approveRIExchange called requireDifferentApprover. Both branches now run the same check as purchase approvals before the pending -> processing transition. The token branch resolves the caller's session for the comparison and fails closed (500) when the mode is on and no session is present, as the purchase token path does. The token branch also matters because a session-branch 403 falls through to it when a token is supplied, so a session-only fix would be bypassed. Exchanges with no recorded creator (every exchange the automated pipeline writes today) keep their current behaviour. The purchase policy denies a nil creator, which applied here would make every RI exchange unapprovable while the mode is on; that decision is open in issue #233. Closes #221
Contributor
|
Warning Review limit reached
This review includes 4 billable files and costs up to $1.00.
Reviews can continue after your included limit without a manual trigger. An admin must approve usage-based billing. Or wait 4 minutes for your next included review. View limit detailsLimit details: You’ve used all 2 included reviews currently available. Your 54 included PR review attempts over the past 7 days set your current allowance at 2 reviews per hour. Review configuration: ⚙️ Run configuration
📒 Files selected for processing (4)
Comment |
This was referenced Oct 5, 2026
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.
What
With
RequireDifferentApproveron, the creator of an RI exchange could approve and execute it themselves. Neither branch ofapproveRIExchange(session or email token) calledrequireDifferentApprover, which only the purchase approval paths used.requireDifferentApprovernow takes(subjectID, creatorUserID *string)instead of a*config.PurchaseExecution, so the RI exchange path reuses the same policy. Purchase behaviour is unchanged (separate refactor commit).approveRIExchangeViaSessionandapproveRIExchangeViaTokenboth run the check after authorization and before thepending -> processingtransition. The token branch resolves the caller's session and fails closed (500) when the mode is on and no session resolves, matchingapproveViaTokenfor purchases.Nil creator (decision left open)
Every RI exchange written today comes from the automated pipeline (
exchange.autoviaconfigExchangeStoreAdapter, which never setsCreatedByUserID), so production exchange rows all have a NULL creator. The purchase policy denies a NULL creator with 403. Applying that here would make every RI exchange unapprovable while 4-eyes is on, the same failure #233 describes for scheduler-created purchases. This PR therefore leaves creator-less exchanges approvable as before and pins that in a test. What 4-eyes should do for creator-less rows is the open question in #233.How verified
Toolchain:
GOTOOLCHAIN=go1.26.6 GOWORK=off GOFLAGS='-p=2 -count=1' AWS_EC2_METADATA_DISABLED=trueTestApproveRIExchange_FourEyes(9 subtests). With the RI exchange change reverted (helper refactor kept), 5 fail and 4 pass:session:denial subtests fail. Token call removed:creator_holding_the_email_token,token: creator sessionandtoken: no sessionfail. Nil-creator skip removed:automated exchange without creator stays approvablefails.go build ./...,go vetandgo vet -tags integrationoninternal/api,internal/purchaseandinternal/serverare clean.go testpasses for all three packages (2445 tests ininternal/api).golangci-lint run ./internal/api/...reports no issues.handleRIExchangeApproveClickinfrontend/src/riexchange.tsshows the backenderrorstring in a toast, so the creator sees "Failed to approve exchange: approval declined: 4-eyes mode requires a different approver than the requester".Deliberately not in this PR
authorizeSessionApproveRIExchangedoes not check the session's account scope (a separate finding).canApproveRIExchangeRowinriexchange.tsstill shows the Approve button to the creator under 4-eyes. It is a UX gate only; the backend now refuses the approval."USD"literal), which are in the same file.Refs #221 (production exchange rows have no creator recorded, so the control only applies to rows that carry one; see "Nil creator")