Repository navigation
fix(api): sign no-cloud-account marketplace rows as the host only for the host account - #1031
Merged
Merged
Conversation
… the host account Marketplace list and cancel already resolve a row's cloud account through credentials.ResolveAWSCredentialProviderWithOpts (LeanerCloud#554), but a row with no cloud_account_id still fell back to the host's ambient credentials whatever AWS account the RI belongs to. Such rows come from the ambient single-account purchase path, from before migration 000011 and from deleted accounts (ON DELETE SET NULL). Use the host credentials for those rows only when the row's AWS account id passes credentials.VerifyHostAccount, the same check the resolver applies to a role_arn account with no role ARN. An empty account id, a different account or a failed host identity lookup is refused with a 400 before any EC2 client is built. Authorization is unchanged. Closes LeanerCloud#261 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
|
Note Currently processing new changes in this PR. This may take a few minutes, please wait... ⚙️ Run configuration
📒 Files selected for processing: 3
✨ Finishing Touches
Comment |
cristim
reviewed
Oct 16, 2026
cristim
left a comment
Member
There was a problem hiding this comment.
Independent review of 1e7c394: draft: blocks ready
No blocking findings.
Findings
- NON-BLOCKING, internal/api/handler_marketplace.go (loadMarketplaceHostAWSConfig, the VerifyHostAccount call): any host STS failure (throttle, 5xx, network) returns the fixed non-retryable 400 "not confirmed as the host account". VerifyHostAccount collapses every failure into ErrNotHostAccount (internal/credentials/host_identity.go:38-41), so the #937 retryable 503 classification cannot apply here. This matches the existing role_arn-without-role path (handler_marketplace.go:90-93), so it is consistent with current behavior; a follow-up could separate transient from mismatch.
Answers
- Host account is whatever STS GetCallerIdentity returns for the ambient credentials (host_identity.go:38,47), compared with row.AccountID. That field is server-written at purchase time (internal/purchase/execution.go:993, from the execution's resolved account), not taken from any request body. Both callers (list at handler_marketplace.go:232, cancel at :349) go through the same loadMarketplaceAWSConfig, and no other caller exists.
- A no-cloud-account row for another account, an empty or blank id, or a failed STS call returns a 400 client error with a fixed message (no host id), before any EC2 client is built. There is no ambient fallback.
- Consistent with #910 (same VerifyHostAccount probe) and with the existing ErrNotHostAccount 400 mapping; see the NON-BLOCKING note for #937.
What I attacked (local, no real AWS)
- Mutation: VerifyHostAccount failure ignored. Fails: NoCloudAccountRefusedUnlessHost (member account, host identity fail) and NoCloudAccountRow list/cancel member_account_refused. Reverted.
- Fail-before: old behavior (return base for nil account) fails the HostRow tests (nil, empty, blank) and the empty, blank and host-identity-fail refusal cases. Pass-after:
go test ./internal/api -run 'Marketplace|PermissionConstraints'ok at this SHA.
Gate status
- Independent review: this comment, at the SHA above.
- CI: all checks still pending at review time (draft, fork run); not green yet.
- CodeRabbit: no review comments; not pinged.
- Labels: match issue #261.
- Local verification: unit and handler tests with fake STS transports on macOS; no real-account check.
Blocks ready
- CI green on this SHA.
- Author marks the PR ready for review.
Member
|
Gate record for 1e7c394 (origin/main CLAUDE.md "Review gate"):
Marking ready (owner-authorized) and merging at 1e7c394 with --match-head-commit. |
cristim
marked this pull request as ready for review
October 16, 2026 22:51
cristim
pushed a commit
that referenced
this pull request
Oct 16, 2026
…AWS account (#1039) A purchase row with a NULL cloud_account_id (legacy rows, or rows whose account was deleted) was signed with the host's credentials only when its AWS account id was the host account, and refused otherwise, even when that id belonged to a registered member account. Resolve the id by (aws, account_id) through uniqueAccountByExternalID, as revoke (#547) and RI exchange history (#553) do, and sign with that account's own credentials through the same resolver as a row with a cloud account. An unregistered or ambiguous id keeps the host-only check from #1031; a failed account lookup is an error, never a fallback to the host's credentials. Closes #560 Co-authored-by: Claude Opus 5.5 <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.
Root cause
Issue #261 reported that marketplace list and cancel signed EC2 calls with the host's ambient credentials (
loadAWSConfigWithRegion) for any account. The main part was fixed by #554 (merged as e522f3d):loadMarketplaceAWSConfigresolves the row's cloud account throughcredentials.ResolveAWSCredentialProviderWithOpts, with the same ambient options the purchase path uses. #554 said "Refs #261", so the issue stayed open.One branch still fell back to ambient credentials. When
cloud_account_idwas NULL or blank,loadMarketplaceAWSConfigreturned the host config without checking which AWS account the RI belongs to. The issue's suggested fix covers this case ("return 400 when the row has no CloudAccountID ..."). Such rows are not only legacy rows:purchase.SingleCloudAccountIDFromRecsreturns nil, andexecuteSingleAccountstamps the ambient STS identity intoaccount_id);ON DELETE SET NULL.Fix
loadMarketplaceHostAWSConfighandles a row with no cloud account. It uses the host credentials only when the row'saccount_idpassescredentials.VerifyHostAccount. That is the existing check the resolver applies to arole_arnaccount with no role ARN, so no new resolver is added. Every other case gets a 400 before any EC2 client is built:account_id;The message has a fixed text and does not contain the host account id. This keeps the ambient single-account deployment working (its rows carry the host id) and fails closed for everything else.
marketplaceList(create listing, incl. offering-class lookup and attempt reconciliation finder)account_idis the host; else 400, no EC2 callmarketplaceCancelauthorizeSessionSellCreate-listing is
marketplaceList(POST .../marketplace-list). All of its AWS calls go through the samecfg:FetchOfferingClass,CreateMarketplaceListing, thebuildMarketplaceFinderreconcile and the rollback cancel. The fix covers all of them. No other marketplace handler builds an AWS config.Relationship to #560
This PR covers part of #560 but does not close it. #560's acceptance is "never signs with host credentials unless the row's AccountID is the host account". This PR enforces that, with a test for each branch.
What #560 still has to decide is the non-host branch. Today a NULL-account row whose
account_idis a different, registered AWS account is refused with 400. #560 suggests resolving it by(aws, account_id), as #547/#553 do, and using that account's credentials. To do that, #560 replaces the refusal inloadMarketplaceHostAWSConfigwith a lookup: if it resolves, call the existing resolver path; if not, keep the 400. The host branch and the fail-closed default stay as they are.Behaviour change
A NULL-account row whose
account_idis empty or is not the host account now gets 400 instead of a call signed as the host. That call was always wrong, because host credentials cannot act on another account's RI (AWS returnedInvalidReservedInstancesId.NotFound, mapped to a misleading 400). An STS failure during the host check is also a 400. This matches howErrNotHostAccountis handled today forrole_arnself accounts. A host-account NULL row now makes one extra STS GetCallerIdentity call per list/cancel.Verification
Fakes only. No real AWS or STS calls were made, and no listings were created or canceled. STS is a fake
aws.HTTPClienttransport; EC2 is the existingstubMarketplaceEC2throughmarketplaceEC2Factory. Tests ran withAWS_CONFIG_FILE=/dev/null AWS_SHARED_CREDENTIALS_FILE=/dev/null.New tests in
internal/api/handler_marketplace_credentials_test.go(replacing the test that documented the host fallback):TestLoadMarketplaceAWSConfig_NoCloudAccountHostRowUsesHostCredentials: nil, empty and blankcloud_account_idwith the hostaccount_idget the host key, after exactly one host identity check.TestLoadMarketplaceAWSConfig_NoCloudAccountRefusedUnlessHost: member account, empty and blankaccount_id(no STS call), and an STS failure each get 400 with the fixed message and no credentials.TestMarketplaceHandlers_NoCloudAccountRow: list and cancel end to end. A member-account row is refused with no EC2 client built; a host-account row is signed with the host key;GetCloudAccountis never called.role_arnis refused, a nil or erroring account lookup and resolver errors never fall back to the host, and authz is unchanged.legacy-accountinpermission_constraints_marketplace_integration_test.gonow gives the NULL-account row the hostaccount_id. Without that fixture change both legacy-account subtests fail with the new code, which is the intended behaviour change.Fail-before/pass-after: putting back main's
return base, nilfails 7 of the new subtests. Mutation probes, each caught by at least one test:VerifyHostAccount;GetCloudAccountfor a NULL-account row.Commands run on this revision:
go build ./...,go vet ./...,go vet -tags integration ./internal/api/go test ./internal/api/ -count=1,go test ./internal/credentials/ -count=1,go test ./internal/mocks/ -count=1go test -tags integration ./internal/api/ -run 'TestMarketplacePermissionConstraintsHTTP|Compensation'(testcontainers Postgres)golangci-lint run --allow-parallel-runners ./internal/api/...(v2.10.1): 0 issuesgocyclo -over 10 internal/api/handler_marketplace.go: cleanbash scripts/test-git-secrets-allowlist.sh: 42 PASS, 0 FAILNot verified against real AWS/STS.
Out of scope
(aws, account_id)lookup for non-host NULL-account rows (fix(api): marketplace rows with a NULL cloud_account_id use the host's ambient credentials #560).ErrNotHostAccounthandling.loadAWSConfigWithRegion(separate handler family).Closes #261
🤖 Generated with Claude Code