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;
+2 -2
View File
@@ -11,14 +11,14 @@ import {
DatabricksConnectionMethod,
GcpConnectionMethod,
GitHubConnectionMethod,
HCVaultConnectionMethod,
HumanitecConnectionMethod,
MsSqlConnectionMethod,
PostgresConnectionMethod,
TAppConnection,
TerraformCloudConnectionMethod,
VercelConnectionMethod,
WindmillConnectionMethod,
HCVaultConnectionMethod
WindmillConnectionMethod
} from "@app/hooks/api/appConnections/types";
export const APP_CONNECTION_MAP: Record<
+6 -1
View File
@@ -39,6 +39,10 @@ export const SECRET_SYNC_MAP: Record<SecretSync, { name: string; image: string }
[SecretSync.Windmill]: {
name: "Windmill",
image: "Windmill.png"
},
[SecretSync.HCVault]: {
name: "Hashicorp Vault",
image: "Vault.png"
}
};
@@ -54,7 +58,8 @@ export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = {
[SecretSync.TerraformCloud]: AppConnection.TerraformCloud,
[SecretSync.Camunda]: AppConnection.Camunda,
[SecretSync.Vercel]: AppConnection.Vercel,
[SecretSync.Windmill]: AppConnection.Windmill
[SecretSync.Windmill]: AppConnection.Windmill,
[SecretSync.HCVault]: AppConnection.HCVault
};
export const SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP: Record<
@@ -1,2 +1 @@
export * from "./queries";
export * from "./types";
@@ -10,7 +10,7 @@ const hcVaultConnectionKeys = {
[...hcVaultConnectionKeys.all, "mounts", connectionId] as const
};
export const useHCVaultConnectionListWorkspaces = (
export const useHCVaultConnectionListMounts = (
connectionId: string,
options?: Omit<
UseQueryOptions<
@@ -26,7 +26,7 @@ export const useHCVaultConnectionListWorkspaces = (
queryKey: hcVaultConnectionKeys.listMounts(connectionId),
queryFn: async () => {
const { data } = await apiRequest.get<string[]>(
`/api/v1/app-connections/hc-vault/${connectionId}/mounts`
`/api/v1/app-connections/hashicorp-vault/${connectionId}/mounts`
);
return data;
@@ -24,13 +24,13 @@ export * from "./camunda-connection";
export * from "./databricks-connection";
export * from "./gcp-connection";
export * from "./github-connection";
export * from "./hc-vault-connection";
export * from "./humanitec-connection";
export * from "./mssql-connection";
export * from "./postgres-connection";
export * from "./terraform-cloud-connection";
export * from "./vercel-connection";
export * from "./windmill-connection";
export * from "./hc-vault-connection";
export type TAppConnection =
| TAwsConnection
+2 -1
View File
@@ -10,7 +10,8 @@ export enum SecretSync {
TerraformCloud = "terraform-cloud",
Camunda = "camunda",
Vercel = "vercel",
Windmill = "windmill"
Windmill = "windmill",
HCVault = "hashicorp-vault"
}
export enum SecretSyncStatus {
@@ -0,0 +1,16 @@
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import { SecretSync } from "@app/hooks/api/secretSyncs";
import { TRootSecretSync } from "@app/hooks/api/secretSyncs/types/root-sync";
export type THCVaultSync = TRootSecretSync & {
destination: SecretSync.HCVault;
destinationConfig: {
mount: string;
path: string;
};
connection: {
app: AppConnection.HCVault;
name: string;
id: string;
};
};
@@ -9,6 +9,7 @@ import { TCamundaSync } from "./camunda-sync";
import { TDatabricksSync } from "./databricks-sync";
import { TGcpSync } from "./gcp-sync";
import { TGitHubSync } from "./github-sync";
import { THCVaultSync } from "./hc-vault-sync";
import { THumanitecSync } from "./humanitec-sync";
import { TTerraformCloudSync } from "./terraform-cloud-sync";
import { TVercelSync } from "./vercel-sync";
@@ -32,7 +33,8 @@ export type TSecretSync =
| TTerraformCloudSync
| TCamundaSync
| TVercelSync
| TWindmillSync;
| TWindmillSync
| THCVaultSync;
export type TListSecretSyncs = { secretSyncs: TSecretSync[] };
@@ -17,13 +17,13 @@ import { CamundaConnectionForm } from "./CamundaConnectionForm";
import { DatabricksConnectionForm } from "./DatabricksConnectionForm";
import { GcpConnectionForm } from "./GcpConnectionForm";
import { GitHubConnectionForm } from "./GitHubConnectionForm";
import { HCVaultConnectionForm } from "./HCVaultConnectionForm";
import { HumanitecConnectionForm } from "./HumanitecConnectionForm";
import { MsSqlConnectionForm } from "./MsSqlConnectionForm";
import { PostgresConnectionForm } from "./PostgresConnectionForm";
import { TerraformCloudConnectionForm } from "./TerraformCloudConnectionForm";
import { VercelConnectionForm } from "./VercelConnectionForm";
import { WindmillConnectionForm } from "./WindmillConnectionForm";
import { HCVaultConnectionForm } from "./HCVaultConnectionForm";
type FormProps = {
onComplete: (appConnection: TAppConnection) => void;
@@ -0,0 +1,14 @@
import { THCVaultSync } from "@app/hooks/api/secretSyncs/types/hc-vault-sync";
import { getSecretSyncDestinationColValues } from "../helpers";
import { SecretSyncTableCell } from "../SecretSyncTableCell";
type Props = {
secretSync: THCVaultSync;
};
export const HCVaultSyncDestinationCol = ({ secretSync }: Props) => {
const { primaryText, secondaryText } = getSecretSyncDestinationColValues(secretSync);
return <SecretSyncTableCell primaryText={primaryText} secondaryText={secondaryText} />;
};
@@ -8,6 +8,7 @@ import { CamundaSyncDestinationCol } from "./CamundaSyncDestinationCol";
import { DatabricksSyncDestinationCol } from "./DatabricksSyncDestinationCol";
import { GcpSyncDestinationCol } from "./GcpSyncDestinationCol";
import { GitHubSyncDestinationCol } from "./GitHubSyncDestinationCol";
import { HCVaultSyncDestinationCol } from "./HCVaultSyncDestinationCol";
import { HumanitecSyncDestinationCol } from "./HumanitecSyncDestinationCol";
import { TerraformCloudSyncDestinationCol } from "./TerraformCloudSyncDestinationCol";
import { VercelSyncDestinationCol } from "./VercelSyncDestinationCol";
@@ -43,6 +44,8 @@ export const SecretSyncDestinationCol = ({ secretSync }: Props) => {
return <VercelSyncDestinationCol secretSync={secretSync} />;
case SecretSync.Windmill:
return <WindmillSyncDestinationCol secretSync={secretSync} />;
case SecretSync.HCVault:
return <HCVaultSyncDestinationCol secretSync={secretSync} />;
default:
throw new Error(
`Unhandled Secret Sync Destination Col: ${(secretSync as TSecretSync).destination}`
@@ -94,6 +94,10 @@ export const getSecretSyncDestinationColValues = (secretSync: TSecretSync) => {
primaryText = destinationConfig.workspace;
secondaryText = destinationConfig.path;
break;
case SecretSync.HCVault:
primaryText = destinationConfig.mount;
secondaryText = destinationConfig.path;
break;
default:
throw new Error(`Unhandled Destination Col Values ${destination}`);
}
@@ -0,0 +1,19 @@
import { GenericFieldLabel } from "@app/components/secret-syncs";
import { THCVaultSync } from "@app/hooks/api/secretSyncs/types/hc-vault-sync";
type Props = {
secretSync: THCVaultSync;
};
export const HCVaultSyncDestinationSection = ({ secretSync }: Props) => {
const {
destinationConfig: { path, mount }
} = secretSync;
return (
<>
<GenericFieldLabel label="Secrets Engine Mount">{mount}</GenericFieldLabel>
<GenericFieldLabel label="Path">{path}</GenericFieldLabel>
</>
);
};
@@ -18,6 +18,7 @@ import { CamundaSyncDestinationSection } from "./CamundaSyncDestinationSection";
import { DatabricksSyncDestinationSection } from "./DatabricksSyncDestinationSection";
import { GcpSyncDestinationSection } from "./GcpSyncDestinationSection";
import { GitHubSyncDestinationSection } from "./GitHubSyncDestinationSection";
import { HCVaultSyncDestinationSection } from "./HCVaultSyncDestinationSection";
import { HumanitecSyncDestinationSection } from "./HumanitecSyncDestinationSection";
import { TerraformCloudSyncDestinationSection } from "./TerraformCloudSyncDestinationSection";
import { VercelSyncDestinationSection } from "./VercelSyncDestinationSection";
@@ -73,6 +74,9 @@ export const SecretSyncDestinationSection = ({ secretSync, onEditDestination }:
case SecretSync.Windmill:
DestinationComponents = <WindmillSyncDestinationSection secretSync={secretSync} />;
break;
case SecretSync.HCVault:
DestinationComponents = <HCVaultSyncDestinationSection secretSync={secretSync} />;
break;
default:
throw new Error(`Unhandled Destination Section components: ${destination}`);
}
@@ -52,6 +52,7 @@ export const SecretSyncOptionsSection = ({ secretSync, onEditOptions }: Props) =
case SecretSync.Camunda:
case SecretSync.Vercel:
case SecretSync.Windmill:
case SecretSync.HCVault:
AdditionalSyncOptionsComponent = null;
break;
default: