mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
Merge branch 'main' into feature/mongodb-secret-rotation
This commit is contained in:
@@ -64,7 +64,7 @@ export const CreatePkiSyncModal = ({
|
||||
"Add Sync"
|
||||
)
|
||||
}
|
||||
className="max-w-2xl"
|
||||
className="max-w-3xl"
|
||||
bodyClassName="overflow-visible"
|
||||
subTitle={
|
||||
selectedSync ? undefined : "Select a third-party service to sync certificates to."
|
||||
|
||||
@@ -15,11 +15,13 @@ type Props = {
|
||||
export const EditPkiSyncModal = ({ pkiSync, onOpenChange, fields, ...props }: Props) => {
|
||||
if (!pkiSync) return null;
|
||||
|
||||
const modalClassName = fields === PkiSyncEditFields.Mappings ? "max-w-4xl" : "max-w-2xl";
|
||||
|
||||
return (
|
||||
<Modal {...props} onOpenChange={onOpenChange}>
|
||||
<ModalContent
|
||||
title={<PkiSyncModalHeader isConfigured destination={pkiSync.destination} />}
|
||||
className="max-w-2xl"
|
||||
className={modalClassName}
|
||||
bodyClassName="overflow-visible"
|
||||
>
|
||||
<EditPkiSyncForm onComplete={() => onOpenChange(false)} fields={fields} pkiSync={pkiSync} />
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import { Controller, useFormContext } from "react-hook-form";
|
||||
|
||||
import { FormControl, Select, SelectItem } from "@app/components/v2";
|
||||
import { AWS_REGIONS } from "@app/helpers/appConnections";
|
||||
import { PkiSync } from "@app/hooks/api/pkiSyncs";
|
||||
|
||||
import { TPkiSyncForm } from "./schemas/pki-sync-schema";
|
||||
import { PkiSyncConnectionField } from "./PkiSyncConnectionField";
|
||||
|
||||
export const AwsSecretsManagerPkiSyncFields = () => {
|
||||
const { control, setValue } = useFormContext<
|
||||
TPkiSyncForm & { destination: PkiSync.AwsSecretsManager }
|
||||
>();
|
||||
|
||||
return (
|
||||
<>
|
||||
<PkiSyncConnectionField
|
||||
onChange={() => {
|
||||
setValue("destinationConfig.region", "");
|
||||
}}
|
||||
/>
|
||||
<Controller
|
||||
name="destinationConfig.region"
|
||||
control={control}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
label="AWS Region"
|
||||
tooltipText="Select the AWS region where your secrets will be stored in AWS Secrets Manager."
|
||||
>
|
||||
<Select
|
||||
value={field.value}
|
||||
onValueChange={field.onChange}
|
||||
className="w-full border border-mineshaft-500 capitalize"
|
||||
position="popper"
|
||||
placeholder="Select an AWS region"
|
||||
>
|
||||
{AWS_REGIONS.map(({ name, slug }) => (
|
||||
<SelectItem value={slug} key={slug}>
|
||||
{name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
import { Controller, useFormContext } from "react-hook-form";
|
||||
|
||||
import { FormControl, Input } from "@app/components/v2";
|
||||
import { PkiSync } from "@app/hooks/api/pkiSyncs";
|
||||
|
||||
import { TPkiSyncForm } from "./schemas/pki-sync-schema";
|
||||
import { PkiSyncConnectionField } from "./PkiSyncConnectionField";
|
||||
|
||||
export const ChefPkiSyncFields = () => {
|
||||
const { control, setValue } = useFormContext<TPkiSyncForm & { destination: PkiSync.Chef }>();
|
||||
|
||||
return (
|
||||
<>
|
||||
<PkiSyncConnectionField
|
||||
onChange={() => {
|
||||
setValue("destinationConfig.dataBagName", "");
|
||||
}}
|
||||
/>
|
||||
<Controller
|
||||
name="destinationConfig.dataBagName"
|
||||
control={control}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
label="Data Bag Name"
|
||||
tooltipText="Enter your Chef data bag name where certificates will be stored. This data bag will be used to store SSL/TLS certificates, private keys, and certificate chains. Data bag names must contain only alphanumeric characters, underscores, and hyphens."
|
||||
>
|
||||
<Input {...field} placeholder="ssl_certificates" maxLength={255} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -4,7 +4,6 @@ import { faInfoCircle } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Tab } from "@headlessui/react";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Button, FormControl, Switch } from "@app/components/v2";
|
||||
@@ -16,6 +15,7 @@ import { PkiSyncFormSchema, TPkiSyncForm } from "./schemas/pki-sync-schema";
|
||||
import { PkiSyncCertificatesFields } from "./PkiSyncCertificatesFields";
|
||||
import { PkiSyncDestinationFields } from "./PkiSyncDestinationFields";
|
||||
import { PkiSyncDetailsFields } from "./PkiSyncDetailsFields";
|
||||
import { PkiSyncFieldMappingsFields } from "./PkiSyncFieldMappingsFields";
|
||||
import { PkiSyncOptionsFields } from "./PkiSyncOptionsFields";
|
||||
import { PkiSyncReviewFields } from "./PkiSyncReviewFields";
|
||||
|
||||
@@ -26,13 +26,38 @@ type Props = {
|
||||
initialData?: any;
|
||||
};
|
||||
|
||||
const FORM_TABS: { name: string; key: string; fields: (keyof TPkiSyncForm)[] }[] = [
|
||||
{ name: "Destination", key: "destination", fields: ["connection", "destinationConfig"] },
|
||||
{ name: "Sync Options", key: "options", fields: ["syncOptions"] },
|
||||
{ name: "Details", key: "details", fields: ["name", "description"] },
|
||||
{ name: "Certificates", key: "certificates", fields: ["certificateIds"] },
|
||||
{ name: "Review", key: "review", fields: [] }
|
||||
];
|
||||
const getFormTabs = (
|
||||
destination: PkiSync
|
||||
): { name: string; key: string; fields: (keyof TPkiSyncForm)[] }[] => {
|
||||
const baseTabs = [
|
||||
{
|
||||
name: "Destination",
|
||||
key: "destination",
|
||||
fields: ["connection", "destinationConfig"] as (keyof TPkiSyncForm)[]
|
||||
},
|
||||
{ name: "Sync Options", key: "options", fields: ["syncOptions"] as (keyof TPkiSyncForm)[] }
|
||||
];
|
||||
|
||||
if (destination === PkiSync.Chef || destination === PkiSync.AwsSecretsManager) {
|
||||
baseTabs.push({
|
||||
name: "Mappings",
|
||||
key: "mappings",
|
||||
fields: ["syncOptions"] as (keyof TPkiSyncForm)[]
|
||||
});
|
||||
}
|
||||
|
||||
baseTabs.push(
|
||||
{ name: "Details", key: "details", fields: ["name", "description"] as (keyof TPkiSyncForm)[] },
|
||||
{
|
||||
name: "Certificates",
|
||||
key: "certificates",
|
||||
fields: ["certificateIds"] as (keyof TPkiSyncForm)[]
|
||||
},
|
||||
{ name: "Review", key: "review", fields: [] as (keyof TPkiSyncForm)[] }
|
||||
);
|
||||
|
||||
return baseTabs;
|
||||
};
|
||||
|
||||
export const CreatePkiSyncForm = ({ destination, onComplete, onCancel, initialData }: Props) => {
|
||||
const createPkiSync = useCreatePkiSync();
|
||||
@@ -42,6 +67,7 @@ export const CreatePkiSyncForm = ({ destination, onComplete, onCancel, initialDa
|
||||
const [showConfirmation, setShowConfirmation] = useState(false);
|
||||
|
||||
const [selectedTabIndex, setSelectedTabIndex] = useState(0);
|
||||
const FORM_TABS = getFormTabs(destination);
|
||||
|
||||
const { syncOption } = usePkiSyncOption(destination);
|
||||
|
||||
@@ -55,7 +81,19 @@ export const CreatePkiSyncForm = ({ destination, onComplete, onCancel, initialDa
|
||||
canImportCertificates: false,
|
||||
canRemoveCertificates: false,
|
||||
preserveArn: true,
|
||||
certificateNameSchema: syncOption?.defaultCertificateNameSchema
|
||||
certificateNameSchema: syncOption?.defaultCertificateNameSchema,
|
||||
...((destination === PkiSync.Chef || destination === PkiSync.AwsSecretsManager) && {
|
||||
fieldMappings: {
|
||||
certificate: "certificate",
|
||||
privateKey: "private_key",
|
||||
certificateChain: "certificate_chain",
|
||||
caCertificate: "ca_certificate"
|
||||
}
|
||||
}),
|
||||
...(destination === PkiSync.AwsSecretsManager && {
|
||||
preserveSecretOnRenewal: true,
|
||||
updateExistingCertificates: true
|
||||
})
|
||||
},
|
||||
...initialData
|
||||
} as Partial<TPkiSyncForm>,
|
||||
@@ -167,10 +205,10 @@ export const CreatePkiSyncForm = ({ destination, onComplete, onCancel, initialDa
|
||||
);
|
||||
|
||||
return (
|
||||
<form className={twMerge(isFinalStep && "max-h-[70vh] overflow-y-auto")}>
|
||||
<form className="flex max-h-[70vh] flex-col overflow-hidden">
|
||||
<FormProvider {...formMethods}>
|
||||
<Tab.Group selectedIndex={selectedTabIndex} onChange={setSelectedTabIndex}>
|
||||
<Tab.List className="-pb-1 mb-6 w-full border-b-2 border-mineshaft-600">
|
||||
<Tab.List className="-pb-1 mb-6 w-full flex-shrink-0 border-b-2 border-mineshaft-600">
|
||||
{FORM_TABS.map((tab, index) => (
|
||||
<Tab
|
||||
onClick={async (e) => {
|
||||
@@ -191,11 +229,11 @@ export const CreatePkiSyncForm = ({ destination, onComplete, onCancel, initialDa
|
||||
</Tab>
|
||||
))}
|
||||
</Tab.List>
|
||||
<Tab.Panels>
|
||||
<Tab.Panel>
|
||||
<Tab.Panels className="flex-1 overflow-y-auto">
|
||||
<Tab.Panel className="max-h-full overflow-y-auto">
|
||||
<PkiSyncDestinationFields />
|
||||
</Tab.Panel>
|
||||
<Tab.Panel>
|
||||
<Tab.Panel className="max-h-full overflow-y-auto">
|
||||
<PkiSyncOptionsFields destination={destination} />
|
||||
<Controller
|
||||
control={control}
|
||||
@@ -225,20 +263,25 @@ export const CreatePkiSyncForm = ({ destination, onComplete, onCancel, initialDa
|
||||
}}
|
||||
/>
|
||||
</Tab.Panel>
|
||||
<Tab.Panel>
|
||||
{(destination === PkiSync.Chef || destination === PkiSync.AwsSecretsManager) && (
|
||||
<Tab.Panel className="max-h-full overflow-y-auto">
|
||||
<PkiSyncFieldMappingsFields destination={destination} />
|
||||
</Tab.Panel>
|
||||
)}
|
||||
<Tab.Panel className="max-h-full overflow-y-auto">
|
||||
<PkiSyncDetailsFields />
|
||||
</Tab.Panel>
|
||||
<Tab.Panel>
|
||||
<Tab.Panel className="max-h-full overflow-y-auto">
|
||||
<PkiSyncCertificatesFields />
|
||||
</Tab.Panel>
|
||||
<Tab.Panel>
|
||||
<Tab.Panel className="max-h-full overflow-y-auto">
|
||||
<PkiSyncReviewFields />
|
||||
</Tab.Panel>
|
||||
</Tab.Panels>
|
||||
</Tab.Group>
|
||||
</FormProvider>
|
||||
|
||||
<div className="flex w-full flex-row-reverse justify-between gap-4 pt-4">
|
||||
<div className="mt-4 flex w-full flex-shrink-0 flex-row-reverse justify-between gap-4 border-t border-mineshaft-600 pt-4">
|
||||
<Button onClick={handleNext} colorSchema="secondary">
|
||||
{isFinalStep ? "Create Sync" : "Next"}
|
||||
</Button>
|
||||
|
||||
@@ -11,6 +11,7 @@ import { TPkiSync, useUpdatePkiSync } from "@app/hooks/api/pkiSyncs";
|
||||
import { TUpdatePkiSyncForm, UpdatePkiSyncFormSchema } from "./schemas/pki-sync-schema";
|
||||
import { PkiSyncDestinationFields } from "./PkiSyncDestinationFields";
|
||||
import { PkiSyncDetailsFields } from "./PkiSyncDetailsFields";
|
||||
import { PkiSyncFieldMappingsFields } from "./PkiSyncFieldMappingsFields";
|
||||
import { PkiSyncOptionsFields } from "./PkiSyncOptionsFields";
|
||||
import { PkiSyncSourceFields } from "./PkiSyncSourceFields";
|
||||
|
||||
@@ -66,6 +67,9 @@ export const EditPkiSyncForm = ({ pkiSync, fields, onComplete }: Props) => {
|
||||
case PkiSyncEditFields.Options:
|
||||
Component = <PkiSyncOptionsFields destination={pkiSync.destination} />;
|
||||
break;
|
||||
case PkiSyncEditFields.Mappings:
|
||||
Component = <PkiSyncFieldMappingsFields destination={pkiSync.destination} />;
|
||||
break;
|
||||
case PkiSyncEditFields.Source:
|
||||
Component = <PkiSyncSourceFields />;
|
||||
break;
|
||||
|
||||
@@ -4,7 +4,9 @@ import { PkiSync } from "@app/hooks/api/pkiSyncs";
|
||||
|
||||
import { TPkiSyncForm } from "./schemas/pki-sync-schema";
|
||||
import { AwsCertificateManagerPkiSyncFields } from "./AwsCertificateManagerPkiSyncFields";
|
||||
import { AwsSecretsManagerPkiSyncFields } from "./AwsSecretsManagerPkiSyncFields";
|
||||
import { AzureKeyVaultPkiSyncFields } from "./AzureKeyVaultPkiSyncFields";
|
||||
import { ChefPkiSyncFields } from "./ChefPkiSyncFields";
|
||||
|
||||
export const PkiSyncDestinationFields = () => {
|
||||
const { watch } = useFormContext<TPkiSyncForm>();
|
||||
@@ -16,6 +18,10 @@ export const PkiSyncDestinationFields = () => {
|
||||
return <AzureKeyVaultPkiSyncFields />;
|
||||
case PkiSync.AwsCertificateManager:
|
||||
return <AwsCertificateManagerPkiSyncFields />;
|
||||
case PkiSync.AwsSecretsManager:
|
||||
return <AwsSecretsManagerPkiSyncFields />;
|
||||
case PkiSync.Chef:
|
||||
return <ChefPkiSyncFields />;
|
||||
default:
|
||||
return (
|
||||
<div className="flex items-center justify-center rounded-md border border-red-500 bg-red-100 p-4 text-red-700">
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
import { Controller, useFormContext } from "react-hook-form";
|
||||
|
||||
import { FormControl, Input } from "@app/components/v2";
|
||||
import { PkiSync } from "@app/hooks/api/pkiSyncs";
|
||||
|
||||
import { TPkiSyncForm } from "./schemas/pki-sync-schema";
|
||||
|
||||
type Props = {
|
||||
destination?: PkiSync;
|
||||
};
|
||||
|
||||
export const PkiSyncFieldMappingsFields = ({ destination }: Props) => {
|
||||
const { control, watch } = useFormContext<TPkiSyncForm>();
|
||||
const currentDestination = destination || watch("destination");
|
||||
|
||||
if (currentDestination !== PkiSync.Chef && currentDestination !== PkiSync.AwsSecretsManager) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<p className="mb-4 text-sm text-bunker-300">
|
||||
Configure how certificate fields are mapped to your{" "}
|
||||
{currentDestination === PkiSync.Chef ? "Chef data bag items" : "AWS secrets"}.
|
||||
</p>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Controller
|
||||
control={control}
|
||||
name="syncOptions.fieldMappings.certificate"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
label="Certificate Field"
|
||||
tooltipText={`The field name used to store the certificate content in the ${currentDestination === PkiSync.Chef ? "Chef data bag item" : "AWS secret"}.`}
|
||||
>
|
||||
<Input {...field} placeholder="certificate" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
control={control}
|
||||
name="syncOptions.fieldMappings.privateKey"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
label="Private Key Field"
|
||||
tooltipText={`The field name used to store the private key content in the ${currentDestination === PkiSync.Chef ? "Chef data bag item" : "AWS secret"}.`}
|
||||
>
|
||||
<Input {...field} placeholder="private_key" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
control={control}
|
||||
name="syncOptions.fieldMappings.certificateChain"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
label="Certificate Chain Field"
|
||||
tooltipText={`The field name used to store the certificate chain content in the ${currentDestination === PkiSync.Chef ? "Chef data bag item" : "AWS secret"}.`}
|
||||
>
|
||||
<Input {...field} placeholder="certificate_chain" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
control={control}
|
||||
name="syncOptions.fieldMappings.caCertificate"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
label="CA Certificate Field"
|
||||
tooltipText={`The field name used to store the CA certificate content in the ${currentDestination === PkiSync.Chef ? "Chef data bag item" : "AWS secret"}.`}
|
||||
>
|
||||
<Input {...field} placeholder="ca_certificate" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 rounded-lg border border-mineshaft-600 bg-mineshaft-800 p-4">
|
||||
<h4 className="mb-2 text-sm font-medium text-mineshaft-100">Preview JSON Structure</h4>
|
||||
<pre className="text-xs text-bunker-300">
|
||||
{`{
|
||||
"id": "certificate-item-name",
|
||||
"${watch("syncOptions.fieldMappings.certificate") || "certificate"}": "<certificate-content>",
|
||||
"${watch("syncOptions.fieldMappings.privateKey") || "private_key"}": "<private-key-content>",
|
||||
"${watch("syncOptions.fieldMappings.certificateChain") || "certificate_chain"}": "<certificate-chain-content>",
|
||||
"${watch("syncOptions.fieldMappings.caCertificate") || "ca_certificate"}": "<ca-certificate-content>"
|
||||
}`}
|
||||
</pre>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -95,6 +95,48 @@ export const PkiSyncOptionsFields = ({ destination }: Props) => {
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
control={control}
|
||||
name="syncOptions.includeRootCa"
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl isError={Boolean(error)} errorText={error?.message}>
|
||||
<Switch
|
||||
className="bg-mineshaft-400/80 shadow-inner data-[state=checked]:bg-green/80"
|
||||
id="include-root-ca"
|
||||
thumbClassName="bg-mineshaft-800"
|
||||
onCheckedChange={onChange}
|
||||
isChecked={value}
|
||||
>
|
||||
<p>
|
||||
Include Root CA in Certificate Chain{" "}
|
||||
<Tooltip
|
||||
className="max-w-md"
|
||||
content={
|
||||
<>
|
||||
<p>
|
||||
When enabled, the full certificate chain including the root CA will be
|
||||
synced to the destination.
|
||||
</p>
|
||||
<p className="mt-4">
|
||||
When disabled, the root CA will be excluded from the certificate chain
|
||||
during sync operations, reducing the size of the synced certificate chain.
|
||||
</p>
|
||||
<p className="mt-4">
|
||||
Most applications and services work correctly with intermediate certificates
|
||||
only, as they can validate the trust chain up to a root CA they already
|
||||
trust.
|
||||
</p>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<FontAwesomeIcon icon={faQuestionCircle} size="sm" className="ml-1" />
|
||||
</Tooltip>
|
||||
</p>
|
||||
</Switch>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
{currentDestination === PkiSync.AwsCertificateManager && (
|
||||
<Controller
|
||||
control={control}
|
||||
@@ -183,6 +225,97 @@ export const PkiSyncOptionsFields = ({ destination }: Props) => {
|
||||
/>
|
||||
)}
|
||||
|
||||
{currentDestination === PkiSync.AwsSecretsManager && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="syncOptions.preserveSecretOnRenewal"
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl isError={Boolean(error)} errorText={error?.message}>
|
||||
<Switch
|
||||
className="bg-mineshaft-400/80 shadow-inner data-[state=checked]:bg-green/80"
|
||||
id="preserve-secret-on-renewal"
|
||||
thumbClassName="bg-mineshaft-800"
|
||||
onCheckedChange={onChange}
|
||||
isChecked={value}
|
||||
>
|
||||
<p>
|
||||
Preserve Secret on Renewal{" "}
|
||||
<Tooltip
|
||||
className="max-w-md"
|
||||
content={
|
||||
<>
|
||||
<p>
|
||||
<strong>Only applies to certificate renewals:</strong> When a certificate
|
||||
is renewed in Infisical, this option controls how the renewed certificate
|
||||
is handled in AWS Secrets Manager.
|
||||
</p>
|
||||
<p className="mt-4">
|
||||
When enabled, the renewed certificate will update the existing secret,
|
||||
preserving the same secret name and ARN. This allows consuming services to
|
||||
continue using the same secret reference without requiring updates.
|
||||
</p>
|
||||
<p className="mt-4">
|
||||
When disabled, the renewed certificate will be created as a new secret
|
||||
with a new name, and the old secret will be removed.
|
||||
</p>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<FontAwesomeIcon icon={faQuestionCircle} size="sm" className="ml-1" />
|
||||
</Tooltip>
|
||||
</p>
|
||||
</Switch>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{currentDestination === PkiSync.Chef && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="syncOptions.preserveItemOnRenewal"
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl isError={Boolean(error)} errorText={error?.message}>
|
||||
<Switch
|
||||
className="bg-mineshaft-400/80 shadow-inner data-[state=checked]:bg-green/80"
|
||||
id="preserve-item-on-renewal"
|
||||
thumbClassName="bg-mineshaft-800"
|
||||
onCheckedChange={onChange}
|
||||
isChecked={value}
|
||||
>
|
||||
<p>
|
||||
Preserve Data Bag Item on Renewal{" "}
|
||||
<Tooltip
|
||||
className="max-w-md"
|
||||
content={
|
||||
<>
|
||||
<p>
|
||||
<strong>Only applies to certificate renewals:</strong> When a certificate
|
||||
is renewed in Infisical, this option controls how the renewed certificate
|
||||
is handled in Chef.
|
||||
</p>
|
||||
<p className="mt-4">
|
||||
When enabled, the renewed certificate will update the existing data bag
|
||||
item, preserving the same item name. This allows consuming services to
|
||||
continue using the same data bag item without requiring updates to Chef
|
||||
cookbooks or recipes.
|
||||
</p>
|
||||
<p className="mt-4">
|
||||
When disabled, the renewed certificate will be created as a new data bag
|
||||
item with a new name, and the old item will be removed.
|
||||
</p>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<FontAwesomeIcon icon={faQuestionCircle} size="sm" className="ml-1" />
|
||||
</Tooltip>
|
||||
</p>
|
||||
</Switch>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Controller
|
||||
control={control}
|
||||
name="syncOptions.certificateNameSchema"
|
||||
|
||||
@@ -7,6 +7,7 @@ import { BasePkiSyncSchema } from "./base-pki-sync-schema";
|
||||
const AwsCertificateManagerSyncOptionsSchema = z.object({
|
||||
canImportCertificates: z.boolean().default(false),
|
||||
canRemoveCertificates: z.boolean().default(false),
|
||||
includeRootCa: z.boolean().default(false),
|
||||
preserveArn: z.boolean().default(true),
|
||||
certificateNameSchema: z
|
||||
.string()
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { PkiSync } from "@app/hooks/api/pkiSyncs";
|
||||
|
||||
import { BasePkiSyncSchema } from "./base-pki-sync-schema";
|
||||
|
||||
const AwsSecretsManagerFieldMappingsSchema = z.object({
|
||||
certificate: z.string().min(1, "Certificate field name is required").default("certificate"),
|
||||
privateKey: z.string().min(1, "Private key field name is required").default("private_key"),
|
||||
certificateChain: z
|
||||
.string()
|
||||
.min(1, "Certificate chain field name is required")
|
||||
.default("certificate_chain"),
|
||||
caCertificate: z
|
||||
.string()
|
||||
.min(1, "CA certificate field name is required")
|
||||
.default("ca_certificate")
|
||||
});
|
||||
|
||||
const AwsSecretsManagerSyncOptionsSchema = z.object({
|
||||
canImportCertificates: z.boolean().default(false),
|
||||
canRemoveCertificates: z.boolean().default(true),
|
||||
includeRootCa: z.boolean().default(false),
|
||||
preserveSecretOnRenewal: z.boolean().default(true),
|
||||
updateExistingCertificates: z.boolean().default(true),
|
||||
certificateNameSchema: z
|
||||
.string()
|
||||
.optional()
|
||||
.refine(
|
||||
(val) => {
|
||||
if (!val) return true;
|
||||
|
||||
const allowedOptionalPlaceholders = [
|
||||
"{{environment}}",
|
||||
"{{profileId}}",
|
||||
"{{commonName}}",
|
||||
"{{friendlyName}}"
|
||||
];
|
||||
|
||||
const allowedPlaceholdersRegexPart = ["{{certificateId}}", ...allowedOptionalPlaceholders]
|
||||
.map((p) => p.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&"))
|
||||
.join("|");
|
||||
|
||||
const allowedContentRegex = new RegExp(
|
||||
`^([a-zA-Z0-9_\\-]|${allowedPlaceholdersRegexPart})*$`
|
||||
);
|
||||
const contentIsValid = allowedContentRegex.test(val);
|
||||
|
||||
if (val.trim()) {
|
||||
const certificateIdRegex = /\{\{certificateId\}\}/;
|
||||
const certificateIdIsPresent = certificateIdRegex.test(val);
|
||||
return contentIsValid && certificateIdIsPresent;
|
||||
}
|
||||
|
||||
return contentIsValid;
|
||||
},
|
||||
{
|
||||
message:
|
||||
"Certificate name schema must include exactly one {{certificateId}} placeholder. It can also include {{environment}}, {{profileId}}, {{commonName}}, or {{friendlyName}} placeholders. Only alphanumeric characters (a-z, A-Z, 0-9), hyphens (-), and underscores (_) are allowed besides the placeholders."
|
||||
}
|
||||
),
|
||||
fieldMappings: AwsSecretsManagerFieldMappingsSchema.optional().default({
|
||||
certificate: "certificate",
|
||||
privateKey: "private_key",
|
||||
certificateChain: "certificate_chain",
|
||||
caCertificate: "ca_certificate"
|
||||
})
|
||||
});
|
||||
|
||||
export const AwsSecretsManagerPkiSyncDestinationSchema = BasePkiSyncSchema(
|
||||
AwsSecretsManagerSyncOptionsSchema
|
||||
).merge(
|
||||
z.object({
|
||||
destination: z.literal(PkiSync.AwsSecretsManager),
|
||||
destinationConfig: z.object({
|
||||
region: z.string().min(1, "AWS region is required")
|
||||
})
|
||||
})
|
||||
);
|
||||
|
||||
export const UpdateAwsSecretsManagerPkiSyncDestinationSchema =
|
||||
AwsSecretsManagerPkiSyncDestinationSchema.partial().merge(
|
||||
z.object({
|
||||
name: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Name is required")
|
||||
.max(255, "Name must be less than 255 characters"),
|
||||
destination: z.literal(PkiSync.AwsSecretsManager),
|
||||
connection: z.object({
|
||||
id: z.string().uuid("Invalid connection ID format"),
|
||||
name: z
|
||||
.string()
|
||||
.min(1, "Connection name is required")
|
||||
.max(255, "Connection name must be less than 255 characters")
|
||||
})
|
||||
})
|
||||
);
|
||||
@@ -7,6 +7,7 @@ import { BasePkiSyncSchema } from "./base-pki-sync-schema";
|
||||
const AzureKeyVaultSyncOptionsSchema = z.object({
|
||||
canImportCertificates: z.boolean().default(false),
|
||||
canRemoveCertificates: z.boolean().default(true),
|
||||
includeRootCa: z.boolean().default(false),
|
||||
enableVersioning: z.boolean().default(true),
|
||||
certificateNameSchema: z
|
||||
.string()
|
||||
|
||||
@@ -6,6 +6,7 @@ export const BasePkiSyncSchema = <T extends AnyZodObject | undefined = undefined
|
||||
const baseSyncOptionsSchema = z.object({
|
||||
canImportCertificates: z.boolean().default(false),
|
||||
canRemoveCertificates: z.boolean().default(false),
|
||||
includeRootCa: z.boolean().default(false),
|
||||
certificateNameSchema: z
|
||||
.string()
|
||||
.optional()
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { PkiSync } from "@app/hooks/api/pkiSyncs";
|
||||
|
||||
import { BasePkiSyncSchema } from "./base-pki-sync-schema";
|
||||
|
||||
const ChefFieldMappingsSchema = z.object({
|
||||
certificate: z.string().min(1, "Certificate field name is required").default("certificate"),
|
||||
privateKey: z.string().min(1, "Private key field name is required").default("private_key"),
|
||||
certificateChain: z
|
||||
.string()
|
||||
.min(1, "Certificate chain field name is required")
|
||||
.default("certificate_chain"),
|
||||
caCertificate: z
|
||||
.string()
|
||||
.min(1, "CA certificate field name is required")
|
||||
.default("ca_certificate")
|
||||
});
|
||||
|
||||
const ChefSyncOptionsSchema = z.object({
|
||||
canImportCertificates: z.boolean().default(false),
|
||||
canRemoveCertificates: z.boolean().default(true),
|
||||
includeRootCa: z.boolean().default(false),
|
||||
preserveItemOnRenewal: z.boolean().default(true),
|
||||
updateExistingCertificates: z.boolean().default(true),
|
||||
certificateNameSchema: z
|
||||
.string()
|
||||
.optional()
|
||||
.refine(
|
||||
(val) => {
|
||||
if (!val) return true;
|
||||
|
||||
const allowedOptionalPlaceholders = [
|
||||
"{{environment}}",
|
||||
"{{profileId}}",
|
||||
"{{commonName}}",
|
||||
"{{friendlyName}}"
|
||||
];
|
||||
|
||||
const allowedPlaceholdersRegexPart = ["{{certificateId}}", ...allowedOptionalPlaceholders]
|
||||
.map((p) => p.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&"))
|
||||
.join("|");
|
||||
|
||||
const allowedContentRegex = new RegExp(
|
||||
`^([a-zA-Z0-9_\\-]|${allowedPlaceholdersRegexPart})*$`
|
||||
);
|
||||
const contentIsValid = allowedContentRegex.test(val);
|
||||
|
||||
if (val.trim()) {
|
||||
const certificateIdRegex = /\{\{certificateId\}\}/;
|
||||
const certificateIdIsPresent = certificateIdRegex.test(val);
|
||||
return contentIsValid && certificateIdIsPresent;
|
||||
}
|
||||
|
||||
return contentIsValid;
|
||||
},
|
||||
{
|
||||
message:
|
||||
"Certificate item name schema must include exactly one {{certificateId}} placeholder. It can also include {{environment}}, {{profileId}}, {{commonName}}, or {{friendlyName}} placeholders. Only alphanumeric characters (a-z, A-Z, 0-9), hyphens (-), and underscores (_) are allowed besides the placeholders."
|
||||
}
|
||||
),
|
||||
fieldMappings: ChefFieldMappingsSchema.optional().default({
|
||||
certificate: "certificate",
|
||||
privateKey: "private_key",
|
||||
certificateChain: "certificate_chain",
|
||||
caCertificate: "ca_certificate"
|
||||
})
|
||||
});
|
||||
|
||||
export const ChefPkiSyncDestinationSchema = BasePkiSyncSchema(ChefSyncOptionsSchema).merge(
|
||||
z.object({
|
||||
destination: z.literal(PkiSync.Chef),
|
||||
destinationConfig: z.object({
|
||||
dataBagName: z
|
||||
.string()
|
||||
.min(1, "Data bag name is required")
|
||||
.max(255, "Data bag name must be less than 255 characters")
|
||||
.regex(
|
||||
/^[a-zA-Z0-9_-]+$/,
|
||||
"Data bag name can only contain alphanumeric characters, underscores, and hyphens"
|
||||
)
|
||||
})
|
||||
})
|
||||
);
|
||||
|
||||
export const UpdateChefPkiSyncDestinationSchema = ChefPkiSyncDestinationSchema.partial().merge(
|
||||
z.object({
|
||||
name: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Name is required")
|
||||
.max(255, "Name must be less than 255 characters"),
|
||||
destination: z.literal(PkiSync.Chef),
|
||||
connection: z.object({
|
||||
id: z.string().uuid("Invalid connection ID format"),
|
||||
name: z
|
||||
.string()
|
||||
.min(1, "Connection name is required")
|
||||
.max(255, "Connection name must be less than 255 characters")
|
||||
})
|
||||
})
|
||||
);
|
||||
@@ -4,19 +4,31 @@ import {
|
||||
AwsCertificateManagerPkiSyncDestinationSchema,
|
||||
UpdateAwsCertificateManagerPkiSyncDestinationSchema
|
||||
} from "./aws-certificate-manager-pki-sync-destination-schema";
|
||||
import {
|
||||
AwsSecretsManagerPkiSyncDestinationSchema,
|
||||
UpdateAwsSecretsManagerPkiSyncDestinationSchema
|
||||
} from "./aws-secrets-manager-pki-sync-destination-schema";
|
||||
import {
|
||||
AzureKeyVaultPkiSyncDestinationSchema,
|
||||
UpdateAzureKeyVaultPkiSyncDestinationSchema
|
||||
} from "./azure-key-vault-pki-sync-destination-schema";
|
||||
import {
|
||||
ChefPkiSyncDestinationSchema,
|
||||
UpdateChefPkiSyncDestinationSchema
|
||||
} from "./chef-pki-sync-destination-schema";
|
||||
|
||||
const PkiSyncUnionSchema = z.discriminatedUnion("destination", [
|
||||
AzureKeyVaultPkiSyncDestinationSchema,
|
||||
AwsCertificateManagerPkiSyncDestinationSchema
|
||||
AwsCertificateManagerPkiSyncDestinationSchema,
|
||||
AwsSecretsManagerPkiSyncDestinationSchema,
|
||||
ChefPkiSyncDestinationSchema
|
||||
]);
|
||||
|
||||
const UpdatePkiSyncUnionSchema = z.discriminatedUnion("destination", [
|
||||
UpdateAzureKeyVaultPkiSyncDestinationSchema,
|
||||
UpdateAwsCertificateManagerPkiSyncDestinationSchema
|
||||
UpdateAwsCertificateManagerPkiSyncDestinationSchema,
|
||||
UpdateAwsSecretsManagerPkiSyncDestinationSchema,
|
||||
UpdateChefPkiSyncDestinationSchema
|
||||
]);
|
||||
|
||||
export const PkiSyncFormSchema = PkiSyncUnionSchema;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export enum PkiSyncEditFields {
|
||||
Details = "details",
|
||||
Options = "options",
|
||||
Mappings = "mappings",
|
||||
Source = "source",
|
||||
Destination = "destination"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user