From f6585078472e29e52913ec6460c1e7d960dd104f Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Wed, 22 Jan 2025 12:45:31 +0100 Subject: [PATCH] feat(providers): make external id field mandatory in the aws role secret form (#6656) --- ui/actions/providers/providers.ts | 2 +- .../(set-up-provider)/launch-scan/page.tsx | 20 ++- .../test-connection/page.tsx | 3 +- .../workflow/forms/launch-scan-form.tsx | 6 +- .../workflow/forms/test-connection-form.tsx | 48 ++++-- .../workflow/forms/update-via-role-form.tsx | 142 ++++++++++++------ .../via-credentials/aws-credentials-form.tsx | 2 +- .../workflow/forms/via-role-form.tsx | 109 ++++++++++---- .../workflow/forms/via-role/aws-role-form.tsx | 113 +++++++++----- ui/components/providers/workflow/index.ts | 1 + .../workflow/skeleton-provider-workflow.tsx | 32 ++++ .../workflow/workflow-add-provider.tsx | 10 +- ui/types/components.ts | 1 + ui/types/formSchemas.ts | 33 ++-- 14 files changed, 363 insertions(+), 159 deletions(-) create mode 100644 ui/components/providers/workflow/skeleton-provider-workflow.tsx diff --git a/ui/actions/providers/providers.ts b/ui/actions/providers/providers.ts index 3b2ea126d3..03ac58ab15 100644 --- a/ui/actions/providers/providers.ts +++ b/ui/actions/providers/providers.ts @@ -175,13 +175,13 @@ export const addCredentialsProvider = async (formData: FormData) => { secretType = "role"; secret = { role_arn: formData.get("role_arn"), + external_id: formData.get("external_id"), aws_access_key_id: formData.get("aws_access_key_id") || undefined, aws_secret_access_key: formData.get("aws_secret_access_key") || undefined, aws_session_token: formData.get("aws_session_token") || undefined, session_duration: parseInt(formData.get("session_duration") as string, 10) || 3600, - external_id: formData.get("external_id") || undefined, role_session_name: formData.get("role_session_name") || undefined, }; } else { diff --git a/ui/app/(prowler)/providers/(set-up-provider)/launch-scan/page.tsx b/ui/app/(prowler)/providers/(set-up-provider)/launch-scan/page.tsx index 3e88a5ea46..cdfba93dbd 100644 --- a/ui/app/(prowler)/providers/(set-up-provider)/launch-scan/page.tsx +++ b/ui/app/(prowler)/providers/(set-up-provider)/launch-scan/page.tsx @@ -1,8 +1,9 @@ import { redirect } from "next/navigation"; -import React from "react"; +import React, { Suspense } from "react"; import { getProvider } from "@/actions/providers"; import { LaunchScanForm } from "@/components/providers/workflow/forms"; +import { SkeletonProviderWorkflow } from "@/components/providers/workflow/skeleton-provider-workflow"; interface Props { searchParams: { type: string; id: string }; @@ -26,6 +27,23 @@ export default async function LaunchScanPage({ searchParams }: Props) { redirect("/providers/connect-account"); } + return ( + }> + + + ); +} + +async function SSRLaunchScan({ + searchParams, +}: { + searchParams: { type: string; id: string }; +}) { + const formData = new FormData(); + formData.append("id", searchParams.id); + + const providerData = await getProvider(formData); + return ( ); diff --git a/ui/app/(prowler)/providers/(set-up-provider)/test-connection/page.tsx b/ui/app/(prowler)/providers/(set-up-provider)/test-connection/page.tsx index b3e3e29c44..b5274cf763 100644 --- a/ui/app/(prowler)/providers/(set-up-provider)/test-connection/page.tsx +++ b/ui/app/(prowler)/providers/(set-up-provider)/test-connection/page.tsx @@ -2,6 +2,7 @@ import { redirect } from "next/navigation"; import React, { Suspense } from "react"; import { getProvider } from "@/actions/providers"; +import { SkeletonProviderWorkflow } from "@/components/providers/workflow"; import { TestConnectionForm } from "@/components/providers/workflow/forms"; interface Props { @@ -16,7 +17,7 @@ export default async function TestConnectionPage({ searchParams }: Props) { } return ( - Loading...

}> + }> ); diff --git a/ui/components/providers/workflow/forms/launch-scan-form.tsx b/ui/components/providers/workflow/forms/launch-scan-form.tsx index 2532fd2e90..968fb8ffb9 100644 --- a/ui/components/providers/workflow/forms/launch-scan-form.tsx +++ b/ui/components/providers/workflow/forms/launch-scan-form.tsx @@ -93,10 +93,8 @@ export const LaunchScanForm = ({ className="flex flex-col space-y-4" >
-
- Scan started -
-
+
Scan started
+
The scan has just started. From now on, a new scan will be launched every 24 hours, starting from this moment.
diff --git a/ui/components/providers/workflow/forms/test-connection-form.tsx b/ui/components/providers/workflow/forms/test-connection-form.tsx index 41e5ebed66..778df1e478 100644 --- a/ui/components/providers/workflow/forms/test-connection-form.tsx +++ b/ui/components/providers/workflow/forms/test-connection-form.tsx @@ -66,6 +66,7 @@ export const TestConnectionForm = ({ error: string | null; } | null>(null); const [isResettingCredentials, setIsResettingCredentials] = useState(false); + const [isRedirecting, setIsRedirecting] = useState(false); const form = useForm({ resolver: zodResolver(formSchema), @@ -129,9 +130,9 @@ export const TestConnectionForm = ({ const isUpdated = urlParams.get("updated") === "true"; if (!isUpdated) { - router.push( - `/providers/launch-scan?type=${providerType}&id=${providerId}`, - ); + setIsRedirecting(true); + router.push("/scans"); + return; } else { setConnectionStatus({ connected: true, @@ -191,6 +192,25 @@ export const TestConnectionForm = ({ } }; + if (isRedirecting) { + return ( +
+
+
+
+
+
+

+ Scan initiated successfully +

+

+ Redirecting to scans dashboard... +

+
+
+ ); + } + return (
-
- Test connection +
+ Check connection and launch scan
-

- Ensure all required credentials and configurations are completed - accurately. A successful connection will enable the option to - initiate a scan in the following step. +

+ A successful connection will launch a daily scheduled scan.

@@ -224,12 +242,12 @@ export const TestConnectionForm = ({ />
-

+

{connectionStatus.error || "Unknown error"}

-

+

It seems there was an issue with your credentials. Please review your credentials and try again.

@@ -243,11 +261,11 @@ export const TestConnectionForm = ({ providerUID={providerData.data.attributes.uid} /> - {!isResettingCredentials && !connectionStatus?.error && ( -

+ {/* {!isResettingCredentials && !connectionStatus?.error && ( +

Test connection and launch scan

- )} + )} */} @@ -308,7 +326,7 @@ export const TestConnectionForm = ({ {isUpdated && connectionStatus?.connected ? "Go to providers" - : "Launch"} + : "Launch scan"} )} diff --git a/ui/components/providers/workflow/forms/update-via-role-form.tsx b/ui/components/providers/workflow/forms/update-via-role-form.tsx index ee57dc0759..85e00e3cb1 100644 --- a/ui/components/providers/workflow/forms/update-via-role-form.tsx +++ b/ui/components/providers/workflow/forms/update-via-role-form.tsx @@ -3,7 +3,8 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react"; import { useRouter, useSearchParams } from "next/navigation"; -import { Control, useForm } from "react-hook-form"; +import { useSession } from "next-auth/react"; +import { Control, useForm, UseFormSetValue } from "react-hook-form"; import * as z from "zod"; import { updateCredentialsProvider } from "@/actions/providers/providers"; @@ -25,80 +26,119 @@ export const UpdateViaRoleForm = ({ }) => { const router = useRouter(); const { toast } = useToast(); + const { data: session } = useSession(); const searchParamsObj = useSearchParams(); - // Handler for back button - const handleBackStep = () => { - const currentParams = new URLSearchParams(window.location.search); - currentParams.delete("via"); - router.push(`?${currentParams.toString()}`); - }; - + // Extract values from searchParams const providerType = searchParams.type; const providerId = searchParams.id; const providerSecretId = searchParams.secretId || ""; + const externalId = session?.tenantId; const formSchema = addCredentialsRoleFormSchema(providerType); - type FormSchemaType = z.infer; + type FormSchemaType = z.infer & { + credentials_type: "aws-sdk-default" | "access-secret-key"; + }; const form = useForm({ resolver: zodResolver(formSchema), defaultValues: { providerId, providerType, - ...(providerType === "aws" - ? { - role_arn: "", - aws_access_key_id: "", - aws_secret_access_key: "", - aws_session_token: "", - session_duration: 3600, - external_id: "", - role_session_name: "", - } - : {}), + credentials_type: "aws-sdk-default", + ...(providerType === "aws" && { + role_arn: "", + external_id: externalId, + aws_access_key_id: "", + aws_secret_access_key: "", + aws_session_token: "", + role_session_name: "", + session_duration: 3600, + }), }, }); const isLoading = form.formState.isSubmitting; + // Handle form submission const onSubmitClient = async (values: FormSchemaType) => { - const formData = new FormData(); + try { + const formData = new FormData(); - Object.entries(values).forEach( - ([key, value]) => - value !== undefined && formData.append(key, String(value)), - ); + Object.entries(values).forEach(([key, value]) => { + if (key === "credentials_type") return; - const data = await updateCredentialsProvider(providerSecretId, formData); + if ( + values.credentials_type === "access-secret-key" && + [ + "aws_access_key_id", + "aws_secret_access_key", + "aws_session_token", + ].includes(key) + ) { + if (value !== undefined && value !== "") { + formData.append(key, String(value)); + } + return; + } - if (data?.errors && data.errors.length > 0) { - data.errors.forEach((error: ApiError) => { - const errorMessage = error.detail; - switch (error.source.pointer) { - case "/data/attributes/secret/role_arn": - form.setError("role_arn" as keyof FormSchemaType, { - type: "server", - message: errorMessage, - }); - break; - - default: - toast({ - variant: "destructive", - title: "Oops! Something went wrong", - description: errorMessage, - }); + if (value !== undefined && value !== "") { + formData.append(key, String(value)); } }); - } else { - router.push( - `/providers/test-connection?type=${providerType}&id=${providerId}&updated=true`, - ); + + const data = await updateCredentialsProvider(providerSecretId, formData); + + // Handle errors + if (data?.errors?.length) { + data.errors.forEach((error: ApiError) => { + const errorMessage = error.detail; + switch (error.source.pointer) { + case "/data/attributes/secret/role_arn": + form.setError("role_arn" as keyof FormSchemaType, { + type: "server", + message: errorMessage, + }); + break; + case "/data/attributes/secret/external_id": + form.setError("external_id" as keyof FormSchemaType, { + type: "server", + message: errorMessage, + }); + break; + default: + toast({ + variant: "destructive", + title: "Oops! Something went wrong", + description: errorMessage, + }); + } + }); + } else { + // Redirect on success + router.push( + `/providers/test-connection?type=${providerType}&id=${providerId}&updated=true`, + ); + } + } catch (error) { + // eslint-disable-next-line no-console + console.error("Error during submission:", error); + toast({ + variant: "destructive", + title: "Submission failed", + description: "An error occurred while processing your request.", + }); } }; + // Handle back navigation + const handleBackStep = () => { + const currentParams = new URLSearchParams(window.location.search); + currentParams.delete("via"); + router.push(`?${currentParams.toString()}`); + }; + return ( + {/* Conditional AWS Form */} {providerType === "aws" && ( } + setValue={ + form.setValue as unknown as UseFormSetValue + } + externalId={externalId || ""} /> )} + {/* Action Buttons */}
{searchParamsObj.get("via") === "role" && ( Connect via Credentials
-
+
Please provide the information for your AWS credentials.
diff --git a/ui/components/providers/workflow/forms/via-role-form.tsx b/ui/components/providers/workflow/forms/via-role-form.tsx index 3a38cb1808..917f049c7c 100644 --- a/ui/components/providers/workflow/forms/via-role-form.tsx +++ b/ui/components/providers/workflow/forms/via-role-form.tsx @@ -3,7 +3,8 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react"; import { useRouter, useSearchParams } from "next/navigation"; -import { Control, useForm } from "react-hook-form"; +import { useSession } from "next-auth/react"; +import { Control, useForm, UseFormSetValue } from "react-hook-form"; import * as z from "zod"; import { addCredentialsProvider } from "@/actions/providers/providers"; @@ -25,8 +26,9 @@ export const ViaRoleForm = ({ }) => { const router = useRouter(); const { toast } = useToast(); - + const { data: session } = useSession(); const searchParamsObj = useSearchParams(); + const externalId = session?.tenantId; // Handler for back button const handleBackStep = () => { @@ -39,22 +41,25 @@ export const ViaRoleForm = ({ const providerId = searchParams.id; const formSchema = addCredentialsRoleFormSchema(providerType); - type FormSchemaType = z.infer; + type FormSchemaType = z.infer & { + credentials_type: "aws-sdk-default" | "access-secret-key"; + }; const form = useForm({ resolver: zodResolver(formSchema), defaultValues: { providerId, providerType, + credentials_type: "aws-sdk-default", ...(providerType === "aws" ? { role_arn: "", + external_id: externalId, aws_access_key_id: "", aws_secret_access_key: "", aws_session_token: "", - session_duration: 3600, - external_id: "", role_session_name: "", + session_duration: 3600, } : {}), }, @@ -65,36 +70,72 @@ export const ViaRoleForm = ({ const onSubmitClient = async (values: FormSchemaType) => { const formData = new FormData(); - Object.entries(values).forEach( - ([key, value]) => - value !== undefined && formData.append(key, String(value)), - ); + Object.entries(values).forEach(([key, value]) => { + // Do not include credentials_type + if (key === "credentials_type") return; - const data = await addCredentialsProvider(formData); - - if (data?.errors && data.errors.length > 0) { - data.errors.forEach((error: ApiError) => { - const errorMessage = error.detail; - switch (error.source.pointer) { - case "/data/attributes/secret/role_arn": - form.setError("role_arn" as keyof FormSchemaType, { - type: "server", - message: errorMessage, - }); - break; - - default: - toast({ - variant: "destructive", - title: "Oops! Something went wrong", - description: errorMessage, - }); + // If credentials_type is "access-secret-key", include the relevant fields + if ( + values.credentials_type === "access-secret-key" && + [ + "aws_access_key_id", + "aws_secret_access_key", + "aws_session_token", + ].includes(key) + ) { + if (value !== undefined && value !== "") { + formData.append(key, String(value)); } + return; + } + + // Add any other valid field + if (value !== undefined && value !== "") { + formData.append(key, String(value)); + } + }); + + try { + const data = await addCredentialsProvider(formData); + + if (data?.errors && data.errors.length > 0) { + data.errors.forEach((error: ApiError) => { + const errorMessage = error.detail; + + switch (error.source.pointer) { + case "/data/attributes/secret/role_arn": + form.setError("role_arn" as keyof FormSchemaType, { + type: "server", + message: errorMessage, + }); + break; + case "/data/attributes/secret/external_id": + form.setError("external_id" as keyof FormSchemaType, { + type: "server", + message: errorMessage, + }); + break; + default: + toast({ + variant: "destructive", + title: "Oops! Something went wrong", + description: errorMessage, + }); + } + }); + } else { + router.push( + `/providers/test-connection?type=${providerType}&id=${providerId}`, + ); + } + } catch (error) { + // eslint-disable-next-line no-console + console.error("Error during submission:", error); + toast({ + variant: "destructive", + title: "Submission failed", + description: "An error occurred while processing your request.", }); - } else { - router.push( - `/providers/test-connection?type=${providerType}&id=${providerId}`, - ); } }; @@ -110,6 +151,10 @@ export const ViaRoleForm = ({ {providerType === "aws" && ( } + setValue={ + form.setValue as unknown as UseFormSetValue + } + externalId={externalId || ""} /> )} diff --git a/ui/components/providers/workflow/forms/via-role/aws-role-form.tsx b/ui/components/providers/workflow/forms/via-role/aws-role-form.tsx index eb7f5d3a43..1240e20e07 100644 --- a/ui/components/providers/workflow/forms/via-role/aws-role-form.tsx +++ b/ui/components/providers/workflow/forms/via-role/aws-role-form.tsx @@ -1,24 +1,95 @@ -import { Control } from "react-hook-form"; +import { Select, SelectItem, Spacer } from "@nextui-org/react"; +import { Control, UseFormSetValue, useWatch } from "react-hook-form"; import { CustomInput } from "@/components/ui/custom"; import { AWSCredentialsRole } from "@/types"; export const AWSCredentialsRoleForm = ({ control, + setValue, + externalId, }: { control: Control; + setValue: UseFormSetValue; + externalId: string; }) => { + const credentialsType = useWatch({ + control, + name: "credentials_type" as const, + defaultValue: "aws-sdk-default", + }); + return ( <>
Connect assuming IAM Role
-
+
Please provide the information for your AWS credentials.
+ Authentication + + + + {credentialsType === "access-secret-key" && ( + <> + + + + + )} + + Assume Role + - Optional fields - - - + Optional fields
{ + return ( + + + +
+
+ +
+
+
+ +
+ +
+
+ +
+
+
+ +
+
+ +
+
+
+
+ ); +}; diff --git a/ui/components/providers/workflow/workflow-add-provider.tsx b/ui/components/providers/workflow/workflow-add-provider.tsx index 6bae1f2dde..d428d92a6c 100644 --- a/ui/components/providers/workflow/workflow-add-provider.tsx +++ b/ui/components/providers/workflow/workflow-add-provider.tsx @@ -19,17 +19,11 @@ const steps = [ href: "/providers/add-credentials", }, { - title: "Test connection", + title: "Check connection and launch scan", description: - "Test your connection to verify that the credentials provided are valid for accessing your cloud account.", + "Test your connection to verify that the credentials provided are valid for accessing your cloud account and launch a scan.", href: "/providers/test-connection", }, - { - title: "Success", - description: - "Your cloud account has been successfully connected and the scan has been launched.", - href: "/providers/launch-scan", - }, ]; export const WorkflowAddProvider = () => { diff --git a/ui/types/components.ts b/ui/types/components.ts index b74bf259b9..6818f3199c 100644 --- a/ui/types/components.ts +++ b/ui/types/components.ts @@ -182,6 +182,7 @@ export type AWSCredentialsRole = { external_id?: string; role_session_name?: string; session_duration?: number; + credentials_type?: "aws-sdk-default" | "access-secret-key"; }; export type AzureCredentials = { diff --git a/ui/types/formSchemas.ts b/ui/types/formSchemas.ts index 76ec266c8f..68319f4a73 100644 --- a/ui/types/formSchemas.ts +++ b/ui/types/formSchemas.ts @@ -133,17 +133,28 @@ export const addCredentialsFormSchema = (providerType: string) => export const addCredentialsRoleFormSchema = (providerType: string) => providerType === "aws" - ? z.object({ - providerId: z.string(), - providerType: z.string(), - role_arn: z.string().optional(), - aws_access_key_id: z.string().optional(), - aws_secret_access_key: z.string().optional(), - aws_session_token: z.string().optional(), - session_duration: z.number().optional(), - external_id: z.string().optional(), - role_session_name: z.string().optional(), - }) + ? z + .object({ + providerId: z.string(), + providerType: z.string(), + role_arn: z.string().optional(), + external_id: z.string().optional(), + aws_access_key_id: z.string().optional(), + aws_secret_access_key: z.string().optional(), + aws_session_token: z.string().optional(), + session_duration: z.number().optional(), + role_session_name: z.string().optional(), + credentials_type: z.string().optional(), + }) + .refine( + (data) => + data.credentials_type !== "access-secret-key" || + (data.aws_access_key_id && data.aws_secret_access_key), + { + message: "AWS Access Key ID and Secret Access Key are required.", + path: ["aws_access_key_id"], + }, + ) : z.object({ providerId: z.string(), providerType: z.string(),