Address PR comment for terraform sync integration

This commit is contained in:
carlosmonastyrski
2025-04-11 16:23:07 -03:00
parent e19c3630d9
commit 4b0e5fa05b
91 changed files with 1463 additions and 40 deletions

View File

@@ -1711,6 +1711,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."
}
}
};
@@ -1821,6 +1825,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.",

View File

@@ -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
@@ -46,7 +50,8 @@ const SanitizedAppConnectionSchema = z.union([
...SanitizedTerraformCloudConnectionSchema.options,
...SanitizedVercelConnectionSchema.options,
...SanitizedPostgresConnectionSchema.options,
...SanitizedMsSqlConnectionSchema.options
...SanitizedMsSqlConnectionSchema.options,
...SanitizedCamundaConnectionSchema.options
]);
const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
@@ -60,7 +65,8 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
TerraformCloudConnectionListItemSchema,
VercelConnectionListItemSchema,
PostgresConnectionListItemSchema,
MsSqlConnectionListItemSchema
MsSqlConnectionListItemSchema,
CamundaConnectionListItemSchema
]);
export const registerAppConnectionRouter = async (server: FastifyZodProvider) => {

View File

@@ -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 };
}
});
};

View File

@@ -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";
@@ -26,5 +27,6 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record<AppConnection, (server:
[AppConnection.TerraformCloud]: registerTerraformCloudConnectionRouter,
[AppConnection.Vercel]: registerVercelConnectionRouter,
[AppConnection.Postgres]: registerPostgresConnectionRouter,
[AppConnection.MsSql]: registerMsSqlConnectionRouter
[AppConnection.MsSql]: registerMsSqlConnectionRouter,
[AppConnection.Camunda]: registerCamundaConnectionRouter
};

View File

@@ -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
});

View File

@@ -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";
@@ -23,5 +24,6 @@ export const SECRET_SYNC_REGISTER_ROUTER_MAP: Record<SecretSync, (server: Fastif
[SecretSync.Databricks]: registerDatabricksSyncRouter,
[SecretSync.Humanitec]: registerHumanitecSyncRouter,
[SecretSync.TerraformCloud]: registerTerraformCloudSyncRouter,
[SecretSync.Camunda]: registerCamundaSyncRouter,
[SecretSync.Vercel]: registerVercelSyncRouter
};

View File

@@ -18,6 +18,7 @@ import {
AzureAppConfigurationSyncSchema
} from "@app/services/secret-sync/azure-app-configuration";
import { AzureKeyVaultSyncListItemSchema, AzureKeyVaultSyncSchema } from "@app/services/secret-sync/azure-key-vault";
import { CamundaSyncListItemSchema, CamundaSyncSchema } from "@app/services/secret-sync/camunda";
import { DatabricksSyncListItemSchema, DatabricksSyncSchema } from "@app/services/secret-sync/databricks";
import { GcpSyncListItemSchema, GcpSyncSchema } from "@app/services/secret-sync/gcp";
import { GitHubSyncListItemSchema, GitHubSyncSchema } from "@app/services/secret-sync/github";
@@ -35,6 +36,7 @@ const SecretSyncSchema = z.discriminatedUnion("destination", [
DatabricksSyncSchema,
HumanitecSyncSchema,
TerraformCloudSyncSchema,
CamundaSyncSchema,
VercelSyncSchema
]);
@@ -48,6 +50,7 @@ const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [
DatabricksSyncListItemSchema,
HumanitecSyncListItemSchema,
TerraformCloudSyncListItemSchema,
CamundaSyncListItemSchema,
VercelSyncListItemSchema
]);

View File

@@ -9,7 +9,8 @@ export enum AppConnection {
TerraformCloud = "terraform-cloud",
Vercel = "vercel",
Postgres = "postgres",
MsSql = "mssql"
MsSql = "mssql",
Camunda = "camunda"
}
export enum AWSRegion {

View File

@@ -27,6 +27,7 @@ import {
getAzureKeyVaultConnectionListItem,
validateAzureKeyVaultConnectionCredentials
} from "./azure-key-vault";
import { CamundaConnectionMethod, getCamundaConnectionListItem, validateCamundaConnectionCredentials } from "./camunda";
import {
DatabricksConnectionMethod,
getDatabricksConnectionListItem,
@@ -61,7 +62,8 @@ export const listAppConnectionOptions = () => {
getTerraformCloudConnectionListItem(),
getVercelConnectionListItem(),
getPostgresConnectionListItem(),
getMsSqlConnectionListItem()
getMsSqlConnectionListItem(),
getCamundaConnectionListItem()
].sort((a, b) => a.name.localeCompare(b.name));
};
@@ -119,6 +121,7 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TAppConnect
[AppConnection.Postgres]: validateSqlConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.MsSql]: validateSqlConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.TerraformCloud]: validateTerraformCloudConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Camunda]: validateCamundaConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Vercel]: validateVercelConnectionCredentials as TAppConnectionCredentialsValidator
};
@@ -142,6 +145,8 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) =>
return "Service Account Impersonation";
case DatabricksConnectionMethod.ServicePrincipal:
return "Service Principal";
case CamundaConnectionMethod.ClientCredentials:
return "Client Credentials";
case HumanitecConnectionMethod.ApiToken:
case TerraformCloudConnectionMethod.ApiToken:
case VercelConnectionMethod.ApiToken:
@@ -190,5 +195,6 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record<
[AppConnection.Postgres]: transferSqlConnectionCredentialsToPlatform as TAppConnectionTransitionCredentialsToPlatform,
[AppConnection.MsSql]: transferSqlConnectionCredentialsToPlatform as TAppConnectionTransitionCredentialsToPlatform,
[AppConnection.TerraformCloud]: platformManagedCredentialsNotSupported,
[AppConnection.Camunda]: platformManagedCredentialsNotSupported,
[AppConnection.Vercel]: platformManagedCredentialsNotSupported
};

View File

@@ -11,5 +11,6 @@ export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
[AppConnection.TerraformCloud]: "Terraform Cloud",
[AppConnection.Vercel]: "Vercel",
[AppConnection.Postgres]: "PostgreSQL",
[AppConnection.MsSql]: "Microsoft SQL Server"
[AppConnection.MsSql]: "Microsoft SQL Server",
[AppConnection.Camunda]: "Camunda"
};

View File

@@ -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";
@@ -65,7 +67,8 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAp
[AppConnection.TerraformCloud]: ValidateTerraformCloudConnectionCredentialsSchema,
[AppConnection.Vercel]: ValidateVercelConnectionCredentialsSchema,
[AppConnection.Postgres]: ValidatePostgresConnectionCredentialsSchema,
[AppConnection.MsSql]: ValidateMsSqlConnectionCredentialsSchema
[AppConnection.MsSql]: ValidateMsSqlConnectionCredentialsSchema,
[AppConnection.Camunda]: ValidateCamundaConnectionCredentialsSchema
};
export const appConnectionServiceFactory = ({
@@ -438,6 +441,7 @@ export const appConnectionServiceFactory = ({
aws: awsConnectionService(connectAppConnectionById),
humanitec: humanitecConnectionService(connectAppConnectionById),
terraformCloud: terraformCloudConnectionService(connectAppConnectionById),
camunda: camundaConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
vercel: vercelConnectionService(connectAppConnectionById)
};
};

View File

@@ -21,6 +21,12 @@ import {
TAzureKeyVaultConnectionInput,
TValidateAzureKeyVaultConnectionCredentialsSchema
} from "./azure-key-vault";
import {
TCamundaConnection,
TCamundaConnectionConfig,
TCamundaConnectionInput,
TValidateCamundaConnectionCredentialsSchema
} from "./camunda";
import {
TDatabricksConnection,
TDatabricksConnectionConfig,
@@ -76,6 +82,7 @@ export type TAppConnection = { id: string } & (
| TVercelConnection
| TPostgresConnection
| TMsSqlConnection
| TCamundaConnection
);
export type TAppConnectionRaw = NonNullable<Awaited<ReturnType<TAppConnectionDALFactory["findById"]>>>;
@@ -94,6 +101,7 @@ export type TAppConnectionInput = { id: string } & (
| TVercelConnectionInput
| TPostgresConnectionInput
| TMsSqlConnectionInput
| TCamundaConnectionInput
);
export type TSqlConnectionInput = TPostgresConnectionInput | TMsSqlConnectionInput;
@@ -117,7 +125,8 @@ export type TAppConnectionConfig =
| THumanitecConnectionConfig
| TTerraformCloudConnectionConfig
| TVercelConnectionConfig
| TSqlConnectionConfig;
| TSqlConnectionConfig
| TCamundaConnectionConfig;
export type TValidateAppConnectionCredentialsSchema =
| TValidateAwsConnectionCredentialsSchema
@@ -130,6 +139,7 @@ export type TValidateAppConnectionCredentialsSchema =
| TValidatePostgresConnectionCredentialsSchema
| TValidateMsSqlConnectionCredentialsSchema
| TValidateTerraformCloudConnectionCredentialsSchema
| TValidateCamundaConnectionCredentialsSchema
| TValidateVercelConnectionCredentialsSchema;
export type TListAwsConnectionKmsKeys = {

View File

@@ -0,0 +1,3 @@
export enum CamundaConnectionMethod {
ClientCredentials = "client-credentials"
}

View File

@@ -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<TCamundaConnection["credentials"], "clientId" | "clientSecret">) => {
const { data } = await request.post<TAuthorizeCamundaConnection>(
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<TAppConnectionDALFactory, "updateById">,
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
) => {
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`
});
}
};

View File

@@ -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()
});

View File

@@ -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<TCamundaConnection>;
const listCamundaClusters = async (
appConnection: TCamundaConnection,
appConnectionDAL: Pick<TAppConnectionDALFactory, "updateById">,
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
) => {
const accessToken = await getCamundaConnectionAccessToken(appConnection, appConnectionDAL, kmsService);
const { data } = await request.get<TCamundaListClustersResponse>(`${IntegrationUrls.CAMUNDA_API_URL}/clusters`, {
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json"
}
});
return data ?? [];
};
export const camundaConnectionService = (
getAppConnection: TGetAppConnectionFunc,
appConnectionDAL: Pick<TAppConnectionDALFactory, "updateById">,
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
) => {
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
};
};

View File

@@ -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<typeof CamundaConnectionSchema>;
export type TCamundaConnectionInput = z.infer<typeof CreateCamundaConnectionSchema> & {
app: AppConnection.Camunda;
};
export type TValidateCamundaConnectionCredentialsSchema = typeof ValidateCamundaConnectionCredentialsSchema;
export type TCamundaConnectionConfig = DiscriminativePick<TCamundaConnectionInput, "method" | "app" | "credentials"> & {
orgId: string;
};
export type TAuthorizeCamundaConnection = {
access_token: string;
scope: string;
token_type: string;
expires_in: number;
};
export type TCamundaListClustersResponse = { uuid: string; name: string }[];

View File

@@ -0,0 +1,4 @@
export * from "./camunda-connection-enums";
export * from "./camunda-connection-fns";
export * from "./camunda-connection-schema";
export * from "./camunda-connection-types";

View File

@@ -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}`,

View File

@@ -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
};

View File

@@ -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<TAppConnectionDALFactory, "updateById">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
};
const getCamundaSecrets = async ({ accessToken, clusterUUID }: TCamundaListSecrets) => {
const { data } = await request.get<TCamundaListSecretsResponse>(
`${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
};
};

View File

@@ -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)
});

View File

@@ -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<typeof CamundaSyncSchema>;
export type TCamundaSyncInput = z.infer<typeof CreateCamundaSyncSchema>;
export type TCamundaSyncListItem = z.infer<typeof CamundaSyncListItemSchema>;
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;

View File

@@ -0,0 +1,4 @@
export * from "./camunda-sync-constants";
export * from "./camunda-sync-fns";
export * from "./camunda-sync-schemas";
export * from "./camunda-sync-types";

View File

@@ -8,6 +8,7 @@ export enum SecretSync {
Databricks = "databricks",
Humanitec = "humanitec",
TerraformCloud = "terraform-cloud",
Camunda = "camunda",
Vercel = "vercel"
}

View File

@@ -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";
@@ -39,6 +40,7 @@ const SECRET_SYNC_LIST_OPTIONS: Record<SecretSync, TSecretSyncListItem> = {
[SecretSync.Databricks]: DATABRICKS_SYNC_LIST_OPTION,
[SecretSync.Humanitec]: HUMANITEC_SYNC_LIST_OPTION,
[SecretSync.TerraformCloud]: TERRAFORM_CLOUD_SYNC_LIST_OPTION,
[SecretSync.Camunda]: CAMUNDA_SYNC_LIST_OPTION,
[SecretSync.Vercel]: VERCEL_SYNC_LIST_OPTION
};
@@ -127,6 +129,11 @@ export const SecretSyncFns = {
return HumanitecSyncFns.syncSecrets(secretSync, secretMap);
case SecretSync.TerraformCloud:
return TerraformCloudSyncFns.syncSecrets(secretSync, secretMap);
case SecretSync.Camunda:
return camundaSyncFactory({
appConnectionDAL,
kmsService
}).syncSecrets(secretSync, secretMap);
case SecretSync.Vercel:
return VercelSyncFns.syncSecrets(secretSync, secretMap);
default:
@@ -176,6 +183,12 @@ export const SecretSyncFns = {
case SecretSync.TerraformCloud:
secretMap = await TerraformCloudSyncFns.getSecrets(secretSync);
break;
case SecretSync.Camunda:
secretMap = await camundaSyncFactory({
appConnectionDAL,
kmsService
}).getSecrets(secretSync);
break;
case SecretSync.Vercel:
secretMap = await VercelSyncFns.getSecrets(secretSync);
break;
@@ -223,6 +236,11 @@ export const SecretSyncFns = {
return HumanitecSyncFns.removeSecrets(secretSync, secretMap);
case SecretSync.TerraformCloud:
return TerraformCloudSyncFns.removeSecrets(secretSync, secretMap);
case SecretSync.Camunda:
return camundaSyncFactory({
appConnectionDAL,
kmsService
}).removeSecrets(secretSync, secretMap);
case SecretSync.Vercel:
return VercelSyncFns.removeSecrets(secretSync, secretMap);
default:

View File

@@ -11,6 +11,7 @@ export const SECRET_SYNC_NAME_MAP: Record<SecretSync, string> = {
[SecretSync.Databricks]: "Databricks",
[SecretSync.Humanitec]: "Humanitec",
[SecretSync.TerraformCloud]: "Terraform Cloud",
[SecretSync.Camunda]: "Camunda",
[SecretSync.Vercel]: "Vercel"
};
@@ -24,5 +25,6 @@ export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = {
[SecretSync.Databricks]: AppConnection.Databricks,
[SecretSync.Humanitec]: AppConnection.Humanitec,
[SecretSync.TerraformCloud]: AppConnection.TerraformCloud,
[SecretSync.Camunda]: AppConnection.Camunda,
[SecretSync.Vercel]: AppConnection.Vercel
};

View File

@@ -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,
@@ -67,6 +73,7 @@ export type TSecretSync =
| TDatabricksSync
| THumanitecSync
| TTerraformCloudSync
| TCamundaSync
| TVercelSync;
export type TSecretSyncWithCredentials =
@@ -79,6 +86,7 @@ export type TSecretSyncWithCredentials =
| TDatabricksSyncWithCredentials
| THumanitecSyncWithCredentials
| TTerraformCloudSyncWithCredentials
| TCamundaSyncWithCredentials
| TVercelSyncWithCredentials;
export type TSecretSyncInput =
@@ -91,6 +99,7 @@ export type TSecretSyncInput =
| TDatabricksSyncInput
| THumanitecSyncInput
| TTerraformCloudSyncInput
| TCamundaSyncInput
| TVercelSyncInput;
export type TSecretSyncListItem =
@@ -103,6 +112,7 @@ export type TSecretSyncListItem =
| TDatabricksSyncListItem
| THumanitecSyncListItem
| TTerraformCloudSyncListItem
| TCamundaSyncListItem
| TVercelSyncListItem;
export type TSyncOptionsConfig = {

View File

@@ -0,0 +1,4 @@
---
title: "Available"
openapi: "GET /api/v1/app-connections/camunda/available"
---

View File

@@ -0,0 +1,4 @@
---
title: "Create"
openapi: "POST /api/v1/app-connections/camunda"
---

View File

@@ -0,0 +1,4 @@
---
title: "Delete"
openapi: "DELETE /api/v1/app-connections/camunda/{connectionId}"
---

View File

@@ -0,0 +1,4 @@
---
title: "Get by ID"
openapi: "GET /api/v1/app-connections/camunda/{connectionId}"
---

View File

@@ -0,0 +1,4 @@
---
title: "Get by Name"
openapi: "GET /api/v1/app-connections/camunda/connection-name/{connectionName}"
---

View File

@@ -0,0 +1,4 @@
---
title: "List"
openapi: "GET /api/v1/app-connections/camunda"
---

View File

@@ -0,0 +1,4 @@
---
title: "Update"
openapi: "PATCH /api/v1/app-connections/camunda/{connectionId}"
---

View File

@@ -0,0 +1,4 @@
---
title: "Create"
openapi: "POST /api/v1/secret-syncs/camunda"
---

View File

@@ -0,0 +1,4 @@
---
title: "Delete"
openapi: "DELETE /api/v1/secret-syncs/camunda/{syncId}"
---

View File

@@ -0,0 +1,4 @@
---
title: "Get by ID"
openapi: "GET /api/v1/secret-syncs/camunda/{syncId}"
---

View File

@@ -0,0 +1,4 @@
---
title: "Get by Name"
openapi: "GET /api/v1/secret-syncs/camunda/sync-name/{syncName}"
---

View File

@@ -0,0 +1,4 @@
---
title: "List"
openapi: "GET /api/v1/secret-syncs/camunda"
---

View File

@@ -0,0 +1,4 @@
---
title: "Remove Secrets"
openapi: "POST /api/v1/secret-syncs/camunda/{syncId}/remove-secrets"
---

View File

@@ -0,0 +1,4 @@
---
title: "Sync Secrets"
openapi: "POST /api/v1/secret-syncs/camunda/{syncId}/sync-secrets"
---

View File

@@ -0,0 +1,4 @@
---
title: "Update"
openapi: "PATCH /api/v1/secret-syncs/camunda/{syncId}"
---

View File

@@ -1,4 +1,4 @@
---
title: "Create"
openapi: "POST /api/v1/secret-syncs/humanitec"
openapi: "POST /api/v1/secret-syncs/terraform-cloud"
---

Binary file not shown.

After

Width:  |  Height:  |  Size: 964 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 532 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 656 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 304 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 268 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 303 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 288 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 225 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 626 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 616 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 667 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 656 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 615 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 640 KiB

View File

@@ -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
<Steps>
<Step title="Navigate to Organization Management">
In your Camunda Cloud Console, navigate to the **Organization** tab in the top navigation menu.
![Organization Management](/images/app-connections/camunda/camunda-console.png)
</Step>
<Step title="Access Administration API">
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)
</Step>
<Step title="Name Your Client">
Enter a recognizable name for your client, such as "my-infisical-client". The name can contain letters, dashes, underscores, and digits.
</Step>
<Step title="Set Credential Permissions">
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)
</Step>
<Step title="Complete Creation">
Click the **Create** button to generate your client credentials.
</Step>
<Step title="Save Your 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)
</Step>
</Steps>
## Setup Camunda Connection in Infisical
<Steps>
<Step title="Navigate to App Connections">
Navigate to the **App Connections** tab on the **Organization Settings**
page. ![App Connections
Tab](/images/app-connections/general/add-connection.png)
</Step>
<Step title="Add Connection">
Select the **Camunda Connection** option from the connection options modal.
![Select Camunda
Connection](/images/app-connections/camunda/camunda-app-connection-select.png)
</Step>
<Step title="Input Credentials">
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)
</Step>
<Step title="Connection Created">
Your **Camunda Connection** is now available for use in your Infisical
projects. ![Camunda Connection
Created](/images/app-connections/camunda/camunda-app-connection-created.png)
</Step>
</Steps>

View File

@@ -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)
<Tabs>
<Tab title="Infisical UI">
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.
<Tip>
If you need to sync secrets from multiple folder locations, check out [secret imports](/documentation/platform/secret-reference#secret-imports).
</Tip>
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)
</Tab>
<Tab title="API">
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"
}
}
}
```
</Tab>
</Tabs>

View File

@@ -417,14 +417,15 @@
"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",
"integrations/app-connections/humanitec",
"integrations/app-connections/terraform-cloud",
"integrations/app-connections/vercel",
"integrations/app-connections/mssql",
"integrations/app-connections/postgres"
"integrations/app-connections/postgres",
"integrations/app-connections/terraform-cloud",
"integrations/app-connections/vercel"
]
}
]
@@ -440,6 +441,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",
@@ -916,6 +918,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": [
@@ -964,30 +978,6 @@
"api-reference/endpoints/app-connections/humanitec/delete"
]
},
{
"group": "Terraform Cloud",
"pages": [
"api-reference/endpoints/app-connections/terraform-cloud/list",
"api-reference/endpoints/app-connections/terraform-cloud/available",
"api-reference/endpoints/app-connections/terraform-cloud/get-by-id",
"api-reference/endpoints/app-connections/terraform-cloud/get-by-name",
"api-reference/endpoints/app-connections/terraform-cloud/create",
"api-reference/endpoints/app-connections/terraform-cloud/update",
"api-reference/endpoints/app-connections/terraform-cloud/delete"
]
},
{
"group": "Vercel",
"pages": [
"api-reference/endpoints/app-connections/vercel/list",
"api-reference/endpoints/app-connections/vercel/available",
"api-reference/endpoints/app-connections/vercel/get-by-id",
"api-reference/endpoints/app-connections/vercel/get-by-name",
"api-reference/endpoints/app-connections/vercel/create",
"api-reference/endpoints/app-connections/vercel/update",
"api-reference/endpoints/app-connections/vercel/delete"
]
},
{
"group": "Microsoft SQL Server",
"pages": [
@@ -1011,6 +1001,30 @@
"api-reference/endpoints/app-connections/postgres/update",
"api-reference/endpoints/app-connections/postgres/delete"
]
},
{
"group": "Terraform Cloud",
"pages": [
"api-reference/endpoints/app-connections/terraform-cloud/list",
"api-reference/endpoints/app-connections/terraform-cloud/available",
"api-reference/endpoints/app-connections/terraform-cloud/get-by-id",
"api-reference/endpoints/app-connections/terraform-cloud/get-by-name",
"api-reference/endpoints/app-connections/terraform-cloud/create",
"api-reference/endpoints/app-connections/terraform-cloud/update",
"api-reference/endpoints/app-connections/terraform-cloud/delete"
]
},
{
"group": "Vercel",
"pages": [
"api-reference/endpoints/app-connections/vercel/list",
"api-reference/endpoints/app-connections/vercel/available",
"api-reference/endpoints/app-connections/vercel/get-by-id",
"api-reference/endpoints/app-connections/vercel/get-by-name",
"api-reference/endpoints/app-connections/vercel/create",
"api-reference/endpoints/app-connections/vercel/update",
"api-reference/endpoints/app-connections/vercel/delete"
]
}
]
},
@@ -1075,6 +1089,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": [

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -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 (
<>
<SecretSyncConnectionField
onChange={() => {
setValue("destinationConfig.clusterUUID", "");
}}
/>
<Controller
name="destinationConfig.clusterUUID"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
isError={Boolean(error)}
errorText={error?.message}
label="Cluster"
helperText={
<Tooltip
className="max-w-md"
content="Ensure that your credential has been granted access to the cluster"
>
<div>
<span>Don&#39;t see the cluster you&#39;re looking for?</span>{" "}
<FontAwesomeIcon icon={faCircleInfo} className="text-mineshaft-400" />
</div>
</Tooltip>
}
>
<FilterableSelect
menuPlacement="top"
isLoading={isPending && Boolean(connectionId)}
isDisabled={!connectionId}
value={clusters?.find((cluster) => cluster.uuid === value) ?? null}
onChange={(option) => {
onChange((option as SingleValue<TCamundaCluster>)?.uuid ?? null);
setValue(
"destinationConfig.clusterName",
(option as SingleValue<TCamundaCluster>)?.name ?? ""
);
}}
options={clusters}
placeholder="Select a cluster..."
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.uuid}
/>
</FormControl>
)}
/>
</>
);
};

View File

@@ -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";
@@ -38,6 +39,8 @@ export const SecretSyncDestinationFields = () => {
return <HumanitecSyncFields />;
case SecretSync.TerraformCloud:
return <TerraformCloudSyncFields />;
case SecretSync.Camunda:
return <CamundaSyncFields />;
case SecretSync.Vercel:
return <VercelSyncFields />;
default:

View File

@@ -40,6 +40,7 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => {
case SecretSync.Databricks:
case SecretSync.Humanitec:
case SecretSync.TerraformCloud:
case SecretSync.Camunda:
case SecretSync.Vercel:
AdditionalSyncOptionsFieldsComponent = null;
break;

View File

@@ -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<TSecretSyncForm & { destination: SecretSync.Camunda }>();
const scope = watch("destinationConfig.scope");
const clusterName = watch("destinationConfig.clusterName");
const clusterUUID = watch("destinationConfig.clusterUUID");
return (
<>
<GenericFieldLabel label="Secret Scope">{scope}</GenericFieldLabel>
<GenericFieldLabel label="Cluster">
{clusterName} (id:{clusterUUID})
</GenericFieldLabel>
</>
);
};

View File

@@ -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";
@@ -77,6 +78,9 @@ export const SecretSyncReviewFields = () => {
case SecretSync.TerraformCloud:
DestinationFieldsComponent = <TerraformCloudSyncReviewFields />;
break;
case SecretSync.Camunda:
DestinationFieldsComponent = <CamundaSyncReviewFields />;
break;
case SecretSync.Vercel:
DestinationFieldsComponent = <VercelSyncReviewFields />;
break;

View File

@@ -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()
})
})
);

