feat(secrey-sync): Hashicorp Vault Secret Sync (and minor app connection fixes)

This commit is contained in:
x
2025-04-24 18:54:05 -04:00
parent 418ac20f91
commit 92084ccd47
38 changed files with 515 additions and 25 deletions
@@ -0,0 +1,85 @@
import { Controller, useFormContext, useWatch } from "react-hook-form";
import { SingleValue } from "react-select";
import { faCircleInfo } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { SecretSyncConnectionField } from "@app/components/secret-syncs/forms/SecretSyncConnectionField";
import { FilterableSelect, FormControl, Input, Tooltip } from "@app/components/v2";
import { useHCVaultConnectionListMounts } from "@app/hooks/api/appConnections/hc-vault";
import { SecretSync } from "@app/hooks/api/secretSyncs";
import { TSecretSyncForm } from "../schemas";
export const HCVaultSyncFields = () => {
const { control, setValue } = useFormContext<
TSecretSyncForm & { destination: SecretSync.HCVault }
>();
const connectionId = useWatch({ name: "connection.id", control });
const { data: mounts, isLoading: isMountsLoading } = useHCVaultConnectionListMounts(
connectionId,
{
enabled: Boolean(connectionId)
}
);
return (
<>
<SecretSyncConnectionField
onChange={() => {
setValue("destinationConfig.mount", "");
}}
/>
<Controller
name="destinationConfig.mount"
control={control}
render={({ field: { onChange }, fieldState: { error } }) => (
<FormControl
isError={Boolean(error)}
errorText={error?.message}
label="Secrets Engine Mount"
helperText={
<Tooltip
className="max-w-md"
content="Ensure the Secrets Engine mount exists and that your App Role / Access Token has permission to access it. Infisical only supports version 2 KV Secrets Engine mounts."
>
<div>
<span>Don&#39;t see the mount you&#39;re looking for?</span>{" "}
<FontAwesomeIcon icon={faCircleInfo} className="text-mineshaft-400" />
</div>
</Tooltip>
}
>
<FilterableSelect
menuPlacement="top"
isLoading={isMountsLoading && Boolean(connectionId)}
isDisabled={!connectionId}
onChange={(option) =>
onChange((option as SingleValue<{ value: string }>)?.value ?? null)
}
options={mounts?.map((v) => ({ label: v, value: v }))}
placeholder="Select a Secrets Engine Mount..."
/>
</FormControl>
)}
/>
<Controller
name="destinationConfig.path"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
tooltipClassName="max-w-sm"
tooltipText="The Secrets Engine mount path where secrets should be synced to."
isError={Boolean(error)}
errorText={error?.message}
label="Path"
>
<Input value={value} onChange={onChange} placeholder="dev/example" />
</FormControl>
)}
/>
</>
);
};
@@ -11,6 +11,7 @@ import { CamundaSyncFields } from "./CamundaSyncFields";
import { DatabricksSyncFields } from "./DatabricksSyncFields";
import { GcpSyncFields } from "./GcpSyncFields";
import { GitHubSyncFields } from "./GitHubSyncFields";
import { HCVaultSyncFields } from "./HCVaultSyncFields";
import { HumanitecSyncFields } from "./HumanitecSyncFields";
import { TerraformCloudSyncFields } from "./TerraformCloudSyncFields";
import { VercelSyncFields } from "./VercelSyncFields";
@@ -46,6 +47,8 @@ export const SecretSyncDestinationFields = () => {
return <VercelSyncFields />;
case SecretSync.Windmill:
return <WindmillSyncFields />;
case SecretSync.HCVault:
return <HCVaultSyncFields />;
default:
throw new Error(`Unhandled Destination Config Field: ${destination}`);
}
@@ -43,6 +43,7 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => {
case SecretSync.Camunda:
case SecretSync.Vercel:
case SecretSync.Windmill:
case SecretSync.HCVault:
AdditionalSyncOptionsFieldsComponent = null;
break;
default:
@@ -0,0 +1,18 @@
import { useFormContext } from "react-hook-form";
import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas";
import { GenericFieldLabel } from "@app/components/v2";
import { SecretSync } from "@app/hooks/api/secretSyncs";
export const HCVaultSyncReviewFields = () => {
const { watch } = useFormContext<TSecretSyncForm & { destination: SecretSync.HCVault }>();
const mount = watch("destinationConfig.mount");
const path = watch("destinationConfig.path");
return (
<>
<GenericFieldLabel label="Secrets Engine Mount">{mount}</GenericFieldLabel>
<GenericFieldLabel label="Path">{path}</GenericFieldLabel>
</>
);
};
@@ -21,6 +21,7 @@ import { CamundaSyncReviewFields } from "./CamundaSyncReviewFields";
import { DatabricksSyncReviewFields } from "./DatabricksSyncReviewFields";
import { GcpSyncReviewFields } from "./GcpSyncReviewFields";
import { GitHubSyncReviewFields } from "./GitHubSyncReviewFields";
import { HCVaultSyncReviewFields } from "./HCVaultSyncReviewFields";
import { HumanitecSyncReviewFields } from "./HumanitecSyncReviewFields";
import { TerraformCloudSyncReviewFields } from "./TerraformCloudSyncReviewFields";
import { VercelSyncReviewFields } from "./VercelSyncReviewFields";
@@ -88,6 +89,9 @@ export const SecretSyncReviewFields = () => {
case SecretSync.Windmill:
DestinationFieldsComponent = <WindmillSyncReviewFields />;
break;
case SecretSync.HCVault:
DestinationFieldsComponent = <HCVaultSyncReviewFields />;
break;
default:
throw new Error(`Unhandled Destination Review Fields: ${destination}`);
}
@@ -0,0 +1,22 @@
import { z } from "zod";
import { BaseSecretSyncSchema } from "@app/components/secret-syncs/forms/schemas/base-secret-sync-schema";
import { SecretSync } from "@app/hooks/api/secretSyncs";
export const HCVaultSyncDestinationSchema = BaseSecretSyncSchema().merge(
z.object({
destination: z.literal(SecretSync.HCVault),
destinationConfig: z.object({
mount: z.string().trim().min(1, "Secrets Engine Mount required"),
path: z
.string()
.trim()
.min(1, "Path required")
.transform((val) => val.trim().replace(/^\/+|\/+$/g, "")) // removes leading/trailing slashes
.refine((val) => /^([a-zA-Z0-9._-]+\/)*[a-zA-Z0-9._-]+$/.test(val), {
message:
"Invalid Vault path format. Use alphanumerics, dots, dashes, underscores, and single slashes between segments."
})
})
})
);
@@ -8,6 +8,7 @@ import { CamundaSyncDestinationSchema } from "./camunda-sync-destination-schema"
import { DatabricksSyncDestinationSchema } from "./databricks-sync-destination-schema";
import { GcpSyncDestinationSchema } from "./gcp-sync-destination-schema";
import { GitHubSyncDestinationSchema } from "./github-sync-destination-schema";
import { HCVaultSyncDestinationSchema } from "./hc-vault-sync-destination-schema";
import { HumanitecSyncDestinationSchema } from "./humanitec-sync-destination-schema";
import { TerraformCloudSyncDestinationSchema } from "./terraform-cloud-destination-schema";
import { VercelSyncDestinationSchema } from "./vercel-sync-destination-schema";
@@ -25,7 +26,8 @@ const SecretSyncUnionSchema = z.discriminatedUnion("destination", [
TerraformCloudSyncDestinationSchema,
CamundaSyncDestinationSchema,
VercelSyncDestinationSchema,
WindmillSyncDestinationSchema
WindmillSyncDestinationSchema,
HCVaultSyncDestinationSchema
]);
export const SecretSyncFormSchema = SecretSyncUnionSchema;