Skip to content

fix(purchase): refuse recommendations owned by another account - #2193

Closed
cristim wants to merge 1 commit into
mainfrom
fix/2191-purchase-account-guard
Closed

cristim wants to merge 1 commit into
mainfrom
fix/2191-purchase-account-guard

Conversation

@cristim

@cristim cristim commented Oct 13, 2026 •

Copy link
Copy Markdown
Member

Closes #2191

What

Recommendations are fetched with linked-account scope, so a payer run gets rows for other accounts, but PurchaseCommitment buys in the caller's own account. Nothing compared rec.Account with the credentials' account, so a purchase would commit the payer account to a linked account's usage (audit: rows for under credentials for ).

  • withCallerAccount resolves the account with STS GetCallerIdentity and stores it in Config.CallerAccountID. It runs in both purchase entry points (runPurchaseAndReport, runToolFromCSV), on dry runs too, and fails the run if the account cannot be resolved.
  • validatePurchasePreconditions (used by the main pipeline and the CSV loop) now rejects a recommendation whose Account differs, with recommendation is for account X but the credentials are for account Y; cross-account purchases are not supported, so run with credentials for account X. The row is recorded as a failed result and audit entry; other rows proceed. Rows with an empty account (CSV may omit it) are not compared.
  • Cross-account purchase is not supported by the current design, so this refuses rather than assuming a role. Related cli#2131 is inventory only and untouched.
  • go.mod: service/sts moves from indirect to direct (already in the module graph).

Tests (mocks only; no purchase API is called anywhere)

TestCheckPurchaseAccount, TestPurchaseSingleRec_RefusesOtherAccountsRecommendation (dry and real), TestProcessPurchaseLoop_RefusesOtherAccountsRecommendationWithoutPurchasing (mock client sees exactly one PurchaseCommitment, for the caller's own row), TestWithCallerAccount. TestMain stubs the STS fetcher to return an empty account, which switches the check off for existing tests that do not set CallerAccountID.

Fail before (guard body neutered):

--- FAIL: TestCheckPurchaseAccount            An error is expected but got nil.
--- FAIL: TestPurchaseSingleRec_RefusesOtherAccountsRecommendation   Should be false
--- FAIL: TestProcessPurchaseLoop_RefusesOtherAccountsRecommendationWithoutPurchasing

After: go test ./cmd passes (35s, skipping TestQueryMajorEngineVersions_ErrorHandling, which hangs locally in this session on a goroutine at multi_service_engine_versions_test.go:814 independent of this change; it passed in an earlier full run). go vet clean; golangci-lint panics locally on this toolchain, pre-commit hooks (gofmt, vet, gocyclo) pass.

Live dry run (read-only, no --purchase), profile shift-prd ()

cudly --services ec2 --regions us-east-1 --term 1 --payment no-upfront --coverage 100:

❌ cannot purchase: recommendation is for account <member-account-id> but the credentials are for account <payer-account-id>; cross-account purchases are not supported, so run with credentials for account <member-account-id>

The CSV row has Success=false and that error. Before this change the same row was reported as a successful dry-run purchase.

Overlap

cli#2186 (merged) and cli#2145 (CSV loader, multi_service_csv*.go) touch none of these lines. Same files as cli#2192 (fetch errors), different functions.

🤖 Generated with Claude Code

Cost Explorer returns linked-account recommendations to a payer, but a
purchase call buys in the caller's own account. The purchase path never
compared rec.Account with the credentials' account, so a payer run could
commit the payer account to a linked account's usage.

Both purchase entry points now resolve the caller account with STS
GetCallerIdentity (failing the run if it cannot) and validatePurchasePreconditions
rejects a recommendation whose account differs, on dry runs too.

Refs #2191

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@cristim cristim added triaged Item has been triaged priority/p0 Drop everything; same-day fix severity/critical Major harm when it happens urgency/now Drop other things impact/many Affects most users effort/s Hours type/bug Defect labels Oct 13, 2026
@coderabbitai

coderabbitai Bot commented Oct 13, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

  • Run on-demand review

This review includes 7 billable files and costs up to $1.75.

  • Ask an admin to make reviews automatic

Open in CodeRabbit

Reviews can continue after your included limit without a manual trigger. An admin must approve usage-based billing.

Or wait 45 minutes for your next included review.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available. Your 61 included PR review attempts over the past 7 days set your current allowance at 1 review per hour.

Learn how review limits work.

Review configuration:

⚙️ Run configuration
  • Configuration used: Repository: LeanerCloud/cloud-commitments-cli/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Essentials
  • Run ID: ecae01d7-06e9-4a08-b872-dbab72ffedf6
📥 Commits

Reviewing files that changed from the base of the PR and between de71edc and 5e815cd.

📒 Files selected for processing: 7
  • cmd/main.go
  • cmd/main_test.go
  • cmd/multi_service.go
  • cmd/purchase_account_guard_test.go
  • cmd/savings_plan_purchase.go
  • cmd/savings_plan_purchase_test.go
  • go.mod
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

@cristim

cristim commented Oct 13, 2026

Copy link
Copy Markdown
Member Author

Superseded by #2196

@cristim cristim closed this Oct 13, 2026
@cristim
cristim deleted the fix/2191-purchase-account-guard branch October 13, 2026 22:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/s Hours impact/many Affects most users priority/p0 Drop everything; same-day fix severity/critical Major harm when it happens triaged Item has been triaged type/bug Defect urgency/now Drop other things

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(purchase): refuse to purchase a recommendation owned by a different account than the caller

1 participant