diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index fe893d5ab..7a350938f 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -1771,6 +1771,12 @@ export const SecretSyncs = { }, DATABRICKS: { scope: "The Databricks secret scope that secrets should be synced to." + }, + HUMANITEC: { + app: "The ID of the Humanitec app to sync secrets to.", + org: "The ID of the Humanitec org to sync secrets to.", + env: "The ID of the Humanitec environment to sync secrets to.", + scope: "The Humanitec scope that secrets should be synced to." } } }; 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 964a57a13..cf8181ec4 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 @@ -18,6 +18,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 { + HumanitecConnectionListItemSchema, + SanitizedHumanitecConnectionSchema +} from "@app/services/app-connection/humanitec"; import { AuthMode } from "@app/services/auth/auth-type"; // can't use discriminated due to multiple schemas for certain apps @@ -27,7 +31,8 @@ const SanitizedAppConnectionSchema = z.union([ ...SanitizedGcpConnectionSchema.options, ...SanitizedAzureKeyVaultConnectionSchema.options, ...SanitizedAzureAppConfigurationConnectionSchema.options, - ...SanitizedDatabricksConnectionSchema.options + ...SanitizedDatabricksConnectionSchema.options, + ...SanitizedHumanitecConnectionSchema.options ]); const AppConnectionOptionsSchema = z.discriminatedUnion("app", [ @@ -36,7 +41,8 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [ GcpConnectionListItemSchema, AzureKeyVaultConnectionListItemSchema, AzureAppConfigurationConnectionListItemSchema, - DatabricksConnectionListItemSchema + DatabricksConnectionListItemSchema, + HumanitecConnectionListItemSchema ]); export const registerAppConnectionRouter = async (server: FastifyZodProvider) => { diff --git a/backend/src/server/routes/v1/app-connection-routers/humanitec-connection-router.ts b/backend/src/server/routes/v1/app-connection-routers/humanitec-connection-router.ts new file mode 100644 index 000000000..2d462c4ef --- /dev/null +++ b/backend/src/server/routes/v1/app-connection-routers/humanitec-connection-router.ts @@ -0,0 +1,69 @@ +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 { + CreateHumanitecConnectionSchema, + HumanitecOrgWithApps, + SanitizedHumanitecConnectionSchema, + UpdateHumanitecConnectionSchema +} from "@app/services/app-connection/humanitec"; +import { AuthMode } from "@app/services/auth/auth-type"; + +import { registerAppConnectionEndpoints } from "./app-connection-endpoints"; + +export const registerHumanitecConnectionRouter = async (server: FastifyZodProvider) => { + registerAppConnectionEndpoints({ + app: AppConnection.Humanitec, + server, + sanitizedResponseSchema: SanitizedHumanitecConnectionSchema, + createSchema: CreateHumanitecConnectionSchema, + updateSchema: UpdateHumanitecConnectionSchema + }); + + // The below endpoints are not exposed and for Infisical App use + server.route({ + method: "GET", + url: `/:connectionId/organizations`, + config: { + rateLimit: readLimit + }, + schema: { + params: z.object({ + connectionId: z.string().uuid() + }), + response: { + 200: z + .object({ + id: z.string(), + name: z.string(), + apps: z + .object({ + id: z.string(), + name: z.string(), + envs: z + .object({ + id: z.string(), + name: z.string() + }) + .array() + }) + .array() + }) + .array() + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { connectionId } = req.params; + + const organizations: HumanitecOrgWithApps[] = await server.services.appConnection.humanitec.listOrganizations( + connectionId, + req.permission + ); + + return organizations; + } + }); +}; 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 e86c37753..c2b688a43 100644 --- a/backend/src/server/routes/v1/app-connection-routers/index.ts +++ b/backend/src/server/routes/v1/app-connection-routers/index.ts @@ -6,6 +6,7 @@ import { registerAzureKeyVaultConnectionRouter } from "./azure-key-vault-connect import { registerDatabricksConnectionRouter } from "./databricks-connection-router"; import { registerGcpConnectionRouter } from "./gcp-connection-router"; import { registerGitHubConnectionRouter } from "./github-connection-router"; +import { registerHumanitecConnectionRouter } from "./humanitec-connection-router"; export * from "./app-connection-router"; @@ -16,5 +17,6 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record + registerSyncSecretsEndpoints({ + destination: SecretSync.Humanitec, + server, + responseSchema: HumanitecSyncSchema, + createSchema: CreateHumanitecSyncSchema, + updateSchema: UpdateHumanitecSyncSchema + }); diff --git a/backend/src/server/routes/v1/secret-sync-routers/index.ts b/backend/src/server/routes/v1/secret-sync-routers/index.ts index 137335186..c342f3b73 100644 --- a/backend/src/server/routes/v1/secret-sync-routers/index.ts +++ b/backend/src/server/routes/v1/secret-sync-routers/index.ts @@ -7,6 +7,7 @@ import { registerAzureKeyVaultSyncRouter } from "./azure-key-vault-sync-router"; import { registerDatabricksSyncRouter } from "./databricks-sync-router"; import { registerGcpSyncRouter } from "./gcp-sync-router"; import { registerGitHubSyncRouter } from "./github-sync-router"; +import { registerHumanitecSyncRouter } from "./humanitec-sync-router"; export * from "./secret-sync-router"; @@ -17,5 +18,6 @@ export const SECRET_SYNC_REGISTER_ROUTER_MAP: Record { diff --git a/backend/src/services/app-connection/app-connection-enums.ts b/backend/src/services/app-connection/app-connection-enums.ts index ce116a80a..9da622541 100644 --- a/backend/src/services/app-connection/app-connection-enums.ts +++ b/backend/src/services/app-connection/app-connection-enums.ts @@ -4,7 +4,8 @@ export enum AppConnection { Databricks = "databricks", GCP = "gcp", AzureKeyVault = "azure-key-vault", - AzureAppConfiguration = "azure-app-configuration" + AzureAppConfiguration = "azure-app-configuration", + Humanitec = "humanitec" } export enum AWSRegion { diff --git a/backend/src/services/app-connection/app-connection-fns.ts b/backend/src/services/app-connection/app-connection-fns.ts index c66fe52c4..fe5130ff1 100644 --- a/backend/src/services/app-connection/app-connection-fns.ts +++ b/backend/src/services/app-connection/app-connection-fns.ts @@ -35,6 +35,11 @@ import { getAzureKeyVaultConnectionListItem, validateAzureKeyVaultConnectionCredentials } from "./azure-key-vault"; +import { + getHumanitecConnectionListItem, + HumanitecConnectionMethod, + validateHumanitecConnectionCredentials +} from "./humanitec"; export const listAppConnectionOptions = () => { return [ @@ -43,7 +48,8 @@ export const listAppConnectionOptions = () => { getGcpConnectionListItem(), getAzureKeyVaultConnectionListItem(), getAzureAppConfigurationConnectionListItem(), - getDatabricksConnectionListItem() + getDatabricksConnectionListItem(), + getHumanitecConnectionListItem() ].sort((a, b) => a.name.localeCompare(b.name)); }; @@ -106,6 +112,8 @@ export const validateAppConnectionCredentials = async ( return validateAzureKeyVaultConnectionCredentials(appConnection); case AppConnection.AzureAppConfiguration: return validateAzureAppConfigurationConnectionCredentials(appConnection); + case AppConnection.Humanitec: + return validateHumanitecConnectionCredentials(appConnection); default: // eslint-disable-next-line @typescript-eslint/restrict-template-expressions throw new Error(`Unhandled App Connection ${app}`); @@ -128,6 +136,8 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) => return "Service Account Impersonation"; case DatabricksConnectionMethod.ServicePrincipal: return "Service Principal"; + case HumanitecConnectionMethod.API_TOKEN: + return "API Token"; default: // eslint-disable-next-line @typescript-eslint/restrict-template-expressions throw new Error(`Unhandled App Connection Method: ${method}`); diff --git a/backend/src/services/app-connection/app-connection-maps.ts b/backend/src/services/app-connection/app-connection-maps.ts index 8a045efe4..8a6c65426 100644 --- a/backend/src/services/app-connection/app-connection-maps.ts +++ b/backend/src/services/app-connection/app-connection-maps.ts @@ -6,5 +6,6 @@ export const APP_CONNECTION_NAME_MAP: Record = { [AppConnection.GCP]: "GCP", [AppConnection.AzureKeyVault]: "Azure Key Vault", [AppConnection.AzureAppConfiguration]: "Azure App Configuration", - [AppConnection.Databricks]: "Databricks" + [AppConnection.Databricks]: "Databricks", + [AppConnection.Humanitec]: "Humanitec" }; diff --git a/backend/src/services/app-connection/app-connection-service.ts b/backend/src/services/app-connection/app-connection-service.ts index 8c4dd6a7c..e2e55bba0 100644 --- a/backend/src/services/app-connection/app-connection-service.ts +++ b/backend/src/services/app-connection/app-connection-service.ts @@ -35,6 +35,8 @@ import { ValidateGcpConnectionCredentialsSchema } from "./gcp"; import { gcpConnectionService } from "./gcp/gcp-connection-service"; import { ValidateGitHubConnectionCredentialsSchema } from "./github"; import { githubConnectionService } from "./github/github-connection-service"; +import { ValidateHumanitecConnectionCredentialsSchema } from "./humanitec"; +import { humanitecConnectionService } from "./humanitec/humanitec-connection-service"; export type TAppConnectionServiceFactoryDep = { appConnectionDAL: TAppConnectionDALFactory; @@ -50,7 +52,8 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record { + return { + name: "Humanitec" as const, + app: AppConnection.Humanitec as const, + methods: Object.values(HumanitecConnectionMethod) as [HumanitecConnectionMethod.API_TOKEN] + }; +}; + +export const validateHumanitecConnectionCredentials = async (config: THumanitecConnectionConfig) => { + const { credentials: inputCredentials } = config; + + let response: AxiosResponse | null = null; + + try { + response = await request.get(`${IntegrationUrls.HUMANITEC_API_URL}/orgs`, { + headers: { + Authorization: `Bearer ${inputCredentials.apiToken}` + } + }); + } 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" + }); + } + + if (!response?.data) { + throw new InternalServerError({ + message: "Failed to get organizations: Response was empty" + }); + } + + return inputCredentials; +}; + +export const listOrganizations = async (appConnection: THumanitecConnection): Promise => { + const { + credentials: { apiToken } + } = appConnection; + const response = await request.get(`${IntegrationUrls.HUMANITEC_API_URL}/orgs`, { + headers: { + Authorization: `Bearer ${apiToken}` + } + }); + + if (!response.data) { + throw new InternalServerError({ + message: "Failed to get organizations: Response was empty" + }); + } + const orgs = response.data; + const orgsWithApps: HumanitecOrgWithApps[] = []; + + for (const org of orgs) { + // eslint-disable-next-line no-await-in-loop + const appsResponse = await request.get(`${IntegrationUrls.HUMANITEC_API_URL}/orgs/${org.id}/apps`, { + headers: { + Authorization: `Bearer ${apiToken}` + } + }); + + if (appsResponse.data) { + const apps = appsResponse.data; + orgsWithApps.push({ + ...org, + apps: apps.map((app) => ({ + name: app.name, + id: app.id, + envs: app.envs + })) + }); + } + } + return orgsWithApps; +}; diff --git a/backend/src/services/app-connection/humanitec/humanitec-connection-schemas.ts b/backend/src/services/app-connection/humanitec/humanitec-connection-schemas.ts new file mode 100644 index 000000000..145f78b85 --- /dev/null +++ b/backend/src/services/app-connection/humanitec/humanitec-connection-schemas.ts @@ -0,0 +1,58 @@ +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 { HumanitecConnectionMethod } from "./humanitec-connection-enums"; + +export const HumanitecConnectionAccessTokenCredentialsSchema = z.object({ + apiToken: z.string().trim().min(1, "API Token required") +}); + +const BaseHumanitecConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.Humanitec) }); + +export const HumanitecConnectionSchema = BaseHumanitecConnectionSchema.extend({ + method: z.literal(HumanitecConnectionMethod.API_TOKEN), + credentials: HumanitecConnectionAccessTokenCredentialsSchema +}); + +export const SanitizedHumanitecConnectionSchema = z.discriminatedUnion("method", [ + BaseHumanitecConnectionSchema.extend({ + method: z.literal(HumanitecConnectionMethod.API_TOKEN), + credentials: HumanitecConnectionAccessTokenCredentialsSchema.pick({}) + }) +]); + +export const ValidateHumanitecConnectionCredentialsSchema = z.discriminatedUnion("method", [ + z.object({ + method: z + .literal(HumanitecConnectionMethod.API_TOKEN) + .describe(AppConnections?.CREATE(AppConnection.Humanitec).method), + credentials: HumanitecConnectionAccessTokenCredentialsSchema.describe( + AppConnections.CREATE(AppConnection.Humanitec).credentials + ) + }) +]); + +export const CreateHumanitecConnectionSchema = ValidateHumanitecConnectionCredentialsSchema.and( + GenericCreateAppConnectionFieldsSchema(AppConnection.Humanitec) +); + +export const UpdateHumanitecConnectionSchema = z + .object({ + credentials: HumanitecConnectionAccessTokenCredentialsSchema.optional().describe( + AppConnections.UPDATE(AppConnection.Humanitec).credentials + ) + }) + .and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Humanitec)); + +export const HumanitecConnectionListItemSchema = z.object({ + name: z.literal("Humanitec"), + app: z.literal(AppConnection.Humanitec), + methods: z.nativeEnum(HumanitecConnectionMethod).array() +}); diff --git a/backend/src/services/app-connection/humanitec/humanitec-connection-service.ts b/backend/src/services/app-connection/humanitec/humanitec-connection-service.ts new file mode 100644 index 000000000..5ade43450 --- /dev/null +++ b/backend/src/services/app-connection/humanitec/humanitec-connection-service.ts @@ -0,0 +1,29 @@ +import { logger } from "@app/lib/logger"; +import { OrgServiceActor } from "@app/lib/types"; + +import { AppConnection } from "../app-connection-enums"; +import { listOrganizations as getHumanitecOrganizations } from "./humanitec-connection-fns"; +import { THumanitecConnection } from "./humanitec-connection-types"; + +type TGetAppConnectionFunc = ( + app: AppConnection, + connectionId: string, + actor: OrgServiceActor +) => Promise; + +export const humanitecConnectionService = (getAppConnection: TGetAppConnectionFunc) => { + const listOrganizations = async (connectionId: string, actor: OrgServiceActor) => { + const appConnection = await getAppConnection(AppConnection.Humanitec, connectionId, actor); + try { + const organizations = await getHumanitecOrganizations(appConnection); + return organizations; + } catch (error) { + logger.error(error, "Failed to establish connection with Humanitec"); + return []; + } + }; + + return { + listOrganizations + }; +}; diff --git a/backend/src/services/app-connection/humanitec/humanitec-connection-types.ts b/backend/src/services/app-connection/humanitec/humanitec-connection-types.ts new file mode 100644 index 000000000..94613bfba --- /dev/null +++ b/backend/src/services/app-connection/humanitec/humanitec-connection-types.ts @@ -0,0 +1,40 @@ +import z from "zod"; + +import { DiscriminativePick } from "@app/lib/types"; + +import { AppConnection } from "../app-connection-enums"; +import { + CreateHumanitecConnectionSchema, + HumanitecConnectionSchema, + ValidateHumanitecConnectionCredentialsSchema +} from "./humanitec-connection-schemas"; + +export type THumanitecConnection = z.infer; + +export type THumanitecConnectionInput = z.infer & { + app: AppConnection.Humanitec; +}; + +export type TValidateHumanitecConnectionCredentials = typeof ValidateHumanitecConnectionCredentialsSchema; + +export type THumanitecConnectionConfig = DiscriminativePick< + THumanitecConnectionInput, + "method" | "app" | "credentials" +> & { + orgId: string; +}; + +export type HumanitecOrg = { + id: string; + name: string; +}; + +export type HumanitecApp = { + name: string; + id: string; + envs: { name: string; id: string }[]; +}; + +export type HumanitecOrgWithApps = HumanitecOrg & { + apps: HumanitecApp[]; +}; diff --git a/backend/src/services/app-connection/humanitec/index.ts b/backend/src/services/app-connection/humanitec/index.ts new file mode 100644 index 000000000..52fb6b3c2 --- /dev/null +++ b/backend/src/services/app-connection/humanitec/index.ts @@ -0,0 +1,4 @@ +export * from "./humanitec-connection-enums"; +export * from "./humanitec-connection-fns"; +export * from "./humanitec-connection-schemas"; +export * from "./humanitec-connection-types"; diff --git a/backend/src/services/integration-auth/integration-list.ts b/backend/src/services/integration-auth/integration-list.ts index 16a717bd6..d6c450751 100644 --- a/backend/src/services/integration-auth/integration-list.ts +++ b/backend/src/services/integration-auth/integration-list.ts @@ -93,6 +93,7 @@ export enum IntegrationUrls { NORTHFLANK_API_URL = "https://api.northflank.com", HASURA_CLOUD_API_URL = "https://data.pro.hasura.io/v1/graphql", AZURE_DEVOPS_API_URL = "https://dev.azure.com", + HUMANITEC_API_URL = "https://api.humanitec.io", GCP_SECRET_MANAGER_SERVICE_NAME = "secretmanager.googleapis.com", GCP_SECRET_MANAGER_URL = `https://${GCP_SECRET_MANAGER_SERVICE_NAME}`, diff --git a/backend/src/services/secret-sync/humanitec/humanitec-sync-constants.ts b/backend/src/services/secret-sync/humanitec/humanitec-sync-constants.ts new file mode 100644 index 000000000..d81cfd041 --- /dev/null +++ b/backend/src/services/secret-sync/humanitec/humanitec-sync-constants.ts @@ -0,0 +1,10 @@ +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; +import { TSecretSyncListItem } from "@app/services/secret-sync/secret-sync-types"; + +export const HUMANITEC_SYNC_LIST_OPTION: TSecretSyncListItem = { + name: "Humanitec", + destination: SecretSync.Humanitec, + connection: AppConnection.Humanitec, + canImportSecrets: false +}; diff --git a/backend/src/services/secret-sync/humanitec/humanitec-sync-enums.ts b/backend/src/services/secret-sync/humanitec/humanitec-sync-enums.ts new file mode 100644 index 000000000..eb86fdf4f --- /dev/null +++ b/backend/src/services/secret-sync/humanitec/humanitec-sync-enums.ts @@ -0,0 +1,4 @@ +export enum HumanitecSyncScope { + Application = "application", + Environment = "environment" +} diff --git a/backend/src/services/secret-sync/humanitec/humanitec-sync-fns.ts b/backend/src/services/secret-sync/humanitec/humanitec-sync-fns.ts new file mode 100644 index 000000000..a07d2d0b5 --- /dev/null +++ b/backend/src/services/secret-sync/humanitec/humanitec-sync-fns.ts @@ -0,0 +1,218 @@ +import { request } from "@app/lib/config/request"; +import { logger } from "@app/lib/logger"; +import { IntegrationUrls } from "@app/services/integration-auth/integration-list"; +import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors"; +import { SECRET_SYNC_NAME_MAP } from "@app/services/secret-sync/secret-sync-maps"; +import { TSecretMap } from "@app/services/secret-sync/secret-sync-types"; + +import { HumanitecSyncScope } from "./humanitec-sync-enums"; +import { HumanitecSecret, THumanitecSyncWithCredentials } from "./humanitec-sync-types"; + +const getHumanitecSecrets = async (secretSync: THumanitecSyncWithCredentials) => { + const { + destinationConfig, + connection: { + credentials: { apiToken } + } + } = secretSync; + + let url = `${IntegrationUrls.HUMANITEC_API_URL}/orgs/${destinationConfig.org}/apps/${destinationConfig.app}`; + if (destinationConfig.scope === HumanitecSyncScope.Environment) { + url += `/envs/${destinationConfig.env}`; + } + url += "/values"; + + const { data } = await request.get(url, { + headers: { + Authorization: `Bearer ${apiToken}`, + Accept: "application/json" + } + }); + + return data; +}; + +const deleteSecret = async (secretSync: THumanitecSyncWithCredentials, encryptedSecret: HumanitecSecret) => { + const { + destinationConfig, + connection: { + credentials: { apiToken } + } + } = secretSync; + + if (destinationConfig.scope === HumanitecSyncScope.Environment && encryptedSecret.source === "app") { + logger.info( + `Humanitec secret ${encryptedSecret.key} on app ${destinationConfig.app} has no environment override, not deleted as it is an app-level secret` + ); + return; + } + + try { + let url = `${IntegrationUrls.HUMANITEC_API_URL}/orgs/${destinationConfig.org}/apps/${destinationConfig.app}`; + if (destinationConfig.scope === HumanitecSyncScope.Environment) { + url += `/envs/${destinationConfig.env}`; + } + url += `/values/${encryptedSecret.key}`; + + await request.delete(url, { + headers: { + Authorization: `Bearer ${apiToken}`, + Accept: "application/json" + } + }); + } catch (error) { + throw new SecretSyncError({ + error, + secretKey: encryptedSecret.key + }); + } +}; + +const createSecret = async (secretSync: THumanitecSyncWithCredentials, secretMap: TSecretMap, key: string) => { + try { + const { + destinationConfig, + connection: { + credentials: { apiToken } + } + } = secretSync; + + const appLevelSecret = destinationConfig.scope === HumanitecSyncScope.Application ? secretMap[key].value : ""; + await request.post( + `${IntegrationUrls.HUMANITEC_API_URL}/orgs/${destinationConfig.org}/apps/${destinationConfig.app}/values`, + { + key, + value: appLevelSecret, + description: secretMap[key].comment || "", + is_secret: true + }, + { + headers: { + Authorization: `Bearer ${apiToken}`, + Accept: "application/json" + } + } + ); + if (destinationConfig.scope === HumanitecSyncScope.Environment) { + await request.patch( + `${IntegrationUrls.HUMANITEC_API_URL}/orgs/${destinationConfig.org}/apps/${destinationConfig.app}/envs/${destinationConfig.env}/values/${key}`, + { + value: secretMap[key].value, + description: secretMap[key].comment || "" + }, + { + headers: { + Authorization: `Bearer ${apiToken}`, + Accept: "application/json" + } + } + ); + } + } catch (error) { + throw new SecretSyncError({ + error, + secretKey: key + }); + } +}; + +const updateSecret = async ( + secretSync: THumanitecSyncWithCredentials, + secretMap: TSecretMap, + encryptedSecret: HumanitecSecret +) => { + try { + const { + destinationConfig, + connection: { + credentials: { apiToken } + } + } = secretSync; + if (destinationConfig.scope === HumanitecSyncScope.Application) { + await request.patch( + `${IntegrationUrls.HUMANITEC_API_URL}/orgs/${destinationConfig.org}/apps/${destinationConfig.app}/values/${encryptedSecret.key}`, + { + value: secretMap[encryptedSecret.key].value, + description: secretMap[encryptedSecret.key].comment || "" + }, + { + headers: { + Authorization: `Bearer ${apiToken}`, + Accept: "application/json" + } + } + ); + } else if (encryptedSecret.source === "app") { + await request.post( + `${IntegrationUrls.HUMANITEC_API_URL}/orgs/${destinationConfig.org}/apps/${destinationConfig.app}/envs/${destinationConfig.env}/values`, + { + value: secretMap[encryptedSecret.key].value, + description: secretMap[encryptedSecret.key].comment || "", + key: encryptedSecret.key, + is_secret: true + }, + { + headers: { + Authorization: `Bearer ${apiToken}`, + Accept: "application/json" + } + } + ); + } else { + await request.patch( + `${IntegrationUrls.HUMANITEC_API_URL}/orgs/${destinationConfig.org}/apps/${destinationConfig.app}/envs/${destinationConfig.env}/values/${encryptedSecret.key}`, + { + value: secretMap[encryptedSecret.key].value, + description: secretMap[encryptedSecret.key].comment || "" + }, + { + headers: { + Authorization: `Bearer ${apiToken}`, + Accept: "application/json" + } + } + ); + } + } catch (error) { + throw new SecretSyncError({ + error, + secretKey: encryptedSecret.key + }); + } +}; + +export const HumanitecSyncFns = { + syncSecrets: async (secretSync: THumanitecSyncWithCredentials, secretMap: TSecretMap) => { + const humanitecSecrets = await getHumanitecSecrets(secretSync); + const humanitecSecretsKeys = new Map(humanitecSecrets.map((s) => [s.key, s])); + + for await (const key of Object.keys(secretMap)) { + const existingSecret = humanitecSecretsKeys.get(key); + + if (!existingSecret) { + await createSecret(secretSync, secretMap, key); + } else { + await updateSecret(secretSync, secretMap, existingSecret); + } + } + + for await (const humanitecSecret of humanitecSecrets) { + if (!secretMap[humanitecSecret.key]) { + await deleteSecret(secretSync, humanitecSecret); + } + } + }, + getSecrets: async (secretSync: THumanitecSyncWithCredentials): Promise => { + throw new Error(`${SECRET_SYNC_NAME_MAP[secretSync.destination]} does not support importing secrets.`); + }, + + removeSecrets: async (secretSync: THumanitecSyncWithCredentials, secretMap: TSecretMap) => { + const encryptedSecrets = await getHumanitecSecrets(secretSync); + + for await (const encryptedSecret of encryptedSecrets) { + if (encryptedSecret.key in secretMap) { + await deleteSecret(secretSync, encryptedSecret); + } + } + } +}; diff --git a/backend/src/services/secret-sync/humanitec/humanitec-sync-schemas.ts b/backend/src/services/secret-sync/humanitec/humanitec-sync-schemas.ts new file mode 100644 index 000000000..cd90ecfdc --- /dev/null +++ b/backend/src/services/secret-sync/humanitec/humanitec-sync-schemas.ts @@ -0,0 +1,54 @@ +import { z } from "zod"; + +import { SecretSyncs } from "@app/lib/api-docs"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { HumanitecSyncScope } from "@app/services/secret-sync/humanitec/humanitec-sync-enums"; +import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; +import { + BaseSecretSyncSchema, + GenericCreateSecretSyncFieldsSchema, + GenericUpdateSecretSyncFieldsSchema +} from "@app/services/secret-sync/secret-sync-schemas"; +import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types"; + +const HumanitecSyncDestinationConfigSchema = z.discriminatedUnion("scope", [ + z.object({ + scope: z.literal(HumanitecSyncScope.Application).describe(SecretSyncs.DESTINATION_CONFIG.HUMANITEC.scope), + org: z.string().min(1, "Org ID is required").describe(SecretSyncs.DESTINATION_CONFIG.HUMANITEC.org), + app: z.string().min(1, "App ID is required").describe(SecretSyncs.DESTINATION_CONFIG.HUMANITEC.app) + }), + z.object({ + scope: z.literal(HumanitecSyncScope.Environment).describe(SecretSyncs.DESTINATION_CONFIG.HUMANITEC.scope), + org: z.string().min(1, "Org ID is required").describe(SecretSyncs.DESTINATION_CONFIG.HUMANITEC.org), + app: z.string().min(1, "App ID is required").describe(SecretSyncs.DESTINATION_CONFIG.HUMANITEC.app), + env: z.string().min(1, "Env ID is required").describe(SecretSyncs.DESTINATION_CONFIG.HUMANITEC.env) + }) +]); + +const HumanitecSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: false }; + +export const HumanitecSyncSchema = BaseSecretSyncSchema(SecretSync.Humanitec, HumanitecSyncOptionsConfig).extend({ + destination: z.literal(SecretSync.Humanitec), + destinationConfig: HumanitecSyncDestinationConfigSchema +}); + +export const CreateHumanitecSyncSchema = GenericCreateSecretSyncFieldsSchema( + SecretSync.Humanitec, + HumanitecSyncOptionsConfig +).extend({ + destinationConfig: HumanitecSyncDestinationConfigSchema +}); + +export const UpdateHumanitecSyncSchema = GenericUpdateSecretSyncFieldsSchema( + SecretSync.Humanitec, + HumanitecSyncOptionsConfig +).extend({ + destinationConfig: HumanitecSyncDestinationConfigSchema.optional() +}); + +export const HumanitecSyncListItemSchema = z.object({ + name: z.literal("Humanitec"), + connection: z.literal(AppConnection.Humanitec), + destination: z.literal(SecretSync.Humanitec), + canImportSecrets: z.literal(false) +}); diff --git a/backend/src/services/secret-sync/humanitec/humanitec-sync-types.ts b/backend/src/services/secret-sync/humanitec/humanitec-sync-types.ts new file mode 100644 index 000000000..d49e4401e --- /dev/null +++ b/backend/src/services/secret-sync/humanitec/humanitec-sync-types.ts @@ -0,0 +1,23 @@ +import z from "zod"; + +import { THumanitecConnection } from "@app/services/app-connection/humanitec"; + +import { CreateHumanitecSyncSchema, HumanitecSyncListItemSchema, HumanitecSyncSchema } from "./humanitec-sync-schemas"; + +export type THumanitecSyncListItem = z.infer; + +export type THumanitecSync = z.infer; + +export type THumanitecSyncInput = z.infer; + +export type THumanitecSyncWithCredentials = THumanitecSync & { + connection: THumanitecConnection; +}; + +export type HumanitecSecret = { + description: string; + is_secret: boolean; + key: string; + source: "app" | "env"; + value: string; +}; diff --git a/backend/src/services/secret-sync/humanitec/index.ts b/backend/src/services/secret-sync/humanitec/index.ts new file mode 100644 index 000000000..c1095fda0 --- /dev/null +++ b/backend/src/services/secret-sync/humanitec/index.ts @@ -0,0 +1,5 @@ +export * from "./humanitec-sync-constants"; +export * from "./humanitec-sync-enums"; +export * from "./humanitec-sync-fns"; +export * from "./humanitec-sync-schemas"; +export * from "./humanitec-sync-types"; diff --git a/backend/src/services/secret-sync/secret-sync-enums.ts b/backend/src/services/secret-sync/secret-sync-enums.ts index 743b900bc..006d033f5 100644 --- a/backend/src/services/secret-sync/secret-sync-enums.ts +++ b/backend/src/services/secret-sync/secret-sync-enums.ts @@ -5,7 +5,8 @@ export enum SecretSync { GCPSecretManager = "gcp-secret-manager", AzureKeyVault = "azure-key-vault", AzureAppConfiguration = "azure-app-configuration", - Databricks = "databricks" + Databricks = "databricks", + Humanitec = "humanitec" } export enum SecretSyncInitialSyncBehavior { diff --git a/backend/src/services/secret-sync/secret-sync-fns.ts b/backend/src/services/secret-sync/secret-sync-fns.ts index 597c7fc01..6c8a6d4df 100644 --- a/backend/src/services/secret-sync/secret-sync-fns.ts +++ b/backend/src/services/secret-sync/secret-sync-fns.ts @@ -24,6 +24,8 @@ import { AZURE_APP_CONFIGURATION_SYNC_LIST_OPTION, azureAppConfigurationSyncFact import { AZURE_KEY_VAULT_SYNC_LIST_OPTION, azureKeyVaultSyncFactory } from "./azure-key-vault"; import { GCP_SYNC_LIST_OPTION } from "./gcp"; import { GcpSyncFns } from "./gcp/gcp-sync-fns"; +import { HUMANITEC_SYNC_LIST_OPTION } from "./humanitec"; +import { HumanitecSyncFns } from "./humanitec/humanitec-sync-fns"; const SECRET_SYNC_LIST_OPTIONS: Record = { [SecretSync.AWSParameterStore]: AWS_PARAMETER_STORE_SYNC_LIST_OPTION, @@ -32,7 +34,8 @@ const SECRET_SYNC_LIST_OPTIONS: Record = { [SecretSync.GCPSecretManager]: GCP_SYNC_LIST_OPTION, [SecretSync.AzureKeyVault]: AZURE_KEY_VAULT_SYNC_LIST_OPTION, [SecretSync.AzureAppConfiguration]: AZURE_APP_CONFIGURATION_SYNC_LIST_OPTION, - [SecretSync.Databricks]: DATABRICKS_SYNC_LIST_OPTION + [SecretSync.Databricks]: DATABRICKS_SYNC_LIST_OPTION, + [SecretSync.Humanitec]: HUMANITEC_SYNC_LIST_OPTION }; export const listSecretSyncOptions = () => { @@ -116,6 +119,8 @@ export const SecretSyncFns = { appConnectionDAL, kmsService }).syncSecrets(secretSync, secretMap); + case SecretSync.Humanitec: + return HumanitecSyncFns.syncSecrets(secretSync, secretMap); default: throw new Error( `Unhandled sync destination for sync secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}` @@ -157,6 +162,9 @@ export const SecretSyncFns = { appConnectionDAL, kmsService }).getSecrets(secretSync); + case SecretSync.Humanitec: + secretMap = await HumanitecSyncFns.getSecrets(secretSync); + break; default: throw new Error( `Unhandled sync destination for get secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}` @@ -197,6 +205,8 @@ export const SecretSyncFns = { appConnectionDAL, kmsService }).removeSecrets(secretSync, secretMap); + case SecretSync.Humanitec: + return HumanitecSyncFns.removeSecrets(secretSync, secretMap); default: throw new Error( `Unhandled sync destination for remove secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}` diff --git a/backend/src/services/secret-sync/secret-sync-maps.ts b/backend/src/services/secret-sync/secret-sync-maps.ts index 92546706d..cd4125e1b 100644 --- a/backend/src/services/secret-sync/secret-sync-maps.ts +++ b/backend/src/services/secret-sync/secret-sync-maps.ts @@ -8,7 +8,8 @@ export const SECRET_SYNC_NAME_MAP: Record = { [SecretSync.GCPSecretManager]: "GCP Secret Manager", [SecretSync.AzureKeyVault]: "Azure Key Vault", [SecretSync.AzureAppConfiguration]: "Azure App Configuration", - [SecretSync.Databricks]: "Databricks" + [SecretSync.Databricks]: "Databricks", + [SecretSync.Humanitec]: "Humanitec" }; export const SECRET_SYNC_CONNECTION_MAP: Record = { @@ -18,5 +19,6 @@ export const SECRET_SYNC_CONNECTION_MAP: Record = { [SecretSync.GCPSecretManager]: AppConnection.GCP, [SecretSync.AzureKeyVault]: AppConnection.AzureKeyVault, [SecretSync.AzureAppConfiguration]: AppConnection.AzureAppConfiguration, - [SecretSync.Databricks]: AppConnection.Databricks + [SecretSync.Databricks]: AppConnection.Databricks, + [SecretSync.Humanitec]: AppConnection.Humanitec }; diff --git a/backend/src/services/secret-sync/secret-sync-types.ts b/backend/src/services/secret-sync/secret-sync-types.ts index 60742e797..2044c7c17 100644 --- a/backend/src/services/secret-sync/secret-sync-types.ts +++ b/backend/src/services/secret-sync/secret-sync-types.ts @@ -43,6 +43,12 @@ import { TAzureKeyVaultSyncWithCredentials } from "./azure-key-vault"; import { TGcpSync, TGcpSyncInput, TGcpSyncListItem, TGcpSyncWithCredentials } from "./gcp"; +import { + THumanitecSync, + THumanitecSyncInput, + THumanitecSyncListItem, + THumanitecSyncWithCredentials +} from "./humanitec"; export type TSecretSync = | TAwsParameterStoreSync @@ -51,7 +57,8 @@ export type TSecretSync = | TGcpSync | TAzureKeyVaultSync | TAzureAppConfigurationSync - | TDatabricksSync; + | TDatabricksSync + | THumanitecSync; export type TSecretSyncWithCredentials = | TAwsParameterStoreSyncWithCredentials @@ -60,7 +67,8 @@ export type TSecretSyncWithCredentials = | TGcpSyncWithCredentials | TAzureKeyVaultSyncWithCredentials | TAzureAppConfigurationSyncWithCredentials - | TDatabricksSyncWithCredentials; + | TDatabricksSyncWithCredentials + | THumanitecSyncWithCredentials; export type TSecretSyncInput = | TAwsParameterStoreSyncInput @@ -69,7 +77,8 @@ export type TSecretSyncInput = | TGcpSyncInput | TAzureKeyVaultSyncInput | TAzureAppConfigurationSyncInput - | TDatabricksSyncInput; + | TDatabricksSyncInput + | THumanitecSyncInput; export type TSecretSyncListItem = | TAwsParameterStoreSyncListItem @@ -78,7 +87,8 @@ export type TSecretSyncListItem = | TGcpSyncListItem | TAzureKeyVaultSyncListItem | TAzureAppConfigurationSyncListItem - | TDatabricksSyncListItem; + | TDatabricksSyncListItem + | THumanitecSyncListItem; export type TSyncOptionsConfig = { canImportSecrets: boolean; diff --git a/docs/api-reference/endpoints/app-connections/humanitec/available.mdx b/docs/api-reference/endpoints/app-connections/humanitec/available.mdx new file mode 100644 index 000000000..eb95b2e54 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/humanitec/available.mdx @@ -0,0 +1,4 @@ +--- +title: "Available" +openapi: "GET /api/v1/app-connections/humanitec/available" +--- diff --git a/docs/api-reference/endpoints/app-connections/humanitec/create.mdx b/docs/api-reference/endpoints/app-connections/humanitec/create.mdx new file mode 100644 index 000000000..a4d196911 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/humanitec/create.mdx @@ -0,0 +1,4 @@ +--- +title: "Create" +openapi: "POST /api/v1/app-connections/humanitec" +--- \ No newline at end of file diff --git a/docs/api-reference/endpoints/app-connections/humanitec/delete.mdx b/docs/api-reference/endpoints/app-connections/humanitec/delete.mdx new file mode 100644 index 000000000..d8786e08a --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/humanitec/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/app-connections/humanitec/{connectionId}" +--- diff --git a/docs/api-reference/endpoints/app-connections/humanitec/get-by-id.mdx b/docs/api-reference/endpoints/app-connections/humanitec/get-by-id.mdx new file mode 100644 index 000000000..22473a7a1 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/humanitec/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by ID" +openapi: "GET /api/v1/app-connections/humanitec/{connectionId}" +--- diff --git a/docs/api-reference/endpoints/app-connections/humanitec/get-by-name.mdx b/docs/api-reference/endpoints/app-connections/humanitec/get-by-name.mdx new file mode 100644 index 000000000..fd848ae4f --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/humanitec/get-by-name.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by Name" +openapi: "GET /api/v1/app-connections/humanitec/connection-name/{connectionName}" +--- diff --git a/docs/api-reference/endpoints/app-connections/humanitec/list.mdx b/docs/api-reference/endpoints/app-connections/humanitec/list.mdx new file mode 100644 index 000000000..07f30f674 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/humanitec/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v1/app-connections/humanitec" +--- diff --git a/docs/api-reference/endpoints/app-connections/humanitec/update.mdx b/docs/api-reference/endpoints/app-connections/humanitec/update.mdx new file mode 100644 index 000000000..2a0806324 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/humanitec/update.mdx @@ -0,0 +1,4 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/app-connections/humanitec/{connectionId}" +--- \ No newline at end of file diff --git a/docs/api-reference/endpoints/secret-syncs/humanitec/create.mdx b/docs/api-reference/endpoints/secret-syncs/humanitec/create.mdx new file mode 100644 index 000000000..f683d67fe --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/humanitec/create.mdx @@ -0,0 +1,4 @@ +--- +title: "Create" +openapi: "POST /api/v1/secret-syncs/humanitec" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/humanitec/delete.mdx b/docs/api-reference/endpoints/secret-syncs/humanitec/delete.mdx new file mode 100644 index 000000000..ceef1fbb4 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/humanitec/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/secret-syncs/humanitec/{syncId}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/humanitec/get-by-id.mdx b/docs/api-reference/endpoints/secret-syncs/humanitec/get-by-id.mdx new file mode 100644 index 000000000..a8a2a9bfc --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/humanitec/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by ID" +openapi: "GET /api/v1/secret-syncs/humanitec/{syncId}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/humanitec/get-by-name.mdx b/docs/api-reference/endpoints/secret-syncs/humanitec/get-by-name.mdx new file mode 100644 index 000000000..ad2f11290 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/humanitec/get-by-name.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by Name" +openapi: "GET /api/v1/secret-syncs/humanitec/sync-name/{syncName}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/humanitec/list.mdx b/docs/api-reference/endpoints/secret-syncs/humanitec/list.mdx new file mode 100644 index 000000000..651e7a435 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/humanitec/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v1/secret-syncs/humanitec" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/humanitec/remove-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/humanitec/remove-secrets.mdx new file mode 100644 index 000000000..7c5148638 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/humanitec/remove-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Remove Secrets" +openapi: "POST /api/v1/secret-syncs/humanitec/{syncId}/remove-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/humanitec/sync-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/humanitec/sync-secrets.mdx new file mode 100644 index 000000000..cb0446f11 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/humanitec/sync-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Sync Secrets" +openapi: "POST /api/v1/secret-syncs/humanitec/{syncId}/sync-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/humanitec/update.mdx b/docs/api-reference/endpoints/secret-syncs/humanitec/update.mdx new file mode 100644 index 000000000..9e958555f --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/humanitec/update.mdx @@ -0,0 +1,4 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/secret-syncs/humanitec/{syncId}" +--- diff --git a/docs/images/app-connections/humanitec/add-humanitec-connection.png b/docs/images/app-connections/humanitec/add-humanitec-connection.png new file mode 100644 index 000000000..9ae9a7bf1 Binary files /dev/null and b/docs/images/app-connections/humanitec/add-humanitec-connection.png differ diff --git a/docs/images/app-connections/humanitec/humanitec-add-api-token.png b/docs/images/app-connections/humanitec/humanitec-add-api-token.png new file mode 100644 index 000000000..6eaf796c4 Binary files /dev/null and b/docs/images/app-connections/humanitec/humanitec-add-api-token.png differ diff --git a/docs/images/app-connections/humanitec/humanitec-add-user-options.png b/docs/images/app-connections/humanitec/humanitec-add-user-options.png new file mode 100644 index 000000000..ad6d28343 Binary files /dev/null and b/docs/images/app-connections/humanitec/humanitec-add-user-options.png differ diff --git a/docs/images/app-connections/humanitec/humanitec-add-user-role.png b/docs/images/app-connections/humanitec/humanitec-add-user-role.png new file mode 100644 index 000000000..71f7647e6 Binary files /dev/null and b/docs/images/app-connections/humanitec/humanitec-add-user-role.png differ diff --git a/docs/images/app-connections/humanitec/humanitec-add-user.png b/docs/images/app-connections/humanitec/humanitec-add-user.png new file mode 100644 index 000000000..d675c8336 Binary files /dev/null and b/docs/images/app-connections/humanitec/humanitec-add-user.png differ diff --git a/docs/images/app-connections/humanitec/humanitec-app-connection-created.png b/docs/images/app-connections/humanitec/humanitec-app-connection-created.png new file mode 100644 index 000000000..0120adfb5 Binary files /dev/null and b/docs/images/app-connections/humanitec/humanitec-app-connection-created.png differ diff --git a/docs/images/app-connections/humanitec/humanitec-app-connection-modal.png b/docs/images/app-connections/humanitec/humanitec-app-connection-modal.png new file mode 100644 index 000000000..9a96d6cee Binary files /dev/null and b/docs/images/app-connections/humanitec/humanitec-app-connection-modal.png differ diff --git a/docs/images/app-connections/humanitec/humanitec-app-connection-option.png b/docs/images/app-connections/humanitec/humanitec-app-connection-option.png new file mode 100644 index 000000000..1f1f724b2 Binary files /dev/null and b/docs/images/app-connections/humanitec/humanitec-app-connection-option.png differ diff --git a/docs/images/app-connections/humanitec/humanitec-applications-tab.png b/docs/images/app-connections/humanitec/humanitec-applications-tab.png new file mode 100644 index 000000000..c97bb96d9 Binary files /dev/null and b/docs/images/app-connections/humanitec/humanitec-applications-tab.png differ diff --git a/docs/images/app-connections/humanitec/humanitec-connection.png b/docs/images/app-connections/humanitec/humanitec-connection.png new file mode 100644 index 000000000..b18ac8aac Binary files /dev/null and b/docs/images/app-connections/humanitec/humanitec-connection.png differ diff --git a/docs/images/app-connections/humanitec/humanitec-copy-api-token.png b/docs/images/app-connections/humanitec/humanitec-copy-api-token.png new file mode 100644 index 000000000..29f67954b Binary files /dev/null and b/docs/images/app-connections/humanitec/humanitec-copy-api-token.png differ diff --git a/docs/images/app-connections/humanitec/humanitec-create-api-token.png b/docs/images/app-connections/humanitec/humanitec-create-api-token.png new file mode 100644 index 000000000..1cb198129 Binary files /dev/null and b/docs/images/app-connections/humanitec/humanitec-create-api-token.png differ diff --git a/docs/images/app-connections/humanitec/humanitec-create-new-user.png b/docs/images/app-connections/humanitec/humanitec-create-new-user.png new file mode 100644 index 000000000..f1ac7d029 Binary files /dev/null and b/docs/images/app-connections/humanitec/humanitec-create-new-user.png differ diff --git a/docs/images/app-connections/humanitec/humanitec-service-account-filled.png b/docs/images/app-connections/humanitec/humanitec-service-account-filled.png new file mode 100644 index 000000000..023253b88 Binary files /dev/null and b/docs/images/app-connections/humanitec/humanitec-service-account-filled.png differ diff --git a/docs/images/app-connections/humanitec/humanitec-service-users.png b/docs/images/app-connections/humanitec/humanitec-service-users.png new file mode 100644 index 000000000..3b95403ff Binary files /dev/null and b/docs/images/app-connections/humanitec/humanitec-service-users.png differ diff --git a/docs/images/app-connections/humanitec/humanitec-user-added.png b/docs/images/app-connections/humanitec/humanitec-user-added.png new file mode 100644 index 000000000..fe918a7bd Binary files /dev/null and b/docs/images/app-connections/humanitec/humanitec-user-added.png differ diff --git a/docs/images/app-connections/humanitec/select-humanitec-connection.png b/docs/images/app-connections/humanitec/select-humanitec-connection.png new file mode 100644 index 000000000..70d430cfc Binary files /dev/null and b/docs/images/app-connections/humanitec/select-humanitec-connection.png differ diff --git a/docs/images/secret-syncs/humanitec/humanitec-created.png b/docs/images/secret-syncs/humanitec/humanitec-created.png new file mode 100644 index 000000000..19e5d503a Binary files /dev/null and b/docs/images/secret-syncs/humanitec/humanitec-created.png differ diff --git a/docs/images/secret-syncs/humanitec/humanitec-destination.png b/docs/images/secret-syncs/humanitec/humanitec-destination.png new file mode 100644 index 000000000..81605e9e2 Binary files /dev/null and b/docs/images/secret-syncs/humanitec/humanitec-destination.png differ diff --git a/docs/images/secret-syncs/humanitec/humanitec-details.png b/docs/images/secret-syncs/humanitec/humanitec-details.png new file mode 100644 index 000000000..172fde3f7 Binary files /dev/null and b/docs/images/secret-syncs/humanitec/humanitec-details.png differ diff --git a/docs/images/secret-syncs/humanitec/humanitec-options.png b/docs/images/secret-syncs/humanitec/humanitec-options.png new file mode 100644 index 000000000..bc907bbfc Binary files /dev/null and b/docs/images/secret-syncs/humanitec/humanitec-options.png differ diff --git a/docs/images/secret-syncs/humanitec/humanitec-review.png b/docs/images/secret-syncs/humanitec/humanitec-review.png new file mode 100644 index 000000000..2ffda9240 Binary files /dev/null and b/docs/images/secret-syncs/humanitec/humanitec-review.png differ diff --git a/docs/images/secret-syncs/humanitec/humanitec-source.png b/docs/images/secret-syncs/humanitec/humanitec-source.png new file mode 100644 index 000000000..ff50c11d8 Binary files /dev/null and b/docs/images/secret-syncs/humanitec/humanitec-source.png differ diff --git a/docs/images/secret-syncs/humanitec/select-humanitec-option.png b/docs/images/secret-syncs/humanitec/select-humanitec-option.png new file mode 100644 index 000000000..bb0cb9aed Binary files /dev/null and b/docs/images/secret-syncs/humanitec/select-humanitec-option.png differ diff --git a/docs/integrations/app-connections/humanitec.mdx b/docs/integrations/app-connections/humanitec.mdx new file mode 100644 index 000000000..570d3ba5d --- /dev/null +++ b/docs/integrations/app-connections/humanitec.mdx @@ -0,0 +1,71 @@ +--- +title: "Humanitec Connection" +description: "Learn how to configure a Humanitec Connection for Infisical." +--- + +Infisical supports connecting to Humanitec using a service user. + +## Setup Humanitec Connection in Infisical + + + + Navigate to the Humanitec **Service Users** tab. + ![Humanitec Service Users Tab](/images/app-connections/humanitec/humanitec-service-users.png) + + + Create a new service user. Take into account that the role set here will affect the permissions of the API Token so be sure to set it so the Service User has access permissions to the App you want to integrate to Infisical. + ![Humanitec Create New Service User](/images/app-connections/humanitec/humanitec-create-new-user.png) + + + Add a new API token for the service user. + ![Humanitec Add API Token](/images/app-connections/humanitec/humanitec-add-api-token.png) + + + Create the API token for the service user. + This token's permission will be limited to the **Service User** role. + + If you configure an expiry date for your API token you will need to manually rotate to a new token prior to expiration to avoid integration downtime. + + ![Humanitec Create API Token](/images/app-connections/humanitec/humanitec-create-api-token.png) + + + A modal with the API token will be displayed. Save the token in a secure location for later use in the following steps. + ![Humanitec Copy API Token](/images/app-connections/humanitec/humanitec-copy-api-token.png) + + + After following the previous steps the Service User has been successfully created, and now should be visible on the Service Users tab. + ![Humanitec Service User Created](/images/app-connections/humanitec/humanitec-service-account-filled.png) + + + Move to the **Applications** tab and add the Service User to the Application you want to sync with Infisical. + Clicking on the App Title will open the App details page. + ![Humanitec Applications Tab](/images/app-connections/humanitec/humanitec-applications-tab.png) + + + Move to the **People** tab and add a new member to this Application. The recently created User Service should be visible on the dropdown shown. + Make sure to assign at least Developer role as Write permissions are required. + ![Humanitec Add User to Application](/images/app-connections/humanitec/humanitec-add-user.png) + ![Humanitec Add User Options](/images/app-connections/humanitec/humanitec-add-user-options.png) + ![Humanitec Add User Role](/images/app-connections/humanitec/humanitec-add-user-role.png) + + + Your **Humanitec Connection** is now available for use. + ![Humanitec Connection Created](/images/app-connections/humanitec/humanitec-user-added.png) + + + Navigate to the **App Connections** tab on the **Organization Settings** page. + ![App Connections Tab](/images/app-connections/general/add-connection.png) + + + Select the **Humanitec Connection** option from the connection options modal. + ![Select Humanitec Connection](/images/app-connections/humanitec/humanitec-app-connection-option.png) + + + Fill the Humanitec Connection modal, here you will need to provide the User Service API Token generated in the previous step. + ![Humanitec Connection Modal](/images/app-connections/humanitec/humanitec-app-connection-modal.png) + + + Your **Humanitec Connection** is now available for use. + ![Humanitec Connection Created](/images/app-connections/humanitec/humanitec-app-connection-created.png) + + diff --git a/docs/integrations/secret-syncs/humanitec.mdx b/docs/integrations/secret-syncs/humanitec.mdx new file mode 100644 index 000000000..0edc2098e --- /dev/null +++ b/docs/integrations/secret-syncs/humanitec.mdx @@ -0,0 +1,156 @@ +--- +title: "Humanitec Sync" +description: "Learn how to configure a Humanitec Sync for Infisical." +--- + +**Prerequisites:** + + - Set up and add secrets to [Infisical Cloud](https://app.infisical.com) + - Create a [Humanitec Connection](/integrations/app-connections/humanitec) + + + + 1. Navigate to **Project** > **Integrations** and select the **Secret Syncs** tab. Click on the **Add Sync** button. + ![Secret Syncs Tab](/images/secret-syncs/general/secret-sync-tab.png) + + 2. Select the **Humanitec** option. + ![Select Humanitec](/images/secret-syncs/humanitec/select-humanitec-option.png) + + 3. Configure the **Source** from where secrets should be retrieved, then click **Next**. + ![Configure Source](/images/secret-syncs/humanitec/humanitec-source.png) + + - **Environment**: The project environment to retrieve secrets from. + - **Secret Path**: The folder path to retrieve secrets from. + + + If you need to sync secrets from multiple folder locations, check out [secret imports](/documentation/platform/secret-reference#secret-imports). + + + 4. Configure the **Destination** to where secrets should be deployed, then click **Next**. + ![Configure Destination](/images/secret-syncs/humanitec/humanitec-destination.png) + + - **Humanitec Connection**: The Humanitec Connection to authenticate with. + - **Scope**: The Humanitec secret scope to sync secrets to. + - **Application**: Sync secrets to a specific application. + - **Environment**: Sync secrets to a specific environment of an application. +

