diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index b9f66adc8..7a350938f 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -1775,7 +1775,8 @@ export const SecretSyncs = { 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." + 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/lib/config/env.ts b/backend/src/lib/config/env.ts index 15ffd2d51..0d13939a2 100644 --- a/backend/src/lib/config/env.ts +++ b/backend/src/lib/config/env.ts @@ -224,9 +224,6 @@ const envSchema = z DATADOG_SERVICE: zpStr(z.string().optional().default("infisical-core")), DATADOG_HOSTNAME: zpStr(z.string().optional()), - // humanitec - INF_APP_CONNECTION_HUMANITEC_ACCESS_KEY: zpStr(z.string().optional()), - /* CORS ----------------------------------------------------------------------------- */ CORS_ALLOWED_ORIGINS: zpStr( diff --git a/backend/src/services/app-connection/app-connection-fns.ts b/backend/src/services/app-connection/app-connection-fns.ts index f1d899a4d..fe5130ff1 100644 --- a/backend/src/services/app-connection/app-connection-fns.ts +++ b/backend/src/services/app-connection/app-connection-fns.ts @@ -136,8 +136,8 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) => return "Service Account Impersonation"; case DatabricksConnectionMethod.ServicePrincipal: return "Service Principal"; - case HumanitecConnectionMethod.AccessKey: - return "Access Key"; + 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/humanitec/humanitec-connection-enums.ts b/backend/src/services/app-connection/humanitec/humanitec-connection-enums.ts index 57db0e3c2..a3f31ed66 100644 --- a/backend/src/services/app-connection/humanitec/humanitec-connection-enums.ts +++ b/backend/src/services/app-connection/humanitec/humanitec-connection-enums.ts @@ -1,3 +1,3 @@ export enum HumanitecConnectionMethod { - AccessKey = "access-key" + API_TOKEN = "api-token" } diff --git a/backend/src/services/app-connection/humanitec/humanitec-connection-fns.ts b/backend/src/services/app-connection/humanitec/humanitec-connection-fns.ts index 8a0d1141c..5775e5b24 100644 --- a/backend/src/services/app-connection/humanitec/humanitec-connection-fns.ts +++ b/backend/src/services/app-connection/humanitec/humanitec-connection-fns.ts @@ -2,6 +2,7 @@ import { AxiosError, AxiosResponse } from "axios"; import { request } from "@app/lib/config/request"; import { BadRequestError, InternalServerError } from "@app/lib/errors"; +import { logger } from "@app/lib/logger"; import { AppConnection } from "@app/services/app-connection/app-connection-enums"; import { IntegrationUrls } from "@app/services/integration-auth/integration-list"; @@ -18,7 +19,7 @@ export const getHumanitecConnectionListItem = () => { return { name: "Humanitec" as const, app: AppConnection.Humanitec as const, - methods: Object.values(HumanitecConnectionMethod) as [HumanitecConnectionMethod.AccessKey] + methods: Object.values(HumanitecConnectionMethod) as [HumanitecConnectionMethod.API_TOKEN] }; }; @@ -30,13 +31,13 @@ export const validateHumanitecConnectionCredentials = async (config: THumanitecC try { response = await request.get(`${IntegrationUrls.HUMANITEC_API_URL}/orgs`, { headers: { - Authorization: `Bearer ${inputCredentials.accessKeyId}` + Authorization: `Bearer ${inputCredentials.apiToken}` } }); } catch (error: unknown) { if (error instanceof AxiosError) { throw new BadRequestError({ - message: `Failed to validate credentials: ${error.response?.data || "Unknown error"}` + message: `Failed to validate credentials: ${error.message || "Unknown error"}` }); } throw new BadRequestError({ @@ -55,11 +56,11 @@ export const validateHumanitecConnectionCredentials = async (config: THumanitecC export const listOrganizations = async (appConnection: THumanitecConnection): Promise => { const { - credentials: { accessKeyId } + credentials: { apiToken } } = appConnection; const response = await request.get(`${IntegrationUrls.HUMANITEC_API_URL}/orgs`, { headers: { - Authorization: `Bearer ${accessKeyId}` + Authorization: `Bearer ${apiToken}` } }); @@ -68,34 +69,39 @@ export const listOrganizations = async (appConnection: THumanitecConnection): Pr message: "Failed to get organizations: Response was empty" }); } - const orgs = response.data; - const appPromises = orgs.map(async (org) => { - return request.get(`${IntegrationUrls.HUMANITEC_API_URL}/orgs/${org.id}/apps`, { - headers: { - Authorization: `Bearer ${accessKeyId}` + const orgsWithApps: HumanitecOrgWithApps[] = []; + + for (const org of orgs) { + try { + // 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) { + throw new InternalServerError({ + message: "Failed to get apps for organization: Response was empty" + }); } - }); - }); - const appsResponses = await Promise.all(appPromises); - - const orgsWithApps: HumanitecOrgWithApps[] = orgs.map((org, index) => { - if (!appsResponses[index].data) { - throw new InternalServerError({ - message: "Failed to get apps for organization: Response was empty" + const apps = appsResponse.data; + orgsWithApps.push({ + ...org, + apps: apps.map((app) => ({ + name: app.name, + id: app.id, + envs: app.envs + })) }); + } catch (error) { + logger.error(error, `Failed to get apps for organization ${org.name}`); } - - const apps = appsResponses[index].data; - return { - ...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 index e0370207f..145f78b85 100644 --- a/backend/src/services/app-connection/humanitec/humanitec-connection-schemas.ts +++ b/backend/src/services/app-connection/humanitec/humanitec-connection-schemas.ts @@ -11,19 +11,19 @@ import { import { HumanitecConnectionMethod } from "./humanitec-connection-enums"; export const HumanitecConnectionAccessTokenCredentialsSchema = z.object({ - accessKeyId: z.string().trim().min(1, "Access Key ID required") + 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.AccessKey), + method: z.literal(HumanitecConnectionMethod.API_TOKEN), credentials: HumanitecConnectionAccessTokenCredentialsSchema }); export const SanitizedHumanitecConnectionSchema = z.discriminatedUnion("method", [ BaseHumanitecConnectionSchema.extend({ - method: z.literal(HumanitecConnectionMethod.AccessKey), + method: z.literal(HumanitecConnectionMethod.API_TOKEN), credentials: HumanitecConnectionAccessTokenCredentialsSchema.pick({}) }) ]); @@ -31,7 +31,7 @@ export const SanitizedHumanitecConnectionSchema = z.discriminatedUnion("method", export const ValidateHumanitecConnectionCredentialsSchema = z.discriminatedUnion("method", [ z.object({ method: z - .literal(HumanitecConnectionMethod.AccessKey) + .literal(HumanitecConnectionMethod.API_TOKEN) .describe(AppConnections?.CREATE(AppConnection.Humanitec).method), credentials: HumanitecConnectionAccessTokenCredentialsSchema.describe( AppConnections.CREATE(AppConnection.Humanitec).credentials diff --git a/backend/src/services/app-connection/humanitec/humanitec-connection-service.ts b/backend/src/services/app-connection/humanitec/humanitec-connection-service.ts index 0036d5c5b..5ade43450 100644 --- a/backend/src/services/app-connection/humanitec/humanitec-connection-service.ts +++ b/backend/src/services/app-connection/humanitec/humanitec-connection-service.ts @@ -1,3 +1,4 @@ +import { logger } from "@app/lib/logger"; import { OrgServiceActor } from "@app/lib/types"; import { AppConnection } from "../app-connection-enums"; @@ -17,6 +18,7 @@ export const humanitecConnectionService = (getAppConnection: TGetAppConnectionFu const organizations = await getHumanitecOrganizations(appConnection); return organizations; } catch (error) { + logger.error(error, "Failed to establish connection with Humanitec"); return []; } }; 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 index d0377d4b8..c9253377a 100644 --- a/backend/src/services/secret-sync/humanitec/humanitec-sync-fns.ts +++ b/backend/src/services/secret-sync/humanitec/humanitec-sync-fns.ts @@ -4,47 +4,60 @@ 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: { accessKeyId } + credentials: { apiToken } } } = secretSync; - const { data } = await request.get( - `${IntegrationUrls.HUMANITEC_API_URL}/orgs/${destinationConfig.org}/apps/${destinationConfig.app}/envs/${destinationConfig.env}/values`, - { + try { + 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 ${accessKeyId}`, + Authorization: `Bearer ${apiToken}`, "Accept-Encoding": "application/json" } - } - ); + }); - return data; + return data; + } catch (error) { + throw new SecretSyncError({ + error + }); + } }; const deleteSecret = async (secretSync: THumanitecSyncWithCredentials, encryptedSecret: HumanitecSecret) => { const { destinationConfig, connection: { - credentials: { accessKeyId } + credentials: { apiToken } } } = secretSync; try { - await request.delete( - `${IntegrationUrls.HUMANITEC_API_URL}/orgs/${destinationConfig.org}/apps/${destinationConfig.app}/envs/${destinationConfig.env}/values/${encryptedSecret.key}`, - { - headers: { - Authorization: `Bearer ${accessKeyId}`, - "Accept-Encoding": "application/json" - } + 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-Encoding": "application/json" } - ); + }); } catch (error) { throw new SecretSyncError({ error, @@ -58,7 +71,7 @@ const createSecret = async (secretSync: THumanitecSyncWithCredentials, secretMap const { destinationConfig, connection: { - credentials: { accessKeyId } + credentials: { apiToken } } } = secretSync; @@ -71,24 +84,26 @@ const createSecret = async (secretSync: THumanitecSyncWithCredentials, secretMap }, { headers: { - Authorization: `Bearer ${accessKeyId}`, + Authorization: `Bearer ${apiToken}`, "Accept-Encoding": "application/json" } } ); - 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 ${accessKeyId}`, - "Accept-Encoding": "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-Encoding": "application/json" + } } - } - ); + ); + } } catch (error) { throw new SecretSyncError({ error, @@ -102,22 +117,38 @@ const updateSecret = async (secretSync: THumanitecSyncWithCredentials, secretMap const { destinationConfig, connection: { - credentials: { accessKeyId } + credentials: { apiToken } } } = secretSync; - 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 ${accessKeyId}`, - "Accept-Encoding": "application/json" + if (destinationConfig.scope === HumanitecSyncScope.Application) { + await request.patch( + `${IntegrationUrls.HUMANITEC_API_URL}/orgs/${destinationConfig.org}/apps/${destinationConfig.app}/values/${key}`, + { + value: secretMap[key].value, + description: secretMap[key].comment || "" + }, + { + headers: { + Authorization: `Bearer ${apiToken}`, + "Accept-Encoding": "application/json" + } } - } - ); + ); + } else { + 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-Encoding": "application/json" + } + } + ); + } } catch (error) { throw new SecretSyncError({ error, diff --git a/backend/src/services/secret-sync/humanitec/humanitec-sync-schemas.ts b/backend/src/services/secret-sync/humanitec/humanitec-sync-schemas.ts index fab0512cd..cd90ecfdc 100644 --- a/backend/src/services/secret-sync/humanitec/humanitec-sync-schemas.ts +++ b/backend/src/services/secret-sync/humanitec/humanitec-sync-schemas.ts @@ -2,6 +2,7 @@ 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, @@ -10,11 +11,19 @@ import { } from "@app/services/secret-sync/secret-sync-schemas"; import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types"; -const HumanitecSyncDestinationConfigSchema = z.object({ - app: z.string().min(1, "App ID is required").describe(SecretSyncs.DESTINATION_CONFIG.HUMANITEC.app), - org: z.string().min(1, "Org ID is required").describe(SecretSyncs.DESTINATION_CONFIG.HUMANITEC.org), - env: z.string().min(1, "Env ID is required").describe(SecretSyncs.DESTINATION_CONFIG.HUMANITEC.env) -}); +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 }; diff --git a/backend/src/services/secret-sync/humanitec/index.ts b/backend/src/services/secret-sync/humanitec/index.ts index 24bad622e..c1095fda0 100644 --- a/backend/src/services/secret-sync/humanitec/index.ts +++ b/backend/src/services/secret-sync/humanitec/index.ts @@ -1,4 +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/docs/images/integrations/humanitec/add-humanitec-connection.png b/docs/images/app-connections/humanitec/add-humanitec-connection.png similarity index 100% rename from docs/images/integrations/humanitec/add-humanitec-connection.png rename to docs/images/app-connections/humanitec/add-humanitec-connection.png diff --git a/docs/images/integrations/humanitec/add-service-user-to-application.png b/docs/images/app-connections/humanitec/add-service-user-to-application.png similarity index 100% rename from docs/images/integrations/humanitec/add-service-user-to-application.png rename to docs/images/app-connections/humanitec/add-service-user-to-application.png diff --git a/docs/images/integrations/humanitec/create-service-user.png b/docs/images/app-connections/humanitec/create-service-user.png similarity index 100% rename from docs/images/integrations/humanitec/create-service-user.png rename to docs/images/app-connections/humanitec/create-service-user.png diff --git a/docs/integrations/app-connections/humanitec.mdx b/docs/integrations/app-connections/humanitec.mdx index 8d54bd783..eeb1f50a8 100644 --- a/docs/integrations/app-connections/humanitec.mdx +++ b/docs/integrations/app-connections/humanitec.mdx @@ -11,12 +11,12 @@ Infisical supports connecting to Humanitec using a service user. Navigate to the Humanitec Service Users tab and create a new service user. This user will be used to connect to Humanitec from Infisical. - ![Humanitec Service User](/images/integrations/humanitec/create-service-user.png) + ![Humanitec Service User](/images/app-connections/humanitec/create-service-user.png) Add the Service User to the Application you want to sync with Infisical. Assign at least the **Developer** role to the Service User. - ![Humanitec Service User Role](/images/integrations/humanitec/add-service-user-to-application.png) + ![Humanitec Service User Role](/images/app-connections/humanitec/add-service-user-to-application.png) Generate an API token for the Service User on the Service Users tab. @@ -33,7 +33,7 @@ Infisical supports connecting to Humanitec using a service user. Add the API token to Infisical as a secret key. This will allow Infisical to connect to Humanitec using the service user. - ![Infisical Secret Key](/images/integrations/humanitec/add-humanitec-connection.png) + ![Infisical Secret Key](/images/app-connections/humanitec/add-humanitec-connection.png) Your **Humanitec Connection** is now available for use. diff --git a/docs/mint.json b/docs/mint.json index d54c27ffa..a80b79ba3 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -1,6 +1,6 @@ { "name": "Infisical", - "openapi": "https://3a13-190-31-36-142.ngrok-free.app/api/docs/json", + "openapi": "https://app.infisical.com/api/docs/json", "logo": { "dark": "/logo/dark.svg", "light": "/logo/light.svg", diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/HumanitecSyncFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/HumanitecSyncFields.tsx index e1b05c606..438717785 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/HumanitecSyncFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/HumanitecSyncFields.tsx @@ -1,13 +1,18 @@ 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 } from "@app/components/v2"; +import { FilterableSelect, FormControl, Select, SelectItem, Tooltip } from "@app/components/v2"; 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"; @@ -19,11 +24,17 @@ export const HumanitecSyncFields = () => { 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.app", ""); }} /> + ( + + + + )} + /> { isDisabled={!connectionId} value={organizations ? (organizations.find((org) => org.id === value) ?? []) : []} onChange={(option) => - onChange((option as SingleValue)?.id ?? null) + onChange((option as SingleValue)?.id ?? null) } options={organizations} placeholder="Select an organization..." @@ -60,7 +96,22 @@ export const HumanitecSyncFields = () => { name="destinationConfig.app" control={control} render={({ field: { value, onChange }, fieldState: { error } }) => ( - + +
+ Don't see the app you're looking for?{" "} + +
+ + } + > { .find((org) => org.id === currentOrg) ?.apps?.find((app) => app.id === value) ?? null } - onChange={(option) => - onChange((option as SingleValue)?.id ?? null) - } + onChange={(option) => { + onChange((option as SingleValue)?.id ?? null); + setValue("destinationConfig.env", ""); + }} options={ currentOrg ? (organizations.find((org) => org.id === currentOrg)?.apps ?? []) : [] } @@ -83,43 +135,34 @@ export const HumanitecSyncFields = () => {
)} /> - ( - - org.id === currentOrg) - ?.apps?.find((app) => app.id === currentApp) - ?.envs?.find((env) => env.id === value) ?? null - } - onChange={(option) => - onChange((option as SingleValue)?.id ?? null) - } - options={ - currentApp - ? ((organizations.find((org) => org.id === currentOrg)?.apps ?? [])?.find( - (app) => app.id === currentApp - )?.envs ?? []) - : [] - } - placeholder="Select an env..." - getOptionLabel={(option) => option.name} - getOptionValue={(option) => option.id.toString()} - /> - - )} - /> + {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/SecretSyncReviewFields/HumanitecSyncReviewFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/HumanitecSyncReviewFields.tsx index 5ae98e232..a680041c4 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/HumanitecSyncReviewFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/HumanitecSyncReviewFields.tsx @@ -3,18 +3,22 @@ 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} - {envId} + {appId} + {scope === HumanitecSyncScope.Environment && ( + {envId} + )} ); }; 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 index d70d3318d..bb438557f 100644 --- 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 @@ -2,14 +2,23 @@ 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.object({ - org: z.string().trim().min(1, "Organization required"), - app: z.string().trim().min(1, "App required"), - env: z.string().trim().min(1, "Environment required") - }) + 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/helpers/appConnections.ts b/frontend/src/helpers/appConnections.ts index 683626e26..cf643218b 100644 --- a/frontend/src/helpers/appConnections.ts +++ b/frontend/src/helpers/appConnections.ts @@ -45,8 +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.AccessKey: - return { name: "Access Key", icon: faKey }; + case HumanitecConnectionMethod.API_TOKEN: + return { name: "API Token", icon: faKey }; default: throw new Error(`Unhandled App Connection Method: ${method}`); } diff --git a/frontend/src/hooks/api/appConnections/humanitec/types.ts b/frontend/src/hooks/api/appConnections/humanitec/types.ts index 22b85f77d..5ab1b4d31 100644 --- a/frontend/src/hooks/api/appConnections/humanitec/types.ts +++ b/frontend/src/hooks/api/appConnections/humanitec/types.ts @@ -12,4 +12,17 @@ export type THumanitecApp = { 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 063dcbc80..a323765f7 100644 --- a/frontend/src/hooks/api/appConnections/types/app-options.ts +++ b/frontend/src/hooks/api/appConnections/types/app-options.ts @@ -44,7 +44,8 @@ export type TAppConnectionOption = | TGcpConnectionOption | TAzureAppConfigurationConnectionOption | TAzureKeyVaultConnectionOption - | TDatabricksConnectionOption; + | TDatabricksConnectionOption + | THumanitecConnectionOption; export type TAppConnectionOptionMap = { [AppConnection.AWS]: TAwsConnectionOption; diff --git a/frontend/src/hooks/api/appConnections/types/humanitec-connection.ts b/frontend/src/hooks/api/appConnections/types/humanitec-connection.ts index b604b8694..2473050cd 100644 --- a/frontend/src/hooks/api/appConnections/types/humanitec-connection.ts +++ b/frontend/src/hooks/api/appConnections/types/humanitec-connection.ts @@ -2,12 +2,12 @@ import { AppConnection } from "@app/hooks/api/appConnections/enums"; import { TRootAppConnection } from "@app/hooks/api/appConnections/types/root-connection"; export enum HumanitecConnectionMethod { - AccessKey = "access-key" + API_TOKEN = "api-token" } export type THumanitecConnection = TRootAppConnection & { app: AppConnection.Humanitec } & { - method: HumanitecConnectionMethod.AccessKey; + method: HumanitecConnectionMethod.API_TOKEN; credentials: { - accessKeyId: string; + apiToken: string; }; }; diff --git a/frontend/src/hooks/api/secretSyncs/types/humanitec-sync.ts b/frontend/src/hooks/api/secretSyncs/types/humanitec-sync.ts index dc22cf3e0..5dc06e031 100644 --- a/frontend/src/hooks/api/secretSyncs/types/humanitec-sync.ts +++ b/frontend/src/hooks/api/secretSyncs/types/humanitec-sync.ts @@ -4,13 +4,26 @@ import { TRootSecretSync } from "@app/hooks/api/secretSyncs/types/root-sync"; export type THumanitecSync = TRootSecretSync & { destination: SecretSync.Humanitec; - destinationConfig: { - org: string; - app: string; - }; + 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/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/HumanitecConnectionForm.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/HumanitecConnectionForm.tsx index a3c9eb4a7..b119f46ad 100644 --- a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/HumanitecConnectionForm.tsx +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/HumanitecConnectionForm.tsx @@ -30,9 +30,9 @@ const rootSchema = genericAppConnectionFieldsSchema.extend({ const formSchema = z.discriminatedUnion("method", [ rootSchema.extend({ - method: z.literal(HumanitecConnectionMethod.AccessKey), + method: z.literal(HumanitecConnectionMethod.API_TOKEN), credentials: z.object({ - accessKeyId: z.string().trim().min(1, "Service API Token required") + apiToken: z.string().trim().min(1, "Service API Token required") }) }) ]); @@ -46,7 +46,7 @@ export const HumanitecConnectionForm = ({ appConnection, onSubmit }: Props) => { resolver: zodResolver(formSchema), defaultValues: appConnection ?? { app: AppConnection.Humanitec, - method: HumanitecConnectionMethod.AccessKey + method: HumanitecConnectionMethod.API_TOKEN } }); @@ -66,7 +66,7 @@ export const HumanitecConnectionForm = ({ appConnection, onSubmit }: Props) => { render={({ field: { value, onChange }, fieldState: { error } }) => ( { )} /> ( 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 f295b964c..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 @@ -60,8 +61,17 @@ export const getSecretSyncDestinationColValues = (secretSync: TSecretSync) => { primaryText = destinationConfig.scope; break; case SecretSync.Humanitec: - primaryText = destinationConfig.app; - secondaryText = `Org - ${destinationConfig.org}`; + 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 index ce80b14ec..f750ce885 100644 --- a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/HumanitecSyncDestinationSection.tsx +++ b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/HumanitecSyncDestinationSection.tsx @@ -1,19 +1,49 @@ +import { ReactNode } from "react"; + import { SecretSyncLabel } from "@app/components/secret-syncs"; -import { THumanitecSync } from "@app/hooks/api/secretSyncs/types/humanitec-sync"; +import { + HumanitecSyncScope, + THumanitecSync +} from "@app/hooks/api/secretSyncs/types/humanitec-sync"; type Props = { secretSync: THumanitecSync; }; export const HumanitecSyncDestinationSection = ({ secretSync }: Props) => { - const { - destinationConfig: { app, org } - } = secretSync; + 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 ( <> - {app} - {org} + + {destinationConfig.scope.replace("-", " ")} + + {Components} ); };