feat(app-connections): Hashicorp Vault App Connection

This commit is contained in:
x
2025-04-24 15:41:21 -04:00
parent 6b3726957a
commit 418ac20f91
23 changed files with 654 additions and 16 deletions
+6
View File
@@ -1857,6 +1857,12 @@ export const AppConnections = {
WINDMILL: {
instanceUrl: "The Windmill instance URL to connect with (defaults to https://app.windmill.dev).",
accessToken: "The access token to use to connect with Windmill."
},
HC_VAULT: {
instanceUrl: "The Hashicrop Vault instance URL to connect with.",
accessToken: "The access token used to connect with Hashicorp Vault.",
roleId: "The Role ID used to connect with Hashicorp Vault.",
secretId: "The Secret ID used to connect with Hashicorp Vault."
}
}
};
@@ -24,6 +24,10 @@ import {
} from "@app/services/app-connection/databricks";
import { GcpConnectionListItemSchema, SanitizedGcpConnectionSchema } from "@app/services/app-connection/gcp";
import { GitHubConnectionListItemSchema, SanitizedGitHubConnectionSchema } from "@app/services/app-connection/github";
import {
HCVaultConnectionListItemSchema,
SanitizedHCVaultConnectionSchema
} from "@app/services/app-connection/hc-vault";
import {
HumanitecConnectionListItemSchema,
SanitizedHumanitecConnectionSchema
@@ -59,7 +63,8 @@ const SanitizedAppConnectionSchema = z.union([
...SanitizedMsSqlConnectionSchema.options,
...SanitizedCamundaConnectionSchema.options,
...SanitizedWindmillConnectionSchema.options,
...SanitizedAuth0ConnectionSchema.options
...SanitizedAuth0ConnectionSchema.options,
...SanitizedHCVaultConnectionSchema.options
]);
const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
@@ -76,7 +81,8 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
MsSqlConnectionListItemSchema,
CamundaConnectionListItemSchema,
WindmillConnectionListItemSchema,
Auth0ConnectionListItemSchema
Auth0ConnectionListItemSchema,
HCVaultConnectionListItemSchema
]);
export const registerAppConnectionRouter = async (server: FastifyZodProvider) => {
@@ -0,0 +1,47 @@
import z from "zod";
import { readLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import {
CreateHCVaultConnectionSchema,
SanitizedHCVaultConnectionSchema,
UpdateHCVaultConnectionSchema
} from "@app/services/app-connection/hc-vault";
import { AuthMode } from "@app/services/auth/auth-type";
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
export const registerHCVaultConnectionRouter = async (server: FastifyZodProvider) => {
registerAppConnectionEndpoints({
app: AppConnection.HCVault,
server,
sanitizedResponseSchema: SanitizedHCVaultConnectionSchema,
createSchema: CreateHCVaultConnectionSchema,
updateSchema: UpdateHCVaultConnectionSchema
});
// The following endpoints are for internal Infisical App use only and not part of the public API
server.route({
method: "GET",
url: `/:connectionId/mounts`,
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
connectionId: z.string().uuid()
}),
response: {
200: z.string().array()
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { connectionId } = req.params;
const mounts = await server.services.appConnection.hcvault.listMounts(connectionId, req.permission);
return mounts;
}
});
};
@@ -8,6 +8,7 @@ import { registerCamundaConnectionRouter } from "./camunda-connection-router";
import { registerDatabricksConnectionRouter } from "./databricks-connection-router";
import { registerGcpConnectionRouter } from "./gcp-connection-router";
import { registerGitHubConnectionRouter } from "./github-connection-router";
import { registerHCVaultConnectionRouter } from "./hc-vault-connection-router";
import { registerHumanitecConnectionRouter } from "./humanitec-connection-router";
import { registerMsSqlConnectionRouter } from "./mssql-connection-router";
import { registerPostgresConnectionRouter } from "./postgres-connection-router";
@@ -32,5 +33,6 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record<AppConnection, (server:
[AppConnection.MsSql]: registerMsSqlConnectionRouter,
[AppConnection.Camunda]: registerCamundaConnectionRouter,
[AppConnection.Windmill]: registerWindmillConnectionRouter,
[AppConnection.Auth0]: registerAuth0ConnectionRouter
[AppConnection.Auth0]: registerAuth0ConnectionRouter,
[AppConnection.HCVault]: registerHCVaultConnectionRouter
};
@@ -12,7 +12,8 @@ export enum AppConnection {
MsSql = "mssql",
Camunda = "camunda",
Windmill = "windmill",
Auth0 = "auth0"
Auth0 = "auth0",
HCVault = "hashicorp-vault"
}
export enum AWSRegion {
@@ -36,6 +36,11 @@ import {
} from "./databricks";
import { GcpConnectionMethod, getGcpConnectionListItem, validateGcpConnectionCredentials } from "./gcp";
import { getGitHubConnectionListItem, GitHubConnectionMethod, validateGitHubConnectionCredentials } from "./github";
import {
getHCVaultConnectionListItem,
HCVaultConnectionMethod,
validateHCVaultConnectionCredentials
} from "./hc-vault";
import {
getHumanitecConnectionListItem,
HumanitecConnectionMethod,
@@ -71,7 +76,8 @@ export const listAppConnectionOptions = () => {
getMsSqlConnectionListItem(),
getCamundaConnectionListItem(),
getWindmillConnectionListItem(),
getAuth0ConnectionListItem()
getAuth0ConnectionListItem(),
getHCVaultConnectionListItem()
].sort((a, b) => a.name.localeCompare(b.name));
};
@@ -135,7 +141,8 @@ export const validateAppConnectionCredentials = async (
[AppConnection.Vercel]: validateVercelConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.TerraformCloud]: validateTerraformCloudConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Auth0]: validateAuth0ConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Windmill]: validateWindmillConnectionCredentials as TAppConnectionCredentialsValidator
[AppConnection.Windmill]: validateWindmillConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.HCVault]: validateHCVaultConnectionCredentials as TAppConnectionCredentialsValidator
};
return VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[appConnection.app](appConnection);
@@ -167,9 +174,12 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) =>
case MsSqlConnectionMethod.UsernameAndPassword:
return "Username & Password";
case WindmillConnectionMethod.AccessToken:
case HCVaultConnectionMethod.AccessToken:
return "Access Token";
case Auth0ConnectionMethod.ClientCredentials:
return "Client Credentials";
case HCVaultConnectionMethod.AppRole:
return "App Role";
default:
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
throw new Error(`Unhandled App Connection Method: ${method}`);
@@ -214,5 +224,6 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record<
[AppConnection.Camunda]: platformManagedCredentialsNotSupported,
[AppConnection.Vercel]: platformManagedCredentialsNotSupported,
[AppConnection.Windmill]: platformManagedCredentialsNotSupported,
[AppConnection.Auth0]: platformManagedCredentialsNotSupported
[AppConnection.Auth0]: platformManagedCredentialsNotSupported,
[AppConnection.HCVault]: platformManagedCredentialsNotSupported
};
@@ -41,6 +41,8 @@ import { ValidateGcpConnectionCredentialsSchema } from "./gcp";
import { gcpConnectionService } from "./gcp/gcp-connection-service";
import { ValidateGitHubConnectionCredentialsSchema } from "./github";
import { githubConnectionService } from "./github/github-connection-service";
import { ValidateHCVaultConnectionCredentialsSchema } from "./hc-vault";
import { hcVaultConnectionService } from "./hc-vault/hc-vault-connection-service";
import { ValidateHumanitecConnectionCredentialsSchema } from "./humanitec";
import { humanitecConnectionService } from "./humanitec/humanitec-connection-service";
import { ValidateMsSqlConnectionCredentialsSchema } from "./mssql";
@@ -74,7 +76,8 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAp
[AppConnection.MsSql]: ValidateMsSqlConnectionCredentialsSchema,
[AppConnection.Camunda]: ValidateCamundaConnectionCredentialsSchema,
[AppConnection.Windmill]: ValidateWindmillConnectionCredentialsSchema,
[AppConnection.Auth0]: ValidateAuth0ConnectionCredentialsSchema
[AppConnection.Auth0]: ValidateAuth0ConnectionCredentialsSchema,
[AppConnection.HCVault]: ValidateHCVaultConnectionCredentialsSchema
};
export const appConnectionServiceFactory = ({
@@ -450,6 +453,7 @@ export const appConnectionServiceFactory = ({
camunda: camundaConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
vercel: vercelConnectionService(connectAppConnectionById),
windmill: windmillConnectionService(connectAppConnectionById),
auth0: auth0ConnectionService(connectAppConnectionById, appConnectionDAL, kmsService)
auth0: auth0ConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
hcvault: hcVaultConnectionService(connectAppConnectionById)
};
};
@@ -51,6 +51,12 @@ import {
TGitHubConnectionInput,
TValidateGitHubConnectionCredentialsSchema
} from "./github";
import {
THCVaultConnection,
THCVaultConnectionConfig,
THCVaultConnectionInput,
TValidateHCVaultConnectionCredentialsSchema
} from "./hc-vault";
import {
THumanitecConnection,
THumanitecConnectionConfig,
@@ -97,6 +103,7 @@ export type TAppConnection = { id: string } & (
| TCamundaConnection
| TWindmillConnection
| TAuth0Connection
| THCVaultConnection
);
export type TAppConnectionRaw = NonNullable<Awaited<ReturnType<TAppConnectionDALFactory["findById"]>>>;
@@ -118,6 +125,7 @@ export type TAppConnectionInput = { id: string } & (
| TCamundaConnectionInput
| TWindmillConnectionInput
| TAuth0ConnectionInput
| THCVaultConnectionInput
);
export type TSqlConnectionInput = TPostgresConnectionInput | TMsSqlConnectionInput;
@@ -144,7 +152,8 @@ export type TAppConnectionConfig =
| TSqlConnectionConfig
| TCamundaConnectionConfig
| TWindmillConnectionConfig
| TAuth0ConnectionConfig;
| TAuth0ConnectionConfig
| THCVaultConnectionConfig;
export type TValidateAppConnectionCredentialsSchema =
| TValidateAwsConnectionCredentialsSchema
@@ -160,7 +169,8 @@ export type TValidateAppConnectionCredentialsSchema =
| TValidateTerraformCloudConnectionCredentialsSchema
| TValidateVercelConnectionCredentialsSchema
| TValidateWindmillConnectionCredentialsSchema
| TValidateAuth0ConnectionCredentialsSchema;
| TValidateAuth0ConnectionCredentialsSchema
| TValidateHCVaultConnectionCredentialsSchema;
export type TListAwsConnectionKmsKeys = {
connectionId: string;
@@ -0,0 +1,4 @@
export enum HCVaultConnectionMethod {
AccessToken = "access-token",
AppRole = "app-role"
}
@@ -0,0 +1,102 @@
import { AxiosError } from "axios";
import { request } from "@app/lib/config/request";
import { BadRequestError } from "@app/lib/errors";
import { removeTrailingSlash } from "@app/lib/fn";
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";
export const getHCVaultConnectionListItem = () => ({
name: "HCVault" as const,
app: AppConnection.HCVault as const,
methods: Object.values(HCVaultConnectionMethod) as [
HCVaultConnectionMethod.AccessToken,
HCVaultConnectionMethod.AppRole
]
});
type TokenRespData = {
auth: {
client_token: string;
};
};
export const getHCVaultAccessToken = async (connection: TValidateHCVaultConnectionCredentials) => {
// Return access token directly if not using AppRole method
if (connection.method !== HCVaultConnectionMethod.AppRole) {
return connection.credentials.accessToken;
}
// Generate temporary token for AppRole method
try {
const { instanceUrl, roleId, secretId } = connection.credentials;
const tokenResp = await request.post<TokenRespData>(
`${removeTrailingSlash(instanceUrl)}/v1/auth/approle/login`,
{ role_id: roleId, secret_id: secretId },
{ headers: { "Content-Type": "application/json" } }
);
if (tokenResp.status !== 200) {
throw new BadRequestError({
message: `Unable to validate credentials: Hashicorp Vault responded with a status code of ${tokenResp.status} (${tokenResp.statusText}). Verify credentials and try again.`
});
}
return tokenResp.data.auth.client_token;
} catch (e: unknown) {
throw new BadRequestError({
message: "Unable to validate connection: verify credentials"
});
}
};
export const validateHCVaultConnectionCredentials = async (config: THCVaultConnectionConfig) => {
const instanceUrl = removeTrailingSlash(config.credentials.instanceUrl);
try {
const accessToken = await getHCVaultAccessToken(config);
// Verify token
await request.get(`${instanceUrl}/v1/auth/token/lookup-self`, {
headers: { "X-Vault-Token": accessToken }
});
return config.credentials;
} catch (error: unknown) {
if (error instanceof AxiosError) {
throw new BadRequestError({
message: `Failed to validate credentials: ${error.message || "Unknown error"}`
});
}
throw new BadRequestError({
message: "Unable to validate connection: verify credentials"
});
}
};
export const listHCVaultMounts = async (appConnection: THCVaultConnection) => {
const instanceUrl = removeTrailingSlash(appConnection.credentials.instanceUrl);
const accessToken = await getHCVaultAccessToken(appConnection);
const { data } = await request.get<THCVaultMountResponse>(`${instanceUrl}/v1/sys/mounts`, {
headers: { "X-Vault-Token": accessToken }
});
const mounts: string[] = [];
// Filter for "kv" type only
Object.entries(data.data).forEach(([path, mount]) => {
if (mount.type === "kv") {
mounts.push(path);
}
});
return mounts;
};
@@ -0,0 +1,95 @@
import z from "zod";
import { AppConnections } from "@app/lib/api-docs";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import {
BaseAppConnectionSchema,
GenericCreateAppConnectionFieldsSchema,
GenericUpdateAppConnectionFieldsSchema
} from "@app/services/app-connection/app-connection-schemas";
import { HCVaultConnectionMethod } from "./hc-vault-connection-enums";
const InstanceUrlSchema = z
.string()
.trim()
.min(1, "Instance URL required")
.url("Invalid Instance URL")
.describe(AppConnections.CREDENTIALS.HC_VAULT.instanceUrl);
export const HCVaultConnectionAccessTokenCredentialsSchema = z.object({
instanceUrl: InstanceUrlSchema,
accessToken: z
.string()
.trim()
.min(1, "Access Token required")
.describe(AppConnections.CREDENTIALS.HC_VAULT.accessToken)
});
export const HCVaultConnectionAppRoleCredentialsSchema = z.object({
instanceUrl: InstanceUrlSchema,
roleId: z.string().trim().min(1, "Role ID required").describe(AppConnections.CREDENTIALS.HC_VAULT.roleId),
secretId: z.string().trim().min(1, "Secret ID required").describe(AppConnections.CREDENTIALS.HC_VAULT.secretId)
});
const BaseHCVaultConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.HCVault) });
export const HCVaultConnectionSchema = z.intersection(
BaseHCVaultConnectionSchema,
z.discriminatedUnion("method", [
z.object({
method: z.literal(HCVaultConnectionMethod.AccessToken),
credentials: HCVaultConnectionAccessTokenCredentialsSchema
}),
z.object({
method: z.literal(HCVaultConnectionMethod.AppRole),
credentials: HCVaultConnectionAppRoleCredentialsSchema
})
])
);
export const SanitizedHCVaultConnectionSchema = z.discriminatedUnion("method", [
BaseHCVaultConnectionSchema.extend({
method: z.literal(HCVaultConnectionMethod.AccessToken),
credentials: HCVaultConnectionAccessTokenCredentialsSchema.pick({})
}),
BaseHCVaultConnectionSchema.extend({
method: z.literal(HCVaultConnectionMethod.AppRole),
credentials: HCVaultConnectionAppRoleCredentialsSchema.pick({})
})
]);
export const ValidateHCVaultConnectionCredentialsSchema = z.discriminatedUnion("method", [
z.object({
method: z
.literal(HCVaultConnectionMethod.AccessToken)
.describe(AppConnections.CREATE(AppConnection.HCVault).method),
credentials: HCVaultConnectionAccessTokenCredentialsSchema.describe(
AppConnections.CREATE(AppConnection.HCVault).credentials
)
}),
z.object({
method: z.literal(HCVaultConnectionMethod.AppRole).describe(AppConnections.CREATE(AppConnection.HCVault).method),
credentials: HCVaultConnectionAppRoleCredentialsSchema.describe(
AppConnections.CREATE(AppConnection.HCVault).credentials
)
})
]);
export const CreateHCVaultConnectionSchema = ValidateHCVaultConnectionCredentialsSchema.and(
GenericCreateAppConnectionFieldsSchema(AppConnection.HCVault)
);
export const UpdateHCVaultConnectionSchema = z
.object({
credentials: HCVaultConnectionAccessTokenCredentialsSchema.optional().describe(
AppConnections.UPDATE(AppConnection.HCVault).credentials
)
})
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.HCVault));
export const HCVaultConnectionListItemSchema = z.object({
name: z.literal("HCVault"),
app: z.literal(AppConnection.HCVault),
methods: z.nativeEnum(HCVaultConnectionMethod).array()
});
@@ -0,0 +1,28 @@
import { OrgServiceActor } from "@app/lib/types";
import { AppConnection } from "../app-connection-enums";
import { listHCVaultMounts } from "./hc-vault-connection-fns";
import { THCVaultConnection } from "./hc-vault-connection-types";
type TGetAppConnectionFunc = (
app: AppConnection,
connectionId: string,
actor: OrgServiceActor
) => Promise<THCVaultConnection>;
export const hcVaultConnectionService = (getAppConnection: TGetAppConnectionFunc) => {
const listMounts = async (connectionId: string, actor: OrgServiceActor) => {
const appConnection = await getAppConnection(AppConnection.HCVault, connectionId, actor);
try {
const mounts = await listHCVaultMounts(appConnection);
return mounts;
} catch (error) {
return [];
}
};
return {
listMounts
};
};
@@ -0,0 +1,32 @@
import z from "zod";
import { DiscriminativePick } from "@app/lib/types";
import { AppConnection } from "../app-connection-enums";
import {
CreateHCVaultConnectionSchema,
HCVaultConnectionSchema,
ValidateHCVaultConnectionCredentialsSchema
} from "./hc-vault-connection-schemas";
export type THCVaultConnection = z.infer<typeof HCVaultConnectionSchema>;
export type THCVaultConnectionInput = z.infer<typeof CreateHCVaultConnectionSchema> & {
app: AppConnection.HCVault;
};
export type TValidateHCVaultConnectionCredentialsSchema = typeof ValidateHCVaultConnectionCredentialsSchema;
export type TValidateHCVaultConnectionCredentials = z.infer<typeof ValidateHCVaultConnectionCredentialsSchema>;
export type THCVaultConnectionConfig = DiscriminativePick<THCVaultConnectionInput, "method" | "app" | "credentials"> & {
orgId: string;
};
export type THCVaultMountResponse = {
data: {
[key: string]: {
type: string; // We're only interested in "kv" types
};
};
};
@@ -0,0 +1,4 @@
export * from "./hc-vault-connection-enums";
export * from "./hc-vault-connection-fns";
export * from "./hc-vault-connection-schemas";
export * from "./hc-vault-connection-types";