+ The remaining fields are determined by the selected **Scope**: + + + - **Organization**: The organization to deploy secrets to. + - **App**: The application to deploy secrets to. + + + - **Organization**: The organization to deploy secrets to. + - **App**: The application to deploy secrets to. + - **Environment**: The environment to deploy secrets to. + + + + 5. Configure the **Sync Options** to specify how secrets should be synced, then click **Next**. + ![Configure Options](/images/secret-syncs/humanitec/humanitec-options.png) + + - **Initial Sync Behavior**: Determines how Infisical should resolve the initial sync. + - **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical. + + Humanitec does not support importing secrets. + + - **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only. + + 6. Configure the **Details** of your Humanitec Sync, then click **Next**. + ![Configure Details](/images/secret-syncs/humanitec/humanitec-details.png) + + - **Name**: The name of your sync. Must be slug-friendly. + - **Description**: An optional description for your sync. + + 7. Review your Humanitec Sync configuration, then click **Create Sync**. + ![Confirm Configuration](/images/secret-syncs/humanitec/humanitec-review.png) + + 8. If enabled, your Humanitec Sync will begin syncing your secrets to the destination endpoint. + ![Sync Secrets](/images/secret-syncs/humanitec/humanitec-created.png) + + + + To create an **Humanitec Sync**, make an API request to the [Create Humanitec Sync](/api-reference/endpoints/secret-syncs/humanitec/create) API endpoint. + + ### Sample request + + ```bash Request + curl --request POST \ + --url https://app.infisical.com/api/v1/secret-syncs/humanitec \ + --header 'Content-Type: application/json' \ + --data '{ + "name": "my-humanitec-sync", + "projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "description": "an example sync", + "connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "environment": "dev", + "secretPath": "/my-secrets", + "isEnabled": true, + "syncOptions": { + "initialSyncBehavior": "overwrite-destination" + }, + "destinationConfig": { + "scope": "application", + "app": "my-app", + "environment": "development" + } + }' + ``` + + ### Sample response + + ```bash Response + { + "secretSync": { + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "name": "my-humanitec-sync", + "description": "an example sync", + "isEnabled": true, + "version": 1, + "folderId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "createdAt": "2023-11-07T05:31:56Z", + "updatedAt": "2023-11-07T05:31:56Z", + "syncStatus": "succeeded", + "lastSyncJobId": "123", + "lastSyncMessage": null, + "lastSyncedAt": "2023-11-07T05:31:56Z", + "importStatus": null, + "lastImportJobId": null, + "lastImportMessage": null, + "lastImportedAt": null, + "removeStatus": null, + "lastRemoveJobId": null, + "lastRemoveMessage": null, + "lastRemovedAt": null, + "syncOptions": { + "initialSyncBehavior": "overwrite-destination" + }, + "projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "connection": { + "app": "humanitec", + "name": "my-humanitec-connection", + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a" + }, + "environment": { + "slug": "dev", + "name": "Development", + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a" + }, + "folder": { + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "path": "/my-secrets" + }, + "destination": "humanitec", + "destinationConfig": { + "scope": "application", + "org": "my-organization", + "app": "my-app", + "env": "development" + } + } + } + ``` + + diff --git a/docs/mint.json b/docs/mint.json index 4ab8f0579..8f4c22bd1 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -408,7 +408,8 @@ "integrations/app-connections/azure-key-vault", "integrations/app-connections/databricks", "integrations/app-connections/gcp", - "integrations/app-connections/github" + "integrations/app-connections/github", + "integrations/app-connections/humanitec" ] } ] @@ -426,7 +427,8 @@ "integrations/secret-syncs/azure-key-vault", "integrations/secret-syncs/databricks", "integrations/secret-syncs/gcp-secret-manager", - "integrations/secret-syncs/github" + "integrations/secret-syncs/github", + "integrations/secret-syncs/humanitec" ] } ] @@ -898,6 +900,18 @@ "api-reference/endpoints/app-connections/github/update", "api-reference/endpoints/app-connections/github/delete" ] + }, + { + "group": "Humanitec", + "pages": [ + "api-reference/endpoints/app-connections/humanitec/list", + "api-reference/endpoints/app-connections/humanitec/available", + "api-reference/endpoints/app-connections/humanitec/get-by-id", + "api-reference/endpoints/app-connections/humanitec/get-by-name", + "api-reference/endpoints/app-connections/humanitec/create", + "api-reference/endpoints/app-connections/humanitec/update", + "api-reference/endpoints/app-connections/humanitec/delete" + ] } ] }, @@ -1001,6 +1015,19 @@ "api-reference/endpoints/secret-syncs/github/sync-secrets", "api-reference/endpoints/secret-syncs/github/remove-secrets" ] + }, + { + "group": "Humanitec", + "pages": [ + "api-reference/endpoints/secret-syncs/humanitec/list", + "api-reference/endpoints/secret-syncs/humanitec/get-by-id", + "api-reference/endpoints/secret-syncs/humanitec/get-by-name", + "api-reference/endpoints/secret-syncs/humanitec/create", + "api-reference/endpoints/secret-syncs/humanitec/update", + "api-reference/endpoints/secret-syncs/humanitec/delete", + "api-reference/endpoints/secret-syncs/humanitec/sync-secrets", + "api-reference/endpoints/secret-syncs/humanitec/remove-secrets" + ] } ] }, diff --git a/frontend/public/images/integrations/Humanitec.png b/frontend/public/images/integrations/Humanitec.png new file mode 100644 index 000000000..7f763d359 Binary files /dev/null and b/frontend/public/images/integrations/Humanitec.png differ diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/HumanitecSyncFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/HumanitecSyncFields.tsx new file mode 100644 index 000000000..229f54b01 --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/HumanitecSyncFields.tsx @@ -0,0 +1,197 @@ +import { Controller, useFormContext, useWatch } from "react-hook-form"; +import { SingleValue } from "react-select"; +import { faCircleInfo } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { SecretSyncConnectionField } from "@app/components/secret-syncs/forms/SecretSyncConnectionField"; +import { FilterableSelect, FormControl, Select, SelectItem, Tooltip } from "@app/components/v2"; +import { HUMANITEC_SYNC_SCOPES } from "@app/helpers/secretSyncs"; +import { + THumanitecConnectionApp, + THumanitecConnectionEnvironment, + THumanitecConnectionOrganization, + useHumanitecConnectionListOrganizations +} from "@app/hooks/api/appConnections/humanitec"; +import { SecretSync } from "@app/hooks/api/secretSyncs"; +import { HumanitecSyncScope } from "@app/hooks/api/secretSyncs/types/humanitec-sync"; + +import { TSecretSyncForm } from "../schemas"; + +export const HumanitecSyncFields = () => { + const { control, watch, setValue } = useFormContext< + TSecretSyncForm & { destination: SecretSync.Humanitec } + >(); + + const connectionId = useWatch({ name: "connection.id", control }); + const currentOrg = watch("destinationConfig.org"); + const currentApp = watch("destinationConfig.app"); + const currentScope = watch("destinationConfig.scope"); + + const { data: organizations = [], isPending: isOrganizationsPending } = + useHumanitecConnectionListOrganizations(connectionId, { + enabled: Boolean(connectionId) + }); + + const selectedOrg = organizations?.find((org) => org.id === currentOrg); + const selectedApp = selectedOrg?.apps?.find((app) => app.id === currentApp); + const environments = selectedApp?.envs || []; + + return ( + <> + { + setValue("destinationConfig.org", ""); + setValue("destinationConfig.app", ""); + setValue("destinationConfig.env", ""); + }} + /> + ( + + org.id === value) ?? []) : []} + onChange={(option) => { + onChange((option as SingleValue)?.id ?? null); + setValue("destinationConfig.app", ""); + setValue("destinationConfig.env", ""); + }} + options={organizations} + placeholder="Select an organization..." + getOptionLabel={(option) => option.name} + getOptionValue={(option) => option.id.toString()} + /> + + )} + /> + ( + +

