diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index 0f88e269c..8120d78fe 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -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." } } }; diff --git a/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts b/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts index 25bef7270..49f60224b 100644 --- a/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts +++ b/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts @@ -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) => { diff --git a/backend/src/server/routes/v1/app-connection-routers/hc-vault-connection-router.ts b/backend/src/server/routes/v1/app-connection-routers/hc-vault-connection-router.ts new file mode 100644 index 000000000..061c02777 --- /dev/null +++ b/backend/src/server/routes/v1/app-connection-routers/hc-vault-connection-router.ts @@ -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; + } + }); +}; diff --git a/backend/src/server/routes/v1/app-connection-routers/index.ts b/backend/src/server/routes/v1/app-connection-routers/index.ts index a833b6882..81f26355e 100644 --- a/backend/src/server/routes/v1/app-connection-routers/index.ts +++ b/backend/src/server/routes/v1/app-connection-routers/index.ts @@ -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 { 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 }; diff --git a/backend/src/services/app-connection/app-connection-service.ts b/backend/src/services/app-connection/app-connection-service.ts index 5293761a8..acc2c84d6 100644 --- a/backend/src/services/app-connection/app-connection-service.ts +++ b/backend/src/services/app-connection/app-connection-service.ts @@ -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>>; @@ -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; diff --git a/backend/src/services/app-connection/hc-vault/hc-vault-connection-enums.ts b/backend/src/services/app-connection/hc-vault/hc-vault-connection-enums.ts new file mode 100644 index 000000000..a1e2c8f09 --- /dev/null +++ b/backend/src/services/app-connection/hc-vault/hc-vault-connection-enums.ts @@ -0,0 +1,4 @@ +export enum HCVaultConnectionMethod { + AccessToken = "access-token", + AppRole = "app-role" +} 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 new file mode 100644 index 000000000..c0d88cb53 --- /dev/null +++ b/backend/src/services/app-connection/hc-vault/hc-vault-connection-fns.ts @@ -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( + `${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(`${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; +}; 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 new file mode 100644 index 000000000..6b46ca54d --- /dev/null +++ b/backend/src/services/app-connection/hc-vault/hc-vault-connection-schemas.ts @@ -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() +}); 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 new file mode 100644 index 000000000..48a1b17f1 --- /dev/null +++ b/backend/src/services/app-connection/hc-vault/hc-vault-connection-service.ts @@ -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; + +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 + }; +}; diff --git a/backend/src/services/app-connection/hc-vault/hc-vault-connection-types.ts b/backend/src/services/app-connection/hc-vault/hc-vault-connection-types.ts new file mode 100644 index 000000000..28af35e64 --- /dev/null +++ b/backend/src/services/app-connection/hc-vault/hc-vault-connection-types.ts @@ -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; + +export type THCVaultConnectionInput = z.infer & { + app: AppConnection.HCVault; +}; + +export type TValidateHCVaultConnectionCredentialsSchema = typeof ValidateHCVaultConnectionCredentialsSchema; + +export type TValidateHCVaultConnectionCredentials = z.infer; + +export type THCVaultConnectionConfig = DiscriminativePick & { + orgId: string; +}; + +export type THCVaultMountResponse = { + data: { + [key: string]: { + type: string; // We're only interested in "kv" types + }; + }; +}; diff --git a/backend/src/services/app-connection/hc-vault/index.ts b/backend/src/services/app-connection/hc-vault/index.ts new file mode 100644 index 000000000..161c2b51c --- /dev/null +++ b/backend/src/services/app-connection/hc-vault/index.ts @@ -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"; diff --git a/frontend/src/helpers/appConnections.ts b/frontend/src/helpers/appConnections.ts index c94bd6648..de1b9d2fd 100644 --- a/frontend/src/helpers/appConnections.ts +++ b/frontend/src/helpers/appConnections.ts @@ -17,7 +17,8 @@ import { TAppConnection, TerraformCloudConnectionMethod, VercelConnectionMethod, - WindmillConnectionMethod + WindmillConnectionMethod, + HCVaultConnectionMethod } from "@app/hooks/api/appConnections/types"; export const APP_CONNECTION_MAP: Record< @@ -43,7 +44,8 @@ export const APP_CONNECTION_MAP: Record< [AppConnection.MsSql]: { name: "Microsoft SQL Server", image: "MsSql.png" }, [AppConnection.Camunda]: { name: "Camunda", image: "Camunda.png" }, [AppConnection.Windmill]: { name: "Windmill", image: "Windmill.png" }, - [AppConnection.Auth0]: { name: "Auth0", image: "Auth0.png", size: 40 } + [AppConnection.Auth0]: { name: "Auth0", image: "Auth0.png", size: 40 }, + [AppConnection.HCVault]: { name: "Hashicorp Vault", image: "Vault.png" } }; export const getAppConnectionMethodDetails = (method: TAppConnection["method"]) => { @@ -71,10 +73,13 @@ export const getAppConnectionMethodDetails = (method: TAppConnection["method"]) case PostgresConnectionMethod.UsernameAndPassword: case MsSqlConnectionMethod.UsernameAndPassword: return { name: "Username & Password", icon: faLock }; + case HCVaultConnectionMethod.AccessToken: case WindmillConnectionMethod.AccessToken: return { name: "Access Token", icon: faKey }; case Auth0ConnectionMethod.ClientCredentials: return { name: "Client Credentials", icon: faServer }; + case HCVaultConnectionMethod.AppRole: + return { name: "App Role", icon: faUser }; default: throw new Error(`Unhandled App Connection Method: ${method}`); } diff --git a/frontend/src/hooks/api/appConnections/enums.ts b/frontend/src/hooks/api/appConnections/enums.ts index 704b200bb..b04cdd938 100644 --- a/frontend/src/hooks/api/appConnections/enums.ts +++ b/frontend/src/hooks/api/appConnections/enums.ts @@ -12,5 +12,6 @@ export enum AppConnection { MsSql = "mssql", Camunda = "camunda", Windmill = "windmill", - Auth0 = "auth0" + Auth0 = "auth0", + HCVault = "hashicorp-vault" } diff --git a/frontend/src/hooks/api/appConnections/hc-vault/index.ts b/frontend/src/hooks/api/appConnections/hc-vault/index.ts new file mode 100644 index 000000000..2c1906d36 --- /dev/null +++ b/frontend/src/hooks/api/appConnections/hc-vault/index.ts @@ -0,0 +1,2 @@ +export * from "./queries"; +export * from "./types"; diff --git a/frontend/src/hooks/api/appConnections/hc-vault/queries.tsx b/frontend/src/hooks/api/appConnections/hc-vault/queries.tsx new file mode 100644 index 000000000..ce49e62bd --- /dev/null +++ b/frontend/src/hooks/api/appConnections/hc-vault/queries.tsx @@ -0,0 +1,36 @@ +import { useQuery, UseQueryOptions } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +import { appConnectionKeys } from "../queries"; + +const hcVaultConnectionKeys = { + all: [...appConnectionKeys.all, "hcvault"] as const, + listMounts: (connectionId: string) => + [...hcVaultConnectionKeys.all, "mounts", connectionId] as const +}; + +export const useHCVaultConnectionListWorkspaces = ( + connectionId: string, + options?: Omit< + UseQueryOptions< + string[], + unknown, + string[], + ReturnType + >, + "queryKey" | "queryFn" + > +) => { + return useQuery({ + queryKey: hcVaultConnectionKeys.listMounts(connectionId), + queryFn: async () => { + const { data } = await apiRequest.get( + `/api/v1/app-connections/hc-vault/${connectionId}/mounts` + ); + + return data; + }, + ...options + }); +}; diff --git a/frontend/src/hooks/api/appConnections/types/app-options.ts b/frontend/src/hooks/api/appConnections/types/app-options.ts index ae57fbda2..043abbdf9 100644 --- a/frontend/src/hooks/api/appConnections/types/app-options.ts +++ b/frontend/src/hooks/api/appConnections/types/app-options.ts @@ -67,6 +67,10 @@ export type TAuth0ConnectionOption = TAppConnectionOptionBase & { app: AppConnection.Auth0; }; +export type THCVaultConnectionOption = TAppConnectionOptionBase & { + app: AppConnection.HCVault; +}; + export type TAppConnectionOption = | TAwsConnectionOption | TGitHubConnectionOption @@ -81,7 +85,8 @@ export type TAppConnectionOption = | TMsSqlConnectionOption | TCamundaConnectionOption | TWindmillConnectionOption - | TAuth0ConnectionOption; + | TAuth0ConnectionOption + | THCVaultConnectionOption; export type TAppConnectionOptionMap = { [AppConnection.AWS]: TAwsConnectionOption; @@ -98,4 +103,5 @@ export type TAppConnectionOptionMap = { [AppConnection.Camunda]: TCamundaConnectionOption; [AppConnection.Windmill]: TWindmillConnectionOption; [AppConnection.Auth0]: TAuth0ConnectionOption; + [AppConnection.HCVault]: THCVaultConnectionOption; }; diff --git a/frontend/src/hooks/api/appConnections/types/hc-vault-connection.ts b/frontend/src/hooks/api/appConnections/types/hc-vault-connection.ts new file mode 100644 index 000000000..2993dc432 --- /dev/null +++ b/frontend/src/hooks/api/appConnections/types/hc-vault-connection.ts @@ -0,0 +1,25 @@ +import { AppConnection } from "@app/hooks/api/appConnections/enums"; +import { TRootAppConnection } from "@app/hooks/api/appConnections/types/root-connection"; + +export enum HCVaultConnectionMethod { + AccessToken = "access-token", + AppRole = "app-role" +} + +export type THCVaultConnection = TRootAppConnection & { app: AppConnection.HCVault } & ( + | { + method: HCVaultConnectionMethod.AccessToken; + credentials: { + instanceUrl: string; + accessToken: string; + }; + } + | { + method: HCVaultConnectionMethod.AppRole; + credentials: { + instanceUrl: string; + roleId: string; + secretId: string; + }; + } + ); diff --git a/frontend/src/hooks/api/appConnections/types/index.ts b/frontend/src/hooks/api/appConnections/types/index.ts index 29e82bff6..a33f3c748 100644 --- a/frontend/src/hooks/api/appConnections/types/index.ts +++ b/frontend/src/hooks/api/appConnections/types/index.ts @@ -8,6 +8,7 @@ import { TCamundaConnection } from "./camunda-connection"; import { TDatabricksConnection } from "./databricks-connection"; import { TGcpConnection } from "./gcp-connection"; import { TGitHubConnection } from "./github-connection"; +import { THCVaultConnection } from "./hc-vault-connection"; import { THumanitecConnection } from "./humanitec-connection"; import { TMsSqlConnection } from "./mssql-connection"; import { TPostgresConnection } from "./postgres-connection"; @@ -29,6 +30,7 @@ export * from "./postgres-connection"; export * from "./terraform-cloud-connection"; export * from "./vercel-connection"; export * from "./windmill-connection"; +export * from "./hc-vault-connection"; export type TAppConnection = | TAwsConnection @@ -44,7 +46,8 @@ export type TAppConnection = | TMsSqlConnection | TCamundaConnection | TWindmillConnection - | TAuth0Connection; + | TAuth0Connection + | THCVaultConnection; export type TAvailableAppConnection = Pick; @@ -86,4 +89,5 @@ export type TAppConnectionMap = { [AppConnection.Camunda]: TCamundaConnection; [AppConnection.Windmill]: TWindmillConnection; [AppConnection.Auth0]: TAuth0Connection; + [AppConnection.HCVault]: THCVaultConnection; }; diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx index 05a7eebbc..70c709ddd 100644 --- a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx @@ -23,6 +23,7 @@ import { PostgresConnectionForm } from "./PostgresConnectionForm"; import { TerraformCloudConnectionForm } from "./TerraformCloudConnectionForm"; import { VercelConnectionForm } from "./VercelConnectionForm"; import { WindmillConnectionForm } from "./WindmillConnectionForm"; +import { HCVaultConnectionForm } from "./HCVaultConnectionForm"; type FormProps = { onComplete: (appConnection: TAppConnection) => void; @@ -89,6 +90,8 @@ const CreateForm = ({ app, onComplete }: CreateFormProps) => { return ; case AppConnection.Auth0: return ; + case AppConnection.HCVault: + return ; default: throw new Error(`Unhandled App ${app}`); } @@ -153,6 +156,8 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => { return ; case AppConnection.Auth0: return ; + case AppConnection.HCVault: + return ; default: throw new Error(`Unhandled App ${(appConnection as TAppConnection).app}`); } diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/HCVaultConnectionForm.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/HCVaultConnectionForm.tsx new file mode 100644 index 000000000..208d535fb --- /dev/null +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/HCVaultConnectionForm.tsx @@ -0,0 +1,202 @@ +import { Controller, FormProvider, useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + +import { + Button, + FormControl, + Input, + ModalClose, + SecretInput, + Select, + SelectItem +} from "@app/components/v2"; +import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections"; +import { HCVaultConnectionMethod, THCVaultConnection } from "@app/hooks/api/appConnections"; +import { AppConnection } from "@app/hooks/api/appConnections/enums"; + +import { + genericAppConnectionFieldsSchema, + GenericAppConnectionsFields +} from "./GenericAppConnectionFields"; + +type Props = { + appConnection?: THCVaultConnection; + onSubmit: (formData: FormData) => Promise; +}; + +const rootSchema = genericAppConnectionFieldsSchema.extend({ + app: z.literal(AppConnection.HCVault) +}); + +const InstanceUrlSchema = z + .string() + .trim() + .min(1, "Instance URL required") + .url("Invalid Instance URL"); +const formSchema = z.discriminatedUnion("method", [ + rootSchema.extend({ + method: z.literal(HCVaultConnectionMethod.AccessToken), + credentials: z.object({ + instanceUrl: InstanceUrlSchema, + accessToken: z.string().trim().min(1, "Access Token required") + }) + }), + rootSchema.extend({ + method: z.literal(HCVaultConnectionMethod.AppRole), + credentials: z.object({ + instanceUrl: InstanceUrlSchema, + roleId: z.string().trim().min(1, "Role ID required"), + secretId: z.string().trim().min(1, "Secret ID required") + }) + }) +]); + +type FormData = z.infer; + +export const HCVaultConnectionForm = ({ appConnection, onSubmit }: Props) => { + const isUpdate = Boolean(appConnection); + + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: appConnection ?? { + app: AppConnection.HCVault, + method: HCVaultConnectionMethod.AppRole + } + }); + + const { + handleSubmit, + control, + watch, + formState: { isSubmitting, isDirty } + } = form; + + const selectedMethod = watch("method"); + + return ( + +
+ {!isUpdate && } + ( + + + + )} + /> + ( + + + + )} + /> + {selectedMethod === HCVaultConnectionMethod.AccessToken ? ( + ( + + + + )} + /> + ) : ( + <> + ( + + + + )} + /> + ( + + + + )} + /> + + )} +
+ + + + +
+ +
+ ); +};