diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index 584f480ad..269944d2d 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -2502,6 +2502,11 @@ export const SecretSyncs = { SUPABASE: { projectId: "The ID of the Supabase project to sync secrets to.", projectName: "The name of the Supabase project to sync secrets to." + }, + BITBUCKET: { + workspace: "The Bitbucket Workspace slug to sync secrets to.", + repository: "The Bitbucket Repository slug to sync secrets to.", + environment: "The Bitbucket Deployment Environment uuid to sync secrets to." } } }; diff --git a/backend/src/server/routes/v1/app-connection-routers/bitbucket-connection-router.ts b/backend/src/server/routes/v1/app-connection-routers/bitbucket-connection-router.ts index 7fe5113e5..23381e65b 100644 --- a/backend/src/server/routes/v1/app-connection-routers/bitbucket-connection-router.ts +++ b/backend/src/server/routes/v1/app-connection-routers/bitbucket-connection-router.ts @@ -85,4 +85,40 @@ export const registerBitbucketConnectionRouter = async (server: FastifyZodProvid return { repositories }; } }); + + server.route({ + method: "GET", + url: `/:connectionId/environments`, + config: { + rateLimit: readLimit + }, + schema: { + params: z.object({ + connectionId: z.string().uuid() + }), + querystring: z.object({ + workspaceSlug: z.string().min(1).max(255), + repositorySlug: z.string().min(1).max(255) + }), + response: { + 200: z.object({ + environments: z.object({ slug: z.string(), name: z.string(), uuid: z.string() }).array() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { + params: { connectionId }, + query: { workspaceSlug, repositorySlug } + } = req; + + const environments = await server.services.appConnection.bitbucket.listEnvironments( + { connectionId, workspaceSlug, repositorySlug }, + req.permission + ); + + return { environments }; + } + }); }; diff --git a/backend/src/server/routes/v1/secret-sync-routers/bitbucket-sync-router.ts b/backend/src/server/routes/v1/secret-sync-routers/bitbucket-sync-router.ts new file mode 100644 index 000000000..17cd0dbbf --- /dev/null +++ b/backend/src/server/routes/v1/secret-sync-routers/bitbucket-sync-router.ts @@ -0,0 +1,17 @@ +import { + BitbucketSyncSchema, + CreateBitbucketSyncSchema, + UpdateBitbucketSyncSchema +} from "@app/services/secret-sync/bitbucket"; +import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; + +import { registerSyncSecretsEndpoints } from "./secret-sync-endpoints"; + +export const registerBitbucketSyncRouter = async (server: FastifyZodProvider) => + registerSyncSecretsEndpoints({ + destination: SecretSync.Bitbucket, + server, + responseSchema: BitbucketSyncSchema, + createSchema: CreateBitbucketSyncSchema, + updateSchema: UpdateBitbucketSyncSchema + }); 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 8e8f696b7..1b640acae 100644 --- a/backend/src/server/routes/v1/secret-sync-routers/index.ts +++ b/backend/src/server/routes/v1/secret-sync-routers/index.ts @@ -7,6 +7,7 @@ import { registerAwsSecretsManagerSyncRouter } from "./aws-secrets-manager-sync- import { registerAzureAppConfigurationSyncRouter } from "./azure-app-configuration-sync-router"; import { registerAzureDevOpsSyncRouter } from "./azure-devops-sync-router"; import { registerAzureKeyVaultSyncRouter } from "./azure-key-vault-sync-router"; +import { registerBitbucketSyncRouter } from "./bitbucket-sync-router"; import { registerCamundaSyncRouter } from "./camunda-sync-router"; import { registerChecklySyncRouter } from "./checkly-sync-router"; import { registerCloudflarePagesSyncRouter } from "./cloudflare-pages-sync-router"; @@ -57,5 +58,6 @@ export const SECRET_SYNC_REGISTER_ROUTER_MAP: Record { diff --git a/backend/src/services/app-connection/bitbucket/bitbucket-connection-fns.ts b/backend/src/services/app-connection/bitbucket/bitbucket-connection-fns.ts index 2d418a8a3..f610a54fd 100644 --- a/backend/src/services/app-connection/bitbucket/bitbucket-connection-fns.ts +++ b/backend/src/services/app-connection/bitbucket/bitbucket-connection-fns.ts @@ -9,6 +9,7 @@ import { BitbucketConnectionMethod } from "./bitbucket-connection-enums"; import { TBitbucketConnection, TBitbucketConnectionConfig, + TBitbucketEnvironment, TBitbucketRepo, TBitbucketWorkspace } from "./bitbucket-connection-types"; @@ -115,3 +116,40 @@ export const listBitbucketRepositories = async (appConnection: TBitbucketConnect return allRepos; }; + +export const listBitbucketEnvironments = async ( + appConnection: TBitbucketConnection, + workspaceSlug: string, + repositorySlug: string +) => { + const { email, apiToken } = appConnection.credentials; + + const headers = { + Authorization: `Basic ${Buffer.from(`${email}:${apiToken}`).toString("base64")}`, + Accept: "application/json" + }; + + const environments: TBitbucketEnvironment[] = []; + let hasNextPage = true; + + let environmentsUrl = `${IntegrationUrls.BITBUCKET_API_URL}/2.0/repositories/${encodeURIComponent(workspaceSlug)}/${encodeURIComponent(repositorySlug)}/environments?pagelen=100`; + + while (hasNextPage) { + // eslint-disable-next-line no-await-in-loop + const { data }: { data: { values: TBitbucketEnvironment[]; next: string } } = await request.get(environmentsUrl, { + headers + }); + + if (data?.values.length > 0) { + environments.push(...data.values); + } + + if (data.next) { + environmentsUrl = data.next; + } else { + hasNextPage = false; + } + } + + return environments; +}; diff --git a/backend/src/services/app-connection/bitbucket/bitbucket-connection-service.ts b/backend/src/services/app-connection/bitbucket/bitbucket-connection-service.ts index f08a8d276..b995cb9c0 100644 --- a/backend/src/services/app-connection/bitbucket/bitbucket-connection-service.ts +++ b/backend/src/services/app-connection/bitbucket/bitbucket-connection-service.ts @@ -1,8 +1,16 @@ import { OrgServiceActor } from "@app/lib/types"; import { AppConnection } from "../app-connection-enums"; -import { listBitbucketRepositories, listBitbucketWorkspaces } from "./bitbucket-connection-fns"; -import { TBitbucketConnection, TGetBitbucketRepositoriesDTO } from "./bitbucket-connection-types"; +import { + listBitbucketEnvironments, + listBitbucketRepositories, + listBitbucketWorkspaces +} from "./bitbucket-connection-fns"; +import { + TBitbucketConnection, + TGetBitbucketEnvironmentsDTO, + TGetBitbucketRepositoriesDTO +} from "./bitbucket-connection-types"; type TGetAppConnectionFunc = ( app: AppConnection, @@ -26,8 +34,18 @@ export const bitbucketConnectionService = (getAppConnection: TGetAppConnectionFu return repositories; }; + const listEnvironments = async ( + { connectionId, workspaceSlug, repositorySlug }: TGetBitbucketEnvironmentsDTO, + actor: OrgServiceActor + ) => { + const appConnection = await getAppConnection(AppConnection.Bitbucket, connectionId, actor); + const environments = await listBitbucketEnvironments(appConnection, workspaceSlug, repositorySlug); + return environments; + }; + return { listWorkspaces, - listRepositories + listRepositories, + listEnvironments }; }; diff --git a/backend/src/services/app-connection/bitbucket/bitbucket-connection-types.ts b/backend/src/services/app-connection/bitbucket/bitbucket-connection-types.ts index b0694c6e3..7311e7dbf 100644 --- a/backend/src/services/app-connection/bitbucket/bitbucket-connection-types.ts +++ b/backend/src/services/app-connection/bitbucket/bitbucket-connection-types.ts @@ -38,3 +38,20 @@ export type TBitbucketRepo = { full_name: string; // workspace-slug/repo-slug slug: string; }; + +export type TGetBitbucketEnvironmentsDTO = { + connectionId: string; + workspaceSlug: string; + repositorySlug: string; +}; + +export type TBitbucketEnvironment = { + uuid: string; + slug: string; + name: string; +}; + +export type BitbucketEnvironmentsResponse = { + values: TBitbucketEnvironment[]; + next?: string; +}; diff --git a/backend/src/services/secret-sync/bitbucket/bitbucket-sync-constants.ts b/backend/src/services/secret-sync/bitbucket/bitbucket-sync-constants.ts new file mode 100644 index 000000000..5d3f1274c --- /dev/null +++ b/backend/src/services/secret-sync/bitbucket/bitbucket-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 BITBUCKET_SYNC_LIST_OPTION: TSecretSyncListItem = { + name: "Bitbucket", + destination: SecretSync.Bitbucket, + connection: AppConnection.Bitbucket, + canImportSecrets: true +}; diff --git a/backend/src/services/secret-sync/bitbucket/bitbucket-sync-fns.ts b/backend/src/services/secret-sync/bitbucket/bitbucket-sync-fns.ts new file mode 100644 index 000000000..6c8e78179 --- /dev/null +++ b/backend/src/services/secret-sync/bitbucket/bitbucket-sync-fns.ts @@ -0,0 +1,380 @@ +import { request } from "@app/lib/config/request"; +import { IntegrationUrls } from "@app/services/integration-auth/integration-list"; +import { + TBitbucketListVariables, + TBitbucketSyncWithCredentials, + TBitbucketVariable, + TDeleteBitbucketVariable, + TPutBitbucketVariable +} from "@app/services/secret-sync/bitbucket/bitbucket-sync-types"; +import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors"; +import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns"; +import { TSecretMap } from "@app/services/secret-sync/secret-sync-types"; + +const listBitbucketSecrets = async ({ email, apiToken, workspace, repository }: TBitbucketListVariables) => { + const { data } = await request.get<{ values: TBitbucketVariable[] }>( + `${IntegrationUrls.BITBUCKET_API_URL}/2.0/repositories/${encodeURIComponent(workspace)}/${encodeURIComponent(repository)}/pipelines_config/variables/`, + { + headers: { + Authorization: `Basic ${Buffer.from(`${email}:${apiToken}`).toString("base64")}`, + Accept: "application/json" + } + } + ); + + return data.values; +}; + +const listBitbucketEnvironmentSecrets = async ({ + email, + apiToken, + workspace, + repository, + environment +}: TBitbucketListVariables & { environment: string }) => { + const { data } = await request.get<{ values: TBitbucketVariable[] }>( + `${IntegrationUrls.BITBUCKET_API_URL}/2.0/repositories/${encodeURIComponent(workspace)}/${encodeURIComponent(repository)}/deployments_config/environments/${environment}/variables`, + { + headers: { + Authorization: `Basic ${Buffer.from(`${email}:${apiToken}`).toString("base64")}`, + Accept: "application/json" + } + } + ); + + return data.values; +}; + +// Helper function to upsert a single variable +const upsertBitbucketVariable = async ({ + email, + apiToken, + workspace, + repository, + key, + value, + existingVariables, + isEnvironment = false, + environment +}: { + email: string; + apiToken: string; + workspace: string; + repository: string; + key: string; + value: string; + existingVariables: TBitbucketVariable[]; + isEnvironment?: boolean; + environment?: string; +}) => { + const existingVariable = existingVariables.find((variable) => variable.key === key); + const auth = `Basic ${Buffer.from(`${email}:${apiToken}`).toString("base64")}`; + + if (existingVariable) { + // Variable exists, use PUT to update it + const baseUrl = `${IntegrationUrls.BITBUCKET_API_URL}/2.0/repositories/${encodeURIComponent(workspace)}/${encodeURIComponent(repository)}`; + const url = isEnvironment + ? `${baseUrl}/deployments_config/environments/${environment}/variables/${existingVariable.uuid}` + : `${baseUrl}/pipelines_config/variables/${existingVariable.uuid}`; + + return request.put( + url, + { + key, + value, + secured: true + }, + { + headers: { + Authorization: auth, + "Content-Type": "application/json" + } + } + ); + } + + const baseUrl = `${IntegrationUrls.BITBUCKET_API_URL}/2.0/repositories/${encodeURIComponent(workspace)}/${encodeURIComponent(repository)}`; + const url = isEnvironment + ? `${baseUrl}/deployments_config/environments/${environment}/variables` + : `${baseUrl}/pipelines_config/variables/`; + + return request.post( + url, + { + key, + value, + secured: true + }, + { + headers: { + Authorization: auth, + "Content-Type": "application/json" + } + } + ); +}; + +const putBitbucketSecrets = async ({ + email, + apiToken, + workspace, + repository, + secretMap +}: TPutBitbucketVariable & { secretMap: TSecretMap }) => { + // Get existing variables first + const existingVariables = await listBitbucketSecrets({ email, apiToken, workspace, repository }); + + const promises = Object.entries(secretMap).map(([key, { value }]) => { + return upsertBitbucketVariable({ + email, + apiToken, + workspace, + repository, + key, + value, + existingVariables, + isEnvironment: false + }); + }); + + return Promise.all(promises); +}; + +const putBitbucketEnvironmentSecrets = async ({ + email, + apiToken, + workspace, + repository, + environment, + secretMap +}: TPutBitbucketVariable & { environment: string; secretMap: TSecretMap }) => { + // Get existing variables first + const existingVariables = await listBitbucketEnvironmentSecrets({ + email, + apiToken, + workspace, + repository, + environment + }); + + const promises = Object.entries(secretMap).map(([key, { value }]) => { + return upsertBitbucketVariable({ + email, + apiToken, + workspace, + repository, + key, + value, + existingVariables, + isEnvironment: true, + environment + }); + }); + + return Promise.all(promises); +}; + +const deleteBitbucketSecrets = async ({ email, apiToken, workspace, repository, keys }: TDeleteBitbucketVariable) => { + // First, we need to get the variable UUIDs since Bitbucket requires UUIDs for deletion + const existingVariables = await listBitbucketSecrets({ email, apiToken, workspace, repository }); + const variablesToDelete = existingVariables.filter((variable) => keys.includes(variable.key)); + + const promises = variablesToDelete.map((variable) => { + return request.delete( + `${IntegrationUrls.BITBUCKET_API_URL}/2.0/repositories/${encodeURIComponent(workspace)}/${encodeURIComponent(repository)}/pipelines_config/variables/${variable.uuid}`, + { + headers: { + Authorization: `Basic ${Buffer.from(`${email}:${apiToken}`).toString("base64")}` + } + } + ); + }); + + return Promise.all(promises); +}; + +const deleteBitbucketEnvironmentSecrets = async ({ + email, + apiToken, + workspace, + repository, + environment, + keys +}: TDeleteBitbucketVariable & { environment: string }) => { + // Get the variable UUIDs since Bitbucket requires UUIDs for deletion + const existingVariables = await listBitbucketEnvironmentSecrets({ + email, + apiToken, + workspace, + repository, + environment + }); + const variablesToDelete = existingVariables.filter((variable) => keys.includes(variable.key)); + + const promises = variablesToDelete.map((variable) => { + return request.delete( + `${IntegrationUrls.BITBUCKET_API_URL}/2.0/repositories/${encodeURIComponent(workspace)}/${encodeURIComponent(repository)}/deployments_config/environments/${environment}/variables/${variable.uuid}`, + { + headers: { + Authorization: `Basic ${Buffer.from(`${email}:${apiToken}`).toString("base64")}` + } + } + ); + }); + + return Promise.all(promises); +}; + +export const BitbucketSyncFns = { + syncSecrets: async (secretSync: TBitbucketSyncWithCredentials, secretMap: TSecretMap) => { + const { + connection, + environment, + destinationConfig: { workspace, repository, environment: configEnvironment } + } = secretSync; + + const { email, apiToken } = connection.credentials; + + try { + // If environment is specified in destinationConfig, use environment variables + if (configEnvironment) { + await putBitbucketEnvironmentSecrets({ + email, + apiToken, + workspace, + repository, + environment: configEnvironment, + secretMap + }); + } else { + // Otherwise, use repository variables (original behavior) + await putBitbucketSecrets({ email, apiToken, workspace, repository, secretMap }); + } + } catch (error) { + throw new SecretSyncError({ + error + }); + } + + if (secretSync.syncOptions.disableSecretDeletion) return; + + // Get existing secrets based on whether we're using environment or repository variables + const existingVariables = configEnvironment + ? await listBitbucketEnvironmentSecrets({ + email, + apiToken, + workspace, + repository, + environment: configEnvironment + }) + : await listBitbucketSecrets({ email, apiToken, workspace, repository }); + + const keys = existingVariables + .map((variable) => variable.key) + .filter( + (secret) => + matchesSchema(secret, environment?.slug || "", secretSync.syncOptions.keySchema) && !(secret in secretMap) + ); + + if (keys.length > 0) { + try { + if (configEnvironment) { + await deleteBitbucketEnvironmentSecrets({ + email, + apiToken, + workspace, + repository, + environment: configEnvironment, + keys + }); + } else { + await deleteBitbucketSecrets({ email, apiToken, workspace, repository, keys }); + } + } catch (error) { + throw new SecretSyncError({ + error + }); + } + } + }, + removeSecrets: async (secretSync: TBitbucketSyncWithCredentials, secretMap: TSecretMap) => { + const { + connection, + destinationConfig: { workspace, repository, environment: configEnvironment } + } = secretSync; + + const { email, apiToken } = connection.credentials; + + const existingVariables = configEnvironment + ? await listBitbucketEnvironmentSecrets({ + email, + apiToken, + workspace, + repository, + environment: configEnvironment + }) + : await listBitbucketSecrets({ email, apiToken, workspace, repository }); + + const keys = existingVariables.map((variable) => variable.key).filter((secret) => secret in secretMap); + + if (keys.length > 0) { + try { + if (configEnvironment) { + await deleteBitbucketEnvironmentSecrets({ + email, + apiToken, + workspace, + repository, + environment: configEnvironment, + keys + }); + } else { + await deleteBitbucketSecrets({ email, apiToken, workspace, repository, keys }); + } + } catch (error) { + throw new SecretSyncError({ + error + }); + } + } + }, + getSecrets: async (secretSync: TBitbucketSyncWithCredentials) => { + const { + connection, + destinationConfig: { workspace, repository, environment } + } = secretSync; + + const { email, apiToken } = connection.credentials; + + try { + let url: string; + + if (environment) { + url = `${IntegrationUrls.BITBUCKET_API_URL}/2.0/repositories/${encodeURIComponent(workspace)}/${encodeURIComponent(repository)}/deployments_config/environments/${environment}/variables`; + } else { + url = `${IntegrationUrls.BITBUCKET_API_URL}/2.0/repositories/${encodeURIComponent(workspace)}/${encodeURIComponent(repository)}/pipelines_config/variables/`; + } + + const { data } = await request.get<{ values: TBitbucketVariable[] }>(url, { + headers: { + Authorization: `Basic ${Buffer.from(`${email}:${apiToken}`).toString("base64")}`, + Accept: "application/json" + } + }); + + const secretMap: TSecretMap = {}; + data.values.forEach((variable) => { + secretMap[variable.key] = { + value: variable.secured ? "[SECURED]" : variable.value || "", + comment: "" + }; + }); + + return secretMap; + } catch (error) { + throw new SecretSyncError({ + error + }); + } + } +}; diff --git a/backend/src/services/secret-sync/bitbucket/bitbucket-sync-schemas.ts b/backend/src/services/secret-sync/bitbucket/bitbucket-sync-schemas.ts new file mode 100644 index 000000000..d5df5fb3a --- /dev/null +++ b/backend/src/services/secret-sync/bitbucket/bitbucket-sync-schemas.ts @@ -0,0 +1,45 @@ +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 BitbucketSyncDestinationConfigSchema = z.object({ + repository: z.string().describe(SecretSyncs.DESTINATION_CONFIG.BITBUCKET.repository), + environment: z.string().optional().describe(SecretSyncs.DESTINATION_CONFIG.BITBUCKET.environment), + workspace: z.string().describe(SecretSyncs.DESTINATION_CONFIG.BITBUCKET.workspace) +}); + +const BitbucketSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: true }; + +export const BitbucketSyncSchema = BaseSecretSyncSchema(SecretSync.Bitbucket, BitbucketSyncOptionsConfig).extend({ + destination: z.literal(SecretSync.Bitbucket), + destinationConfig: BitbucketSyncDestinationConfigSchema +}); + +export const CreateBitbucketSyncSchema = GenericCreateSecretSyncFieldsSchema( + SecretSync.Bitbucket, + BitbucketSyncOptionsConfig +).extend({ + destinationConfig: BitbucketSyncDestinationConfigSchema +}); + +export const UpdateBitbucketSyncSchema = GenericUpdateSecretSyncFieldsSchema( + SecretSync.Bitbucket, + BitbucketSyncOptionsConfig +).extend({ + destinationConfig: BitbucketSyncDestinationConfigSchema.optional() +}); + +export const BitbucketSyncListItemSchema = z.object({ + name: z.literal("Bitbucket"), + connection: z.literal(AppConnection.Bitbucket), + destination: z.literal(SecretSync.Bitbucket), + canImportSecrets: z.literal(true) +}); diff --git a/backend/src/services/secret-sync/bitbucket/bitbucket-sync-types.ts b/backend/src/services/secret-sync/bitbucket/bitbucket-sync-types.ts new file mode 100644 index 000000000..b8ff85f30 --- /dev/null +++ b/backend/src/services/secret-sync/bitbucket/bitbucket-sync-types.ts @@ -0,0 +1,50 @@ +import { z } from "zod"; + +import { TBitbucketConnection } from "@app/services/app-connection/bitbucket"; + +import { BitbucketSyncListItemSchema, BitbucketSyncSchema, CreateBitbucketSyncSchema } from "./bitbucket-sync-schemas"; + +export type TBitbucketSync = z.infer; + +export type TBitbucketSyncInput = z.infer; + +export type TBitbucketSyncListItem = z.infer; + +export type TBitbucketSyncWithCredentials = TBitbucketSync & { + connection: TBitbucketConnection; +}; + +export type TBitbucketVariable = { + key: string; + value?: string; + secured: boolean; + uuid: string; + type: string; +}; + +export type TBitbucketListVariables = { + apiToken: string; + email: string; + workspace: string; + repository: string; +}; + +export type TPutBitbucketVariable = { + email: string; + apiToken: string; + workspace: string; + repository: string; +}; + +export type TDeleteBitbucketVariable = { + email: string; + apiToken: string; + workspace: string; + repository: string; + keys: string[]; +}; + +export type TBitbucketConnectionCredentials = { + email: string; + apiToken: string; +}; diff --git a/backend/src/services/secret-sync/bitbucket/index.ts b/backend/src/services/secret-sync/bitbucket/index.ts new file mode 100644 index 000000000..d0f20bd45 --- /dev/null +++ b/backend/src/services/secret-sync/bitbucket/index.ts @@ -0,0 +1,4 @@ +export * from "./bitbucket-sync-constants"; +export * from "./bitbucket-sync-fns"; +export * from "./bitbucket-sync-schemas"; +export * from "./bitbucket-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 8d08e4d82..2c02e2ff4 100644 --- a/backend/src/services/secret-sync/secret-sync-enums.ts +++ b/backend/src/services/secret-sync/secret-sync-enums.ts @@ -25,7 +25,8 @@ export enum SecretSync { Supabase = "supabase", Zabbix = "zabbix", Railway = "railway", - Checkly = "checkly" + Checkly = "checkly", + Bitbucket = "bitbucket" } export enum SecretSyncInitialSyncBehavior { diff --git a/backend/src/services/secret-sync/secret-sync-fns.ts b/backend/src/services/secret-sync/secret-sync-fns.ts index 3daa9232f..827a41b7d 100644 --- a/backend/src/services/secret-sync/secret-sync-fns.ts +++ b/backend/src/services/secret-sync/secret-sync-fns.ts @@ -28,6 +28,7 @@ import { ONEPASS_SYNC_LIST_OPTION, OnePassSyncFns } from "./1password"; import { AZURE_APP_CONFIGURATION_SYNC_LIST_OPTION, azureAppConfigurationSyncFactory } from "./azure-app-configuration"; import { AZURE_DEVOPS_SYNC_LIST_OPTION, azureDevOpsSyncFactory } from "./azure-devops"; import { AZURE_KEY_VAULT_SYNC_LIST_OPTION, azureKeyVaultSyncFactory } from "./azure-key-vault"; +import { BITBUCKET_SYNC_LIST_OPTION, BitbucketSyncFns } from "./bitbucket"; import { CAMUNDA_SYNC_LIST_OPTION, camundaSyncFactory } from "./camunda"; import { CHECKLY_SYNC_LIST_OPTION } from "./checkly/checkly-sync-constants"; import { ChecklySyncFns } from "./checkly/checkly-sync-fns"; @@ -80,7 +81,8 @@ const SECRET_SYNC_LIST_OPTIONS: Record = { [SecretSync.Supabase]: SUPABASE_SYNC_LIST_OPTION, [SecretSync.Zabbix]: ZABBIX_SYNC_LIST_OPTION, [SecretSync.Railway]: RAILWAY_SYNC_LIST_OPTION, - [SecretSync.Checkly]: CHECKLY_SYNC_LIST_OPTION + [SecretSync.Checkly]: CHECKLY_SYNC_LIST_OPTION, + [SecretSync.Bitbucket]: BITBUCKET_SYNC_LIST_OPTION }; export const listSecretSyncOptions = () => { @@ -258,6 +260,8 @@ export const SecretSyncFns = { return ChecklySyncFns.syncSecrets(secretSync, schemaSecretMap); case SecretSync.Supabase: return SupabaseSyncFns.syncSecrets(secretSync, schemaSecretMap); + case SecretSync.Bitbucket: + return BitbucketSyncFns.syncSecrets(secretSync, schemaSecretMap); default: throw new Error( `Unhandled sync destination for sync secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}` @@ -365,6 +369,9 @@ export const SecretSyncFns = { case SecretSync.Supabase: secretMap = await SupabaseSyncFns.getSecrets(secretSync); break; + case SecretSync.Bitbucket: + secretMap = await BitbucketSyncFns.getSecrets(secretSync); + break; default: throw new Error( `Unhandled sync destination for get secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}` @@ -452,6 +459,8 @@ export const SecretSyncFns = { return ChecklySyncFns.removeSecrets(secretSync, schemaSecretMap); case SecretSync.Supabase: return SupabaseSyncFns.removeSecrets(secretSync, schemaSecretMap); + case SecretSync.Bitbucket: + return BitbucketSyncFns.removeSecrets(secretSync, schemaSecretMap); default: throw new Error( `Unhandled sync destination for remove secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}` diff --git a/backend/src/services/secret-sync/secret-sync-maps.ts b/backend/src/services/secret-sync/secret-sync-maps.ts index a8a017480..1de9838e3 100644 --- a/backend/src/services/secret-sync/secret-sync-maps.ts +++ b/backend/src/services/secret-sync/secret-sync-maps.ts @@ -28,7 +28,8 @@ export const SECRET_SYNC_NAME_MAP: Record = { [SecretSync.Supabase]: "Supabase", [SecretSync.Zabbix]: "Zabbix", [SecretSync.Railway]: "Railway", - [SecretSync.Checkly]: "Checkly" + [SecretSync.Checkly]: "Checkly", + [SecretSync.Bitbucket]: "Bitbucket" }; export const SECRET_SYNC_CONNECTION_MAP: Record = { @@ -58,7 +59,8 @@ export const SECRET_SYNC_CONNECTION_MAP: Record = { [SecretSync.Supabase]: AppConnection.Supabase, [SecretSync.Zabbix]: AppConnection.Zabbix, [SecretSync.Railway]: AppConnection.Railway, - [SecretSync.Checkly]: AppConnection.Checkly + [SecretSync.Checkly]: AppConnection.Checkly, + [SecretSync.Bitbucket]: AppConnection.Bitbucket }; export const SECRET_SYNC_PLAN_MAP: Record = { @@ -88,5 +90,6 @@ export const SECRET_SYNC_PLAN_MAP: Record = { [SecretSync.Supabase]: SecretSyncPlanType.Regular, [SecretSync.Zabbix]: SecretSyncPlanType.Regular, [SecretSync.Railway]: SecretSyncPlanType.Regular, - [SecretSync.Checkly]: SecretSyncPlanType.Regular + [SecretSync.Checkly]: SecretSyncPlanType.Regular, + [SecretSync.Bitbucket]: SecretSyncPlanType.Regular }; diff --git a/backend/src/services/secret-sync/secret-sync-types.ts b/backend/src/services/secret-sync/secret-sync-types.ts index 2c8753d66..007c79bf7 100644 --- a/backend/src/services/secret-sync/secret-sync-types.ts +++ b/backend/src/services/secret-sync/secret-sync-types.ts @@ -72,6 +72,12 @@ import { TAzureKeyVaultSyncListItem, TAzureKeyVaultSyncWithCredentials } from "./azure-key-vault"; +import { + TBitbucketSync, + TBitbucketSyncInput, + TBitbucketSyncListItem, + TBitbucketSyncWithCredentials +} from "./bitbucket/bitbucket-sync-types"; import { TChecklySync, TChecklySyncInput, @@ -166,7 +172,8 @@ export type TSecretSync = | TZabbixSync | TRailwaySync | TChecklySync - | TSupabaseSync; + | TSupabaseSync + | TBitbucketSync; export type TSecretSyncWithCredentials = | TAwsParameterStoreSyncWithCredentials @@ -195,7 +202,8 @@ export type TSecretSyncWithCredentials = | TZabbixSyncWithCredentials | TRailwaySyncWithCredentials | TChecklySyncWithCredentials - | TSupabaseSyncWithCredentials; + | TSupabaseSyncWithCredentials + | TBitbucketSyncWithCredentials; export type TSecretSyncInput = | TAwsParameterStoreSyncInput @@ -224,7 +232,8 @@ export type TSecretSyncInput = | TZabbixSyncInput | TRailwaySyncInput | TChecklySyncInput - | TSupabaseSyncInput; + | TSupabaseSyncInput + | TBitbucketSyncInput; export type TSecretSyncListItem = | TAwsParameterStoreSyncListItem @@ -253,7 +262,8 @@ export type TSecretSyncListItem = | TZabbixSyncListItem | TRailwaySyncListItem | TChecklySyncListItem - | TSupabaseSyncListItem; + | TSupabaseSyncListItem + | TBitbucketSyncListItem; export type TSyncOptionsConfig = { canImportSecrets: boolean; diff --git a/docs/api-reference/endpoints/secret-syncs/bitbucket/create.mdx b/docs/api-reference/endpoints/secret-syncs/bitbucket/create.mdx new file mode 100644 index 000000000..69535ace0 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/bitbucket/create.mdx @@ -0,0 +1,4 @@ +--- +title: "Create" +openapi: "POST /api/v1/secret-syncs/bitbucket" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/bitbucket/delete.mdx b/docs/api-reference/endpoints/secret-syncs/bitbucket/delete.mdx new file mode 100644 index 000000000..55cfc0359 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/bitbucket/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/secret-syncs/bitbucket/{syncId}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/bitbucket/get-by-id.mdx b/docs/api-reference/endpoints/secret-syncs/bitbucket/get-by-id.mdx new file mode 100644 index 000000000..46373e310 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/bitbucket/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by ID" +openapi: "GET /api/v1/secret-syncs/bitbucket/{syncId}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/bitbucket/get-by-name.mdx b/docs/api-reference/endpoints/secret-syncs/bitbucket/get-by-name.mdx new file mode 100644 index 000000000..82bb47d45 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/bitbucket/get-by-name.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by Name" +openapi: "GET /api/v1/secret-syncs/bitbucket/sync-name/{syncName}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/bitbucket/import-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/bitbucket/import-secrets.mdx new file mode 100644 index 000000000..eed3d9124 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/bitbucket/import-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Import Secrets" +openapi: "POST /api/v1/secret-syncs/bitbucket/{syncId}/import-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/bitbucket/list.mdx b/docs/api-reference/endpoints/secret-syncs/bitbucket/list.mdx new file mode 100644 index 000000000..98bf3fdda --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/bitbucket/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v1/secret-syncs/bitbucket" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/bitbucket/remove-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/bitbucket/remove-secrets.mdx new file mode 100644 index 000000000..3d52e14e2 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/bitbucket/remove-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Remove Secrets" +openapi: "POST /api/v1/secret-syncs/bitbucket/{syncId}/remove-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/bitbucket/sync-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/bitbucket/sync-secrets.mdx new file mode 100644 index 000000000..47fee7642 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/bitbucket/sync-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Sync Secrets" +openapi: "POST /api/v1/secret-syncs/bitbucket/{syncId}/sync-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/bitbucket/update.mdx b/docs/api-reference/endpoints/secret-syncs/bitbucket/update.mdx new file mode 100644 index 000000000..c9dfac1c8 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/bitbucket/update.mdx @@ -0,0 +1,4 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/secret-syncs/bitbucket/{syncId}" +--- diff --git a/docs/docs.json b/docs/docs.json index a32453c89..5bc5f77d5 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -508,6 +508,7 @@ "integrations/secret-syncs/azure-app-configuration", "integrations/secret-syncs/azure-devops", "integrations/secret-syncs/azure-key-vault", + "integrations/secret-syncs/bitbucket", "integrations/secret-syncs/camunda", "integrations/secret-syncs/checkly", "integrations/secret-syncs/cloudflare-pages", @@ -1707,6 +1708,20 @@ "api-reference/endpoints/secret-syncs/azure-key-vault/remove-secrets" ] }, + { + "group": "Bitbucket", + "pages": [ + "api-reference/endpoints/secret-syncs/bitbucket/list", + "api-reference/endpoints/secret-syncs/bitbucket/get-by-id", + "api-reference/endpoints/secret-syncs/bitbucket/get-by-name", + "api-reference/endpoints/secret-syncs/bitbucket/create", + "api-reference/endpoints/secret-syncs/bitbucket/update", + "api-reference/endpoints/secret-syncs/bitbucket/delete", + "api-reference/endpoints/secret-syncs/bitbucket/sync-secrets", + "api-reference/endpoints/secret-syncs/bitbucket/import-secrets", + "api-reference/endpoints/secret-syncs/bitbucket/remove-secrets" + ] + }, { "group": "Camunda", "pages": [ diff --git a/docs/images/secret-syncs/bitbucket/configure-destination.png b/docs/images/secret-syncs/bitbucket/configure-destination.png new file mode 100644 index 000000000..f393387be Binary files /dev/null and b/docs/images/secret-syncs/bitbucket/configure-destination.png differ diff --git a/docs/images/secret-syncs/bitbucket/configure-details.png b/docs/images/secret-syncs/bitbucket/configure-details.png new file mode 100644 index 000000000..64f9c2720 Binary files /dev/null and b/docs/images/secret-syncs/bitbucket/configure-details.png differ diff --git a/docs/images/secret-syncs/bitbucket/configure-source.png b/docs/images/secret-syncs/bitbucket/configure-source.png new file mode 100644 index 000000000..2b70abba5 Binary files /dev/null and b/docs/images/secret-syncs/bitbucket/configure-source.png differ diff --git a/docs/images/secret-syncs/bitbucket/configure-sync-options.png b/docs/images/secret-syncs/bitbucket/configure-sync-options.png new file mode 100644 index 000000000..9afbaaafe Binary files /dev/null and b/docs/images/secret-syncs/bitbucket/configure-sync-options.png differ diff --git a/docs/images/secret-syncs/bitbucket/review-configuration.png b/docs/images/secret-syncs/bitbucket/review-configuration.png new file mode 100644 index 000000000..e76c726fa Binary files /dev/null and b/docs/images/secret-syncs/bitbucket/review-configuration.png differ diff --git a/docs/images/secret-syncs/bitbucket/select-option.png b/docs/images/secret-syncs/bitbucket/select-option.png new file mode 100644 index 000000000..85446dd55 Binary files /dev/null and b/docs/images/secret-syncs/bitbucket/select-option.png differ diff --git a/docs/images/secret-syncs/bitbucket/sync-created.png b/docs/images/secret-syncs/bitbucket/sync-created.png new file mode 100644 index 000000000..31afde5be Binary files /dev/null and b/docs/images/secret-syncs/bitbucket/sync-created.png differ diff --git a/docs/integrations/secret-syncs/bitbucket.mdx b/docs/integrations/secret-syncs/bitbucket.mdx new file mode 100644 index 000000000..21f262bdd --- /dev/null +++ b/docs/integrations/secret-syncs/bitbucket.mdx @@ -0,0 +1,158 @@ +--- +title: "Bitbucket Sync" +description: "Learn how to configure a Bitbucket Sync for Infisical." +--- + +**Prerequisites:** +- Create a [Bitbucket Connection](/integrations/app-connections/bitbucket) + + + + + + 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) + + + ![Select Bitbucket](/images/secret-syncs/bitbucket/select-option.png) + + + Configure the **Source** from where secrets should be retrieved, then click **Next**. + + ![Configure Source](/images/secret-syncs/bitbucket/configure-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). + + + + Configure the **Destination** to where secrets should be deployed, then click **Next**. + + ![Configure Destination](/images/secret-syncs/bitbucket/configure-destination.png) + + - **Bitbucket Connection**: The Bitbucket Connection to authenticate with. + - **Workspace**: The Bitbucket workspace to sync secrets to. + - **Repository**: The Bitbucket repository to sync secrets to. + - **Deployment Environment (Optional)**: The Bitbucket deployment environment to sync secrets to. + + + Configure the **Sync Options** to specify how secrets should be synced, then click **Next**. + + ![Configure Sync Options](/images/secret-syncs/bitbucket/configure-sync-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 Bitbucket when keys conflict. + - **Import Secrets (Prioritize Bitbucket)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Bitbucket over Infisical when keys conflict. + - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name and `{{environment}}` for the environment. + + We highly recommend using a Key Schema to ensure that Infisical only manages the specific keys you intend, keeping everything else untouched. + + - **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. + + + Configure the **Details** of your Bitbucket Sync, then click **Next**. + + ![Configure Details](/images/secret-syncs/bitbucket/configure-details.png) + + - **Name**: The name of your sync. Must be slug-friendly. + - **Description**: An optional description for your sync. + + + Review your Bitbucket Sync configuration, then click **Create Sync**. + + ![Review Configuration](/images/secret-syncs/bitbucket/review-configuration.png) + + + If enabled, your Bitbucket Sync will begin syncing your secrets to the destination endpoint. + + ![Sync Created](/images/secret-syncs/bitbucket/sync-created.png) + + + + + To create a **Bitbucket Sync**, make an API request to the [Create Bitbucket Sync](/api-reference/endpoints/secret-syncs/bitbucket/create) API endpoint. + + ### Sample request + + ```bash Request + curl --request POST \ + --url https://app.infisical.com/api/v1/secret-syncs/bitbucket \ + --header 'Content-Type: application/json' \ + --data '{ + "name": "my-bitbucket-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": { + "workspace": "...", + "repository": "..." + } + }' + ``` + + ### Sample response + + ```bash Response + { + "secretSync": { + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "name": "my-bitbucket-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": "bitbucket", + "name": "my-bitbucket-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": "bitbucket", + "destinationConfig": { + "workspace": "...", + "repository": "..." + } + } + } + ``` + + diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/BitbucketSyncFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/BitbucketSyncFields.tsx new file mode 100644 index 000000000..2cf5a541f --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/BitbucketSyncFields.tsx @@ -0,0 +1,140 @@ +import { Controller, useFormContext, useWatch } from "react-hook-form"; +import { SingleValue } from "react-select"; + +import { SecretSyncConnectionField } from "@app/components/secret-syncs/forms/SecretSyncConnectionField"; +import { FilterableSelect, FormControl } from "@app/components/v2"; +import { + TBitbucketEnvironment, + TBitbucketRepo, + TBitbucketWorkspace, + useBitbucketConnectionListEnvironments, + useBitbucketConnectionListRepositories, + useBitbucketConnectionListWorkspaces +} from "@app/hooks/api/appConnections/bitbucket"; +import { SecretSync } from "@app/hooks/api/secretSyncs"; + +import { TSecretSyncForm } from "../schemas"; + +export const BitbucketSyncFields = () => { + const { control, setValue } = useFormContext< + TSecretSyncForm & { destination: SecretSync.Bitbucket } + >(); + + const connectionId = useWatch({ name: "connection.id", control }); + const workspace = useWatch({ name: "destinationConfig.workspace", control }); + const repository = useWatch({ name: "destinationConfig.repository", control }); + + const { data: workspaces = [], isPending: isWorkspacesLoading } = + useBitbucketConnectionListWorkspaces(connectionId, { + enabled: Boolean(connectionId) + }); + + const { data: repositories = [], isPending: isRepositoriesLoading } = + useBitbucketConnectionListRepositories(connectionId, workspace ?? "", { + enabled: Boolean(connectionId) && Boolean(workspace) + }); + + const { data: environments = [], isPending: isEnvironmentsLoading } = + useBitbucketConnectionListEnvironments(connectionId, workspace ?? "", repository ?? "", { + enabled: Boolean(connectionId) && Boolean(workspace) && Boolean(repository) + }); + + return ( + <> + { + setValue("destinationConfig.workspace", ""); + setValue("destinationConfig.repository", ""); + setValue("destinationConfig.environment", ""); + }} + /> + + ( + + w.slug === value) ?? null} + onChange={(option) => { + const v = option as SingleValue; + onChange(v?.slug ?? ""); + // Clear downstream selections + setValue("destinationConfig.repository", ""); + setValue("destinationConfig.environment", ""); + }} + options={workspaces} + placeholder="Select workspace..." + getOptionLabel={(option) => option.slug} + getOptionValue={(option) => option.slug} + /> + + )} + /> + + ( + + r.slug === value) ?? null} + onChange={(option) => { + const v = option as SingleValue; + onChange(v?.slug ?? ""); + // Clear downstream selections + setValue("destinationConfig.environment", ""); + }} + options={repositories} + placeholder="Select repository..." + getOptionLabel={(option) => option.full_name} + getOptionValue={(option) => option.slug} + /> + + )} + /> + + ( + + e.uuid === value) ?? null} + onChange={(option) => { + const v = option as SingleValue; + onChange(v?.uuid ?? ""); + }} + options={environments} + placeholder="Select environment..." + getOptionLabel={(option) => option.name} + getOptionValue={(option) => option.uuid} + isClearable + /> + + )} + /> + + ); +}; diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx index 09cf2caec..c933fa44d 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx @@ -9,6 +9,7 @@ import { AwsSecretsManagerSyncFields } from "./AwsSecretsManagerSyncFields"; import { AzureAppConfigurationSyncFields } from "./AzureAppConfigurationSyncFields"; import { AzureDevOpsSyncFields } from "./AzureDevOpsSyncFields"; import { AzureKeyVaultSyncFields } from "./AzureKeyVaultSyncFields"; +import { BitbucketSyncFields } from "./BitbucketSyncFields"; import { CamundaSyncFields } from "./CamundaSyncFields"; import { ChecklySyncFields } from "./ChecklySyncFields"; import { CloudflarePagesSyncFields } from "./CloudflarePagesSyncFields"; @@ -91,6 +92,8 @@ export const SecretSyncDestinationFields = () => { return ; case SecretSync.Supabase: return ; + case SecretSync.Bitbucket: + return ; default: throw new Error(`Unhandled Destination Config Field: ${destination}`); } diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx index eaf66a053..e7226eb68 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx @@ -63,6 +63,7 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => { case SecretSync.Railway: case SecretSync.Checkly: case SecretSync.Supabase: + case SecretSync.Bitbucket: AdditionalSyncOptionsFieldsComponent = null; break; default: diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/BitBucketSyncReviewFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/BitBucketSyncReviewFields.tsx new file mode 100644 index 000000000..2925633cc --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/BitBucketSyncReviewFields.tsx @@ -0,0 +1,20 @@ +import { useFormContext } from "react-hook-form"; + +import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas"; +import { GenericFieldLabel } from "@app/components/v2"; +import { SecretSync } from "@app/hooks/api/secretSyncs"; + +export const BitBucketSyncReviewFields = () => { + const { watch } = useFormContext(); + const repository = watch("destinationConfig.repository"); + const environment = watch("destinationConfig.environment"); + const workspace = watch("destinationConfig.workspace"); + + return ( + <> + {repository} + {environment} + {workspace} + + ); +}; diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx index d09f58a2e..ee58c4262 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx @@ -18,6 +18,7 @@ import { import { AzureAppConfigurationSyncReviewFields } from "./AzureAppConfigurationSyncReviewFields"; import { AzureDevOpsSyncReviewFields } from "./AzureDevOpsSyncReviewFields"; import { AzureKeyVaultSyncReviewFields } from "./AzureKeyVaultSyncReviewFields"; +import { BitBucketSyncReviewFields } from "./BitBucketSyncReviewFields"; import { CamundaSyncReviewFields } from "./CamundaSyncReviewFields"; import { ChecklySyncReviewFields } from "./ChecklySyncReviewFields"; import { CloudflarePagesSyncReviewFields } from "./CloudflarePagesReviewFields"; @@ -144,6 +145,9 @@ export const SecretSyncReviewFields = () => { case SecretSync.Supabase: DestinationFieldsComponent = ; break; + case SecretSync.Bitbucket: + DestinationFieldsComponent = ; + break; default: throw new Error(`Unhandled Destination Review Fields: ${destination}`); } diff --git a/frontend/src/components/secret-syncs/forms/schemas/bitbucket-sync-destination-schema.ts b/frontend/src/components/secret-syncs/forms/schemas/bitbucket-sync-destination-schema.ts new file mode 100644 index 000000000..0f918d845 --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/schemas/bitbucket-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 BitbucketSyncDestinationSchema = BaseSecretSyncSchema().merge( + z.object({ + destination: z.literal(SecretSync.Bitbucket), + destinationConfig: z.object({ + repository: z.string().trim().describe("Repository Name"), + environment: z.string().trim().optional().describe("Environment Name"), + workspace: z.string().trim().describe("Workspace Name") + }) + }) +); 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 83c334f71..dc082e168 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 @@ -6,6 +6,7 @@ import { AwsSecretsManagerSyncDestinationSchema } from "./aws-secrets-manager-sy import { AzureAppConfigurationSyncDestinationSchema } from "./azure-app-configuration-sync-destination-schema"; import { AzureDevOpsSyncDestinationSchema } from "./azure-devops-sync-destination-schema"; import { AzureKeyVaultSyncDestinationSchema } from "./azure-key-vault-sync-destination-schema"; +import { BitbucketSyncDestinationSchema } from "./bitbucket-sync-destination-schema"; import { CamundaSyncDestinationSchema } from "./camunda-sync-destination-schema"; import { ChecklySyncDestinationSchema } from "./checkly-sync-destination-schema"; import { CloudflarePagesSyncDestinationSchema } from "./cloudflare-pages-sync-destination-schema"; @@ -55,7 +56,8 @@ const SecretSyncUnionSchema = z.discriminatedUnion("destination", [ SupabaseSyncDestinationSchema, ZabbixSyncDestinationSchema, RailwaySyncDestinationSchema, - ChecklySyncDestinationSchema + ChecklySyncDestinationSchema, + BitbucketSyncDestinationSchema ]); export const SecretSyncFormSchema = SecretSyncUnionSchema; diff --git a/frontend/src/helpers/secretSyncs.ts b/frontend/src/helpers/secretSyncs.ts index 29b4f2f73..cabd7fa6e 100644 --- a/frontend/src/helpers/secretSyncs.ts +++ b/frontend/src/helpers/secretSyncs.ts @@ -101,6 +101,10 @@ export const SECRET_SYNC_MAP: Record = { [SecretSync.Supabase]: AppConnection.Supabase, [SecretSync.Zabbix]: AppConnection.Zabbix, [SecretSync.Railway]: AppConnection.Railway, - [SecretSync.Checkly]: AppConnection.Checkly + [SecretSync.Checkly]: AppConnection.Checkly, + [SecretSync.Bitbucket]: AppConnection.Bitbucket }; export const SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP: Record< diff --git a/frontend/src/hooks/api/appConnections/bitbucket/queries.tsx b/frontend/src/hooks/api/appConnections/bitbucket/queries.tsx index bfae0534f..d4f85bcf7 100644 --- a/frontend/src/hooks/api/appConnections/bitbucket/queries.tsx +++ b/frontend/src/hooks/api/appConnections/bitbucket/queries.tsx @@ -4,8 +4,10 @@ import { apiRequest } from "@app/config/request"; import { appConnectionKeys } from "../queries"; import { + TBitbucketConnectionListEnvironmentsResponse, TBitbucketConnectionListRepositoriesResponse, TBitbucketConnectionListWorkspacesResponse, + TBitbucketEnvironment, TBitbucketRepo, TBitbucketWorkspace } from "./types"; @@ -15,7 +17,9 @@ const bitbucketConnectionKeys = { listRepos: (connectionId: string, workspaceSlug: string) => [...bitbucketConnectionKeys.all, "repos", connectionId, workspaceSlug] as const, listWorkspaces: (connectionId: string) => - [...bitbucketConnectionKeys.all, "workspaces", connectionId] as const + [...bitbucketConnectionKeys.all, "workspaces", connectionId] as const, + listEnvironments: (connectionId: string, workspaceSlug: string, repoSlug: string) => + [...bitbucketConnectionKeys.all, "environments", connectionId, workspaceSlug, repoSlug] as const }; export const useBitbucketConnectionListWorkspaces = ( @@ -68,3 +72,30 @@ export const useBitbucketConnectionListRepositories = ( ...options }); }; + +export const useBitbucketConnectionListEnvironments = ( + connectionId: string, + workspaceSlug: string, + repoSlug: string, + options?: Omit< + UseQueryOptions< + TBitbucketEnvironment[], + unknown, + TBitbucketEnvironment[], + ReturnType + >, + "queryKey" | "queryFn" + > +) => { + return useQuery({ + queryKey: bitbucketConnectionKeys.listEnvironments(connectionId, workspaceSlug, repoSlug), + queryFn: async () => { + const { data } = await apiRequest.get( + `/api/v1/app-connections/bitbucket/${connectionId}/environments?workspaceSlug=${encodeURIComponent(workspaceSlug)}&repositorySlug=${encodeURIComponent(repoSlug)}` + ); + + return data.environments; + }, + ...options + }); +}; diff --git a/frontend/src/hooks/api/appConnections/bitbucket/types.ts b/frontend/src/hooks/api/appConnections/bitbucket/types.ts index e7e653a93..79ebbc7cc 100644 --- a/frontend/src/hooks/api/appConnections/bitbucket/types.ts +++ b/frontend/src/hooks/api/appConnections/bitbucket/types.ts @@ -15,3 +15,13 @@ export type TBitbucketConnectionListWorkspacesResponse = { export type TBitbucketConnectionListRepositoriesResponse = { repositories: TBitbucketRepo[]; }; + +export type TBitbucketEnvironment = { + uuid: string; + name: string; + slug: string; +}; + +export type TBitbucketConnectionListEnvironmentsResponse = { + environments: TBitbucketEnvironment[]; +}; diff --git a/frontend/src/hooks/api/secretSyncs/enums.ts b/frontend/src/hooks/api/secretSyncs/enums.ts index dfba4bf4b..f29599a08 100644 --- a/frontend/src/hooks/api/secretSyncs/enums.ts +++ b/frontend/src/hooks/api/secretSyncs/enums.ts @@ -25,7 +25,8 @@ export enum SecretSync { Supabase = "supabase", Zabbix = "zabbix", Railway = "railway", - Checkly = "checkly" + Checkly = "checkly", + Bitbucket = "bitbucket" } export enum SecretSyncStatus { diff --git a/frontend/src/hooks/api/secretSyncs/types/bitbucket-sync.ts b/frontend/src/hooks/api/secretSyncs/types/bitbucket-sync.ts new file mode 100644 index 000000000..17bd2be05 --- /dev/null +++ b/frontend/src/hooks/api/secretSyncs/types/bitbucket-sync.ts @@ -0,0 +1,17 @@ +import { AppConnection } from "@app/hooks/api/appConnections/enums"; +import { SecretSync } from "@app/hooks/api/secretSyncs"; +import { TRootSecretSync } from "@app/hooks/api/secretSyncs/types/root-sync"; + +export type TBitbucketSync = TRootSecretSync & { + destination: SecretSync.Bitbucket; + destinationConfig: { + workspace: string; + repository: string; + environment?: string; + }; + connection: { + app: AppConnection.Bitbucket; + 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 df02872ad..daffac56a 100644 --- a/frontend/src/hooks/api/secretSyncs/types/index.ts +++ b/frontend/src/hooks/api/secretSyncs/types/index.ts @@ -8,6 +8,7 @@ import { TAwsSecretsManagerSync } from "./aws-secrets-manager-sync"; import { TAzureAppConfigurationSync } from "./azure-app-configuration-sync"; import { TAzureDevOpsSync } from "./azure-devops-sync"; import { TAzureKeyVaultSync } from "./azure-key-vault-sync"; +import { TBitbucketSync } from "./bitbucket-sync"; import { TCamundaSync } from "./camunda-sync"; import { TChecklySync } from "./checkly-sync"; import { TCloudflarePagesSync } from "./cloudflare-pages-sync"; @@ -63,7 +64,8 @@ export type TSecretSync = | TZabbixSync | TRailwaySync | TChecklySync - | TSupabaseSync; + | TSupabaseSync + | TBitbucketSync; export type TListSecretSyncs = { secretSyncs: TSecretSync[] }; diff --git a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/BitbucketSyncDestinationCol.tsx b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/BitbucketSyncDestinationCol.tsx new file mode 100644 index 000000000..80243daec --- /dev/null +++ b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/BitbucketSyncDestinationCol.tsx @@ -0,0 +1,14 @@ +import { TBitbucketSync } from "@app/hooks/api/secretSyncs/types/bitbucket-sync"; + +import { getSecretSyncDestinationColValues } from "../helpers"; +import { SecretSyncTableCell } from "../SecretSyncTableCell"; + +type Props = { + secretSync: TBitbucketSync; +}; + +export const BitbucketSyncDestinationCol = ({ secretSync }: Props) => { + const { primaryText, secondaryText } = getSecretSyncDestinationColValues(secretSync); + + return ; +}; diff --git a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/SecretSyncDestinationCol.tsx b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/SecretSyncDestinationCol.tsx index 0a41b2c4b..10ffa9e8c 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 @@ -6,6 +6,7 @@ import { AwsSecretsManagerSyncDestinationCol } from "./AwsSecretsManagerSyncDest import { AzureAppConfigurationDestinationSyncCol } from "./AzureAppConfigurationDestinationSyncCol"; import { AzureDevOpsSyncDestinationCol } from "./AzureDevOpsSyncDestinationCol"; import { AzureKeyVaultDestinationSyncCol } from "./AzureKeyVaultDestinationSyncCol"; +import { BitbucketSyncDestinationCol } from "./BitbucketSyncDestinationCol"; import { CamundaSyncDestinationCol } from "./CamundaSyncDestinationCol"; import { ChecklySyncDestinationCol } from "./ChecklySyncDestinationCol"; import { CloudflarePagesSyncDestinationCol } from "./CloudflarePagesSyncDestinationCol"; @@ -88,6 +89,8 @@ export const SecretSyncDestinationCol = ({ secretSync }: Props) => { return ; case SecretSync.Supabase: return ; + case SecretSync.Bitbucket: + return ; default: throw new Error( `Unhandled Secret Sync Destination Col: ${(secretSync as TSecretSync).destination}` diff --git a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/helpers/index.ts b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/helpers/index.ts index a4a7e5480..364efaaab 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 @@ -174,6 +174,10 @@ export const getSecretSyncDestinationColValues = (secretSync: TSecretSync) => { primaryText = destinationConfig.projectName; secondaryText = "Supabase Project"; break; + case SecretSync.Bitbucket: + primaryText = destinationConfig.workspace; + secondaryText = destinationConfig.repository; + break; default: throw new Error(`Unhandled Destination Col Values ${destination}`); } diff --git a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/BitbucketSyncDestinationSection.tsx b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/BitbucketSyncDestinationSection.tsx new file mode 100644 index 000000000..eb5378f2f --- /dev/null +++ b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/BitbucketSyncDestinationSection.tsx @@ -0,0 +1,22 @@ +import { GenericFieldLabel } from "@app/components/secret-syncs"; +import { TBitbucketSync } from "@app/hooks/api/secretSyncs/types/bitbucket-sync"; + +type Props = { + secretSync: TBitbucketSync; +}; + +export const BitbucketSyncDestinationSection = ({ secretSync }: Props) => { + const { + destinationConfig: { workspace, repository, environment } + } = secretSync; + + return ( + <> + {workspace} + {repository} + {environment && ( + {environment} + )} + + ); +}; 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 e7230e815..44220c200 100644 --- a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/SecretSyncDestinatonSection.tsx +++ b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/SecretSyncDestinatonSection.tsx @@ -17,6 +17,7 @@ import { AwsSecretsManagerSyncDestinationSection } from "./AwsSecretsManagerSync import { AzureAppConfigurationSyncDestinationSection } from "./AzureAppConfigurationSyncDestinationSection"; import { AzureDevOpsSyncDestinationSection } from "./AzureDevOpsSyncDestinationSection"; import { AzureKeyVaultSyncDestinationSection } from "./AzureKeyVaultSyncDestinationSection"; +import { BitbucketSyncDestinationSection } from "./BitbucketSyncDestinationSection"; import { CamundaSyncDestinationSection } from "./CamundaSyncDestinationSection"; import { ChecklySyncDestinationSection } from "./ChecklySyncDestinationSection"; import { CloudflarePagesSyncDestinationSection } from "./CloudflarePagesSyncDestinationSection"; @@ -134,6 +135,9 @@ export const SecretSyncDestinationSection = ({ secretSync, onEditDestination }: case SecretSync.Supabase: DestinationComponents = ; break; + case SecretSync.Bitbucket: + DestinationComponents = ; + break; default: throw new Error(`Unhandled Destination Section components: ${destination}`); } diff --git a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/SecretSyncOptionsSection.tsx b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/SecretSyncOptionsSection.tsx index 3cc70ebcb..eb366952d 100644 --- a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/SecretSyncOptionsSection.tsx +++ b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/SecretSyncOptionsSection.tsx @@ -65,6 +65,7 @@ export const SecretSyncOptionsSection = ({ secretSync, onEditOptions }: Props) = case SecretSync.Railway: case SecretSync.Supabase: case SecretSync.Checkly: + case SecretSync.Bitbucket: AdditionalSyncOptionsComponent = null; break; default: