Repository navigation
feat(ri-exchange): name the RIs' account in the RI table and Exchange dialog - #1714
Conversation
… dialog Neither the Active Convertible RIs table nor the Exchange dialog said which account owns the RI, so an operator could not confirm the target before an irreversible exchange. The instances API now labels each RI with the STS-resolved running account (the account filter still fails closed); the UI shows the registered account name and ID above the table and in the dialog, or says the account could not be determined. Closes #1692 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Warning Review limit reachedYou've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Next included review available in 43 minutes. View limit details
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. |
|
@coderabbitai review |
Action not completed
|
|
Cold review: APPROVE at What I checked:
Local verification at this SHA (macOS, scratch worktree):
Non-blocking:
|
cristim
left a comment
There was a problem hiding this comment.
APPROVE @ fd922b0 by claude-nibblets
Cold review of c0d57f4...fd922b0 (1 commit), done in my own scratch clone; c0d57f4 is the current main tip. The PR body was treated as a claim. This is a comment review because GitHub does not let the PR author's account approve its own PR.
No account ID or name reaches a caller who lacks access to that account.
-
The scope gate runs before the new label.
listConvertibleRIscallsreshapeCloudAccountInScopefirst (handler_ri_exchange.go:1531):- an unrestricted session passes;
- a restricted session resolves the deployment's AWS account through STS and maps it to a registered CloudAccount (
resolveAWSCloudAccountID,exchange_lookup.go:175), which must be in its allowed accounts; - an STS failure, a cloud-account list failure, or an AWS account matching no registered account while others exist is an error, so it fails closed;
- the bootstrap case (no registered accounts) gives
"", which a restricted scope doesn't allow.
So
account_idis only ever returned alongside RIs the caller was already allowed to list, and an out-of-scope caller getsinstances: []with no account at all. -
The label is the RIs' real account. STS and the EC2 RI client use the same cached base config (
getBaseAWSConfig,:1367-1390), soaccount_idnames the account the RIs are read from. -
Names come only from accounts the caller may see. The UI takes names from
/api/accounts/list, which requiresview:recommendationsand filters by the session's allowed accounts (handler_accounts.go:160-185). A caller without that grant gets a 403. It is caught and falls back to "AWS account ", an ID they already received with the RIs. -
Fail closed where it matters. The
?account_id=filter still fails closed on an STS error and still returns an empty list on a mismatch; the existing #871 tests pin both. Without a filter the account only labels rows, so an STS failure logs a warning and omitsaccount_id(the UI says "could not be determined"). That is fine, because the scope gate has already run and failed closed for restricted sessions.
Multi-account and constrained grants.
- The list holds only the deployment account's RIs, from ambient credentials without AssumeRole, so a single label above the table is accurate. The body's reason for not adding a per-row column holds.
- A restricted grant whose allowed accounts exclude the deployment account sees an empty list. A grant that includes it sees the label. An unregistered deployment account gives an error for restricted sessions and "AWS account " for unrestricted ones.
Rendering is safe. The table label is built with escapeHtml(...) inside innerHTML, and the dialog row uses textContent. An empty RI list returns early to the empty state, so there's no bare "Account:" line.
The new tests fail on main.
- jest: with
riexchange.tsreverted to main (the head'stypes.tskept), all 6 tests inriexchange-account-1692.test.tsfail. - Go: the 3 new tests in
handler_ri_exchange_account_test.gocan't compile against main's handler, which has noAccountIDonec2ConvertibleRI. They pass at the head.
Mutation probes (each run in a scratch copy and reverted):
| Mutation | Result |
|---|---|
| Unfiltered STS failure fails the whole list | killed |
| Label left empty | killed (2) |
| Filtered STS failure fails open | killed (existing #871 AccountResolveErrorFailsClosed) |
| Filter mismatch kept in scope | killed (existing #871 AccountScopeMismatchReturnsEmpty) |
| ID always appended, even when the name contains it | killed (2) |
| Unregistered account shown as "could not be determined" | killed (2) |
Missing account_id shown blank |
killed |
escapeHtml removed from the table label |
survives (see 3) |
Local verification at this SHA (macOS, under the review lock):
go test -count=1 ./internal/api/passes;npm cisucceeds;npm testgives 183 suites, 4720 passed and 1 skipped;npm run lintreports 0 errors (141 existing warnings);npm run typecheckandnpm run buildsucceed.
Not blocking
-
STS cost per request.
resolveAWSCallerIdentity(handler.go:1132-1155) is not cached, andlistRIsAccountnow calls it on every list. An unrestricted session goes from 0 STS calls (without a filter) to 1, and a restricted session from 1 to 2, since the scope gate resolves separately.GetCallerIdentityis free, but each call adds a network round trip and uses STS quota on every RI Exchange load. The ambient account can't change within a process, so caching it besideawsCfgOnce, or resolving it once and passing it to both the gate and the label, would remove the repeat calls. -
A dialog opened before the list loads has no account row. The Exchange dialog can open from a reshape recommendation (
fillQuoteFromRecommendation,riexchange.ts:1161-1165). The reshape list loads in parallel with the RI list, so a dialog opened before the RI list lands, or after it failed, finds nosourceRIand silently omits the row. That's the irreversible-exchange case #1692 is about. Rendering "Account: could not be determined" whensourceRIis missing would keep the dialog honest. -
Escaping is untested. No test pins
escapeHtmlon the table label. Account names are operator-set, so a case with a name like<b>x</b>, asserting it appears as literal text, would guard against a regression to unescaped HTML.
CI at this SHA: 41 checks SUCCESS and CodeRabbit NEUTRAL. The merge state is CLEAN.
Not merging.
Cold review verdict (second reviewer): APPROVE at fd922b0Reviewer: claude-ui-fix-2. I did not author this PR and had no author transcript, and I treated the PR body as a claim. Local verification (macOS, at fd922b0, base c0d57f4, under heavy-review-2.lock with the pw-4173 port lock)
What I checked
Non-blocking
|
|
Merge gate (claude-qa-ui-4) at fd922b0:
Squash-merging with |
Closes #1692
Problem
Neither the Active Convertible RIs table on Inventory & Coverage > RI Exchange nor the Exchange dialog said which cloud account owns the RI. An operator couldn't confirm the target account before quoting or executing an irreversible exchange.
Change
API (
internal/api/handler_ri_exchange.go)GET /api/ri-exchange/instancesnow carriesaccount_id: the AWS account the RIs are read from, which is the deployment's ambient credentials resolved via STS. The newconvertibleRIWithAccountwraps the library'sConvertibleRI, and the embedded fields stay at the top level of the JSON.listRIsAccount. The?account_id=filter behaves as before: an STS failure fails closed, and a different account returns an empty list. Without a filter the account only labels the rows, so an STS failure logs a warning, omitsaccount_idand still returns the list.listConvertibleRIsbuilds its EC2 client through the existingbuildReshapeEC2Clientseam (production behaviour unchanged), so the handler can be tested with a stub.openapi.yaml: the 200 description documentsaccount_id.UI (
frontend/src/riexchange.ts)listAccountsMinimal({ provider: 'aws' }), fetched alongside the RI list. The ID is appended only when the name doesn't already contain it (avoiding the doubled ID noted in ux(admin): Admin pages UX nits (Accounts, Users/Groups, Purchasing, cross-page) #1691). It falls back to "AWS account " for an unregistered account or when names can't be loaded, and to "could not be determined" when the API omittedaccount_id.Why an account line, not a per-row column: the list only ever holds the deployment's own account (the RIs are read with its ambient credentials, without AssumeRole), so a column would repeat one value on every row. It would also add width to a table that already clips its Utilization header at 1440px (#1682).
Tests
handler_ri_exchange_account_test.go): every RI is labelled with the running account, and the JSON keepsreserved_instance_id/instance_typetop-level; a matching filter keeps the label; an STS failure without a filter still lists and omitsaccount_id. The existing fix(purchase): isolate manual cleanup failures per plan #871 mismatch and fail-closed tests are unchanged and pass.riexchange-account-1692.test.ts): name with the ID inside, name without it, unregistered account, names fail to load, missingaccount_id, and the dialog row. All 6 fail against main'sriexchange.ts.{ provider: 'aws' }name lookup.🤖 Generated with Claude Code