feat(m365): enhance M365 credentials handling with new forms and selection options

This commit is contained in:
alejandrobailo
2025-10-16 17:25:54 +02:00
parent 6ea9d006c4
commit 461be92fb0
12 changed files with 307 additions and 14 deletions
@@ -3,6 +3,7 @@
import { SelectViaAWS } from "@/components/providers/workflow/forms/select-credentials-type/aws";
import { SelectViaGCP } from "@/components/providers/workflow/forms/select-credentials-type/gcp";
import { SelectViaGitHub } from "@/components/providers/workflow/forms/select-credentials-type/github";
import { SelectViaM365 } from "@/components/providers/workflow/forms/select-credentials-type/m365";
import { ProviderType } from "@/types/providers";
interface UpdateCredentialsInfoProps {
@@ -24,6 +25,9 @@ export const CredentialsUpdateInfo = ({
if (providerType === "github") {
return <SelectViaGitHub initialVia={initialVia} />;
}
if (providerType === "m365") {
return <SelectViaM365 initialVia={initialVia} />;
}
return null;
};
@@ -17,7 +17,8 @@ import {
GCPDefaultCredentials,
GCPServiceAccountKey,
KubernetesCredentials,
M365Credentials,
M365CertificateCredentials,
M365ClientSecretCredentials,
ProviderType,
} from "@/types";
@@ -26,10 +27,13 @@ import { AWSStaticCredentialsForm } from "./select-credentials-type/aws/credenti
import { AWSRoleCredentialsForm } from "./select-credentials-type/aws/credentials-type/aws-role-credentials-form";
import { GCPDefaultCredentialsForm } from "./select-credentials-type/gcp/credentials-type";
import { GCPServiceAccountKeyForm } from "./select-credentials-type/gcp/credentials-type/gcp-service-account-key-form";
import {
M365CertificateCredentialsForm,
M365ClientSecretCredentialsForm,
} from "./select-credentials-type/m365";
import { AzureCredentialsForm } from "./via-credentials/azure-credentials-form";
import { GitHubCredentialsForm } from "./via-credentials/github-credentials-form";
import { KubernetesCredentialsForm } from "./via-credentials/k8s-credentials-form";
import { M365CredentialsForm } from "./via-credentials/m365-credentials-form";
type BaseCredentialsFormProps = {
providerType: ProviderType;
@@ -103,11 +107,22 @@ export const BaseCredentialsForm = ({
control={form.control as unknown as Control<AzureCredentials>}
/>
)}
{providerType === "m365" && (
<M365CredentialsForm
control={form.control as unknown as Control<M365Credentials>}
/>
)}
{providerType === "m365" &&
searchParamsObj.get("via") === "app_client_secret" && (
<M365ClientSecretCredentialsForm
control={
form.control as unknown as Control<M365ClientSecretCredentials>
}
/>
)}
{providerType === "m365" &&
searchParamsObj.get("via") === "app_certificate" && (
<M365CertificateCredentialsForm
control={
form.control as unknown as Control<M365CertificateCredentials>
}
/>
)}
{providerType === "gcp" &&
searchParamsObj.get("via") === "service-account" && (
<GCPServiceAccountKeyForm
@@ -159,6 +174,15 @@ export const BaseCredentialsForm = ({
size="lg"
isLoading={isLoading}
endContent={!isLoading && <ChevronRightIcon size={24} />}
onPress={(e) => {
const formElement = e.target as HTMLElement;
const form = formElement.closest("form");
if (form) {
form.dispatchEvent(
new Event("submit", { bubbles: true, cancelable: true }),
);
}
}}
>
{isLoading ? <>Loading</> : <span>{submitButtonText}</span>}
</CustomButton>
@@ -0,0 +1,2 @@
export { M365CertificateCredentialsForm } from "./m365-certificate-credentials-form";
export { M365ClientSecretCredentialsForm } from "./m365-client-secret-credentials-form";
@@ -0,0 +1,72 @@
"use client";
import { Control } from "react-hook-form";
import { CustomInput, CustomTextarea } from "@/components/ui/custom";
import { CustomLink } from "@/components/ui/custom/custom-link";
import { M365CertificateCredentials } from "@/types";
export const M365CertificateCredentialsForm = ({
control,
}: {
control: Control<M365CertificateCredentials>;
}) => {
return (
<>
<div className="flex flex-col">
<div className="text-md text-default-foreground leading-9 font-bold">
App Certificate Credentials
</div>
<div className="text-default-500 text-sm">
Please provide your Microsoft 365 application credentials with
certificate authentication.
</div>
</div>
<CustomInput
control={control}
name="tenant_id"
type="text"
label="Tenant ID"
labelPlacement="inside"
placeholder="Enter the Tenant ID"
variant="bordered"
isRequired
isInvalid={!!control._formState.errors.tenant_id}
/>
<CustomInput
control={control}
name="client_id"
type="text"
label="Client ID"
labelPlacement="inside"
placeholder="Enter the Client ID"
variant="bordered"
isRequired
isInvalid={!!control._formState.errors.client_id}
/>
<CustomTextarea
control={control}
name="certificate_content"
label="Certificate Content"
labelPlacement="inside"
placeholder="Enter the base64 encoded certificate content"
variant="bordered"
isRequired
isInvalid={!!control._formState.errors.certificate_content}
minRows={4}
/>
<p className="text-default-500 text-sm">
The certificate content must be base64 encoded from an unsigned
certificate. For detailed instructions on how to generate and encode
your certificate, please refer to the{" "}
<CustomLink
href="https://docs.prowler.com/projects/prowler-open-source/en/latest/tutorials/microsoft365/getting-started-m365/#step-3-configure-your-m365-account"
size="sm"
>
Microsoft 365 getting started guide
</CustomLink>
.
</p>
</>
);
};
@@ -0,0 +1,58 @@
"use client";
import { Control } from "react-hook-form";
import { CustomInput } from "@/components/ui/custom";
import { M365ClientSecretCredentials } from "@/types";
export const M365ClientSecretCredentialsForm = ({
control,
}: {
control: Control<M365ClientSecretCredentials>;
}) => {
return (
<>
<div className="flex flex-col">
<div className="text-md text-default-foreground leading-9 font-bold">
App Client Secret Credentials
</div>
<div className="text-default-500 text-sm">
Please provide your Microsoft 365 application credentials.
</div>
</div>
<CustomInput
control={control}
name="tenant_id"
type="text"
label="Tenant ID"
labelPlacement="inside"
placeholder="Enter the Tenant ID"
variant="bordered"
isRequired
isInvalid={!!control._formState.errors.tenant_id}
/>
<CustomInput
control={control}
name="client_id"
type="text"
label="Client ID"
labelPlacement="inside"
placeholder="Enter the Client ID"
variant="bordered"
isRequired
isInvalid={!!control._formState.errors.client_id}
/>
<CustomInput
control={control}
name="client_secret"
type="password"
label="Client Secret"
labelPlacement="inside"
placeholder="Enter the Client Secret"
variant="bordered"
isRequired
isInvalid={!!control._formState.errors.client_secret}
/>
</>
);
};
@@ -0,0 +1,5 @@
export {
M365CertificateCredentialsForm,
M365ClientSecretCredentialsForm,
} from "./credentials-type";
export { SelectViaM365 } from "./select-via-m365";
@@ -0,0 +1,72 @@
"use client";
import { RadioGroup } from "@heroui/radio";
import React from "react";
import { Control, Controller } from "react-hook-form";
import { CustomRadio } from "@/components/ui/custom";
import { FormMessage } from "@/components/ui/form";
type 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-default-500 text-sm">
Select Authentication Method
</span>
<CustomRadio
description="Connect using Application Client Secret"
value="app_client_secret"
>
<div className="flex items-center">
<span className="ml-2">App Client Secret Credentials</span>
</div>
</CustomRadio>
<CustomRadio
description="Connect using Application Certificate"
value="app_certificate"
>
<div className="flex items-center">
<span className="ml-2">App Certificate 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>
);
};
@@ -1,4 +1,3 @@
export * from "./azure-credentials-form";
export * from "./github-credentials-form";
export * from "./k8s-credentials-form";
export * from "./m365-credentials-form";
@@ -80,14 +80,21 @@ export const buildAzureSecret = (formData: FormData) => {
export const buildM365Secret = (formData: FormData) => {
const secret = {
...buildAzureSecret(formData),
[ProviderCredentialFields.USER]: getFormValue(
[ProviderCredentialFields.CLIENT_ID]: getFormValue(
formData,
ProviderCredentialFields.USER,
ProviderCredentialFields.CLIENT_ID,
),
[ProviderCredentialFields.PASSWORD]: getFormValue(
[ProviderCredentialFields.TENANT_ID]: getFormValue(
formData,
ProviderCredentialFields.PASSWORD,
ProviderCredentialFields.TENANT_ID,
),
[ProviderCredentialFields.CLIENT_SECRET]: getFormValue(
formData,
ProviderCredentialFields.CLIENT_SECRET,
),
[ProviderCredentialFields.CERTIFICATE_CONTENT]: getFormValue(
formData,
ProviderCredentialFields.CERTIFICATE_CONTENT,
),
};
return filterEmptyValues(secret);
@@ -29,6 +29,7 @@ export const ProviderCredentialFields = {
TENANT_ID: "tenant_id",
USER: "user",
PASSWORD: "password",
CERTIFICATE_CONTENT: "certificate_content",
// GCP fields
REFRESH_TOKEN: "refresh_token",
@@ -70,6 +71,7 @@ export const ErrorPointers = {
OAUTH_APP_TOKEN: "/data/attributes/secret/oauth_app_token",
GITHUB_APP_ID: "/data/attributes/secret/github_app_id",
GITHUB_APP_KEY: "/data/attributes/secret/github_app_key_content",
CERTIFICATE_CONTENT: "/data/attributes/secret/certificate_content",
} as const;
export type ErrorPointer = (typeof ErrorPointers)[keyof typeof ErrorPointers];
+11 -1
View File
@@ -53,7 +53,7 @@ export const getProviderFormType = (
via?: string,
): ProviderFormType => {
// Providers that need credential type selection
const needsSelector = ["aws", "gcp", "github"].includes(providerType);
const needsSelector = ["aws", "gcp", "github", "m365"].includes(providerType);
// Show selector if no via parameter and provider needs it
if (needsSelector && !via) {
@@ -80,6 +80,14 @@ export const getProviderFormType = (
return "credentials";
}
// M365 credential types
if (
providerType === "m365" &&
["app_client_secret", "app_certificate"].includes(via || "")
) {
return "credentials";
}
// Other providers go directly to credentials form
if (!needsSelector) {
return "credentials";
@@ -99,6 +107,8 @@ export const requiresBackButton = (via?: string | null): boolean => {
"personal_access_token",
"oauth_app",
"github_app",
"app_client_secret",
"app_certificate",
];
return validViaTypes.includes(via);