feat: add select auth method form

This commit is contained in:
HugoPBrito
2025-07-21 10:34:46 +02:00
parent cf2738810a
commit 186851e264
12 changed files with 423 additions and 6 deletions
@@ -9,6 +9,7 @@ import {
AddViaServiceAccountForm,
SelectViaGCP,
} from "@/components/providers/workflow/forms/select-credentials-type/gcp";
import { SelectViaM365 } from "@/components/providers/workflow/forms/select-credentials-type/m365/select-via-m365";
import { ProviderType } from "@/types/providers";
interface Props {
@@ -26,9 +27,15 @@ export default function AddCredentialsPage({ searchParams }: Props) {
<SelectViaGCP initialVia={searchParams.via} />
)}
{searchParams.type === "m365" && !searchParams.via && (
<SelectViaM365 initialVia={searchParams.via} />
)}
{((searchParams.type === "aws" && searchParams.via === "credentials") ||
(searchParams.type === "gcp" && searchParams.via === "credentials") ||
(searchParams.type !== "aws" && searchParams.type !== "gcp")) && (
(searchParams.type === "m365" && searchParams.via === "credentials") ||
(searchParams.type === "m365" && searchParams.via === "service-principal-user") ||
(searchParams.type !== "aws" && searchParams.type !== "gcp" && searchParams.type !== "m365")) && (
<AddViaCredentialsForm searchParams={searchParams} />
)}
@@ -20,7 +20,7 @@ interface Props {
export default function UpdateCredentialsPage({ searchParams }: Props) {
return (
<>
{(searchParams.type === "aws" || searchParams.type === "gcp") &&
{(searchParams.type === "aws" || searchParams.type === "gcp" || searchParams.type === "m365") &&
!searchParams.via && (
<CredentialsUpdateInfo
providerType={searchParams.type}
@@ -30,7 +30,9 @@ export default function UpdateCredentialsPage({ searchParams }: Props) {
{((searchParams.type === "aws" && searchParams.via === "credentials") ||
(searchParams.type === "gcp" && searchParams.via === "credentials") ||
(searchParams.type !== "aws" && searchParams.type !== "gcp")) && (
(searchParams.type === "m365" && searchParams.via === "credentials") ||
(searchParams.type === "m365" && searchParams.via === "service-principal-user") ||
(searchParams.type !== "aws" && searchParams.type !== "gcp" && searchParams.type !== "m365")) && (
<UpdateViaCredentialsForm searchParams={searchParams} />
)}
@@ -2,6 +2,7 @@
import { SelectViaAWS } from "@/components/providers/workflow/forms/select-credentials-type/aws";
import { SelectViaGCP } from "@/components/providers/workflow/forms/select-credentials-type/gcp";
import { SelectViaM365 } from "@/components/providers/workflow/forms/select-credentials-type/m365/select-via-m365";
import { ProviderType } from "@/types/providers";
interface UpdateCredentialsInfoProps {
@@ -20,6 +21,9 @@ export const CredentialsUpdateInfo = ({
if (providerType === "gcp") {
return <SelectViaGCP initialVia={initialVia} />;
}
if (providerType === "m365") {
return <SelectViaM365 initialVia={initialVia} />;
}
return null;
};
@@ -26,7 +26,8 @@ import { GCPDefaultCredentialsForm } from "./select-credentials-type/gcp/credent
import { GCPServiceAccountKeyForm } from "./select-credentials-type/gcp/credentials-type/gcp-service-account-key-form";
import { AzureCredentialsForm } from "./via-credentials/azure-credentials-form";
import { KubernetesCredentialsForm } from "./via-credentials/k8s-credentials-form";
import { M365CredentialsForm } from "./via-credentials/m365-credentials-form";
import { M365ServicePrincipalForm } from "./select-credentials-type/m365/credentials-type";
import { M365ServicePrincipalUserForm } from "./select-credentials-type/m365/credentials-type/m365-service-principal-user-form";
type BaseCredentialsFormProps = {
providerType: ProviderType;
@@ -97,8 +98,13 @@ export const BaseCredentialsForm = ({
control={form.control as unknown as Control<AzureCredentials>}
/>
)}
{providerType === "m365" && (
<M365CredentialsForm
{providerType === "m365" && searchParamsObj.get("via") === "service-principal-user" && (
<M365ServicePrincipalUserForm
control={form.control as unknown as Control<M365Credentials>}
/>
)}
{providerType === "m365" && searchParamsObj.get("via") !== "service-principal-user" && (
<M365ServicePrincipalForm
control={form.control as unknown as Control<M365Credentials>}
/>
)}
@@ -0,0 +1,2 @@
export { M365ServicePrincipalForm } from "./m365-service-principal-form";
export { M365ServicePrincipalUserForm } from "./m365-service-principal-user-form";
@@ -0,0 +1,132 @@
import Link from "next/link";
import { Control, UseFormSetValue, useWatch } from "react-hook-form";
import { CustomInput } from "@/components/ui/custom";
import { ProviderCredentialFields } from "@/lib/provider-credentials/provider-credential-fields";
import { M365Credentials } from "@/types";
export const M365ServicePrincipalForm = ({
control,
setValue,
}: {
control: Control<M365Credentials>;
setValue: UseFormSetValue<M365Credentials>;
externalId: string;
}) => {
const credentialsType = useWatch({
control,
name: ProviderCredentialFields.CREDENTIALS_TYPE,
defaultValue: "m365-service-principal",
});
return (
<>
<div className="flex flex-col">
<div className="text-md font-bold leading-9 text-default-foreground">
Connect using Application/Service Principal
</div>
<div className="text-sm text-default-500">
Please provide the information for your Microsoft 365 Service Principal.
</div>
</div>
<span className="text-xs font-bold text-default-500">Authentication</span>
<Select
name={ProviderCredentialFields.CREDENTIALS_TYPE}
label="Authentication Method"
placeholder="Select credentials type"
defaultSelectedKeys={["m365-service-principal"]}
className="mb-4"
variant="bordered"
onSelectionChange={(keys) =>
setValue(
ProviderCredentialFields.CREDENTIALS_TYPE,
Array.from(keys)[0] as "m365-service-principal" | "m365-user-credentials",
)
}
>
<SelectItem key="m365-service-principal">Application/Service Principal</SelectItem>
<SelectItem key="m365-user-credentials">User credentials</SelectItem>
</Select>
{credentialsType === "m365-service-principal" && (
<>
<CustomInput
control={control}
name={ProviderCredentialFields.CLIENT_ID}
type="text"
label="Client ID"
labelPlacement="inside"
placeholder="Enter the Client ID"
variant="bordered"
isRequired
isInvalid={!!control._formState.errors[ProviderCredentialFields.CLIENT_ID]}
/>
<CustomInput
control={control}
name={ProviderCredentialFields.CLIENT_SECRET}
type="password"
label="Client Secret"
labelPlacement="inside"
placeholder="Enter the Client Secret"
variant="bordered"
isRequired
isInvalid={!!control._formState.errors[ProviderCredentialFields.CLIENT_SECRET]}
/>
<CustomInput
control={control}
name={ProviderCredentialFields.TENANT_ID}
type="text"
label="Tenant ID"
labelPlacement="inside"
placeholder="Enter the Tenant ID"
variant="bordered"
isRequired
isInvalid={!!control._formState.errors[ProviderCredentialFields.TENANT_ID]}
/>
</>
)}
{credentialsType === "m365-user-credentials" && (
<>
<CustomInput
control={control}
name={ProviderCredentialFields.CLIENT_ID}
type="text"
label="Client ID"
labelPlacement="inside"
placeholder="Enter the Client ID"
variant="bordered"
isRequired
isInvalid={!!control._formState.errors[ProviderCredentialFields.CLIENT_ID]}
/>
<CustomInput
control={control}
name={ProviderCredentialFields.CLIENT_SECRET}
type="password"
label="Client Secret"
labelPlacement="inside"
placeholder="Enter the Client Secret"
variant="bordered"
isRequired
isInvalid={!!control._formState.errors[ProviderCredentialFields.CLIENT_SECRET]}
/>
<CustomInput
control={control}
name={ProviderCredentialFields.TENANT_ID}
type="text"
label="Tenant ID"
labelPlacement="inside"
placeholder="Enter the Tenant ID"
variant="bordered"
isRequired
isInvalid={!!control._formState.errors[ProviderCredentialFields.TENANT_ID]}
/>
</>
)}
</>
);
};
@@ -0,0 +1,122 @@
import Link from "next/link";
import { Control } from "react-hook-form";
import { InfoIcon } from "@/components/icons";
import { CustomInput } from "@/components/ui/custom";
import { ProviderCredentialFields } from "@/lib/provider-credentials/provider-credential-fields";
import { M365Credentials } from "@/types";
export const M365ServicePrincipalUserForm = ({
control,
}: {
control: Control<M365Credentials>;
}) => {
return (
<>
<div className="flex flex-col">
<div className="text-md font-bold leading-9 text-default-foreground">
Service Principal + User Credentials
</div>
<div className="text-sm text-default-500">
Connect using Service Principal credentials combined with user authentication for PowerShell modules.
</div>
</div>
<div className="flex items-center rounded-lg border border-system-info bg-system-info-medium p-3 text-sm dark:text-default-300">
<InfoIcon className="mr-2 inline h-4 w-4 flex-shrink-0" />
<div className="text-xs">
<p className="font-extrabold mb-2">This method provides:</p>
<ul className="list-disc list-inside space-y-1">
<li>Service Principal authentication for Microsoft Graph APIs</li>
<li>User credentials for PowerShell modules access</li>
<li>Complete M365 service coverage</li>
</ul>
</div>
</div>
<span className="text-xs font-bold text-default-500">Service Principal Information</span>
<CustomInput
control={control}
name={ProviderCredentialFields.CLIENT_ID}
type="text"
label="Client ID"
labelPlacement="inside"
placeholder="Enter the Client ID"
variant="bordered"
isRequired
isInvalid={!!control._formState.errors[ProviderCredentialFields.CLIENT_ID]}
/>
<CustomInput
control={control}
name={ProviderCredentialFields.CLIENT_SECRET}
type="password"
label="Client Secret"
labelPlacement="inside"
placeholder="Enter the Client Secret"
variant="bordered"
isRequired
isInvalid={!!control._formState.errors[ProviderCredentialFields.CLIENT_SECRET]}
/>
<CustomInput
control={control}
name={ProviderCredentialFields.TENANT_ID}
type="text"
label="Tenant ID"
labelPlacement="inside"
placeholder="Enter the Tenant ID"
variant="bordered"
isRequired
isInvalid={!!control._formState.errors[ProviderCredentialFields.TENANT_ID]}
/>
<span className="text-xs font-bold text-default-500">User Credentials for PowerShell</span>
<CustomInput
control={control}
name={ProviderCredentialFields.USER}
type="text"
label="User"
labelPlacement="inside"
placeholder="Enter the User (e.g., user@company.onmicrosoft.com)"
variant="bordered"
isRequired
isInvalid={!!control._formState.errors[ProviderCredentialFields.USER]}
/>
<CustomInput
control={control}
name={ProviderCredentialFields.PASSWORD}
type="password"
label="Password"
labelPlacement="inside"
placeholder="Enter the Password"
variant="bordered"
isRequired
isInvalid={!!control._formState.errors[ProviderCredentialFields.PASSWORD]}
/>
<div className="flex items-center rounded-lg border border-system-warning bg-system-warning-medium p-2 text-sm dark:text-default-300">
<InfoIcon className="mr-2 inline h-4 w-4 flex-shrink-0" />
<p className="text-xs font-extrabold">
By September 2025, User Authentication will be deprecated.
</p>
</div>
<p className="text-sm text-default-500">
For more information on setting up authentication, see the{" "}
<Link
href="https://docs.prowler.com/projects/prowler-open-source/en/latest/tutorials/microsoft365/getting-started-m365/"
target="_blank"
rel="noopener noreferrer"
className="text-sm text-primary hover:underline"
>
M365 setup documentation
</Link>
.
</p>
</>
);
};
@@ -0,0 +1,3 @@
export { SelectViaM365 } from "./select-via-m365";
export { RadioGroupM365ViaCredentialsTypeForm } from "./radio-group-m365-via-credentials-type-form";
export * from "./credentials-type";
@@ -0,0 +1,70 @@
import { RadioGroup } from "@nextui-org/react";
import { Control, Controller } from "react-hook-form";
import { CustomRadio } from "@/components/ui/custom";
import { FormMessage } from "@/components/ui/form";
interface RadioGroupM365ViaCredentialsFormProps {
control: Control<any>;
isInvalid?: boolean;
errorMessage?: string;
onChange?: (value: string) => void;
}
export const RadioGroupM365ViaCredentialsTypeForm = ({
control,
isInvalid,
errorMessage,
onChange,
}: RadioGroupM365ViaCredentialsFormProps) => {
return (
<Controller
name="m365CredentialsType"
control={control}
render={({ field }) => (
<>
<RadioGroup
className="flex flex-wrap"
isInvalid={isInvalid}
{...field}
value={field.value || ""}
onValueChange={(value) => {
field.onChange(value);
if (onChange) {
onChange(value);
}
}}
>
<div className="flex flex-col gap-4">
<span className="text-sm text-default-500">Application Authentication</span>
<CustomRadio
description="Connect using Service Principal credentials only"
value="credentials"
>
<div className="flex items-center">
<span className="ml-2">Service Principal</span>
</div>
</CustomRadio>
<span className="text-sm text-default-500">
Application + User Authentication
</span>
<CustomRadio
description="Connect using Service Principal + User credentials"
value="service-principal-user"
>
<div className="flex items-center">
<span className="ml-2">Service Principal + User Credentials</span>
</div>
</CustomRadio>
</div>
</RadioGroup>
{errorMessage && (
<FormMessage className="text-system-error dark:text-system-error">
{errorMessage}
</FormMessage>
)}
</>
)}
/>
);
};
@@ -0,0 +1,38 @@
"use client";
import { useRouter } from "next/navigation";
import { useForm } from "react-hook-form";
import { Form } from "@/components/ui/form";
import { RadioGroupM365ViaCredentialsTypeForm } from "./radio-group-m365-via-credentials-type-form";
interface SelectViaM365Props {
initialVia?: string;
}
export const SelectViaM365 = ({ initialVia }: SelectViaM365Props) => {
const router = useRouter();
const form = useForm({
defaultValues: {
m365CredentialsType: initialVia || "",
},
});
const handleSelectionChange = (value: string) => {
const url = new URL(window.location.href);
url.searchParams.set("via", value);
router.push(url.toString());
};
return (
<Form {...form}>
<RadioGroupM365ViaCredentialsTypeForm
control={form.control}
isInvalid={!!form.formState.errors.m365CredentialsType}
errorMessage={form.formState.errors.m365CredentialsType?.message}
onChange={handleSelectionChange}
/>
</Form>
);
};
+4
View File
@@ -11,6 +11,7 @@ import {
addCredentialsFormSchema,
addCredentialsRoleFormSchema,
addCredentialsServiceAccountFormSchema,
addCredentialsM365UserFormSchema,
ProviderType,
} from "@/types";
@@ -46,6 +47,9 @@ export const useCredentialsForm = ({
if (providerType === "gcp" && via === "service-account") {
return addCredentialsServiceAccountFormSchema(providerType);
}
if (providerType === "m365" && via === "service-principal-user") {
return addCredentialsM365UserFormSchema();
}
return addCredentialsFormSchema(providerType);
};
+27
View File
@@ -66,6 +66,12 @@ export const awsCredentialsTypeSchema = z.object({
}),
});
export const m365CredentialsTypeSchema = z.object({
m365CredentialsType: z.string().min(1, {
message: "Please select the type of credentials you want to use",
}),
});
export const addProviderFormSchema = z
.object({
providerType: z.enum(["aws", "azure", "gcp", "kubernetes", "m365"], {
@@ -254,6 +260,27 @@ export const addCredentialsServiceAccountFormSchema = (
[ProviderCredentialFields.PROVIDER_TYPE]: z.string(),
});
export const addCredentialsM365UserFormSchema = () =>
z.object({
[ProviderCredentialFields.PROVIDER_ID]: z.string(),
[ProviderCredentialFields.PROVIDER_TYPE]: z.string(),
[ProviderCredentialFields.CLIENT_ID]: z
.string()
.nonempty("Client ID is required"),
[ProviderCredentialFields.CLIENT_SECRET]: z
.string()
.nonempty("Client Secret is required"),
[ProviderCredentialFields.TENANT_ID]: z
.string()
.nonempty("Tenant ID is required"),
[ProviderCredentialFields.USER]: z
.string()
.nonempty("User is required for this authentication method"),
[ProviderCredentialFields.PASSWORD]: z
.string()
.nonempty("Password is required for this authentication method"),
});
export const testConnectionFormSchema = z.object({
[ProviderCredentialFields.PROVIDER_ID]: z.string(),
runOnce: z.boolean().default(false),