Repository navigation
fix(api): scope executeExchange to session's allowed_accounts - #379
Conversation
executeExchange (POST /api/ri-exchange/execute) was the only one of the four RI-exchange endpoints that did not call reshapeCloudAccountInScope. The three sibling reads (listConvertibleRIs, getRIUtilization, getReshapeRecommendations) already refuse a deployment cloud account outside the session's allowed_accounts by returning an empty list; a session scoped to one account with an execute:ri-exchange permission carrying no AccountIDs constraint could still execute an exchange against a different, unscoped deployment account, since the separate requirePermissionConstraints check (SEC-01, issue #1141) is satisfied unconditionally by an unconstrained permission. Call the existing reshapeCloudAccountInScope helper at the top of executeExchange and return 403 (not an empty list, since this endpoint is a mutating, financially irreversible action) when out of scope. Extracted two small helpers (requireReshapeAccountScope and resolveReshapeCloudAccountIDOrUnattributed) so the added branch keeps executeExchange's cyclomatic complexity at the project's CI-enforced gate of 10. Added TestExecuteExchange_SessionAccountScopeMismatchReturns403, confirmed to fail against the pre-fix code (stashed diff) and pass after. Updated five existing tests to stub the session's account scope via the existing allowAnyAccountScope helper, since they exercise unrelated paths that now also reach the new check.
|
Navigate logical layers of code changes, visualize relationships, and explore their blast radius. No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository: LeanerCloud/cloud-commitments-platform/.coderabbit.yaml Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (5)
Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review. 📝 WalkthroughWalkthrough
ChangesRI exchange account-scope enforcement
Priority: ⚪ Not assessed Estimated code review effort: 2 (Simple) | ~10 minutes Change: Bug fix · Severity of issue fixed: High Merge Risk: ⚪ Minimal · up to The exchange now checks the session’s account scope before submission. No remaining issue was established that would prevent merging after normal checks. 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
|
Independent adversarial review + local verification: MERGE at 0421afe. executeExchange now enforces the session's allowed_accounts via the same reshapeCloudAccountInScope helper its three sibling reads use, before the constraint check: out-of-scope -> 403, scope-lookup error -> 500 (verified), admin/unrestricted unaffected, and a name-scoped in-scope session still reaches the constraint check (verified). Regression test fails at merge-base and passes on head; go test -race ./internal/api 2217 passed; golangci-lint 0 issues; checks green. Follow-ups filed: quote endpoint scoping, two extra tests. |
…cope test(api): cover RI-exchange scope in-scope and lookup-error paths (follow-up to #379)
Summary
executeExchange(POST /api/ri-exchange/execute) was the only one of the four RI-exchange endpoints that did not enforce the session'sallowed_accountsscope, allowing a scoped user with an unconstrainedexecute:ri-exchangegrant to execute an irreversible AWS RI exchange against a cloud account outside their session's scope.Root cause
The three sibling read endpoints (
listConvertibleRIs,getRIUtilization,getReshapeRecommendations) each callreshapeCloudAccountInScope(ctx, session)and return an empty list when the deployment's registered AWS cloud account falls outside the session'sallowed_accounts.executeExchangeonly checkedrequirePermissionConstraints(the SEC-01/#1141 gate on the granting permission's ownConstraints.AccountIDs), a different, independent gate that a permission with noAccountIDsconstraint satisfies unconditionally. A user scoped toallowed_accounts=[acct-A]holding such an unconstrained permission could therefore execute against a deployment whose ambient AWS account is acct-B, even though the read endpoints correctly hid acct-B's data from that same user.Fix
Call the existing
reshapeCloudAccountInScopehelper at the top ofexecuteExchange(via a newrequireReshapeAccountScopewrapper) and return 403 (not an empty list, since this is a mutating, financially irreversible endpoint) when out of scope. ExtractedresolveReshapeCloudAccountIDOrUnattributed(identical logic to the pre-existing inline resolution, just named) so the added branch keepsexecuteExchangeat or under the project's CI-enforced gocyclo gate of 10 (it measured exactly 10 pre-fix).Regression test
Added
TestExecuteExchange_SessionAccountScopeMismatchReturns403ininternal/api/handler_ri_exchange_test.go, replaying the issue's exact scenario: a session scoped toallowed_accounts=[acct-A]with an unconstrainedexecute:ri-exchangegrant attempting to execute against a deployment whose ambient account resolves to acct-B.Proved it fails pre-fix: stashed only the
internal/api/handler_ri_exchange.gochange (keeping the new test) and reran the test, which failed because the code never reachedGetAllowedAccountsAPI(2 of 3 expected mock calls met). Restored the fix and reran: passes, withHasPermissionForConstraintsAPI(the SEC-01 gate) asserted as never called, proving the session-scope gate refuses before that check.Also updated five existing tests (
TestExecuteExchange_PermissionConstraintsDenied,TestExecuteExchange_AccountResolutionErrorFailsClosed,TestExecuteExchange_UnattributedAccountStillConstrained,TestExecuteExchange_InvalidBodyNeverClaims,TestExecuteExchange_DuplicateSubmitRefusedBeforeAWS,TestExecuteExchange_PermissionGate) to stub the session's account scope via the existingallowAnyAccountScopehelper, since they exercise unrelated paths that now also reach the new check.Verification
All commands run from the repo root with
GOWORK=offand the CI-pinned toolchain (go1.26.6, matchinggo.mod'sgodirective and.github/workflows/ci.yml'sGO_VERSION) and the CI-pinnedgolangci-lint v2.10.1:go build ./...-- cleango vet ./...-- cleango test ./internal/api/... -race -short-- 2217 passedgo mod tidy -diff-- emptygolangci-lint run ./internal/api/...(v2.10.1, matching.github/workflows/ci.yml) -- 0 issuesgocyclo -over 10 -ignore "_test\.go" .-- 0 functions over 10 (repo-wide, matching the CI step)Docker is unavailable on this host; no testcontainers-based tests are affected by this change (all touched tests are handler-level unit tests using mocks), so this does not change CI's container-suite coverage.
Notes
executeExchangerequest: once insidereshapeCloudAccountInScopefor the new scope check, once again inresolveReshapeCloudAccountIDOrUnattributedfor the permission-constraints check. This is unchanged for unrestricted sessions (the common case), where the first resolution is skipped entirely. Threading the resolved ID through from the scope check would remove the duplicate call but pushesexecuteExchangeback over the gocyclo gate; the extra STS/ListCloudAccountsround trip on this low-QPS, already-network-bound endpoint was judged an acceptable cost for the simpler diff.Follow-up commit
Addressed an independent adversarial review (no blocking findings) with a second commit:
TestExecuteExchange_SessionAccountScopeMatchReachesConstraintsCheck: a session whoseallowed_accountsincludes the deployment's own account must clear the new gate and reach the existing constraints check, proving the fix does not over-deny in-scope requests.TestExecuteExchange_SessionScopeLookupErrorFailsClosed: a failure resolving the session's scope propagates as a plain error, not a 403.reshapeCloudAccountInScope's doc comment to listexecuteExchangeamong its callers.Closes #93
Summary by CodeRabbit