From 3f9a793578e3c8ec34e4e0d1d7bf0adf6d099f65 Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Tue, 8 Apr 2025 23:52:27 +0800 Subject: [PATCH] feat: added camunda secret sync --- backend/src/lib/api-docs/constants.ts | 4 + .../camunda-sync-router.ts | 13 ++ .../routes/v1/secret-sync-routers/index.ts | 4 +- .../secret-sync-routers/secret-sync-router.ts | 7 +- .../app-connection/app-connection-maps.ts | 3 +- .../camunda/camunda-sync-constants.ts | 10 ++ .../secret-sync/camunda/camunda-sync-fns.ts | 161 ++++++++++++++++++ .../camunda/camunda-sync-schemas.ts | 47 +++++ .../secret-sync/camunda/camunda-sync-types.ts | 38 +++++ .../src/services/secret-sync/camunda/index.ts | 4 + .../services/secret-sync/secret-sync-enums.ts | 3 +- .../services/secret-sync/secret-sync-fns.ts | 20 ++- .../services/secret-sync/secret-sync-maps.ts | 6 +- .../services/secret-sync/secret-sync-types.ts | 18 +- .../CamundaSyncFields.tsx | 80 +++++++++ .../SecretSyncDestinationFields.tsx | 3 + .../SecretSyncOptionsFields.tsx | 1 + .../CamundaSyncReviewFields.tsx | 20 +++ .../SecretSyncReviewFields.tsx | 4 + .../camunda-sync-destination-schema.ts | 15 ++ .../forms/schemas/secret-sync-schema.ts | 4 +- frontend/src/helpers/secretSyncs.ts | 7 +- .../hooks/api/appConnections/camunda/index.ts | 1 + .../api/appConnections/camunda/queries.tsx | 37 ++++ .../hooks/api/appConnections/camunda/types.ts | 4 + frontend/src/hooks/api/secretSyncs/enums.ts | 3 +- .../api/secretSyncs/types/camunda-sync.ts | 20 +++ .../src/hooks/api/secretSyncs/types/index.ts | 4 +- .../CamundaSyncDestinationCol.tsx | 14 ++ .../SecretSyncDestinationCol.tsx | 3 + .../SecretSyncTable/helpers/index.ts | 4 + .../CamundaSyncDestinationSection.tsx | 14 ++ .../SecretSyncDestinatonSection.tsx | 4 + .../SecretSyncOptionsSection.tsx | 1 + 34 files changed, 565 insertions(+), 16 deletions(-) create mode 100644 backend/src/server/routes/v1/secret-sync-routers/camunda-sync-router.ts create mode 100644 backend/src/services/secret-sync/camunda/camunda-sync-constants.ts create mode 100644 backend/src/services/secret-sync/camunda/camunda-sync-fns.ts create mode 100644 backend/src/services/secret-sync/camunda/camunda-sync-schemas.ts create mode 100644 backend/src/services/secret-sync/camunda/camunda-sync-types.ts create mode 100644 backend/src/services/secret-sync/camunda/index.ts create mode 100644 frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/CamundaSyncFields.tsx create mode 100644 frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/CamundaSyncReviewFields.tsx create mode 100644 frontend/src/components/secret-syncs/forms/schemas/camunda-sync-destination-schema.ts create mode 100644 frontend/src/hooks/api/appConnections/camunda/index.ts create mode 100644 frontend/src/hooks/api/appConnections/camunda/queries.tsx create mode 100644 frontend/src/hooks/api/appConnections/camunda/types.ts create mode 100644 frontend/src/hooks/api/secretSyncs/types/camunda-sync.ts create mode 100644 frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/CamundaSyncDestinationCol.tsx create mode 100644 frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/CamundaSyncDestinationSection.tsx diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index ea5c07789..0f25324c3 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -1804,6 +1804,10 @@ export const SecretSyncs = { DATABRICKS: { scope: "The Databricks secret scope that secrets should be synced to." }, + CAMUNDA: { + scope: "The Camunda scope that secrets should be synced to.", + clusterUUID: "The UUID of the Camunda cluster 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.", diff --git a/backend/src/server/routes/v1/secret-sync-routers/camunda-sync-router.ts b/backend/src/server/routes/v1/secret-sync-routers/camunda-sync-router.ts new file mode 100644 index 000000000..13a8680c8 --- /dev/null +++ b/backend/src/server/routes/v1/secret-sync-routers/camunda-sync-router.ts @@ -0,0 +1,13 @@ +import { CamundaSyncSchema, CreateCamundaSyncSchema, UpdateCamundaSyncSchema } from "@app/services/secret-sync/camunda"; +import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; + +import { registerSyncSecretsEndpoints } from "./secret-sync-endpoints"; + +export const registerCamundaSyncRouter = async (server: FastifyZodProvider) => + registerSyncSecretsEndpoints({ + destination: SecretSync.Camunda, + server, + responseSchema: CamundaSyncSchema, + createSchema: CreateCamundaSyncSchema, + updateSchema: UpdateCamundaSyncSchema + }); 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 c342f3b73..2126048b8 100644 --- a/backend/src/server/routes/v1/secret-sync-routers/index.ts +++ b/backend/src/server/routes/v1/secret-sync-routers/index.ts @@ -4,6 +4,7 @@ import { registerAwsParameterStoreSyncRouter } from "./aws-parameter-store-sync- import { registerAwsSecretsManagerSyncRouter } from "./aws-secrets-manager-sync-router"; import { registerAzureAppConfigurationSyncRouter } from "./azure-app-configuration-sync-router"; import { registerAzureKeyVaultSyncRouter } from "./azure-key-vault-sync-router"; +import { registerCamundaSyncRouter } from "./camunda-sync-router"; import { registerDatabricksSyncRouter } from "./databricks-sync-router"; import { registerGcpSyncRouter } from "./gcp-sync-router"; import { registerGitHubSyncRouter } from "./github-sync-router"; @@ -19,5 +20,6 @@ export const SECRET_SYNC_REGISTER_ROUTER_MAP: Record { diff --git a/backend/src/services/app-connection/app-connection-maps.ts b/backend/src/services/app-connection/app-connection-maps.ts index eb28070d5..9fffe5ac0 100644 --- a/backend/src/services/app-connection/app-connection-maps.ts +++ b/backend/src/services/app-connection/app-connection-maps.ts @@ -9,5 +9,6 @@ export const APP_CONNECTION_NAME_MAP: Record = { [AppConnection.Databricks]: "Databricks", [AppConnection.Humanitec]: "Humanitec", [AppConnection.Postgres]: "PostgreSQL", - [AppConnection.MsSql]: "Microsoft SQL Server" + [AppConnection.MsSql]: "Microsoft SQL Server", + [AppConnection.Camunda]: "Camunda" }; diff --git a/backend/src/services/secret-sync/camunda/camunda-sync-constants.ts b/backend/src/services/secret-sync/camunda/camunda-sync-constants.ts new file mode 100644 index 000000000..7a2bad8f7 --- /dev/null +++ b/backend/src/services/secret-sync/camunda/camunda-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 CAMUNDA_SYNC_LIST_OPTION: TSecretSyncListItem = { + name: "Camunda", + destination: SecretSync.Camunda, + connection: AppConnection.Camunda, + canImportSecrets: true +}; diff --git a/backend/src/services/secret-sync/camunda/camunda-sync-fns.ts b/backend/src/services/secret-sync/camunda/camunda-sync-fns.ts new file mode 100644 index 000000000..e89df89e6 --- /dev/null +++ b/backend/src/services/secret-sync/camunda/camunda-sync-fns.ts @@ -0,0 +1,161 @@ +import { request } from "@app/lib/config/request"; +import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal"; +import { getCamundaConnectionAccessToken } from "@app/services/app-connection/camunda"; +import { IntegrationUrls } from "@app/services/integration-auth/integration-list"; +import { TKmsServiceFactory } from "@app/services/kms/kms-service"; +import { + TCamundaCreateSecret, + TCamundaDeleteSecret, + TCamundaListSecrets, + TCamundaListSecretsResponse, + TCamundaPutSecret, + TCamundaSyncWithCredentials +} from "@app/services/secret-sync/camunda/camunda-sync-types"; +import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors"; + +import { TSecretMap } from "../secret-sync-types"; + +type TCamundaSecretSyncFactoryDeps = { + appConnectionDAL: Pick; + kmsService: Pick; +}; + +const getCamundaSecrets = async ({ accessToken, clusterUUID }: TCamundaListSecrets) => { + const { data } = await request.get( + `${IntegrationUrls.CAMUNDA_API_URL}/clusters/${clusterUUID}/secrets`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" + } + } + ); + + return data; +}; + +const createCamundaSecret = async ({ accessToken, clusterUUID, key, value }: TCamundaCreateSecret) => + request.post( + `${IntegrationUrls.CAMUNDA_API_URL}/clusters/${clusterUUID}/secrets`, + { + secretName: key, + secretValue: value + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" + } + } + ); + +const deleteCamundaSecret = async ({ accessToken, clusterUUID, key }: TCamundaDeleteSecret) => + request.delete(`${IntegrationUrls.CAMUNDA_API_URL}/clusters/${clusterUUID}/secrets/${key}`, { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" + } + }); + +const updateCamundaSecret = async ({ accessToken, clusterUUID, key, value }: TCamundaPutSecret) => + request.put( + `${IntegrationUrls.CAMUNDA_API_URL}/clusters/${clusterUUID}/secrets/${key}`, + { + secretValue: value + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" + } + } + ); + +export const camundaSyncFactory = ({ kmsService, appConnectionDAL }: TCamundaSecretSyncFactoryDeps) => { + const syncSecrets = async (secretSync: TCamundaSyncWithCredentials, secretMap: TSecretMap) => { + const { + destinationConfig: { clusterUUID }, + connection + } = secretSync; + + const accessToken = await getCamundaConnectionAccessToken(connection, appConnectionDAL, kmsService); + const camundaSecrets = await getCamundaSecrets({ accessToken, clusterUUID }); + + for await (const entry of Object.entries(secretMap)) { + const [key, { value }] = entry; + + try { + if (camundaSecrets[key] === undefined) { + await createCamundaSecret({ + key, + value, + clusterUUID, + accessToken + }); + } else if (camundaSecrets[key] !== value) { + await updateCamundaSecret({ + key, + value, + clusterUUID, + accessToken + }); + } + } catch (error) { + throw new SecretSyncError({ + error, + secretKey: key + }); + } + } + + if (secretSync.syncOptions.disableSecretDeletion) return; + + for await (const secret of Object.keys(camundaSecrets)) { + if (!(secret in secretMap)) { + await deleteCamundaSecret({ + key: secret, + clusterUUID, + accessToken + }); + } + } + }; + + const removeSecrets = async (secretSync: TCamundaSyncWithCredentials, secretMap: TSecretMap) => { + const { + destinationConfig: { clusterUUID }, + connection + } = secretSync; + + const accessToken = await getCamundaConnectionAccessToken(connection, appConnectionDAL, kmsService); + const camundaSecrets = await getCamundaSecrets({ accessToken, clusterUUID }); + + for await (const secret of Object.keys(camundaSecrets)) { + if (!(secret in secretMap)) { + await deleteCamundaSecret({ + key: secret, + clusterUUID, + accessToken + }); + } + } + }; + + const getSecrets = async (secretSync: TCamundaSyncWithCredentials) => { + const { + destinationConfig: { clusterUUID }, + connection + } = secretSync; + + const accessToken = await getCamundaConnectionAccessToken(connection, appConnectionDAL, kmsService); + const camundaSecrets = await getCamundaSecrets({ accessToken, clusterUUID }); + + return Object.fromEntries(Object.entries(camundaSecrets).map(([key, value]) => [key, { value }])); + }; + + return { + syncSecrets, + removeSecrets, + getSecrets + }; +}; diff --git a/backend/src/services/secret-sync/camunda/camunda-sync-schemas.ts b/backend/src/services/secret-sync/camunda/camunda-sync-schemas.ts new file mode 100644 index 000000000..726b5dfac --- /dev/null +++ b/backend/src/services/secret-sync/camunda/camunda-sync-schemas.ts @@ -0,0 +1,47 @@ +import { z } from "zod"; + +import { SecretSyncs } from "@app/lib/api-docs"; +import { AppConnection } from "@app/services/app-connection/app-connection-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 CamundaSyncDestinationConfigSchema = z.object({ + scope: z.string().trim().min(1, "Camunda scope required").describe(SecretSyncs.DESTINATION_CONFIG.CAMUNDA.scope), + clusterUUID: z + .string() + .min(1, "Camunda cluster UUID is required") + .describe(SecretSyncs.DESTINATION_CONFIG.CAMUNDA.clusterUUID) +}); + +const CamundaSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: true }; + +export const CamundaSyncSchema = BaseSecretSyncSchema(SecretSync.Camunda, CamundaSyncOptionsConfig).extend({ + destination: z.literal(SecretSync.Camunda), + destinationConfig: CamundaSyncDestinationConfigSchema +}); + +export const CreateCamundaSyncSchema = GenericCreateSecretSyncFieldsSchema( + SecretSync.Camunda, + CamundaSyncOptionsConfig +).extend({ + destinationConfig: CamundaSyncDestinationConfigSchema +}); + +export const UpdateCamundaSyncSchema = GenericUpdateSecretSyncFieldsSchema( + SecretSync.Camunda, + CamundaSyncOptionsConfig +).extend({ + destinationConfig: CamundaSyncDestinationConfigSchema.optional() +}); + +export const CamundaSyncListItemSchema = z.object({ + name: z.literal("Camunda"), + connection: z.literal(AppConnection.Camunda), + destination: z.literal(SecretSync.Camunda), + canImportSecrets: z.literal(true) +}); diff --git a/backend/src/services/secret-sync/camunda/camunda-sync-types.ts b/backend/src/services/secret-sync/camunda/camunda-sync-types.ts new file mode 100644 index 000000000..49eb3262e --- /dev/null +++ b/backend/src/services/secret-sync/camunda/camunda-sync-types.ts @@ -0,0 +1,38 @@ +import { z } from "zod"; + +import { TCamundaConnection } from "@app/services/app-connection/camunda"; + +import { CamundaSyncListItemSchema, CamundaSyncSchema, CreateCamundaSyncSchema } from "./camunda-sync-schemas"; + +export type TCamundaSync = z.infer; + +export type TCamundaSyncInput = z.infer; + +export type TCamundaSyncListItem = z.infer; + +export type TCamundaSyncWithCredentials = TCamundaSync & { + connection: TCamundaConnection; +}; + +export type TCamundaListSecretsResponse = { [key: string]: string }; + +type TBaseCamundaSecretRequest = { + accessToken: string; + clusterUUID: string; +}; + +export type TCamundaListSecrets = TBaseCamundaSecretRequest; + +export type TCamundaCreateSecret = { + key: string; + value?: string; +} & TBaseCamundaSecretRequest; + +export type TCamundaPutSecret = { + key: string; + value?: string; +} & TBaseCamundaSecretRequest; + +export type TCamundaDeleteSecret = { + key: string; +} & TBaseCamundaSecretRequest; diff --git a/backend/src/services/secret-sync/camunda/index.ts b/backend/src/services/secret-sync/camunda/index.ts new file mode 100644 index 000000000..c81d82c99 --- /dev/null +++ b/backend/src/services/secret-sync/camunda/index.ts @@ -0,0 +1,4 @@ +export * from "./camunda-sync-constants"; +export * from "./camunda-sync-fns"; +export * from "./camunda-sync-schemas"; +export * from "./camunda-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 006d033f5..9503d45a9 100644 --- a/backend/src/services/secret-sync/secret-sync-enums.ts +++ b/backend/src/services/secret-sync/secret-sync-enums.ts @@ -6,7 +6,8 @@ export enum SecretSync { AzureKeyVault = "azure-key-vault", AzureAppConfiguration = "azure-app-configuration", Databricks = "databricks", - Humanitec = "humanitec" + Humanitec = "humanitec", + Camunda = "camunda" } 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 6c8a6d4df..077d0ac05 100644 --- a/backend/src/services/secret-sync/secret-sync-fns.ts +++ b/backend/src/services/secret-sync/secret-sync-fns.ts @@ -22,6 +22,7 @@ import { TAppConnectionDALFactory } from "../app-connection/app-connection-dal"; import { TKmsServiceFactory } from "../kms/kms-service"; import { AZURE_APP_CONFIGURATION_SYNC_LIST_OPTION, azureAppConfigurationSyncFactory } from "./azure-app-configuration"; import { AZURE_KEY_VAULT_SYNC_LIST_OPTION, azureKeyVaultSyncFactory } from "./azure-key-vault"; +import { CAMUNDA_SYNC_LIST_OPTION, camundaSyncFactory } from "./camunda"; import { GCP_SYNC_LIST_OPTION } from "./gcp"; import { GcpSyncFns } from "./gcp/gcp-sync-fns"; import { HUMANITEC_SYNC_LIST_OPTION } from "./humanitec"; @@ -35,7 +36,8 @@ const SECRET_SYNC_LIST_OPTIONS: Record = { [SecretSync.AzureKeyVault]: AZURE_KEY_VAULT_SYNC_LIST_OPTION, [SecretSync.AzureAppConfiguration]: AZURE_APP_CONFIGURATION_SYNC_LIST_OPTION, [SecretSync.Databricks]: DATABRICKS_SYNC_LIST_OPTION, - [SecretSync.Humanitec]: HUMANITEC_SYNC_LIST_OPTION + [SecretSync.Humanitec]: HUMANITEC_SYNC_LIST_OPTION, + [SecretSync.Camunda]: CAMUNDA_SYNC_LIST_OPTION }; export const listSecretSyncOptions = () => { @@ -121,6 +123,11 @@ export const SecretSyncFns = { }).syncSecrets(secretSync, secretMap); case SecretSync.Humanitec: return HumanitecSyncFns.syncSecrets(secretSync, secretMap); + case SecretSync.Camunda: + return camundaSyncFactory({ + appConnectionDAL, + kmsService + }).syncSecrets(secretSync, secretMap); default: throw new Error( `Unhandled sync destination for sync secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}` @@ -165,6 +172,12 @@ export const SecretSyncFns = { case SecretSync.Humanitec: secretMap = await HumanitecSyncFns.getSecrets(secretSync); break; + case SecretSync.Camunda: + secretMap = await camundaSyncFactory({ + appConnectionDAL, + kmsService + }).getSecrets(secretSync); + break; default: throw new Error( `Unhandled sync destination for get secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}` @@ -207,6 +220,11 @@ export const SecretSyncFns = { }).removeSecrets(secretSync, secretMap); case SecretSync.Humanitec: return HumanitecSyncFns.removeSecrets(secretSync, secretMap); + case SecretSync.Camunda: + return camundaSyncFactory({ + appConnectionDAL, + kmsService + }).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 cd4125e1b..9830a89f0 100644 --- a/backend/src/services/secret-sync/secret-sync-maps.ts +++ b/backend/src/services/secret-sync/secret-sync-maps.ts @@ -9,7 +9,8 @@ export const SECRET_SYNC_NAME_MAP: Record = { [SecretSync.AzureKeyVault]: "Azure Key Vault", [SecretSync.AzureAppConfiguration]: "Azure App Configuration", [SecretSync.Databricks]: "Databricks", - [SecretSync.Humanitec]: "Humanitec" + [SecretSync.Humanitec]: "Humanitec", + [SecretSync.Camunda]: "Camunda" }; export const SECRET_SYNC_CONNECTION_MAP: Record = { @@ -20,5 +21,6 @@ export const SECRET_SYNC_CONNECTION_MAP: Record = { [SecretSync.AzureKeyVault]: AppConnection.AzureKeyVault, [SecretSync.AzureAppConfiguration]: AppConnection.AzureAppConfiguration, [SecretSync.Databricks]: AppConnection.Databricks, - [SecretSync.Humanitec]: AppConnection.Humanitec + [SecretSync.Humanitec]: AppConnection.Humanitec, + [SecretSync.Camunda]: AppConnection.Camunda }; diff --git a/backend/src/services/secret-sync/secret-sync-types.ts b/backend/src/services/secret-sync/secret-sync-types.ts index bd28e1ee7..056749a92 100644 --- a/backend/src/services/secret-sync/secret-sync-types.ts +++ b/backend/src/services/secret-sync/secret-sync-types.ts @@ -9,6 +9,12 @@ import { TAwsSecretsManagerSyncListItem, TAwsSecretsManagerSyncWithCredentials } from "@app/services/secret-sync/aws-secrets-manager"; +import { + TCamundaSync, + TCamundaSyncInput, + TCamundaSyncListItem, + TCamundaSyncWithCredentials +} from "@app/services/secret-sync/camunda"; import { TDatabricksSync, TDatabricksSyncInput, @@ -58,7 +64,8 @@ export type TSecretSync = | TAzureKeyVaultSync | TAzureAppConfigurationSync | TDatabricksSync - | THumanitecSync; + | THumanitecSync + | TCamundaSync; export type TSecretSyncWithCredentials = | TAwsParameterStoreSyncWithCredentials @@ -68,7 +75,8 @@ export type TSecretSyncWithCredentials = | TAzureKeyVaultSyncWithCredentials | TAzureAppConfigurationSyncWithCredentials | TDatabricksSyncWithCredentials - | THumanitecSyncWithCredentials; + | THumanitecSyncWithCredentials + | TCamundaSyncWithCredentials; export type TSecretSyncInput = | TAwsParameterStoreSyncInput @@ -78,7 +86,8 @@ export type TSecretSyncInput = | TAzureKeyVaultSyncInput | TAzureAppConfigurationSyncInput | TDatabricksSyncInput - | THumanitecSyncInput; + | THumanitecSyncInput + | TCamundaSyncInput; export type TSecretSyncListItem = | TAwsParameterStoreSyncListItem @@ -88,7 +97,8 @@ export type TSecretSyncListItem = | TAzureKeyVaultSyncListItem | TAzureAppConfigurationSyncListItem | TDatabricksSyncListItem - | THumanitecSyncListItem; + | THumanitecSyncListItem + | TCamundaSyncListItem; export type TSyncOptionsConfig = { canImportSecrets: boolean; diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/CamundaSyncFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/CamundaSyncFields.tsx new file mode 100644 index 000000000..820699951 --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/CamundaSyncFields.tsx @@ -0,0 +1,80 @@ +import { useEffect } from "react"; +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, Tooltip } from "@app/components/v2"; +import { useCamundaConnectionListClusters } from "@app/hooks/api/appConnections/camunda"; +import { TCamundaCluster } from "@app/hooks/api/appConnections/camunda/types"; +import { SecretSync } from "@app/hooks/api/secretSyncs"; +import { CamundaSyncScope } from "@app/hooks/api/secretSyncs/types/camunda-sync"; + +import { TSecretSyncForm } from "../schemas"; + +export const CamundaSyncFields = () => { + const { control, setValue } = useFormContext< + TSecretSyncForm & { destination: SecretSync.Camunda } + >(); + + const connectionId = useWatch({ name: "connection.id", control }); + + const { data: clusters, isPending } = useCamundaConnectionListClusters(connectionId, { + enabled: Boolean(connectionId) + }); + + useEffect(() => { + setValue("destinationConfig.scope", CamundaSyncScope.Cluster); + }, []); + + return ( + <> + { + setValue("destinationConfig.clusterUUID", ""); + }} + /> + ( + +
+ Don't see the cluster you're looking for?{" "} + +
+ + } + > + cluster.uuid === value) ?? null} + onChange={(option) => { + onChange((option as SingleValue)?.uuid ?? null); + setValue( + "destinationConfig.clusterName", + (option as SingleValue)?.name ?? "" + ); + }} + options={clusters} + placeholder="Select a cluster..." + getOptionLabel={(option) => option.name} + getOptionValue={(option) => option.uuid} + /> +
+ )} + /> + + ); +}; diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx index 973f8bf17..ea41c68ab 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx @@ -7,6 +7,7 @@ import { AwsParameterStoreSyncFields } from "./AwsParameterStoreSyncFields"; import { AwsSecretsManagerSyncFields } from "./AwsSecretsManagerSyncFields"; import { AzureAppConfigurationSyncFields } from "./AzureAppConfigurationSyncFields"; import { AzureKeyVaultSyncFields } from "./AzureKeyVaultSyncFields"; +import { CamundaSyncFields } from "./CamundaSyncFields"; import { DatabricksSyncFields } from "./DatabricksSyncFields"; import { GcpSyncFields } from "./GcpSyncFields"; import { GitHubSyncFields } from "./GitHubSyncFields"; @@ -34,6 +35,8 @@ export const SecretSyncDestinationFields = () => { return ; case SecretSync.Humanitec: return ; + case SecretSync.Camunda: + 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 b14bce809..063c06323 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx @@ -39,6 +39,7 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => { case SecretSync.AzureAppConfiguration: case SecretSync.Databricks: case SecretSync.Humanitec: + case SecretSync.Camunda: AdditionalSyncOptionsFieldsComponent = null; break; default: diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/CamundaSyncReviewFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/CamundaSyncReviewFields.tsx new file mode 100644 index 000000000..53aacae40 --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/CamundaSyncReviewFields.tsx @@ -0,0 +1,20 @@ +import { useFormContext } from "react-hook-form"; + +import { GenericFieldLabel } from "@app/components/secret-syncs"; +import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas"; +import { SecretSync } from "@app/hooks/api/secretSyncs"; + +export const CamundaSyncReviewFields = () => { + const { watch } = useFormContext(); + const scope = watch("destinationConfig.scope"); + const clusterName = watch("destinationConfig.clusterName"); + const clusterUUID = watch("destinationConfig.clusterUUID"); + return ( + <> + {scope} + + {clusterName} (uuid:{clusterUUID}) + + + ); +}; diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx index 5816635f6..4587d7ca5 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx @@ -17,6 +17,7 @@ import { } from "./AwsSecretsManagerSyncReviewFields"; import { AzureAppConfigurationSyncReviewFields } from "./AzureAppConfigurationSyncReviewFields"; import { AzureKeyVaultSyncReviewFields } from "./AzureKeyVaultSyncReviewFields"; +import { CamundaSyncReviewFields } from "./CamundaSyncReviewFields"; import { DatabricksSyncReviewFields } from "./DatabricksSyncReviewFields"; import { GcpSyncReviewFields } from "./GcpSyncReviewFields"; import { GitHubSyncReviewFields } from "./GitHubSyncReviewFields"; @@ -72,6 +73,9 @@ export const SecretSyncReviewFields = () => { case SecretSync.Humanitec: DestinationFieldsComponent = ; break; + case SecretSync.Camunda: + DestinationFieldsComponent = ; + break; default: throw new Error(`Unhandled Destination Review Fields: ${destination}`); } diff --git a/frontend/src/components/secret-syncs/forms/schemas/camunda-sync-destination-schema.ts b/frontend/src/components/secret-syncs/forms/schemas/camunda-sync-destination-schema.ts new file mode 100644 index 000000000..d95156e91 --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/schemas/camunda-sync-destination-schema.ts @@ -0,0 +1,15 @@ +import { z } from "zod"; + +import { BaseSecretSyncSchema } from "@app/components/secret-syncs/forms/schemas/base-secret-sync-schema"; +import { SecretSync } from "@app/hooks/api/secretSyncs"; + +export const CamundaSyncDestinationSchema = BaseSecretSyncSchema().merge( + z.object({ + destination: z.literal(SecretSync.Camunda), + destinationConfig: z.object({ + scope: z.string().trim().min(1, "Camunda scope required"), + clusterUUID: z.string().trim().min(1, "Camunda cluster UUID required"), + clusterName: z.string().optional() + }) + }) +); 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 be2322304..31fe6c2f5 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 @@ -7,6 +7,7 @@ import { GitHubSyncDestinationSchema } from "@app/components/secret-syncs/forms/ import { AwsParameterStoreSyncDestinationSchema } from "./aws-parameter-store-sync-destination-schema"; import { AzureAppConfigurationSyncDestinationSchema } from "./azure-app-configuration-sync-destination-schema"; import { AzureKeyVaultSyncDestinationSchema } from "./azure-key-vault-sync-destination-schema"; +import { CamundaSyncDestinationSchema } from "./camunda-sync-destination-schema"; import { GcpSyncDestinationSchema } from "./gcp-sync-destination-schema"; import { HumanitecSyncDestinationSchema } from "./humanitec-sync-destination-schema"; @@ -18,7 +19,8 @@ const SecretSyncUnionSchema = z.discriminatedUnion("destination", [ AzureKeyVaultSyncDestinationSchema, AzureAppConfigurationSyncDestinationSchema, DatabricksSyncDestinationSchema, - HumanitecSyncDestinationSchema + HumanitecSyncDestinationSchema, + CamundaSyncDestinationSchema ]); export const SecretSyncFormSchema = SecretSyncUnionSchema; diff --git a/frontend/src/helpers/secretSyncs.ts b/frontend/src/helpers/secretSyncs.ts index 1cc076cce..4c20bcf8e 100644 --- a/frontend/src/helpers/secretSyncs.ts +++ b/frontend/src/helpers/secretSyncs.ts @@ -23,6 +23,10 @@ export const SECRET_SYNC_MAP: Record = { [SecretSync.AzureKeyVault]: AppConnection.AzureKeyVault, [SecretSync.AzureAppConfiguration]: AppConnection.AzureAppConfiguration, [SecretSync.Databricks]: AppConnection.Databricks, - [SecretSync.Humanitec]: AppConnection.Humanitec + [SecretSync.Humanitec]: AppConnection.Humanitec, + [SecretSync.Camunda]: AppConnection.Camunda }; export const SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP: Record< diff --git a/frontend/src/hooks/api/appConnections/camunda/index.ts b/frontend/src/hooks/api/appConnections/camunda/index.ts new file mode 100644 index 000000000..b69c25120 --- /dev/null +++ b/frontend/src/hooks/api/appConnections/camunda/index.ts @@ -0,0 +1 @@ +export * from "./queries"; diff --git a/frontend/src/hooks/api/appConnections/camunda/queries.tsx b/frontend/src/hooks/api/appConnections/camunda/queries.tsx new file mode 100644 index 000000000..e159096e5 --- /dev/null +++ b/frontend/src/hooks/api/appConnections/camunda/queries.tsx @@ -0,0 +1,37 @@ +import { useQuery, UseQueryOptions } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +import { appConnectionKeys } from "../queries"; +import { TCamundaCluster } from "./types"; + +const camundaConnectionKeys = { + all: [...appConnectionKeys.all, "camunda"] as const, + listClusters: (connectionId: string) => + [...camundaConnectionKeys.all, "clusters", connectionId] as const +}; + +export const useCamundaConnectionListClusters = ( + connectionId: string, + options?: Omit< + UseQueryOptions< + TCamundaCluster[], + unknown, + TCamundaCluster[], + ReturnType + >, + "queryKey" | "queryFn" + > +) => { + return useQuery({ + queryKey: camundaConnectionKeys.listClusters(connectionId), + queryFn: async () => { + const { data } = await apiRequest.get<{ clusters: TCamundaCluster[] }>( + `/api/v1/app-connections/camunda/${connectionId}/clusters` + ); + + return data.clusters; + }, + ...options + }); +}; diff --git a/frontend/src/hooks/api/appConnections/camunda/types.ts b/frontend/src/hooks/api/appConnections/camunda/types.ts new file mode 100644 index 000000000..b24988eba --- /dev/null +++ b/frontend/src/hooks/api/appConnections/camunda/types.ts @@ -0,0 +1,4 @@ +export type TCamundaCluster = { + name: string; + uuid: string; +}; diff --git a/frontend/src/hooks/api/secretSyncs/enums.ts b/frontend/src/hooks/api/secretSyncs/enums.ts index 08accba16..9a5b7208b 100644 --- a/frontend/src/hooks/api/secretSyncs/enums.ts +++ b/frontend/src/hooks/api/secretSyncs/enums.ts @@ -6,7 +6,8 @@ export enum SecretSync { AzureKeyVault = "azure-key-vault", AzureAppConfiguration = "azure-app-configuration", Databricks = "databricks", - Humanitec = "humanitec" + Humanitec = "humanitec", + Camunda = "camunda" } export enum SecretSyncStatus { diff --git a/frontend/src/hooks/api/secretSyncs/types/camunda-sync.ts b/frontend/src/hooks/api/secretSyncs/types/camunda-sync.ts new file mode 100644 index 000000000..1f7f346c6 --- /dev/null +++ b/frontend/src/hooks/api/secretSyncs/types/camunda-sync.ts @@ -0,0 +1,20 @@ +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 enum CamundaSyncScope { + Cluster = "cluster" +} + +export type TCamundaSync = TRootSecretSync & { + destination: SecretSync.Camunda; + destinationConfig: { + scope: string; + clusterUUID: string; + }; + connection: { + app: AppConnection.Camunda; + name: string; + id: string; + }; +}; diff --git a/frontend/src/hooks/api/secretSyncs/types/index.ts b/frontend/src/hooks/api/secretSyncs/types/index.ts index a90a8a3ef..f0049eab2 100644 --- a/frontend/src/hooks/api/secretSyncs/types/index.ts +++ b/frontend/src/hooks/api/secretSyncs/types/index.ts @@ -7,6 +7,7 @@ import { DiscriminativePick } from "@app/types"; import { TAwsSecretsManagerSync } from "./aws-secrets-manager-sync"; import { TAzureAppConfigurationSync } from "./azure-app-configuration-sync"; import { TAzureKeyVaultSync } from "./azure-key-vault-sync"; +import { TCamundaSync } from "./camunda-sync"; import { TGcpSync } from "./gcp-sync"; import { THumanitecSync } from "./humanitec-sync"; @@ -24,7 +25,8 @@ export type TSecretSync = | TAzureKeyVaultSync | TAzureAppConfigurationSync | TDatabricksSync - | THumanitecSync; + | THumanitecSync + | TCamundaSync; export type TListSecretSyncs = { secretSyncs: TSecretSync[] }; diff --git a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/CamundaSyncDestinationCol.tsx b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/CamundaSyncDestinationCol.tsx new file mode 100644 index 000000000..ebfd80c4e --- /dev/null +++ b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/CamundaSyncDestinationCol.tsx @@ -0,0 +1,14 @@ +import { TCamundaSync } from "@app/hooks/api/secretSyncs/types/camunda-sync"; + +import { getSecretSyncDestinationColValues } from "../helpers"; +import { SecretSyncTableCell } from "../SecretSyncTableCell"; + +type Props = { + secretSync: TCamundaSync; +}; + +export const CamundaDestinationSyncCol = ({ 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 c4dc564b0..4ad6d2802 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 @@ -4,6 +4,7 @@ import { AwsParameterStoreSyncDestinationCol } from "./AwsParameterStoreSyncDest import { AwsSecretsManagerSyncDestinationCol } from "./AwsSecretsManagerSyncDestinationCol"; import { AzureAppConfigurationDestinationSyncCol } from "./AzureAppConfigurationDestinationSyncCol"; import { AzureKeyVaultDestinationSyncCol } from "./AzureKeyVaultDestinationSyncCol"; +import { CamundaDestinationSyncCol } from "./CamundaSyncDestinationCol"; import { DatabricksSyncDestinationCol } from "./DatabricksSyncDestinationCol"; import { GcpSyncDestinationCol } from "./GcpSyncDestinationCol"; import { GitHubSyncDestinationCol } from "./GitHubSyncDestinationCol"; @@ -31,6 +32,8 @@ export const SecretSyncDestinationCol = ({ secretSync }: Props) => { return ; case SecretSync.Humanitec: return ; + case SecretSync.Camunda: + 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 6eb3a04e3..0c4c788e3 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 @@ -73,6 +73,10 @@ export const getSecretSyncDestinationColValues = (secretSync: TSecretSync) => { } secondaryText = `Organization - ${destinationConfig.org}`; break; + case SecretSync.Camunda: + primaryText = destinationConfig.clusterUUID; + secondaryText = "Cluster"; + break; default: throw new Error(`Unhandled Destination Col Values ${destination}`); } diff --git a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/CamundaSyncDestinationSection.tsx b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/CamundaSyncDestinationSection.tsx new file mode 100644 index 000000000..35a77c3ee --- /dev/null +++ b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/CamundaSyncDestinationSection.tsx @@ -0,0 +1,14 @@ +import { GenericFieldLabel } from "@app/components/secret-syncs"; +import { TCamundaSync } from "@app/hooks/api/secretSyncs/types/camunda-sync"; + +type Props = { + secretSync: TCamundaSync; +}; + +export const CamundaSyncDestinationSection = ({ secretSync }: Props) => { + const { + destinationConfig: { clusterUUID } + } = secretSync; + + return {clusterUUID}; +}; 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 49892a0a2..4e149b2d3 100644 --- a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/SecretSyncDestinatonSection.tsx +++ b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/SecretSyncDestinatonSection.tsx @@ -16,6 +16,7 @@ import { GitHubSyncDestinationSection } from "@app/pages/secret-manager/SecretSy import { AzureAppConfigurationSyncDestinationSection } from "./AzureAppConfigurationSyncDestinationSection"; import { AzureKeyVaultSyncDestinationSection } from "./AzureKeyVaultSyncDestinationSection"; +import { CamundaSyncDestinationSection } from "./CamundaSyncDestinationSection"; import { GcpSyncDestinationSection } from "./GcpSyncDestinationSection"; import { HumanitecSyncDestinationSection } from "./HumanitecSyncDestinationSection"; @@ -57,6 +58,9 @@ export const SecretSyncDestinationSection = ({ secretSync, onEditDestination }: case SecretSync.Humanitec: DestinationComponents = ; break; + case SecretSync.Camunda: + 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 9349e7c63..99f313571 100644 --- a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/SecretSyncOptionsSection.tsx +++ b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/SecretSyncOptionsSection.tsx @@ -48,6 +48,7 @@ export const SecretSyncOptionsSection = ({ secretSync, onEditOptions }: Props) = case SecretSync.AzureAppConfiguration: case SecretSync.Databricks: case SecretSync.Humanitec: + case SecretSync.Camunda: AdditionalSyncOptionsComponent = null; break; default: