mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-19 13:23:23 +00:00
Add Bitbucket Secret Sync
This commit is contained in:
@@ -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
|
||||
};
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -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)
|
||||
});
|
||||
@@ -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<typeof BitbucketSyncSchema>;
|
||||
|
||||
export type TBitbucketSyncInput = z.infer<typeof CreateBitbucketSyncSchema>;
|
||||
|
||||
export type TBitbucketSyncListItem = z.infer<typeof BitbucketSyncListItemSchema>;
|
||||
|
||||
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;
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from "./bitbucket-sync-constants";
|
||||
export * from "./bitbucket-sync-fns";
|
||||
export * from "./bitbucket-sync-schemas";
|
||||
export * from "./bitbucket-sync-types";
|
||||
Reference in New Issue
Block a user