18 KiB
AWS Organizations Bulk Account Connection - Implementation Plan
Context
Prowler SaaS users currently add AWS accounts one at a time. This feature adds a modal-based wizard to bulk discover and connect all accounts in an AWS Organization. In OSS mode, the option appears disabled with a "Get Prowler Cloud" CTA. The backend API (PR #2826) is already merged.
Spec: ui/docs/aws-organizations-bulk-connect.md
Figma: OSS disabled 10248:3258 | Validate Connection 10160:2965 | Launch Scan 10004:102323
1. Architecture: Modal-Based Wizard
The entire Organizations flow lives in a modal dialog (not separate pages). This is a self-contained wizard opened from the existing connect-account page.
User journey:
/providers/connect-account→ Select AWS → See method selector (inline in existing form)- Click "Add Multiple Accounts With AWS Organizations" → Opens
OrgWizardModal - Modal handles all remaining steps internally, closes on completion
Modal internal steps (4-step stepper):
| Step | Name | Content |
|---|---|---|
| 1 ✓ | Link a Cloud Provider | Pre-completed (AWS + Organizations chosen on the page) |
| 2 | Authenticate Credentials | Org name, AWS Org ID, Role ARN, External ID form |
| 3 | Validate Connection | Discovery polling → Account selection → Apply → Connection testing |
| 4 | Launch Scan | Success summary, scan schedule dropdown, Launch Scan button |
Step 3 sub-phases:
- Phase A — Discovery: Polls
getDiscovery()every 3s. Shows spinner + "Discovering your AWS Organization..." - Phase B — Account Selection + In-place Testing: TreeView with checkboxes + "Name (optional)" input fields. User selects accounts and clicks "Test Connections". The same TreeView stays visible while apply + connection tests run in place (checkboxes become spinners/status icons).
Step 4 behavior:
- Shows org name + UID badge
- "Accounts Connected!" with green check
- Scan schedule dropdown ("Scan Daily every 24 hours")
- "Done" → close modal, navigate to
/providers - "Launch Scan" → schedule scans for all providers → close modal → navigate to
/providers→ toast notification
2. File Structure
New Files
ui/
├── types/organizations.ts # Types + const enums
├── actions/organizations/
│ ├── organizations.ts # Server actions (5 API calls)
│ └── organizations.adapter.ts # buildOrgTreeData(), helpers
├── store/organizations/
│ └── store.ts # Zustand store (multi-step state)
├── components/providers/organizations/
│ ├── org-wizard-modal.tsx # Modal shell + stepper + step router
│ ├── org-wizard-stepper.tsx # 4-step vertical stepper (shadcn-based)
│ ├── aws-method-selector.tsx # "Single Account" vs "Organizations" radio
│ ├── org-setup-form.tsx # Step 2: Org details form
│ ├── org-discovery-loader.tsx # Step 3 Phase A: Discovery polling spinner
│ ├── org-account-selection.tsx # Step 3: Tree + checkboxes + in-place apply/test
│ ├── org-launch-scan.tsx # Step 4: Success + scan schedule
│ └── org-account-tree-item.tsx # Custom renderItem for TreeView
Modified Files
ui/
├── components/providers/workflow/forms/connect-account-form.tsx # Add method selector for AWS
├── store/index.ts # Export org store
├── types/index.ts # Export org types
3. Types (ui/types/organizations.ts)
Const enums (following as const pattern)
export const DISCOVERY_STATUS = { PENDING: "pending", RUNNING: "running", SUCCEEDED: "succeeded", FAILED: "failed" } as const;
export const APPLY_STATUS = { READY: "ready", BLOCKED: "blocked" } as const;
export const ORG_RELATION = { ALREADY_LINKED: "already_linked", LINK_REQUIRED: "link_required", LINKED_TO_OTHER: "linked_to_other_organization" } as const;
export const SECRET_STATE = { ALREADY_EXISTS: "already_exists", WILL_CREATE: "will_create", MANUAL_REQUIRED: "manual_required" } as const;
export const ORG_WIZARD_STEP = { SETUP: 0, VALIDATE: 1, LAUNCH: 2 } as const;
// Note: Step 0 in modal = Step 2 in stepper (Step 1 is pre-completed)
Key interfaces (flat, one-level depth)
AccountRegistration—{ provider_exists, provider_id, organization_relation, apply_status, blocked_reasons[] }DiscoveredAccount—{ id, name, email, status, parent_id, registration }DiscoveredOu—{ id, name, arn, parent_id }DiscoveredRoot—{ id, arn, name, policy_types }DiscoveryResult—{ roots[], organizational_units[], accounts[] }OrganizationResource— JSON:API{ id, type: "organizations", attributes }DiscoveryResource— JSON:API{ id, type: "organization-discoveries", attributes }ApplyResultResource— JSON:API with counts + relationships (provider IDs)
4. Server Actions (ui/actions/organizations/organizations.ts)
All follow existing pattern: "use server", getAuthHeaders(), handleApiResponse().
| Action | Method | Endpoint | Notes |
|---|---|---|---|
createOrganization(formData) |
POST | /organizations |
Returns org ID |
createOrganizationSecret(formData) |
POST | /organization-secrets |
Includes org relationship |
triggerDiscovery(orgId) |
POST | /organizations/{id}/discover |
No body, returns discovery ID |
getDiscovery(orgId, discoveryId) |
GET | /organizations/{id}/discoveries/{did} |
Poll status |
applyDiscovery(formData) |
POST | /organizations/{id}/discoveries/{did}/apply |
Returns provider IDs + counts |
Reuse existing utilities:
getAuthHeaders()—ui/lib/helper.tshandleApiResponse()—ui/lib/server-actions-helper.tshandleApiError()—ui/lib/server-actions-helper.tscheckConnectionProvider()—ui/actions/providers/providers.ts(for testing each created provider)checkTaskStatus()—ui/lib/helper.ts(polling pattern for connection test tasks)scheduleDaily()/scanOnDemand()—ui/actions/scans/scans.ts(for Launch Scan step)
5. Adapter (ui/actions/organizations/organizations.adapter.ts)
buildOrgTreeData(result: DiscoveryResult): TreeDataItem[]
Transforms flat API arrays into hierarchical TreeDataItem[] for the TreeView component:
- Create root-level nodes from
result.roots[] - Build OU nodes, nest under parent root/OU via
parent_id - Build account leaf nodes, nest under parent OU/root via
parent_id - Set
disabled: trueon accounts whereapply_status === "blocked"
getSelectableAccountIds(result: DiscoveryResult): string[]
Returns IDs of accounts with apply_status === "ready" (pre-selects all selectable accounts).
buildAccountLookup(result: DiscoveryResult): Map<string, DiscoveredAccount>
Lookup map for org-account-tree-item.tsx to access registration data by account ID.
getOuIdsForSelectedAccounts(result: DiscoveryResult, selectedAccountIds: string[]): string[]
Given selected account IDs, returns the set of OU IDs needed (ancestor OUs for selected accounts).
6. Zustand Store (ui/store/organizations/store.ts)
interface OrgSetupState {
// Identity
organizationId: string | null;
organizationName: string | null;
organizationExternalId: string | null;
discoveryId: string | null;
// Discovery
discoveryResult: DiscoveryResult | null;
// Selection + aliases
selectedAccountIds: string[];
accountAliases: Record<string, string>; // accountId -> alias
// Apply result
createdProviderIds: string[];
// Connection test results
connectionResults: Record<string, "pending" | "success" | "error">; // providerId -> status
// Actions
setOrganization: (id: string, name: string, externalId: string) => void;
setDiscovery: (id: string, result: DiscoveryResult) => void;
setSelectedAccountIds: (ids: string[]) => void;
setAccountAlias: (accountId: string, alias: string) => void;
setCreatedProviderIds: (ids: string[]) => void;
setConnectionResult: (providerId: string, status: "pending" | "success" | "error") => void;
reset: () => void;
}
Uses persist middleware with sessionStorage. Pattern matches ui/store/ui/store.ts.
7. Component Details
7.1 org-wizard-modal.tsx — Modal Shell
A large Dialog (shadcn Dialog from @/components/shadcn/dialog) containing:
- Header: "Adding A Cloud Provider" + info link + X close button
- Two-column layout: Left =
OrgWizardStepper, Right = step content - Footer: Back/Next/action buttons (vary by step)
- Internal state:
currentStep(0=Setup, 1=Validate, 2=Launch) - On close (X or "Done"): calls
orgStore.reset(), closes dialog
Props: open: boolean, onOpenChange: (open: boolean) => void
7.2 org-wizard-stepper.tsx — 4-Step Stepper
Custom vertical stepper (shadcn-based, NOT HeroUI) with 4 steps:
- "Link a Cloud Provider" — always completed (green check)
- "Authenticate Credentials" — active/completed based on
currentStep - "Validate Connection" — active/completed/error based on state
- "Launch Scan" — active when reached
Icons: folder-git-2, key-round, rocket, Prowler logo (matching Figma) Step 3 shows error icon when connection tests have failures.
7.3 aws-method-selector.tsx — Method Selection
Renders inside connect-account-form.tsx when providerType === "aws".
Two radio-style cards:
- "Add A Single AWS Cloud Account" —
Boxicon, always enabled - "Add Multiple Accounts With AWS Organizations" —
Building2icon- SaaS (
NEXT_PUBLIC_IS_CLOUD_ENV === "true"): enabled - OSS: disabled + gradient "Get Prowler Cloud" CTA badge
- SaaS (
7.4 connect-account-form.tsx — Modification
Add AWS method selection as conditional within prevStep === 2:
prevStep === 1: RadioGroupProvider — unchanged
prevStep === 2 && providerType === "aws" && !awsMethod: AwsMethodSelector + OrgWizardModal
prevStep === 2 && providerType !== "aws": UID/Alias input — unchanged
prevStep === 2 && awsMethod === "single": UID/Alias input (existing)
New local state: awsMethod: "single" | null, isOrgModalOpen: boolean
When "Organizations" is selected → setIsOrgModalOpen(true) (opens the wizard modal)
When "Single Account" is selected → setAwsMethod("single") (shows UID input)
7.5 org-setup-form.tsx — Step 2: Organization Details
React Hook Form + Zod schema:
organizationName:z.string().min(3)awsOrgId:z.string().regex(/^o-[a-z0-9]{10,32}$/)roleArn:z.string().regex(/^arn:aws:iam::\d{12}:role\//)externalId:z.string().min(1)
Submit flow (sequential chain):
createOrganization()→ getorgIdcreateOrganizationSecret()withorgId→ getsecretIdtriggerDiscovery(orgId)→ getdiscoveryId- Store all in Zustand:
setOrganization(orgId, name, externalId), discoveryId - Advance to step 3 (Validate Connection)
Shows loading state during 3-call chain. If any fails, shows error on the relevant field.
7.6 org-discovery-loader.tsx — Step 3 Phase A: Discovery Polling
On mount, polls getDiscovery(orgId, discoveryId) every 3s, max 60 retries (3 min).
- Shows spinner + "Discovering your AWS Organization..."
- On
succeeded: stores result in Zustand, transitions to Phase B - On
failed: shows error message + "Retry" button
7.7 org-account-selection.tsx — Step 3 Phase B: Selection
Renders:
- Org info header (AWS badge + org name + UID badge)
TreeViewfrom@/components/shadcn/tree-viewwith:showCheckboxes={true},enableSelectChildren={true},expandAll={true}- Custom
renderItem→ shows account ID + "Name (optional)" input field
- Pre-selects all
readyaccounts - Blocked accounts shown as disabled with reason tooltip
Buttons: "Back" (go to step 2), "Test Connections" (starts in-place Apply + Test), "Skip Connection Validation" (advance to step 4)
7.8 In-place Apply + Test (inside org-account-selection.tsx)
On "Test Connections" click:
- Calls
applyDiscovery()with selected account IDs + aliases from Zustand - Stores returned
createdProviderIdsin Zustand - Automatically tests all connections: for each provider ID, calls
checkConnectionProvider()then pollscheckTaskStatus() - Updates TreeView status icons in real-time via
connectionResultsin store
TreeView display behavior:
- The same TreeView remains visible (no screen/step change).
- Selected account checkboxes switch to spinner/status icons while testing.
- Failed accounts can be retried via "Test Connections".
Error state (from Figma 10160:2965):
- Red error banner: "There was a problem connecting to some accounts. Ensure the Prowler StackSet has successfully deployed then retry testing connections."
- "Test Connections" button retries all failed connections
- "Skip Connection Validation" advances to step 4 anyway
Success: All connections pass → automatically advance to step 4
Buttons: "Back", "Skip Connection Validation" (secondary), "Test Connections" (primary)
7.9 org-launch-scan.tsx — Step 4: Launch Scan
From Figma (10004:102323):
- Org info header (AWS badge + org name + UID badge)
- Green check + "Accounts Connected!"
- "Your accounts are connected to Prowler and ready to Scan!"
- "Select a Prowler scan schedule for these accounts."
- Dropdown: "Scan Daily (every 24 hours)" (default selected)
"Done" button:
- Close modal, navigate to
/providers, revalidate - No scan triggered
"Launch Scan" button:
- For each provider ID in
createdProviderIds:- Call
scheduleDaily()(fromui/actions/scans/scans.ts)
- Call
- Close modal, navigate to
/providers - Show toast: "Scan launched for N accounts"
- Revalidate
/providersand/scans
7.10 org-account-tree-item.tsx — Custom Tree Renderer
Used in both selection and in-place testing modes via renderItem prop.
Phase B (selection mode):
- Shows: account ID text + "Name (optional)" input field
- Checkboxes handled by TreeView's built-in checkbox support
Testing mode (in-place):
- Shows: status icon (✓/✗/spinner) + account ID + "Name (optional)" input (read-only or editable)
- OUs show aggregate status (all children pass = ✓, any fail = ✗)
- Uses
item.status("success"/"error") anditem.isLoadingfrom TreeDataItem
Account data lookup via closure over Map<string, DiscoveredAccount> from adapter.
8. SaaS/OSS Gating
Single check in AwsMethodSelector:
const isCloudEnv = process.env.NEXT_PUBLIC_IS_CLOUD_ENV === "true";
- OSS: Organizations option disabled with gradient "Get Prowler Cloud" CTA
- SaaS: Organizations option fully functional
- No server-side gating needed
9. Implementation Order
| # | Task | Depends On |
|---|---|---|
| 1 | Types (types/organizations.ts) |
— |
| 2 | Server actions (actions/organizations/organizations.ts) |
Types |
| 3 | Adapter (actions/organizations/organizations.adapter.ts) |
Types |
| 4 | Zustand store (store/organizations/store.ts) |
Types |
| 5 | AwsMethodSelector component |
— |
| 6 | OrgWizardStepper component |
— |
| 7 | OrgSetupForm component |
Actions, Store |
| 8 | OrgDiscoveryLoader component |
Actions, Store |
| 9 | OrgAccountTreeItem component |
Adapter, Types |
| 10 | OrgAccountSelection component (selection + in-place test) |
TreeView, TreeItem, Actions, Store |
| 11 | OrgLaunchScan component |
Scan actions, Store |
| 12 | OrgWizardModal (shell + step router) |
All above components |
| 13 | Modify connect-account-form.tsx |
AwsMethodSelector, OrgWizardModal |
| 14 | Wire exports (store/index.ts, types/index.ts) |
All |
10. Verification
Local testing (UI states — no real API)
pnpm run dev→ verify no errorspnpm run typecheck→ all types compilepnpm run lint:fix→ no lint errors/providers/connect-account:- Select AWS → method selector appears
- OSS mode: Organizations option disabled + "Get Prowler Cloud" badge
- Click "Single Account" → existing UID form (unchanged)
- Click "Organizations" (SaaS) → modal opens
- Non-AWS providers → go straight to UID form (no method selector)
- Modal stepper: verify 4 steps render, step transitions work
- Org setup form: validation works (Zod errors on invalid inputs)
- Mock discovery states: pending spinner, succeeded tree, failed error
- TreeView: checkboxes toggle, blocked accounts disabled, names editable
- Close modal (X or Done) → state resets
Cloud DEV E2E testing (required before merge)
- Happy path: create org → secret → discover → select → apply → test connections → launch scan
- Partial failure: some connections fail → error banner → "Test Connections" retry → "Skip" works
- All pass: auto-advance to Launch Scan
- Discovery failure: shows error + "Retry" button
- Blocked accounts: cannot be selected in tree
- Launch Scan: modal closes →
/providers→ toast → providers visible in table - OSS gate: method selector shows disabled option with CTA
Key utilities to reuse
| Utility | Path | Used For |
|---|---|---|
getAuthHeaders() |
ui/lib/helper.ts |
All API calls |
handleApiResponse() |
ui/lib/server-actions-helper.ts |
Response parsing |
checkConnectionProvider() |
ui/actions/providers/providers.ts |
Testing each provider |
checkTaskStatus() |
ui/lib/helper.ts:257 |
Polling connection test tasks |
scheduleDaily() |
ui/actions/scans/scans.ts:141 |
Launch Scan step |
useFormServerErrors |
ui/hooks/use-form-server-errors.ts |
Map API errors to form fields |
TreeView |
ui/components/shadcn/tree-view/ |
Account hierarchy + selection |
Dialog |
ui/components/shadcn/dialog |
Modal container |
useUIStore pattern |
ui/store/ui/store.ts |
Zustand + persist reference |
addProvider pattern |
ui/actions/providers/providers.ts |
JSON:API server action reference |