+ Don't see the app you're looking for?{" "} + +
+ + } + > + org.id === currentOrg) + ?.apps?.find((app) => app.id === value) ?? null + } + onChange={(option) => { + onChange((option as SingleValue)?.id ?? null); + setValue("destinationConfig.env", ""); + }} + options={ + currentOrg ? (organizations.find((org) => org.id === currentOrg)?.apps ?? []) : [] + } + placeholder="Select an app..." + getOptionLabel={(option) => option.name} + getOptionValue={(option) => option.id.toString()} + /> + + )} + /> + ( + +

+ Specify how Infisical should manage secrets from Humanitec. The following options + are available: +

+
    + {Object.values(HUMANITEC_SYNC_SCOPES).map(({ name, description }) => { + return ( +
  • +

    + {name}: {description} +

    +
  • + ); + })} +
+ + } + > + +
+ )} + /> + {currentScope === HumanitecSyncScope.Environment && ( + ( + + env.id === value) ?? null} + onChange={(option) => + onChange((option as SingleValue)?.id ?? null) + } + options={environments} + placeholder="Select an env..." + getOptionLabel={(option) => option.name} + getOptionValue={(option) => option.id.toString()} + /> + + )} + /> + )} + + ); +}; diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx index fa1aaa84c..973f8bf17 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx @@ -10,6 +10,7 @@ import { AzureKeyVaultSyncFields } from "./AzureKeyVaultSyncFields"; import { DatabricksSyncFields } from "./DatabricksSyncFields"; import { GcpSyncFields } from "./GcpSyncFields"; import { GitHubSyncFields } from "./GitHubSyncFields"; +import { HumanitecSyncFields } from "./HumanitecSyncFields"; export const SecretSyncDestinationFields = () => { const { watch } = useFormContext(); @@ -31,6 +32,8 @@ export const SecretSyncDestinationFields = () => { return ; case SecretSync.Databricks: return ; + case SecretSync.Humanitec: + return ; default: throw new Error(`Unhandled Destination Config Field: ${destination}`); } diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx index ad9a26408..d3213caf7 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx @@ -38,6 +38,7 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => { case SecretSync.AzureKeyVault: case SecretSync.AzureAppConfiguration: case SecretSync.Databricks: + case SecretSync.Humanitec: AdditionalSyncOptionsFieldsComponent = null; break; default: diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/HumanitecSyncReviewFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/HumanitecSyncReviewFields.tsx new file mode 100644 index 000000000..a680041c4 --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/HumanitecSyncReviewFields.tsx @@ -0,0 +1,24 @@ +import { useFormContext } from "react-hook-form"; + +import { SecretSyncLabel } from "@app/components/secret-syncs"; +import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas"; +import { SecretSync } from "@app/hooks/api/secretSyncs"; +import { HumanitecSyncScope } from "@app/hooks/api/secretSyncs/types/humanitec-sync"; + +export const HumanitecSyncReviewFields = () => { + const { watch } = useFormContext(); + const orgId = watch("destinationConfig.org"); + const appId = watch("destinationConfig.app"); + const envId = watch("destinationConfig.env"); + const scope = watch("destinationConfig.scope"); + + return ( + <> + {orgId} + {appId} + {scope === HumanitecSyncScope.Environment && ( + {envId} + )} + + ); +}; diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx index 2846433ec..4004de105 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx @@ -20,6 +20,7 @@ import { AzureKeyVaultSyncReviewFields } from "./AzureKeyVaultSyncReviewFields"; import { DatabricksSyncReviewFields } from "./DatabricksSyncReviewFields"; import { GcpSyncReviewFields } from "./GcpSyncReviewFields"; import { GitHubSyncReviewFields } from "./GitHubSyncReviewFields"; +import { HumanitecSyncReviewFields } from "./HumanitecSyncReviewFields"; export const SecretSyncReviewFields = () => { const { watch } = useFormContext(); @@ -67,6 +68,9 @@ export const SecretSyncReviewFields = () => { case SecretSync.Databricks: DestinationFieldsComponent = ; break; + case SecretSync.Humanitec: + DestinationFieldsComponent = ; + break; default: throw new Error(`Unhandled Destination Review Fields: ${destination}`); } diff --git a/frontend/src/components/secret-syncs/forms/schemas/humanitec-sync-destination-schema.ts b/frontend/src/components/secret-syncs/forms/schemas/humanitec-sync-destination-schema.ts new file mode 100644 index 000000000..bb438557f --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/schemas/humanitec-sync-destination-schema.ts @@ -0,0 +1,24 @@ +import { z } from "zod"; + +import { BaseSecretSyncSchema } from "@app/components/secret-syncs/forms/schemas/base-secret-sync-schema"; +import { SecretSync } from "@app/hooks/api/secretSyncs"; +import { HumanitecSyncScope } from "@app/hooks/api/secretSyncs/types/humanitec-sync"; + +export const HumanitecSyncDestinationSchema = BaseSecretSyncSchema().merge( + z.object({ + destination: z.literal(SecretSync.Humanitec), + destinationConfig: z.discriminatedUnion("scope", [ + z.object({ + scope: z.literal(HumanitecSyncScope.Application), + org: z.string().trim().min(1, "Organization required"), + app: z.string().trim().min(1, "Application required") + }), + z.object({ + scope: z.literal(HumanitecSyncScope.Environment), + org: z.string().trim().min(1, "Organization required"), + app: z.string().trim().min(1, "Application required"), + env: z.string().trim().min(1, "Environment required") + }) + ]) + }) +); diff --git a/frontend/src/components/secret-syncs/forms/schemas/secret-sync-schema.ts b/frontend/src/components/secret-syncs/forms/schemas/secret-sync-schema.ts index 55ee6cb3d..be2322304 100644 --- a/frontend/src/components/secret-syncs/forms/schemas/secret-sync-schema.ts +++ b/frontend/src/components/secret-syncs/forms/schemas/secret-sync-schema.ts @@ -8,6 +8,7 @@ import { AwsParameterStoreSyncDestinationSchema } from "./aws-parameter-store-sy import { AzureAppConfigurationSyncDestinationSchema } from "./azure-app-configuration-sync-destination-schema"; import { AzureKeyVaultSyncDestinationSchema } from "./azure-key-vault-sync-destination-schema"; import { GcpSyncDestinationSchema } from "./gcp-sync-destination-schema"; +import { HumanitecSyncDestinationSchema } from "./humanitec-sync-destination-schema"; const SecretSyncUnionSchema = z.discriminatedUnion("destination", [ AwsParameterStoreSyncDestinationSchema, @@ -16,7 +17,8 @@ const SecretSyncUnionSchema = z.discriminatedUnion("destination", [ GcpSyncDestinationSchema, AzureKeyVaultSyncDestinationSchema, AzureAppConfigurationSyncDestinationSchema, - DatabricksSyncDestinationSchema + DatabricksSyncDestinationSchema, + HumanitecSyncDestinationSchema ]); export const SecretSyncFormSchema = SecretSyncUnionSchema; diff --git a/frontend/src/helpers/appConnections.ts b/frontend/src/helpers/appConnections.ts index 016a86264..cf643218b 100644 --- a/frontend/src/helpers/appConnections.ts +++ b/frontend/src/helpers/appConnections.ts @@ -11,6 +11,7 @@ import { TAppConnection } from "@app/hooks/api/appConnections/types"; import { DatabricksConnectionMethod } from "@app/hooks/api/appConnections/types/databricks-connection"; +import { HumanitecConnectionMethod } from "@app/hooks/api/appConnections/types/humanitec-connection"; export const APP_CONNECTION_MAP: Record = { [AppConnection.AWS]: { name: "AWS", image: "Amazon Web Services.png" }, @@ -24,7 +25,8 @@ export const APP_CONNECTION_MAP: Record { @@ -43,6 +45,8 @@ export const getAppConnectionMethodDetails = (method: TAppConnection["method"]) return { name: "Service Account Impersonation", icon: faUser }; case DatabricksConnectionMethod.ServicePrincipal: return { name: "Service Principal", icon: faUser }; + case HumanitecConnectionMethod.API_TOKEN: + return { name: "API Token", icon: faKey }; default: throw new Error(`Unhandled App Connection Method: ${method}`); } diff --git a/frontend/src/helpers/secretSyncs.ts b/frontend/src/helpers/secretSyncs.ts index b119983a0..1cc076cce 100644 --- a/frontend/src/helpers/secretSyncs.ts +++ b/frontend/src/helpers/secretSyncs.ts @@ -4,6 +4,7 @@ import { SecretSyncImportBehavior, SecretSyncInitialSyncBehavior } from "@app/hooks/api/secretSyncs"; +import { HumanitecSyncScope } from "@app/hooks/api/secretSyncs/types/humanitec-sync"; export const SECRET_SYNC_MAP: Record = { [SecretSync.AWSParameterStore]: { name: "AWS Parameter Store", image: "Amazon Web Services.png" }, @@ -18,6 +19,10 @@ export const SECRET_SYNC_MAP: Record = { [SecretSync.GCPSecretManager]: AppConnection.GCP, [SecretSync.AzureKeyVault]: AppConnection.AzureKeyVault, [SecretSync.AzureAppConfiguration]: AppConnection.AzureAppConfiguration, - [SecretSync.Databricks]: AppConnection.Databricks + [SecretSync.Databricks]: AppConnection.Databricks, + [SecretSync.Humanitec]: AppConnection.Humanitec }; export const SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP: Record< @@ -62,3 +68,19 @@ export const SECRET_SYNC_IMPORT_BEHAVIOR_MAP: Record< description: `Infisical will import any secrets present in the ${destinationName} destination, prioritizing values from ${destinationName} over Infisical when keys conflict.` }) }; + +export const HUMANITEC_SYNC_SCOPES: Record< + HumanitecSyncScope, + { name: string; description: string } +> = { + [HumanitecSyncScope.Application]: { + name: "Application", + description: + "Infisical will sync secrets as application level shared values to the specified Humanitec application." + }, + [HumanitecSyncScope.Environment]: { + name: "Environment", + description: + "Infisical will sync secrets as environment level shared values to the specified Humanitec application environment." + } +}; diff --git a/frontend/src/hooks/api/appConnections/enums.ts b/frontend/src/hooks/api/appConnections/enums.ts index 97fcf9be6..c07808af9 100644 --- a/frontend/src/hooks/api/appConnections/enums.ts +++ b/frontend/src/hooks/api/appConnections/enums.ts @@ -4,5 +4,6 @@ export enum AppConnection { GCP = "gcp", AzureKeyVault = "azure-key-vault", AzureAppConfiguration = "azure-app-configuration", - Databricks = "databricks" + Databricks = "databricks", + Humanitec = "humanitec" } diff --git a/frontend/src/hooks/api/appConnections/humanitec/index.ts b/frontend/src/hooks/api/appConnections/humanitec/index.ts new file mode 100644 index 000000000..2c1906d36 --- /dev/null +++ b/frontend/src/hooks/api/appConnections/humanitec/index.ts @@ -0,0 +1,2 @@ +export * from "./queries"; +export * from "./types"; diff --git a/frontend/src/hooks/api/appConnections/humanitec/queries.tsx b/frontend/src/hooks/api/appConnections/humanitec/queries.tsx new file mode 100644 index 000000000..d53913e37 --- /dev/null +++ b/frontend/src/hooks/api/appConnections/humanitec/queries.tsx @@ -0,0 +1,37 @@ +import { useQuery, UseQueryOptions } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +import { appConnectionKeys } from "../queries"; +import { THumanitecOrganization } from "./types"; + +const humanitecConnectionKeys = { + all: [...appConnectionKeys.all, "humanitec"] as const, + listOrganizations: (connectionId: string) => + [...humanitecConnectionKeys.all, "organizations", connectionId] as const +}; + +export const useHumanitecConnectionListOrganizations = ( + connectionId: string, + options?: Omit< + UseQueryOptions< + THumanitecOrganization[], + unknown, + THumanitecOrganization[], + ReturnType + >, + "queryKey" | "queryFn" + > +) => { + return useQuery({ + queryKey: humanitecConnectionKeys.listOrganizations(connectionId), + queryFn: async () => { + const { data } = await apiRequest.get( + `/api/v1/app-connections/humanitec/${connectionId}/organizations` + ); + + return data; + }, + ...options + }); +}; diff --git a/frontend/src/hooks/api/appConnections/humanitec/types.ts b/frontend/src/hooks/api/appConnections/humanitec/types.ts new file mode 100644 index 000000000..5ab1b4d31 --- /dev/null +++ b/frontend/src/hooks/api/appConnections/humanitec/types.ts @@ -0,0 +1,28 @@ +export type THumanitecOrganization = { + name: string; + id: string; + apps: THumanitecApp[]; +}; + +export type THumanitecApp = { + id: string; + name: string; + envs: { id: string; name: string }[]; +}; + +export type THumanitecConnectionApp = { + id: string; + name: string; + envs: THumanitecConnectionEnvironment[]; +}; + +export type THumanitecConnectionEnvironment = { + id: string; + name: string; +}; + +export type THumanitecConnectionOrganization = { + id: string; + name: string; + apps: THumanitecConnectionApp[]; +}; diff --git a/frontend/src/hooks/api/appConnections/types/app-options.ts b/frontend/src/hooks/api/appConnections/types/app-options.ts index e42593c94..a323765f7 100644 --- a/frontend/src/hooks/api/appConnections/types/app-options.ts +++ b/frontend/src/hooks/api/appConnections/types/app-options.ts @@ -34,13 +34,18 @@ export type TDatabricksConnectionOption = TAppConnectionOptionBase & { app: AppConnection.Databricks; }; +export type THumanitecConnectionOption = TAppConnectionOptionBase & { + app: AppConnection.Humanitec; +}; + export type TAppConnectionOption = | TAwsConnectionOption | TGitHubConnectionOption | TGcpConnectionOption | TAzureAppConfigurationConnectionOption | TAzureKeyVaultConnectionOption - | TDatabricksConnectionOption; + | TDatabricksConnectionOption + | THumanitecConnectionOption; export type TAppConnectionOptionMap = { [AppConnection.AWS]: TAwsConnectionOption; @@ -49,4 +54,5 @@ export type TAppConnectionOptionMap = { [AppConnection.AzureKeyVault]: TAzureKeyVaultConnectionOption; [AppConnection.AzureAppConfiguration]: TAzureAppConfigurationConnectionOption; [AppConnection.Databricks]: TDatabricksConnectionOption; + [AppConnection.Humanitec]: THumanitecConnectionOption; }; diff --git a/frontend/src/hooks/api/appConnections/types/humanitec-connection.ts b/frontend/src/hooks/api/appConnections/types/humanitec-connection.ts new file mode 100644 index 000000000..2473050cd --- /dev/null +++ b/frontend/src/hooks/api/appConnections/types/humanitec-connection.ts @@ -0,0 +1,13 @@ +import { AppConnection } from "@app/hooks/api/appConnections/enums"; +import { TRootAppConnection } from "@app/hooks/api/appConnections/types/root-connection"; + +export enum HumanitecConnectionMethod { + API_TOKEN = "api-token" +} + +export type THumanitecConnection = TRootAppConnection & { app: AppConnection.Humanitec } & { + method: HumanitecConnectionMethod.API_TOKEN; + credentials: { + apiToken: string; + }; +}; diff --git a/frontend/src/hooks/api/appConnections/types/index.ts b/frontend/src/hooks/api/appConnections/types/index.ts index a2a1b6792..db56839fc 100644 --- a/frontend/src/hooks/api/appConnections/types/index.ts +++ b/frontend/src/hooks/api/appConnections/types/index.ts @@ -3,6 +3,7 @@ import { TAppConnectionOption } from "@app/hooks/api/appConnections/types/app-op import { TAwsConnection } from "@app/hooks/api/appConnections/types/aws-connection"; import { TDatabricksConnection } from "@app/hooks/api/appConnections/types/databricks-connection"; import { TGitHubConnection } from "@app/hooks/api/appConnections/types/github-connection"; +import { THumanitecConnection } from "@app/hooks/api/appConnections/types/humanitec-connection"; import { TAzureAppConfigurationConnection } from "./azure-app-configuration-connection"; import { TAzureKeyVaultConnection } from "./azure-key-vault-connection"; @@ -13,6 +14,7 @@ export * from "./azure-app-configuration-connection"; export * from "./azure-key-vault-connection"; export * from "./gcp-connection"; export * from "./github-connection"; +export * from "./humanitec-connection"; export type TAppConnection = | TAwsConnection @@ -20,7 +22,8 @@ export type TAppConnection = | TGcpConnection | TAzureKeyVaultConnection | TAzureAppConfigurationConnection - | TDatabricksConnection; + | TDatabricksConnection + | THumanitecConnection; export type TAvailableAppConnection = Pick; @@ -54,4 +57,5 @@ export type TAppConnectionMap = { [AppConnection.AzureKeyVault]: TAzureKeyVaultConnection; [AppConnection.AzureAppConfiguration]: TAzureAppConfigurationConnection; [AppConnection.Databricks]: TDatabricksConnection; + [AppConnection.Humanitec]: THumanitecConnection; }; diff --git a/frontend/src/hooks/api/secretSyncs/enums.ts b/frontend/src/hooks/api/secretSyncs/enums.ts index c942c388d..08accba16 100644 --- a/frontend/src/hooks/api/secretSyncs/enums.ts +++ b/frontend/src/hooks/api/secretSyncs/enums.ts @@ -5,7 +5,8 @@ export enum SecretSync { GCPSecretManager = "gcp-secret-manager", AzureKeyVault = "azure-key-vault", AzureAppConfiguration = "azure-app-configuration", - Databricks = "databricks" + Databricks = "databricks", + Humanitec = "humanitec" } export enum SecretSyncStatus { diff --git a/frontend/src/hooks/api/secretSyncs/types/humanitec-sync.ts b/frontend/src/hooks/api/secretSyncs/types/humanitec-sync.ts new file mode 100644 index 000000000..5dc06e031 --- /dev/null +++ b/frontend/src/hooks/api/secretSyncs/types/humanitec-sync.ts @@ -0,0 +1,29 @@ +import { AppConnection } from "@app/hooks/api/appConnections/enums"; +import { SecretSync } from "@app/hooks/api/secretSyncs"; +import { TRootSecretSync } from "@app/hooks/api/secretSyncs/types/root-sync"; + +export type THumanitecSync = TRootSecretSync & { + destination: SecretSync.Humanitec; + destinationConfig: + | { + scope: HumanitecSyncScope.Application; + org: string; + app: string; + } + | { + scope: HumanitecSyncScope.Environment; + org: string; + app: string; + env: string; + }; + connection: { + app: AppConnection.Humanitec; + name: string; + id: string; + }; +}; + +export enum HumanitecSyncScope { + Application = "application", + Environment = "environment" +} diff --git a/frontend/src/hooks/api/secretSyncs/types/index.ts b/frontend/src/hooks/api/secretSyncs/types/index.ts index e797c6458..a90a8a3ef 100644 --- a/frontend/src/hooks/api/secretSyncs/types/index.ts +++ b/frontend/src/hooks/api/secretSyncs/types/index.ts @@ -8,6 +8,7 @@ import { TAwsSecretsManagerSync } from "./aws-secrets-manager-sync"; import { TAzureAppConfigurationSync } from "./azure-app-configuration-sync"; import { TAzureKeyVaultSync } from "./azure-key-vault-sync"; import { TGcpSync } from "./gcp-sync"; +import { THumanitecSync } from "./humanitec-sync"; export type TSecretSyncOption = { name: string; @@ -22,7 +23,8 @@ export type TSecretSync = | TGcpSync | TAzureKeyVaultSync | TAzureAppConfigurationSync - | TDatabricksSync; + | TDatabricksSync + | THumanitecSync; export type TListSecretSyncs = { secretSyncs: TSecretSync[] }; 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 c84b20885..fc3257841 100644 --- a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx @@ -15,6 +15,7 @@ import { AzureKeyVaultConnectionForm } from "./AzureKeyVaultConnectionForm"; import { DatabricksConnectionForm } from "./DatabricksConnectionForm"; import { GcpConnectionForm } from "./GcpConnectionForm"; import { GitHubConnectionForm } from "./GitHubConnectionForm"; +import { HumanitecConnectionForm } from "./HumanitecConnectionForm"; type FormProps = { onComplete: (appConnection: TAppConnection) => void; @@ -62,6 +63,8 @@ const CreateForm = ({ app, onComplete }: CreateFormProps) => { return ; case AppConnection.Databricks: return ; + case AppConnection.Humanitec: + return ; default: throw new Error(`Unhandled App ${app}`); } @@ -107,6 +110,8 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => { return ; case AppConnection.Databricks: return ; + case AppConnection.Humanitec: + return ; default: throw new Error(`Unhandled App ${(appConnection as TAppConnection).app}`); } diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/HumanitecConnectionForm.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/HumanitecConnectionForm.tsx new file mode 100644 index 000000000..b119f46ad --- /dev/null +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/HumanitecConnectionForm.tsx @@ -0,0 +1,132 @@ +import { Controller, FormProvider, useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + +import { + Button, + FormControl, + ModalClose, + SecretInput, + Select, + SelectItem +} from "@app/components/v2"; +import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections"; +import { HumanitecConnectionMethod, THumanitecConnection } from "@app/hooks/api/appConnections"; +import { AppConnection } from "@app/hooks/api/appConnections/enums"; + +import { + genericAppConnectionFieldsSchema, + GenericAppConnectionsFields +} from "./GenericAppConnectionFields"; + +type Props = { + appConnection?: THumanitecConnection; + onSubmit: (formData: FormData) => void; +}; + +const rootSchema = genericAppConnectionFieldsSchema.extend({ + app: z.literal(AppConnection.Humanitec) +}); + +const formSchema = z.discriminatedUnion("method", [ + rootSchema.extend({ + method: z.literal(HumanitecConnectionMethod.API_TOKEN), + credentials: z.object({ + apiToken: z.string().trim().min(1, "Service API Token required") + }) + }) +]); + +type FormData = z.infer; + +export const HumanitecConnectionForm = ({ appConnection, onSubmit }: Props) => { + const isUpdate = Boolean(appConnection); + + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: appConnection ?? { + app: AppConnection.Humanitec, + method: HumanitecConnectionMethod.API_TOKEN + } + }); + + const { + handleSubmit, + control, + formState: { isSubmitting, isDirty } + } = form; + + return ( + +
+ {!isUpdate && } + ( + + + + )} + /> + ( + + onChange(e.target.value)} + /> + + )} + /> +
+ + + + +
+ +
+ ); +}; diff --git a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/HumanitecSyncDestinationCol.tsx b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/HumanitecSyncDestinationCol.tsx new file mode 100644 index 000000000..b0fffebfe --- /dev/null +++ b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/HumanitecSyncDestinationCol.tsx @@ -0,0 +1,14 @@ +import { THumanitecSync } from "@app/hooks/api/secretSyncs/types/humanitec-sync"; + +import { getSecretSyncDestinationColValues } from "../helpers"; +import { SecretSyncTableCell } from "../SecretSyncTableCell"; + +type Props = { + secretSync: THumanitecSync; +}; + +export const HumanitecSyncDestinationCol = ({ secretSync }: Props) => { + const { primaryText, secondaryText } = getSecretSyncDestinationColValues(secretSync); + + return ; +}; diff --git a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/SecretSyncDestinationCol.tsx b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/SecretSyncDestinationCol.tsx index 2c375a8b5..c4dc564b0 100644 --- a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/SecretSyncDestinationCol.tsx +++ b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/SecretSyncDestinationCol.tsx @@ -7,6 +7,7 @@ import { AzureKeyVaultDestinationSyncCol } from "./AzureKeyVaultDestinationSyncC import { DatabricksSyncDestinationCol } from "./DatabricksSyncDestinationCol"; import { GcpSyncDestinationCol } from "./GcpSyncDestinationCol"; import { GitHubSyncDestinationCol } from "./GitHubSyncDestinationCol"; +import { HumanitecSyncDestinationCol } from "./HumanitecSyncDestinationCol"; type Props = { secretSync: TSecretSync; @@ -28,6 +29,8 @@ export const SecretSyncDestinationCol = ({ secretSync }: Props) => { return ; case SecretSync.Databricks: return ; + case SecretSync.Humanitec: + return ; default: throw new Error( `Unhandled Secret Sync Destination Col: ${(secretSync as TSecretSync).destination}` diff --git a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/helpers/index.ts b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/helpers/index.ts index f1087c03c..6eb3a04e3 100644 --- a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/helpers/index.ts +++ b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/helpers/index.ts @@ -3,6 +3,7 @@ import { GitHubSyncScope, GitHubSyncVisibility } from "@app/hooks/api/secretSyncs/types/github-sync"; +import { HumanitecSyncScope } from "@app/hooks/api/secretSyncs/types/humanitec-sync"; // This functional ensures parity across what is displayed in the destination column // and the values used when search filtering @@ -59,6 +60,19 @@ export const getSecretSyncDestinationColValues = (secretSync: TSecretSync) => { case SecretSync.Databricks: primaryText = destinationConfig.scope; break; + case SecretSync.Humanitec: + switch (destinationConfig.scope) { + case HumanitecSyncScope.Application: + primaryText = destinationConfig.app; + break; + case HumanitecSyncScope.Environment: + primaryText = `${destinationConfig.app} / ${destinationConfig.env}`; + break; + default: + throw new Error(`Unhandled Humanitec Scope Destination Col Values ${destination}`); + } + secondaryText = `Organization - ${destinationConfig.org}`; + break; default: throw new Error(`Unhandled Destination Col Values ${destination}`); } diff --git a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/HumanitecSyncDestinationSection.tsx b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/HumanitecSyncDestinationSection.tsx new file mode 100644 index 000000000..f750ce885 --- /dev/null +++ b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/HumanitecSyncDestinationSection.tsx @@ -0,0 +1,49 @@ +import { ReactNode } from "react"; + +import { SecretSyncLabel } from "@app/components/secret-syncs"; +import { + HumanitecSyncScope, + THumanitecSync +} from "@app/hooks/api/secretSyncs/types/humanitec-sync"; + +type Props = { + secretSync: THumanitecSync; +}; + +export const HumanitecSyncDestinationSection = ({ secretSync }: Props) => { + const { destinationConfig } = secretSync; + + let Components: ReactNode; + switch (destinationConfig.scope) { + case HumanitecSyncScope.Application: + Components = ( + <> + {destinationConfig.app} + {destinationConfig.org} + + ); + break; + case HumanitecSyncScope.Environment: + Components = ( + <> + {destinationConfig.app} + {destinationConfig.org} + {destinationConfig.env} + + ); + break; + default: + throw new Error( + `Uhandled Humanitec Sync Destination Section Scope ${secretSync.destinationConfig.scope}` + ); + } + + return ( + <> + + {destinationConfig.scope.replace("-", " ")} + + {Components} + + ); +}; diff --git a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/SecretSyncDestinatonSection.tsx b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/SecretSyncDestinatonSection.tsx index b7d3964c7..c1dee10c2 100644 --- a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/SecretSyncDestinatonSection.tsx +++ b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/SecretSyncDestinatonSection.tsx @@ -17,6 +17,7 @@ import { GitHubSyncDestinationSection } from "@app/pages/secret-manager/SecretSy import { AzureAppConfigurationSyncDestinationSection } from "./AzureAppConfigurationSyncDestinationSection"; import { AzureKeyVaultSyncDestinationSection } from "./AzureKeyVaultSyncDestinationSection"; import { GcpSyncDestinationSection } from "./GcpSyncDestinationSection"; +import { HumanitecSyncDestinationSection } from "./HumanitecSyncDestinationSection"; type Props = { secretSync: TSecretSync; @@ -53,6 +54,9 @@ export const SecretSyncDestinationSection = ({ secretSync, onEditDestination }: case SecretSync.Databricks: DestinationComponents = ; break; + case SecretSync.Humanitec: + DestinationComponents = ; + break; default: throw new Error(`Unhandled Destination Section components: ${destination}`); } diff --git a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/SecretSyncOptionsSection.tsx b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/SecretSyncOptionsSection.tsx index df6927b48..be48428ea 100644 --- a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/SecretSyncOptionsSection.tsx +++ b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/SecretSyncOptionsSection.tsx @@ -46,6 +46,7 @@ export const SecretSyncOptionsSection = ({ secretSync, onEditOptions }: Props) = case SecretSync.AzureKeyVault: case SecretSync.AzureAppConfiguration: case SecretSync.Databricks: + case SecretSync.Humanitec: AdditionalSyncOptionsComponent = null; break; default: