diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index 2f365b6d4..1f11e3077 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -1708,6 +1708,10 @@ export const AppConnections = { }, VERCEL: { apiToken: "The API token used to authenticate with Vercel." + }, + CAMUNDA: { + clientId: "The client ID used to authenticate with Camunda.", + clientSecret: "The client secret used to authenticate with Camunda." } } }; @@ -1818,6 +1822,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/app-connection-routers/app-connection-router.ts b/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts index adfce8c86..040d32bf4 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 @@ -12,6 +12,10 @@ import { AzureKeyVaultConnectionListItemSchema, SanitizedAzureKeyVaultConnectionSchema } from "@app/services/app-connection/azure-key-vault"; +import { + CamundaConnectionListItemSchema, + SanitizedCamundaConnectionSchema +} from "@app/services/app-connection/camunda"; import { DatabricksConnectionListItemSchema, SanitizedDatabricksConnectionSchema @@ -41,7 +45,8 @@ const SanitizedAppConnectionSchema = z.union([ ...SanitizedHumanitecConnectionSchema.options, ...SanitizedVercelConnectionSchema.options, ...SanitizedPostgresConnectionSchema.options, - ...SanitizedMsSqlConnectionSchema.options + ...SanitizedMsSqlConnectionSchema.options, + ...SanitizedCamundaConnectionSchema.options ]); const AppConnectionOptionsSchema = z.discriminatedUnion("app", [ @@ -54,7 +59,8 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [ HumanitecConnectionListItemSchema, VercelConnectionListItemSchema, PostgresConnectionListItemSchema, - MsSqlConnectionListItemSchema + MsSqlConnectionListItemSchema, + CamundaConnectionListItemSchema ]); export const registerAppConnectionRouter = async (server: FastifyZodProvider) => { diff --git a/backend/src/server/routes/v1/app-connection-routers/camunda-connection-router.ts b/backend/src/server/routes/v1/app-connection-routers/camunda-connection-router.ts new file mode 100644 index 000000000..7da0b7e5f --- /dev/null +++ b/backend/src/server/routes/v1/app-connection-routers/camunda-connection-router.ts @@ -0,0 +1,51 @@ +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 { + CreateCamundaConnectionSchema, + SanitizedCamundaConnectionSchema, + UpdateCamundaConnectionSchema +} from "@app/services/app-connection/camunda"; +import { AuthMode } from "@app/services/auth/auth-type"; + +import { registerAppConnectionEndpoints } from "./app-connection-endpoints"; + +export const registerCamundaConnectionRouter = async (server: FastifyZodProvider) => { + registerAppConnectionEndpoints({ + app: AppConnection.Camunda, + server, + sanitizedResponseSchema: SanitizedCamundaConnectionSchema, + createSchema: CreateCamundaConnectionSchema, + updateSchema: UpdateCamundaConnectionSchema + }); + + // The below endpoints are not exposed and for Infisical App use + + server.route({ + method: "GET", + url: `/:connectionId/clusters`, + config: { + rateLimit: readLimit + }, + schema: { + params: z.object({ + connectionId: z.string().uuid() + }), + response: { + 200: z.object({ + clusters: z.object({ uuid: z.string(), name: z.string() }).array() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { connectionId } = req.params; + + const clusters = await server.services.appConnection.camunda.listClusters(connectionId, req.permission); + + return { clusters }; + } + }); +}; 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 fe9fbe5e3..aa5297800 100644 --- a/backend/src/server/routes/v1/app-connection-routers/index.ts +++ b/backend/src/server/routes/v1/app-connection-routers/index.ts @@ -3,6 +3,7 @@ import { AppConnection } from "@app/services/app-connection/app-connection-enums import { registerAwsConnectionRouter } from "./aws-connection-router"; import { registerAzureAppConfigurationConnectionRouter } from "./azure-app-configuration-connection-router"; import { registerAzureKeyVaultConnectionRouter } from "./azure-key-vault-connection-router"; +import { registerCamundaConnectionRouter } from "./camunda-connection-router"; import { registerDatabricksConnectionRouter } from "./databricks-connection-router"; import { registerGcpConnectionRouter } from "./gcp-connection-router"; import { registerGitHubConnectionRouter } from "./github-connection-router"; @@ -24,5 +25,6 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record + 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 f0a35b3c0..ad087fdd1 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"; @@ -21,5 +22,6 @@ export const SECRET_SYNC_REGISTER_ROUTER_MAP: Record { getHumanitecConnectionListItem(), getVercelConnectionListItem(), getPostgresConnectionListItem(), - getMsSqlConnectionListItem() + getMsSqlConnectionListItem(), + getCamundaConnectionListItem() ].sort((a, b) => a.name.localeCompare(b.name)); }; @@ -112,6 +114,7 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record return "Service Account Impersonation"; case DatabricksConnectionMethod.ServicePrincipal: return "Service Principal"; + case CamundaConnectionMethod.ClientCredentials: + return "Client Credentials"; case HumanitecConnectionMethod.ApiToken: case VercelConnectionMethod.ApiToken: return "API Token"; @@ -181,5 +186,6 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record< [AppConnection.Humanitec]: platformManagedCredentialsNotSupported, [AppConnection.Postgres]: transferSqlConnectionCredentialsToPlatform as TAppConnectionTransitionCredentialsToPlatform, [AppConnection.MsSql]: transferSqlConnectionCredentialsToPlatform as TAppConnectionTransitionCredentialsToPlatform, + [AppConnection.Camunda]: platformManagedCredentialsNotSupported, [AppConnection.Vercel]: platformManagedCredentialsNotSupported }; diff --git a/backend/src/services/app-connection/app-connection-maps.ts b/backend/src/services/app-connection/app-connection-maps.ts index a39b3d1eb..9df7037c2 100644 --- a/backend/src/services/app-connection/app-connection-maps.ts +++ b/backend/src/services/app-connection/app-connection-maps.ts @@ -10,5 +10,6 @@ export const APP_CONNECTION_NAME_MAP: Record = { [AppConnection.Humanitec]: "Humanitec", [AppConnection.Vercel]: "Vercel", [AppConnection.Postgres]: "PostgreSQL", - [AppConnection.MsSql]: "Microsoft SQL Server" + [AppConnection.MsSql]: "Microsoft SQL Server", + [AppConnection.Camunda]: "Camunda" }; diff --git a/backend/src/services/app-connection/app-connection-service.ts b/backend/src/services/app-connection/app-connection-service.ts index 1803d7127..3114b4731 100644 --- a/backend/src/services/app-connection/app-connection-service.ts +++ b/backend/src/services/app-connection/app-connection-service.ts @@ -31,6 +31,8 @@ import { ValidateAwsConnectionCredentialsSchema } from "./aws"; import { awsConnectionService } from "./aws/aws-connection-service"; import { ValidateAzureAppConfigurationConnectionCredentialsSchema } from "./azure-app-configuration"; import { ValidateAzureKeyVaultConnectionCredentialsSchema } from "./azure-key-vault"; +import { ValidateCamundaConnectionCredentialsSchema } from "./camunda"; +import { camundaConnectionService } from "./camunda/camunda-connection-service"; import { ValidateDatabricksConnectionCredentialsSchema } from "./databricks"; import { databricksConnectionService } from "./databricks/databricks-connection-service"; import { ValidateGcpConnectionCredentialsSchema } from "./gcp"; @@ -62,7 +64,8 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record>>; @@ -86,6 +93,7 @@ export type TAppConnectionInput = { id: string } & ( | TVercelConnectionInput | TPostgresConnectionInput | TMsSqlConnectionInput + | TCamundaConnectionInput ); export type TSqlConnectionInput = TPostgresConnectionInput | TMsSqlConnectionInput; @@ -107,8 +115,9 @@ export type TAppConnectionConfig = | TAzureAppConfigurationConnectionConfig | TDatabricksConnectionConfig | THumanitecConnectionConfig - | TVercelConnectionConfig - | TSqlConnectionConfig; + | TSqlConnectionConfig + | TCamundaConnectionConfig + | TVercelConnectionConfig; export type TValidateAppConnectionCredentialsSchema = | TValidateAwsConnectionCredentialsSchema @@ -120,6 +129,7 @@ export type TValidateAppConnectionCredentialsSchema = | TValidateHumanitecConnectionCredentialsSchema | TValidatePostgresConnectionCredentialsSchema | TValidateMsSqlConnectionCredentialsSchema + | TValidateCamundaConnectionCredentialsSchema | TValidateVercelConnectionCredentialsSchema; export type TListAwsConnectionKmsKeys = { diff --git a/backend/src/services/app-connection/camunda/camunda-connection-enums.ts b/backend/src/services/app-connection/camunda/camunda-connection-enums.ts new file mode 100644 index 000000000..ea1ea0aaf --- /dev/null +++ b/backend/src/services/app-connection/camunda/camunda-connection-enums.ts @@ -0,0 +1,3 @@ +export enum CamundaConnectionMethod { + ClientCredentials = "client-credentials" +} diff --git a/backend/src/services/app-connection/camunda/camunda-connection-fns.ts b/backend/src/services/app-connection/camunda/camunda-connection-fns.ts new file mode 100644 index 000000000..90d9744b8 --- /dev/null +++ b/backend/src/services/app-connection/camunda/camunda-connection-fns.ts @@ -0,0 +1,88 @@ +import { request } from "@app/lib/config/request"; +import { BadRequestError } from "@app/lib/errors"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { encryptAppConnectionCredentials } from "@app/services/app-connection/app-connection-fns"; +import { IntegrationUrls } from "@app/services/integration-auth/integration-list"; +import { TKmsServiceFactory } from "@app/services/kms/kms-service"; + +import { TAppConnectionDALFactory } from "../app-connection-dal"; +import { CamundaConnectionMethod } from "./camunda-connection-enums"; +import { TAuthorizeCamundaConnection, TCamundaConnection, TCamundaConnectionConfig } from "./camunda-connection-types"; + +export const getCamundaConnectionListItem = () => { + return { + name: "Camunda" as const, + app: AppConnection.Camunda as const, + methods: Object.values(CamundaConnectionMethod) as [CamundaConnectionMethod.ClientCredentials] + }; +}; + +const authorizeCamundaConnection = async ({ + clientId, + clientSecret +}: Pick) => { + const { data } = await request.post( + IntegrationUrls.CAMUNDA_TOKEN_URL, + { + grant_type: "client_credentials", + client_id: clientId, + client_secret: clientSecret, + audience: "api.cloud.camunda.io" + }, + { + headers: { + "Content-Type": "application/json" + } + } + ); + + return { accessToken: data.access_token, expiresAt: data.expires_in * 1000 + Date.now() }; +}; + +export const getCamundaConnectionAccessToken = async ( + { id, orgId, credentials }: TCamundaConnection, + appConnectionDAL: Pick, + kmsService: Pick +) => { + const { clientSecret, clientId, accessToken, expiresAt } = credentials; + + // get new token if less than 30 seconds from expiry + if (Date.now() < expiresAt - 30_000) { + return accessToken; + } + + const authData = await authorizeCamundaConnection({ clientId, clientSecret }); + + const updatedCredentials: TCamundaConnection["credentials"] = { + ...credentials, + ...authData + }; + + const encryptedCredentials = await encryptAppConnectionCredentials({ + credentials: updatedCredentials, + orgId, + kmsService + }); + + await appConnectionDAL.updateById(id, { encryptedCredentials }); + + return authData.accessToken; +}; + +export const validateCamundaConnectionCredentials = async (appConnection: TCamundaConnectionConfig) => { + const { credentials } = appConnection; + + try { + const { accessToken, expiresAt } = await authorizeCamundaConnection(appConnection.credentials); + + return { + ...credentials, + accessToken, + expiresAt + }; + } catch (e: unknown) { + throw new BadRequestError({ + message: `Unable to validate connection: verify credentials` + }); + } +}; diff --git a/backend/src/services/app-connection/camunda/camunda-connection-schema.ts b/backend/src/services/app-connection/camunda/camunda-connection-schema.ts new file mode 100644 index 000000000..fa769c650 --- /dev/null +++ b/backend/src/services/app-connection/camunda/camunda-connection-schema.ts @@ -0,0 +1,77 @@ +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 { CamundaConnectionMethod } from "./camunda-connection-enums"; + +const BaseCamundaConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.Camunda) }); + +export const CamundaConnectionClientCredentialsInputCredentialsSchema = z.object({ + clientId: z.string().trim().min(1, "Client ID required").describe(AppConnections.CREDENTIALS.CAMUNDA.clientId), + clientSecret: z + .string() + .trim() + .min(1, "Client Secret required") + .describe(AppConnections.CREDENTIALS.CAMUNDA.clientSecret) +}); + +export const CamundaConnectionClientCredentialsOutputCredentialsSchema = z + .object({ + accessToken: z.string(), + expiresAt: z.number() + }) + .merge(CamundaConnectionClientCredentialsInputCredentialsSchema); + +export const CamundaConnectionSchema = z.intersection( + BaseCamundaConnectionSchema, + z.discriminatedUnion("method", [ + z.object({ + method: z.literal(CamundaConnectionMethod.ClientCredentials), + credentials: CamundaConnectionClientCredentialsOutputCredentialsSchema + }) + ]) +); + +export const SanitizedCamundaConnectionSchema = z.discriminatedUnion("method", [ + BaseCamundaConnectionSchema.extend({ + method: z.literal(CamundaConnectionMethod.ClientCredentials), + credentials: CamundaConnectionClientCredentialsOutputCredentialsSchema.pick({ + clientId: true + }) + }) +]); + +export const ValidateCamundaConnectionCredentialsSchema = z.discriminatedUnion("method", [ + z.object({ + method: z + .literal(CamundaConnectionMethod.ClientCredentials) + .describe(AppConnections.CREATE(AppConnection.Camunda).method), + credentials: CamundaConnectionClientCredentialsInputCredentialsSchema.describe( + AppConnections.CREATE(AppConnection.Camunda).credentials + ) + }) +]); + +export const CreateCamundaConnectionSchema = ValidateCamundaConnectionCredentialsSchema.and( + GenericCreateAppConnectionFieldsSchema(AppConnection.Camunda) +); + +export const UpdateCamundaConnectionSchema = z + .object({ + credentials: CamundaConnectionClientCredentialsInputCredentialsSchema.optional().describe( + AppConnections.UPDATE(AppConnection.Camunda).credentials + ) + }) + .and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Camunda)); + +export const CamundaConnectionListItemSchema = z.object({ + name: z.literal("Camunda"), + app: z.literal(AppConnection.Camunda), + methods: z.nativeEnum(CamundaConnectionMethod).array() +}); diff --git a/backend/src/services/app-connection/camunda/camunda-connection-service.ts b/backend/src/services/app-connection/camunda/camunda-connection-service.ts new file mode 100644 index 000000000..28b3882fa --- /dev/null +++ b/backend/src/services/app-connection/camunda/camunda-connection-service.ts @@ -0,0 +1,50 @@ +import { request } from "@app/lib/config/request"; +import { OrgServiceActor } from "@app/lib/types"; +import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { IntegrationUrls } from "@app/services/integration-auth/integration-list"; +import { TKmsServiceFactory } from "@app/services/kms/kms-service"; + +import { getCamundaConnectionAccessToken } from "./camunda-connection-fns"; +import { TCamundaConnection, TCamundaListClustersResponse } from "./camunda-connection-types"; + +type TGetAppConnectionFunc = ( + app: AppConnection, + connectionId: string, + actor: OrgServiceActor +) => Promise; + +const listCamundaClusters = async ( + appConnection: TCamundaConnection, + appConnectionDAL: Pick, + kmsService: Pick +) => { + const accessToken = await getCamundaConnectionAccessToken(appConnection, appConnectionDAL, kmsService); + + const { data } = await request.get(`${IntegrationUrls.CAMUNDA_API_URL}/clusters`, { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" + } + }); + + return data ?? []; +}; + +export const camundaConnectionService = ( + getAppConnection: TGetAppConnectionFunc, + appConnectionDAL: Pick, + kmsService: Pick +) => { + const listClusters = async (connectionId: string, actor: OrgServiceActor) => { + const appConnection = await getAppConnection(AppConnection.Camunda, connectionId, actor); + + const clusters = await listCamundaClusters(appConnection, appConnectionDAL, kmsService); + + return clusters; + }; + + return { + listClusters + }; +}; diff --git a/backend/src/services/app-connection/camunda/camunda-connection-types.ts b/backend/src/services/app-connection/camunda/camunda-connection-types.ts new file mode 100644 index 000000000..d59a8c7ce --- /dev/null +++ b/backend/src/services/app-connection/camunda/camunda-connection-types.ts @@ -0,0 +1,31 @@ +import { z } from "zod"; + +import { DiscriminativePick } from "@app/lib/types"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; + +import { + CamundaConnectionSchema, + CreateCamundaConnectionSchema, + ValidateCamundaConnectionCredentialsSchema +} from "./camunda-connection-schema"; + +export type TCamundaConnection = z.infer; + +export type TCamundaConnectionInput = z.infer & { + app: AppConnection.Camunda; +}; + +export type TValidateCamundaConnectionCredentialsSchema = typeof ValidateCamundaConnectionCredentialsSchema; + +export type TCamundaConnectionConfig = DiscriminativePick & { + orgId: string; +}; + +export type TAuthorizeCamundaConnection = { + access_token: string; + scope: string; + token_type: string; + expires_in: number; +}; + +export type TCamundaListClustersResponse = { uuid: string; name: string }[]; diff --git a/backend/src/services/app-connection/camunda/index.ts b/backend/src/services/app-connection/camunda/index.ts new file mode 100644 index 000000000..445871724 --- /dev/null +++ b/backend/src/services/app-connection/camunda/index.ts @@ -0,0 +1,4 @@ +export * from "./camunda-connection-enums"; +export * from "./camunda-connection-fns"; +export * from "./camunda-connection-schema"; +export * from "./camunda-connection-types"; diff --git a/backend/src/services/integration-auth/integration-list.ts b/backend/src/services/integration-auth/integration-list.ts index d6c450751..7bd33d86b 100644 --- a/backend/src/services/integration-auth/integration-list.ts +++ b/backend/src/services/integration-auth/integration-list.ts @@ -63,6 +63,7 @@ export enum IntegrationUrls { GITHUB_TOKEN_URL = "https://github.com/login/oauth/access_token", GITLAB_TOKEN_URL = "https://gitlab.com/oauth/token", BITBUCKET_TOKEN_URL = "https://bitbucket.org/site/oauth2/access_token", + CAMUNDA_TOKEN_URL = "https://login.cloud.camunda.io/oauth/token", // integration apps endpoints GCP_API_URL = "https://cloudresourcemanager.googleapis.com", @@ -94,6 +95,7 @@ export enum IntegrationUrls { 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", + CAMUNDA_API_URL = "https://api.cloud.camunda.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/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..3a52a4939 --- /dev/null +++ b/backend/src/services/secret-sync/camunda/camunda-sync-fns.ts @@ -0,0 +1,173 @@ +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; + + if (!value) { + // eslint-disable-next-line no-continue + continue; + } + + 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) || !secretMap[secret].value) { + try { + await deleteCamundaSecret({ + key: secret, + clusterUUID, + accessToken + }); + } catch (error) { + throw new SecretSyncError({ + error, + secretKey: secret + }); + } + } + } + }; + + 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 0d1ff6dec..70febfc0d 100644 --- a/backend/src/services/secret-sync/secret-sync-enums.ts +++ b/backend/src/services/secret-sync/secret-sync-enums.ts @@ -7,6 +7,7 @@ export enum SecretSync { AzureAppConfiguration = "azure-app-configuration", Databricks = "databricks", Humanitec = "humanitec", + Camunda = "camunda", Vercel = "vercel" } diff --git a/backend/src/services/secret-sync/secret-sync-fns.ts b/backend/src/services/secret-sync/secret-sync-fns.ts index 6a224ce31..ea22ebffc 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"; @@ -37,6 +38,7 @@ const SECRET_SYNC_LIST_OPTIONS: Record = { [SecretSync.AzureAppConfiguration]: AZURE_APP_CONFIGURATION_SYNC_LIST_OPTION, [SecretSync.Databricks]: DATABRICKS_SYNC_LIST_OPTION, [SecretSync.Humanitec]: HUMANITEC_SYNC_LIST_OPTION, + [SecretSync.Camunda]: CAMUNDA_SYNC_LIST_OPTION, [SecretSync.Vercel]: VERCEL_SYNC_LIST_OPTION }; @@ -123,6 +125,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); case SecretSync.Vercel: return VercelSyncFns.syncSecrets(secretSync, secretMap); default: @@ -169,6 +176,12 @@ export const SecretSyncFns = { case SecretSync.Humanitec: secretMap = await HumanitecSyncFns.getSecrets(secretSync); break; + case SecretSync.Camunda: + secretMap = await camundaSyncFactory({ + appConnectionDAL, + kmsService + }).getSecrets(secretSync); + break; case SecretSync.Vercel: secretMap = await VercelSyncFns.getSecrets(secretSync); break; @@ -214,6 +227,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); case SecretSync.Vercel: return VercelSyncFns.removeSecrets(secretSync, secretMap); default: diff --git a/backend/src/services/secret-sync/secret-sync-maps.ts b/backend/src/services/secret-sync/secret-sync-maps.ts index da39c330e..10e89ff03 100644 --- a/backend/src/services/secret-sync/secret-sync-maps.ts +++ b/backend/src/services/secret-sync/secret-sync-maps.ts @@ -10,6 +10,7 @@ export const SECRET_SYNC_NAME_MAP: Record = { [SecretSync.AzureAppConfiguration]: "Azure App Configuration", [SecretSync.Databricks]: "Databricks", [SecretSync.Humanitec]: "Humanitec", + [SecretSync.Camunda]: "Camunda", [SecretSync.Vercel]: "Vercel" }; @@ -22,5 +23,6 @@ export const SECRET_SYNC_CONNECTION_MAP: Record = { [SecretSync.AzureAppConfiguration]: AppConnection.AzureAppConfiguration, [SecretSync.Databricks]: AppConnection.Databricks, [SecretSync.Humanitec]: AppConnection.Humanitec, + [SecretSync.Camunda]: AppConnection.Camunda, [SecretSync.Vercel]: AppConnection.Vercel }; diff --git a/backend/src/services/secret-sync/secret-sync-types.ts b/backend/src/services/secret-sync/secret-sync-types.ts index d36f3ff05..091f91240 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, @@ -60,6 +66,7 @@ export type TSecretSync = | TAzureAppConfigurationSync | TDatabricksSync | THumanitecSync + | TCamundaSync | TVercelSync; export type TSecretSyncWithCredentials = @@ -71,6 +78,7 @@ export type TSecretSyncWithCredentials = | TAzureAppConfigurationSyncWithCredentials | TDatabricksSyncWithCredentials | THumanitecSyncWithCredentials + | TCamundaSyncWithCredentials | TVercelSyncWithCredentials; export type TSecretSyncInput = @@ -82,6 +90,7 @@ export type TSecretSyncInput = | TAzureAppConfigurationSyncInput | TDatabricksSyncInput | THumanitecSyncInput + | TCamundaSyncInput | TVercelSyncInput; export type TSecretSyncListItem = @@ -93,6 +102,7 @@ export type TSecretSyncListItem = | TAzureAppConfigurationSyncListItem | TDatabricksSyncListItem | THumanitecSyncListItem + | TCamundaSyncListItem | TVercelSyncListItem; export type TSyncOptionsConfig = { diff --git a/docs/api-reference/endpoints/app-connections/camunda/available.mdx b/docs/api-reference/endpoints/app-connections/camunda/available.mdx new file mode 100644 index 000000000..7df54478b --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/camunda/available.mdx @@ -0,0 +1,4 @@ +--- +title: "Available" +openapi: "GET /api/v1/app-connections/camunda/available" +--- diff --git a/docs/api-reference/endpoints/app-connections/camunda/create.mdx b/docs/api-reference/endpoints/app-connections/camunda/create.mdx new file mode 100644 index 000000000..54896202d --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/camunda/create.mdx @@ -0,0 +1,4 @@ +--- +title: "Create" +openapi: "POST /api/v1/app-connections/camunda" +--- diff --git a/docs/api-reference/endpoints/app-connections/camunda/delete.mdx b/docs/api-reference/endpoints/app-connections/camunda/delete.mdx new file mode 100644 index 000000000..a8e6be6b6 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/camunda/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/app-connections/camunda/{connectionId}" +--- diff --git a/docs/api-reference/endpoints/app-connections/camunda/get-by-id.mdx b/docs/api-reference/endpoints/app-connections/camunda/get-by-id.mdx new file mode 100644 index 000000000..a8ede9255 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/camunda/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by ID" +openapi: "GET /api/v1/app-connections/camunda/{connectionId}" +--- diff --git a/docs/api-reference/endpoints/app-connections/camunda/get-by-name.mdx b/docs/api-reference/endpoints/app-connections/camunda/get-by-name.mdx new file mode 100644 index 000000000..ae1cced4e --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/camunda/get-by-name.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by Name" +openapi: "GET /api/v1/app-connections/camunda/connection-name/{connectionName}" +--- diff --git a/docs/api-reference/endpoints/app-connections/camunda/list.mdx b/docs/api-reference/endpoints/app-connections/camunda/list.mdx new file mode 100644 index 000000000..e98e535dd --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/camunda/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v1/app-connections/camunda" +--- diff --git a/docs/api-reference/endpoints/app-connections/camunda/update.mdx b/docs/api-reference/endpoints/app-connections/camunda/update.mdx new file mode 100644 index 000000000..0db91e9d9 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/camunda/update.mdx @@ -0,0 +1,4 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/app-connections/camunda/{connectionId}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/camunda/create.mdx b/docs/api-reference/endpoints/secret-syncs/camunda/create.mdx new file mode 100644 index 000000000..d1a6b4354 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/camunda/create.mdx @@ -0,0 +1,4 @@ +--- +title: "Create" +openapi: "POST /api/v1/secret-syncs/camunda" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/camunda/delete.mdx b/docs/api-reference/endpoints/secret-syncs/camunda/delete.mdx new file mode 100644 index 000000000..8ac85b1e9 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/camunda/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/secret-syncs/camunda/{syncId}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/camunda/get-by-id.mdx b/docs/api-reference/endpoints/secret-syncs/camunda/get-by-id.mdx new file mode 100644 index 000000000..2579281fe --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/camunda/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by ID" +openapi: "GET /api/v1/secret-syncs/camunda/{syncId}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/camunda/get-by-name.mdx b/docs/api-reference/endpoints/secret-syncs/camunda/get-by-name.mdx new file mode 100644 index 000000000..876808543 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/camunda/get-by-name.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by Name" +openapi: "GET /api/v1/secret-syncs/camunda/sync-name/{syncName}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/camunda/list.mdx b/docs/api-reference/endpoints/secret-syncs/camunda/list.mdx new file mode 100644 index 000000000..54040e359 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/camunda/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v1/secret-syncs/camunda" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/camunda/remove-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/camunda/remove-secrets.mdx new file mode 100644 index 000000000..5757238f4 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/camunda/remove-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Remove Secrets" +openapi: "POST /api/v1/secret-syncs/camunda/{syncId}/remove-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/camunda/sync-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/camunda/sync-secrets.mdx new file mode 100644 index 000000000..24a28909a --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/camunda/sync-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Sync Secrets" +openapi: "POST /api/v1/secret-syncs/camunda/{syncId}/sync-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/camunda/update.mdx b/docs/api-reference/endpoints/secret-syncs/camunda/update.mdx new file mode 100644 index 000000000..bc10cb500 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/camunda/update.mdx @@ -0,0 +1,4 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/secret-syncs/camunda/{syncId}" +--- diff --git a/docs/images/app-connections/camunda/camunda-app-connection-created.png b/docs/images/app-connections/camunda/camunda-app-connection-created.png new file mode 100644 index 000000000..7af738e89 Binary files /dev/null and b/docs/images/app-connections/camunda/camunda-app-connection-created.png differ diff --git a/docs/images/app-connections/camunda/camunda-app-connection-form.png b/docs/images/app-connections/camunda/camunda-app-connection-form.png new file mode 100644 index 000000000..d8b26f0b5 Binary files /dev/null and b/docs/images/app-connections/camunda/camunda-app-connection-form.png differ diff --git a/docs/images/app-connections/camunda/camunda-app-connection-select.png b/docs/images/app-connections/camunda/camunda-app-connection-select.png new file mode 100644 index 000000000..33c45bd84 Binary files /dev/null and b/docs/images/app-connections/camunda/camunda-app-connection-select.png differ diff --git a/docs/images/app-connections/camunda/camunda-client-credentials.png b/docs/images/app-connections/camunda/camunda-client-credentials.png new file mode 100644 index 000000000..522f7f74c Binary files /dev/null and b/docs/images/app-connections/camunda/camunda-client-credentials.png differ diff --git a/docs/images/app-connections/camunda/camunda-console.png b/docs/images/app-connections/camunda/camunda-console.png new file mode 100644 index 000000000..ca96adbd2 Binary files /dev/null and b/docs/images/app-connections/camunda/camunda-console.png differ diff --git a/docs/images/app-connections/camunda/camunda-create-client-1.png b/docs/images/app-connections/camunda/camunda-create-client-1.png new file mode 100644 index 000000000..5ef589806 Binary files /dev/null and b/docs/images/app-connections/camunda/camunda-create-client-1.png differ diff --git a/docs/images/app-connections/camunda/camunda-create-client-2.png b/docs/images/app-connections/camunda/camunda-create-client-2.png new file mode 100644 index 000000000..9b8575c76 Binary files /dev/null and b/docs/images/app-connections/camunda/camunda-create-client-2.png differ diff --git a/docs/images/app-connections/camunda/camunda-organization-page.png b/docs/images/app-connections/camunda/camunda-organization-page.png new file mode 100644 index 000000000..fdc71a378 Binary files /dev/null and b/docs/images/app-connections/camunda/camunda-organization-page.png differ diff --git a/docs/images/secret-syncs/camunda/camunda-created.png b/docs/images/secret-syncs/camunda/camunda-created.png new file mode 100644 index 000000000..10778748d Binary files /dev/null and b/docs/images/secret-syncs/camunda/camunda-created.png differ diff --git a/docs/images/secret-syncs/camunda/camunda-destination.png b/docs/images/secret-syncs/camunda/camunda-destination.png new file mode 100644 index 000000000..4dffbe1e6 Binary files /dev/null and b/docs/images/secret-syncs/camunda/camunda-destination.png differ diff --git a/docs/images/secret-syncs/camunda/camunda-details.png b/docs/images/secret-syncs/camunda/camunda-details.png new file mode 100644 index 000000000..06402a67f Binary files /dev/null and b/docs/images/secret-syncs/camunda/camunda-details.png differ diff --git a/docs/images/secret-syncs/camunda/camunda-options.png b/docs/images/secret-syncs/camunda/camunda-options.png new file mode 100644 index 000000000..b38153833 Binary files /dev/null and b/docs/images/secret-syncs/camunda/camunda-options.png differ diff --git a/docs/images/secret-syncs/camunda/camunda-review.png b/docs/images/secret-syncs/camunda/camunda-review.png new file mode 100644 index 000000000..6283b38b0 Binary files /dev/null and b/docs/images/secret-syncs/camunda/camunda-review.png differ diff --git a/docs/images/secret-syncs/camunda/camunda-source.png b/docs/images/secret-syncs/camunda/camunda-source.png new file mode 100644 index 000000000..5b7332ebd Binary files /dev/null and b/docs/images/secret-syncs/camunda/camunda-source.png differ diff --git a/docs/images/secret-syncs/camunda/select-camunda-option.png b/docs/images/secret-syncs/camunda/select-camunda-option.png new file mode 100644 index 000000000..e9e138e90 Binary files /dev/null and b/docs/images/secret-syncs/camunda/select-camunda-option.png differ diff --git a/docs/integrations/app-connections/camunda.mdx b/docs/integrations/app-connections/camunda.mdx new file mode 100644 index 000000000..68084cea3 --- /dev/null +++ b/docs/integrations/app-connections/camunda.mdx @@ -0,0 +1,77 @@ +--- +title: "Camunda Connection" +description: "Learn how to configure a Camunda Connection for Infisical." +--- + +Infisical supports connecting to Camunda APIs using [client credentials](https://docs.camunda.io/docs/apis-tools/administration-api/authentication/#client-credentials-and-scopes). + +## Configure Client Credentials for Infisical + + + + In your Camunda Cloud Console, navigate to the **Organization** tab in the top navigation menu. + ![Organization Management](/images/app-connections/camunda/camunda-console.png) + + + From the Organization Management tabs, click on **Administration API** to manage your API credentials and click the **Create client credentials** button. + ![Create Client Credentials](/images/app-connections/camunda/camunda-organization-page.png) + + + Enter a recognizable name for your client, such as "my-infisical-client". The name can contain letters, dashes, underscores, and digits. + + + In the "Create new client credentials" modal, select the following permissions required for secret syncs: + + - **Cluster**: Enable read access (Get) + - **Connector secrets**: Enable all operations (Get, Create, Update, Delete) + + These specific permissions are required for Infisical to properly sync and manage your Camunda secrets. + ![Set Permissions](/images/app-connections/camunda/camunda-create-client-1.png) + ![Set Permissions 2](/images/app-connections/camunda/camunda-create-client-2.png) + + + Click the **Create** button to generate your client credentials. + + + After creation, you'll be shown your client credentials. For the Infisical connection, you'll need: + + - **Client ID** (`CAMUNDA_CONSOLE_CLIENT_ID`) + - **Client Secret** (`CAMUNDA_CONSOLE_CLIENT_SECRET`) + + **IMPORTANT**: Make sure to securely save the Client Secret, as it will not be shown again after you close this dialog. + + You can download these credentials or copy them to use in the next section. + ![Client Credentials](/images/app-connections/camunda/camunda-client-credentials.png) + + + + +## Setup Camunda Connection in Infisical + + + + Navigate to the **App Connections** tab on the **Organization Settings** + page. ![App Connections + Tab](/images/app-connections/general/add-connection.png) + + + Select the **Camunda Connection** option from the connection options modal. + ![Select Camunda + Connection](/images/app-connections/camunda/camunda-app-connection-select.png) + + + Select the **Client Credentials** method and enter the Camunda client + credentials you created: + + - **Client ID**: Your `CAMUNDA_CONSOLE_CLIENT_ID` value + - **Client Secret**: Your `CAMUNDA_CONSOLE_CLIENT_SECRET` value + + Infisical will automatically configure the connection using these credentials to access the Camunda API. Click **Connect to Camunda** to establish the connection. ![Connect to Camunda](/images/app-connections/camunda/camunda-app-connection-form.png) + + + + Your **Camunda Connection** is now available for use in your Infisical + projects. ![Camunda Connection + Created](/images/app-connections/camunda/camunda-app-connection-created.png) + + diff --git a/docs/integrations/secret-syncs/camunda.mdx b/docs/integrations/secret-syncs/camunda.mdx new file mode 100644 index 000000000..5ed2cd9ae --- /dev/null +++ b/docs/integrations/secret-syncs/camunda.mdx @@ -0,0 +1,139 @@ +--- +title: "Camunda Sync" +description: "Learn how to configure a Camunda Sync for Infisical." +--- + +**Prerequisites:** + + - Set up and add secrets to [Infisical Cloud](https://app.infisical.com) + - Create a [Camunda Connection](/integrations/app-connections/camunda) + + + + 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 **Camunda** option. + ![Select Camunda](/images/secret-syncs/camunda/select-camunda-option.png) + + 3. Configure the **Source** from where secrets should be retrieved, then click **Next**. + ![Configure Source](/images/secret-syncs/camunda/camunda-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/camunda/camunda-destination.png) + + - **Camunda Connection**: The Camunda Connection to authenticate with. + - **Cluster**: The Camunda cluster to sync connector secrets to. + + 5. Configure the **Sync Options** to specify how secrets should be synced, then click **Next**. + ![Configure Options](/images/secret-syncs/camunda/camunda-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. + - **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over Camunda when keys conflict. + - **Import Secrets (Prioritize Camunda)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Camunda over Infisical when keys conflict. + - **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only. + - **Disable Secret Deletion**: If enabled, Infisical will not remove secrets from the sync destination. Enable this option if you intend to manage some secrets manually outside of Infisical. + + 6. Configure the **Details** of your Camunda Sync, then click **Next**. + ![Configure Details](/images/secret-syncs/camunda/camunda-details.png) + + - **Name**: The name of your sync. Must be slug-friendly. + - **Description**: An optional description for your sync. + + 7. Review your Camunda Sync configuration, then click **Create Sync**. + ![Confirm Configuration](/images/secret-syncs/camunda/camunda-review.png) + + 8. If enabled, your Camunda Sync will begin syncing your secrets to the destination endpoint. + ![Sync Secrets](/images/secret-syncs/camunda/camunda-created.png) + + + + To create an **Camunda Sync**, make an API request to the [Create Camunda Sync](/api-reference/endpoints/secret-syncs/camunda/create) API endpoint. + + ### Sample request + + ```bash Request + curl --request POST \ + --url https://app.infisical.com/api/v1/secret-syncs/camunda \ + --header 'Content-Type: application/json' \ + --data '{ + "name": "my-camunda-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": "cluster", + "clusterUUID": "cc4c8dae-dce9-4f4c-9882-132b2bd65fa5" + } + }' + ``` + + ### Sample response + + ```bash Response + { + "secretSync": { + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "name": "my-camunda-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": "camunda", + "name": "my-camunda-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": "camunda", + "destinationConfig": { + "scope": "cluster", + "clusterUUID": "cc4c8dae-dce9-4f4c-9882-132b2bd65fa5" + } + } + } + ``` + + + diff --git a/docs/mint.json b/docs/mint.json index 7ec5f2a35..d0ad859b7 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -417,6 +417,7 @@ "integrations/app-connections/aws", "integrations/app-connections/azure-app-configuration", "integrations/app-connections/azure-key-vault", + "integrations/app-connections/camunda", "integrations/app-connections/databricks", "integrations/app-connections/gcp", "integrations/app-connections/github", @@ -439,6 +440,7 @@ "integrations/secret-syncs/aws-secrets-manager", "integrations/secret-syncs/azure-app-configuration", "integrations/secret-syncs/azure-key-vault", + "integrations/secret-syncs/camunda", "integrations/secret-syncs/databricks", "integrations/secret-syncs/gcp-secret-manager", "integrations/secret-syncs/github", @@ -914,6 +916,18 @@ "api-reference/endpoints/app-connections/azure-key-vault/delete" ] }, + { + "group": "Camunda", + "pages": [ + "api-reference/endpoints/app-connections/camunda/list", + "api-reference/endpoints/app-connections/camunda/available", + "api-reference/endpoints/app-connections/camunda/get-by-id", + "api-reference/endpoints/app-connections/camunda/get-by-name", + "api-reference/endpoints/app-connections/camunda/create", + "api-reference/endpoints/app-connections/camunda/update", + "api-reference/endpoints/app-connections/camunda/delete" + ] + }, { "group": "Databricks", "pages": [ @@ -1061,6 +1075,19 @@ "api-reference/endpoints/secret-syncs/azure-key-vault/remove-secrets" ] }, + { + "group": "Camunda", + "pages": [ + "api-reference/endpoints/secret-syncs/camunda/list", + "api-reference/endpoints/secret-syncs/camunda/get-by-id", + "api-reference/endpoints/secret-syncs/camunda/get-by-name", + "api-reference/endpoints/secret-syncs/camunda/create", + "api-reference/endpoints/secret-syncs/camunda/update", + "api-reference/endpoints/secret-syncs/camunda/delete", + "api-reference/endpoints/secret-syncs/camunda/sync-secrets", + "api-reference/endpoints/secret-syncs/camunda/remove-secrets" + ] + }, { "group": "Databricks", "pages": [ diff --git a/frontend/public/images/integrations/Camunda.png b/frontend/public/images/integrations/Camunda.png new file mode 100644 index 000000000..a3bb215b3 Binary files /dev/null and b/frontend/public/images/integrations/Camunda.png differ 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 b2f9f1c2f..d0a53019d 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"; @@ -35,6 +36,8 @@ export const SecretSyncDestinationFields = () => { return ; case SecretSync.Humanitec: return ; + case SecretSync.Camunda: + return ; case SecretSync.Vercel: return ; default: diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx index eb1b72b51..6c09c65cc 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: case SecretSync.Vercel: AdditionalSyncOptionsFieldsComponent = null; break; 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..483aa67fb --- /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} (id:{clusterUUID}) + + + ); +}; diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx index cdc651400..573854906 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"; @@ -73,6 +74,9 @@ export const SecretSyncReviewFields = () => { case SecretSync.Humanitec: DestinationFieldsComponent = ; break; + case SecretSync.Camunda: + DestinationFieldsComponent = ; + break; case SecretSync.Vercel: DestinationFieldsComponent = ; break; 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 fa723b5e0..e8f68de91 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"; import { VercelSyncDestinationSchema } from "./vercel-sync-destination-schema"; @@ -20,6 +21,7 @@ const SecretSyncUnionSchema = z.discriminatedUnion("destination", [ AzureAppConfigurationSyncDestinationSchema, DatabricksSyncDestinationSchema, HumanitecSyncDestinationSchema, + CamundaSyncDestinationSchema, VercelSyncDestinationSchema ]); diff --git a/frontend/src/helpers/appConnections.ts b/frontend/src/helpers/appConnections.ts index 286f2cb3f..cf87ae50e 100644 --- a/frontend/src/helpers/appConnections.ts +++ b/frontend/src/helpers/appConnections.ts @@ -6,6 +6,7 @@ import { AwsConnectionMethod, AzureAppConfigurationConnectionMethod, AzureKeyVaultConnectionMethod, + CamundaConnectionMethod, DatabricksConnectionMethod, GcpConnectionMethod, GitHubConnectionMethod, @@ -32,7 +33,8 @@ export const APP_CONNECTION_MAP: Record { @@ -51,6 +53,8 @@ export const getAppConnectionMethodDetails = (method: TAppConnection["method"]) return { name: "Service Account Impersonation", icon: faUser }; case DatabricksConnectionMethod.ServicePrincipal: return { name: "Service Principal", icon: faUser }; + case CamundaConnectionMethod.ClientCredentials: + return { name: "Client Credentials", icon: faKey }; case HumanitecConnectionMethod.ApiToken: case VercelConnectionMethod.ApiToken: return { name: "API Token", icon: faKey }; diff --git a/frontend/src/helpers/secretSyncs.ts b/frontend/src/helpers/secretSyncs.ts index 91dc06465..12521c4f8 100644 --- a/frontend/src/helpers/secretSyncs.ts +++ b/frontend/src/helpers/secretSyncs.ts @@ -24,6 +24,10 @@ export const SECRET_SYNC_MAP: Record = { [SecretSync.AzureAppConfiguration]: AppConnection.AzureAppConfiguration, [SecretSync.Databricks]: AppConnection.Databricks, [SecretSync.Humanitec]: AppConnection.Humanitec, + [SecretSync.Camunda]: AppConnection.Camunda, [SecretSync.Vercel]: AppConnection.Vercel }; 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/appConnections/enums.ts b/frontend/src/hooks/api/appConnections/enums.ts index 4a9a8be7a..ce8fea116 100644 --- a/frontend/src/hooks/api/appConnections/enums.ts +++ b/frontend/src/hooks/api/appConnections/enums.ts @@ -8,5 +8,6 @@ export enum AppConnection { Humanitec = "humanitec", Vercel = "vercel", Postgres = "postgres", - MsSql = "mssql" + MsSql = "mssql", + Camunda = "camunda" } diff --git a/frontend/src/hooks/api/appConnections/types/app-options.ts b/frontend/src/hooks/api/appConnections/types/app-options.ts index f389bf230..73d42c463 100644 --- a/frontend/src/hooks/api/appConnections/types/app-options.ts +++ b/frontend/src/hooks/api/appConnections/types/app-options.ts @@ -51,6 +51,10 @@ export type TMsSqlConnectionOption = TAppConnectionOptionBase & { app: AppConnection.MsSql; }; +export type TCamundaConnectionOption = TAppConnectionOptionBase & { + app: AppConnection.Camunda; +}; + export type TAppConnectionOption = | TAwsConnectionOption | TGitHubConnectionOption @@ -61,7 +65,8 @@ export type TAppConnectionOption = | THumanitecConnectionOption | TVercelConnectionOption | TPostgresConnectionOption - | TMsSqlConnectionOption; + | TMsSqlConnectionOption + | TCamundaConnectionOption; export type TAppConnectionOptionMap = { [AppConnection.AWS]: TAwsConnectionOption; @@ -74,4 +79,5 @@ export type TAppConnectionOptionMap = { [AppConnection.Vercel]: TVercelConnectionOption; [AppConnection.Postgres]: TPostgresConnectionOption; [AppConnection.MsSql]: TMsSqlConnectionOption; + [AppConnection.Camunda]: TCamundaConnectionOption; }; diff --git a/frontend/src/hooks/api/appConnections/types/camunda-connection.ts b/frontend/src/hooks/api/appConnections/types/camunda-connection.ts new file mode 100644 index 000000000..06b6f5b78 --- /dev/null +++ b/frontend/src/hooks/api/appConnections/types/camunda-connection.ts @@ -0,0 +1,14 @@ +import { AppConnection } from "../enums"; +import { TRootAppConnection } from "./root-connection"; + +export enum CamundaConnectionMethod { + ClientCredentials = "client-credentials" +} + +export type TCamundaConnection = TRootAppConnection & { app: AppConnection.Camunda } & { + method: CamundaConnectionMethod.ClientCredentials; + credentials: { + clientId: string; + clientSecret: string; + }; +}; diff --git a/frontend/src/hooks/api/appConnections/types/index.ts b/frontend/src/hooks/api/appConnections/types/index.ts index 15c2e9691..b8a529a18 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-options"; import { TAwsConnection } from "./aws-connection"; import { TAzureAppConfigurationConnection } from "./azure-app-configuration-connection"; import { TAzureKeyVaultConnection } from "./azure-key-vault-connection"; +import { TCamundaConnection } from "./camunda-connection"; import { TDatabricksConnection } from "./databricks-connection"; import { TGcpConnection } from "./gcp-connection"; import { TGitHubConnection } from "./github-connection"; @@ -14,6 +15,7 @@ import { TVercelConnection } from "./vercel-connection"; export * from "./aws-connection"; export * from "./azure-app-configuration-connection"; export * from "./azure-key-vault-connection"; +export * from "./camunda-connection"; export * from "./databricks-connection"; export * from "./gcp-connection"; export * from "./github-connection"; @@ -32,7 +34,8 @@ export type TAppConnection = | THumanitecConnection | TVercelConnection | TPostgresConnection - | TMsSqlConnection; + | TMsSqlConnection + | TCamundaConnection; export type TAvailableAppConnection = Pick; @@ -70,4 +73,5 @@ export type TAppConnectionMap = { [AppConnection.Vercel]: TVercelConnection; [AppConnection.Postgres]: TPostgresConnection; [AppConnection.MsSql]: TMsSqlConnection; + [AppConnection.Camunda]: TCamundaConnection; }; diff --git a/frontend/src/hooks/api/secretSyncs/enums.ts b/frontend/src/hooks/api/secretSyncs/enums.ts index 6f1fcac88..9fa5f6c99 100644 --- a/frontend/src/hooks/api/secretSyncs/enums.ts +++ b/frontend/src/hooks/api/secretSyncs/enums.ts @@ -7,6 +7,7 @@ export enum SecretSync { AzureAppConfiguration = "azure-app-configuration", Databricks = "databricks", Humanitec = "humanitec", + Camunda = "camunda", Vercel = "vercel" } 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..7332a0a4a --- /dev/null +++ b/frontend/src/hooks/api/secretSyncs/types/camunda-sync.ts @@ -0,0 +1,21 @@ +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; + clusterName?: 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 01edd8193..207ab0118 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"; import { TVercelSync } from "./vercel-sync"; @@ -26,6 +27,7 @@ export type TSecretSync = | TAzureAppConfigurationSync | TDatabricksSync | THumanitecSync + | TCamundaSync | TVercelSync; 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 b4b615938..59bdd581f 100644 --- a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx @@ -12,6 +12,7 @@ import { AppConnectionHeader } from "../AppConnectionHeader"; import { AwsConnectionForm } from "./AwsConnectionForm"; import { AzureAppConfigurationConnectionForm } from "./AzureAppConfigurationConnectionForm"; import { AzureKeyVaultConnectionForm } from "./AzureKeyVaultConnectionForm"; +import { CamundaConnectionForm } from "./CamundaConnectionForm"; import { DatabricksConnectionForm } from "./DatabricksConnectionForm"; import { GcpConnectionForm } from "./GcpConnectionForm"; import { GitHubConnectionForm } from "./GitHubConnectionForm"; @@ -77,6 +78,8 @@ const CreateForm = ({ app, onComplete }: CreateFormProps) => { return ; case AppConnection.MsSql: return ; + case AppConnection.Camunda: + return ; default: throw new Error(`Unhandled App ${app}`); } @@ -133,6 +136,8 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => { return ; case AppConnection.MsSql: return ; + case AppConnection.Camunda: + return ; default: throw new Error(`Unhandled App ${(appConnection as TAppConnection).app}`); } diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/CamundaConnectionForm.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/CamundaConnectionForm.tsx new file mode 100644 index 000000000..d8b2ac4b6 --- /dev/null +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/CamundaConnectionForm.tsx @@ -0,0 +1,148 @@ +import { Controller, FormProvider, useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + +import { + Button, + FormControl, + Input, + ModalClose, + SecretInput, + Select, + SelectItem +} from "@app/components/v2"; +import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections"; +import { CamundaConnectionMethod, TCamundaConnection } from "@app/hooks/api/appConnections"; +import { AppConnection } from "@app/hooks/api/appConnections/enums"; + +import { + genericAppConnectionFieldsSchema, + GenericAppConnectionsFields +} from "./GenericAppConnectionFields"; + +type Props = { + appConnection?: TCamundaConnection; + onSubmit: (formData: FormData) => Promise; +}; + +const rootSchema = genericAppConnectionFieldsSchema.extend({ + app: z.literal(AppConnection.Camunda) +}); + +const formSchema = z.discriminatedUnion("method", [ + rootSchema.extend({ + method: z.literal(CamundaConnectionMethod.ClientCredentials), + credentials: z.object({ + clientId: z.string().trim().min(1, "Client ID required"), + clientSecret: z.string().trim().min(1, "Client secret required") + }) + }) +]); + +type FormData = z.infer; + +export const CamundaConnectionForm = ({ appConnection, onSubmit }: Props) => { + const isUpdate = Boolean(appConnection); + + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: appConnection ?? { + app: AppConnection.Camunda, + method: CamundaConnectionMethod.ClientCredentials + } + }); + + 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/CamundaSyncDestinationCol.tsx b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/CamundaSyncDestinationCol.tsx new file mode 100644 index 000000000..7cd62f3b4 --- /dev/null +++ b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/CamundaSyncDestinationCol.tsx @@ -0,0 +1,29 @@ +import { useCamundaConnectionListClusters } from "@app/hooks/api/appConnections/camunda"; +import { TCamundaSync } from "@app/hooks/api/secretSyncs/types/camunda-sync"; + +import { getSecretSyncDestinationColValues } from "../helpers"; +import { SecretSyncTableCell } from "../SecretSyncTableCell"; + +type Props = { + secretSync: TCamundaSync; +}; + +export const CamundaSyncDestinationCol = ({ secretSync }: Props) => { + const { data: clusters, isPending } = useCamundaConnectionListClusters(secretSync.connectionId); + + const { primaryText, secondaryText } = getSecretSyncDestinationColValues({ + ...secretSync, + destinationConfig: { + ...secretSync.destinationConfig, + clusterName: clusters?.find( + (cluster) => cluster.uuid === secretSync.destinationConfig.clusterUUID + )?.name + } + }); + + if (isPending) { + return ; + } + + 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 7ccbbc545..b8965b140 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 { CamundaSyncDestinationCol } from "./CamundaSyncDestinationCol"; import { DatabricksSyncDestinationCol } from "./DatabricksSyncDestinationCol"; import { GcpSyncDestinationCol } from "./GcpSyncDestinationCol"; import { GitHubSyncDestinationCol } from "./GitHubSyncDestinationCol"; @@ -32,6 +33,8 @@ export const SecretSyncDestinationCol = ({ secretSync }: Props) => { return ; case SecretSync.Humanitec: return ; + case SecretSync.Camunda: + return ; case SecretSync.Vercel: return ; default: 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 941196f60..1b457dc2b 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.clusterName ?? destinationConfig.clusterUUID; + secondaryText = "Cluster"; + break; case SecretSync.Vercel: primaryText = destinationConfig.appName || destinationConfig.app; secondaryText = destinationConfig.env; 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..078fe58a4 --- /dev/null +++ b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/CamundaSyncDestinationSection.tsx @@ -0,0 +1,21 @@ +import { GenericFieldLabel } from "@app/components/secret-syncs"; +import { useCamundaConnectionListClusters } from "@app/hooks/api/appConnections/camunda"; +import { TCamundaSync } from "@app/hooks/api/secretSyncs/types/camunda-sync"; + +type Props = { + secretSync: TCamundaSync; +}; + +export const CamundaSyncDestinationSection = ({ secretSync }: Props) => { + const { data: clusters, isPending } = useCamundaConnectionListClusters(secretSync.connectionId); + const { + destinationConfig: { clusterUUID } + } = secretSync; + + if (isPending) { + return Loading...; + } + + const clusterName = clusters?.find((cluster) => cluster.uuid === clusterUUID)?.name; + return {clusterName ?? 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 202d78cea..9b8ff36a7 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"; import { VercelSyncDestinationSection } from "./VercelSyncDestinationSection"; @@ -58,6 +59,9 @@ export const SecretSyncDestinationSection = ({ secretSync, onEditDestination }: case SecretSync.Humanitec: DestinationComponents = ; break; + case SecretSync.Camunda: + DestinationComponents = ; + break; case SecretSync.Vercel: DestinationComponents = ; break; 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 d1f330f4f..eb3dad61a 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: case SecretSync.Vercel: AdditionalSyncOptionsComponent = null; break;