View File

@@ -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 { TerraformCloudSyncDestinationSchema } from "./terraform-cloud-destination-schema";
@@ -22,6 +23,7 @@ const SecretSyncUnionSchema = z.discriminatedUnion("destination", [
DatabricksSyncDestinationSchema,
HumanitecSyncDestinationSchema,
TerraformCloudSyncDestinationSchema,
CamundaSyncDestinationSchema,
VercelSyncDestinationSchema
]);

View File

@@ -6,6 +6,7 @@ import {
AwsConnectionMethod,
AzureAppConfigurationConnectionMethod,
AzureKeyVaultConnectionMethod,
CamundaConnectionMethod,
DatabricksConnectionMethod,
GcpConnectionMethod,
GitHubConnectionMethod,
@@ -34,7 +35,8 @@ export const APP_CONNECTION_MAP: Record<AppConnection, { name: string; image: st
[AppConnection.TerraformCloud]: { name: "Terraform Cloud", image: "Terraform Cloud.png" },
[AppConnection.Vercel]: { name: "Vercel", image: "Vercel.png" },
[AppConnection.Postgres]: { name: "PostgreSQL", image: "Postgres.png" },
[AppConnection.MsSql]: { name: "Microsoft SQL Server", image: "MsSql.png" }
[AppConnection.MsSql]: { name: "Microsoft SQL Server", image: "MsSql.png" },
[AppConnection.Camunda]: { name: "Camunda", image: "Camunda.png" }
};
export const getAppConnectionMethodDetails = (method: TAppConnection["method"]) => {
@@ -53,6 +55,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 TerraformCloudConnectionMethod.ApiToken:
case VercelConnectionMethod.ApiToken:

View File

@@ -28,6 +28,10 @@ export const SECRET_SYNC_MAP: Record<SecretSync, { name: string; image: string }
name: "Terraform Cloud",
image: "Terraform Cloud.png"
},
[SecretSync.Camunda]: {
name: "Camunda",
image: "Camunda.png"
},
[SecretSync.Vercel]: {
name: "Vercel",
image: "Vercel.png"
@@ -44,6 +48,7 @@ export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = {
[SecretSync.Databricks]: AppConnection.Databricks,
[SecretSync.Humanitec]: AppConnection.Humanitec,
[SecretSync.TerraformCloud]: AppConnection.TerraformCloud,
[SecretSync.Camunda]: AppConnection.Camunda,
[SecretSync.Vercel]: AppConnection.Vercel
};

View File

@@ -0,0 +1 @@
export * from "./queries";

View File

@@ -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<typeof camundaConnectionKeys.listClusters>
>,
"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
});
};

View File

@@ -0,0 +1,4 @@
export type TCamundaCluster = {
name: string;
uuid: string;
};

View File

@@ -9,5 +9,6 @@ export enum AppConnection {
TerraformCloud = "terraform-cloud",
Vercel = "vercel",
Postgres = "postgres",
MsSql = "mssql"
MsSql = "mssql",
Camunda = "camunda"
}

View File

@@ -55,6 +55,10 @@ export type TMsSqlConnectionOption = TAppConnectionOptionBase & {
app: AppConnection.MsSql;
};
export type TCamundaConnectionOption = TAppConnectionOptionBase & {
app: AppConnection.Camunda;
};
export type TAppConnectionOption =
| TAwsConnectionOption
| TGitHubConnectionOption
@@ -66,7 +70,8 @@ export type TAppConnectionOption =
| TTerraformCloudConnectionOption
| TVercelConnectionOption
| TPostgresConnectionOption
| TMsSqlConnectionOption;
| TMsSqlConnectionOption
| TCamundaConnectionOption;
export type TAppConnectionOptionMap = {
[AppConnection.AWS]: TAwsConnectionOption;
@@ -80,4 +85,5 @@ export type TAppConnectionOptionMap = {
[AppConnection.Vercel]: TVercelConnectionOption;
[AppConnection.Postgres]: TPostgresConnectionOption;
[AppConnection.MsSql]: TMsSqlConnectionOption;
[AppConnection.Camunda]: TCamundaConnectionOption;
};

View File

@@ -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;
};
};

View File

@@ -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";
@@ -15,6 +16,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";
@@ -35,7 +37,8 @@ export type TAppConnection =
| TTerraformCloudConnection
| TVercelConnection
| TPostgresConnection
| TMsSqlConnection;
| TMsSqlConnection
| TCamundaConnection;
export type TAvailableAppConnection = Pick<TAppConnection, "name" | "id">;
@@ -74,4 +77,5 @@ export type TAppConnectionMap = {
[AppConnection.Vercel]: TVercelConnection;
[AppConnection.Postgres]: TPostgresConnection;
[AppConnection.MsSql]: TMsSqlConnection;
[AppConnection.Camunda]: TCamundaConnection;
};

View File

@@ -8,6 +8,7 @@ export enum SecretSync {
Databricks = "databricks",
Humanitec = "humanitec",
TerraformCloud = "terraform-cloud",
Camunda = "camunda",
Vercel = "vercel"
}

View File

@@ -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;
};
};

View File

@@ -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 { TTerraformCloudSync } from "./terraform-cloud-sync";
@@ -28,6 +29,7 @@ export type TSecretSync =
| TDatabricksSync
| THumanitecSync
| TTerraformCloudSync
| TCamundaSync
| TVercelSync;
export type TListSecretSyncs = { secretSyncs: TSecretSync[] };

View File

@@ -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";
@@ -80,6 +81,8 @@ const CreateForm = ({ app, onComplete }: CreateFormProps) => {
return <PostgresConnectionForm onSubmit={onSubmit} />;
case AppConnection.MsSql:
return <MsSqlConnectionForm onSubmit={onSubmit} />;
case AppConnection.Camunda:
return <CamundaConnectionForm onSubmit={onSubmit} />;
default:
throw new Error(`Unhandled App ${app}`);
}
@@ -138,6 +141,8 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => {
return <PostgresConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
case AppConnection.MsSql:
return <MsSqlConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
case AppConnection.Camunda:
return <CamundaConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
default:
throw new Error(`Unhandled App ${(appConnection as TAppConnection).app}`);
}

View File

@@ -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<void>;
};
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<typeof formSchema>;
export const CamundaConnectionForm = ({ appConnection, onSubmit }: Props) => {
const isUpdate = Boolean(appConnection);
const form = useForm<FormData>({
resolver: zodResolver(formSchema),
defaultValues: appConnection ?? {
app: AppConnection.Camunda,
method: CamundaConnectionMethod.ClientCredentials
}
});
const {
handleSubmit,
control,
formState: { isSubmitting, isDirty }
} = form;
return (
<FormProvider {...form}>
<form onSubmit={handleSubmit(onSubmit)}>
{!isUpdate && <GenericAppConnectionsFields />}
<Controller
name="method"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
tooltipText={`The method you would like to use to connect with ${
APP_CONNECTION_MAP[AppConnection.Camunda].name
}. This field cannot be changed after creation.`}
errorText={error?.message}
isError={Boolean(error?.message)}
label="Method"
>
<Select
isDisabled={isUpdate}
value={value}
onValueChange={(val) => onChange(val)}
className="w-full border border-mineshaft-500"
position="popper"
dropdownContainerClassName="max-w-none"
>
{Object.values(CamundaConnectionMethod).map((method) => {
return (
<SelectItem value={method} key={method}>
{getAppConnectionMethodDetails(method).name}
</SelectItem>
);
})}
</Select>
</FormControl>
)}
/>
<Controller
name="credentials.clientId"
control={control}
shouldUnregister
render={({ field, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error?.message)}
label="Client ID"
>
<Input {...field} placeholder="YrBDnQYLevSe40PJ" />
</FormControl>
)}
/>
<Controller
name="credentials.clientSecret"
control={control}
shouldUnregister
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error?.message)}
label="Client Secret"
>
<SecretInput
containerClassName="text-gray-400 group-focus-within:!border-primary-400/50 border border-mineshaft-500 bg-mineshaft-900 px-2.5 py-1.5"
value={value}
onChange={(e) => onChange(e.target.value)}
/>
</FormControl>
)}
/>
<div className="mt-8 flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
colorSchema="secondary"
isLoading={isSubmitting}
isDisabled={isSubmitting || !isDirty}
>
{isUpdate ? "Update Credentials" : "Connect to Camunda"}
</Button>
<ModalClose asChild>
<Button colorSchema="secondary" variant="plain">
Cancel
</Button>
</ModalClose>
</div>
</form>
</FormProvider>
);
};

