Merge pull request #4455 from Infisical/ENG-3635

feat(app-connection, secret-sync): HC Vault Gateway Support
This commit is contained in:
x032205
2025-09-02 19:31:05 -04:00
committed by GitHub
18 changed files with 299 additions and 109 deletions
@@ -600,7 +600,7 @@ export const appConnectionServiceFactory = ({
azureClientSecrets: azureClientSecretsConnectionService(connectAppConnectionById, appConnectionDAL, kmsService), azureClientSecrets: azureClientSecretsConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
azureDevOps: azureDevOpsConnectionService(connectAppConnectionById, appConnectionDAL, kmsService), azureDevOps: azureDevOpsConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
auth0: auth0ConnectionService(connectAppConnectionById, appConnectionDAL, kmsService), auth0: auth0ConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
hcvault: hcVaultConnectionService(connectAppConnectionById), hcvault: hcVaultConnectionService(connectAppConnectionById, gatewayService),
windmill: windmillConnectionService(connectAppConnectionById), windmill: windmillConnectionService(connectAppConnectionById),
teamcity: teamcityConnectionService(connectAppConnectionById), teamcity: teamcityConnectionService(connectAppConnectionById),
oci: ociConnectionService(connectAppConnectionById, licenseService), oci: ociConnectionService(connectAppConnectionById, licenseService),
@@ -91,7 +91,7 @@ export const validateAuth0ConnectionCredentials = async ({ credentials }: TAuth0
}; };
} catch (e: unknown) { } catch (e: unknown) {
throw new BadRequestError({ 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"
}); });
} }
}; };
@@ -70,7 +70,7 @@ export const validateAzureAppConfigurationConnectionCredentials = async (
tokenError = e; tokenError = e;
} else { } else {
throw new BadRequestError({ throw new BadRequestError({
message: `Unable to validate connection: verify credentials` message: "Unable to validate connection: verify credentials"
}); });
} }
} }
@@ -186,7 +186,7 @@ export const validateAzureClientSecretsConnectionCredentials = async (config: TA
tokenError = e; tokenError = e;
} else { } else {
throw new BadRequestError({ throw new BadRequestError({
message: `Unable to validate connection: verify credentials` message: "Unable to validate connection: verify credentials"
}); });
} }
} }
@@ -204,7 +204,7 @@ export const validateAzureDevOpsConnectionCredentials = async (config: TAzureDev
tokenError = e; tokenError = e;
} else { } else {
throw new BadRequestError({ throw new BadRequestError({
message: `Unable to validate connection: verify credentials` message: "Unable to validate connection: verify credentials"
}); });
} }
} }
@@ -186,7 +186,7 @@ export const validateAzureKeyVaultConnectionCredentials = async (config: TAzureK
tokenError = e; tokenError = e;
} else { } else {
throw new BadRequestError({ throw new BadRequestError({
message: `Unable to validate connection: verify credentials` message: "Unable to validate connection: verify credentials"
}); });
} }
} }
@@ -82,7 +82,7 @@ export const validateCamundaConnectionCredentials = async (appConnection: TCamun
}; };
} catch (e: unknown) { } catch (e: unknown) {
throw new BadRequestError({ throw new BadRequestError({
message: `Unable to validate connection: verify credentials` message: "Unable to validate connection: verify credentials"
}); });
} }
}; };
@@ -89,7 +89,7 @@ export const validateDatabricksConnectionCredentials = async (appConnection: TDa
}; };
} catch (e: unknown) { } catch (e: unknown) {
throw new BadRequestError({ throw new BadRequestError({
message: `Unable to validate connection: verify credentials` message: "Unable to validate connection: verify credentials"
}); });
} }
}; };
@@ -114,7 +114,7 @@ export const validateGitHubRadarConnectionCredentials = async (config: TGitHubRa
} }
throw new BadRequestError({ throw new BadRequestError({
message: `Unable to validate connection: verify credentials` message: "Unable to validate connection: verify credentials"
}); });
} }
@@ -447,7 +447,7 @@ export const validateGitHubConnectionCredentials = async (
} }
throw new BadRequestError({ throw new BadRequestError({
message: `Unable to validate connection: verify credentials` message: "Unable to validate connection: verify credentials"
}); });
} }
@@ -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 { request } from "@app/lib/config/request";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError } from "@app/lib/errors";
import { removeTrailingSlash } from "@app/lib/fn"; 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 { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
import { AppConnection } from "@app/services/app-connection/app-connection-enums"; import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { HCVaultConnectionMethod } from "./hc-vault-connection-enums"; import { HCVaultConnectionMethod } from "./hc-vault-connection-enums";
import { import { THCVaultConnection, THCVaultConnectionConfig, THCVaultMountResponse } from "./hc-vault-connection-types";
THCVaultConnection,
THCVaultConnectionConfig,
THCVaultMountResponse,
TValidateHCVaultConnectionCredentials
} from "./hc-vault-connection-types";
export const getHCVaultInstanceUrl = async (config: THCVaultConnectionConfig) => { export const getHCVaultInstanceUrl = async (config: THCVaultConnectionConfig) => {
const instanceUrl = removeTrailingSlash(config.credentials.instanceUrl); const instanceUrl = removeTrailingSlash(config.credentials.instanceUrl);
@@ -37,7 +37,78 @@ type TokenRespData = {
}; };
}; };
export const getHCVaultAccessToken = async (connection: TValidateHCVaultConnectionCredentials) => { export const requestWithHCVaultGateway = async <T>(
appConnection: { gatewayId?: string | null },
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">,
requestConfig: AxiosRequestConfig
): Promise<AxiosResponse<T>> => {
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<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">
) => {
// Return access token directly if not using AppRole method // Return access token directly if not using AppRole method
if (connection.method !== HCVaultConnectionMethod.AppRole) { if (connection.method !== HCVaultConnectionMethod.AppRole) {
return connection.credentials.accessToken; return connection.credentials.accessToken;
@@ -46,16 +117,16 @@ export const getHCVaultAccessToken = async (connection: TValidateHCVaultConnecti
// Generate temporary token for AppRole method // Generate temporary token for AppRole method
try { try {
const { instanceUrl, roleId, secretId } = connection.credentials; const { instanceUrl, roleId, secretId } = connection.credentials;
const tokenResp = await request.post<TokenRespData>(
`${removeTrailingSlash(instanceUrl)}/v1/auth/approle/login`, const tokenResp = await requestWithHCVaultGateway<TokenRespData>(connection, gatewayService, {
{ role_id: roleId, secret_id: secretId }, url: `${removeTrailingSlash(instanceUrl)}/v1/auth/approle/login`,
{ method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
...(connection.credentials.namespace ? { "X-Vault-Namespace": connection.credentials.namespace } : {}) ...(connection.credentials.namespace ? { "X-Vault-Namespace": connection.credentials.namespace } : {})
} },
} data: { role_id: roleId, secret_id: secretId }
); });
if (tokenResp.status !== 200) { if (tokenResp.status !== 200) {
throw new BadRequestError({ throw new BadRequestError({
@@ -71,38 +142,55 @@ export const getHCVaultAccessToken = async (connection: TValidateHCVaultConnecti
} }
}; };
export const validateHCVaultConnectionCredentials = async (config: THCVaultConnectionConfig) => { export const validateHCVaultConnectionCredentials = async (
const instanceUrl = await getHCVaultInstanceUrl(config); connection: THCVaultConnection,
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">
) => {
const instanceUrl = await getHCVaultInstanceUrl(connection);
try { try {
const accessToken = await getHCVaultAccessToken(config); const accessToken = await getHCVaultAccessToken(connection, gatewayService);
// Verify token // 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 } headers: { "X-Vault-Token": accessToken }
}); });
return config.credentials; return connection.credentials;
} catch (error: unknown) { } catch (error: unknown) {
logger.error(error, "Unable to verify HC Vault connection");
if (error instanceof AxiosError) { if (error instanceof AxiosError) {
throw new BadRequestError({ throw new BadRequestError({
message: `Failed to validate credentials: ${error.message || "Unknown error"}` message: `Failed to validate credentials: ${error.message || "Unknown error"}`
}); });
} }
if (error instanceof BadRequestError) {
throw error;
}
throw new BadRequestError({ throw new BadRequestError({
message: "Unable to validate connection: verify credentials" message: "Unable to validate connection: verify credentials"
}); });
} }
}; };
export const listHCVaultMounts = async (appConnection: THCVaultConnection) => { export const listHCVaultMounts = async (
const instanceUrl = await getHCVaultInstanceUrl(appConnection); connection: THCVaultConnection,
const accessToken = await getHCVaultAccessToken(appConnection); gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">
) => {
const instanceUrl = await getHCVaultInstanceUrl(connection);
const accessToken = await getHCVaultAccessToken(connection, gatewayService);
const { data } = await request.get<THCVaultMountResponse>(`${instanceUrl}/v1/sys/mounts`, { const { data } = await requestWithHCVaultGateway<THCVaultMountResponse>(connection, gatewayService, {
url: `${instanceUrl}/v1/sys/mounts`,
method: "GET",
headers: { headers: {
"X-Vault-Token": accessToken, "X-Vault-Token": accessToken,
...(appConnection.credentials.namespace ? { "X-Vault-Namespace": appConnection.credentials.namespace } : {}) ...(connection.credentials.namespace ? { "X-Vault-Namespace": connection.credentials.namespace } : {})
} }
}); });
@@ -55,11 +55,18 @@ export const HCVaultConnectionSchema = z.intersection(
export const SanitizedHCVaultConnectionSchema = z.discriminatedUnion("method", [ export const SanitizedHCVaultConnectionSchema = z.discriminatedUnion("method", [
BaseHCVaultConnectionSchema.extend({ BaseHCVaultConnectionSchema.extend({
method: z.literal(HCVaultConnectionMethod.AccessToken), method: z.literal(HCVaultConnectionMethod.AccessToken),
credentials: HCVaultConnectionAccessTokenCredentialsSchema.pick({}) credentials: HCVaultConnectionAccessTokenCredentialsSchema.pick({
namespace: true,
instanceUrl: true
})
}), }),
BaseHCVaultConnectionSchema.extend({ BaseHCVaultConnectionSchema.extend({
method: z.literal(HCVaultConnectionMethod.AppRole), 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( export const CreateHCVaultConnectionSchema = ValidateHCVaultConnectionCredentialsSchema.and(
GenericCreateAppConnectionFieldsSchema(AppConnection.HCVault) GenericCreateAppConnectionFieldsSchema(AppConnection.HCVault, { supportsGateways: true })
); );
export const UpdateHCVaultConnectionSchema = z export const UpdateHCVaultConnectionSchema = z
@@ -91,7 +98,7 @@ export const UpdateHCVaultConnectionSchema = z
.optional() .optional()
.describe(AppConnections.UPDATE(AppConnection.HCVault).credentials) .describe(AppConnections.UPDATE(AppConnection.HCVault).credentials)
}) })
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.HCVault)); .and(GenericUpdateAppConnectionFieldsSchema(AppConnection.HCVault, { supportsGateways: true }));
export const HCVaultConnectionListItemSchema = z.object({ export const HCVaultConnectionListItemSchema = z.object({
name: z.literal("HCVault"), name: z.literal("HCVault"),
@@ -1,3 +1,4 @@
import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
import { OrgServiceActor } from "@app/lib/types"; import { OrgServiceActor } from "@app/lib/types";
@@ -11,12 +12,15 @@ type TGetAppConnectionFunc = (
actor: OrgServiceActor actor: OrgServiceActor
) => Promise<THCVaultConnection>; ) => Promise<THCVaultConnection>;
export const hcVaultConnectionService = (getAppConnection: TGetAppConnectionFunc) => { export const hcVaultConnectionService = (
getAppConnection: TGetAppConnectionFunc,
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">
) => {
const listMounts = async (connectionId: string, actor: OrgServiceActor) => { const listMounts = async (connectionId: string, actor: OrgServiceActor) => {
const appConnection = await getAppConnection(AppConnection.HCVault, connectionId, actor); const appConnection = await getAppConnection(AppConnection.HCVault, connectionId, actor);
try { try {
const mounts = await listHCVaultMounts(appConnection); const mounts = await listHCVaultMounts(appConnection, gatewayService);
return mounts; return mounts;
} catch (error) { } catch (error) {
logger.error(error, "Failed to establish connection with Hashicorp Vault"); logger.error(error, "Failed to establish connection with Hashicorp Vault");
@@ -1,9 +1,13 @@
import { isAxiosError } from "axios"; 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 { removeTrailingSlash } from "@app/lib/fn";
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator"; import {
import { getHCVaultAccessToken, getHCVaultInstanceUrl } from "@app/services/app-connection/hc-vault"; getHCVaultAccessToken,
getHCVaultInstanceUrl,
requestWithHCVaultGateway,
THCVaultConnection
} from "@app/services/app-connection/hc-vault";
import { import {
THCVaultListVariables, THCVaultListVariables,
THCVaultListVariablesResponse, 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 { matchesSchema } from "@app/services/secret-sync/secret-sync-fns";
import { TSecretMap } from "@app/services/secret-sync/secret-sync-types"; import { TSecretMap } from "@app/services/secret-sync/secret-sync-types";
const listHCVaultVariables = async ({ instanceUrl, namespace, mount, accessToken, path }: THCVaultListVariables) => { const listHCVaultVariables = async (
await blockLocalAndPrivateIpAddresses(instanceUrl); { instanceUrl, namespace, mount, accessToken, path }: THCVaultListVariables,
connection: THCVaultConnection,
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">
) => {
try { try {
const { data } = await request.get<THCVaultListVariablesResponse>( const { data } = await requestWithHCVaultGateway<THCVaultListVariablesResponse>(connection, gatewayService, {
`${instanceUrl}/v1/${removeTrailingSlash(mount)}/data/${path}`, url: `${instanceUrl}/v1/${removeTrailingSlash(mount)}/data/${path}`,
{ method: "GET",
headers: { headers: {
"X-Vault-Token": accessToken, "X-Vault-Token": accessToken,
...(namespace ? { "X-Vault-Namespace": namespace } : {}) ...(namespace ? { "X-Vault-Namespace": namespace } : {})
}
} }
); });
return data.data.data; return data.data.data;
} catch (error: unknown) { } 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 // Hashicorp Vault updates all variables in one batch. This is to respect their versioning
const updateHCVaultVariables = async ({ const updateHCVaultVariables = async (
path, { path, instanceUrl, namespace, accessToken, mount, data }: TPostHCVaultVariable,
instanceUrl, connection: THCVaultConnection,
namespace, gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">
accessToken, ) => {
mount, return requestWithHCVaultGateway(connection, gatewayService, {
data url: `${instanceUrl}/v1/${removeTrailingSlash(mount)}/data/${path}`,
}: TPostHCVaultVariable) => { method: "POST",
await blockLocalAndPrivateIpAddresses(instanceUrl); headers: {
"X-Vault-Token": accessToken,
return request.post( ...(namespace ? { "X-Vault-Namespace": namespace } : {}),
`${instanceUrl}/v1/${removeTrailingSlash(mount)}/data/${path}`, "Content-Type": "application/json"
{
data
}, },
{ data: { data }
headers: { });
"X-Vault-Token": accessToken,
...(namespace ? { "X-Vault-Namespace": namespace } : {}),
"Content-Type": "application/json"
}
}
);
}; };
export const HCVaultSyncFns = { export const HCVaultSyncFns = {
syncSecrets: async (secretSync: THCVaultSyncWithCredentials, secretMap: TSecretMap) => { syncSecrets: async (
secretSync: THCVaultSyncWithCredentials,
secretMap: TSecretMap,
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">
) => {
const { const {
connection, connection,
environment, environment,
@@ -74,16 +75,20 @@ export const HCVaultSyncFns = {
} = secretSync; } = secretSync;
const { namespace } = connection.credentials; const { namespace } = connection.credentials;
const accessToken = await getHCVaultAccessToken(connection); const accessToken = await getHCVaultAccessToken(connection, gatewayService);
const instanceUrl = await getHCVaultInstanceUrl(connection); const instanceUrl = await getHCVaultInstanceUrl(connection);
const variables = await listHCVaultVariables({ const variables = await listHCVaultVariables(
instanceUrl, {
accessToken, instanceUrl,
namespace, accessToken,
mount, namespace,
path mount,
}); path
},
connection,
gatewayService
);
let tainted = false; let tainted = false;
for (const entry of Object.entries(secretMap)) { for (const entry of Object.entries(secretMap)) {
@@ -110,24 +115,36 @@ export const HCVaultSyncFns = {
if (!tainted) return; if (!tainted) return;
try { try {
await updateHCVaultVariables({ accessToken, instanceUrl, namespace, mount, path, data: variables }); await updateHCVaultVariables(
{ accessToken, instanceUrl, namespace, mount, path, data: variables },
connection,
gatewayService
);
} catch (error) { } catch (error) {
throw new SecretSyncError({ throw new SecretSyncError({
error error
}); });
} }
}, },
removeSecrets: async (secretSync: THCVaultSyncWithCredentials, secretMap: TSecretMap) => { removeSecrets: async (
secretSync: THCVaultSyncWithCredentials,
secretMap: TSecretMap,
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">
) => {
const { const {
connection, connection,
destinationConfig: { mount, path } destinationConfig: { mount, path }
} = secretSync; } = secretSync;
const { namespace } = connection.credentials; const { namespace } = connection.credentials;
const accessToken = await getHCVaultAccessToken(connection); const accessToken = await getHCVaultAccessToken(connection, gatewayService);
const instanceUrl = await getHCVaultInstanceUrl(connection); 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)) { for await (const [key] of Object.entries(variables)) {
if (key in secretMap) { if (key in secretMap) {
@@ -136,30 +153,41 @@ export const HCVaultSyncFns = {
} }
try { try {
await updateHCVaultVariables({ accessToken, instanceUrl, namespace, mount, path, data: variables }); await updateHCVaultVariables(
{ accessToken, instanceUrl, namespace, mount, path, data: variables },
connection,
gatewayService
);
} catch (error) { } catch (error) {
throw new SecretSyncError({ throw new SecretSyncError({
error error
}); });
} }
}, },
getSecrets: async (secretSync: THCVaultSyncWithCredentials) => { getSecrets: async (
secretSync: THCVaultSyncWithCredentials,
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">
) => {
const { const {
connection, connection,
destinationConfig: { mount, path } destinationConfig: { mount, path }
} = secretSync; } = secretSync;
const { namespace } = connection.credentials; const { namespace } = connection.credentials;
const accessToken = await getHCVaultAccessToken(connection); const accessToken = await getHCVaultAccessToken(connection, gatewayService);
const instanceUrl = await getHCVaultInstanceUrl(connection); const instanceUrl = await getHCVaultInstanceUrl(connection);
const variables = await listHCVaultVariables({ const variables = await listHCVaultVariables(
instanceUrl, {
namespace, instanceUrl,
accessToken, namespace,
mount, accessToken,
path mount,
}); path
},
connection,
gatewayService
);
return Object.fromEntries(Object.entries(variables).map(([key, value]) => [key, { value }])); return Object.fromEntries(Object.entries(variables).map(([key, value]) => [key, { value }]));
} }
@@ -244,7 +244,7 @@ export const SecretSyncFns = {
case SecretSync.Windmill: case SecretSync.Windmill:
return WindmillSyncFns.syncSecrets(secretSync, schemaSecretMap); return WindmillSyncFns.syncSecrets(secretSync, schemaSecretMap);
case SecretSync.HCVault: case SecretSync.HCVault:
return HCVaultSyncFns.syncSecrets(secretSync, schemaSecretMap); return HCVaultSyncFns.syncSecrets(secretSync, schemaSecretMap, gatewayService);
case SecretSync.TeamCity: case SecretSync.TeamCity:
return TeamCitySyncFns.syncSecrets(secretSync, schemaSecretMap); return TeamCitySyncFns.syncSecrets(secretSync, schemaSecretMap);
case SecretSync.OCIVault: case SecretSync.OCIVault:
@@ -283,7 +283,7 @@ export const SecretSyncFns = {
}, },
getSecrets: async ( getSecrets: async (
secretSync: TSecretSyncWithCredentials, secretSync: TSecretSyncWithCredentials,
{ kmsService, appConnectionDAL }: TSyncSecretDeps { kmsService, appConnectionDAL, gatewayService }: TSyncSecretDeps
): Promise<TSecretMap> => { ): Promise<TSecretMap> => {
let secretMap: TSecretMap; let secretMap: TSecretMap;
switch (secretSync.destination) { switch (secretSync.destination) {
@@ -341,7 +341,7 @@ export const SecretSyncFns = {
secretMap = await WindmillSyncFns.getSecrets(secretSync); secretMap = await WindmillSyncFns.getSecrets(secretSync);
break; break;
case SecretSync.HCVault: case SecretSync.HCVault:
secretMap = await HCVaultSyncFns.getSecrets(secretSync); secretMap = await HCVaultSyncFns.getSecrets(secretSync, gatewayService);
break; break;
case SecretSync.TeamCity: case SecretSync.TeamCity:
secretMap = await TeamCitySyncFns.getSecrets(secretSync); secretMap = await TeamCitySyncFns.getSecrets(secretSync);
@@ -451,7 +451,7 @@ export const SecretSyncFns = {
case SecretSync.Windmill: case SecretSync.Windmill:
return WindmillSyncFns.removeSecrets(secretSync, schemaSecretMap); return WindmillSyncFns.removeSecrets(secretSync, schemaSecretMap);
case SecretSync.HCVault: case SecretSync.HCVault:
return HCVaultSyncFns.removeSecrets(secretSync, schemaSecretMap); return HCVaultSyncFns.removeSecrets(secretSync, schemaSecretMap, gatewayService);
case SecretSync.TeamCity: case SecretSync.TeamCity:
return TeamCitySyncFns.removeSecrets(secretSync, schemaSecretMap); return TeamCitySyncFns.removeSecrets(secretSync, schemaSecretMap);
case SecretSync.OCIVault: case SecretSync.OCIVault:
Binary file not shown.

Before

Width:  |  Height:  |  Size: 643 KiB

After

Width:  |  Height:  |  Size: 542 KiB

@@ -149,6 +149,7 @@ Infisical supports two methods for connecting to Hashicorp Vault.
<Tab title="App Role"> <Tab title="App Role">
- **Name**: The name of the connection being created. Must be slug-friendly. - **Name**: The name of the connection being created. Must be slug-friendly.
- **Description**: An optional description to provide details about this connection. - **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. - **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. - **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. - **Role ID**: The Role ID generated in the steps above.
@@ -157,6 +158,7 @@ Infisical supports two methods for connecting to Hashicorp Vault.
<Tab title="Access Token"> <Tab title="Access Token">
- **Name**: The name of the connection being created. Must be slug-friendly. - **Name**: The name of the connection being created. Must be slug-friendly.
- **Description**: An optional description to provide details about this connection. - **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. - **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. - **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. - **Access Token**: The Access Token generated in the steps above.
@@ -1,7 +1,9 @@
import { Controller, FormProvider, useForm } from "react-hook-form"; import { Controller, FormProvider, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { useQuery } from "@tanstack/react-query";
import { z } from "zod"; import { z } from "zod";
import { OrgPermissionCan } from "@app/components/permissions";
import { import {
Button, Button,
FormControl, FormControl,
@@ -9,9 +11,16 @@ import {
ModalClose, ModalClose,
SecretInput, SecretInput,
Select, Select,
SelectItem SelectItem,
Tooltip
} from "@app/components/v2"; } 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 { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections";
import { gatewaysQueryKeys } from "@app/hooks/api";
import { HCVaultConnectionMethod, THCVaultConnection } from "@app/hooks/api/appConnections"; import { HCVaultConnectionMethod, THCVaultConnection } from "@app/hooks/api/appConnections";
import { AppConnection } from "@app/hooks/api/appConnections/enums"; import { AppConnection } from "@app/hooks/api/appConnections/enums";
@@ -66,7 +75,8 @@ export const HCVaultConnectionForm = ({ appConnection, onSubmit }: Props) => {
resolver: zodResolver(formSchema), resolver: zodResolver(formSchema),
defaultValues: appConnection ?? { defaultValues: appConnection ?? {
app: AppConnection.HCVault, 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 selectedMethod = watch("method");
const { subscription } = useSubscription();
const { data: gateways, isPending: isGatewaysLoading } = useQuery(gatewaysQueryKeys.list());
return ( return (
<FormProvider {...form}> <FormProvider {...form}>
<form onSubmit={handleSubmit(onSubmit)}> <form onSubmit={handleSubmit(onSubmit)}>
@@ -115,6 +128,54 @@ export const HCVaultConnectionForm = ({ appConnection, onSubmit }: Props) => {
</FormControl> </FormControl>
)} )}
/> />
{subscription.gateway && (
<OrgPermissionCan
I={OrgGatewayPermissionActions.AttachGateways}
a={OrgPermissionSubjects.Gateway}
>
{(isAllowed) => (
<Controller
control={control}
name="gatewayId"
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
label="Gateway"
>
<Tooltip
isDisabled={isAllowed}
content="Restricted access. You don't have permission to attach gateways to resources."
>
<Select
isDisabled={!isAllowed}
value={value as string}
onValueChange={onChange}
className="w-full border border-mineshaft-500"
dropdownContainerClassName="max-w-none"
isLoading={isGatewaysLoading}
placeholder="Default: Internet Gateway"
position="popper"
>
<SelectItem
value={null as unknown as string}
onClick={() => onChange(undefined)}
>
Internet Gateway
</SelectItem>
{gateways?.map((el) => (
<SelectItem value={el.id} key={el.id}>
{el.name}
</SelectItem>
))}
</Select>
</Tooltip>
</FormControl>
)}
/>
)}
</OrgPermissionCan>
)}
<Controller <Controller
name="credentials.instanceUrl" name="credentials.instanceUrl"
control={control} control={control}