mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-06 23:27:14 +00:00
Merge pull request #4455 from Infisical/ENG-3635
feat(app-connection, secret-sync): HC Vault Gateway Support
This commit is contained in:
@@ -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"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
+1
-1
@@ -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"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -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"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -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.
|
||||||
|
|||||||
+63
-2
@@ -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}
|
||||||
|
|||||||
Reference in New Issue
Block a user