Skip to content

Commit 23a8d4e

Browse files
authored
feat(ui): improve organizations onboarding (#10274)
1 parent 809142d commit 23a8d4e

File tree

3 files changed

+132
-50
lines changed

3 files changed

+132
-50
lines changed

ui/CHANGELOG.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,13 @@ All notable changes to the **Prowler UI** are documented in this file.
44

55
## [1.20.0] (Prowler v5.20.0 UNRELEASED)
66

7-
### 🐞 Changed
7+
### 🔄 Changed
88

99
- Attack Paths: Improved error handling for server errors (5xx) and network failures with user-friendly messages instead of raw internal errors and layout changes. [(#10249)](https://github.com/prowler-cloud/prowler/pull/10249)
1010
- Refactor simple providers with new components and styles.[(#10259)](https://github.com/prowler-cloud/prowler/pull/10259)
11+
- AWS Organizations onboarding now uses a clearer 3-step flow: deploy the ProwlerScan role in the management account via CloudFormation Stack, deploy to member accounts via StackSet with a copyable template URL, and confirm with the Role ARN [(#10274)](https://github.com/prowler-cloud/prowler/pull/10274)
12+
13+
---
1114

1215
## [1.19.1] (Prowler v5.19.1 UNRELEASED)
1316

ui/components/providers/organizations/org-setup-form.tsx

Lines changed: 101 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"use client";
22

3+
import { useClipboard } from "@heroui/use-clipboard";
34
import { zodResolver } from "@hookform/resolvers/zod";
45
import { Check, Copy, ExternalLink } from "lucide-react";
56
import { useSession } from "next-auth/react";
@@ -18,7 +19,11 @@ import { Button } from "@/components/shadcn/button/button";
1819
import { Checkbox } from "@/components/shadcn/checkbox/checkbox";
1920
import { TreeSpinner } from "@/components/shadcn/tree-view/tree-spinner";
2021
import { Form } from "@/components/ui/form";
21-
import { getAWSCredentialsTemplateLinks } from "@/lib";
22+
import {
23+
getAWSCredentialsTemplateLinks,
24+
PROWLER_CF_TEMPLATE_URL,
25+
STACKSET_CONSOLE_URL,
26+
} from "@/lib";
2227
import { ORG_SETUP_PHASE, OrgSetupPhase } from "@/types/organizations";
2328

2429
import { useOrgSetupSubmission } from "./hooks/use-org-setup-submission";
@@ -39,7 +44,7 @@ const orgSetupSchema = z.object({
3944
.min(1, "Role ARN is required")
4045
.regex(
4146
/^arn:aws:iam::\d{12}:role\//,
42-
"Must be a valid IAM Role ARN (e.g., arn:aws:iam::123456789012:role/ProwlerOrgRole)",
47+
"Must be a valid IAM Role ARN (e.g., arn:aws:iam::123456789012:role/ProwlerScan)",
4348
),
4449
stackSetDeployed: z.boolean().refine((value) => value, {
4550
message: "You must confirm the StackSet deployment before continuing.",
@@ -64,8 +69,13 @@ export function OrgSetupForm({
6469
initialPhase = ORG_SETUP_PHASE.DETAILS,
6570
}: OrgSetupFormProps) {
6671
const { data: session } = useSession();
67-
const [isExternalIdCopied, setIsExternalIdCopied] = useState(false);
6872
const stackSetExternalId = session?.tenantId ?? "";
73+
const { copied: isExternalIdCopied, copy: copyExternalId } = useClipboard({
74+
timeout: 1500,
75+
});
76+
const { copied: isTemplateUrlCopied, copy: copyTemplateUrl } = useClipboard({
77+
timeout: 1500,
78+
});
6979
const [setupPhase, setSetupPhase] = useState<OrgSetupPhase>(initialPhase);
7080
const formId = "org-wizard-setup-form";
7181

@@ -90,9 +100,10 @@ export function OrgSetupForm({
90100

91101
const awsOrgId = watch("awsOrgId") || "";
92102
const isOrgIdValid = /^o-[a-z0-9]{10,32}$/.test(awsOrgId.trim());
93-
const stackSetQuickLink =
94-
stackSetExternalId &&
95-
getAWSCredentialsTemplateLinks(stackSetExternalId).cloudformationQuickLink;
103+
const templateLinks = stackSetExternalId
104+
? getAWSCredentialsTemplateLinks(stackSetExternalId)
105+
: null;
106+
const orgQuickLink = templateLinks?.cloudformationOrgQuickLink;
96107

97108
const { apiError, setApiError, submitOrganizationSetup } =
98109
useOrgSetupSubmission({
@@ -260,35 +271,11 @@ export function OrgSetupForm({
260271

261272
{setupPhase === ORG_SETUP_PHASE.ACCESS && !isSubmitting && (
262273
<div className="flex flex-col gap-8">
274+
{/* External ID - shown first for both deployment steps */}
263275
<div className="flex flex-col gap-4">
264276
<p className="text-text-neutral-primary text-sm leading-7 font-normal">
265-
1) Launch the Prowler CloudFormation StackSet in your AWS
266-
Console.
267-
</p>
268-
<Button
269-
variant="outline"
270-
size="lg"
271-
className="border-border-input-primary bg-bg-input-primary text-button-tertiary hover:bg-bg-input-primary active:bg-bg-input-primary h-12 w-full justify-start"
272-
disabled={!stackSetQuickLink}
273-
asChild
274-
>
275-
<a
276-
href={stackSetQuickLink || "#"}
277-
target="_blank"
278-
rel="noopener noreferrer"
279-
>
280-
<ExternalLink className="size-5" />
281-
<span>
282-
Prowler CloudFormation StackSet for AWS Organizations
283-
</span>
284-
</a>
285-
</Button>
286-
</div>
287-
288-
<div className="flex flex-col gap-4">
289-
<p className="text-text-neutral-primary text-sm leading-7 font-normal">
290-
2) Use the following Prowler External ID parameter in the
291-
StackSet.
277+
Use the following <strong>External ID</strong> when deploying
278+
the CloudFormation Stack and StackSet.
292279
</p>
293280
<div className="flex items-center gap-3">
294281
<span className="text-text-neutral-tertiary text-xs">
@@ -302,15 +289,7 @@ export function OrgSetupForm({
302289
<button
303290
type="button"
304291
disabled={!stackSetExternalId}
305-
onClick={async () => {
306-
try {
307-
await navigator.clipboard.writeText(stackSetExternalId);
308-
setIsExternalIdCopied(true);
309-
setTimeout(() => setIsExternalIdCopied(false), 1500);
310-
} catch {
311-
// Ignore clipboard errors (e.g., unsupported browser context).
312-
}
313-
}}
292+
onClick={() => copyExternalId(stackSetExternalId)}
314293
className="text-text-neutral-secondary hover:text-text-neutral-primary shrink-0 transition-colors"
315294
aria-label="Copy external ID"
316295
>
@@ -324,20 +303,93 @@ export function OrgSetupForm({
324303
</div>
325304
</div>
326305

306+
{/* Step 1: Management account - CloudFormation Stack */}
307+
<div className="flex flex-col gap-4">
308+
<p className="text-text-neutral-primary text-sm leading-7 font-normal">
309+
1) Deploy the ProwlerScan role in your{" "}
310+
<strong>management account</strong> using a CloudFormation
311+
Stack.
312+
</p>
313+
<Button
314+
variant="outline"
315+
size="lg"
316+
className="border-border-input-primary bg-bg-input-primary text-button-tertiary hover:bg-bg-input-primary active:bg-bg-input-primary h-12 w-full justify-start"
317+
disabled={!orgQuickLink}
318+
asChild
319+
>
320+
<a
321+
href={orgQuickLink || "#"}
322+
target="_blank"
323+
rel="noopener noreferrer"
324+
>
325+
<ExternalLink className="size-5" />
326+
<span>Create Stack in Management Account</span>
327+
</a>
328+
</Button>
329+
</div>
330+
331+
{/* Step 2: Member accounts - CloudFormation StackSet */}
332+
<div className="flex flex-col gap-4">
333+
<p className="text-text-neutral-primary text-sm leading-7 font-normal">
334+
2) Deploy the ProwlerScan role to{" "}
335+
<strong>member accounts</strong> using a CloudFormation
336+
StackSet.
337+
</p>
338+
<p className="text-text-neutral-tertiary text-xs leading-5">
339+
Open the StackSets console, select{" "}
340+
<strong>Service-managed permissions</strong>, and paste the
341+
template URL below. Set the <strong>ExternalId</strong>{" "}
342+
parameter to the value shown above.
343+
</p>
344+
<div className="bg-bg-neutral-tertiary border-border-input-primary flex items-center gap-3 rounded-lg border px-4 py-2.5">
345+
<span className="text-text-neutral-primary min-w-0 flex-1 truncate font-mono text-xs">
346+
{PROWLER_CF_TEMPLATE_URL}
347+
</span>
348+
<button
349+
type="button"
350+
onClick={() => copyTemplateUrl(PROWLER_CF_TEMPLATE_URL)}
351+
className="text-text-neutral-secondary hover:text-text-neutral-primary shrink-0 transition-colors"
352+
aria-label="Copy template URL"
353+
>
354+
{isTemplateUrlCopied ? (
355+
<Check className="size-4" />
356+
) : (
357+
<Copy className="size-4" />
358+
)}
359+
</button>
360+
</div>
361+
<Button
362+
variant="outline"
363+
size="lg"
364+
className="border-border-input-primary bg-bg-input-primary text-button-tertiary hover:bg-bg-input-primary active:bg-bg-input-primary h-12 w-full justify-start"
365+
disabled={!isExternalIdCopied}
366+
asChild
367+
>
368+
<a
369+
href={STACKSET_CONSOLE_URL}
370+
target="_blank"
371+
rel="noopener noreferrer"
372+
>
373+
<ExternalLink className="size-5" />
374+
<span>Open StackSets Console</span>
375+
</a>
376+
</Button>
377+
</div>
378+
379+
{/* Step 3: Role ARN + confirm */}
327380
<div className="flex flex-col gap-4">
328381
<p className="text-text-neutral-primary text-sm leading-7 font-normal">
329-
3) Copy the Prowler IAM Role ARN from AWS and confirm the
330-
StackSet is successfully deployed by clicking the checkbox
331-
below.
382+
3) Paste the management account Role ARN and confirm both
383+
deployments are complete.
332384
</p>
333385
</div>
334386

335387
<WizardInputField
336388
control={control}
337389
name="roleArn"
338-
label="Role ARN"
390+
label="Management Account Role ARN"
339391
labelPlacement="outside"
340-
placeholder="e.g. arn:aws:iam::123456789012:role/ProwlerOrgRole"
392+
placeholder="e.g. arn:aws:iam::123456789012:role/ProwlerScan"
341393
isRequired={false}
342394
requiredIndicator
343395
/>
@@ -365,7 +417,8 @@ export function OrgSetupForm({
365417
htmlFor="stackSetDeployed"
366418
className="text-text-neutral-tertiary text-xs leading-5 font-normal"
367419
>
368-
The StackSet has been successfully deployed in AWS
420+
The Stack and StackSet have been successfully deployed in
421+
AWS
369422
<span className="text-text-error-primary">*</span>
370423
</label>
371424
</>

ui/lib/external-urls.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,18 @@ export const DOCS_URLS = {
88
"https://docs.prowler.com/user-guide/tutorials/prowler-cloud-aws-organizations",
99
} as const;
1010

11+
// CloudFormation template URL for the ProwlerScan role.
12+
// Also used (URL-encoded) as the templateURL param in cloudformationQuickLink
13+
// and cloudformationOrgQuickLink below — keep both in sync.
14+
export const PROWLER_CF_TEMPLATE_URL =
15+
"https://prowler-cloud-public.s3.eu-west-1.amazonaws.com/permissions/templates/aws/cloudformation/prowler-scan-role.yml";
16+
17+
// AWS Console URL for creating a new StackSet.
18+
// Hardcoded to us-east-1 — StackSets are typically managed from this region.
19+
// Users in AWS GovCloud or China partitions would need different URLs.
20+
export const STACKSET_CONSOLE_URL =
21+
"https://us-east-1.console.aws.amazon.com/cloudformation/home?region=us-east-1#/stacksets/create";
22+
1123
export const getProviderHelpText = (provider: string) => {
1224
switch (provider) {
1325
case "aws":
@@ -86,6 +98,7 @@ export const getAWSCredentialsTemplateLinks = (
8698
cloudformation: string;
8799
terraform: string;
88100
cloudformationQuickLink: string;
101+
cloudformationOrgQuickLink: string;
89102
} => {
90103
let links = {};
91104

@@ -107,11 +120,24 @@ export const getAWSCredentialsTemplateLinks = (
107120
};
108121
}
109122

123+
const encodedTemplateUrl = encodeURIComponent(PROWLER_CF_TEMPLATE_URL);
124+
const cfBaseUrl =
125+
"https://us-east-1.console.aws.amazon.com/cloudformation/home?region=us-east-1#/stacks/quickcreate";
126+
const s3Params = bucketName
127+
? `&param_EnableS3Integration=true&param_S3IntegrationBucketName=${bucketName}`
128+
: "";
129+
110130
return {
111131
...(links as {
112132
cloudformation: string;
113133
terraform: string;
114134
}),
115-
cloudformationQuickLink: `https://us-east-1.console.aws.amazon.com/cloudformation/home?region=us-east-1#/stacks/quickcreate?templateURL=https%3A%2F%2Fprowler-cloud-public.s3.eu-west-1.amazonaws.com%2Fpermissions%2Ftemplates%2Faws%2Fcloudformation%2Fprowler-scan-role.yml&stackName=Prowler&param_ExternalId=${externalId}${bucketName ? `&param_EnableS3Integration=true&param_S3IntegrationBucketName=${bucketName}` : ""}`,
135+
cloudformationQuickLink:
136+
`${cfBaseUrl}?templateURL=${encodedTemplateUrl}` +
137+
`&stackName=Prowler&param_ExternalId=${externalId}${s3Params}`,
138+
cloudformationOrgQuickLink:
139+
`${cfBaseUrl}?templateURL=${encodedTemplateUrl}` +
140+
`&stackName=Prowler&param_ExternalId=${externalId}` +
141+
`&param_EnableOrganizations=true${s3Params}`,
116142
};
117143
};

0 commit comments

Comments
 (0)