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

This commit is contained in:
Pablo Lara authored and GitHub committed 2025-01-22 12:45:31 +01:00
1 parent 374078683b
commit f658507847
14 files changed
+363 -159

No files matched your search

@@ -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}