View File

@@ -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 <SecretSyncTableCell primaryText="Loading cluster info..." secondaryText="Cluster" />;
}
return <SecretSyncTableCell primaryText={primaryText} secondaryText={secondaryText} />;
};

View File

@@ -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";
@@ -35,6 +36,8 @@ export const SecretSyncDestinationCol = ({ secretSync }: Props) => {
return <HumanitecSyncDestinationCol secretSync={secretSync} />;
case SecretSync.TerraformCloud:
return <TerraformCloudSyncDestinationCol secretSync={secretSync} />;
case SecretSync.Camunda:
return <CamundaSyncDestinationCol secretSync={secretSync} />;
case SecretSync.Vercel:
return <VercelSyncDestinationCol secretSync={secretSync} />;
default:

View File

@@ -82,6 +82,10 @@ export const getSecretSyncDestinationColValues = (secretSync: TSecretSync) => {
secondaryText = destinationConfig.workspaceName;
}
break;
case SecretSync.Camunda:
primaryText = destinationConfig.clusterName ?? destinationConfig.clusterUUID;
secondaryText = "Cluster";
break;
case SecretSync.Vercel:
primaryText = destinationConfig.appName || destinationConfig.app;
secondaryText = destinationConfig.env;

View File

@@ -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 <GenericFieldLabel label="Cluster">Loading...</GenericFieldLabel>;
}
const clusterName = clusters?.find((cluster) => cluster.uuid === clusterUUID)?.name;
return <GenericFieldLabel label="Cluster">{clusterName ?? clusterUUID}</GenericFieldLabel>;
};

View File

@@ -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 { TerraformCloudSyncDestinationSection } from "./TerraformCloudSyncDestinationSection";
@@ -62,6 +63,9 @@ export const SecretSyncDestinationSection = ({ secretSync, onEditDestination }:
case SecretSync.TerraformCloud:
DestinationComponents = <TerraformCloudSyncDestinationSection secretSync={secretSync} />;
break;
case SecretSync.Camunda:
DestinationComponents = <CamundaSyncDestinationSection secretSync={secretSync} />;
break;
case SecretSync.Vercel:
DestinationComponents = <VercelSyncDestinationSection secretSync={secretSync} />;
break;

View File

@@ -1,6 +1,7 @@
import { ReactNode } from "react";
import { GenericFieldLabel } from "@app/components/secret-syncs";
import { TerraformCloudSyncCategory } from "@app/hooks/api/appConnections/terraform-cloud";
import {
TerraformCloudSyncScope,
TTerraformCloudSync
@@ -22,6 +23,13 @@ export const TerraformCloudSyncDestinationSection = ({ secretSync }: Props) => {
<GenericFieldLabel label="Variable Set">
{destinationConfig.variableSetName}
</GenericFieldLabel>
<GenericFieldLabel label="Category">
{Object.keys(TerraformCloudSyncCategory).find(
(key) =>
TerraformCloudSyncCategory[key as keyof typeof TerraformCloudSyncCategory] ===
destinationConfig.category
)}
</GenericFieldLabel>
</>
);
break;
@@ -30,6 +38,13 @@ export const TerraformCloudSyncDestinationSection = ({ secretSync }: Props) => {
<>
<GenericFieldLabel label="Organization">{destinationConfig.org}</GenericFieldLabel>
<GenericFieldLabel label="Workspace">{destinationConfig.workspaceName}</GenericFieldLabel>
<GenericFieldLabel label="Category">
{Object.keys(TerraformCloudSyncCategory).find(
(key) =>
TerraformCloudSyncCategory[key as keyof typeof TerraformCloudSyncCategory] ===
destinationConfig.category
)}
</GenericFieldLabel>
</>
);
break;

View File

@@ -49,6 +49,7 @@ export const SecretSyncOptionsSection = ({ secretSync, onEditOptions }: Props) =
case SecretSync.Databricks:
case SecretSync.Humanitec:
case SecretSync.TerraformCloud:
case SecretSync.Camunda:
case SecretSync.Vercel:
AdditionalSyncOptionsComponent = null;
break;