diff --git a/backend/src/services/app-connection/app-connection-service.ts b/backend/src/services/app-connection/app-connection-service.ts index 691f6c4c9..869364853 100644 --- a/backend/src/services/app-connection/app-connection-service.ts +++ b/backend/src/services/app-connection/app-connection-service.ts @@ -600,7 +600,7 @@ export const appConnectionServiceFactory = ({ azureClientSecrets: azureClientSecretsConnectionService(connectAppConnectionById, appConnectionDAL, kmsService), azureDevOps: azureDevOpsConnectionService(connectAppConnectionById, appConnectionDAL, kmsService), auth0: auth0ConnectionService(connectAppConnectionById, appConnectionDAL, kmsService), - hcvault: hcVaultConnectionService(connectAppConnectionById), + hcvault: hcVaultConnectionService(connectAppConnectionById, gatewayService), windmill: windmillConnectionService(connectAppConnectionById), teamcity: teamcityConnectionService(connectAppConnectionById), oci: ociConnectionService(connectAppConnectionById, licenseService), diff --git a/backend/src/services/app-connection/auth0/auth0-connection-fns.ts b/backend/src/services/app-connection/auth0/auth0-connection-fns.ts index 5a9989b43..de4faf683 100644 --- a/backend/src/services/app-connection/auth0/auth0-connection-fns.ts +++ b/backend/src/services/app-connection/auth0/auth0-connection-fns.ts @@ -91,7 +91,7 @@ export const validateAuth0ConnectionCredentials = async ({ credentials }: TAuth0 }; } catch (e: unknown) { throw new BadRequestError({ - message: (e as Error).message ?? `Unable to validate connection: verify credentials` + message: (e as Error).message ?? "Unable to validate connection: verify credentials" }); } }; diff --git a/backend/src/services/app-connection/azure-app-configuration/azure-app-configuration-connection-fns.ts b/backend/src/services/app-connection/azure-app-configuration/azure-app-configuration-connection-fns.ts index 80e65a821..d173a47b5 100644 --- a/backend/src/services/app-connection/azure-app-configuration/azure-app-configuration-connection-fns.ts +++ b/backend/src/services/app-connection/azure-app-configuration/azure-app-configuration-connection-fns.ts @@ -70,7 +70,7 @@ export const validateAzureAppConfigurationConnectionCredentials = async ( tokenError = e; } else { throw new BadRequestError({ - message: `Unable to validate connection: verify credentials` + message: "Unable to validate connection: verify credentials" }); } } diff --git a/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-fns.ts b/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-fns.ts index 41dbb4392..2614cfd12 100644 --- a/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-fns.ts +++ b/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-fns.ts @@ -186,7 +186,7 @@ export const validateAzureClientSecretsConnectionCredentials = async (config: TA tokenError = e; } else { throw new BadRequestError({ - message: `Unable to validate connection: verify credentials` + message: "Unable to validate connection: verify credentials" }); } } diff --git a/backend/src/services/app-connection/azure-devops/azure-devops-fns.ts b/backend/src/services/app-connection/azure-devops/azure-devops-fns.ts index 39a330360..a3a9f10bd 100644 --- a/backend/src/services/app-connection/azure-devops/azure-devops-fns.ts +++ b/backend/src/services/app-connection/azure-devops/azure-devops-fns.ts @@ -204,7 +204,7 @@ export const validateAzureDevOpsConnectionCredentials = async (config: TAzureDev tokenError = e; } else { throw new BadRequestError({ - message: `Unable to validate connection: verify credentials` + message: "Unable to validate connection: verify credentials" }); } } diff --git a/backend/src/services/app-connection/azure-key-vault/azure-key-vault-connection-fns.ts b/backend/src/services/app-connection/azure-key-vault/azure-key-vault-connection-fns.ts index 1f88488c0..d6a260050 100644 --- a/backend/src/services/app-connection/azure-key-vault/azure-key-vault-connection-fns.ts +++ b/backend/src/services/app-connection/azure-key-vault/azure-key-vault-connection-fns.ts @@ -186,7 +186,7 @@ export const validateAzureKeyVaultConnectionCredentials = async (config: TAzureK tokenError = e; } else { throw new BadRequestError({ - message: `Unable to validate connection: verify credentials` + message: "Unable to validate connection: verify credentials" }); } } diff --git a/backend/src/services/app-connection/camunda/camunda-connection-fns.ts b/backend/src/services/app-connection/camunda/camunda-connection-fns.ts index 90d9744b8..91a033c0e 100644 --- a/backend/src/services/app-connection/camunda/camunda-connection-fns.ts +++ b/backend/src/services/app-connection/camunda/camunda-connection-fns.ts @@ -82,7 +82,7 @@ export const validateCamundaConnectionCredentials = async (appConnection: TCamun }; } catch (e: unknown) { throw new BadRequestError({ - message: `Unable to validate connection: verify credentials` + message: "Unable to validate connection: verify credentials" }); } }; diff --git a/backend/src/services/app-connection/databricks/databricks-connection-fns.ts b/backend/src/services/app-connection/databricks/databricks-connection-fns.ts index a35cc4aec..8912ad936 100644 --- a/backend/src/services/app-connection/databricks/databricks-connection-fns.ts +++ b/backend/src/services/app-connection/databricks/databricks-connection-fns.ts @@ -89,7 +89,7 @@ export const validateDatabricksConnectionCredentials = async (appConnection: TDa }; } catch (e: unknown) { throw new BadRequestError({ - message: `Unable to validate connection: verify credentials` + message: "Unable to validate connection: verify credentials" }); } }; diff --git a/backend/src/services/app-connection/github-radar/github-radar-connection-fns.ts b/backend/src/services/app-connection/github-radar/github-radar-connection-fns.ts index fba852a0a..7349f89a6 100644 --- a/backend/src/services/app-connection/github-radar/github-radar-connection-fns.ts +++ b/backend/src/services/app-connection/github-radar/github-radar-connection-fns.ts @@ -114,7 +114,7 @@ export const validateGitHubRadarConnectionCredentials = async (config: TGitHubRa } throw new BadRequestError({ - message: `Unable to validate connection: verify credentials` + message: "Unable to validate connection: verify credentials" }); } diff --git a/backend/src/services/app-connection/github/github-connection-fns.ts b/backend/src/services/app-connection/github/github-connection-fns.ts index 164aa31d9..05e6fdda3 100644 --- a/backend/src/services/app-connection/github/github-connection-fns.ts +++ b/backend/src/services/app-connection/github/github-connection-fns.ts @@ -447,7 +447,7 @@ export const validateGitHubConnectionCredentials = async ( } throw new BadRequestError({ - message: `Unable to validate connection: verify credentials` + message: "Unable to validate connection: verify credentials" }); } 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 48695a2a5..46a59bcec 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 @@ -1,18 +1,18 @@ -import { AxiosError } from "axios"; +import { AxiosError, AxiosRequestConfig, AxiosResponse } from "axios"; +import https from "https"; +import { verifyHostInputValidity } from "@app/ee/services/dynamic-secret/dynamic-secret-fns"; +import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service"; import { request } from "@app/lib/config/request"; import { BadRequestError } from "@app/lib/errors"; import { removeTrailingSlash } from "@app/lib/fn"; +import { GatewayProxyProtocol, withGatewayProxy } from "@app/lib/gateway"; +import { logger } from "@app/lib/logger"; import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator"; import { AppConnection } from "@app/services/app-connection/app-connection-enums"; import { HCVaultConnectionMethod } from "./hc-vault-connection-enums"; -import { - THCVaultConnection, - THCVaultConnectionConfig, - THCVaultMountResponse, - TValidateHCVaultConnectionCredentials -} from "./hc-vault-connection-types"; +import { THCVaultConnection, THCVaultConnectionConfig, THCVaultMountResponse } from "./hc-vault-connection-types"; export const getHCVaultInstanceUrl = async (config: THCVaultConnectionConfig) => { const instanceUrl = removeTrailingSlash(config.credentials.instanceUrl); @@ -37,7 +37,78 @@ type TokenRespData = { }; }; -export const getHCVaultAccessToken = async (connection: TValidateHCVaultConnectionCredentials) => { +export const requestWithHCVaultGateway = async ( + appConnection: { gatewayId?: string | null }, + gatewayService: Pick, + requestConfig: AxiosRequestConfig +): Promise> => { + const { gatewayId } = appConnection; + + // If gateway isn't set up, don't proxy request + if (!gatewayId) { + return request.request(requestConfig); + } + + const url = new URL(requestConfig.url as string); + + await blockLocalAndPrivateIpAddresses(url.toString()); + + const [targetHost] = await verifyHostInputValidity(url.hostname, true); + const relayDetails = await gatewayService.fnGetGatewayClientTlsByGatewayId(gatewayId); + const [relayHost, relayPort] = relayDetails.relayAddress.split(":"); + + return withGatewayProxy( + async (proxyPort) => { + const httpsAgent = new https.Agent({ + servername: targetHost + }); + + url.protocol = "https:"; + url.host = `localhost:${proxyPort}`; + + const finalRequestConfig: AxiosRequestConfig = { + ...requestConfig, + url: url.toString(), + httpsAgent, + headers: { + ...requestConfig.headers, + Host: targetHost + } + }; + + try { + return await request.request(finalRequestConfig); + } catch (error) { + if (error instanceof AxiosError) { + logger.error( + { message: error.message, data: (error.response as undefined | { data: unknown })?.data }, + "Error during HashiCorp Vault gateway request:" + ); + } + throw error; + } + }, + { + protocol: GatewayProxyProtocol.Tcp, + targetHost, + targetPort: url.port ? Number(url.port) : 8200, // 8200 is the default port for Vault self-hosted/dedicated + relayHost, + relayPort: Number(relayPort), + identityId: relayDetails.identityId, + orgId: relayDetails.orgId, + tlsOptions: { + ca: relayDetails.certChain, + cert: relayDetails.certificate, + key: relayDetails.privateKey.toString() + } + } + ); +}; + +export const getHCVaultAccessToken = async ( + connection: THCVaultConnection, + gatewayService: Pick +) => { // Return access token directly if not using AppRole method if (connection.method !== HCVaultConnectionMethod.AppRole) { return connection.credentials.accessToken; @@ -46,16 +117,16 @@ export const getHCVaultAccessToken = async (connection: TValidateHCVaultConnecti // Generate temporary token for AppRole method try { const { instanceUrl, roleId, secretId } = connection.credentials; - const tokenResp = await request.post( - `${removeTrailingSlash(instanceUrl)}/v1/auth/approle/login`, - { role_id: roleId, secret_id: secretId }, - { - headers: { - "Content-Type": "application/json", - ...(connection.credentials.namespace ? { "X-Vault-Namespace": connection.credentials.namespace } : {}) - } - } - ); + + const tokenResp = await requestWithHCVaultGateway(connection, gatewayService, { + url: `${removeTrailingSlash(instanceUrl)}/v1/auth/approle/login`, + method: "POST", + headers: { + "Content-Type": "application/json", + ...(connection.credentials.namespace ? { "X-Vault-Namespace": connection.credentials.namespace } : {}) + }, + data: { role_id: roleId, secret_id: secretId } + }); if (tokenResp.status !== 200) { throw new BadRequestError({ @@ -71,38 +142,55 @@ export const getHCVaultAccessToken = async (connection: TValidateHCVaultConnecti } }; -export const validateHCVaultConnectionCredentials = async (config: THCVaultConnectionConfig) => { - const instanceUrl = await getHCVaultInstanceUrl(config); +export const validateHCVaultConnectionCredentials = async ( + connection: THCVaultConnection, + gatewayService: Pick +) => { + const instanceUrl = await getHCVaultInstanceUrl(connection); try { - const accessToken = await getHCVaultAccessToken(config); + const accessToken = await getHCVaultAccessToken(connection, gatewayService); // Verify token - await request.get(`${instanceUrl}/v1/auth/token/lookup-self`, { + await requestWithHCVaultGateway(connection, gatewayService, { + url: `${instanceUrl}/v1/auth/token/lookup-self`, + method: "GET", headers: { "X-Vault-Token": accessToken } }); - return config.credentials; + return connection.credentials; } catch (error: unknown) { + logger.error(error, "Unable to verify HC Vault connection"); + if (error instanceof AxiosError) { throw new BadRequestError({ message: `Failed to validate credentials: ${error.message || "Unknown error"}` }); } + + if (error instanceof BadRequestError) { + throw error; + } + throw new BadRequestError({ message: "Unable to validate connection: verify credentials" }); } }; -export const listHCVaultMounts = async (appConnection: THCVaultConnection) => { - const instanceUrl = await getHCVaultInstanceUrl(appConnection); - const accessToken = await getHCVaultAccessToken(appConnection); +export const listHCVaultMounts = async ( + connection: THCVaultConnection, + gatewayService: Pick +) => { + const instanceUrl = await getHCVaultInstanceUrl(connection); + const accessToken = await getHCVaultAccessToken(connection, gatewayService); - const { data } = await request.get(`${instanceUrl}/v1/sys/mounts`, { + const { data } = await requestWithHCVaultGateway(connection, gatewayService, { + url: `${instanceUrl}/v1/sys/mounts`, + method: "GET", headers: { "X-Vault-Token": accessToken, - ...(appConnection.credentials.namespace ? { "X-Vault-Namespace": appConnection.credentials.namespace } : {}) + ...(connection.credentials.namespace ? { "X-Vault-Namespace": connection.credentials.namespace } : {}) } }); diff --git a/backend/src/services/app-connection/hc-vault/hc-vault-connection-schemas.ts b/backend/src/services/app-connection/hc-vault/hc-vault-connection-schemas.ts index a6db4d0eb..57e8fbeaf 100644 --- a/backend/src/services/app-connection/hc-vault/hc-vault-connection-schemas.ts +++ b/backend/src/services/app-connection/hc-vault/hc-vault-connection-schemas.ts @@ -55,11 +55,18 @@ export const HCVaultConnectionSchema = z.intersection( export const SanitizedHCVaultConnectionSchema = z.discriminatedUnion("method", [ BaseHCVaultConnectionSchema.extend({ method: z.literal(HCVaultConnectionMethod.AccessToken), - credentials: HCVaultConnectionAccessTokenCredentialsSchema.pick({}) + credentials: HCVaultConnectionAccessTokenCredentialsSchema.pick({ + namespace: true, + instanceUrl: true + }) }), BaseHCVaultConnectionSchema.extend({ method: z.literal(HCVaultConnectionMethod.AppRole), - credentials: HCVaultConnectionAppRoleCredentialsSchema.pick({}) + credentials: HCVaultConnectionAppRoleCredentialsSchema.pick({ + namespace: true, + instanceUrl: true, + roleId: true + }) }) ]); @@ -81,7 +88,7 @@ export const ValidateHCVaultConnectionCredentialsSchema = z.discriminatedUnion(" ]); export const CreateHCVaultConnectionSchema = ValidateHCVaultConnectionCredentialsSchema.and( - GenericCreateAppConnectionFieldsSchema(AppConnection.HCVault) + GenericCreateAppConnectionFieldsSchema(AppConnection.HCVault, { supportsGateways: true }) ); export const UpdateHCVaultConnectionSchema = z @@ -91,7 +98,7 @@ export const UpdateHCVaultConnectionSchema = z .optional() .describe(AppConnections.UPDATE(AppConnection.HCVault).credentials) }) - .and(GenericUpdateAppConnectionFieldsSchema(AppConnection.HCVault)); + .and(GenericUpdateAppConnectionFieldsSchema(AppConnection.HCVault, { supportsGateways: true })); export const HCVaultConnectionListItemSchema = z.object({ name: z.literal("HCVault"), diff --git a/backend/src/services/app-connection/hc-vault/hc-vault-connection-service.ts b/backend/src/services/app-connection/hc-vault/hc-vault-connection-service.ts index b5cee6fdd..589c7c1bd 100644 --- a/backend/src/services/app-connection/hc-vault/hc-vault-connection-service.ts +++ b/backend/src/services/app-connection/hc-vault/hc-vault-connection-service.ts @@ -1,3 +1,4 @@ +import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service"; import { logger } from "@app/lib/logger"; import { OrgServiceActor } from "@app/lib/types"; @@ -11,12 +12,15 @@ type TGetAppConnectionFunc = ( actor: OrgServiceActor ) => Promise; -export const hcVaultConnectionService = (getAppConnection: TGetAppConnectionFunc) => { +export const hcVaultConnectionService = ( + getAppConnection: TGetAppConnectionFunc, + gatewayService: Pick +) => { const listMounts = async (connectionId: string, actor: OrgServiceActor) => { const appConnection = await getAppConnection(AppConnection.HCVault, connectionId, actor); try { - const mounts = await listHCVaultMounts(appConnection); + const mounts = await listHCVaultMounts(appConnection, gatewayService); return mounts; } catch (error) { logger.error(error, "Failed to establish connection with Hashicorp Vault"); diff --git a/backend/src/services/secret-sync/hc-vault/hc-vault-sync-fns.ts b/backend/src/services/secret-sync/hc-vault/hc-vault-sync-fns.ts index 724eec7be..9168c96a6 100644 --- a/backend/src/services/secret-sync/hc-vault/hc-vault-sync-fns.ts +++ b/backend/src/services/secret-sync/hc-vault/hc-vault-sync-fns.ts @@ -1,9 +1,13 @@ import { isAxiosError } from "axios"; -import { request } from "@app/lib/config/request"; +import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service"; import { removeTrailingSlash } from "@app/lib/fn"; -import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator"; -import { getHCVaultAccessToken, getHCVaultInstanceUrl } from "@app/services/app-connection/hc-vault"; +import { + getHCVaultAccessToken, + getHCVaultInstanceUrl, + requestWithHCVaultGateway, + THCVaultConnection +} from "@app/services/app-connection/hc-vault"; import { THCVaultListVariables, THCVaultListVariablesResponse, @@ -14,19 +18,20 @@ import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors"; import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns"; import { TSecretMap } from "@app/services/secret-sync/secret-sync-types"; -const listHCVaultVariables = async ({ instanceUrl, namespace, mount, accessToken, path }: THCVaultListVariables) => { - await blockLocalAndPrivateIpAddresses(instanceUrl); - +const listHCVaultVariables = async ( + { instanceUrl, namespace, mount, accessToken, path }: THCVaultListVariables, + connection: THCVaultConnection, + gatewayService: Pick +) => { try { - const { data } = await request.get( - `${instanceUrl}/v1/${removeTrailingSlash(mount)}/data/${path}`, - { - headers: { - "X-Vault-Token": accessToken, - ...(namespace ? { "X-Vault-Namespace": namespace } : {}) - } + const { data } = await requestWithHCVaultGateway(connection, gatewayService, { + url: `${instanceUrl}/v1/${removeTrailingSlash(mount)}/data/${path}`, + method: "GET", + headers: { + "X-Vault-Token": accessToken, + ...(namespace ? { "X-Vault-Namespace": namespace } : {}) } - ); + }); return data.data.data; } catch (error: unknown) { @@ -39,33 +44,29 @@ const listHCVaultVariables = async ({ instanceUrl, namespace, mount, accessToken }; // Hashicorp Vault updates all variables in one batch. This is to respect their versioning -const updateHCVaultVariables = async ({ - path, - instanceUrl, - namespace, - accessToken, - mount, - data -}: TPostHCVaultVariable) => { - await blockLocalAndPrivateIpAddresses(instanceUrl); - - return request.post( - `${instanceUrl}/v1/${removeTrailingSlash(mount)}/data/${path}`, - { - data +const updateHCVaultVariables = async ( + { path, instanceUrl, namespace, accessToken, mount, data }: TPostHCVaultVariable, + connection: THCVaultConnection, + gatewayService: Pick +) => { + return requestWithHCVaultGateway(connection, gatewayService, { + url: `${instanceUrl}/v1/${removeTrailingSlash(mount)}/data/${path}`, + method: "POST", + headers: { + "X-Vault-Token": accessToken, + ...(namespace ? { "X-Vault-Namespace": namespace } : {}), + "Content-Type": "application/json" }, - { - headers: { - "X-Vault-Token": accessToken, - ...(namespace ? { "X-Vault-Namespace": namespace } : {}), - "Content-Type": "application/json" - } - } - ); + data: { data } + }); }; export const HCVaultSyncFns = { - syncSecrets: async (secretSync: THCVaultSyncWithCredentials, secretMap: TSecretMap) => { + syncSecrets: async ( + secretSync: THCVaultSyncWithCredentials, + secretMap: TSecretMap, + gatewayService: Pick + ) => { const { connection, environment, @@ -74,16 +75,20 @@ export const HCVaultSyncFns = { } = secretSync; const { namespace } = connection.credentials; - const accessToken = await getHCVaultAccessToken(connection); + const accessToken = await getHCVaultAccessToken(connection, gatewayService); const instanceUrl = await getHCVaultInstanceUrl(connection); - const variables = await listHCVaultVariables({ - instanceUrl, - accessToken, - namespace, - mount, - path - }); + const variables = await listHCVaultVariables( + { + instanceUrl, + accessToken, + namespace, + mount, + path + }, + connection, + gatewayService + ); let tainted = false; for (const entry of Object.entries(secretMap)) { @@ -110,24 +115,36 @@ export const HCVaultSyncFns = { if (!tainted) return; try { - await updateHCVaultVariables({ accessToken, instanceUrl, namespace, mount, path, data: variables }); + await updateHCVaultVariables( + { accessToken, instanceUrl, namespace, mount, path, data: variables }, + connection, + gatewayService + ); } catch (error) { throw new SecretSyncError({ error }); } }, - removeSecrets: async (secretSync: THCVaultSyncWithCredentials, secretMap: TSecretMap) => { + removeSecrets: async ( + secretSync: THCVaultSyncWithCredentials, + secretMap: TSecretMap, + gatewayService: Pick + ) => { const { connection, destinationConfig: { mount, path } } = secretSync; const { namespace } = connection.credentials; - const accessToken = await getHCVaultAccessToken(connection); + const accessToken = await getHCVaultAccessToken(connection, gatewayService); const instanceUrl = await getHCVaultInstanceUrl(connection); - const variables = await listHCVaultVariables({ instanceUrl, namespace, accessToken, mount, path }); + const variables = await listHCVaultVariables( + { instanceUrl, namespace, accessToken, mount, path }, + connection, + gatewayService + ); for await (const [key] of Object.entries(variables)) { if (key in secretMap) { @@ -136,30 +153,41 @@ export const HCVaultSyncFns = { } try { - await updateHCVaultVariables({ accessToken, instanceUrl, namespace, mount, path, data: variables }); + await updateHCVaultVariables( + { accessToken, instanceUrl, namespace, mount, path, data: variables }, + connection, + gatewayService + ); } catch (error) { throw new SecretSyncError({ error }); } }, - getSecrets: async (secretSync: THCVaultSyncWithCredentials) => { + getSecrets: async ( + secretSync: THCVaultSyncWithCredentials, + gatewayService: Pick + ) => { const { connection, destinationConfig: { mount, path } } = secretSync; const { namespace } = connection.credentials; - const accessToken = await getHCVaultAccessToken(connection); + const accessToken = await getHCVaultAccessToken(connection, gatewayService); const instanceUrl = await getHCVaultInstanceUrl(connection); - const variables = await listHCVaultVariables({ - instanceUrl, - namespace, - accessToken, - mount, - path - }); + const variables = await listHCVaultVariables( + { + instanceUrl, + namespace, + accessToken, + mount, + path + }, + connection, + gatewayService + ); return Object.fromEntries(Object.entries(variables).map(([key, value]) => [key, { value }])); } diff --git a/backend/src/services/secret-sync/secret-sync-fns.ts b/backend/src/services/secret-sync/secret-sync-fns.ts index 3ff7cefbc..5b083baf6 100644 --- a/backend/src/services/secret-sync/secret-sync-fns.ts +++ b/backend/src/services/secret-sync/secret-sync-fns.ts @@ -244,7 +244,7 @@ export const SecretSyncFns = { case SecretSync.Windmill: return WindmillSyncFns.syncSecrets(secretSync, schemaSecretMap); case SecretSync.HCVault: - return HCVaultSyncFns.syncSecrets(secretSync, schemaSecretMap); + return HCVaultSyncFns.syncSecrets(secretSync, schemaSecretMap, gatewayService); case SecretSync.TeamCity: return TeamCitySyncFns.syncSecrets(secretSync, schemaSecretMap); case SecretSync.OCIVault: @@ -283,7 +283,7 @@ export const SecretSyncFns = { }, getSecrets: async ( secretSync: TSecretSyncWithCredentials, - { kmsService, appConnectionDAL }: TSyncSecretDeps + { kmsService, appConnectionDAL, gatewayService }: TSyncSecretDeps ): Promise => { let secretMap: TSecretMap; switch (secretSync.destination) { @@ -341,7 +341,7 @@ export const SecretSyncFns = { secretMap = await WindmillSyncFns.getSecrets(secretSync); break; case SecretSync.HCVault: - secretMap = await HCVaultSyncFns.getSecrets(secretSync); + secretMap = await HCVaultSyncFns.getSecrets(secretSync, gatewayService); break; case SecretSync.TeamCity: secretMap = await TeamCitySyncFns.getSecrets(secretSync); @@ -451,7 +451,7 @@ export const SecretSyncFns = { case SecretSync.Windmill: return WindmillSyncFns.removeSecrets(secretSync, schemaSecretMap); case SecretSync.HCVault: - return HCVaultSyncFns.removeSecrets(secretSync, schemaSecretMap); + return HCVaultSyncFns.removeSecrets(secretSync, schemaSecretMap, gatewayService); case SecretSync.TeamCity: return TeamCitySyncFns.removeSecrets(secretSync, schemaSecretMap); case SecretSync.OCIVault: diff --git a/docs/images/app-connections/hashicorp-vault/vault-infisical-connect-modal.png b/docs/images/app-connections/hashicorp-vault/vault-infisical-connect-modal.png index 872d11cab..ebe9fc2ad 100644 Binary files a/docs/images/app-connections/hashicorp-vault/vault-infisical-connect-modal.png and b/docs/images/app-connections/hashicorp-vault/vault-infisical-connect-modal.png differ diff --git a/docs/integrations/app-connections/hashicorp-vault.mdx b/docs/integrations/app-connections/hashicorp-vault.mdx index 6e2ff68bf..c49b53ab8 100644 --- a/docs/integrations/app-connections/hashicorp-vault.mdx +++ b/docs/integrations/app-connections/hashicorp-vault.mdx @@ -149,6 +149,7 @@ Infisical supports two methods for connecting to Hashicorp Vault. - **Name**: The name of the connection being created. Must be slug-friendly. - **Description**: An optional description to provide details about this connection. + - **Gateway (optional):** The gateway connected to your private network. All requests made to your Vault instance will be made through the configured gateway. - **Instance URL**: The URL of your Hashicorp Vault instance. - **Namespace (optional)**: The namespace within your vault. Self-hosted and enterprise clusters may not use namespaces. - **Role ID**: The Role ID generated in the steps above. @@ -157,6 +158,7 @@ Infisical supports two methods for connecting to Hashicorp Vault. - **Name**: The name of the connection being created. Must be slug-friendly. - **Description**: An optional description to provide details about this connection. + - **Gateway (optional):** The gateway connected to your private network. All requests made to your Vault instance will be made through the configured gateway. - **Instance URL**: The URL of your Hashicorp Vault instance. - **Namespace (optional)**: The namespace within your vault. Self-hosted and enterprise clusters may not use namespaces. - **Access Token**: The Access Token generated in the steps above. diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/HCVaultConnectionForm.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/HCVaultConnectionForm.tsx index af3954b45..78d937fc6 100644 --- a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/HCVaultConnectionForm.tsx +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/HCVaultConnectionForm.tsx @@ -1,7 +1,9 @@ import { Controller, FormProvider, useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; +import { useQuery } from "@tanstack/react-query"; import { z } from "zod"; +import { OrgPermissionCan } from "@app/components/permissions"; import { Button, FormControl, @@ -9,9 +11,16 @@ import { ModalClose, SecretInput, Select, - SelectItem + SelectItem, + Tooltip } from "@app/components/v2"; +import { useSubscription } from "@app/context"; +import { + OrgGatewayPermissionActions, + OrgPermissionSubjects +} from "@app/context/OrgPermissionContext/types"; import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections"; +import { gatewaysQueryKeys } from "@app/hooks/api"; import { HCVaultConnectionMethod, THCVaultConnection } from "@app/hooks/api/appConnections"; import { AppConnection } from "@app/hooks/api/appConnections/enums"; @@ -66,7 +75,8 @@ export const HCVaultConnectionForm = ({ appConnection, onSubmit }: Props) => { resolver: zodResolver(formSchema), defaultValues: appConnection ?? { app: AppConnection.HCVault, - method: HCVaultConnectionMethod.AppRole + method: HCVaultConnectionMethod.AppRole, + gatewayId: null } }); @@ -79,6 +89,9 @@ export const HCVaultConnectionForm = ({ appConnection, onSubmit }: Props) => { const selectedMethod = watch("method"); + const { subscription } = useSubscription(); + const { data: gateways, isPending: isGatewaysLoading } = useQuery(gatewaysQueryKeys.list()); + return (
@@ -115,6 +128,54 @@ export const HCVaultConnectionForm = ({ appConnection, onSubmit }: Props) => { )} /> + {subscription.gateway && ( + + {(isAllowed) => ( + ( + + + + + + )} + /> + )} + + )}