mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
feat(secrey-sync): Hashicorp Vault Secret Sync (and minor app connection fixes)
This commit is contained in:
@@ -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."
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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
|
||||
});
|
||||
@@ -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<SecretSync, (server: Fastif
|
||||
[SecretSync.TerraformCloud]: registerTerraformCloudSyncRouter,
|
||||
[SecretSync.Camunda]: registerCamundaSyncRouter,
|
||||
[SecretSync.Vercel]: registerVercelSyncRouter,
|
||||
[SecretSync.Windmill]: registerWindmillSyncRouter
|
||||
[SecretSync.Windmill]: registerWindmillSyncRouter,
|
||||
[SecretSync.HCVault]: registerHCVaultSyncRouter
|
||||
};
|
||||
|
||||
@@ -22,6 +22,7 @@ import { CamundaSyncListItemSchema, CamundaSyncSchema } from "@app/services/secr
|
||||
import { DatabricksSyncListItemSchema, DatabricksSyncSchema } from "@app/services/secret-sync/databricks";
|
||||
import { GcpSyncListItemSchema, GcpSyncSchema } from "@app/services/secret-sync/gcp";
|
||||
import { GitHubSyncListItemSchema, GitHubSyncSchema } from "@app/services/secret-sync/github";
|
||||
import { HCVaultSyncListItemSchema, HCVaultSyncSchema } from "@app/services/secret-sync/hc-vault";
|
||||
import { HumanitecSyncListItemSchema, HumanitecSyncSchema } from "@app/services/secret-sync/humanitec";
|
||||
import { TerraformCloudSyncListItemSchema, TerraformCloudSyncSchema } from "@app/services/secret-sync/terraform-cloud";
|
||||
import { VercelSyncListItemSchema, VercelSyncSchema } from "@app/services/secret-sync/vercel";
|
||||
@@ -39,7 +40,8 @@ const SecretSyncSchema = z.discriminatedUnion("destination", [
|
||||
TerraformCloudSyncSchema,
|
||||
CamundaSyncSchema,
|
||||
VercelSyncSchema,
|
||||
WindmillSyncSchema
|
||||
WindmillSyncSchema,
|
||||
HCVaultSyncSchema
|
||||
]);
|
||||
|
||||
const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [
|
||||
@@ -54,7 +56,8 @@ const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [
|
||||
TerraformCloudSyncListItemSchema,
|
||||
CamundaSyncListItemSchema,
|
||||
VercelSyncListItemSchema,
|
||||
WindmillSyncListItemSchema
|
||||
WindmillSyncListItemSchema,
|
||||
HCVaultSyncListItemSchema
|
||||
]);
|
||||
|
||||
export const registerSecretSyncRouter = async (server: FastifyZodProvider) => {
|
||||
|
||||
@@ -14,5 +14,6 @@ export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
|
||||
[AppConnection.MsSql]: "Microsoft SQL Server",
|
||||
[AppConnection.Camunda]: "Camunda",
|
||||
[AppConnection.Windmill]: "Windmill",
|
||||
[AppConnection.Auth0]: "Auth0"
|
||||
[AppConnection.Auth0]: "Auth0",
|
||||
[AppConnection.HCVault]: "Hashicorp Vault"
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -26,6 +26,9 @@ export type THCVaultConnectionConfig = DiscriminativePick<THCVaultConnectionInpu
|
||||
export type THCVaultMountResponse = {
|
||||
data: {
|
||||
[key: string]: {
|
||||
options: {
|
||||
version?: string | null;
|
||||
} | null;
|
||||
type: string; // We're only interested in "kv" types
|
||||
};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||
import { TSecretSyncListItem } from "@app/services/secret-sync/secret-sync-types";
|
||||
|
||||
export const HC_VAULT_SYNC_LIST_OPTION: TSecretSyncListItem = {
|
||||
name: "Hashicorp Vault",
|
||||
destination: SecretSync.HCVault,
|
||||
connection: AppConnection.HCVault,
|
||||
canImportSecrets: true
|
||||
};
|
||||
126
backend/src/services/secret-sync/hc-vault/hc-vault-sync-fns.ts
Normal file
126
backend/src/services/secret-sync/hc-vault/hc-vault-sync-fns.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
import { request } from "@app/lib/config/request";
|
||||
import { removeTrailingSlash } from "@app/lib/fn";
|
||||
import { getHCVaultAccessToken } from "@app/services/app-connection/hc-vault";
|
||||
import {
|
||||
THCVaultListVariables,
|
||||
THCVaultListVariablesResponse,
|
||||
THCVaultSyncWithCredentials,
|
||||
TPostHCVaultVariable
|
||||
} from "@app/services/secret-sync/hc-vault/hc-vault-sync-types";
|
||||
import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors";
|
||||
import { TSecretMap } from "@app/services/secret-sync/secret-sync-types";
|
||||
|
||||
const listHCVaultVariables = async ({ instanceUrl, mount, accessToken, path }: THCVaultListVariables) => {
|
||||
const { data } = await request.get<THCVaultListVariablesResponse>(
|
||||
`${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 }]));
|
||||
}
|
||||
};
|
||||
@@ -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)
|
||||
});
|
||||
@@ -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<typeof HCVaultSyncSchema>;
|
||||
|
||||
export type THCVaultSyncInput = z.infer<typeof CreateHCVaultSyncSchema>;
|
||||
|
||||
export type THCVaultSyncListItem = z.infer<typeof HCVaultSyncListItemSchema>;
|
||||
|
||||
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;
|
||||
4
backend/src/services/secret-sync/hc-vault/index.ts
Normal file
4
backend/src/services/secret-sync/hc-vault/index.ts
Normal file
@@ -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";
|
||||
@@ -10,7 +10,8 @@ export enum SecretSync {
|
||||
TerraformCloud = "terraform-cloud",
|
||||
Camunda = "camunda",
|
||||
Vercel = "vercel",
|
||||
Windmill = "windmill"
|
||||
Windmill = "windmill",
|
||||
HCVault = "hashicorp-vault"
|
||||
}
|
||||
|
||||
export enum SecretSyncInitialSyncBehavior {
|
||||
|
||||
@@ -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, TSecretSyncListItem> = {
|
||||
[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}`
|
||||
|
||||
@@ -13,7 +13,8 @@ export const SECRET_SYNC_NAME_MAP: Record<SecretSync, string> = {
|
||||
[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<SecretSync, AppConnection> = {
|
||||
@@ -28,5 +29,6 @@ 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
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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't see the mount you'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;
|
||||
|
||||
@@ -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<
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -10,7 +10,8 @@ export enum SecretSync {
|
||||
TerraformCloud = "terraform-cloud",
|
||||
Camunda = "camunda",
|
||||
Vercel = "vercel",
|
||||
Windmill = "windmill"
|
||||
Windmill = "windmill",
|
||||
HCVault = "hashicorp-vault"
|
||||
}
|
||||
|
||||
export enum SecretSyncStatus {
|
||||
|
||||
16
frontend/src/hooks/api/secretSyncs/types/hc-vault-sync.ts
Normal file
16
frontend/src/hooks/api/secretSyncs/types/hc-vault-sync.ts
Normal file
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user