Skip to content

fix(auth): scope ladder configs to the caller's allowed accounts - #1489

Merged
cristim merged 2 commits into
mainfrom
fix/1428-ladder-config-account-scoping
Jul 22, 2026
Merged

cristim merged 2 commits into
mainfrom
fix/1428-ladder-config-account-scoping

Conversation

@cristim

@cristim cristim commented Jul 22, 2026 •

Copy link
Copy Markdown
Member

Summary

Fixes a cross-account data leak in getLadderConfigs (GET /ladder/configs), found via adversarial review of #1428.

  • Migration 000088 (part of fix(auth): grant view:config to non-admin groups; disable settings fields for read-only users #1428) granted view:config permission to the Standard-User and Read-Only-User groups, which previously only admins held.
  • getLadderConfigs in internal/api/handler_ladder.go was never updated to account for that: it returned every account's LadderConfigDB (spend caps, ramp schedules) unconditionally, regardless of the caller's allowed_accounts scope.
  • Result: any scoped non-admin user could enumerate other accounts' spend caps and ramp schedules simply by having been granted view:config.

Root cause

getLadderConfigs called h.config.GetLadderConfigs(ctx) and returned the result unfiltered. There was no scoping step, unlike the analogous purchase-history endpoint which already went through filterPurchaseHistoryByAllowedAccounts (added for issue #1030).

Fix

  • getLadderConfigs now captures the session from requirePermission and filters the result through a new filterLadderConfigsByAllowedAccounts method, mirroring the existing filterPurchaseHistoryByAllowedAccounts pattern.
  • Admin / unrestricted sessions (auth.IsUnrestrictedAccess) pass through unfiltered.
  • Scoped sessions are filtered via auth.MatchesAccount(allowed, c.CloudAccountID, nameByID[c.CloudAccountID]).
  • Fails closed: if getAllowedAccounts errors, the whole request errors out rather than falling through to unfiltered data.

Tests

Two new regression tests in internal/api/handler_per_account_perms_test.go:

  • TestPerAccountPerms_GetLadderConfigs_ScopedUserSeesOnlyOwnAccount: the real failing scenario. A scoped user with allowed_accounts: [accA] must not see account B's config when the store returns configs for both accounts. Verified this fails on the pre-fix code (account-B config leaks through) and passes post-fix.
  • TestPerAccountPerms_GetLadderConfigs_AdminSeesAll: an admin/unrestricted session still receives every account's config.

Also added t.Cleanup(mockStore/mockAuth.AssertExpectations) to an existing test in handler_config_test.go per this repo's mock-assertion convention.

Test plan

  • go build ./...
  • go vet ./...
  • go test ./internal/api/... (full package, all green)
  • golangci-lint run (pinned v2.10.1, matching CI) — 0 issues
  • gocyclo -over 10 on changed files — clean
  • Manually reverted the filter call and confirmed the new regression test fails pre-fix, then confirmed it passes after restoring the fix

Context only: #1428 (already merged; this is a fix-forward PR for a scoping gap it introduced, not a fix to #1428 itself).

Summary by CodeRabbit

  • Bug Fixes
    • Ladder configuration results are now restricted to the accounts permitted for the signed-in user.
    • Users with unrestricted access continue to see ladder configurations across all accounts.
  • Tests
    • Added coverage to verify account-level filtering and unrestricted access behavior.
    • Improved automatic validation of mocked service expectations in configuration update tests.

Migration 000088 granted view:config to the Standard-User and
Read-Only-User groups (previously admin-only), but getLadderConfigs
returned every account's ladder config unconditionally, a cross-account
data leak: any scoped non-admin user with view:config could enumerate
other accounts' spend caps and ramp schedules.

Add filterLadderConfigsByAllowedAccounts, mirroring the existing
filterPurchaseHistoryByAllowedAccounts pattern from issue #1030.
Admin/unrestricted sessions pass through unchanged; scoped sessions
are filtered by allowed_accounts. Fails closed: if getAllowedAccounts
errors, the request errors out instead of falling through to
unfiltered data.

Adds two regression tests: a scoped user must see only their own
account's config when configs exist for multiple accounts, and an
admin session must still see every account's config.
@cristim cristim added triaged Item has been triaged priority/p1 Next up; this sprint severity/high Significant harm urgency/this-sprint Within the current sprint impact/many Affects most users type/security Security finding labels Jul 22, 2026
@cristim

cristim commented Jul 22, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 22, 2026 •

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Jul 22, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Review skipped

No new commits to review since the last review.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 252aa239-0723-4d6f-99ee-de805aec872a

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The ladder configuration endpoint now filters results by the caller’s allowed cloud accounts. New tests cover scoped users and admins, and an existing test adds mock expectation cleanup.

Changes

Ladder configuration account scoping

Layer / File(s) Summary
Filter ladder configurations
internal/api/handler_ladder.go
getLadderConfigs captures the permission session and filters configurations by allowed cloud accounts while preserving unrestricted access.
Validate scoped responses
internal/api/handler_per_account_perms_test.go, internal/api/handler_config_test.go
Tests verify scoped users receive only their account’s configurations, admins receive all configurations, and mocks assert expectations during cleanup.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

Suggested labels: type/bug

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: restricting ladder configs to the caller's allowed accounts.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/1428-ladder-config-account-scoping

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

@coderabbitai coderabbitai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/api/handler_per_account_perms_test.go`:
- Around line 1383-1413: Add a mock-first regression test alongside
TestPerAccountPerms_GetLadderConfigs_ScopedUserSeesOnlyOwnAccount that
configures GetAllowedAccountsAPI to return an error, then invokes
handler.getLadderConfigs with the scoped request. Assert the error is returned
and the response is nil, covering the fail-closed allowlist-lookup branch.
🪄 Autofix (Beta)

✅ Autofix completed


ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c0f34d08-3b73-459e-92b9-44fa50e5fa03

📥 Commits

Reviewing files that changed from the base of the PR and between 1cc33d3 and de752b2.

📒 Files selected for processing (3)
  • internal/api/handler_config_test.go
  • internal/api/handler_ladder.go
  • internal/api/handler_per_account_perms_test.go

Comment on lines +1383 to +1413
func TestPerAccountPerms_GetLadderConfigs_ScopedUserSeesOnlyOwnAccount(t *testing.T) {
ctx := context.Background()

cfgA := config.LadderConfigDB{ID: "cfg-a", CloudAccountID: permsAccA, Provider: "aws"}
cfgB := config.LadderConfigDB{ID: "cfg-b", CloudAccountID: permsAccB, Provider: "aws"}

mockStore := new(MockConfigStore)
mockStore.On("GetLadderConfigs", ctx).Return([]config.LadderConfigDB{cfgA, cfgB}, nil)
mockStore.ListCloudAccountsFn = func(_ context.Context, _ config.CloudAccountFilter) ([]config.CloudAccount, error) {
return permsAccountList(), nil
}

handler := &Handler{
auth: scopedAuthMock(ctx),
config: mockStore,
}

resp, err := handler.getLadderConfigs(ctx, scopedReq())
require.NoError(t, err)
body, ok := resp.(map[string]any)
require.True(t, ok, "expected map[string]any, got %T", resp)
configs, ok := body["configs"].([]config.LadderConfigDB)
require.True(t, ok, "expected []config.LadderConfigDB, got %T", body["configs"])

ids := make([]string, len(configs))
for i, c := range configs {
ids[i] = c.ID
}
assert.ElementsMatch(t, []string{"cfg-a"}, ids,
"scoped user must see only account-A's ladder config; account-B config must be filtered out")
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

go build ./...
go test ./...

if [[ -f pkg/go.mod ]]; then
  (cd pkg && go test ./...)
fi

Repository: LeanerCloud/CUDly

Length of output: 147


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

printf '\n## File outline\n'
ast-grep outline internal/api/handler_per_account_perms_test.go --view expanded || true

printf '\n## Search for GetAllowedAccountsAPI and getLadderConfigs\n'
rg -n "GetAllowedAccountsAPI|getLadderConfigs|GetLadderConfigs" internal/api -S

printf '\n## Relevant test region\n'
sed -n '1320,1460p' internal/api/handler_per_account_perms_test.go

printf '\n## Relevant handler implementation region\n'
sed -n '1,260p' internal/api/handler_per_account_perms.go

Repository: LeanerCloud/CUDly

Length of output: 16632


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

rg -n "GetAllowedAccountsAPI|allowed-account|allowlist|GetLadderConfigs" internal/api/handler_per_account_perms_test.go internal/api/handler_per_account_perms.go -S

sed -n '1360,1455p' internal/api/handler_per_account_perms_test.go

Repository: LeanerCloud/CUDly

Length of output: 1540


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

printf '\n## Locate ladder handler\n'
fd -a "handler_ladder.go|handler_per_account_perms.go" internal/api

printf '\n## Outline ladder handler\n'
ast-grep outline internal/api/handler_ladder.go --view expanded || true

printf '\n## Read ladder handler\n'
sed -n '1,220p' internal/api/handler_ladder.go

printf '\n## Read auth interface and any shared permission helper\n'
sed -n '180,260p' internal/api/types.go
sed -n '380,460p' internal/api/handler.go

Repository: LeanerCloud/CUDly

Length of output: 16417


Cover the allowlist-lookup failure path. Add a mock-first regression test for GetAllowedAccountsAPI returning an error, and assert getLadderConfigs returns an error with a nil response. The current tests only cover successful scoped and unrestricted paths, so the fail-closed branch stays unguarded.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/api/handler_per_account_perms_test.go` around lines 1383 - 1413, Add
a mock-first regression test alongside
TestPerAccountPerms_GetLadderConfigs_ScopedUserSeesOnlyOwnAccount that
configures GetAllowedAccountsAPI to return an error, then invokes
handler.getLadderConfigs with the scoped request. Assert the error is returned
and the response is nil, covering the fail-closed allowlist-lookup branch.

Source: Coding guidelines

@coderabbitai

coderabbitai Bot commented Jul 22, 2026 •

Copy link
Copy Markdown
Contributor

Note

Autofix is a beta feature. Expect some limitations and changes as we gather feedback and continue to improve it.

Fixes Applied Successfully

Fixed 1 file(s) based on 1 unresolved review comment.

Files modified:

  • internal/api/handler_per_account_perms_test.go

Commit: 2f55302d14d0ddb7ce2bd0599761d95c57f55d67

The changes have been pushed to the fix/1428-ladder-config-account-scoping branch.

Time taken: 4m 3s

Fixed 1 file(s) based on 1 unresolved review comment.

Co-authored-by: CodeRabbit <noreply@coderabbit.ai>
@cristim
cristim merged commit 1dea72c into main Jul 22, 2026
19 checks passed
@cristim
cristim deleted the fix/1428-ladder-config-account-scoping branch July 27, 2026 11:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

impact/many Affects most users priority/p1 Next up; this sprint severity/high Significant harm triaged Item has been triaged type/security Security finding urgency/this-sprint Within the current sprint

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant