Repository navigation
fix(api): refuse direct RI exchange execution while 4-eyes mode is on - #673
Conversation
executeExchange (AWS) and executeAzureExchange quote and run an irreversible exchange in one request, so a single user holding execute:ri-exchange bypassed the dual-control policy that the approve path enforces. With RequireDifferentApprover on, both now return 403 after authorization and before any quote or exchange call; a global-config lookup failure fails closed. Closes #517 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
Warning Review limit reached
This review includes 3 billable files and costs up to $0.75.
Reviews can continue after your included limit without a manual trigger. An admin must approve usage-based billing. Or wait 54 minutes for your next included review. View limit detailsLimit details: You’ve used the included review currently available. Your 82 included PR review attempts over the past 7 days set your current allowance at 1 review per hour. Review configuration: ⚙️ Run configuration
📒 Files selected for processing (3)
Comment |
Independent review of PR 673 at 2aa8f1dVerdict: APPROVE the diff as scoped (both direct execute endpoints). Two items outside the diff need an explicit decision before #517 is described as "no RI exchange executes without a different approver". CI was still in progress when I wrote this; the verdict covers only the SHA above and needs green CI. Execution paths enumerated (grep over internal/, cmd/, frontend/ at this SHA)
No other caller of ExecuteExchange exists outside handler_ri_exchange.go. The API-key admin session is covered because the gate reads the global flag, not the caller's role. A config read error fails closed (tested for AWS). Findings
Evidence reproduced (macOS, exact SHA in the author's worktree, non-race, under the platform test-suite lock, GOWORK=off GOTOOLCHAIN=go1.26.6, 11 GiB free)
|
…on (#1108) * fix(ri-exchange): run unattended exchanges as manual while 4-eyes is on With RIExchangeEnabled and a non-manual RIExchangeMode, the scheduled ri_exchange_reshape task called exchange.RunAutoExchange in auto mode and executed exchanges with no approver, even with RequireDifferentApprover on. The direct execute endpoints already refuse under 4-eyes (#673), so auto mode was the remaining path that moved money with no second person. With 4-eyes on, every non-manual mode ("auto", "" and typos, which pkg/exchange all treats as auto) now runs as "manual": the run only raises pending records with approval tokens, sends the pending-approval email, and each exchange then goes through the approve endpoint and its 4-eyes check. The stored mode is unchanged, so turning 4-eyes off brings auto back. The ladder exchange runner gets the same downgrade; it reads the global config itself and fails closed (no AWS call) when it cannot. Also tightens the #673 tests: the AWS direct-execute 4-eyes tests now spy on the exchange executor and assert it is never called, and the Azure global-config lookup failure has a fail-closed test. Closes #679 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(server): use a 12-digit account ID in the 4-eyes exchange fixtures Main now refuses to record an RI exchange whose account ID is not a 12-digit AWS account ID (#1170), so the "acct-four-eyes" fixture made the 4-eyes downgrade tests fail before any assertion they make. Use the 123456789012 documentation placeholder instead; every assertion is kept. The one-line adapter literals are split so the account field sits on its own line, the only shape the .gitallowed placeholder rule exempts from the AWS account-ID detector. Refs #679 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Summary
Closes #517.
executeExchange(AWS) andexecuteAzureExchangequote and run an irreversible exchange in one request. A single user holdingexecute:ri-exchangebypassed the 4-eyes policy that only the approve path enforced (#501).With
RequireDifferentApproveron, both endpoints now return 403 (direct RI exchange execution is disabled while 4-eyes approval mode is on; use the approval flow). The check runs after the permission, account-scope and constraints gates and before any quote, exchange or idempotency-claim call. A global-config lookup failure fails closed (server error, not "mode off"). With the mode off nothing changes.authorizeAWSExchangeExecution(also keepsexecuteExchangeunder the cyclomatic limit), which now ends with the 4-eyes check.authorizeAzureExchangeExecution.Behaviour to be aware of
There is no direct-execute path with a second approver: no user-facing flow creates a pending manual record from a quote (pending records come from automated runs in manual mode, via
/api/ri-exchange/approve). So with 4-eyes on, interactive one-shot exchanges are refused, and exchanges go through the approval flow. Single-operator deployments can turn the mode off in settings. A real second-approver flow for interactive exchanges would be a separate feature. #82 (no audit row for direct executes) is out of scope.Scope limits (from the independent review, tracked in #679)
This gates the two direct-execute endpoints named in #517. Scheduler reshape in
automode still executes exchanges unattended and does not consultRequireDifferentApprover(by design of auto mode; whether it should is an owner question in #679).requireDifferentRIExchangeApproveralso accepts rows with no recorded creator, unlike purchases (pre-existing, #233; also in #679). So "no exchange executes without a different approver" is not yet true for those two paths.Tests
GOWORK=off GOTOOLCHAIN=go1.26.6, non-race, under the platform test-suite lock (localgois 1.27.1; I did not confirm the toolchain switch took effect):go test ./internal/api: PASS (full package).TestExecuteExchange_FourEyesRefusesDirectExecute,TestExecuteExchange_FourEyesPolicyLookupErrorFailsClosed,TestExecuteAzureExchange_FourEyesRefusesDirectExecute(403, exact message, zero exchange calls). Mode-off behaviour is covered by the existing execute tests, which use a default (off) global config.Not run locally: full race suite (disk hold; CI covers it), frontend.
🤖 Generated with Claude Code