diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index 8120d78fe..240ef3524 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -2002,6 +2002,10 @@ export const SecretSyncs = { WINDMILL: { workspace: "The Windmill workspace to sync secrets to.", path: "The Windmill workspace path to sync secrets to." + }, + HC_VAULT: { + mount: "The Hashicorp Vault Secrets Engine Mount to sync secrets to.", + path: "The Hashicorp Vault path to sync secrets to." } } }; diff --git a/backend/src/server/routes/v1/secret-sync-routers/hc-vault-sync-router.ts b/backend/src/server/routes/v1/secret-sync-routers/hc-vault-sync-router.ts new file mode 100644 index 000000000..97ee6963b --- /dev/null +++ b/backend/src/server/routes/v1/secret-sync-routers/hc-vault-sync-router.ts @@ -0,0 +1,17 @@ +import { + CreateHCVaultSyncSchema, + HCVaultSyncSchema, + UpdateHCVaultSyncSchema +} from "@app/services/secret-sync/hc-vault"; +import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; + +import { registerSyncSecretsEndpoints } from "./secret-sync-endpoints"; + +export const registerHCVaultSyncRouter = async (server: FastifyZodProvider) => + registerSyncSecretsEndpoints({ + destination: SecretSync.HCVault, + server, + responseSchema: HCVaultSyncSchema, + createSchema: CreateHCVaultSyncSchema, + updateSchema: UpdateHCVaultSyncSchema + }); diff --git a/backend/src/server/routes/v1/secret-sync-routers/index.ts b/backend/src/server/routes/v1/secret-sync-routers/index.ts index ee407cee3..5127a70e5 100644 --- a/backend/src/server/routes/v1/secret-sync-routers/index.ts +++ b/backend/src/server/routes/v1/secret-sync-routers/index.ts @@ -8,6 +8,7 @@ import { registerCamundaSyncRouter } from "./camunda-sync-router"; import { registerDatabricksSyncRouter } from "./databricks-sync-router"; import { registerGcpSyncRouter } from "./gcp-sync-router"; import { registerGitHubSyncRouter } from "./github-sync-router"; +import { registerHCVaultSyncRouter } from "./hc-vault-sync-router"; import { registerHumanitecSyncRouter } from "./humanitec-sync-router"; import { registerTerraformCloudSyncRouter } from "./terraform-cloud-sync-router"; import { registerVercelSyncRouter } from "./vercel-sync-router"; @@ -27,5 +28,6 @@ export const SECRET_SYNC_REGISTER_ROUTER_MAP: Record { diff --git a/backend/src/services/app-connection/app-connection-maps.ts b/backend/src/services/app-connection/app-connection-maps.ts index 762a9bcf2..012851ed5 100644 --- a/backend/src/services/app-connection/app-connection-maps.ts +++ b/backend/src/services/app-connection/app-connection-maps.ts @@ -14,5 +14,6 @@ export const APP_CONNECTION_NAME_MAP: Record = { [AppConnection.MsSql]: "Microsoft SQL Server", [AppConnection.Camunda]: "Camunda", [AppConnection.Windmill]: "Windmill", - [AppConnection.Auth0]: "Auth0" + [AppConnection.Auth0]: "Auth0", + [AppConnection.HCVault]: "Hashicorp Vault" }; diff --git a/backend/src/services/app-connection/hc-vault/hc-vault-connection-fns.ts b/backend/src/services/app-connection/hc-vault/hc-vault-connection-fns.ts index c0d88cb53..a7fa21996 100644 --- a/backend/src/services/app-connection/hc-vault/hc-vault-connection-fns.ts +++ b/backend/src/services/app-connection/hc-vault/hc-vault-connection-fns.ts @@ -91,9 +91,9 @@ export const listHCVaultMounts = async (appConnection: THCVaultConnection) => { const mounts: string[] = []; - // Filter for "kv" type only + // Filter for "kv" version 2 type only Object.entries(data.data).forEach(([path, mount]) => { - if (mount.type === "kv") { + if (mount.type === "kv" && mount.options?.version === "2") { mounts.push(path); } }); diff --git a/backend/src/services/app-connection/hc-vault/hc-vault-connection-types.ts b/backend/src/services/app-connection/hc-vault/hc-vault-connection-types.ts index 28af35e64..6f254eda0 100644 --- a/backend/src/services/app-connection/hc-vault/hc-vault-connection-types.ts +++ b/backend/src/services/app-connection/hc-vault/hc-vault-connection-types.ts @@ -26,6 +26,9 @@ export type THCVaultConnectionConfig = DiscriminativePick { + const { data } = await request.get( + `${instanceUrl}/v1/${removeTrailingSlash(mount)}/data/${path}`, + { + headers: { + "X-Vault-Token": accessToken + } + } + ); + + return data.data.data; +}; + +// Hashicorp Vault updates all variables in one batch. This is to respect their versioning +const updateHCVaultVariables = async ({ path, instanceUrl, accessToken, mount, data }: TPostHCVaultVariable) => + request.post( + `${instanceUrl}/v1/${removeTrailingSlash(mount)}/data/${path}`, + { + data + }, + { + headers: { + "X-Vault-Token": accessToken, + "Content-Type": "application/json" + } + } + ); + +export const HCVaultSyncFns = { + syncSecrets: async (secretSync: THCVaultSyncWithCredentials, secretMap: TSecretMap) => { + const { + connection, + destinationConfig: { mount, path }, + syncOptions: { disableSecretDeletion } + } = secretSync; + + const accessToken = await getHCVaultAccessToken(connection); + const { instanceUrl } = connection.credentials; + + const variables = await listHCVaultVariables({ instanceUrl, accessToken, mount, path }); + let tainted = false; + + for await (const entry of Object.entries(secretMap)) { + const [key, { value }] = entry; + if (value !== variables[key]) { + variables[key] = value; + tainted = true; + } + } + + if (disableSecretDeletion) return; + + for await (const [key] of Object.entries(variables)) { + if (!(key in secretMap)) { + delete variables[key]; + tainted = true; + } + } + + // Only update variables if there was a change detected + if (!tainted) return; + + try { + await updateHCVaultVariables({ accessToken, instanceUrl, mount, path, data: variables }); + } catch (error) { + throw new SecretSyncError({ + error + }); + } + }, + removeSecrets: async (secretSync: THCVaultSyncWithCredentials, secretMap: TSecretMap) => { + const { + connection, + destinationConfig: { mount, path } + } = secretSync; + + const accessToken = await getHCVaultAccessToken(connection); + const { instanceUrl } = connection.credentials; + + const variables = await listHCVaultVariables({ instanceUrl, accessToken, mount, path }); + + for await (const [key] of Object.entries(variables)) { + if (key in secretMap) { + delete variables[key]; + } + } + + try { + await updateHCVaultVariables({ accessToken, instanceUrl, mount, path, data: variables }); + } catch (error) { + throw new SecretSyncError({ + error + }); + } + }, + getSecrets: async (secretSync: THCVaultSyncWithCredentials) => { + const { + connection, + destinationConfig: { mount, path } + } = secretSync; + + const accessToken = await getHCVaultAccessToken(connection); + const { instanceUrl } = connection.credentials; + + const variables = await listHCVaultVariables({ + instanceUrl, + accessToken, + mount, + path + }); + + return Object.fromEntries(Object.entries(variables).map(([key, value]) => [key, { value }])); + } +}; diff --git a/backend/src/services/secret-sync/hc-vault/hc-vault-sync-schemas.ts b/backend/src/services/secret-sync/hc-vault/hc-vault-sync-schemas.ts new file mode 100644 index 000000000..c61c384a0 --- /dev/null +++ b/backend/src/services/secret-sync/hc-vault/hc-vault-sync-schemas.ts @@ -0,0 +1,57 @@ +import { z } from "zod"; + +import { SecretSyncs } from "@app/lib/api-docs"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; +import { + BaseSecretSyncSchema, + GenericCreateSecretSyncFieldsSchema, + GenericUpdateSecretSyncFieldsSchema +} from "@app/services/secret-sync/secret-sync-schemas"; +import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types"; + +const HCVaultSyncDestinationConfigSchema = z.object({ + mount: z + .string() + .trim() + .min(1, "Secrets Engine Mount required") + .describe(SecretSyncs.DESTINATION_CONFIG.HC_VAULT.mount), + 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." + }) + .describe(SecretSyncs.DESTINATION_CONFIG.HC_VAULT.path) +}); + +const HCVaultSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: true }; + +export const HCVaultSyncSchema = BaseSecretSyncSchema(SecretSync.HCVault, HCVaultSyncOptionsConfig).extend({ + destination: z.literal(SecretSync.HCVault), + destinationConfig: HCVaultSyncDestinationConfigSchema +}); + +export const CreateHCVaultSyncSchema = GenericCreateSecretSyncFieldsSchema( + SecretSync.HCVault, + HCVaultSyncOptionsConfig +).extend({ + destinationConfig: HCVaultSyncDestinationConfigSchema +}); + +export const UpdateHCVaultSyncSchema = GenericUpdateSecretSyncFieldsSchema( + SecretSync.HCVault, + HCVaultSyncOptionsConfig +).extend({ + destinationConfig: HCVaultSyncDestinationConfigSchema.optional() +}); + +export const HCVaultSyncListItemSchema = z.object({ + name: z.literal("Hashicorp Vault"), + connection: z.literal(AppConnection.HCVault), + destination: z.literal(SecretSync.HCVault), + canImportSecrets: z.literal(true) +}); diff --git a/backend/src/services/secret-sync/hc-vault/hc-vault-sync-types.ts b/backend/src/services/secret-sync/hc-vault/hc-vault-sync-types.ts new file mode 100644 index 000000000..73ecc3b5f --- /dev/null +++ b/backend/src/services/secret-sync/hc-vault/hc-vault-sync-types.ts @@ -0,0 +1,38 @@ +import { z } from "zod"; + +import { THCVaultConnection } from "@app/services/app-connection/hc-vault"; + +import { CreateHCVaultSyncSchema, HCVaultSyncListItemSchema, HCVaultSyncSchema } from "./hc-vault-sync-schemas"; + +export type THCVaultSync = z.infer; + +export type THCVaultSyncInput = z.infer; + +export type THCVaultSyncListItem = z.infer; + +export type THCVaultSyncWithCredentials = THCVaultSync & { + connection: THCVaultConnection; +}; + +export type THCVaultListVariablesResponse = { + data: { + data: { + [key: string]: string; + }; + }; +}; + +export type THCVaultListVariables = { + accessToken: string; + instanceUrl: string; + mount: string; + path: string; +}; + +export type TPostHCVaultVariable = THCVaultListVariables & { + data: { + [key: string]: string; + }; +}; + +export type TDeleteHCVaultVariable = THCVaultListVariables; diff --git a/backend/src/services/secret-sync/hc-vault/index.ts b/backend/src/services/secret-sync/hc-vault/index.ts new file mode 100644 index 000000000..6fd9f4310 --- /dev/null +++ b/backend/src/services/secret-sync/hc-vault/index.ts @@ -0,0 +1,4 @@ +export * from "./hc-vault-sync-constants"; +export * from "./hc-vault-sync-fns"; +export * from "./hc-vault-sync-schemas"; +export * from "./hc-vault-sync-types"; diff --git a/backend/src/services/secret-sync/secret-sync-enums.ts b/backend/src/services/secret-sync/secret-sync-enums.ts index 86273a4ff..af49060df 100644 --- a/backend/src/services/secret-sync/secret-sync-enums.ts +++ b/backend/src/services/secret-sync/secret-sync-enums.ts @@ -10,7 +10,8 @@ export enum SecretSync { TerraformCloud = "terraform-cloud", Camunda = "camunda", Vercel = "vercel", - Windmill = "windmill" + Windmill = "windmill", + HCVault = "hashicorp-vault" } export enum SecretSyncInitialSyncBehavior { diff --git a/backend/src/services/secret-sync/secret-sync-fns.ts b/backend/src/services/secret-sync/secret-sync-fns.ts index 0b821b593..cc5f8ed07 100644 --- a/backend/src/services/secret-sync/secret-sync-fns.ts +++ b/backend/src/services/secret-sync/secret-sync-fns.ts @@ -25,6 +25,7 @@ import { AZURE_KEY_VAULT_SYNC_LIST_OPTION, azureKeyVaultSyncFactory } from "./az import { CAMUNDA_SYNC_LIST_OPTION, camundaSyncFactory } from "./camunda"; import { GCP_SYNC_LIST_OPTION } from "./gcp"; import { GcpSyncFns } from "./gcp/gcp-sync-fns"; +import { HC_VAULT_SYNC_LIST_OPTION, HCVaultSyncFns } from "./hc-vault"; import { HUMANITEC_SYNC_LIST_OPTION } from "./humanitec"; import { HumanitecSyncFns } from "./humanitec/humanitec-sync-fns"; import { TERRAFORM_CLOUD_SYNC_LIST_OPTION, TerraformCloudSyncFns } from "./terraform-cloud"; @@ -43,7 +44,8 @@ const SECRET_SYNC_LIST_OPTIONS: Record = { [SecretSync.TerraformCloud]: TERRAFORM_CLOUD_SYNC_LIST_OPTION, [SecretSync.Camunda]: CAMUNDA_SYNC_LIST_OPTION, [SecretSync.Vercel]: VERCEL_SYNC_LIST_OPTION, - [SecretSync.Windmill]: WINDMILL_SYNC_LIST_OPTION + [SecretSync.Windmill]: WINDMILL_SYNC_LIST_OPTION, + [SecretSync.HCVault]: HC_VAULT_SYNC_LIST_OPTION }; export const listSecretSyncOptions = () => { @@ -140,6 +142,8 @@ export const SecretSyncFns = { return VercelSyncFns.syncSecrets(secretSync, secretMap); case SecretSync.Windmill: return WindmillSyncFns.syncSecrets(secretSync, secretMap); + case SecretSync.HCVault: + return HCVaultSyncFns.syncSecrets(secretSync, secretMap); default: throw new Error( `Unhandled sync destination for sync secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}` @@ -199,6 +203,9 @@ export const SecretSyncFns = { case SecretSync.Windmill: secretMap = await WindmillSyncFns.getSecrets(secretSync); break; + case SecretSync.HCVault: + secretMap = await HCVaultSyncFns.getSecrets(secretSync); + break; default: throw new Error( `Unhandled sync destination for get secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}` @@ -252,6 +259,8 @@ export const SecretSyncFns = { return VercelSyncFns.removeSecrets(secretSync, secretMap); case SecretSync.Windmill: return WindmillSyncFns.removeSecrets(secretSync, secretMap); + case SecretSync.HCVault: + return HCVaultSyncFns.removeSecrets(secretSync, secretMap); default: throw new Error( `Unhandled sync destination for remove secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}` diff --git a/backend/src/services/secret-sync/secret-sync-maps.ts b/backend/src/services/secret-sync/secret-sync-maps.ts index a9099543d..e4dbc4324 100644 --- a/backend/src/services/secret-sync/secret-sync-maps.ts +++ b/backend/src/services/secret-sync/secret-sync-maps.ts @@ -13,7 +13,8 @@ export const SECRET_SYNC_NAME_MAP: Record = { [SecretSync.TerraformCloud]: "Terraform Cloud", [SecretSync.Camunda]: "Camunda", [SecretSync.Vercel]: "Vercel", - [SecretSync.Windmill]: "Windmill" + [SecretSync.Windmill]: "Windmill", + [SecretSync.HCVault]: "Hashicorp Vault" }; export const SECRET_SYNC_CONNECTION_MAP: Record = { @@ -28,5 +29,6 @@ export const SECRET_SYNC_CONNECTION_MAP: Record = { [SecretSync.TerraformCloud]: AppConnection.TerraformCloud, [SecretSync.Camunda]: AppConnection.Camunda, [SecretSync.Vercel]: AppConnection.Vercel, - [SecretSync.Windmill]: AppConnection.Windmill + [SecretSync.Windmill]: AppConnection.Windmill, + [SecretSync.HCVault]: AppConnection.HCVault }; diff --git a/backend/src/services/secret-sync/secret-sync-types.ts b/backend/src/services/secret-sync/secret-sync-types.ts index 03e92a57a..602846616 100644 --- a/backend/src/services/secret-sync/secret-sync-types.ts +++ b/backend/src/services/secret-sync/secret-sync-types.ts @@ -55,6 +55,12 @@ import { TAzureKeyVaultSyncWithCredentials } from "./azure-key-vault"; import { TGcpSync, TGcpSyncInput, TGcpSyncListItem, TGcpSyncWithCredentials } from "./gcp"; +import { + THCVaultSync, + THCVaultSyncInput, + THCVaultSyncListItem, + THCVaultSyncWithCredentials +} from "./hc-vault/hc-vault-sync-types"; import { THumanitecSync, THumanitecSyncInput, @@ -81,7 +87,8 @@ export type TSecretSync = | TTerraformCloudSync | TCamundaSync | TVercelSync - | TWindmillSync; + | TWindmillSync + | THCVaultSync; export type TSecretSyncWithCredentials = | TAwsParameterStoreSyncWithCredentials @@ -95,7 +102,8 @@ export type TSecretSyncWithCredentials = | TTerraformCloudSyncWithCredentials | TCamundaSyncWithCredentials | TVercelSyncWithCredentials - | TWindmillSyncWithCredentials; + | TWindmillSyncWithCredentials + | THCVaultSyncWithCredentials; export type TSecretSyncInput = | TAwsParameterStoreSyncInput @@ -109,7 +117,8 @@ export type TSecretSyncInput = | TTerraformCloudSyncInput | TCamundaSyncInput | TVercelSyncInput - | TWindmillSyncInput; + | TWindmillSyncInput + | THCVaultSyncInput; export type TSecretSyncListItem = | TAwsParameterStoreSyncListItem @@ -123,7 +132,8 @@ export type TSecretSyncListItem = | TTerraformCloudSyncListItem | TCamundaSyncListItem | TVercelSyncListItem - | TWindmillSyncListItem; + | TWindmillSyncListItem + | THCVaultSyncListItem; export type TSyncOptionsConfig = { canImportSecrets: boolean; diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/HCVaultSyncFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/HCVaultSyncFields.tsx new file mode 100644 index 000000000..ee0b7038f --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/HCVaultSyncFields.tsx @@ -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 ( + <> + { + setValue("destinationConfig.mount", ""); + }} + /> + + ( + +
+ Don't see the mount you're looking for?{" "} + +
+ + } + > + + onChange((option as SingleValue<{ value: string }>)?.value ?? null) + } + options={mounts?.map((v) => ({ label: v, value: v }))} + placeholder="Select a Secrets Engine Mount..." + /> +
+ )} + /> + ( + + + + )} + /> + + ); +}; diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx index b2008b4f6..04a2fd8ed 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx @@ -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 ; case SecretSync.Windmill: return ; + case SecretSync.HCVault: + return ; default: throw new Error(`Unhandled Destination Config Field: ${destination}`); } diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx index 79c437d27..2373dd14e 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx @@ -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: diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/HCVaultSyncReviewFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/HCVaultSyncReviewFields.tsx new file mode 100644 index 000000000..2e1abac21 --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/HCVaultSyncReviewFields.tsx @@ -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(); + const mount = watch("destinationConfig.mount"); + const path = watch("destinationConfig.path"); + + return ( + <> + {mount} + {path} + + ); +}; diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx index 99182f207..9400874b7 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx @@ -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 = ; break; + case SecretSync.HCVault: + DestinationFieldsComponent = ; + break; default: throw new Error(`Unhandled Destination Review Fields: ${destination}`); } diff --git a/frontend/src/components/secret-syncs/forms/schemas/hc-vault-sync-destination-schema.ts b/frontend/src/components/secret-syncs/forms/schemas/hc-vault-sync-destination-schema.ts new file mode 100644 index 000000000..a02778aaf --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/schemas/hc-vault-sync-destination-schema.ts @@ -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." + }) + }) + }) +); diff --git a/frontend/src/components/secret-syncs/forms/schemas/secret-sync-schema.ts b/frontend/src/components/secret-syncs/forms/schemas/secret-sync-schema.ts index d58e60b19..c0f9454b2 100644 --- a/frontend/src/components/secret-syncs/forms/schemas/secret-sync-schema.ts +++ b/frontend/src/components/secret-syncs/forms/schemas/secret-sync-schema.ts @@ -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; diff --git a/frontend/src/helpers/appConnections.ts b/frontend/src/helpers/appConnections.ts index de1b9d2fd..93458b9c1 100644 --- a/frontend/src/helpers/appConnections.ts +++ b/frontend/src/helpers/appConnections.ts @@ -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< diff --git a/frontend/src/helpers/secretSyncs.ts b/frontend/src/helpers/secretSyncs.ts index 291b6f80a..1a06ad88b 100644 --- a/frontend/src/helpers/secretSyncs.ts +++ b/frontend/src/helpers/secretSyncs.ts @@ -39,6 +39,10 @@ export const SECRET_SYNC_MAP: Record = { [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< diff --git a/frontend/src/hooks/api/appConnections/hc-vault/index.ts b/frontend/src/hooks/api/appConnections/hc-vault/index.ts index 2c1906d36..b69c25120 100644 --- a/frontend/src/hooks/api/appConnections/hc-vault/index.ts +++ b/frontend/src/hooks/api/appConnections/hc-vault/index.ts @@ -1,2 +1 @@ export * from "./queries"; -export * from "./types"; diff --git a/frontend/src/hooks/api/appConnections/hc-vault/queries.tsx b/frontend/src/hooks/api/appConnections/hc-vault/queries.tsx index ce49e62bd..6f1b6ae46 100644 --- a/frontend/src/hooks/api/appConnections/hc-vault/queries.tsx +++ b/frontend/src/hooks/api/appConnections/hc-vault/queries.tsx @@ -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( - `/api/v1/app-connections/hc-vault/${connectionId}/mounts` + `/api/v1/app-connections/hashicorp-vault/${connectionId}/mounts` ); return data; diff --git a/frontend/src/hooks/api/appConnections/types/index.ts b/frontend/src/hooks/api/appConnections/types/index.ts index a33f3c748..3e979aacc 100644 --- a/frontend/src/hooks/api/appConnections/types/index.ts +++ b/frontend/src/hooks/api/appConnections/types/index.ts @@ -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 diff --git a/frontend/src/hooks/api/secretSyncs/enums.ts b/frontend/src/hooks/api/secretSyncs/enums.ts index b0d3fd7bb..7191ce68f 100644 --- a/frontend/src/hooks/api/secretSyncs/enums.ts +++ b/frontend/src/hooks/api/secretSyncs/enums.ts @@ -10,7 +10,8 @@ export enum SecretSync { TerraformCloud = "terraform-cloud", Camunda = "camunda", Vercel = "vercel", - Windmill = "windmill" + Windmill = "windmill", + HCVault = "hashicorp-vault" } export enum SecretSyncStatus { diff --git a/frontend/src/hooks/api/secretSyncs/types/hc-vault-sync.ts b/frontend/src/hooks/api/secretSyncs/types/hc-vault-sync.ts new file mode 100644 index 000000000..388698ef8 --- /dev/null +++ b/frontend/src/hooks/api/secretSyncs/types/hc-vault-sync.ts @@ -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; + }; +}; diff --git a/frontend/src/hooks/api/secretSyncs/types/index.ts b/frontend/src/hooks/api/secretSyncs/types/index.ts index 21fda56c7..189923d7e 100644 --- a/frontend/src/hooks/api/secretSyncs/types/index.ts +++ b/frontend/src/hooks/api/secretSyncs/types/index.ts @@ -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[] }; diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx index 70c709ddd..14b75b5f8 100644 --- a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx @@ -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; diff --git a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/HCVaultSyncDestinationCol.tsx b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/HCVaultSyncDestinationCol.tsx new file mode 100644 index 000000000..8011234e1 --- /dev/null +++ b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/HCVaultSyncDestinationCol.tsx @@ -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 ; +}; diff --git a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/SecretSyncDestinationCol.tsx b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/SecretSyncDestinationCol.tsx index 03371c8ce..06105001d 100644 --- a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/SecretSyncDestinationCol.tsx +++ b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/SecretSyncDestinationCol.tsx @@ -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 ; case SecretSync.Windmill: return ; + case SecretSync.HCVault: + return ; default: throw new Error( `Unhandled Secret Sync Destination Col: ${(secretSync as TSecretSync).destination}` diff --git a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/helpers/index.ts b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/helpers/index.ts index 7ef5644ed..1dc008014 100644 --- a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/helpers/index.ts +++ b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/helpers/index.ts @@ -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}`); } diff --git a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/HCVaultSyncDestinationSection.tsx b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/HCVaultSyncDestinationSection.tsx new file mode 100644 index 000000000..423e56cf0 --- /dev/null +++ b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/HCVaultSyncDestinationSection.tsx @@ -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 ( + <> + {mount} + {path} + + ); +}; diff --git a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/SecretSyncDestinatonSection.tsx b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/SecretSyncDestinatonSection.tsx index a3c948b94..5385c6cf8 100644 --- a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/SecretSyncDestinatonSection.tsx +++ b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/SecretSyncDestinatonSection.tsx @@ -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 = ; break; + case SecretSync.HCVault: + DestinationComponents = ; + break; default: throw new Error(`Unhandled Destination Section components: ${destination}`); } diff --git a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/SecretSyncOptionsSection.tsx b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/SecretSyncOptionsSection.tsx index 4fe701c8a..f2526e3b6 100644 --- a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/SecretSyncOptionsSection.tsx +++ b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/SecretSyncOptionsSection.tsx @@ -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: