Skip to content

feat(accounts): wire AWS Organizations discovery endpoint (closes #208)#212

Merged
cristim merged 5 commits intofeat/multicloud-web-frontendfrom
feat/issue-208-org-discovery
Apr 30, 2026
Merged

feat(accounts): wire AWS Organizations discovery endpoint (closes #208)#212
cristim merged 5 commits intofeat/multicloud-web-frontendfrom
feat/issue-208-org-discovery

Conversation

@cristim
Copy link
Copy Markdown
Member

@cristim cristim commented Apr 30, 2026

Closes #208 — wires POST /api/accounts/discover-org to the already-shipped accounts.DiscoverOrgAccounts function. Replaces the stub at internal/api/handler_accounts.go:1089 that returned "org discovery not yet implemented".

What the endpoint does

Given {"account_id": "<org-root-uuid>"}:

  1. Validate — body parses, account_id is a UUID, named account exists, provider == "aws", aws_is_org_root == true. Each failure returns the right ClientError code (400 for parse/UUID/non-aws/non-root, 404 for missing).
  2. Resolve — load the org-root's stored credentials via credentials.ResolveAWSCredentialProvider; build an aws.Config for the call.
  3. Discover — call accounts.DiscoverOrgAccounts(ctx, cfg) (the existing function with full Organizations API pagination + tests at internal/accounts/org_discovery_test.go). Injectable seam: Handler.discoverOrgFn for tests; nil-default falls back to the real call in production.
  4. Dedupe — load existing aws cloud_accounts via ListCloudAccounts(provider="aws"); skip member accounts whose external_id already exists.
  5. Persist — for each new member, force enabled=false + aws_auth_mode="bastion" + aws_bastion_id=<org-root.ID> and call CreateCloudAccount. The operator must review/approve and fill in aws_role_arn before the row participates in scheduled collection.
  6. Return {discovered, created, skipped} counts.

Spec sections: specs/multi-account-execution/acceptance.md F-1, F-2, F-3.

Why these defaults

  • enabled=false: gates the operator into reviewing each discovered account before the scheduler picks it up. Spec requirement.
  • aws_auth_mode="bastion" + aws_bastion_id=<org-root>: signals the intended cred-resolution path (the org-root assumes a role in the member). The operator fills in aws_role_arn before flipping enabled=true. The scheduler silently skips disabled rows so a half-configured account never causes a runtime error.
  • Created excludes skipped rows: dedupe is by (provider="aws", external_id), matching the spec's natural-key semantics. Re-running the endpoint is idempotent.

Refactor for gocyclo

The handler logic split into four helpers to fit the project's gocyclo budget (≤10):

  • parseDiscoverOrgRoot — body decode + validate
  • buildOrgRootAWSConfig — credential resolution + aws.Config build
  • runOrgDiscoverydiscoverOrgFn dispatcher
  • persistDiscoveredMembers — dedupe + persist loop

The main discoverOrgAccounts is now a 5-step coordinator.

Tests

Six new test cases in internal/api/handler_accounts_test.go cover the spec's acceptance dimensions:

Test Asserts
TestDiscoverOrgAccounts_RejectsInvalidBody 400 ClientError on non-JSON body
TestDiscoverOrgAccounts_RejectsInvalidAccountID 400 on account_id that isn't a UUID
TestDiscoverOrgAccounts_RejectsNonAWSAccount 400 + "aws" in message when provider != "aws"
TestDiscoverOrgAccounts_RejectsNonOrgRoot 400 + "org root" in message when aws_is_org_root == false
TestDiscoverOrgAccounts_NotFound 404 when the named account doesn't exist
TestDiscoverOrgAccounts_HappyPathDedupesAndPersists 3 discovered → 1 skipped (already exists) → 2 created. Each persisted row has enabled=false, aws_auth_mode="bastion", aws_bastion_id=<root.ID>

MockConfigStore gains ListCloudAccountsFn + CreateCloudAccountFn injection points; fakeCredStore is a minimal CredentialStore satisfying access_keys mode without dragging in Postgres / Secrets Manager.

TestRouter_discoverOrgAccountsHandler (routing-only smoke) updated to assert the dispatcher reaches the handler — now expects a 400 ClientError on the empty body it sends, instead of the prior NotNil result from the stub.

Verification

  • go build ./... clean
  • go test ./... (full repo) — all green
  • Pre-commit (gocyclo / gosec / trivy / migration-conflicts / go-test) — all green

Triage

type/feat, severity/high, urgency/this-sprint, impact/many, effort/m, priority/p1, triaged. Closes #208.

Summary by CodeRabbit

  • New Features

    • Admin-only AWS Organizations discovery endpoint to import member accounts with input validation, deduplication, safe defaults, and summary counts for discovered/created/skipped.
    • Configurable discovery hook to allow tests to run without calling live AWS.
  • Tests

    • Expanded handler tests and improved test mocks covering request validation, error cases, deduplication, creation behavior, and persistence verification.

…OrgAccounts (closes #208)

POST /api/accounts/discover-org used to return a stub
"not yet implemented" message. Wire it end-to-end against the
already-shipped accounts.DiscoverOrgAccounts function:

1. Parse {account_id} body, validate it's a UUID.
2. Load the named account via config.GetCloudAccount; 404 if missing.
3. Validate provider == "aws" and aws_is_org_root == true; 400
   otherwise.
4. Resolve the org-root's stored credentials via
   credentials.ResolveAWSCredentialProvider and build an aws.Config.
5. Call accounts.DiscoverOrgAccounts (injectable seam:
   Handler.discoverOrgFn for tests; nil-default falls back to the
   real Organizations API call).
6. Dedupe each member account by (provider="aws", external_id)
   against the existing aws cloud_accounts roster.
7. Persist new members with enabled=false, aws_auth_mode=bastion,
   aws_bastion_id pointing at the org root — so the operator must
   review/approve and fill in the target role ARN before any
   discovered account participates in scheduled collection.
8. Return {discovered, created, skipped} counts.

Spec acceptance F-1, F-2, F-3 from
specs/multi-account-execution/acceptance.md.

Test additions in handler_accounts_test.go cover the primary
acceptance dimensions:
- 400 on invalid JSON body
- 400 on invalid account_id (not a UUID)
- 400 on non-AWS account
- 400 on aws_is_org_root=false
- 404 on account not found
- happy path: 3 discovered, 1 already known (skipped), 2 created;
  asserts each persisted row has enabled=false +
  aws_auth_mode=bastion + aws_bastion_id=org-root.ID

The router-level smoke test in router_handlers_test.go is updated
to assert the routing dispatcher reaches the handler (now expects
a ClientError 400 on the empty body it sends, instead of the prior
NotNil result from the stub).

MockConfigStore gains ListCloudAccountsFn + CreateCloudAccountFn
hooks so the org-discovery happy-path test can inject an existing
roster and capture the persisted rows.
@cristim cristim added type/feat New capability severity/high Significant harm urgency/this-sprint Within the current sprint impact/many Affects most users effort/m Days priority/p1 Next up; this sprint triaged Item has been triaged labels Apr 30, 2026
@cristim
Copy link
Copy Markdown
Member Author

cristim commented Apr 30, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 30, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c18cd455-0fff-4928-82e3-31521d730597

📥 Commits

Reviewing files that changed from the base of the PR and between 38d74ea and c6bf717.

📒 Files selected for processing (2)
  • internal/api/handler_accounts.go
  • internal/api/handler_accounts_test.go

📝 Walkthrough

Walkthrough

Adds a production implementation for POST /api/accounts/discover-org: validates org-root account, resolves its AWS credentials, invokes AWS Organizations discovery (injectable for tests), deduplicates by external_id against stored cloud accounts, persists new cloud accounts with locked defaults, and returns discovered/created/skipped counts.

Changes

Cohort / File(s) Summary
Endpoint implementation
internal/api/handler_accounts.go
Implements POST /api/accounts/discover-org: request/response types, request parsing/validation, aws_is_org_root check, credential resolution, call to discovery function (injectable), dedupe by external_id, create new cloud_accounts with enabled=false and aws_bastion_id=orgRoot, return counts and surface errors.
Handler DI
internal/api/handler.go
Adds optional discoverOrgFn func(context.Context, aws.Config) (*accounts.OrgDiscoveryResult, error) field to Handler to allow injecting org discovery in tests (production path uses accounts.DiscoverOrgAccounts when nil).
Handler tests
internal/api/handler_accounts_test.go
Replaces stub with comprehensive handler-level tests covering malformed requests (400), invalid account_id (400), non-AWS / non-org-root (400/404), and happy-paths using injected discoverOrgFn + fake credential store that verify dedupe, creation, and persisted defaults.
Mock test helpers
internal/api/mocks_test.go
Adds ListCloudAccountsFn and CreateCloudAccountFn hooks to MockConfigStore so tests can supply/fetch cloud-account fixtures and capture persisted rows for dedupe/verification.
Router test
internal/api/router_handlers_test.go
Updates router-level test for discover-org to expect a 400 client error on empty request body instead of prior success assertion.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Handler
    participant ConfigStore
    participant CredResolver
    participant DiscoveryFn as Discovery
    participant CloudAccountStore

    Client->>Handler: POST /api/accounts/discover-org { account_id }
    Handler->>ConfigStore: Load account by ID
    ConfigStore-->>Handler: Account (may include stored credentials)
    Handler->>Handler: Validate aws_is_org_root == true
    Handler->>CredResolver: Resolve AWS credentials for account
    CredResolver-->>Handler: aws.Config
    Handler->>DiscoveryFn: DiscoverOrg(ctx, aws.Config)
    DiscoveryFn-->>Handler: []MemberAccounts
    Handler->>ConfigStore: ListCloudAccounts(filter by provider/external_id)
    ConfigStore-->>Handler: []ExistingAccounts
    Handler->>Handler: Deduplicate discovered vs existing
    loop For each new member
        Handler->>CloudAccountStore: CreateCloudAccount(enabled=false, aws_bastion_id=orgRoot, defaults)
        CloudAccountStore-->>Handler: Persisted account
    end
    Handler-->>Client: 200 OK { discovered, created, skipped }
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 I sniffed the org and found its thread,
Keys in paw, across accounts I sped,
I skipped the twins and tucked new ones in,
Disabled gates, bastion set on the win,
Hop-hop, I cheer — discovery well fed.
🌿✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(accounts): wire AWS Organizations discovery endpoint (closes #208)' accurately and concisely describes the main change: implementing the AWS Organizations discovery endpoint by wiring it to the existing functionality.
Linked Issues check ✅ Passed The PR comprehensively implements all acceptance criteria from issue #208: admin-only access, org-root validation, credential resolution, deduplication by (provider, external_id), new account creation with enabled=false and aws_bastion_id set, and returns discovered/created/skipped counts.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing the AWS Organizations discovery endpoint. The mock enhancements, handler modifications, and comprehensive test coverage align with the PR objectives and issue #208 requirements.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/issue-208-org-discovery

Review rate limit: 3/5 reviews remaining, refill in 23 minutes and 8 seconds.

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

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 30, 2026

✅ Actions performed

Review triggered.

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.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/api/handler_accounts.go`:
- Around line 1174-1183: The error handling in buildOrgRootAWSConfig incorrectly
maps all failures from credentials.ResolveAWSCredentialProvider to a 400 client
error; instead, examine the returned error from ResolveAWSCredentialProvider (in
buildOrgRootAWSConfig) and only convert to NewClientError(400, ...) for definite
client-side validation issues (e.g., sentinel ErrBadCredentials or a typed
validation error), while mapping transient/server-side failures (credential
store unavailable, STS errors, network/timeouts) to a server error (e.g., return
NewServerError(500, fmt.Sprintf(...)) or wrap and return the original error) so
retries are possible; update the conditional around
credentials.ResolveAWSCredentialProvider to branch on error type or sentinel
values and return the appropriate error type rather than unconditionally using
NewClientError(400,...).
- Around line 1226-1232: The code is defaulting new members to
member.AWSAuthMode = "bastion" while leaving member.AWSRoleARN empty, which
causes awsAmbientCredResult to treat the account as having valid ambient creds;
change the default so newly persisted rows do not claim bastion mode until a
role is configured (e.g., set member.AWSAuthMode = "" or "bastion_pending"
instead of "bastion"), or alternatively update awsAmbientCredResult to
explicitly require a non-empty AWSRoleARN when AWSAuthMode == "bastion"; modify
either the initializer around
member.Enabled/member.AWSAuthMode/member.AWSBastionID or the
awsAmbientCredResult validation logic to enforce that bastion mode implies a
configured AWSRoleARN.
- Around line 1119-1125: The handler discoverOrgAccounts currently calls
requirePermission(ctx, req, "create", "accounts") which allows non-admin users;
change the permission check to require admin-only by calling
requirePermission(ctx, req, "admin", "accounts") (or otherwise enforce admin
scope) before parsing the org root, and/or additionally call
requireAccountAccess to scope the org-root account if applicable; update the
check in discoverOrgAccounts so only admin-scoped principals can invoke this
endpoint.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2f85acbe-8891-43e2-a4c1-3c9e210edae1

📥 Commits

Reviewing files that changed from the base of the PR and between a21c140 and 289071f.

📒 Files selected for processing (5)
  • internal/api/handler.go
  • internal/api/handler_accounts.go
  • internal/api/handler_accounts_test.go
  • internal/api/mocks_test.go
  • internal/api/router_handlers_test.go

Comment thread internal/api/handler_accounts.go
Comment thread internal/api/handler_accounts.go
Comment thread internal/api/handler_accounts.go
…, drop misleading bastion default, drop blanket-400 (#212)

Three CR-pass-1 findings on PR #212 addressed in one commit:

1. Permission scope: switch from requirePermission("create",
   "accounts") to requireAdmin. Org discovery can create N
   cloud_accounts rows in one call and bring unfamiliar accounts
   into the roster — admin scope is the right gate even though
   the rows boot disabled. requireAdmin honours both the admin
   API-key and bearer-token paths used elsewhere.

2. Drop the default AWSAuthMode="bastion" on persisted rows.
   awsAmbientCredResult treats role_arn or bastion modes with an
   empty AWSRoleARN as "ambient host credentials" — semantically
   correct for the CUDly self-account, but WRONG for a
   freshly-discovered member account, which would have falsely
   passed the test-credentials endpoint. Persist with
   AWSAuthMode="" instead; the operator's review step must set
   BOTH the mode AND a non-empty AWSRoleARN before flipping
   enabled=true. AWSBastionID is still pre-filled with the org
   root's ID so the operator's edit is just "add role ARN, set
   mode=bastion". The empty mode also fails
   ResolveAWSCredentialProvider's switch with a clear
   "unsupported aws_auth_mode" error if the row is enabled
   prematurely.

3. buildOrgRootAWSConfig no longer wraps every
   ResolveAWSCredentialProvider error as ClientError(400). The
   resolver mixes definite client-side validation failures
   (missing aws_role_arn) with transient server-side ones
   (credential store unavailable, network errors during
   access-key load); without a sentinel/typed error in the
   credentials package today, blanket-400 was misleading. Wrap
   the error as a regular Go error instead — the handler-default
   surface is 5xx, retries are possible, and the error message
   makes it into the structured log either way. A proper
   400/500 split lives in a future credentials-package
   error-type cleanup; flagged in the doc comment.

Test update: TestDiscoverOrgAccounts_HappyPathDedupesAndPersists
now asserts AWSAuthMode is empty on persisted rows (was checking
"bastion"). The doc above the assertions explains the WHY so a
future maintainer doesn't "fix" it back.

go test ./... clean. No behaviour change other than the three
items above.
@cristim
Copy link
Copy Markdown
Member Author

cristim commented Apr 30, 2026

CR pass 1 addressed — pushed 3c713b8da. All three findings.

1. Permission scope: requirePermission("create", "accounts")requireAdmin

Org discovery is a privileged operation (creates N cloud_accounts rows in one call, can bring unfamiliar accounts into the roster). Even though the rows boot enabled=false, admin-scope is the right gate. requireAdmin honours both the admin-API-key path and the bearer-token-with-session.Role==admin path.

2. Default AWSAuthMode = "bastion""" (verified the bug)

CR was right — verified against awsAmbientCredResult (internal/api/handler_accounts.go:609-625): case "role_arn", "bastion": returns OK: true when AWSRoleARN == "" (the "self-account ambient" branch). Persisting a discovered member with bastion + empty AWSRoleARN would have falsely classified it as having valid ambient creds — wrong semantics for a cross-account member.

Fix: persist with AWSAuthMode = "" instead. Operator must set BOTH mode AND a non-empty AWSRoleARN before flipping enabled=true. AWSBastionID is still pre-filled with the org root's ID so the operator's edit is just "add role ARN, set mode=bastion" — minimal friction. Empty mode also fails ResolveAWSCredentialProvider's switch with a clear "unsupported aws_auth_mode" error if the row is enabled prematurely.

The test's persisted-row assertions updated to expect AWSAuthMode empty, with a comment block above explaining the WHY so a future maintainer doesn't "fix" it back.

3. NewClientError(400, …) on cred-resolver failure → unconditional fmt.Errorf wrap

credentials.ResolveAWSCredentialProvider mixes:

  • definite client-side validation (e.g. missing aws_role_arn for role_arn mode) — would warrant 400
  • transient server-side (credential store unavailable, network errors during access-key load) — definitely 5xx + retryable

Without a sentinel/typed error in the credentials package today, a blanket 400 was misleading. Switched to wrapping as a regular Go error — the handler-default surface is 5xx, retries are possible, and the structured log carries the detail either way. A proper 400/500 split lives in a follow-up credentials-package error-type cleanup; flagged inline in the buildOrgRootAWSConfig doc.

Verification

  • go test ./... (full repo) — all green
  • Pre-commit (gocyclo / gosec / trivy / migration-conflicts / go-test) — green
  • Behaviour: 3 changes only — all per CR's findings; no drive-bys.

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 30, 2026

@cristim: Thanks for the thorough write-up — all three rationales are sound:

  1. requireAdmin — the blast-radius argument (N accounts created in one call) is solid justification for the stricter gate.
  2. AWSAuthMode = "" — the awsAmbientCredResult trace confirms the bug clearly; empty mode failing ResolveAWSCredentialProvider early with an explicit "unsupported aws_auth_mode" is a much safer failure mode than silently passing ambient-cred checks. The explanatory comment block in the test is a good safeguard against accidental reversion.
  3. fmt.Errorf wrap — deferring the 400/500 split until the credentials package gains typed errors is the pragmatic call; a blanket 400 on a transient network error would have been worse than a generic 5xx.

Triggering a fresh review of 3c713b8da.

✅ Actions performed

Review triggered.

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.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
internal/api/handler_accounts.go (1)

1113-1114: ⚡ Quick win

Update the stale aws_auth_mode comments.

These comments still describe the old aws_auth_mode=bastion behavior, but the implementation now intentionally persists an empty auth mode. Keeping them out of sync makes the safety rationale easy to undo by accident.

Also applies to: 1220-1221

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/api/handler_accounts.go` around lines 1113 - 1114, Update the stale
comments that reference aws_auth_mode=bastion to reflect the current behavior of
persisting an empty auth mode: find the comment blocks in handler_accounts.go
(the comments around the code handling creation/persistence of external IDs and
AWS auth setup, e.g., the comment near the logic that "persists the new ones
with enabled=false" and the later comment at the other occurrence) and change
the wording to state that aws_auth_mode is intentionally persisted as an empty
value (not "bastion"), explaining the safety rationale accordingly and keeping
the explanation aligned with the code that writes an empty aws_auth_mode.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/api/handler_accounts.go`:
- Around line 1235-1258: The discovered-member save path fails to initialize the
account metadata: before calling h.config.CreateCloudAccount inside the members
loop, set member.ID and the timestamp fields (member.CreatedAt and
member.UpdatedAt) the same way other CreateCloudAccount callers in this file do
(i.e., generate a new ID and assign current time for CreatedAt/UpdatedAt) so the
store receives valid metadata; do this just prior to the CreateCloudAccount call
in the loop that processes discovered members.

---

Nitpick comments:
In `@internal/api/handler_accounts.go`:
- Around line 1113-1114: Update the stale comments that reference
aws_auth_mode=bastion to reflect the current behavior of persisting an empty
auth mode: find the comment blocks in handler_accounts.go (the comments around
the code handling creation/persistence of external IDs and AWS auth setup, e.g.,
the comment near the logic that "persists the new ones with enabled=false" and
the later comment at the other occurrence) and change the wording to state that
aws_auth_mode is intentionally persisted as an empty value (not "bastion"),
explaining the safety rationale accordingly and keeping the explanation aligned
with the code that writes an empty aws_auth_mode.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 771cf1ae-05b7-45ea-bc13-02cf15900a05

📥 Commits

Reviewing files that changed from the base of the PR and between 289071f and 3c713b8.

📒 Files selected for processing (2)
  • internal/api/handler_accounts.go
  • internal/api/handler_accounts_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • internal/api/handler_accounts_test.go

Comment thread internal/api/handler_accounts.go
@cristim
Copy link
Copy Markdown
Member Author

cristim commented Apr 30, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 30, 2026

✅ Actions performed

Review triggered.

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.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
internal/api/handler_accounts_test.go (1)

525-684: ⚡ Quick win

Add regression tests for the admin-only and 5xx behaviors.

This block locks down the 400/404 paths, but it still doesn't assert the two review fixes that changed the handler contract: non-admin callers must be rejected, and credential-resolution failures must not surface as client errors. One focused case for each here would make those regressions much harder to reintroduce.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/api/handler_accounts_test.go` around lines 525 - 684, Add two
regression tests for discoverOrgAccounts: one that verifies non-admin callers
are rejected (call handler.discoverOrgAccounts without setupAdminAuth or with a
MockAuthService that is not admin and assert it returns a client error with
appropriate forbidden code), and one that verifies credential-resolution
failures do not surface as client errors (inject a credStore or
ResolveAWSCredentialProvider stub that returns an error when
Handler.discoverOrgAccounts is invoked and assert the returned error is not
classified by IsClientError and carries a 5xx server error code). Ensure tests
reference the same Handler.discoverOrgAccounts path, reuse
MockAuthService/setupAdminAuth patterns for auth state, and inject the failing
credential resolver via the Handler.credStore or discoverOrgFn hooks used in the
existing tests so behavior is exercised in-process.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/api/handler_accounts.go`:
- Around line 1225-1263: The loop that persists discovered members treats any
CreateCloudAccount error as fatal but doesn't handle duplicate-key races or
duplicate entries in members; update the logic in the loop that iterates over
members (referencing members, knownExternal, CreateCloudAccount, and
DiscoverOrgResult) to: on duplicate-key (unique constraint) errors increment
result.Skipped and continue, on successful create add member.ExternalID to
knownExternal so subsequent iterations see it as existing (to keep idempotency),
and only return an error for non-duplicate failures; ensure
result.Discovered/Skipped counters reflect these outcomes.

---

Nitpick comments:
In `@internal/api/handler_accounts_test.go`:
- Around line 525-684: Add two regression tests for discoverOrgAccounts: one
that verifies non-admin callers are rejected (call handler.discoverOrgAccounts
without setupAdminAuth or with a MockAuthService that is not admin and assert it
returns a client error with appropriate forbidden code), and one that verifies
credential-resolution failures do not surface as client errors (inject a
credStore or ResolveAWSCredentialProvider stub that returns an error when
Handler.discoverOrgAccounts is invoked and assert the returned error is not
classified by IsClientError and carries a 5xx server error code). Ensure tests
reference the same Handler.discoverOrgAccounts path, reuse
MockAuthService/setupAdminAuth patterns for auth state, and inject the failing
credential resolver via the Handler.credStore or discoverOrgFn hooks used in the
existing tests so behavior is exercised in-process.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f701f71e-d9b6-4f18-8776-22eb4648b725

📥 Commits

Reviewing files that changed from the base of the PR and between 3c713b8 and bb0eddd.

📒 Files selected for processing (2)
  • internal/api/handler_accounts.go
  • internal/api/handler_accounts_test.go

Comment thread internal/api/handler_accounts.go
@cristim
Copy link
Copy Markdown
Member Author

cristim commented Apr 30, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 30, 2026

✅ Actions performed

Review triggered.

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.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/api/handler_accounts.go`:
- Around line 1260-1262: The code sets member.AWSAuthMode = "" before calling
CreateCloudAccount, but the DB column cloud_accounts.aws_auth_mode only allows
('access_keys','role_arn','bastion') so an empty string will cause insert
failures; change the logic in the handler so that AWSAuthMode is left unset/nil
(or explicitly set to a null-equivalent) instead of an empty string before
calling h.config.CreateCloudAccount (or alternatively adjust CreateCloudAccount
to convert empty-string sentinel to NULL) and ensure CreateCloudAccount uses a
NULL value when AWSAuthMode is not provided.
- Around line 1138-1143: The code calls runOrgDiscovery and then unconditionally
dereferences disco.Accounts when returning persistDiscoveredMembers; add a nil
guard to handle cases where runOrgDiscovery returns (nil, nil) to avoid a panic:
after disco, err := h.runOrgDiscovery(ctx, cfg) check if disco == nil and return
an appropriate nil/empty response or error (consistent with surrounding error
handling) before calling persistDiscoveredMembers, referencing the disco
variable and the persistDiscoveredMembers(ctx, root, disco.Accounts) call so the
fix is applied at that call site.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b03ad946-a2fc-43c8-8013-3e924afef40c

📥 Commits

Reviewing files that changed from the base of the PR and between bb0eddd and 38d74ea.

📒 Files selected for processing (2)
  • internal/api/handler_accounts.go
  • internal/api/handler_accounts_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • internal/api/handler_accounts_test.go

Comment thread internal/api/handler_accounts.go
Comment on lines +1260 to +1262
member.AWSAuthMode = ""
member.AWSBastionID = root.ID
if err := h.config.CreateCloudAccount(ctx, &member); err != nil {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

aws_auth_mode="" may fail DB writes under current CHECK constraint.

Line 1260 persists an empty string, but cloud_accounts.aws_auth_mode is constrained to ('access_keys','role_arn','bastion') in internal/database/postgres/migrations/000011_cloud_accounts.up.sql. If CreateCloudAccount writes "" directly (not NULL), discovered-member inserts will fail at runtime.

Consider using NULL for “unset” auth mode (or migrating the constraint to explicitly allow your sentinel).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/api/handler_accounts.go` around lines 1260 - 1262, The code sets
member.AWSAuthMode = "" before calling CreateCloudAccount, but the DB column
cloud_accounts.aws_auth_mode only allows ('access_keys','role_arn','bastion') so
an empty string will cause insert failures; change the logic in the handler so
that AWSAuthMode is left unset/nil (or explicitly set to a null-equivalent)
instead of an empty string before calling h.config.CreateCloudAccount (or
alternatively adjust CreateCloudAccount to convert empty-string sentinel to
NULL) and ensure CreateCloudAccount uses a NULL value when AWSAuthMode is not
provided.

@cristim
Copy link
Copy Markdown
Member Author

cristim commented Apr 30, 2026

Addressed the latest review note with a nil-guard on the discovery path and a regression test for a nil discovery result.

I did not change the empty storage path: already writes through , so an empty string is persisted as SQL NULL rather than an invalid empty enum value. That keeps the current safety invariant intact without widening the DB contract.

@cristim
Copy link
Copy Markdown
Member Author

cristim commented Apr 30, 2026

Addressed the latest review note with a nil-guard on the discovery path and a regression test for a nil discovery result.
The empty AWS auth mode path is already safe because the storage layer writes empty strings as SQL NULL through nullStringFromString, so I left that contract unchanged.

@cristim
Copy link
Copy Markdown
Member Author

cristim commented Apr 30, 2026

@coderabbitai review

@cristim cristim merged commit c84fd02 into feat/multicloud-web-frontend Apr 30, 2026
5 checks passed
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 30, 2026

✅ Actions performed

Review triggered.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/m Days impact/many Affects most users priority/p1 Next up; this sprint severity/high Significant harm triaged Item has been triaged type/feat New capability urgency/this-sprint Within the current sprint

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant