mirror of
https://github.com/Infisical/infisical.git
synced 2026-01-09 23:48:05 -05:00
1092 lines
36 KiB
TypeScript
1092 lines
36 KiB
TypeScript
import { useEffect } from "react";
|
|
import { Controller, useForm } from "react-hook-form";
|
|
import { faQuestionCircle } from "@fortawesome/free-solid-svg-icons";
|
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
import { zodResolver } from "@hookform/resolvers/zod";
|
|
import { z } from "zod";
|
|
|
|
import { createNotification } from "@app/components/notifications";
|
|
import {
|
|
Button,
|
|
Checkbox,
|
|
FilterableSelect,
|
|
FormControl,
|
|
Input,
|
|
Modal,
|
|
ModalContent,
|
|
Select,
|
|
SelectItem,
|
|
TextArea,
|
|
Tooltip
|
|
} from "@app/components/v2";
|
|
import { useProject, useSubscription } from "@app/context";
|
|
import { CaType } from "@app/hooks/api/ca/enums";
|
|
import { useGetAzureAdcsTemplates, useListCasByProjectId } from "@app/hooks/api/ca/queries";
|
|
import {
|
|
EnrollmentType,
|
|
IssuerType,
|
|
TCertificateProfileWithDetails,
|
|
TCreateCertificateProfileDTO,
|
|
TUpdateCertificateProfileDTO,
|
|
useCreateCertificateProfile,
|
|
useUpdateCertificateProfile
|
|
} from "@app/hooks/api/certificateProfiles";
|
|
import { useListCertificateTemplatesV2 } from "@app/hooks/api/certificateTemplates/queries";
|
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
|
|
|
const createSchema = z
|
|
.object({
|
|
slug: z
|
|
.string()
|
|
.trim()
|
|
.min(1, "Profile slug is required")
|
|
.max(255, "Profile slug must be less than 255 characters")
|
|
.regex(
|
|
/^[a-zA-Z0-9-_]+$/,
|
|
"Profile slug must contain only letters, numbers, hyphens, and underscores"
|
|
),
|
|
description: z
|
|
.string()
|
|
.trim()
|
|
.max(1000, "Description must be less than 1000 characters")
|
|
.optional(),
|
|
enrollmentType: z.nativeEnum(EnrollmentType),
|
|
issuerType: z.nativeEnum(IssuerType),
|
|
certificateAuthorityId: z.string().nullable().optional(),
|
|
certificateTemplateId: z.string().min(1, "Certificate Template is required"),
|
|
estConfig: z
|
|
.object({
|
|
disableBootstrapCaValidation: z.boolean().optional(),
|
|
passphrase: z.string().min(1, "EST passphrase is required"),
|
|
caChain: z.string().min(1, "EST CA chain is required").optional()
|
|
})
|
|
.refine(
|
|
(data) => {
|
|
if (!data.disableBootstrapCaValidation && !data.caChain) {
|
|
return false;
|
|
}
|
|
return true;
|
|
},
|
|
{
|
|
message: "EST CA chain is required when bootstrap CA validation is enabled",
|
|
path: ["caChain"]
|
|
}
|
|
)
|
|
.optional(),
|
|
apiConfig: z
|
|
.object({
|
|
autoRenew: z.boolean().optional(),
|
|
renewBeforeDays: z.number().min(1).max(365).optional()
|
|
})
|
|
.optional(),
|
|
acmeConfig: z
|
|
.object({
|
|
skipDnsOwnershipVerification: z.boolean().optional()
|
|
})
|
|
.optional(),
|
|
externalConfigs: z
|
|
.object({
|
|
template: z.string().min(1, "Azure ADCS template is required")
|
|
})
|
|
.optional()
|
|
})
|
|
.refine(
|
|
(data) => {
|
|
if (data.enrollmentType === EnrollmentType.EST) {
|
|
return !!data.estConfig;
|
|
}
|
|
return true;
|
|
},
|
|
{
|
|
message: "EST enrollment type requires EST configuration"
|
|
}
|
|
)
|
|
.refine(
|
|
(data) => {
|
|
if (data.enrollmentType === EnrollmentType.API) {
|
|
return !!data.apiConfig;
|
|
}
|
|
return true;
|
|
},
|
|
{
|
|
message: "API enrollment type requires API configuration"
|
|
}
|
|
)
|
|
.refine(
|
|
(data) => {
|
|
if (data.enrollmentType === EnrollmentType.ACME) {
|
|
return !!data.acmeConfig;
|
|
}
|
|
return true;
|
|
},
|
|
{
|
|
message: "ACME enrollment type requires ACME configuration"
|
|
}
|
|
)
|
|
.refine(
|
|
(data) => {
|
|
if (data.enrollmentType === EnrollmentType.EST) {
|
|
return !data.apiConfig && !data.acmeConfig;
|
|
}
|
|
return true;
|
|
},
|
|
{
|
|
message: "EST enrollment type cannot have API or ACME configuration"
|
|
}
|
|
)
|
|
.refine(
|
|
(data) => {
|
|
if (data.enrollmentType === EnrollmentType.API) {
|
|
return !data.estConfig && !data.acmeConfig;
|
|
}
|
|
return true;
|
|
},
|
|
{
|
|
message: "API enrollment type cannot have EST or ACME configuration"
|
|
}
|
|
)
|
|
.refine(
|
|
(data) => {
|
|
if (data.enrollmentType === EnrollmentType.ACME) {
|
|
return !data.estConfig && !data.apiConfig;
|
|
}
|
|
return true;
|
|
},
|
|
{
|
|
message: "ACME enrollment type cannot have EST or API configuration"
|
|
}
|
|
)
|
|
.refine(
|
|
(data) => {
|
|
if (data.issuerType === IssuerType.CA) {
|
|
return !!data.certificateAuthorityId;
|
|
}
|
|
return true;
|
|
},
|
|
{
|
|
message: "CA issuer type requires a certificate authority"
|
|
}
|
|
)
|
|
.refine(
|
|
(data) => {
|
|
if (data.issuerType === IssuerType.SELF_SIGNED) {
|
|
return !data.certificateAuthorityId;
|
|
}
|
|
return true;
|
|
},
|
|
{
|
|
message: "Self-signed issuer type cannot have a certificate authority"
|
|
}
|
|
)
|
|
.refine(
|
|
(data) => {
|
|
if (data.issuerType === IssuerType.SELF_SIGNED) {
|
|
return data.enrollmentType === EnrollmentType.API;
|
|
}
|
|
return true;
|
|
},
|
|
{
|
|
message: "Self-signed issuer type only supports API enrollment"
|
|
}
|
|
);
|
|
|
|
const editSchema = z
|
|
.object({
|
|
slug: z
|
|
.string()
|
|
.trim()
|
|
.min(1, "Profile slug is required")
|
|
.max(255, "Profile slug must be less than 255 characters")
|
|
.regex(
|
|
/^[a-zA-Z0-9-_]+$/,
|
|
"Profile slug must contain only letters, numbers, hyphens, and underscores"
|
|
),
|
|
description: z
|
|
.string()
|
|
.trim()
|
|
.max(1000, "Description must be less than 1000 characters")
|
|
.optional(),
|
|
enrollmentType: z.nativeEnum(EnrollmentType),
|
|
issuerType: z.nativeEnum(IssuerType),
|
|
certificateAuthorityId: z.string().nullable().optional(),
|
|
certificateTemplateId: z.string().optional(),
|
|
estConfig: z
|
|
.object({
|
|
disableBootstrapCaValidation: z.boolean().optional(),
|
|
passphrase: z.string().optional(),
|
|
caChain: z.string().optional()
|
|
})
|
|
.optional(),
|
|
apiConfig: z
|
|
.object({
|
|
autoRenew: z.boolean().optional(),
|
|
renewBeforeDays: z.number().min(1).max(365).optional()
|
|
})
|
|
.optional(),
|
|
acmeConfig: z
|
|
.object({
|
|
skipDnsOwnershipVerification: z.boolean().optional()
|
|
})
|
|
.optional(),
|
|
externalConfigs: z
|
|
.object({
|
|
template: z.string().optional()
|
|
})
|
|
.optional()
|
|
})
|
|
.refine(
|
|
(data) => {
|
|
if (data.enrollmentType === EnrollmentType.EST) {
|
|
return !!data.estConfig;
|
|
}
|
|
return true;
|
|
},
|
|
{
|
|
message: "EST enrollment type requires EST configuration"
|
|
}
|
|
)
|
|
.refine(
|
|
(data) => {
|
|
if (data.enrollmentType === EnrollmentType.API) {
|
|
return !!data.apiConfig;
|
|
}
|
|
return true;
|
|
},
|
|
{
|
|
message: "API enrollment type requires API configuration"
|
|
}
|
|
)
|
|
.refine(
|
|
(data) => {
|
|
if (data.enrollmentType === EnrollmentType.ACME) {
|
|
return !!data.acmeConfig;
|
|
}
|
|
return true;
|
|
},
|
|
{
|
|
message: "ACME enrollment type requires ACME configuration"
|
|
}
|
|
)
|
|
.refine(
|
|
(data) => {
|
|
if (data.enrollmentType === EnrollmentType.EST) {
|
|
return !data.apiConfig && !data.acmeConfig;
|
|
}
|
|
return true;
|
|
},
|
|
{
|
|
message: "EST enrollment type cannot have API or ACME configuration"
|
|
}
|
|
)
|
|
.refine(
|
|
(data) => {
|
|
if (data.enrollmentType === EnrollmentType.API) {
|
|
return !data.estConfig && !data.acmeConfig;
|
|
}
|
|
return true;
|
|
},
|
|
{
|
|
message: "API enrollment type cannot have EST or ACME configuration"
|
|
}
|
|
)
|
|
.refine(
|
|
(data) => {
|
|
if (data.enrollmentType === EnrollmentType.ACME) {
|
|
return !data.estConfig && !data.apiConfig;
|
|
}
|
|
return true;
|
|
},
|
|
{
|
|
message: "ACME enrollment type cannot have EST or API configuration"
|
|
}
|
|
)
|
|
.refine(
|
|
(data) => {
|
|
if (data.issuerType === IssuerType.CA) {
|
|
return !!data.certificateAuthorityId;
|
|
}
|
|
return true;
|
|
},
|
|
{
|
|
message: "CA issuer type requires a certificate authority"
|
|
}
|
|
)
|
|
.refine(
|
|
(data) => {
|
|
if (data.issuerType === IssuerType.SELF_SIGNED) {
|
|
return !data.certificateAuthorityId;
|
|
}
|
|
return true;
|
|
},
|
|
{
|
|
message: "Self-signed issuer type cannot have a certificate authority"
|
|
}
|
|
)
|
|
.refine(
|
|
(data) => {
|
|
if (data.issuerType === IssuerType.SELF_SIGNED) {
|
|
return data.enrollmentType === EnrollmentType.API;
|
|
}
|
|
return true;
|
|
},
|
|
{
|
|
message: "Self-signed issuer type only supports API enrollment"
|
|
}
|
|
);
|
|
|
|
export type FormData = z.infer<typeof createSchema>;
|
|
|
|
interface Props {
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
handlePopUpOpen: (
|
|
popUpName: keyof UsePopUpState<["upgradePlan"]>,
|
|
data?: {
|
|
isEnterpriseFeature?: boolean;
|
|
}
|
|
) => void;
|
|
profile?: TCertificateProfileWithDetails;
|
|
mode?: "create" | "edit";
|
|
}
|
|
|
|
export const CreateProfileModal = ({
|
|
isOpen,
|
|
onClose,
|
|
handlePopUpOpen,
|
|
profile,
|
|
mode = "create"
|
|
}: Props) => {
|
|
const { currentProject } = useProject();
|
|
const { subscription } = useSubscription();
|
|
|
|
const { data: allCaData } = useListCasByProjectId(currentProject?.id || "");
|
|
const { data: templateData } = useListCertificateTemplatesV2({
|
|
projectId: currentProject?.id || "",
|
|
limit: 100,
|
|
offset: 0
|
|
});
|
|
|
|
const createProfile = useCreateCertificateProfile();
|
|
const updateProfile = useUpdateCertificateProfile();
|
|
|
|
const isEdit = mode === "edit" && profile;
|
|
|
|
const certificateAuthorities = (allCaData || []).map((ca) => ({
|
|
...ca,
|
|
groupType: ca.type === "internal" ? "internal" : "external"
|
|
}));
|
|
const certificateTemplates = templateData?.certificateTemplates || [];
|
|
|
|
const getGroupHeaderLabel = (groupType: "internal" | "external") => {
|
|
switch (groupType) {
|
|
case "internal":
|
|
return "Internal CAs";
|
|
case "external":
|
|
return "External CAs";
|
|
default:
|
|
return "";
|
|
}
|
|
};
|
|
|
|
const { control, handleSubmit, reset, watch, setValue, formState } = useForm<FormData>({
|
|
resolver: zodResolver(isEdit ? editSchema : createSchema),
|
|
defaultValues: isEdit
|
|
? {
|
|
slug: profile.slug,
|
|
description: profile.description || "",
|
|
enrollmentType: profile.enrollmentType,
|
|
issuerType: profile.issuerType,
|
|
certificateAuthorityId: profile.caId || undefined,
|
|
certificateTemplateId: profile.certificateTemplateId,
|
|
estConfig:
|
|
profile.enrollmentType === EnrollmentType.EST
|
|
? {
|
|
disableBootstrapCaValidation:
|
|
profile.estConfig?.disableBootstrapCaValidation || false,
|
|
passphrase: profile.estConfig?.passphrase || "",
|
|
caChain: profile.estConfig?.caChain || ""
|
|
}
|
|
: undefined,
|
|
apiConfig:
|
|
profile.enrollmentType === EnrollmentType.API
|
|
? {
|
|
autoRenew: profile.apiConfig?.autoRenew || false,
|
|
renewBeforeDays: profile.apiConfig?.renewBeforeDays || 30
|
|
}
|
|
: undefined,
|
|
acmeConfig:
|
|
profile.enrollmentType === EnrollmentType.ACME
|
|
? {
|
|
skipDnsOwnershipVerification:
|
|
profile.acmeConfig?.skipDnsOwnershipVerification || false
|
|
}
|
|
: undefined,
|
|
externalConfigs: profile.externalConfigs
|
|
? {
|
|
template:
|
|
typeof profile.externalConfigs === "object" &&
|
|
profile.externalConfigs !== null &&
|
|
typeof profile.externalConfigs.template === "string"
|
|
? profile.externalConfigs.template
|
|
: ""
|
|
}
|
|
: undefined
|
|
}
|
|
: {
|
|
slug: "",
|
|
description: "",
|
|
enrollmentType: EnrollmentType.API,
|
|
issuerType: IssuerType.CA,
|
|
certificateAuthorityId: "",
|
|
certificateTemplateId: "",
|
|
apiConfig: {
|
|
autoRenew: false,
|
|
renewBeforeDays: 30
|
|
},
|
|
acmeConfig: {
|
|
skipDnsOwnershipVerification: false
|
|
},
|
|
externalConfigs: undefined
|
|
}
|
|
});
|
|
|
|
const watchedEnrollmentType = watch("enrollmentType");
|
|
const watchedIssuerType = watch("issuerType");
|
|
const watchedCertificateAuthorityId = watch("certificateAuthorityId");
|
|
const watchedDisableBootstrapValidation = watch("estConfig.disableBootstrapCaValidation");
|
|
const watchedAutoRenew = watch("apiConfig.autoRenew");
|
|
|
|
// Get the selected CA to check if it's Azure ADCS
|
|
const selectedCa = certificateAuthorities.find((ca) => ca.id === watchedCertificateAuthorityId);
|
|
const isAzureAdcsCa = selectedCa?.type === CaType.AZURE_AD_CS;
|
|
|
|
// Fetch Azure ADCS templates if needed
|
|
const { data: azureAdcsTemplatesData } = useGetAzureAdcsTemplates({
|
|
caId: watchedCertificateAuthorityId || "",
|
|
projectId: currentProject?.id || "",
|
|
isAzureAdcsCa
|
|
});
|
|
|
|
useEffect(() => {
|
|
if (isEdit && profile) {
|
|
reset({
|
|
slug: profile.slug,
|
|
description: profile.description || "",
|
|
enrollmentType: profile.enrollmentType,
|
|
issuerType: profile.issuerType,
|
|
certificateAuthorityId: profile.caId || undefined,
|
|
certificateTemplateId: profile.certificateTemplateId,
|
|
estConfig:
|
|
profile.enrollmentType === "est"
|
|
? {
|
|
disableBootstrapCaValidation:
|
|
profile.estConfig?.disableBootstrapCaValidation || false,
|
|
passphrase: profile.estConfig?.passphrase || "",
|
|
caChain: profile.estConfig?.caChain || ""
|
|
}
|
|
: undefined,
|
|
apiConfig:
|
|
profile.enrollmentType === "api"
|
|
? {
|
|
autoRenew: profile.apiConfig?.autoRenew || false,
|
|
renewBeforeDays: profile.apiConfig?.renewBeforeDays || 30
|
|
}
|
|
: undefined,
|
|
acmeConfig:
|
|
profile.enrollmentType === EnrollmentType.ACME
|
|
? {
|
|
skipDnsOwnershipVerification:
|
|
profile.acmeConfig?.skipDnsOwnershipVerification || false
|
|
}
|
|
: undefined,
|
|
externalConfigs: profile.externalConfigs
|
|
? {
|
|
template:
|
|
typeof profile.externalConfigs === "object" &&
|
|
profile.externalConfigs !== null &&
|
|
typeof profile.externalConfigs.template === "string"
|
|
? profile.externalConfigs.template
|
|
: ""
|
|
}
|
|
: undefined
|
|
});
|
|
}
|
|
}, [isEdit, profile, reset, allCaData]);
|
|
|
|
// Additional effect to reset external configs when Azure ADCS templates are loaded
|
|
useEffect(() => {
|
|
if (
|
|
isEdit &&
|
|
profile &&
|
|
isAzureAdcsCa &&
|
|
azureAdcsTemplatesData?.templates &&
|
|
profile.externalConfigs &&
|
|
typeof profile.externalConfigs === "object" &&
|
|
profile.externalConfigs !== null &&
|
|
typeof profile.externalConfigs.template === "string"
|
|
) {
|
|
// Re-set the external configs to ensure the template value is properly set
|
|
// after the Azure ADCS templates have been loaded
|
|
setValue("externalConfigs.template", profile.externalConfigs.template);
|
|
}
|
|
}, [isEdit, profile, isAzureAdcsCa, azureAdcsTemplatesData, setValue]);
|
|
|
|
const onFormSubmit = async (data: FormData) => {
|
|
if (!isEdit && !subscription?.pkiAcme && data.enrollmentType === EnrollmentType.ACME) {
|
|
reset();
|
|
onClose();
|
|
handlePopUpOpen("upgradePlan", {
|
|
isEnterpriseFeature: true
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (!currentProject?.id && !isEdit) return;
|
|
|
|
// Validate Azure ADCS template requirement
|
|
if (
|
|
isAzureAdcsCa &&
|
|
(!data.externalConfigs?.template || data.externalConfigs.template.trim() === "")
|
|
) {
|
|
createNotification({
|
|
text: "Azure ADCS Certificate Authority requires a template to be specified",
|
|
type: "error"
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (isEdit) {
|
|
const updateData: TUpdateCertificateProfileDTO = {
|
|
profileId: profile.id,
|
|
slug: data.slug,
|
|
description: data.description,
|
|
issuerType: data.issuerType
|
|
};
|
|
|
|
if (data.enrollmentType === EnrollmentType.EST && data.estConfig) {
|
|
updateData.estConfig = data.estConfig;
|
|
} else if (data.enrollmentType === EnrollmentType.API && data.apiConfig) {
|
|
updateData.apiConfig = data.apiConfig;
|
|
} else if (data.enrollmentType === EnrollmentType.ACME && data.acmeConfig) {
|
|
updateData.acmeConfig = data.acmeConfig;
|
|
}
|
|
|
|
// Add external configs if present
|
|
if (data.externalConfigs) {
|
|
updateData.externalConfigs = data.externalConfigs;
|
|
}
|
|
|
|
await updateProfile.mutateAsync(updateData);
|
|
} else {
|
|
if (!currentProject?.id) {
|
|
throw new Error("Project ID is required for creating a profile");
|
|
}
|
|
|
|
const createData: TCreateCertificateProfileDTO = {
|
|
projectId: currentProject.id,
|
|
slug: data.slug,
|
|
description: data.description,
|
|
enrollmentType: data.enrollmentType,
|
|
issuerType: data.issuerType,
|
|
caId:
|
|
data.issuerType === IssuerType.SELF_SIGNED
|
|
? undefined
|
|
: data.certificateAuthorityId || undefined,
|
|
certificateTemplateId: data.certificateTemplateId
|
|
};
|
|
|
|
if (data.enrollmentType === EnrollmentType.EST && data.estConfig) {
|
|
createData.estConfig = {
|
|
passphrase: data.estConfig.passphrase,
|
|
caChain: data.estConfig.caChain || undefined,
|
|
disableBootstrapCaValidation: data.estConfig.disableBootstrapCaValidation
|
|
};
|
|
} else if (data.enrollmentType === EnrollmentType.API && data.apiConfig) {
|
|
createData.apiConfig = data.apiConfig;
|
|
} else if (data.enrollmentType === EnrollmentType.ACME && data.acmeConfig) {
|
|
createData.acmeConfig = data.acmeConfig;
|
|
}
|
|
|
|
// Add external configs if present
|
|
if (data.externalConfigs) {
|
|
createData.externalConfigs = data.externalConfigs;
|
|
}
|
|
|
|
await createProfile.mutateAsync(createData);
|
|
}
|
|
|
|
createNotification({
|
|
text: `Certificate profile ${isEdit ? "updated" : "created"} successfully`,
|
|
type: "success"
|
|
});
|
|
|
|
reset();
|
|
onClose();
|
|
};
|
|
|
|
return (
|
|
<Modal
|
|
isOpen={isOpen}
|
|
onOpenChange={(open) => {
|
|
if (!open) {
|
|
reset();
|
|
}
|
|
onClose();
|
|
}}
|
|
>
|
|
<ModalContent
|
|
title={isEdit ? "Edit Certificate Profile" : "Create Certificate Profile"}
|
|
subTitle={
|
|
isEdit
|
|
? `Update configuration for ${profile?.slug}`
|
|
: "Configure a new certificate profile for unified certificate issuance"
|
|
}
|
|
>
|
|
<form onSubmit={handleSubmit(onFormSubmit)}>
|
|
<Controller
|
|
control={control}
|
|
name="slug"
|
|
render={({ field, fieldState: { error } }) => (
|
|
<FormControl
|
|
label="Name"
|
|
isRequired
|
|
isError={Boolean(error)}
|
|
errorText={error?.message}
|
|
>
|
|
<Input {...field} placeholder="your-profile-name" />
|
|
</FormControl>
|
|
)}
|
|
/>
|
|
|
|
<Controller
|
|
control={control}
|
|
name="description"
|
|
render={({ field, fieldState: { error } }) => (
|
|
<FormControl label="Description" isError={Boolean(error)} errorText={error?.message}>
|
|
<TextArea {...field} placeholder="Enter profile description" rows={3} />
|
|
</FormControl>
|
|
)}
|
|
/>
|
|
|
|
<Controller
|
|
control={control}
|
|
name="issuerType"
|
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
|
<FormControl
|
|
label="Issuer Type"
|
|
isRequired
|
|
isError={Boolean(error)}
|
|
errorText={error?.message}
|
|
>
|
|
<Select
|
|
{...field}
|
|
onValueChange={(value) => {
|
|
if (value === "self-signed") {
|
|
setValue("certificateAuthorityId", "");
|
|
setValue("enrollmentType", EnrollmentType.API);
|
|
setValue("apiConfig", {
|
|
autoRenew: false,
|
|
renewBeforeDays: 30
|
|
});
|
|
setValue("estConfig", undefined);
|
|
setValue("acmeConfig", {
|
|
skipDnsOwnershipVerification: false
|
|
});
|
|
}
|
|
onChange(value);
|
|
}}
|
|
className="w-full"
|
|
position="popper"
|
|
isDisabled={Boolean(isEdit)}
|
|
>
|
|
<SelectItem value="ca">Certificate Authority</SelectItem>
|
|
<SelectItem value="self-signed">Self-Signed</SelectItem>
|
|
</Select>
|
|
</FormControl>
|
|
)}
|
|
/>
|
|
|
|
{watchedIssuerType === "ca" && (
|
|
<Controller
|
|
control={control}
|
|
name="certificateAuthorityId"
|
|
render={({ field: { onChange, value }, fieldState: { error } }) => (
|
|
<FormControl
|
|
label="Issuing CA"
|
|
isRequired
|
|
isError={Boolean(error)}
|
|
errorText={error?.message}
|
|
>
|
|
<FilterableSelect
|
|
value={certificateAuthorities.find((ca) => ca.id === value) || null}
|
|
onChange={(selectedCaValue) => {
|
|
if (Array.isArray(selectedCaValue)) {
|
|
onChange(selectedCaValue[0]?.id || "");
|
|
} else if (
|
|
selectedCaValue &&
|
|
typeof selectedCaValue === "object" &&
|
|
"id" in selectedCaValue
|
|
) {
|
|
onChange(selectedCaValue.id || "");
|
|
} else {
|
|
onChange("");
|
|
}
|
|
}}
|
|
getOptionLabel={(ca) =>
|
|
ca.type === "internal" && ca.configuration.friendlyName
|
|
? ca.configuration.friendlyName
|
|
: ca.name
|
|
}
|
|
getOptionValue={(ca) => ca.id}
|
|
options={certificateAuthorities}
|
|
groupBy="groupType"
|
|
getGroupHeaderLabel={getGroupHeaderLabel}
|
|
placeholder="Select a certificate authority"
|
|
isDisabled={Boolean(isEdit)}
|
|
className="w-full"
|
|
/>
|
|
</FormControl>
|
|
)}
|
|
/>
|
|
)}
|
|
|
|
{/* Azure ADCS Template Selection */}
|
|
{isAzureAdcsCa && (
|
|
<Controller
|
|
control={control}
|
|
name="externalConfigs.template"
|
|
render={({ field: { onChange, value }, fieldState: { error } }) => (
|
|
<FormControl
|
|
label="Windows ADCS Template"
|
|
isRequired
|
|
isError={Boolean(error)}
|
|
errorText={error?.message}
|
|
>
|
|
<FilterableSelect
|
|
value={
|
|
azureAdcsTemplatesData?.templates.find((template) => template.id === value) ||
|
|
null
|
|
}
|
|
onChange={(selectedTemplate) => {
|
|
if (Array.isArray(selectedTemplate)) {
|
|
onChange(selectedTemplate[0]?.id || "");
|
|
} else if (
|
|
selectedTemplate &&
|
|
typeof selectedTemplate === "object" &&
|
|
"id" in selectedTemplate
|
|
) {
|
|
onChange(selectedTemplate.id || "");
|
|
} else {
|
|
onChange("");
|
|
}
|
|
}}
|
|
getOptionLabel={(template) => template.name}
|
|
getOptionValue={(template) => template.id}
|
|
options={azureAdcsTemplatesData?.templates || []}
|
|
placeholder="Select an Azure ADCS certificate template"
|
|
className="w-full"
|
|
/>
|
|
</FormControl>
|
|
)}
|
|
/>
|
|
)}
|
|
|
|
<Controller
|
|
control={control}
|
|
name="certificateTemplateId"
|
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
|
<FormControl
|
|
label="Certificate Template"
|
|
isRequired
|
|
isError={Boolean(error)}
|
|
errorText={error?.message}
|
|
>
|
|
<Select
|
|
{...field}
|
|
onValueChange={(value) => {
|
|
if (watchedEnrollmentType === "est") {
|
|
setValue("apiConfig", undefined);
|
|
setValue("estConfig", {
|
|
disableBootstrapCaValidation: false,
|
|
passphrase: ""
|
|
});
|
|
setValue("acmeConfig", undefined);
|
|
} else if (watchedEnrollmentType === "api") {
|
|
setValue("apiConfig", {
|
|
autoRenew: false,
|
|
renewBeforeDays: 30
|
|
});
|
|
setValue("estConfig", undefined);
|
|
setValue("acmeConfig", undefined);
|
|
} else if (watchedEnrollmentType === "acme") {
|
|
setValue("estConfig", undefined);
|
|
setValue("apiConfig", undefined);
|
|
setValue("acmeConfig", {
|
|
skipDnsOwnershipVerification: false
|
|
});
|
|
}
|
|
onChange(value);
|
|
}}
|
|
placeholder="Select a certificate template"
|
|
className="w-full"
|
|
position="popper"
|
|
isDisabled={Boolean(isEdit)}
|
|
>
|
|
{certificateTemplates.map((template) => (
|
|
<SelectItem key={template.id} value={template.id}>
|
|
{template.name}
|
|
</SelectItem>
|
|
))}
|
|
</Select>
|
|
</FormControl>
|
|
)}
|
|
/>
|
|
|
|
<Controller
|
|
control={control}
|
|
name="enrollmentType"
|
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
|
<FormControl
|
|
label="Enrollment Method"
|
|
isRequired
|
|
isError={Boolean(error)}
|
|
errorText={error?.message}
|
|
>
|
|
<Select
|
|
{...field}
|
|
onValueChange={(value) => {
|
|
if (value === "est") {
|
|
setValue("apiConfig", undefined);
|
|
setValue("estConfig", {
|
|
disableBootstrapCaValidation: false,
|
|
passphrase: ""
|
|
});
|
|
setValue("acmeConfig", undefined);
|
|
} else if (value === "api") {
|
|
setValue("apiConfig", {
|
|
autoRenew: false,
|
|
renewBeforeDays: 30
|
|
});
|
|
setValue("estConfig", undefined);
|
|
setValue("acmeConfig", undefined);
|
|
} else if (value === "acme") {
|
|
setValue("apiConfig", undefined);
|
|
setValue("estConfig", undefined);
|
|
setValue("acmeConfig", {
|
|
skipDnsOwnershipVerification: false
|
|
});
|
|
}
|
|
onChange(value);
|
|
}}
|
|
className="w-full"
|
|
position="popper"
|
|
isDisabled={Boolean(isEdit)}
|
|
>
|
|
<SelectItem value="api">API</SelectItem>
|
|
{watchedIssuerType !== IssuerType.SELF_SIGNED && (
|
|
<SelectItem value="est">EST</SelectItem>
|
|
)}
|
|
{watchedIssuerType !== IssuerType.SELF_SIGNED && (
|
|
<SelectItem value="acme">ACME</SelectItem>
|
|
)}
|
|
</Select>
|
|
</FormControl>
|
|
)}
|
|
/>
|
|
|
|
{/* EST Configuration */}
|
|
{watchedEnrollmentType === "est" && (
|
|
<div className="mb-4 space-y-4">
|
|
<div className="space-y-4">
|
|
<Controller
|
|
control={control}
|
|
name="estConfig.disableBootstrapCaValidation"
|
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
|
<FormControl isError={Boolean(error)} errorText={error?.message}>
|
|
<div className="flex items-center gap-3 rounded-md border border-mineshaft-600 bg-mineshaft-900 p-4">
|
|
<Checkbox
|
|
id="disableBootstrapCaValidation"
|
|
isChecked={value}
|
|
onCheckedChange={onChange}
|
|
/>
|
|
<div className="space-y-1">
|
|
<span className="text-sm font-medium text-mineshaft-100">
|
|
Disable Bootstrap CA Validation
|
|
</span>
|
|
<p className="text-xs text-bunker-300">
|
|
Skip CA certificate validation during EST bootstrap phase
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</FormControl>
|
|
)}
|
|
/>
|
|
|
|
<Controller
|
|
control={control}
|
|
name="estConfig.passphrase"
|
|
render={({ field, fieldState: { error } }) => (
|
|
<FormControl
|
|
label="EST Passphrase"
|
|
isRequired={!isEdit}
|
|
isError={Boolean(error)}
|
|
errorText={error?.message}
|
|
>
|
|
<Input
|
|
{...field}
|
|
type="password"
|
|
placeholder="Enter secure passphrase for EST authentication"
|
|
className="w-full"
|
|
/>
|
|
</FormControl>
|
|
)}
|
|
/>
|
|
|
|
{!watchedDisableBootstrapValidation && (
|
|
<Controller
|
|
control={control}
|
|
name="estConfig.caChain"
|
|
render={({ field, fieldState: { error } }) => (
|
|
<FormControl
|
|
label="CA Chain Certificate"
|
|
isRequired={!isEdit}
|
|
isError={Boolean(error)}
|
|
errorText={error?.message}
|
|
>
|
|
<div className="space-y-2">
|
|
<TextArea
|
|
{...field}
|
|
placeholder="-----BEGIN CERTIFICATE----- MIIDXTCCAkWgAwIBAgIJAKoK/heBjcOuMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX ... -----END CERTIFICATE-----"
|
|
rows={6}
|
|
className="w-full font-mono text-xs"
|
|
/>
|
|
<p className="text-xs text-bunker-400">
|
|
Paste the complete CA certificate chain in PEM format
|
|
</p>
|
|
</div>
|
|
</FormControl>
|
|
)}
|
|
/>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* API Configuration */}
|
|
{watchedEnrollmentType === "api" && (
|
|
<div className="mb-4 space-y-4">
|
|
<Controller
|
|
control={control}
|
|
name="apiConfig.autoRenew"
|
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
|
<FormControl isError={Boolean(error)} errorText={error?.message}>
|
|
<div className="flex items-center gap-2">
|
|
<Checkbox id="autoRenew" isChecked={value} onCheckedChange={onChange}>
|
|
Enable Auto-Renewal By Default
|
|
</Checkbox>
|
|
<Tooltip content="If enabled, certificates issued against this profile will auto-renew at specified days before expiration.">
|
|
<FontAwesomeIcon
|
|
icon={faQuestionCircle}
|
|
className="cursor-help text-mineshaft-400 hover:text-mineshaft-300"
|
|
size="sm"
|
|
/>
|
|
</Tooltip>
|
|
</div>
|
|
</FormControl>
|
|
)}
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{/* ACME Configuration */}
|
|
{watchedEnrollmentType === "acme" && (
|
|
<div className="mb-4 space-y-4">
|
|
<Controller
|
|
control={control}
|
|
name="acmeConfig.skipDnsOwnershipVerification"
|
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
|
<FormControl isError={Boolean(error)} errorText={error?.message}>
|
|
<div className="flex items-center gap-3 rounded-md border border-mineshaft-600 bg-mineshaft-900 p-4">
|
|
<Checkbox
|
|
id="skipDnsOwnershipVerification"
|
|
isChecked={value || false}
|
|
onCheckedChange={onChange}
|
|
/>
|
|
<div className="space-y-1">
|
|
<span className="text-sm font-medium text-mineshaft-100">
|
|
Skip DNS Ownership Validation
|
|
</span>
|
|
<p className="text-xs text-bunker-300">
|
|
Skip DNS ownership verification during ACME certificate issuance.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</FormControl>
|
|
)}
|
|
/>
|
|
</div>
|
|
)}
|
|
{watchedAutoRenew && (
|
|
<div className="mb-4 space-y-4">
|
|
<Controller
|
|
control={control}
|
|
name="apiConfig.renewBeforeDays"
|
|
render={({ field, fieldState: { error } }) => (
|
|
<FormControl
|
|
label="Auto-Renewal Days Before Expiration"
|
|
isError={Boolean(error)}
|
|
errorText={error?.message}
|
|
>
|
|
<Input
|
|
{...field}
|
|
type="number"
|
|
placeholder="30"
|
|
min="1"
|
|
max="365"
|
|
className="w-full"
|
|
isDisabled={!watchedAutoRenew}
|
|
onChange={(e) => {
|
|
const { value } = e.target;
|
|
if (value === "") {
|
|
field.onChange("");
|
|
} else {
|
|
const parsed = parseInt(value, 10);
|
|
if (!Number.isNaN(parsed) && parsed >= 1 && parsed <= 365) {
|
|
field.onChange(parsed);
|
|
} else {
|
|
field.onChange(field.value || "");
|
|
}
|
|
}
|
|
}}
|
|
/>
|
|
</FormControl>
|
|
)}
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex gap-3">
|
|
<Button
|
|
type="submit"
|
|
colorSchema="primary"
|
|
isLoading={isEdit ? updateProfile.isPending : createProfile.isPending}
|
|
isDisabled={
|
|
!formState.isValid || (isEdit ? updateProfile.isPending : createProfile.isPending)
|
|
}
|
|
>
|
|
{isEdit ? "Save Changes" : "Create"}
|
|
</Button>
|
|
<Button
|
|
variant="outline_bg"
|
|
onClick={onClose}
|
|
disabled={isEdit ? updateProfile.isPending : createProfile.isPending}
|
|
>
|
|
Cancel
|
|
</Button>
|
|
</div>
|
|
</form>
|
|
</ModalContent>
|
|
</Modal>
|
|
);
|
|
};
|