Merge pull request #4596 from Infisical/ENG-3798

Add PKI sync AWS Certificate Manager
This commit is contained in:
carlosmonastyrski
2025-10-06 17:46:11 -03:00
committed by GitHub
46 changed files with 1415 additions and 71 deletions

View File

@@ -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 AwsCertificateManagerPkiSyncFields = () => {
const { control, setValue } = useFormContext<
TPkiSyncForm & { destination: PkiSync.AwsCertificateManager }
>();
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 Certificate Manager certificates should be stored."
>
<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>
)}
/>
</>
);
};

View File

@@ -3,8 +3,8 @@ 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";
import { TPkiSyncForm } from "./schemas";
export const AzureKeyVaultPkiSyncFields = () => {
const { control, setValue } = useFormContext<

View File

@@ -12,12 +12,12 @@ import { useProject } from "@app/context";
import { PKI_SYNC_MAP } from "@app/helpers/pkiSyncs";
import { PkiSync, TPkiSync, useCreatePkiSync, usePkiSyncOption } from "@app/hooks/api/pkiSyncs";
import { PkiSyncFormSchema, TPkiSyncForm } from "./schemas/pki-sync-schema";
import { PkiSyncDestinationFields } from "./PkiSyncDestinationFields";
import { PkiSyncDetailsFields } from "./PkiSyncDetailsFields";
import { PkiSyncOptionsFields } from "./PkiSyncOptionsFields";
import { PkiSyncReviewFields } from "./PkiSyncReviewFields";
import { PkiSyncSourceFields } from "./PkiSyncSourceFields";
import { PkiSyncFormSchema, TPkiSyncForm } from "./schemas";
type Props = {
onComplete: (pkiSync: TPkiSync) => void;

View File

@@ -8,11 +8,11 @@ import { Button, ModalClose } from "@app/components/v2";
import { PKI_SYNC_MAP } from "@app/helpers/pkiSyncs";
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 { PkiSyncOptionsFields } from "./PkiSyncOptionsFields";
import { PkiSyncSourceFields } from "./PkiSyncSourceFields";
import { TPkiSyncForm, UpdatePkiSyncFormSchema } from "./schemas";
type Props = {
onComplete: (pkiSync: TPkiSync) => void;
@@ -24,7 +24,7 @@ export const EditPkiSyncForm = ({ pkiSync, fields, onComplete }: Props) => {
const updatePkiSync = useUpdatePkiSync();
const { name: destinationName } = PKI_SYNC_MAP[pkiSync.destination];
const formMethods = useForm<TPkiSyncForm>({
const formMethods = useForm<TUpdatePkiSyncForm>({
resolver: zodResolver(UpdatePkiSyncFormSchema),
defaultValues: {
...pkiSync,
@@ -33,11 +33,11 @@ export const EditPkiSyncForm = ({ pkiSync, fields, onComplete }: Props) => {
id: pkiSync.connectionId,
name: pkiSync.appConnectionName
}
} as Partial<TPkiSyncForm>,
} as Partial<TUpdatePkiSyncForm>,
reValidateMode: "onChange"
});
const onSubmit = async ({ connection, ...formData }: TPkiSyncForm) => {
const onSubmit = async ({ connection, ...formData }: TUpdatePkiSyncForm) => {
try {
const updatedPkiSync = await updatePkiSync.mutateAsync({
syncId: pkiSync.id,

View File

@@ -10,7 +10,7 @@ import { APP_CONNECTION_MAP } from "@app/helpers/appConnections";
import { PKI_SYNC_CONNECTION_MAP } from "@app/helpers/pkiSyncs";
import { useListAvailableAppConnections } from "@app/hooks/api/appConnections";
import { TPkiSyncForm } from "./schemas";
import { TPkiSyncForm } from "./schemas/pki-sync-schema";
type Props = {
onChange?: VoidFunction;

View File

@@ -2,8 +2,9 @@ import { useFormContext } from "react-hook-form";
import { PkiSync } from "@app/hooks/api/pkiSyncs";
import { TPkiSyncForm } from "./schemas/pki-sync-schema";
import { AwsCertificateManagerPkiSyncFields } from "./AwsCertificateManagerPkiSyncFields";
import { AzureKeyVaultPkiSyncFields } from "./AzureKeyVaultPkiSyncFields";
import { TPkiSyncForm } from "./schemas";
export const PkiSyncDestinationFields = () => {
const { watch } = useFormContext<TPkiSyncForm>();
@@ -13,6 +14,8 @@ export const PkiSyncDestinationFields = () => {
switch (destination) {
case PkiSync.AzureKeyVault:
return <AzureKeyVaultPkiSyncFields />;
case PkiSync.AwsCertificateManager:
return <AwsCertificateManagerPkiSyncFields />;
default:
return (
<div className="flex items-center justify-center rounded-md border border-red-500 bg-red-100 p-4 text-red-700">

View File

@@ -2,7 +2,7 @@ import { Controller, useFormContext } from "react-hook-form";
import { FormControl, Input, TextArea } from "@app/components/v2";
import { TPkiSyncForm } from "./schemas";
import { TPkiSyncForm } from "./schemas/pki-sync-schema";
export const PkiSyncDetailsFields = () => {
const { control } = useFormContext<TPkiSyncForm>();

View File

@@ -5,7 +5,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { FormControl, Input, Switch, Tooltip } from "@app/components/v2";
import { PkiSync, usePkiSyncOption } from "@app/hooks/api/pkiSyncs";
import { TPkiSyncForm } from "../schemas";
import { TPkiSyncForm } from "../schemas/pki-sync-schema";
type Props = {
destination?: PkiSync;

View File

@@ -5,7 +5,7 @@ import { useProject } from "@app/context";
import { PKI_SYNC_MAP } from "@app/helpers/pkiSyncs";
import { useListWorkspacePkiSubscribers } from "@app/hooks/api";
import { TPkiSyncForm } from "./schemas";
import { TPkiSyncForm } from "./schemas/pki-sync-schema";
export const PkiSyncReviewFields = () => {
const { watch } = useFormContext<TPkiSyncForm>();

View File

@@ -4,7 +4,7 @@ import { FilterableSelect, FormControl } from "@app/components/v2";
import { useProject } from "@app/context";
import { useListWorkspacePkiSubscribers } from "@app/hooks/api";
import { TPkiSyncForm } from "./schemas";
import { TPkiSyncForm } from "./schemas/pki-sync-schema";
export const PkiSyncSourceFields = () => {
const { control } = useFormContext<TPkiSyncForm>();

View File

@@ -5,4 +5,4 @@ export { PkiSyncDetailsFields } from "./PkiSyncDetailsFields";
export { PkiSyncOptionsFields } from "./PkiSyncOptionsFields/PkiSyncOptionsFields";
export { PkiSyncReviewFields } from "./PkiSyncReviewFields";
export { PkiSyncSourceFields } from "./PkiSyncSourceFields";
export { PkiSyncFormSchema, type TPkiSyncForm } from "./schemas";
export { PkiSyncFormSchema, type TPkiSyncForm } from "./schemas/pki-sync-schema";

View File

@@ -0,0 +1,65 @@
import { z } from "zod";
import { PkiSync } from "@app/hooks/api/pkiSyncs";
import { BasePkiSyncSchema } from "./base-pki-sync-schema";
const AwsCertificateManagerSyncOptionsSchema = z.object({
canImportCertificates: z.boolean().default(false),
canRemoveCertificates: z.boolean().default(false),
certificateNameSchema: z
.string()
.optional()
.refine(
(val) => {
// For AWS Certificate Manager, {{certificateId}} is always required if certificateNameSchema is provided
if (!val) return true;
if (!val.includes("{{certificateId}}")) {
return false;
}
const allowedOptionalPlaceholders = ["{{environment}}"];
const allowedPlaceholdersRegexPart = ["{{certificateId}}", ...allowedOptionalPlaceholders]
.map((p) => p.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&"))
.join("|");
const allowedContentRegex = new RegExp(
`^([a-zA-Z0-9_\\-/]|${allowedPlaceholdersRegexPart})*$`
);
return allowedContentRegex.test(val);
},
{
message:
"Certificate name schema must include {{certificateId}} placeholder for AWS Certificate Manager."
}
)
});
export const AwsCertificateManagerPkiSyncDestinationSchema = BasePkiSyncSchema(
AwsCertificateManagerSyncOptionsSchema
).merge(
z.object({
destination: z.literal(PkiSync.AwsCertificateManager),
destinationConfig: z.object({
region: z.string().min(1, "AWS region is required")
})
})
);
export const UpdateAwsCertificateManagerPkiSyncDestinationSchema =
AwsCertificateManagerPkiSyncDestinationSchema.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.AwsCertificateManager),
connection: z.object({
id: z.string().uuid("Invalid connection ID format"),
name: z.string().max(255, "Connection name must be less than 255 characters")
})
})
);

View File

@@ -0,0 +1,33 @@
import { z } from "zod";
import { PkiSync } from "@app/hooks/api/pkiSyncs";
import { BasePkiSyncSchema } from "./base-pki-sync-schema";
export const AzureKeyVaultPkiSyncDestinationSchema = BasePkiSyncSchema().merge(
z.object({
destination: z.literal(PkiSync.AzureKeyVault),
destinationConfig: z.object({
vaultBaseUrl: z.string().min(1, "Vault base URL is required").url("Valid URL is required")
})
})
);
export const UpdateAzureKeyVaultPkiSyncDestinationSchema =
AzureKeyVaultPkiSyncDestinationSchema.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.AzureKeyVault),
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")
})
})
);

View File

@@ -1,25 +1,9 @@
import { z } from "zod";
import { AnyZodObject, z } from "zod";
import { PkiSync } from "@app/hooks/api/pkiSyncs";
export const PkiSyncFormSchema = z.object({
name: z
.string()
.trim()
.min(1, "Name is required")
.max(255, "Name must be less than 255 characters"),
description: z.string().optional(),
destination: z.nativeEnum(PkiSync),
isAutoSyncEnabled: z.boolean().default(true),
subscriberId: z.string().min(1, "PKI Subscriber is required"),
connection: z.object({
id: z.string().uuid("Invalid connection ID format"),
name: z.string().max(255, "Connection name must be less than 255 characters")
}),
destinationConfig: z.object({
vaultBaseUrl: z.string().url("Valid URL is required")
}),
syncOptions: z.object({
export const BasePkiSyncSchema = <T extends AnyZodObject | undefined = undefined>(
additionalSyncOptions?: T
) => {
const baseSyncOptionsSchema = z.object({
canImportCertificates: z.boolean().default(false),
canRemoveCertificates: z.boolean().default(false),
certificateNameSchema: z
@@ -53,22 +37,27 @@ export const PkiSyncFormSchema = z.object({
"Certificate name schema must include exactly one {{certificateId}} placeholder. It can also include {{environment}} placeholders. Only alphanumeric characters (a-z, A-Z, 0-9), dashes (-), underscores (_), and slashes (/) are allowed besides the placeholders."
}
)
})
});
});
export type TPkiSyncForm = z.infer<typeof PkiSyncFormSchema>;
const syncOptionsSchema = additionalSyncOptions
? baseSyncOptionsSchema.merge(additionalSyncOptions)
: (baseSyncOptionsSchema as T extends AnyZodObject
? z.ZodObject<z.objectUtil.MergeShapes<typeof baseSyncOptionsSchema.shape, T["shape"]>>
: typeof baseSyncOptionsSchema);
export const UpdatePkiSyncFormSchema = PkiSyncFormSchema.partial().merge(
z.object({
return z.object({
name: z
.string()
.trim()
.min(1, "Name is required")
.max(255, "Name must be less than 255 characters"),
destination: z.nativeEnum(PkiSync),
description: z.string().optional(),
isAutoSyncEnabled: z.boolean().default(true),
subscriberId: z.string().min(1, "PKI Subscriber is required"),
connection: z.object({
id: z.string().uuid("Invalid connection ID format"),
name: z.string().max(255, "Connection name must be less than 255 characters")
})
})
);
}),
syncOptions: syncOptionsSchema
});
};

View File

@@ -0,0 +1,28 @@
import { z } from "zod";
import {
AwsCertificateManagerPkiSyncDestinationSchema,
UpdateAwsCertificateManagerPkiSyncDestinationSchema
} from "./aws-certificate-manager-pki-sync-destination-schema";
import {
AzureKeyVaultPkiSyncDestinationSchema,
UpdateAzureKeyVaultPkiSyncDestinationSchema
} from "./azure-key-vault-pki-sync-destination-schema";
const PkiSyncUnionSchema = z.discriminatedUnion("destination", [
AzureKeyVaultPkiSyncDestinationSchema,
AwsCertificateManagerPkiSyncDestinationSchema
]);
const UpdatePkiSyncUnionSchema = z.discriminatedUnion("destination", [
UpdateAzureKeyVaultPkiSyncDestinationSchema,
UpdateAwsCertificateManagerPkiSyncDestinationSchema
]);
export const PkiSyncFormSchema = PkiSyncUnionSchema;
export const UpdatePkiSyncFormSchema = UpdatePkiSyncUnionSchema;
export type TPkiSyncForm = z.infer<typeof PkiSyncFormSchema>;
export type TUpdatePkiSyncForm = z.infer<typeof UpdatePkiSyncFormSchema>;

View File

@@ -11,9 +11,14 @@ export const PKI_SYNC_MAP: Record<
[PkiSync.AzureKeyVault]: {
name: "Azure Key Vault",
image: "Microsoft Azure.png"
},
[PkiSync.AwsCertificateManager]: {
name: "AWS Certificate Manager",
image: "Amazon Web Services.png"
}
};
export const PKI_SYNC_CONNECTION_MAP: Record<PkiSync, AppConnection> = {
[PkiSync.AzureKeyVault]: AppConnection.AzureKeyVault
[PkiSync.AzureKeyVault]: AppConnection.AzureKeyVault,
[PkiSync.AwsCertificateManager]: AppConnection.AWS
};

View File

@@ -1,5 +1,6 @@
export enum PkiSync {
AzureKeyVault = "azure-key-vault"
AzureKeyVault = "azure-key-vault",
AwsCertificateManager = "aws-certificate-manager"
}
export enum PkiSyncStatus {

View File

@@ -0,0 +1,16 @@
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import { PkiSync } from "../enums";
import { TRootPkiSync } from "./common";
export type TAwsCertificateManagerPkiSync = TRootPkiSync & {
destination: PkiSync.AwsCertificateManager;
destinationConfig: {
region: string;
};
connection: {
app: AppConnection.AWS;
name: string;
id: string;
};
};

View File

@@ -1,6 +1,6 @@
import { PkiSync } from "@app/hooks/api/pkiSyncs";
import { DiscriminativePick } from "@app/types";
import { TAwsCertificateManagerPkiSync } from "./aws-certificate-manager-sync";
import { TAzureKeyVaultPkiSync } from "./azure-key-vault-sync";
export type TPkiSyncOption = {
@@ -16,27 +16,38 @@ export type TPkiSyncOption = {
minCertificateNameLength?: number;
};
export type TPkiSync = TAzureKeyVaultPkiSync;
export type TPkiSync = TAzureKeyVaultPkiSync | TAwsCertificateManagerPkiSync;
export type TListPkiSyncs = { pkiSyncs: TPkiSync[] };
export type TListPkiSyncOptions = { pkiSyncOptions: TPkiSyncOption[] };
export type TCreatePkiSyncDTO = DiscriminativePick<
TPkiSync,
| "name"
| "destinationConfig"
| "description"
| "connectionId"
| "syncOptions"
| "destination"
| "isAutoSyncEnabled"
> & { subscriberId?: string; projectId: string };
type TCreatePkiSyncDTOBase = {
name: string;
description?: string;
connectionId: string;
syncOptions: {
canImportCertificates: boolean;
canRemoveCertificates: boolean;
certificateNamePrefix?: string;
certificateNameSchema?: string;
};
isAutoSyncEnabled: boolean;
subscriberId?: string;
projectId: string;
};
export type TCreatePkiSyncDTO = TCreatePkiSyncDTOBase & {
destination: PkiSync;
destinationConfig: {
vaultBaseUrl?: string;
region?: string;
};
};
export type TUpdatePkiSyncDTO = Partial<Omit<TCreatePkiSyncDTO, "projectId">> & {
syncId: string;
projectId: string;
destination: PkiSync;
};
export type TDeletePkiSyncDTO = {
@@ -63,4 +74,6 @@ export type TTriggerPkiSyncRemoveCertificatesDTO = {
projectId: string;
};
export * from "./aws-certificate-manager-sync";
export * from "./azure-key-vault-sync";
export * from "./common";