feat(providers): make external id field mandatory in the aws role secret form (#6656)

This commit is contained in:
Pablo Lara
2025-01-22 12:45:31 +01:00
committed by GitHub
parent 374078683b
commit f658507847
14 changed files with 363 additions and 159 deletions
+1 -1
View File
@@ -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 {
@@ -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 (
<Suspense fallback={<SkeletonProviderWorkflow />}>
<SSRLaunchScan searchParams={searchParams} />
</Suspense>
);
}
async function SSRLaunchScan({
searchParams,
}: {
searchParams: { type: string; id: string };
}) {
const formData = new FormData();
formData.append("id", searchParams.id);
const providerData = await getProvider(formData);
return (
<LaunchScanForm searchParams={searchParams} providerData={providerData} />
);
@@ -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 (
<Suspense fallback={<p>Loading...</p>}>
<Suspense fallback={<SkeletonProviderWorkflow />}>
<SSRTestConnection searchParams={searchParams} />
</Suspense>
);
@@ -93,10 +93,8 @@ export const LaunchScanForm = ({
className="flex flex-col space-y-4"
>
<div className="text-left">
<div className="text-2xl font-bold leading-9 text-default-foreground">
Scan started
</div>
<div className="py-2 text-default-500">
<div className="mb-1 text-xl font-medium">Scan started</div>
<div className="py-2 text-small text-default-500">
The scan has just started. From now on, a new scan will be launched
every 24 hours, starting from this moment.
</div>
@@ -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<FormValues>({
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 (
<div className="flex flex-col items-center justify-center space-y-6 py-12">
<div className="relative">
<div className="h-24 w-24 animate-pulse rounded-full bg-primary/20" />
<div className="absolute inset-0 h-24 w-24 animate-spin rounded-full border-4 border-primary border-t-transparent" />
</div>
<div className="text-center">
<p className="text-xl font-medium text-primary">
Scan initiated successfully
</p>
<p className="mt-2 text-gray-500">
Redirecting to scans dashboard...
</p>
</div>
</div>
);
}
return (
<Form {...form}>
<form
@@ -198,13 +218,11 @@ export const TestConnectionForm = ({
className="flex flex-col space-y-4"
>
<div className="text-left">
<div className="text-2xl font-bold leading-9 text-default-foreground">
Test connection
<div className="mb-2 text-xl font-medium">
Check connection and launch scan
</div>
<p className="py-2 text-default-500">
Ensure all required credentials and configurations are completed
accurately. A successful connection will enable the option to
initiate a scan in the following step.
<p className="py-2 text-small text-default-500">
A successful connection will launch a daily scheduled scan.
</p>
</div>
@@ -224,12 +242,12 @@ export const TestConnectionForm = ({
/>
</div>
<div className="flex items-center">
<p className="text-danger">
<p className="text-small text-danger">
{connectionStatus.error || "Unknown error"}
</p>
</div>
</div>
<p className="text-md text-danger">
<p className="text-small text-danger">
It seems there was an issue with your credentials. Please review
your credentials and try again.
</p>
@@ -243,11 +261,11 @@ export const TestConnectionForm = ({
providerUID={providerData.data.attributes.uid}
/>
{!isResettingCredentials && !connectionStatus?.error && (
<p className="py-2 text-default-500">
{/* {!isResettingCredentials && !connectionStatus?.error && (
<p className="py-2 text-small text-default-500">
Test connection and launch scan
</p>
)}
)} */}
<input type="hidden" name="providerId" value={providerId} />
@@ -308,7 +326,7 @@ export const TestConnectionForm = ({
<span>
{isUpdated && connectionStatus?.connected
? "Go to providers"
: "Launch"}
: "Launch scan"}
</span>
)}
</CustomButton>
@@ -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<typeof formSchema>;
type FormSchemaType = z.infer<typeof formSchema> & {
credentials_type: "aws-sdk-default" | "access-secret-key";
};
const form = useForm<FormSchemaType>({
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 (
<Form {...form}>
<form
@@ -108,12 +148,18 @@ export const UpdateViaRoleForm = ({
<input type="hidden" name="providerId" value={providerId} />
<input type="hidden" name="providerType" value={providerType} />
{/* Conditional AWS Form */}
{providerType === "aws" && (
<AWSCredentialsRoleForm
control={form.control as unknown as Control<AWSCredentialsRole>}
setValue={
form.setValue as unknown as UseFormSetValue<AWSCredentialsRole>
}
externalId={externalId || ""}
/>
)}
{/* Action Buttons */}
<div className="flex w-full justify-end sm:space-x-6">
{searchParamsObj.get("via") === "role" && (
<CustomButton
@@ -132,7 +178,7 @@ export const UpdateViaRoleForm = ({
)}
<CustomButton
type="submit"
ariaLabel={"Save"}
ariaLabel="Save"
className="w-1/2"
variant="solid"
color="action"
@@ -14,7 +14,7 @@ export const AWScredentialsForm = ({
<div className="text-2xl font-bold leading-9 text-default-foreground">
Connect via Credentials
</div>
<div className="py-2 text-default-500">
<div className="py-2 text-small text-default-500">
Please provide the information for your AWS credentials.
</div>
</div>
@@ -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<typeof formSchema>;
type FormSchemaType = z.infer<typeof formSchema> & {
credentials_type: "aws-sdk-default" | "access-secret-key";
};
const form = useForm<FormSchemaType>({
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" && (
<AWSCredentialsRoleForm
control={form.control as unknown as Control<AWSCredentialsRole>}
setValue={
form.setValue as unknown as UseFormSetValue<AWSCredentialsRole>
}
externalId={externalId || ""}
/>
)}
@@ -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<AWSCredentialsRole>;
setValue: UseFormSetValue<AWSCredentialsRole>;
externalId: string;
}) => {
const credentialsType = useWatch({
control,
name: "credentials_type" as const,
defaultValue: "aws-sdk-default",
});
return (
<>
<div className="mb-4 text-left">
<div className="text-2xl font-bold leading-9 text-default-foreground">
Connect assuming IAM Role
</div>
<div className="py-2 text-default-500">
<div className="py-2 text-small text-default-500">
Please provide the information for your AWS credentials.
</div>
</div>
<span className="text-xs font-bold text-default-500">Authentication</span>
<Select
name="credentials_type"
label="Authentication Method"
placeholder="Select credentials type"
defaultSelectedKeys={["aws-sdk-default"]}
className="mb-4"
variant="bordered"
onSelectionChange={(keys) =>
setValue(
"credentials_type",
Array.from(keys)[0] as "aws-sdk-default" | "access-secret-key",
)
}
>
<SelectItem key="aws-sdk-default">AWS SDK default</SelectItem>
<SelectItem key="access-secret-key">Access & secret key</SelectItem>
</Select>
{credentialsType === "access-secret-key" && (
<>
<CustomInput
control={control}
name="aws_access_key_id"
type="password"
label="AWS Access Key ID"
labelPlacement="inside"
placeholder="Enter the AWS Access Key ID"
variant="bordered"
isRequired
isInvalid={!!control._formState.errors.aws_access_key_id}
/>
<CustomInput
control={control}
name="aws_secret_access_key"
type="password"
label="AWS Secret Access Key"
labelPlacement="inside"
placeholder="Enter the AWS Secret Access Key"
variant="bordered"
isRequired
isInvalid={!!control._formState.errors.aws_secret_access_key}
/>
<CustomInput
control={control}
name="aws_session_token"
type="password"
label="AWS Session Token (optional)"
labelPlacement="inside"
placeholder="Enter the AWS Session Token"
variant="bordered"
isRequired={false}
isInvalid={!!control._formState.errors.aws_session_token}
/>
</>
)}
<Spacer y={2} />
<span className="text-xs font-bold text-default-500">Assume Role</span>
<CustomInput
control={control}
name="role_arn"
@@ -30,40 +101,6 @@ export const AWSCredentialsRoleForm = ({
isRequired
isInvalid={!!control._formState.errors.role_arn}
/>
<span className="text-sm text-default-500">Optional fields</span>
<CustomInput
control={control}
name="aws_access_key_id"
type="password"
label="AWS Access Key ID"
labelPlacement="inside"
placeholder="Enter the AWS Access Key ID"
variant="bordered"
isRequired={false}
isInvalid={!!control._formState.errors.aws_access_key_id}
/>
<CustomInput
control={control}
name="aws_secret_access_key"
type="password"
label="AWS Secret Access Key"
labelPlacement="inside"
placeholder="Enter the AWS Secret Access Key"
variant="bordered"
isRequired={false}
isInvalid={!!control._formState.errors.aws_secret_access_key}
/>
<CustomInput
control={control}
name="aws_session_token"
type="password"
label="AWS Session Token"
labelPlacement="inside"
placeholder="Enter the AWS Session Token"
variant="bordered"
isRequired={false}
isInvalid={!!control._formState.errors.aws_session_token}
/>
<CustomInput
control={control}
name="external_id"
@@ -72,10 +109,12 @@ export const AWSCredentialsRoleForm = ({
labelPlacement="inside"
placeholder="Enter the External ID"
variant="bordered"
isRequired={false}
defaultValue={externalId}
isRequired
isInvalid={!!control._formState.errors.external_id}
/>
<span className="text-sm text-default-500">Optional fields</span>
<div className="mt-4 grid grid-cols-1 gap-4 md:grid-cols-2">
<CustomInput
control={control}
@@ -1,2 +1,3 @@
export * from "./skeleton-provider-workflow";
export * from "./vertical-steps";
export * from "./workflow-add-provider";
@@ -0,0 +1,32 @@
import { Card, CardBody, CardHeader, Skeleton } from "@nextui-org/react";
export const SkeletonProviderWorkflow = () => {
return (
<Card>
<CardHeader className="flex flex-col items-start space-y-2">
<Skeleton className="h-6 w-2/3 rounded-lg">
<div className="h-6 bg-default-200"></div>
</Skeleton>
<Skeleton className="h-4 w-1/2 rounded-lg">
<div className="h-4 bg-default-200"></div>
</Skeleton>
</CardHeader>
<CardBody className="flex flex-col items-start space-y-6">
<div className="flex space-x-4">
<Skeleton className="h-12 w-12 rounded-lg">
<div className="h-12 w-12 bg-default-200"></div>
</Skeleton>
<Skeleton className="h-12 w-12 rounded-lg">
<div className="h-12 w-12 bg-default-200"></div>
</Skeleton>
</div>
<Skeleton className="h-5 w-3/4 rounded-lg">
<div className="h-5 bg-default-200"></div>
</Skeleton>
<Skeleton className="h-12 w-40 self-end rounded-lg">
<div className="h-12 bg-default-200"></div>
</Skeleton>
</CardBody>
</Card>
);
};
@@ -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 = () => {
+1
View File
@@ -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 = {
+22 -11
View File
@@ -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(),