PKI Syncs improvements

This commit is contained in:
Carlos Monastyrski
2025-11-03 08:25:27 -03:00
parent 04767a819e
commit cc52c2c713
30 changed files with 835 additions and 303 deletions
@@ -11,21 +11,24 @@ type Props = {
isOpen: boolean;
onOpenChange: (isOpen: boolean) => void;
selectSync?: PkiSync | null;
initialData?: any;
};
type ContentProps = {
onComplete: (pkiSync: TPkiSync) => void;
selectedSync: PkiSync | null;
setSelectedSync: (selectedSync: PkiSync | null) => void;
initialData?: any;
};
const Content = ({ onComplete, setSelectedSync, selectedSync }: ContentProps) => {
const Content = ({ onComplete, setSelectedSync, selectedSync, initialData }: ContentProps) => {
if (selectedSync) {
return (
<CreatePkiSyncForm
onComplete={onComplete}
onCancel={() => setSelectedSync(null)}
destination={selectedSync}
initialData={initialData}
/>
);
}
@@ -33,7 +36,12 @@ const Content = ({ onComplete, setSelectedSync, selectedSync }: ContentProps) =>
return <PkiSyncSelect onSelect={setSelectedSync} />;
};
export const CreatePkiSyncModal = ({ onOpenChange, selectSync = null, ...props }: Props) => {
export const CreatePkiSyncModal = ({
onOpenChange,
selectSync = null,
initialData,
...props
}: Props) => {
const [selectedSync, setSelectedSync] = useState<PkiSync | null>(selectSync);
useEffect(() => {
@@ -69,6 +77,7 @@ export const CreatePkiSyncModal = ({ onOpenChange, selectSync = null, ...props }
}}
selectedSync={selectedSync}
setSelectedSync={setSelectedSync}
initialData={initialData}
/>
</ModalContent>
</Modal>
@@ -23,6 +23,7 @@ type Props = {
onComplete: (pkiSync: TPkiSync) => void;
destination: PkiSync;
onCancel: () => void;
initialData?: any;
};
const FORM_TABS: { name: string; key: string; fields: (keyof TPkiSyncForm)[] }[] = [
@@ -33,7 +34,7 @@ const FORM_TABS: { name: string; key: string; fields: (keyof TPkiSyncForm)[] }[]
{ name: "Review", key: "review", fields: [] }
];
export const CreatePkiSyncForm = ({ destination, onComplete, onCancel }: Props) => {
export const CreatePkiSyncForm = ({ destination, onComplete, onCancel, initialData }: Props) => {
const createPkiSync = useCreatePkiSync();
const { currentProject } = useProject();
const { name: destinationName } = PKI_SYNC_MAP[destination];
@@ -55,7 +56,8 @@ export const CreatePkiSyncForm = ({ destination, onComplete, onCancel }: Props)
canRemoveCertificates: false,
preserveArn: true,
certificateNameSchema: syncOption?.defaultCertificateNameSchema
}
},
...initialData
} as Partial<TPkiSyncForm>,
reValidateMode: "onChange"
});
@@ -27,12 +27,16 @@ export const EditPkiSyncForm = ({ pkiSync, fields, onComplete }: Props) => {
const formMethods = useForm<TUpdatePkiSyncForm>({
resolver: zodResolver(UpdatePkiSyncFormSchema),
defaultValues: {
...pkiSync,
name: pkiSync.name,
destination: pkiSync.destination,
description: pkiSync.description ?? "",
connection: {
id: pkiSync.connectionId,
name: pkiSync.appConnectionName
}
},
syncOptions: pkiSync.syncOptions,
destinationConfig: pkiSync.destinationConfig,
isAutoSyncEnabled: pkiSync.isAutoSyncEnabled
} as Partial<TUpdatePkiSyncForm>,
reValidateMode: "onChange"
});
@@ -1,14 +1,18 @@
import { Controller, useFormContext } from "react-hook-form";
import { SingleValue } from "react-select";
import { faInfoCircle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link } from "@tanstack/react-router";
import { useRouterState } from "@tanstack/react-router";
import { AppConnectionOption } from "@app/components/app-connections";
import { FilterableSelect, FormControl } from "@app/components/v2";
import { ProjectPermissionSub, useProject, useProjectPermission } from "@app/context";
import { ProjectPermissionAppConnectionActions } from "@app/context/ProjectPermissionContext/types";
import { APP_CONNECTION_MAP } from "@app/helpers/appConnections";
import { PKI_SYNC_CONNECTION_MAP } from "@app/helpers/pkiSyncs";
import { usePopUp } from "@app/hooks";
import { useListAvailableAppConnections } from "@app/hooks/api/appConnections";
import { AddAppConnectionModal } from "@app/pages/organization/AppConnections/AppConnectionsPage/components";
import { TPkiSyncForm } from "./schemas/pki-sync-schema";
@@ -18,12 +22,30 @@ type Props = {
export const PkiSyncConnectionField = ({ onChange: callback }: Props) => {
const { permission } = useProjectPermission();
const { control, watch } = useFormContext<TPkiSyncForm>();
const { currentProject } = useProject();
const { control, watch, setValue } = useFormContext<TPkiSyncForm>();
const { popUp, handlePopUpToggle, handlePopUpOpen } = usePopUp(["addConnection"] as const);
const destination = watch("destination");
const app = PKI_SYNC_CONNECTION_MAP[destination];
const { currentProject } = useProject();
const {
location: { pathname }
} = useRouterState();
const getPkiSyncReturnUrl = () => {
if (pathname.includes("selectedTab=secret-syncs")) {
return pathname.replace("selectedTab=secret-syncs", "selectedTab=pki-syncs");
}
if (!pathname.includes("selectedTab=")) {
const separator = pathname.includes("?") ? "&" : "?";
return `${pathname}${separator}selectedTab=pki-syncs`;
}
return pathname;
};
const { data: availableConnections, isPending } = useListAvailableAppConnections(
app,
currentProject.id
@@ -47,6 +69,7 @@ export const PkiSyncConnectionField = ({ onChange: callback }: Props) => {
<Controller
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
tooltipText="App Connections can be created from the Organization Settings page."
isError={Boolean(error)}
errorText={error?.message}
label={`${connectionName} Connection`}
@@ -54,36 +77,54 @@ export const PkiSyncConnectionField = ({ onChange: callback }: Props) => {
<FilterableSelect
value={value}
onChange={(newValue) => {
if ((newValue as SingleValue<{ id: string; name: string }>)?.id === "_create") {
handlePopUpOpen("addConnection");
onChange(null);
const formData = { ...watch(), returnUrl: getPkiSyncReturnUrl() };
localStorage.setItem("pkiSyncFormData", JSON.stringify(formData));
if (callback) callback();
return;
}
onChange(newValue);
if (callback) callback();
}}
isLoading={isPending}
options={availableConnections}
options={[
...(canCreateConnection ? [{ id: "_create", name: "Create Connection" }] : []),
...(availableConnections ?? [])
]}
placeholder="Select connection..."
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.id}
components={{ Option: AppConnectionOption }}
/>
</FormControl>
)}
control={control}
name="connection"
/>
{availableConnections?.length === 0 && (
{!isPending && !availableConnections?.length && !canCreateConnection && (
<p className="-mt-2.5 mb-2.5 text-xs text-yellow">
<FontAwesomeIcon className="mr-1" size="xs" icon={faInfoCircle} />
{canCreateConnection ? (
<>
You do not have access to any {appName} Connections. Create one from the{" "}
<Link to="/organization/app-connections" className="underline">
App Connections
</Link>{" "}
page.
</>
) : (
`You do not have access to any ${appName} Connections. Contact an admin to create one.`
)}
You do not have access to any {appName} Connections. Contact an admin to create one.
</p>
)}
<AddAppConnectionModal
isOpen={popUp.addConnection.isOpen}
onOpenChange={(isOpen) => {
localStorage.removeItem("pkiSyncFormData");
handlePopUpToggle("addConnection", isOpen);
}}
projectType={currentProject.type}
projectId={currentProject.id}
app={app}
onComplete={(connection) => {
if (connection) {
setValue("connection", connection);
}
}}
/>
</>
);
};
@@ -71,7 +71,7 @@ export const PkiSyncOptionsFields = ({ destination }: Props) => {
isChecked={value}
>
<p>
Enable Inactive Certificate Removal{" "}
Enable Removal of Active/Revoked Certificates{" "}
<Tooltip
className="max-w-md"
content={
@@ -138,6 +138,51 @@ export const PkiSyncOptionsFields = ({ destination }: Props) => {
/>
)}
{currentDestination === PkiSync.AzureKeyVault && (
<Controller
control={control}
name="syncOptions.preserveVersion"
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-version"
thumbClassName="bg-mineshaft-800"
onCheckedChange={onChange}
isChecked={value}
>
<p>
Preserve Version on Renewal{" "}
<Tooltip
className="max-w-md"
content={
<>
<p>
When enabled, Infisical will create a new version of the existing
certificate in Azure Key Vault during certificate renewal syncs,
preserving the certificate name.
</p>
<p className="mt-4">
This allows consuming services to continue using the same certificate name
while automatically using the latest version without requiring manual
updates.
</p>
<p className="mt-4">
When disabled, new certificates will be created with new names, and old
certificates will be removed.
</p>
</>
}
>
<FontAwesomeIcon icon={faQuestionCircle} size="sm" className="ml-1" />
</Tooltip>
</p>
</Switch>
</FormControl>
)}
/>
)}
<Controller
control={control}
name="syncOptions.certificateNameSchema"
@@ -4,7 +4,46 @@ import { PkiSync } from "@app/hooks/api/pkiSyncs";
import { BasePkiSyncSchema } from "./base-pki-sync-schema";
export const AzureKeyVaultPkiSyncDestinationSchema = BasePkiSyncSchema().merge(
const AzureKeyVaultSyncOptionsSchema = z.object({
canImportCertificates: z.boolean().default(false),
canRemoveCertificates: z.boolean().default(true),
preserveVersion: z.boolean().default(true),
certificateNameSchema: z
.string()
.optional()
.refine(
(val) => {
if (!val) return true;
const allowedOptionalPlaceholders = ["{{environment}}"];
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}} placeholders. Only alphanumeric characters (a-z, A-Z, 0-9), dashes (-), underscores (_), and slashes (/) are allowed besides the placeholders."
}
)
});
export const AzureKeyVaultPkiSyncDestinationSchema = BasePkiSyncSchema(
AzureKeyVaultSyncOptionsSchema
).merge(
z.object({
destination: z.literal(PkiSync.AzureKeyVault),
destinationConfig: z.object({