feat(integration): add integration with BitBucket

This commit is contained in:
Kee, Zhen Wei
2023-07-16 22:04:51 +08:00
parent 9b96daa185
commit df7ad9e645
26 changed files with 794 additions and 36 deletions
+2
View File
@@ -47,11 +47,13 @@ CLIENT_ID_VERCEL=
CLIENT_ID_NETLIFY= CLIENT_ID_NETLIFY=
CLIENT_ID_GITHUB= CLIENT_ID_GITHUB=
CLIENT_ID_GITLAB= CLIENT_ID_GITLAB=
CLIENT_ID_BITBUCKET=
CLIENT_SECRET_HEROKU= CLIENT_SECRET_HEROKU=
CLIENT_SECRET_VERCEL= CLIENT_SECRET_VERCEL=
CLIENT_SECRET_NETLIFY= CLIENT_SECRET_NETLIFY=
CLIENT_SECRET_GITHUB= CLIENT_SECRET_GITHUB=
CLIENT_SECRET_GITLAB= CLIENT_SECRET_GITLAB=
CLIENT_SECRET_BITBUCKET=
CLIENT_SLUG_VERCEL= CLIENT_SLUG_VERCEL=
# Sentry (optional) for monitoring errors # Sentry (optional) for monitoring errors
+2
View File
@@ -37,6 +37,7 @@ export const getClientIdNetlify = async () => (await client.getSecret("CLIENT_ID
export const getClientIdGitHub = async () => (await client.getSecret("CLIENT_ID_GITHUB")).secretValue; export const getClientIdGitHub = async () => (await client.getSecret("CLIENT_ID_GITHUB")).secretValue;
export const getClientIdGitLab = async () => (await client.getSecret("CLIENT_ID_GITLAB")).secretValue; export const getClientIdGitLab = async () => (await client.getSecret("CLIENT_ID_GITLAB")).secretValue;
export const getClientIdGoogle = async () => (await client.getSecret("CLIENT_ID_GOOGLE")).secretValue; export const getClientIdGoogle = async () => (await client.getSecret("CLIENT_ID_GOOGLE")).secretValue;
export const getClientIdBitBucket = async () => (await client.getSecret("CLIENT_ID_BITBUCKET")).secretValue;
export const getClientSecretAzure = async () => (await client.getSecret("CLIENT_SECRET_AZURE")).secretValue; export const getClientSecretAzure = async () => (await client.getSecret("CLIENT_SECRET_AZURE")).secretValue;
export const getClientSecretHeroku = async () => (await client.getSecret("CLIENT_SECRET_HEROKU")).secretValue; export const getClientSecretHeroku = async () => (await client.getSecret("CLIENT_SECRET_HEROKU")).secretValue;
export const getClientSecretVercel = async () => (await client.getSecret("CLIENT_SECRET_VERCEL")).secretValue; export const getClientSecretVercel = async () => (await client.getSecret("CLIENT_SECRET_VERCEL")).secretValue;
@@ -44,6 +45,7 @@ export const getClientSecretNetlify = async () => (await client.getSecret("CLIEN
export const getClientSecretGitHub = async () => (await client.getSecret("CLIENT_SECRET_GITHUB")).secretValue; export const getClientSecretGitHub = async () => (await client.getSecret("CLIENT_SECRET_GITHUB")).secretValue;
export const getClientSecretGitLab = async () => (await client.getSecret("CLIENT_SECRET_GITLAB")).secretValue; export const getClientSecretGitLab = async () => (await client.getSecret("CLIENT_SECRET_GITLAB")).secretValue;
export const getClientSecretGoogle = async () => (await client.getSecret("CLIENT_SECRET_GOOGLE")).secretValue; export const getClientSecretGoogle = async () => (await client.getSecret("CLIENT_SECRET_GOOGLE")).secretValue;
export const getClientSecretBitBucket = async () => (await client.getSecret("CLIENT_SECRET_BITBUCKET")).secretValue;
export const getClientSlugVercel = async () => (await client.getSecret("CLIENT_SLUG_VERCEL")).secretValue; export const getClientSlugVercel = async () => (await client.getSecret("CLIENT_SLUG_VERCEL")).secretValue;
export const getPostHogHost = async () => (await client.getSecret("POSTHOG_HOST")).secretValue || "https://app.posthog.com"; export const getPostHogHost = async () => (await client.getSecret("POSTHOG_HOST")).secretValue || "https://app.posthog.com";
export const getPostHogProjectApiKey = async () => (await client.getSecret("POSTHOG_PROJECT_API_KEY")).secretValue || "phc_nSin8j5q2zdhpFDI1ETmFNUIuTG4DwKVyIigrY10XiE"; export const getPostHogProjectApiKey = async () => (await client.getSecret("POSTHOG_PROJECT_API_KEY")).secretValue || "phc_nSin8j5q2zdhpFDI1ETmFNUIuTG4DwKVyIigrY10XiE";
@@ -7,6 +7,7 @@ import { IntegrationService } from "../../services";
import { import {
ALGORITHM_AES_256_GCM, ALGORITHM_AES_256_GCM,
ENCODING_SCHEME_UTF8, ENCODING_SCHEME_UTF8,
INTEGRATION_BITBUCKET_API_URL,
INTEGRATION_RAILWAY_API_URL, INTEGRATION_RAILWAY_API_URL,
INTEGRATION_SET, INTEGRATION_SET,
INTEGRATION_VERCEL_API_URL, INTEGRATION_VERCEL_API_URL,
@@ -141,12 +142,14 @@ export const saveIntegrationAccessToken = async (req: Request, res: Response) =>
*/ */
export const getIntegrationAuthApps = async (req: Request, res: Response) => { export const getIntegrationAuthApps = async (req: Request, res: Response) => {
const teamId = req.query.teamId as string; const teamId = req.query.teamId as string;
const workspaceSlug = req.query.workspaceSlug as string;
const apps = await getApps({ const apps = await getApps({
integrationAuth: req.integrationAuth, integrationAuth: req.integrationAuth,
accessToken: req.accessToken, accessToken: req.accessToken,
accessId: req.accessId, accessId: req.accessId,
...(teamId && { teamId }) ...(teamId && { teamId }),
...(workspaceSlug && { workspaceSlug })
}); });
return res.status(200).send({ return res.status(200).send({
@@ -382,6 +385,66 @@ export const getIntegrationAuthRailwayServices = async (req: Request, res: Respo
}); });
}; };
/**
* Return list of workspaces allowed for integration with integration authorization id [integrationAuthId]
* @param req
* @param res
* @returns
*/
export const getIntegrationAuthBitBucketWorkspaces = async (req: Request, res: Response) => {
interface WorkspaceResponse {
size: number;
page: number;
pageLen: number;
next: string;
previous: string;
values: Array<Workspace>;
}
interface Workspace {
type: string;
uuid: string;
name: string;
slug: string;
is_private: boolean;
created_on: string;
updated_on: string;
}
const workspaces: Workspace[] = [];
let hasNextPage = true;
let workspaceUrl = `${INTEGRATION_BITBUCKET_API_URL}/2.0/workspaces`
while (hasNextPage) {
const { data }: { data: WorkspaceResponse } = await standardRequest.get(
workspaceUrl,
{
headers: {
Authorization: `Bearer ${req.accessToken}`,
"Accept-Encoding": "application/json"
}
}
);
if (data?.values.length > 0) {
data.values.forEach((workspace) => {
workspaces.push(workspace)
})
}
if (data.next) {
workspaceUrl = data.next
} else {
hasNextPage = false
}
}
return res.status(200).send({
workspaces
});
};
/** /**
* Delete integration authorization with id [integrationAuthId] * Delete integration authorization with id [integrationAuthId]
* @param req * @param req
+83
View File
@@ -5,6 +5,8 @@ import {
INTEGRATION_AWS_PARAMETER_STORE, INTEGRATION_AWS_PARAMETER_STORE,
INTEGRATION_AWS_SECRET_MANAGER, INTEGRATION_AWS_SECRET_MANAGER,
INTEGRATION_AZURE_KEY_VAULT, INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_BITBUCKET,
INTEGRATION_BITBUCKET_API_URL,
INTEGRATION_CHECKLY, INTEGRATION_CHECKLY,
INTEGRATION_CHECKLY_API_URL, INTEGRATION_CHECKLY_API_URL,
INTEGRATION_CIRCLECI, INTEGRATION_CIRCLECI,
@@ -54,11 +56,13 @@ const getApps = async ({
accessToken, accessToken,
accessId, accessId,
teamId, teamId,
workspaceSlug,
}: { }: {
integrationAuth: IIntegrationAuth; integrationAuth: IIntegrationAuth;
accessToken: string; accessToken: string;
accessId?: string; accessId?: string;
teamId?: string; teamId?: string;
workspaceSlug?: string;
}) => { }) => {
let apps: App[] = []; let apps: App[] = [];
switch (integrationAuth.integration) { switch (integrationAuth.integration) {
@@ -145,6 +149,11 @@ const getApps = async ({
accountId: accessId accountId: accessId
}) })
break; break;
case INTEGRATION_BITBUCKET:
apps = await getAppsBitBucket({
accessToken,
workspaceSlug
})
} }
return apps; return apps;
@@ -721,4 +730,78 @@ const getAppsCloudflarePages = async ({
return apps; return apps;
} }
/**
* Return list of repositories for the BitBucket integration based on provided BitBucket workspace
* @param {Object} obj
* @param {String} obj.accessToken - access token for BitBucket API
* @param {String} obj.workspaceSlug - Workspace identifier for fetching BitBucket repositories
* @returns {Object[]} apps - BitBucket repositories
* @returns {String} apps.name - name of BitBucket repository
*/
const getAppsBitBucket = async ({
accessToken,
workspaceSlug,
}: {
accessToken: string;
workspaceSlug?: string;
}) => {
interface RepositoriesResponse {
size: number;
page: number;
pageLen: number;
next: string;
previous: string;
values: Array<Repository>;
}
interface Repository {
type: string;
uuid: string;
name: string;
is_private: boolean;
created_on: string;
updated_on: string;
}
if (!workspaceSlug) {
return []
}
const repositories: Repository[] = [];
let hasNextPage = true;
let repositoriesUrl = `${INTEGRATION_BITBUCKET_API_URL}/2.0/repositories/${workspaceSlug}`
while (hasNextPage) {
const { data }: { data: RepositoriesResponse } = await standardRequest.get(
repositoriesUrl,
{
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept": "application/json",
},
}
);
if (data?.values.length > 0) {
data.values.forEach((repository) => {
repositories.push(repository)
})
}
if (data.next) {
repositoriesUrl = data.next
} else {
hasNextPage = false
}
}
const apps = repositories.map((repository) => {
return {
name: repository.name,
appId: repository.uuid,
};
});
return apps;
}
export { getApps }; export { getApps };
+57
View File
@@ -2,6 +2,8 @@ import { standardRequest } from "../config/request";
import { import {
INTEGRATION_AZURE_KEY_VAULT, INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_AZURE_TOKEN_URL, INTEGRATION_AZURE_TOKEN_URL,
INTEGRATION_BITBUCKET,
INTEGRATION_BITBUCKET_TOKEN_URL,
INTEGRATION_GITHUB, INTEGRATION_GITHUB,
INTEGRATION_GITHUB_TOKEN_URL, INTEGRATION_GITHUB_TOKEN_URL,
INTEGRATION_GITLAB, INTEGRATION_GITLAB,
@@ -15,11 +17,13 @@ import {
} from "../variables"; } from "../variables";
import { import {
getClientIdAzure, getClientIdAzure,
getClientIdBitBucket,
getClientIdGitHub, getClientIdGitHub,
getClientIdGitLab, getClientIdGitLab,
getClientIdNetlify, getClientIdNetlify,
getClientIdVercel, getClientIdVercel,
getClientSecretAzure, getClientSecretAzure,
getClientSecretBitBucket,
getClientSecretGitHub, getClientSecretGitHub,
getClientSecretGitLab, getClientSecretGitLab,
getClientSecretHeroku, getClientSecretHeroku,
@@ -78,6 +82,15 @@ interface ExchangeCodeGitlabResponse {
created_at: number; created_at: number;
} }
interface ExchangeCodeBitBucketResponse {
access_token: string;
token_type: string;
expires_in: number;
refresh_token: string;
scopes: string;
state: string;
}
/** /**
* Return [accessToken], [accessExpiresAt], and [refreshToken] for OAuth2 * Return [accessToken], [accessExpiresAt], and [refreshToken] for OAuth2
* code-token exchange for integration named [integration] * code-token exchange for integration named [integration]
@@ -129,6 +142,11 @@ const exchangeCode = async ({
obj = await exchangeCodeGitlab({ obj = await exchangeCodeGitlab({
code, code,
}); });
break;
case INTEGRATION_BITBUCKET:
obj = await exchangeCodeBitBucket({
code,
})
} }
return obj; return obj;
@@ -347,4 +365,43 @@ const exchangeCodeGitlab = async ({ code }: { code: string }) => {
}; };
}; };
/**
* Return [accessToken], [accessExpiresAt], and [refreshToken] for BitBucket
* code-token exchange
* @param {Object} obj1
* @param {Object} obj1.code - code for code-token exchange
* @returns {Object} obj2
* @returns {String} obj2.accessToken - access token for BitBucket API
* @returns {String} obj2.refreshToken - refresh token for BitBucket API
* @returns {Date} obj2.accessExpiresAt - date of expiration for access token
*/
const exchangeCodeBitBucket = async ({ code }: { code: string }) => {
const accessExpiresAt = new Date();
const res: ExchangeCodeBitBucketResponse = (
await standardRequest.post(
INTEGRATION_BITBUCKET_TOKEN_URL,
new URLSearchParams({
grant_type: "authorization_code",
code: code,
client_id: await getClientIdBitBucket(),
client_secret: await getClientSecretBitBucket(),
redirect_uri: `${await getSiteURL()}/integrations/bitbucket/oauth2/callback`,
} as any),
{
headers: {
"Accept-Encoding": "application/json",
},
}
)
).data;
accessExpiresAt.setSeconds(accessExpiresAt.getSeconds() + res.expires_in);
return {
accessToken: res.access_token,
refreshToken: res.refresh_token,
accessExpiresAt,
};
};
export { exchangeCode }; export { exchangeCode };
+60
View File
@@ -2,6 +2,8 @@ import { standardRequest } from "../config/request";
import { IIntegrationAuth } from "../models"; import { IIntegrationAuth } from "../models";
import { import {
INTEGRATION_AZURE_KEY_VAULT, INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_BITBUCKET,
INTEGRATION_BITBUCKET_TOKEN_URL,
INTEGRATION_GITLAB, INTEGRATION_GITLAB,
INTEGRATION_HEROKU, INTEGRATION_HEROKU,
} from "../variables"; } from "../variables";
@@ -13,8 +15,10 @@ import {
import { IntegrationService } from "../services"; import { IntegrationService } from "../services";
import { import {
getClientIdAzure, getClientIdAzure,
getClientIdBitBucket,
getClientIdGitLab, getClientIdGitLab,
getClientSecretAzure, getClientSecretAzure,
getClientSecretBitBucket,
getClientSecretGitLab, getClientSecretGitLab,
getClientSecretHeroku, getClientSecretHeroku,
getSiteURL, getSiteURL,
@@ -46,6 +50,15 @@ interface RefreshTokenGitLabResponse {
created_at: number; created_at: number;
} }
interface RefreshTokenBitBucketResponse {
access_token: string;
token_type: string;
expires_in: number;
refresh_token: string;
scopes: string;
state: string;
}
/** /**
* Return new access token by exchanging refresh token [refreshToken] for integration * Return new access token by exchanging refresh token [refreshToken] for integration
* named [integration] * named [integration]
@@ -83,6 +96,11 @@ const exchangeRefresh = async ({
refreshToken, refreshToken,
}); });
break; break;
case INTEGRATION_BITBUCKET:
tokenDetails = await exchangeRefreshBitBucket({
refreshToken,
});
break;
default: default:
throw new Error("Failed to exchange token for incompatible integration"); throw new Error("Failed to exchange token for incompatible integration");
} }
@@ -218,4 +236,46 @@ const exchangeRefreshGitLab = async ({
}; };
}; };
/**
* Return new access token by exchanging refresh token [refreshToken] for the
* BitBucket integration
* @param {Object} obj
* @param {String} obj.refreshToken - refresh token to use to get new access token for BitBucket
* @returns
*/
const exchangeRefreshBitBucket = async ({
refreshToken,
}: {
refreshToken: string;
}) => {
const accessExpiresAt = new Date();
const {
data,
}: {
data: RefreshTokenBitBucketResponse;
} = await standardRequest.post(
INTEGRATION_BITBUCKET_TOKEN_URL,
new URLSearchParams({
grant_type: "refresh_token",
refresh_token: refreshToken,
client_id: await getClientIdBitBucket(),
client_secret: await getClientSecretBitBucket(),
redirect_uri: `${await getSiteURL()}/integrations/bitbucket/oauth2/callback`,
} as any),
{
headers: {
"Accept-Encoding": "application/json",
},
}
);
accessExpiresAt.setSeconds(accessExpiresAt.getSeconds() + data.expires_in);
return {
accessToken: data.access_token,
refreshToken: data.refresh_token,
accessExpiresAt,
};
};
export { exchangeRefresh }; export { exchangeRefresh };
+128 -1
View File
@@ -14,6 +14,8 @@ import {
INTEGRATION_AWS_PARAMETER_STORE, INTEGRATION_AWS_PARAMETER_STORE,
INTEGRATION_AWS_SECRET_MANAGER, INTEGRATION_AWS_SECRET_MANAGER,
INTEGRATION_AZURE_KEY_VAULT, INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_BITBUCKET,
INTEGRATION_BITBUCKET_API_URL,
INTEGRATION_CHECKLY, INTEGRATION_CHECKLY,
INTEGRATION_CHECKLY_API_URL, INTEGRATION_CHECKLY_API_URL,
INTEGRATION_CIRCLECI, INTEGRATION_CIRCLECI,
@@ -202,7 +204,14 @@ const syncSecrets = async ({
accessToken accessToken
}); });
break; break;
} case INTEGRATION_BITBUCKET:
await syncSecretsBitBucket({
integration,
secrets,
accessToken,
});
break;
}
}; };
/** /**
@@ -1937,4 +1946,122 @@ const syncSecretsCloudflarePages = async ({
); );
} }
/**
* Sync/push [secrets] to BitBucket repo with name [integration.app]
* @param {Object} obj
* @param {IIntegration} obj.integration - integration details
* @param {IIntegrationAuth} obj.integrationAuth - integration auth details
* @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values)
* @param {String} obj.accessToken - access token for BitBucket integration
*/
const syncSecretsBitBucket = async ({
integration,
secrets,
accessToken,
}: {
integration: IIntegration;
secrets: any;
accessToken: string;
}) => {
interface VariablesResponse {
size: number;
page: number;
pageLen: number;
next: string;
previous: string;
values: Array<Variable>;
}
interface Variable {
type: string;
uuid: string;
key: string;
value: string;
secured: boolean;
}
const existingSecrets: Variable[] = [];
const workspaceSlug = integration.targetEnvironmentId
const repoSlug = integration.appId
let hasNextPage = true;
let variablesUrl = `${INTEGRATION_BITBUCKET_API_URL}/2.0/repositories/${workspaceSlug}/${repoSlug}/pipelines_config/variables`
// Fetch all repository variables
while (hasNextPage) {
const { data }: { data: VariablesResponse } = await standardRequest.get(
variablesUrl,
{
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept": "application/json",
},
}
);
if (data?.values.length > 0) {
data.values.forEach((variable) => {
existingSecrets.push(variable)
})
}
if (data.next) {
variablesUrl = data.next
} else {
hasNextPage = false
}
}
Object.keys(secrets).forEach(async (key) => {
const existingSecret = existingSecrets.find((secret) => secret.key.toUpperCase() === key.toUpperCase());
if (existingSecret) {
// Update existing secrets
await standardRequest.put(
`${variablesUrl}/${existingSecret.uuid}`,
{
key,
value: secrets[key],
secured: true
},
{
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept": "application/json",
},
}
);
} else {
// Create new secrets
await standardRequest.post(
variablesUrl,
{
key,
value: secrets[key],
secured: true
},
{
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept": "application/json",
},
}
);
}
})
// Delete secrets
existingSecrets.forEach(async (existingSecret) => {
if (!(existingSecret.key in secrets) && existingSecret.secured) {
await standardRequest.delete(
`${variablesUrl}/${existingSecret.uuid}`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept": "application/json",
},
}
);
}
})
}
export { syncSecrets }; export { syncSecrets };
+4 -1
View File
@@ -3,6 +3,7 @@ import {
INTEGRATION_AWS_PARAMETER_STORE, INTEGRATION_AWS_PARAMETER_STORE,
INTEGRATION_AWS_SECRET_MANAGER, INTEGRATION_AWS_SECRET_MANAGER,
INTEGRATION_AZURE_KEY_VAULT, INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_BITBUCKET,
INTEGRATION_CHECKLY, INTEGRATION_CHECKLY,
INTEGRATION_CIRCLECI, INTEGRATION_CIRCLECI,
INTEGRATION_CLOUDFLARE_PAGES, INTEGRATION_CLOUDFLARE_PAGES,
@@ -54,7 +55,8 @@ export interface IIntegration {
| "supabase" | "supabase"
| "checkly" | "checkly"
| "hashicorp-vault" | "hashicorp-vault"
| "cloudflare-pages"; | "cloudflare-pages"
| "bitbucket";
integrationAuth: Types.ObjectId; integrationAuth: Types.ObjectId;
} }
@@ -144,6 +146,7 @@ const integrationSchema = new Schema<IIntegration>(
INTEGRATION_CHECKLY, INTEGRATION_CHECKLY,
INTEGRATION_HASHICORP_VAULT, INTEGRATION_HASHICORP_VAULT,
INTEGRATION_CLOUDFLARE_PAGES, INTEGRATION_CLOUDFLARE_PAGES,
INTEGRATION_BITBUCKET,
], ],
required: true, required: true,
}, },
+21 -1
View File
@@ -6,6 +6,7 @@ import {
INTEGRATION_AWS_PARAMETER_STORE, INTEGRATION_AWS_PARAMETER_STORE,
INTEGRATION_AWS_SECRET_MANAGER, INTEGRATION_AWS_SECRET_MANAGER,
INTEGRATION_AZURE_KEY_VAULT, INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_BITBUCKET,
INTEGRATION_CIRCLECI, INTEGRATION_CIRCLECI,
INTEGRATION_CLOUDFLARE_PAGES, INTEGRATION_CLOUDFLARE_PAGES,
INTEGRATION_FLYIO, INTEGRATION_FLYIO,
@@ -25,7 +26,25 @@ import {
export interface IIntegrationAuth extends Document { export interface IIntegrationAuth extends Document {
_id: Types.ObjectId; _id: Types.ObjectId;
workspace: Types.ObjectId; workspace: Types.ObjectId;
integration: "heroku" | "vercel" | "netlify" | "github" | "gitlab" | "render" | "railway" | "flyio" | "azure-key-vault" | "laravel-forge" | "circleci" | "travisci" | "supabase" | "aws-parameter-store" | "aws-secret-manager" | "checkly" | "cloudflare-pages"; integration:
| "heroku"
| "vercel"
| "netlify"
| "github"
| "gitlab"
| "render"
| "railway"
| "flyio"
| "azure-key-vault"
| "laravel-forge"
| "circleci"
| "travisci"
| "supabase"
| "aws-parameter-store"
| "aws-secret-manager"
| "checkly"
| "cloudflare-pages"
| "bitbucket";
teamId: string; teamId: string;
accountId: string; accountId: string;
url: string; url: string;
@@ -71,6 +90,7 @@ const integrationAuthSchema = new Schema<IIntegrationAuth>(
INTEGRATION_SUPABASE, INTEGRATION_SUPABASE,
INTEGRATION_HASHICORP_VAULT, INTEGRATION_HASHICORP_VAULT,
INTEGRATION_CLOUDFLARE_PAGES, INTEGRATION_CLOUDFLARE_PAGES,
INTEGRATION_BITBUCKET
], ],
required: true, required: true,
}, },
+14
View File
@@ -81,6 +81,7 @@ router.get(
}), }),
param("integrationAuthId"), param("integrationAuthId"),
query("teamId"), query("teamId"),
query("workspaceSlug"),
validateRequest, validateRequest,
integrationAuthController.getIntegrationAuthApps integrationAuthController.getIntegrationAuthApps
); );
@@ -141,6 +142,19 @@ router.get(
integrationAuthController.getIntegrationAuthRailwayServices integrationAuthController.getIntegrationAuthRailwayServices
); );
router.get(
"/:integrationAuthId/bitbucket/workspaces",
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT],
}),
requireIntegrationAuthorizationAuth({
acceptedRoles: [ADMIN, MEMBER],
}),
param("integrationAuthId"),
validateRequest,
integrationAuthController.getIntegrationAuthBitBucketWorkspaces
);
router.delete( router.delete(
"/:integrationAuthId", "/:integrationAuthId",
requireAuth({ requireAuth({
+15 -1
View File
@@ -1,5 +1,6 @@
import { import {
getClientIdAzure, getClientIdAzure,
getClientIdBitBucket,
getClientIdGitHub, getClientIdGitHub,
getClientIdGitLab, getClientIdGitLab,
getClientIdHeroku, getClientIdHeroku,
@@ -26,6 +27,7 @@ export const INTEGRATION_SUPABASE = "supabase";
export const INTEGRATION_CHECKLY = "checkly"; export const INTEGRATION_CHECKLY = "checkly";
export const INTEGRATION_HASHICORP_VAULT = "hashicorp-vault"; export const INTEGRATION_HASHICORP_VAULT = "hashicorp-vault";
export const INTEGRATION_CLOUDFLARE_PAGES = "cloudflare-pages"; export const INTEGRATION_CLOUDFLARE_PAGES = "cloudflare-pages";
export const INTEGRATION_BITBUCKET = "bitbucket";
export const INTEGRATION_SET = new Set([ export const INTEGRATION_SET = new Set([
INTEGRATION_AZURE_KEY_VAULT, INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_HEROKU, INTEGRATION_HEROKU,
@@ -41,7 +43,8 @@ export const INTEGRATION_SET = new Set([
INTEGRATION_SUPABASE, INTEGRATION_SUPABASE,
INTEGRATION_CHECKLY, INTEGRATION_CHECKLY,
INTEGRATION_HASHICORP_VAULT, INTEGRATION_HASHICORP_VAULT,
INTEGRATION_CLOUDFLARE_PAGES INTEGRATION_CLOUDFLARE_PAGES,
INTEGRATION_BITBUCKET
]); ]);
// integration types // integration types
@@ -56,6 +59,7 @@ export const INTEGRATION_NETLIFY_TOKEN_URL = "https://api.netlify.com/oauth/toke
export const INTEGRATION_GITHUB_TOKEN_URL = export const INTEGRATION_GITHUB_TOKEN_URL =
"https://github.com/login/oauth/access_token"; "https://github.com/login/oauth/access_token";
export const INTEGRATION_GITLAB_TOKEN_URL = "https://gitlab.com/oauth/token"; export const INTEGRATION_GITLAB_TOKEN_URL = "https://gitlab.com/oauth/token";
export const INTEGRATION_BITBUCKET_TOKEN_URL = "https://bitbucket.org/site/oauth2/access_token"
// integration apps endpoints // integration apps endpoints
export const INTEGRATION_HEROKU_API_URL = "https://api.heroku.com"; export const INTEGRATION_HEROKU_API_URL = "https://api.heroku.com";
@@ -71,6 +75,7 @@ export const INTEGRATION_SUPABASE_API_URL = "https://api.supabase.com";
export const INTEGRATION_LARAVELFORGE_API_URL = "https://forge.laravel.com"; export const INTEGRATION_LARAVELFORGE_API_URL = "https://forge.laravel.com";
export const INTEGRATION_CHECKLY_API_URL = "https://api.checklyhq.com"; export const INTEGRATION_CHECKLY_API_URL = "https://api.checklyhq.com";
export const INTEGRATION_CLOUDFLARE_PAGES_API_URL = "https://api.cloudflare.com"; export const INTEGRATION_CLOUDFLARE_PAGES_API_URL = "https://api.cloudflare.com";
export const INTEGRATION_BITBUCKET_API_URL = "https://api.bitbucket.org/";
export const getIntegrationOptions = async () => { export const getIntegrationOptions = async () => {
const INTEGRATION_OPTIONS = [ const INTEGRATION_OPTIONS = [
@@ -245,6 +250,15 @@ export const getIntegrationOptions = async () => {
type: "pat", type: "pat",
clientId: "", clientId: "",
docsLink: "" docsLink: ""
},
{
name: "BitBucket",
slug: "bitbucket",
image: "BitBucket.png",
isAvailable: true,
type: "oauth",
clientId: await getClientIdBitBucket(),
docsLink: ""
} }
] ]
Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 184 KiB

+29
View File
@@ -0,0 +1,29 @@
---
title: "BitBucket"
description: "How to sync secrets from Infisical to BitBucket"
---
Prerequisites:
- Set up and add envars to [Infisical Cloud](https://app.infisical.com)
## Navigate to your project's integrations tab
![integrations](../../images/integrations.png)
## Authorize Infisical for BitBucket
Press on the BitBucket tile and grant Infisical access to your BitBucket account.
![integrations bitbucket authorization](../../images/integrations-bitbucket-auth.png)
<Info>
If this is your project's first cloud integration, then you'll have to grant Infisical access to your project's environment variables.
Although this step breaks E2EE, it's necessary for Infisical to sync the environment variables to the cloud platform.
</Info>
## Start integration
Select which Infisical environment secrets you want to sync to which BitBucket repo and press start integration to start syncing secrets to the repo.
![integrations bitbucket](../../images/integrations-bitbucket.png)
+1
View File
@@ -28,6 +28,7 @@ Missing an integration? [Throw in a request](https://github.com/Infisical/infisi
| [AWS Parameter Store](/integrations/cloud/aws-parameter-store) | Cloud | Available | | [AWS Parameter Store](/integrations/cloud/aws-parameter-store) | Cloud | Available |
| [AWS Secret Manager](/integrations/cloud/aws-secret-manager) | Cloud | Available | | [AWS Secret Manager](/integrations/cloud/aws-secret-manager) | Cloud | Available |
| [Azure Key Vault](/integrations/cloud/azure-key-vault) | Cloud | Available | | [Azure Key Vault](/integrations/cloud/azure-key-vault) | Cloud | Available |
| [BitBucket](/integrations/cicd/bitbucket) | CI/CD | Available |
| [GitHub Actions](/integrations/cicd/githubactions) | CI/CD | Available | | [GitHub Actions](/integrations/cicd/githubactions) | CI/CD | Available |
| [GitLab](/integrations/cicd/gitlab) | CI/CD | Available | | [GitLab](/integrations/cicd/gitlab) | CI/CD | Available |
| [CircleCI](/integrations/cicd/circleci) | CI/CD | Available | | [CircleCI](/integrations/cicd/circleci) | CI/CD | Available |
+2 -1
View File
@@ -220,7 +220,8 @@
"integrations/cicd/githubactions", "integrations/cicd/githubactions",
"integrations/cicd/gitlab", "integrations/cicd/gitlab",
"integrations/cicd/circleci", "integrations/cicd/circleci",
"integrations/cicd/travisci" "integrations/cicd/travisci",
"integrations/cicd/bitbucket"
] ]
}, },
{ {
@@ -107,6 +107,14 @@ Other environment variables are listed below to increase the functionality of yo
<ParamField query="CLIENT_SLUG_VERCEL" type="string" default="none" optional> <ParamField query="CLIENT_SLUG_VERCEL" type="string" default="none" optional>
OAuth2 slug for Vercel integration OAuth2 slug for Vercel integration
</ParamField> </ParamField>
<ParamField query="CLIENT_ID_BITBUCKET" type="string" default="none" optional>
OAuth2 client ID for BitBucket integration
</ParamField>
<ParamField query="CLIENT_SECRET_BITBUCKET" type="string" default="none" optional>
OAuth2 client secret for BitBucket integration
</ParamField>
</Tab> </Tab>
<Tab title="Auth Integrations"> <Tab title="Auth Integrations">
To integrate with external auth providers, provide value for the related keys To integrate with external auth providers, provide value for the related keys
+20 -19
View File
@@ -3,25 +3,26 @@ interface Mapping {
} }
const integrationSlugNameMapping: Mapping = { const integrationSlugNameMapping: Mapping = {
'azure-key-vault': 'Azure Key Vault', "azure-key-vault": "Azure Key Vault",
'aws-parameter-store': 'AWS Parameter Store', "aws-parameter-store": "AWS Parameter Store",
'aws-secret-manager': 'AWS Secret Manager', "aws-secret-manager": "AWS Secret Manager",
'heroku': 'Heroku', heroku: "Heroku",
'vercel': 'Vercel', vercel: "Vercel",
'netlify': 'Netlify', netlify: "Netlify",
'github': 'GitHub', github: "GitHub",
'gitlab': 'GitLab', gitlab: "GitLab",
'render': 'Render', render: "Render",
'laravel-forge': "Laravel Forge", "laravel-forge": "Laravel Forge",
'railway': 'Railway', railway: "Railway",
'flyio': 'Fly.io', flyio: "Fly.io",
'circleci': 'CircleCI', circleci: "CircleCI",
'travisci': 'TravisCI', travisci: "TravisCI",
'supabase': 'Supabase', supabase: "Supabase",
'checkly': 'Checkly', checkly: "Checkly",
'hashicorp-vault': 'Vault', "hashicorp-vault": "Vault",
'cloudflare-pages': 'Cloudflare Pages' "cloudflare-pages": "Cloudflare Pages",
} bitbucket: "BitBucket"
};
const envMapping: Mapping = { const envMapping: Mapping = {
Development: "dev", Development: "dev",
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

@@ -1,9 +1,10 @@
export { export {
useDeleteIntegrationAuth, useDeleteIntegrationAuth,
useGetIntegrationAuthApps, useGetIntegrationAuthApps,
useGetIntegrationAuthBitBucketWorkspaces,
useGetIntegrationAuthById, useGetIntegrationAuthById,
useGetIntegrationAuthRailwayEnvironments, useGetIntegrationAuthRailwayEnvironments,
useGetIntegrationAuthRailwayServices, useGetIntegrationAuthRailwayServices,
useGetIntegrationAuthTeams, useGetIntegrationAuthTeams,
useGetIntegrationAuthVercelBranches useGetIntegrationAuthVercelBranches,
} from "./queries"; } from "./queries";
@@ -3,13 +3,13 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request"; import { apiRequest } from "@app/config/request";
import { workspaceKeys } from "../workspace/queries"; import { workspaceKeys } from "../workspace/queries";
import { App, Environment, IntegrationAuth, Service, Team } from "./types"; import { App, BitBucketWorkspace, Environment, IntegrationAuth, Service, Team } from "./types";
const integrationAuthKeys = { const integrationAuthKeys = {
getIntegrationAuthById: (integrationAuthId: string) => getIntegrationAuthById: (integrationAuthId: string) =>
[{ integrationAuthId }, "integrationAuth"] as const, [{ integrationAuthId }, "integrationAuth"] as const,
getIntegrationAuthApps: (integrationAuthId: string, teamId?: string) => getIntegrationAuthApps: (integrationAuthId: string, teamId?: string, workspaceSlug?: string) =>
[{ integrationAuthId, teamId }, "integrationAuthApps"] as const, [{ integrationAuthId, teamId, workspaceSlug }, "integrationAuthApps"] as const,
getIntegrationAuthTeams: (integrationAuthId: string) => getIntegrationAuthTeams: (integrationAuthId: string) =>
[{ integrationAuthId }, "integrationAuthTeams"] as const, [{ integrationAuthId }, "integrationAuthTeams"] as const,
getIntegrationAuthVercelBranches: ({ getIntegrationAuthVercelBranches: ({
@@ -32,7 +32,9 @@ const integrationAuthKeys = {
}: { }: {
integrationAuthId: string; integrationAuthId: string;
appId: string; appId: string;
}) => [{ integrationAuthId, appId }, "integrationAuthRailwayServices"] as const }) => [{ integrationAuthId, appId }, "integrationAuthRailwayServices"] as const,
getIntegrationAuthBitBucketWorkspaces: (integrationAuthId: string) =>
[{ integrationAuthId }, "integrationAuthTeams"] as const,
}; };
const fetchIntegrationAuthById = async (integrationAuthId: string) => { const fetchIntegrationAuthById = async (integrationAuthId: string) => {
@@ -44,12 +46,22 @@ const fetchIntegrationAuthById = async (integrationAuthId: string) => {
const fetchIntegrationAuthApps = async ({ const fetchIntegrationAuthApps = async ({
integrationAuthId, integrationAuthId,
teamId teamId,
workspaceSlug
}: { }: {
integrationAuthId: string; integrationAuthId: string;
teamId?: string; teamId?: string;
workspaceSlug?: string;
}) => { }) => {
const searchParams = new URLSearchParams(teamId ? { teamId } : undefined); const params: Record<string, string> = {}
if (teamId) {
params.teamId = teamId
}
if (workspaceSlug) {
params.workspaceSlug = workspaceSlug
}
const searchParams = new URLSearchParams(params);
const { data } = await apiRequest.get<{ apps: App[] }>( const { data } = await apiRequest.get<{ apps: App[] }>(
`/api/v1/integration-auth/${integrationAuthId}/apps`, `/api/v1/integration-auth/${integrationAuthId}/apps`,
{ params: searchParams } { params: searchParams }
@@ -127,6 +139,13 @@ const fetchIntegrationAuthRailwayServices = async ({
return services; return services;
}; };
const fetchIntegrationAuthBitBucketWorkspaces = async (integrationAuthId: string) => {
const { data: { workspaces } } = await apiRequest.get<{ workspaces: BitBucketWorkspace[] }>(
`/api/v1/integration-auth/${integrationAuthId}/bitbucket/workspaces`
);
return workspaces;
};
export const useGetIntegrationAuthById = (integrationAuthId: string) => { export const useGetIntegrationAuthById = (integrationAuthId: string) => {
return useQuery({ return useQuery({
queryKey: integrationAuthKeys.getIntegrationAuthById(integrationAuthId), queryKey: integrationAuthKeys.getIntegrationAuthById(integrationAuthId),
@@ -137,17 +156,20 @@ export const useGetIntegrationAuthById = (integrationAuthId: string) => {
export const useGetIntegrationAuthApps = ({ export const useGetIntegrationAuthApps = ({
integrationAuthId, integrationAuthId,
teamId teamId,
workspaceSlug,
}: { }: {
integrationAuthId: string; integrationAuthId: string;
teamId?: string; teamId?: string;
workspaceSlug?: string;
}) => { }) => {
return useQuery({ return useQuery({
queryKey: integrationAuthKeys.getIntegrationAuthApps(integrationAuthId, teamId), queryKey: integrationAuthKeys.getIntegrationAuthApps(integrationAuthId, teamId, workspaceSlug),
queryFn: () => queryFn: () =>
fetchIntegrationAuthApps({ fetchIntegrationAuthApps({
integrationAuthId, integrationAuthId,
teamId teamId,
workspaceSlug
}), }),
enabled: true enabled: true
}); });
@@ -224,6 +246,14 @@ export const useGetIntegrationAuthRailwayServices = ({
}); });
}; };
export const useGetIntegrationAuthBitBucketWorkspaces = (integrationAuthId: string) => {
return useQuery({
queryKey: integrationAuthKeys.getIntegrationAuthBitBucketWorkspaces(integrationAuthId),
queryFn: () => fetchIntegrationAuthBitBucketWorkspaces(integrationAuthId),
enabled: true
});
};
export const useDeleteIntegrationAuth = () => { export const useDeleteIntegrationAuth = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
@@ -29,3 +29,9 @@ export type Service = {
name: string; name: string;
serviceId: string; serviceId: string;
}; };
export type BitBucketWorkspace = {
uuid: string;
name: string;
slug: string;
}
@@ -0,0 +1,198 @@
import { useEffect, useState } from "react";
import { useRouter } from "next/router";
import queryString from "query-string";
import {
Button,
Card,
CardTitle,
FormControl,
Input,
Select,
SelectItem
} from "../../../components/v2";
import {
useGetIntegrationAuthApps,
useGetIntegrationAuthBitBucketWorkspaces,
useGetIntegrationAuthById,
} from "../../../hooks/api/integrationAuth";
import { useGetWorkspaceById } from "../../../hooks/api/workspace";
import createIntegration from "../../api/integrations/createIntegration";
export default function BitBucketCreateIntegrationPage() {
const router = useRouter();
const [targetAppId, setTargetAppId] = useState("");
const [targetEnvironmentId, setTargetEnvironmentId] = useState("");
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState("");
const [secretPath, setSecretPath] = useState("/");
const [isLoading, setIsLoading] = useState(false);
const { integrationAuthId } = queryString.parse(router.asPath.split("?")[1]);
const { data: integrationAuth } = useGetIntegrationAuthById((integrationAuthId as string) ?? "");
const { data: workspace } = useGetWorkspaceById(localStorage.getItem("projectData.id") ?? "");
const { data: targetEnvironments } = useGetIntegrationAuthBitBucketWorkspaces((integrationAuthId as string) ?? "");
const { data: integrationAuthApps } = useGetIntegrationAuthApps({
integrationAuthId: (integrationAuthId as string) ?? "",
workspaceSlug: targetEnvironmentId
});
useEffect(() => {
if (workspace) {
setSelectedSourceEnvironment(workspace.environments[0].slug);
}
}, [workspace]);
useEffect(() => {
if (integrationAuthApps) {
if (integrationAuthApps.length > 0) {
setTargetAppId(integrationAuthApps[0].appId as string);
} else {
setTargetAppId("none");
}
}
}, [integrationAuthApps]);
useEffect(() => {
if (targetEnvironments) {
if (targetEnvironments.length > 0) {
setTargetEnvironmentId(targetEnvironments[0].slug);
} else {
setTargetEnvironmentId("none");
}
}
}, [targetEnvironments]);
const handleButtonClick = async () => {
try {
setIsLoading(true);
if (!integrationAuth?._id) return;
const targetApp = integrationAuthApps?.find(
(integrationAuthApp) => integrationAuthApp.appId === targetAppId
);
const targetEnvironment = targetEnvironments?.find(
(environment) => environment.slug === targetEnvironmentId
);
if (!targetApp || !targetApp.appId || !targetEnvironment) return;
await createIntegration({
integrationAuthId: integrationAuth?._id,
isActive: true,
app: targetApp.name,
appId: targetApp.appId,
sourceEnvironment: selectedSourceEnvironment,
targetEnvironment: targetEnvironment.name,
targetEnvironmentId: targetEnvironment.slug,
targetService: null,
targetServiceId: null,
owner: null,
path: null,
region: null,
secretPath
});
setIsLoading(false);
router.push(`/integrations/${localStorage.getItem("projectData.id")}`);
} catch (err) {
console.error(err);
}
};
return integrationAuth &&
workspace &&
selectedSourceEnvironment &&
integrationAuthApps &&
targetEnvironments ? (
<div className="flex h-full w-full items-center justify-center">
<Card className="max-w-md rounded-md p-8">
<CardTitle className="text-center">BitBucket Integration</CardTitle>
<FormControl label="Project Environment" className="mt-4">
<Select
value={selectedSourceEnvironment}
onValueChange={(val) => setSelectedSourceEnvironment(val)}
className="w-full border border-mineshaft-500"
>
{workspace?.environments.map((sourceEnvironment) => (
<SelectItem
value={sourceEnvironment.slug}
key={`source-environment-${sourceEnvironment.slug}`}
>
{sourceEnvironment.name}
</SelectItem>
))}
</Select>
</FormControl>
<FormControl label="Secrets Path">
<Input
value={secretPath}
onChange={(evt) => setSecretPath(evt.target.value)}
placeholder="Provide a path, default is /"
/>
</FormControl>
<FormControl label="BitBucket Workspace">
<Select
value={targetEnvironmentId}
onValueChange={(val) => setTargetEnvironmentId(val)}
className="w-full border border-mineshaft-500"
isDisabled={targetEnvironments.length === 0}
>
{targetEnvironments.length > 0 ? (
targetEnvironments.map((targetEnvironment) => (
<SelectItem
value={targetEnvironment.slug as string}
key={`target-environment-${targetEnvironment.slug as string}`}
>
{targetEnvironment.name}
</SelectItem>
))
) : (
<SelectItem value="none" key="target-environment-none">
No workspaces found
</SelectItem>
)}
</Select>
</FormControl>
<FormControl label="BitBucket Repo">
<Select
value={targetAppId}
onValueChange={(val) => setTargetAppId(val)}
className="w-full border border-mineshaft-500"
>
{integrationAuthApps.length > 0 ? (
integrationAuthApps.map((integrationAuthApp) => (
<SelectItem
value={integrationAuthApp.appId as string}
key={`target-app-${integrationAuthApp.appId as string}`}
>
{integrationAuthApp.name}
</SelectItem>
))
) : (
<SelectItem value="none" key="target-app-none">
No repositories found
</SelectItem>
)}
</Select>
</FormControl>
<Button
onClick={handleButtonClick}
color="mineshaft"
className="mt-4"
isLoading={isLoading}
isDisabled={integrationAuthApps.length === 0}
>
Create Integration
</Button>
</Card>
</div>
) : (
<div />
);
}
BitBucketCreateIntegrationPage.requireAuth = true;
@@ -0,0 +1,34 @@
import { useEffect } from "react";
import { useRouter } from "next/router";
import queryString from "query-string";
import AuthorizeIntegration from "../../../api/integrations/authorizeIntegration";
export default function BitBucketOAuth2CallbackPage() {
const router = useRouter();
const { code, state } = queryString.parse(router.asPath.split("?")[1]);
useEffect(() => {
(async () => {
try {
// validate state
if (state !== localStorage.getItem("latestCSRFToken")) return;
localStorage.removeItem("latestCSRFToken");
const integrationAuth = await AuthorizeIntegration({
workspaceId: localStorage.getItem("projectData.id") as string,
code: code as string,
integration: "bitbucket"
});
router.push(`/integrations/bitbucket/create?integrationAuthId=${integrationAuth._id}`);
} catch (err) {
console.error(err);
}
})();
}, []);
return <div />;
}
BitBucketOAuth2CallbackPage.requireAuth = true;
@@ -92,6 +92,9 @@ export const redirectForProviderAuth = (integrationOption: TCloudIntegration) =>
case "cloudflare-pages": case "cloudflare-pages":
link = `${window.location.origin}/integrations/cloudflare-pages/authorize`; link = `${window.location.origin}/integrations/cloudflare-pages/authorize`;
break; break;
case "bitbucket":
link = `https://bitbucket.org/site/oauth2/authorize?client_id=${integrationOption.clientId}&response_type=code&redirect_uri=${window.location.origin}/integrations/bitbucket/oauth2/callback&state=${state}`;
break;
default: default:
break; break;
} }
@@ -106,7 +106,8 @@ export const IntegrationsSection = ({
{(integration.integration === "vercel" || {(integration.integration === "vercel" ||
integration.integration === "netlify" || integration.integration === "netlify" ||
integration.integration === "railway" || integration.integration === "railway" ||
integration.integration === "gitlab") && ( integration.integration === "gitlab" ||
integration.integration === "bitbucket") && (
<div className="ml-4 flex flex-col"> <div className="ml-4 flex flex-col">
<FormLabel label="Target Environment" /> <FormLabel label="Target Environment" />
<div className="rounded-md border border-mineshaft-700 bg-mineshaft-900 px-3 py-2 font-inter text-sm text-bunker-200"> <div className="rounded-md border border-mineshaft-700 bg-mineshaft-900 px-3 py-2 font-inter text-sm text-bunker-200">