From ce877cd352174dbd64f665e8f34e60ce781b2b2c Mon Sep 17 00:00:00 2001 From: Carlos Monastyrski Date: Mon, 21 Jul 2025 11:01:22 -0300 Subject: [PATCH] Addressed PR suggestions --- .../bitbucket/bitbucket-connection-types.ts | 2 +- .../bitbucket/bitbucket-sync-fns.ts | 345 ++++++------------ docs/integrations/secret-syncs/bitbucket.mdx | 8 +- ...elds.tsx => BitbucketSyncReviewFields.tsx} | 2 +- .../SecretSyncReviewFields.tsx | 4 +- .../bitbucket-sync-destination-schema.ts | 6 +- 6 files changed, 126 insertions(+), 241 deletions(-) rename frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/{BitBucketSyncReviewFields.tsx => BitbucketSyncReviewFields.tsx} (94%) 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 7311e7dbf..40af42321 100644 --- a/backend/src/services/app-connection/bitbucket/bitbucket-connection-types.ts +++ b/backend/src/services/app-connection/bitbucket/bitbucket-connection-types.ts @@ -51,7 +51,7 @@ export type TBitbucketEnvironment = { name: string; }; -export type BitbucketEnvironmentsResponse = { +export type TBitbucketEnvironmentsResponse = { values: TBitbucketEnvironment[]; next?: string; }; diff --git a/backend/src/services/secret-sync/bitbucket/bitbucket-sync-fns.ts b/backend/src/services/secret-sync/bitbucket/bitbucket-sync-fns.ts index 6c8e78179..d0a0df7a5 100644 --- a/backend/src/services/secret-sync/bitbucket/bitbucket-sync-fns.ts +++ b/backend/src/services/secret-sync/bitbucket/bitbucket-sync-fns.ts @@ -11,145 +11,85 @@ 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 createAuthHeader = (email: string, apiToken: string): string => { + return `Basic ${Buffer.from(`${email}:${apiToken}`).toString("base64")}`; }; -const listBitbucketEnvironmentSecrets = async ({ +const buildVariablesUrl = (workspace: string, repository: string, environment?: string, uuid?: string): string => { + const baseUrl = `${IntegrationUrls.BITBUCKET_API_URL}/2.0/repositories/${encodeURIComponent(workspace)}/${encodeURIComponent(repository)}`; + + if (environment) { + return `${baseUrl}/deployments_config/environments/${environment}/variables${uuid ? `/${uuid}` : ""}`; + } + + return `${baseUrl}/pipelines_config/variables/${uuid || ""}`; +}; + +const listVariables = 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" - } +}: TBitbucketListVariables & { environment?: string }): Promise => { + const url = buildVariablesUrl(workspace, repository, environment); + const authHeader = createAuthHeader(email, apiToken); + + const { data } = await request.get<{ values: TBitbucketVariable[] }>(url, { + headers: { + Authorization: authHeader, + Accept: "application/json" } - ); + }); return data.values; }; -// Helper function to upsert a single variable -const upsertBitbucketVariable = async ({ +const upsertVariable = async ({ email, apiToken, workspace, repository, + environment, key, value, - existingVariables, - isEnvironment = false, - environment + existingVariables }: { email: string; apiToken: string; workspace: string; repository: string; + environment?: 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")}`; + const authHeader = createAuthHeader(email, apiToken); + const requestData = { key, value, secured: true }; + const headers = { + Authorization: authHeader, + "Content-Type": "application/json" + }; 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 url = buildVariablesUrl(workspace, repository, environment, existingVariable.uuid); + return request.put(url, requestData, { headers }); } - 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 url = buildVariablesUrl(workspace, repository, environment); + return request.post(url, requestData, { headers }); }; -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 ({ +const putVariables = async ({ email, apiToken, workspace, repository, environment, secretMap -}: TPutBitbucketVariable & { environment: string; secretMap: TSecretMap }) => { - // Get existing variables first - const existingVariables = await listBitbucketEnvironmentSecrets({ +}: TPutBitbucketVariable & { environment?: string; secretMap: TSecretMap }) => { + const existingVariables = await listVariables({ email, apiToken, workspace, @@ -157,69 +97,46 @@ const putBitbucketEnvironmentSecrets = async ({ environment }); - const promises = Object.entries(secretMap).map(([key, { value }]) => { - return upsertBitbucketVariable({ + const promises = Object.entries(secretMap).map(([key, { value }]) => + upsertVariable({ email, apiToken, workspace, repository, + environment, key, value, - existingVariables, - isEnvironment: true, - environment - }); - }); + existingVariables + }) + ); 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 ({ +const deleteVariables = async ({ email, apiToken, workspace, repository, environment, keys -}: TDeleteBitbucketVariable & { environment: string }) => { - // Get the variable UUIDs since Bitbucket requires UUIDs for deletion - const existingVariables = await listBitbucketEnvironmentSecrets({ +}: TDeleteBitbucketVariable & { environment?: string }) => { + const existingVariables = await listVariables({ email, apiToken, workspace, repository, environment }); + const variablesToDelete = existingVariables.filter((variable) => keys.includes(variable.key)); + const authHeader = createAuthHeader(email, apiToken); 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")}` - } - } - ); + const url = buildVariablesUrl(workspace, repository, environment, variable.uuid); + return request.delete(url, { + headers: { Authorization: authHeader } + }); }); return Promise.all(promises); @@ -236,67 +153,51 @@ export const BitbucketSyncFns = { const { email, apiToken } = connection.credentials; try { - // If environment is specified in destinationConfig, use environment variables - if (configEnvironment) { - await putBitbucketEnvironmentSecrets({ + await putVariables({ + email, + apiToken, + workspace, + repository, + environment: configEnvironment, + secretMap + }); + } catch (error) { + throw new SecretSyncError({ error }); + } + + if (secretSync.syncOptions.disableSecretDeletion) return; + + try { + const existingVariables = await listVariables({ + email, + apiToken, + workspace, + repository, + environment: configEnvironment + }); + + const keysToDelete = existingVariables + .map((variable) => variable.key) + .filter( + (secret) => + matchesSchema(secret, environment?.slug || "", secretSync.syncOptions.keySchema) && !(secret in secretMap) + ); + + if (keysToDelete.length > 0) { + await deleteVariables({ email, apiToken, workspace, repository, environment: configEnvironment, - secretMap + keys: keysToDelete }); - } 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 - }); - } + throw new SecretSyncError({ error }); } }, + removeSecrets: async (secretSync: TBitbucketSyncWithCredentials, secretMap: TSecretMap) => { const { connection, @@ -305,39 +206,32 @@ export const BitbucketSyncFns = { const { email, apiToken } = connection.credentials; - const existingVariables = configEnvironment - ? await listBitbucketEnvironmentSecrets({ + try { + const existingVariables = await listVariables({ + email, + apiToken, + workspace, + repository, + environment: configEnvironment + }); + + const keysToRemove = existingVariables.map((variable) => variable.key).filter((secret) => secret in secretMap); + + if (keysToRemove.length > 0) { + await deleteVariables({ 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 + environment: configEnvironment, + keys: keysToRemove }); } + } catch (error) { + throw new SecretSyncError({ error }); } }, + getSecrets: async (secretSync: TBitbucketSyncWithCredentials) => { const { connection, @@ -347,23 +241,16 @@ export const BitbucketSyncFns = { 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 variables = await listVariables({ + email, + apiToken, + workspace, + repository, + environment }); const secretMap: TSecretMap = {}; - data.values.forEach((variable) => { + variables.forEach((variable) => { secretMap[variable.key] = { value: variable.secured ? "[SECURED]" : variable.value || "", comment: "" @@ -372,9 +259,7 @@ export const BitbucketSyncFns = { return secretMap; } catch (error) { - throw new SecretSyncError({ - error - }); + throw new SecretSyncError({ error }); } } }; diff --git a/docs/integrations/secret-syncs/bitbucket.mdx b/docs/integrations/secret-syncs/bitbucket.mdx index 21f262bdd..f05f5d822 100644 --- a/docs/integrations/secret-syncs/bitbucket.mdx +++ b/docs/integrations/secret-syncs/bitbucket.mdx @@ -96,8 +96,8 @@ description: "Learn how to configure a Bitbucket Sync for Infisical." "initialSyncBehavior": "overwrite-destination" }, "destinationConfig": { - "workspace": "...", - "repository": "..." + "workspace": "my-bitbucket-workspace", + "repository": "my-bitbucket-repository" } }' ``` @@ -148,8 +148,8 @@ description: "Learn how to configure a Bitbucket Sync for Infisical." }, "destination": "bitbucket", "destinationConfig": { - "workspace": "...", - "repository": "..." + "workspace": "my-bitbucket-workspace", + "repository": "my-bitbucket-repository" } } } diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/BitBucketSyncReviewFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/BitbucketSyncReviewFields.tsx similarity index 94% rename from frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/BitBucketSyncReviewFields.tsx rename to frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/BitbucketSyncReviewFields.tsx index 2925633cc..b45def375 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/BitBucketSyncReviewFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/BitbucketSyncReviewFields.tsx @@ -4,7 +4,7 @@ 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 = () => { +export const BitbucketSyncReviewFields = () => { const { watch } = useFormContext(); const repository = watch("destinationConfig.repository"); const environment = watch("destinationConfig.environment"); diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx index ee58c4262..d785dc87c 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx @@ -18,7 +18,7 @@ import { import { AzureAppConfigurationSyncReviewFields } from "./AzureAppConfigurationSyncReviewFields"; import { AzureDevOpsSyncReviewFields } from "./AzureDevOpsSyncReviewFields"; import { AzureKeyVaultSyncReviewFields } from "./AzureKeyVaultSyncReviewFields"; -import { BitBucketSyncReviewFields } from "./BitBucketSyncReviewFields"; +import { BitbucketSyncReviewFields } from "./BitbucketSyncReviewFields"; import { CamundaSyncReviewFields } from "./CamundaSyncReviewFields"; import { ChecklySyncReviewFields } from "./ChecklySyncReviewFields"; import { CloudflarePagesSyncReviewFields } from "./CloudflarePagesReviewFields"; @@ -146,7 +146,7 @@ export const SecretSyncReviewFields = () => { DestinationFieldsComponent = ; break; case SecretSync.Bitbucket: - DestinationFieldsComponent = ; + 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 index 0f918d845..352e36f24 100644 --- 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 @@ -7,9 +7,9 @@ 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") + repository: z.string().trim().min(1, "Repository slug required").describe("Repository slug"), + environment: z.string().trim().optional().describe("Deployment environment uuid"), + workspace: z.string().trim().min(1, "Workspace slug required").describe("Workspace slug") }) }) );