Merge pull request #755 from zwkee/integration/bitbucket

BitBucket Integration
This commit is contained in:
BlackMagiq
2023-07-21 20:55:42 +07:00
committed by GitHub
30 changed files with 813 additions and 91 deletions

View File

@@ -47,11 +47,13 @@ CLIENT_ID_VERCEL=
CLIENT_ID_NETLIFY=
CLIENT_ID_GITHUB=
CLIENT_ID_GITLAB=
CLIENT_ID_BITBUCKET=
CLIENT_SECRET_HEROKU=
CLIENT_SECRET_VERCEL=
CLIENT_SECRET_NETLIFY=
CLIENT_SECRET_GITHUB=
CLIENT_SECRET_GITLAB=
CLIENT_SECRET_BITBUCKET=
CLIENT_SLUG_VERCEL=
# Sentry (optional) for monitoring errors

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 getClientIdGitLab = async () => (await client.getSecret("CLIENT_ID_GITLAB")).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 getClientSecretHeroku = async () => (await client.getSecret("CLIENT_SECRET_HEROKU")).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 getClientSecretGitLab = async () => (await client.getSecret("CLIENT_SECRET_GITLAB")).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 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";

View File

@@ -7,6 +7,7 @@ import { IntegrationService } from "../../services";
import {
ALGORITHM_AES_256_GCM,
ENCODING_SCHEME_UTF8,
INTEGRATION_BITBUCKET_API_URL,
INTEGRATION_RAILWAY_API_URL,
INTEGRATION_SET,
INTEGRATION_VERCEL_API_URL,
@@ -141,12 +142,14 @@ export const saveIntegrationAccessToken = async (req: Request, res: Response) =>
*/
export const getIntegrationAuthApps = async (req: Request, res: Response) => {
const teamId = req.query.teamId as string;
const workspaceSlug = req.query.workspaceSlug as string;
const apps = await getApps({
integrationAuth: req.integrationAuth,
accessToken: req.accessToken,
accessId: req.accessId,
...(teamId && { teamId })
...(teamId && { teamId }),
...(workspaceSlug && { workspaceSlug })
});
return res.status(200).send({
@@ -382,6 +385,66 @@ export const getIntegrationAuthRailwayServices = async (req: Request, res: Respo
});
};
/**
* Return list of workspaces allowed for Bitbucket integration
* @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]
* @param req

View File

@@ -11,8 +11,8 @@ import { ssoController } from "../../controllers/v1";
import { authLimiter } from "../../../helpers/rateLimiter";
import {
ACCEPTED,
OWNER,
ADMIN
ADMIN,
OWNER
} from "../../../variables";
router.get(

View File

@@ -35,6 +35,7 @@ import {
membership as v1MembershipRouter,
organization as v1OrganizationRouter,
password as v1PasswordRouter,
secretImport as v1SecretImportRouter,
secret as v1SecretRouter,
secretScanning as v1SecretScanningRouter,
secretsFolder as v1SecretsFolder,
@@ -43,8 +44,7 @@ import {
userAction as v1UserActionRouter,
user as v1UserRouter,
webhooks as v1WebhooksRouter,
workspace as v1WorkspaceRouter,
secretImport as v1SecretImportRouter
workspace as v1WorkspaceRouter
} from "./routes/v1";
import {
auth as v2AuthRouter,

View File

@@ -5,12 +5,16 @@ import {
INTEGRATION_AWS_PARAMETER_STORE,
INTEGRATION_AWS_SECRET_MANAGER,
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_BITBUCKET,
INTEGRATION_BITBUCKET_API_URL,
INTEGRATION_CHECKLY,
INTEGRATION_CHECKLY_API_URL,
INTEGRATION_CIRCLECI,
INTEGRATION_CIRCLECI_API_URL,
INTEGRATION_CLOUDFLARE_PAGES,
INTEGRATION_CLOUDFLARE_PAGES_API_URL,
INTEGRATION_CODEFRESH,
INTEGRATION_CODEFRESH_API_URL,
INTEGRATION_FLYIO,
INTEGRATION_FLYIO_API_URL,
INTEGRATION_GITHUB,
@@ -31,10 +35,7 @@ import {
INTEGRATION_TRAVISCI,
INTEGRATION_TRAVISCI_API_URL,
INTEGRATION_VERCEL,
INTEGRATION_VERCEL_API_URL,
INTEGRATION_CODEFRESH,
INTEGRATION_CODEFRESH_API_URL
INTEGRATION_VERCEL_API_URL
} from "../variables";
interface App {
@@ -57,11 +58,13 @@ const getApps = async ({
accessToken,
accessId,
teamId,
workspaceSlug,
}: {
integrationAuth: IIntegrationAuth;
accessToken: string;
accessId?: string;
teamId?: string;
workspaceSlug?: string;
}) => {
let apps: App[] = [];
switch (integrationAuth.integration) {
@@ -148,6 +151,12 @@ const getApps = async ({
accountId: accessId
})
break;
case INTEGRATION_BITBUCKET:
apps = await getAppsBitBucket({
accessToken,
workspaceSlug
});
break;
case INTEGRATION_CODEFRESH:
apps = await getAppsCodefresh({
accessToken,
@@ -729,6 +738,79 @@ const getAppsCloudflarePages = async ({
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;
}
/**
* Return list of projects for Supabase integration

View File

@@ -2,6 +2,8 @@ import { standardRequest } from "../config/request";
import {
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_AZURE_TOKEN_URL,
INTEGRATION_BITBUCKET,
INTEGRATION_BITBUCKET_TOKEN_URL,
INTEGRATION_GITHUB,
INTEGRATION_GITHUB_TOKEN_URL,
INTEGRATION_GITLAB,
@@ -15,11 +17,13 @@ import {
} from "../variables";
import {
getClientIdAzure,
getClientIdBitBucket,
getClientIdGitHub,
getClientIdGitLab,
getClientIdNetlify,
getClientIdVercel,
getClientSecretAzure,
getClientSecretBitBucket,
getClientSecretGitHub,
getClientSecretGitLab,
getClientSecretHeroku,
@@ -78,6 +82,15 @@ interface ExchangeCodeGitlabResponse {
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
* code-token exchange for integration named [integration]
@@ -129,6 +142,12 @@ const exchangeCode = async ({
obj = await exchangeCodeGitlab({
code,
});
break;
case INTEGRATION_BITBUCKET:
obj = await exchangeCodeBitBucket({
code,
});
break;
}
return obj;
@@ -347,4 +366,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 };

View File

@@ -2,6 +2,8 @@ import { standardRequest } from "../config/request";
import { IIntegrationAuth } from "../models";
import {
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_BITBUCKET,
INTEGRATION_BITBUCKET_TOKEN_URL,
INTEGRATION_GITLAB,
INTEGRATION_HEROKU,
} from "../variables";
@@ -13,8 +15,10 @@ import {
import { IntegrationService } from "../services";
import {
getClientIdAzure,
getClientIdBitBucket,
getClientIdGitLab,
getClientSecretAzure,
getClientSecretBitBucket,
getClientSecretGitLab,
getClientSecretHeroku,
getSiteURL,
@@ -46,6 +50,15 @@ interface RefreshTokenGitLabResponse {
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
* named [integration]
@@ -83,6 +96,11 @@ const exchangeRefresh = async ({
refreshToken,
});
break;
case INTEGRATION_BITBUCKET:
tokenDetails = await exchangeRefreshBitBucket({
refreshToken,
});
break;
default:
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 };

View File

@@ -14,12 +14,16 @@ import {
INTEGRATION_AWS_PARAMETER_STORE,
INTEGRATION_AWS_SECRET_MANAGER,
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_BITBUCKET,
INTEGRATION_BITBUCKET_API_URL,
INTEGRATION_CHECKLY,
INTEGRATION_CHECKLY_API_URL,
INTEGRATION_CIRCLECI,
INTEGRATION_CIRCLECI_API_URL,
INTEGRATION_CLOUDFLARE_PAGES,
INTEGRATION_CLOUDFLARE_PAGES_API_URL,
INTEGRATION_CODEFRESH,
INTEGRATION_CODEFRESH_API_URL,
INTEGRATION_FLYIO,
INTEGRATION_FLYIO_API_URL,
INTEGRATION_GITHUB,
@@ -41,9 +45,7 @@ import {
INTEGRATION_TRAVISCI,
INTEGRATION_TRAVISCI_API_URL,
INTEGRATION_VERCEL,
INTEGRATION_VERCEL_API_URL,
INTEGRATION_CODEFRESH,
INTEGRATION_CODEFRESH_API_URL
INTEGRATION_VERCEL_API_URL
} from "../variables";
import { standardRequest } from "../config/request";
@@ -180,34 +182,6 @@ const syncSecrets = async ({
accessToken,
});
break;
case INTEGRATION_FLYIO:
await syncSecretsFlyio({
integration,
secrets,
accessToken,
});
break;
case INTEGRATION_CIRCLECI:
await syncSecretsCircleCI({
integration,
secrets,
accessToken,
});
break;
case INTEGRATION_TRAVISCI:
await syncSecretsTravisCI({
integration,
secrets,
accessToken,
});
break;
case INTEGRATION_SUPABASE:
await syncSecretsSupabase({
integration,
secrets,
accessToken,
});
break;
case INTEGRATION_CHECKLY:
await syncSecretsCheckly({
integration,
@@ -239,7 +213,14 @@ const syncSecrets = async ({
accessToken,
});
break;
}
case INTEGRATION_BITBUCKET:
await syncSecretsBitBucket({
integration,
secrets,
accessToken,
});
break;
}
};
/**
@@ -706,8 +687,6 @@ const syncSecretsVercel = async ({
return true;
});
// return secret.target.includes(integration.targetEnvironment);
const res: { [key: string]: VercelSecret } = {};
for await (const vercelSecret of vercelSecrets) {
@@ -1935,7 +1914,7 @@ const syncSecretsCloudflarePages = async ({
}
)
)
.data.result['deployment_configs'][integration.targetEnvironment]['env_vars'];
.data.result["deployment_configs"][integration.targetEnvironment]["env_vars"];
// copy the secrets object, so we can set deleted keys to null
const secretsObj: any = { ...secrets };
@@ -1975,6 +1954,121 @@ 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<BitbucketVariable>;
}
interface BitbucketVariable {
type: string;
uuid: string;
key: string;
value: string;
secured: boolean;
}
const res: { [key: string]: BitbucketVariable } = {};
let hasNextPage = true;
let variablesUrl = `${INTEGRATION_BITBUCKET_API_URL}/2.0/repositories/${integration.targetEnvironmentId}/${integration.appId}/pipelines_config/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) => {
res[variable.key] = variable;
});
}
if (data.next) {
variablesUrl = data.next
} else {
hasNextPage = false
}
}
for await (const key of Object.keys(secrets)) {
if (key in res) {
// update existing secret
await standardRequest.put(
`${variablesUrl}/${res[key].uuid}`,
{
key,
value: secrets[key],
secured: true
},
{
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept": "application/json",
},
}
);
} else {
// create new secret
await standardRequest.post(
variablesUrl,
{
key,
value: secrets[key],
secured: true
},
{
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept": "application/json",
},
}
);
}
}
for await (const key of Object.keys(res)) {
if (!(key in secrets)) {
// delete secret
await standardRequest.delete(
`${variablesUrl}/${res[key].uuid}`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept": "application/json",
}
}
);
}
}
}
/*
* Sync/push [secrets] to Codefresh with name [integration.app]
* @param {Object} obj
* @param {IIntegration} obj.integration - integration details

View File

@@ -3,9 +3,11 @@ import {
INTEGRATION_AWS_PARAMETER_STORE,
INTEGRATION_AWS_SECRET_MANAGER,
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_BITBUCKET,
INTEGRATION_CHECKLY,
INTEGRATION_CIRCLECI,
INTEGRATION_CLOUDFLARE_PAGES,
INTEGRATION_CODEFRESH,
INTEGRATION_FLYIO,
INTEGRATION_GITHUB,
INTEGRATION_GITLAB,
@@ -17,8 +19,7 @@ import {
INTEGRATION_RENDER,
INTEGRATION_SUPABASE,
INTEGRATION_TRAVISCI,
INTEGRATION_VERCEL,
INTEGRATION_CODEFRESH
INTEGRATION_VERCEL
} from "../variables";
export interface IIntegration {
@@ -56,7 +57,8 @@ export interface IIntegration {
| "checkly"
| "hashicorp-vault"
| "cloudflare-pages"
| "codefresh";
| "bitbucket"
| "codefresh"
integrationAuth: Types.ObjectId;
}
@@ -146,6 +148,7 @@ const integrationSchema = new Schema<IIntegration>(
INTEGRATION_CHECKLY,
INTEGRATION_HASHICORP_VAULT,
INTEGRATION_CLOUDFLARE_PAGES,
INTEGRATION_BITBUCKET,
INTEGRATION_CODEFRESH
],
required: true,

View File

@@ -6,8 +6,10 @@ import {
INTEGRATION_AWS_PARAMETER_STORE,
INTEGRATION_AWS_SECRET_MANAGER,
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_BITBUCKET,
INTEGRATION_CIRCLECI,
INTEGRATION_CLOUDFLARE_PAGES,
INTEGRATION_CODEFRESH,
INTEGRATION_FLYIO,
INTEGRATION_GITHUB,
INTEGRATION_GITLAB,
@@ -19,14 +21,32 @@ import {
INTEGRATION_RENDER,
INTEGRATION_SUPABASE,
INTEGRATION_TRAVISCI,
INTEGRATION_VERCEL,
INTEGRATION_CODEFRESH
INTEGRATION_VERCEL
} from "../variables";
export interface IIntegrationAuth extends Document {
_id: 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' | 'codefresh';
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"
| "codefresh"
| "bitbucket";
teamId: string;
accountId: string;
url: string;
@@ -72,6 +92,7 @@ const integrationAuthSchema = new Schema<IIntegrationAuth>(
INTEGRATION_SUPABASE,
INTEGRATION_HASHICORP_VAULT,
INTEGRATION_CLOUDFLARE_PAGES,
INTEGRATION_BITBUCKET,
INTEGRATION_CODEFRESH
],
required: true,

View File

@@ -81,6 +81,7 @@ router.get(
}),
param("integrationAuthId"),
query("teamId"),
query("workspaceSlug"),
validateRequest,
integrationAuthController.getIntegrationAuthApps
);
@@ -141,6 +142,19 @@ router.get(
integrationAuthController.getIntegrationAuthRailwayServices
);
router.get(
"/:integrationAuthId/bitbucket/workspaces",
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT],
}),
requireIntegrationAuthorizationAuth({
acceptedRoles: [ADMIN, MEMBER],
}),
param("integrationAuthId").exists().isString(),
validateRequest,
integrationAuthController.getIntegrationAuthBitBucketWorkspaces
);
router.delete(
"/:integrationAuthId",
requireAuth({

View File

@@ -4,8 +4,6 @@ const ALGORITHM = "aes-256-gcm";
const BLOCK_SIZE_BYTES = 16;
export default class AesGCM {
constructor() {}
static encrypt(
text: string,
secret: string

View File

@@ -4,11 +4,11 @@ import { Types } from "mongoose";
import { AuthData } from "../interfaces/middleware";
import {
AuthProvider,
MembershipOrg,
Organization,
ServiceAccount,
ServiceTokenData,
User,
Organization,
MembershipOrg
User
} from "../models";
import { createToken } from "../helpers/auth";
import {
@@ -18,8 +18,8 @@ import {
getJwtProviderAuthSecret,
} from "../config";
import { getSSOConfigHelper } from "../ee/helpers/organizations";
import { OrganizationNotFoundError, InternalServerError } from "./errors";
import { MEMBER, INVITED } from "../variables";
import { InternalServerError, OrganizationNotFoundError } from "./errors";
import { INVITED, MEMBER } from "../variables";
import { getSiteURL } from "../config";
// eslint-disable-next-line @typescript-eslint/no-var-requires

View File

@@ -1,5 +1,6 @@
import {
getClientIdAzure,
getClientIdBitBucket,
getClientIdGitHub,
getClientIdGitLab,
getClientIdHeroku,
@@ -26,6 +27,7 @@ export const INTEGRATION_SUPABASE = "supabase";
export const INTEGRATION_CHECKLY = "checkly";
export const INTEGRATION_HASHICORP_VAULT = "hashicorp-vault";
export const INTEGRATION_CLOUDFLARE_PAGES = "cloudflare-pages";
export const INTEGRATION_BITBUCKET = "bitbucket";
export const INTEGRATION_CODEFRESH = "codefresh";
export const INTEGRATION_SET = new Set([
INTEGRATION_AZURE_KEY_VAULT,
@@ -43,6 +45,7 @@ export const INTEGRATION_SET = new Set([
INTEGRATION_CHECKLY,
INTEGRATION_HASHICORP_VAULT,
INTEGRATION_CLOUDFLARE_PAGES,
INTEGRATION_BITBUCKET,
INTEGRATION_CODEFRESH
]);
@@ -58,6 +61,7 @@ export const INTEGRATION_NETLIFY_TOKEN_URL = "https://api.netlify.com/oauth/toke
export const INTEGRATION_GITHUB_TOKEN_URL =
"https://github.com/login/oauth/access_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
export const INTEGRATION_HEROKU_API_URL = "https://api.heroku.com";
@@ -73,6 +77,7 @@ export const INTEGRATION_SUPABASE_API_URL = "https://api.supabase.com";
export const INTEGRATION_LARAVELFORGE_API_URL = "https://forge.laravel.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_BITBUCKET_API_URL = "https://api.bitbucket.org";
export const INTEGRATION_CODEFRESH_API_URL = "https://g.codefresh.io/api";
export const getIntegrationOptions = async () => {
@@ -249,6 +254,15 @@ export const getIntegrationOptions = async () => {
clientId: "",
docsLink: ""
},
{
name: "BitBucket",
slug: "bitbucket",
image: "BitBucket.png",
isAvailable: true,
type: "oauth",
clientId: await getClientIdBitBucket(),
docsLink: ""
},
{
name: "Codefresh",
slug: "codefresh",
@@ -257,7 +271,7 @@ export const getIntegrationOptions = async () => {
type: "pat",
clientId: "",
docsLink: "",
},
}
]
return INTEGRATION_OPTIONS;

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 184 KiB

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)

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 Secret Manager](/integrations/cloud/aws-secret-manager) | 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 |
| [GitLab](/integrations/cicd/gitlab) | CI/CD | Available |
| [CircleCI](/integrations/cicd/circleci) | CI/CD | Available |

View File

@@ -222,7 +222,8 @@
"integrations/cicd/githubactions",
"integrations/cicd/gitlab",
"integrations/cicd/circleci",
"integrations/cicd/travisci"
"integrations/cicd/travisci",
"integrations/cicd/bitbucket"
]
},
{

View File

@@ -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>
OAuth2 slug for Vercel integration
</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 title="Auth Integrations">
To integrate with external auth providers, provide value for the related keys

View File

@@ -3,26 +3,27 @@ interface Mapping {
}
const integrationSlugNameMapping: Mapping = {
'azure-key-vault': 'Azure Key Vault',
'aws-parameter-store': 'AWS Parameter Store',
'aws-secret-manager': 'AWS Secret Manager',
'heroku': 'Heroku',
'vercel': 'Vercel',
'netlify': 'Netlify',
'github': 'GitHub',
'gitlab': 'GitLab',
'render': 'Render',
'laravel-forge': "Laravel Forge",
'railway': 'Railway',
'flyio': 'Fly.io',
'circleci': 'CircleCI',
'travisci': 'TravisCI',
'supabase': 'Supabase',
'checkly': 'Checkly',
'hashicorp-vault': 'Vault',
'cloudflare-pages': 'Cloudflare Pages',
'codefresh': 'Codefresh'
}
"azure-key-vault": "Azure Key Vault",
"aws-parameter-store": "AWS Parameter Store",
"aws-secret-manager": "AWS Secret Manager",
heroku: "Heroku",
vercel: "Vercel",
netlify: "Netlify",
github: "GitHub",
gitlab: "GitLab",
render: "Render",
"laravel-forge": "Laravel Forge",
railway: "Railway",
flyio: "Fly.io",
circleci: "CircleCI",
travisci: "TravisCI",
supabase: "Supabase",
checkly: "Checkly",
"hashicorp-vault": "Vault",
"cloudflare-pages": "Cloudflare Pages",
"codefresh": "Codefresh",
bitbucket: "BitBucket"
};
const envMapping: Mapping = {
Development: "dev",

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

View File

@@ -1,6 +1,7 @@
export {
useDeleteIntegrationAuth,
useGetIntegrationAuthApps,
useGetIntegrationAuthBitBucketWorkspaces,
useGetIntegrationAuthById,
useGetIntegrationAuthRailwayEnvironments,
useGetIntegrationAuthRailwayServices,

View File

@@ -3,13 +3,13 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { workspaceKeys } from "../workspace/queries";
import { App, Environment, IntegrationAuth, Service, Team } from "./types";
import { App, BitBucketWorkspace, Environment, IntegrationAuth, Service, Team } from "./types";
const integrationAuthKeys = {
getIntegrationAuthById: (integrationAuthId: string) =>
[{ integrationAuthId }, "integrationAuth"] as const,
getIntegrationAuthApps: (integrationAuthId: string, teamId?: string) =>
[{ integrationAuthId, teamId }, "integrationAuthApps"] as const,
getIntegrationAuthApps: (integrationAuthId: string, teamId?: string, workspaceSlug?: string) =>
[{ integrationAuthId, teamId, workspaceSlug }, "integrationAuthApps"] as const,
getIntegrationAuthTeams: (integrationAuthId: string) =>
[{ integrationAuthId }, "integrationAuthTeams"] as const,
getIntegrationAuthVercelBranches: ({
@@ -33,7 +33,9 @@ const integrationAuthKeys = {
}: {
integrationAuthId: string;
appId: string;
}) => [{ integrationAuthId, appId }, "integrationAuthRailwayServices"] as const
}) => [{ integrationAuthId, appId }, "integrationAuthRailwayServices"] as const,
getIntegrationAuthBitBucketWorkspaces: (integrationAuthId: string) =>
[{ integrationAuthId }, "integrationAuthBitbucketWorkspaces"] as const,
};
const fetchIntegrationAuthById = async (integrationAuthId: string) => {
@@ -45,12 +47,22 @@ const fetchIntegrationAuthById = async (integrationAuthId: string) => {
const fetchIntegrationAuthApps = async ({
integrationAuthId,
teamId
teamId,
workspaceSlug
}: {
integrationAuthId: 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[] }>(
`/api/v1/integration-auth/${integrationAuthId}/apps`,
{ params: searchParams }
@@ -129,6 +141,13 @@ const fetchIntegrationAuthRailwayServices = async ({
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) => {
return useQuery({
queryKey: integrationAuthKeys.getIntegrationAuthById(integrationAuthId),
@@ -139,17 +158,20 @@ export const useGetIntegrationAuthById = (integrationAuthId: string) => {
export const useGetIntegrationAuthApps = ({
integrationAuthId,
teamId
teamId,
workspaceSlug,
}: {
integrationAuthId: string;
teamId?: string;
workspaceSlug?: string;
}) => {
return useQuery({
queryKey: integrationAuthKeys.getIntegrationAuthApps(integrationAuthId, teamId),
queryKey: integrationAuthKeys.getIntegrationAuthApps(integrationAuthId, teamId, workspaceSlug),
queryFn: () =>
fetchIntegrationAuthApps({
integrationAuthId,
teamId
teamId,
workspaceSlug
}),
enabled: true
});
@@ -226,6 +248,13 @@ export const useGetIntegrationAuthRailwayServices = ({
});
};
export const useGetIntegrationAuthBitBucketWorkspaces = (integrationAuthId: string) => {
return useQuery({
queryKey: integrationAuthKeys.getIntegrationAuthBitBucketWorkspaces(integrationAuthId),
queryFn: () => fetchIntegrationAuthBitBucketWorkspaces(integrationAuthId),
enabled: true
});
};
export const useDeleteIntegrationAuth = () => {
const queryClient = useQueryClient();

View File

@@ -29,3 +29,9 @@ export type Service = {
name: string;
serviceId: string;
};
export type BitBucketWorkspace = {
uuid: string;
name: string;
slug: string;
}

View File

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

View File

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

View File

@@ -92,10 +92,12 @@ export const redirectForProviderAuth = (integrationOption: TCloudIntegration) =>
case "cloudflare-pages":
link = `${window.location.origin}/integrations/cloudflare-pages/authorize`;
break;
case "codefresh":
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;
case "codefresh":
link = `${window.location.origin}/integrations/codefresh/authorize`;
break;
default:
break;
}

View File

@@ -105,7 +105,8 @@ export const IntegrationsSection = ({
{(integration.integration === "vercel" ||
integration.integration === "netlify" ||
integration.integration === "railway" ||
integration.integration === "gitlab") && (
integration.integration === "gitlab" ||
integration.integration === "bitbucket") && (
<div className="ml-4 flex flex-col">
<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">