diff --git a/backend/src/server/routes/v1/integration-auth-router.ts b/backend/src/server/routes/v1/integration-auth-router.ts index 1d2959f5b..dae37ddb3 100644 --- a/backend/src/server/routes/v1/integration-auth-router.ts +++ b/backend/src/server/routes/v1/integration-auth-router.ts @@ -891,6 +891,48 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider) } }); + server.route({ + method: "GET", + url: "/:integrationAuthId/bitbucket/environments", + config: { + rateLimit: readLimit + }, + onRequest: verifyAuth([AuthMode.JWT]), + schema: { + params: z.object({ + integrationAuthId: z.string().trim() + }), + querystring: z.object({ + workspaceSlug: z.string().trim().min(1, { message: "Workspace slug required" }), + repoSlug: z.string().trim().min(1, { message: "Repo slug required" }) + }), + response: { + 200: z.object({ + environments: z + .object({ + name: z.string(), + slug: z.string(), + uuid: z.string(), + type: z.string() + }) + .array() + }) + } + }, + handler: async (req) => { + const environments = await server.services.integrationAuth.getBitbucketEnvironments({ + actorId: req.permission.id, + actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + id: req.params.integrationAuthId, + workspaceSlug: req.query.workspaceSlug, + repoSlug: req.query.repoSlug + }); + return { environments }; + } + }); + server.route({ method: "GET", url: "/:integrationAuthId/northflank/secret-groups", diff --git a/backend/src/services/integration-auth/integration-auth-service.ts b/backend/src/services/integration-auth/integration-auth-service.ts index d7d7c45ab..636f634a3 100644 --- a/backend/src/services/integration-auth/integration-auth-service.ts +++ b/backend/src/services/integration-auth/integration-auth-service.ts @@ -20,6 +20,7 @@ import { getApps } from "./integration-app-list"; import { TIntegrationAuthDALFactory } from "./integration-auth-dal"; import { IntegrationAuthMetadataSchema, TIntegrationAuthMetadata } from "./integration-auth-schema"; import { + TBitbucketEnvironment, TBitbucketWorkspace, TChecklyGroups, TDeleteIntegrationAuthByIdDTO, @@ -30,6 +31,7 @@ import { THerokuPipelineCoupling, TIntegrationAuthAppsDTO, TIntegrationAuthAwsKmsKeyDTO, + TIntegrationAuthBitbucketEnvironmentsDTO, TIntegrationAuthBitbucketWorkspaceDTO, TIntegrationAuthChecklyGroupsDTO, TIntegrationAuthGithubEnvsDTO, @@ -1261,6 +1263,55 @@ export const integrationAuthServiceFactory = ({ return workspaces; }; + const getBitbucketEnvironments = async ({ + workspaceSlug, + repoSlug, + actorId, + actor, + actorOrgId, + actorAuthMethod, + id + }: TIntegrationAuthBitbucketEnvironmentsDTO) => { + const integrationAuth = await integrationAuthDAL.findById(id); + if (!integrationAuth) throw new NotFoundError({ message: `Integration auth with ID '${id}' not found` }); + + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + integrationAuth.projectId, + actorAuthMethod, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); + const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(integrationAuth.projectId); + const { accessToken } = await getIntegrationAccessToken(integrationAuth, shouldUseSecretV2Bridge, botKey); + const environments: TBitbucketEnvironment[] = []; + let hasNextPage = true; + + let environmentsUrl = `${IntegrationUrls.BITBUCKET_API_URL}/2.0/repositories/${workspaceSlug}/${repoSlug}/environments`; + + while (hasNextPage) { + // eslint-disable-next-line + const { data }: { data: { values: TBitbucketEnvironment[]; next: string } } = await request.get(environmentsUrl, { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" + } + }); + + if (data?.values.length > 0) { + environments.push(...data.values); + } + + if (data.next) { + environmentsUrl = data.next; + } else { + hasNextPage = false; + } + } + return environments; + }; + const getNorthFlankSecretGroups = async ({ id, actor, @@ -1499,6 +1550,7 @@ export const integrationAuthServiceFactory = ({ getNorthFlankSecretGroups, getTeamcityBuildConfigs, getBitbucketWorkspaces, + getBitbucketEnvironments, getIntegrationAccessToken, duplicateIntegrationAuth }; diff --git a/backend/src/services/integration-auth/integration-auth-types.ts b/backend/src/services/integration-auth/integration-auth-types.ts index eb8b8044d..0b92c29f1 100644 --- a/backend/src/services/integration-auth/integration-auth-types.ts +++ b/backend/src/services/integration-auth/integration-auth-types.ts @@ -99,6 +99,12 @@ export type TIntegrationAuthBitbucketWorkspaceDTO = { id: string; } & Omit; +export type TIntegrationAuthBitbucketEnvironmentsDTO = { + workspaceSlug: string; + repoSlug: string; + id: string; +} & Omit; + export type TIntegrationAuthNorthflankSecretGroupDTO = { id: string; appId: string; @@ -148,6 +154,13 @@ export type TBitbucketWorkspace = { updated_on: string; }; +export type TBitbucketEnvironment = { + type: string; + uuid: string; + name: string; + slug: string; +}; + export type TNorthflankSecretGroup = { id: string; name: string; diff --git a/backend/src/services/integration-auth/integration-list.ts b/backend/src/services/integration-auth/integration-list.ts index b31f70241..007fccd52 100644 --- a/backend/src/services/integration-auth/integration-list.ts +++ b/backend/src/services/integration-auth/integration-list.ts @@ -334,7 +334,7 @@ export const getIntegrationOptions = async () => { docsLink: "" }, { - name: "BitBucket", + name: "Bitbucket", slug: "bitbucket", image: "BitBucket.png", isAvailable: true, diff --git a/backend/src/services/integration-auth/integration-sync-secret.ts b/backend/src/services/integration-auth/integration-sync-secret.ts index 18c28afac..449d2d14d 100644 --- a/backend/src/services/integration-auth/integration-sync-secret.ts +++ b/backend/src/services/integration-auth/integration-sync-secret.ts @@ -3631,7 +3631,16 @@ const syncSecretsBitBucket = async ({ const res: { [key: string]: BitbucketVariable } = {}; let hasNextPage = true; - let variablesUrl = `${IntegrationUrls.BITBUCKET_API_URL}/2.0/repositories/${integration.targetEnvironmentId}/${integration.appId}/pipelines_config/variables`; + + let variablesUrl: string; + + if (integration.targetServiceId) { + // scope: deployment environment + variablesUrl = `${IntegrationUrls.BITBUCKET_API_URL}/2.0/repositories/${integration.targetEnvironmentId}/${integration.appId}/deployments_config/environments/${integration.targetServiceId}/variables`; + } else { + // scope: repository + variablesUrl = `${IntegrationUrls.BITBUCKET_API_URL}/2.0/repositories/${integration.targetEnvironmentId}/${integration.appId}/pipelines_config/variables`; + } while (hasNextPage) { const { data }: { data: VariablesResponse } = await request.get(variablesUrl, { diff --git a/docs/images/integrations/bitbucket/integrations-bitbucket-configuration.png b/docs/images/integrations/bitbucket/integrations-bitbucket-configuration.png new file mode 100644 index 000000000..658cefc7b Binary files /dev/null and b/docs/images/integrations/bitbucket/integrations-bitbucket-configuration.png differ diff --git a/docs/integrations/cicd/bitbucket.mdx b/docs/integrations/cicd/bitbucket.mdx index 2aa2106da..3c1330308 100644 --- a/docs/integrations/cicd/bitbucket.mdx +++ b/docs/integrations/cicd/bitbucket.mdx @@ -3,29 +3,37 @@ title: "Bitbucket" description: "How to sync secrets from Infisical to Bitbucket" --- +Infisical lets you sync secrets to Bitbucket at the repository-level and deployment environment-level. + + Prerequisites: - Set up and add envars to [Infisical Cloud](https://app.infisical.com) - - - Navigate to your project's integrations tab in Infisical. + + + Navigate to your project's integrations tab in Infisical. - ![integrations](../../images/integrations.png) + ![integrations](/images/integrations.png) - Press on the Bitbucket tile and grant Infisical access to your Bitbucket account. + Press on the Bitbucket tile and grant Infisical access to your Bitbucket account. - ![integrations bitbucket authorization](../../images/integrations/bitbucket/integrations-bitbucket-auth.png) + ![integrations bitbucket authorization](/images/integrations/bitbucket/integrations-bitbucket.png) + + + Select which workspace, repository, and optionally, deployment environment, you'd like to sync your secrets + to. + ![integrations configure + bitbucket](/images/integrations/bitbucket/integrations-bitbucket-configuration.png) - - - Select which Infisical environment secrets you want to sync to which Bitbucket repo and press start integration to start syncing secrets to the repo. + Once created, your integration will begin syncing secrets to the configured repository or deployment + environment. - ![integrations bitbucket](../../images/integrations/bitbucket/integrations-bitbucket.png) - - + ![integrations bitbucket](/images/integrations/bitbucket/integrations-bitbucket.png) + + @@ -36,7 +44,7 @@ Prerequisites: Create Bitbucket variables (can be either workspace, repository, or deployment-level) to store Machine Identity Client ID and Client Secret. - ![integrations bitbucket](../../images/integrations/bitbucket/integrations-bitbucket-env.png) + ![integrations bitbucket](/images/integrations/bitbucket/integrations-bitbucket-env.png) Edit your Bitbucket pipeline YAML file to include the use of the Infisical CLI to fetch and inject secrets into any script or command within the pipeline. diff --git a/frontend/public/data/frequentConstants.ts b/frontend/public/data/frequentConstants.ts index f430cd8f4..1a536e522 100644 --- a/frontend/public/data/frequentConstants.ts +++ b/frontend/public/data/frequentConstants.ts @@ -28,7 +28,7 @@ const integrationSlugNameMapping: Mapping = { "cloudflare-workers": "Cloudflare Workers", codefresh: "Codefresh", "digital-ocean-app-platform": "Digital Ocean App Platform", - bitbucket: "BitBucket", + bitbucket: "Bitbucket", "cloud-66": "Cloud 66", northflank: "Northflank", windmill: "Windmill", diff --git a/frontend/src/components/v2/MultiSelect/MultiSelect.tsx b/frontend/src/components/v2/MultiSelect/MultiSelect.tsx index f086a9b4c..987dda90f 100644 --- a/frontend/src/components/v2/MultiSelect/MultiSelect.tsx +++ b/frontend/src/components/v2/MultiSelect/MultiSelect.tsx @@ -11,7 +11,7 @@ import { faChevronDown, faX } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { twMerge } from "tailwind-merge"; -const DropdownIndicator = (props: DropdownIndicatorProps) => { +const DropdownIndicator = (props: DropdownIndicatorProps) => { return ( @@ -19,7 +19,7 @@ const DropdownIndicator = (props: DropdownIndicatorProps) => { ); }; -const ClearIndicator = (props: ClearIndicatorProps) => { +const ClearIndicator = (props: ClearIndicatorProps) => { return ( @@ -35,7 +35,7 @@ const MultiValueRemove = (props: MultiValueRemoveProps) => { ); }; -const Option = ({ isSelected, children, ...props }: OptionProps) => { +const Option = ({ isSelected, children, ...props }: OptionProps) => { return ( {children} @@ -46,9 +46,9 @@ const Option = ({ isSelected, children, ...props }: OptionProps) => { ); }; -export const MultiSelect = (props: Props) => ( +export const MultiSelect = ({ isMulti = true, ...props }: Props) => ( setSelectedSourceEnvironment(val)} - className="w-full border border-mineshaft-500" - > - {workspace?.environments.map((sourceEnvironment) => ( - { + if (!bitbucketRepos || !bitbucketWorkspaces || !currentWorkspace) return; + + reset({ + targetRepo: bitbucketRepos[0], + targetWorkspace: bitbucketWorkspaces[0], + sourceEnvironment: currentWorkspace.environments[0], + secretPath: "/", + scope: ScopeOptions[0] + }); + }, [bitbucketWorkspaces, bitbucketRepos, currentWorkspace]); + + if (isProjectLoading || isBitbucketWorkspacesLoading || isBitbucketReposLoading) + return ( +
+ +
+ ); + + const scope = watch("scope"); + + return ( +
+ + + + Bitbucket Integration + + ( + + option.slug} + value={value} + getOptionLabel={(option) => option.name} + onChange={onChange} + options={currentWorkspace?.environments} + placeholder="Select a project environment" + isDisabled={!bitbucketWorkspaces?.length} + /> + + )} + /> + ( + + + + )} + /> + ( + + option.slug} + value={value} + getOptionLabel={(option) => option.name} + onChange={onChange} + options={bitbucketWorkspaces} + placeholder={ + bitbucketWorkspaces?.length ? "Select a workspace..." : "No workspaces found..." + } + isDisabled={!bitbucketWorkspaces?.length} + /> + + )} + /> + ( + + option.appId!} + value={value} + getOptionLabel={(option) => option.name} + onChange={onChange} + options={bitbucketRepos} + placeholder={ + bitbucketRepos?.length ? "Select a repository..." : "No repositories found..." + } + isDisabled={!bitbucketRepos?.length} + /> + + )} + /> + ( + + option.value} + getOptionLabel={(option) => option.label} + onChange={onChange} + options={ScopeOptions} + /> + + )} + /> + + {scope?.value === BitbucketScope.Env && ( + ( + - {sourceEnvironment.name} - - ))} - - - - setSecretPath(evt.target.value)} - placeholder="Provide a path, default is /" + option.uuid} + value={value} + getOptionLabel={(option) => option.name} + onChange={onChange} + options={bitbucketEnvironments} + placeholder={ + bitbucketEnvironments?.length + ? "Select an environment..." + : "No environments found..." + } + isDisabled={!bitbucketEnvironments?.length} + /> + + )} /> - - - - - - - + )} - - ) : ( -
+ ); } diff --git a/frontend/src/views/IntegrationsPage/IntegrationDetailsPage/components/IntegrationConnectionSection.tsx b/frontend/src/views/IntegrationsPage/IntegrationDetailsPage/components/IntegrationConnectionSection.tsx index 7aa862593..8187801c8 100644 --- a/frontend/src/views/IntegrationsPage/IntegrationDetailsPage/components/IntegrationConnectionSection.tsx +++ b/frontend/src/views/IntegrationsPage/IntegrationDetailsPage/components/IntegrationConnectionSection.tsx @@ -53,6 +53,8 @@ export const IntegrationConnectionSection = ({ integration }: Props) => { case "aws-parameter-store": case "rundeck": return "Path"; + case "bitbucket": + return "Repository"; case "github": if (["github-env", "github-repo"].includes(integration.scope!)) { return "Repository"; @@ -92,10 +94,18 @@ export const IntegrationConnectionSection = ({ integration }: Props) => { }; const targetEnvironmentDetails = () => { + if (integration.integration === "bitbucket") { + return ( +
+ +
+ {integration.targetEnvironment || integration.targetEnvironmentId} +
+
+ ); + } if ( - ["vercel", "netlify", "railway", "gitlab", "teamcity", "bitbucket"].includes( - integration.integration - ) || + ["vercel", "netlify", "railway", "gitlab", "teamcity"].includes(integration.integration) || (integration.integration === "github" && integration.scope === "github-env") ) { return ( @@ -153,6 +163,18 @@ export const IntegrationConnectionSection = ({ integration }: Props) => { ); } + if (integration.integration === "bitbucket" && integration.targetServiceId) { + return ( +
+ +
{integration.targetService}
+
+ ); + } + return null; }; diff --git a/frontend/src/views/IntegrationsPage/components/IntegrationsSection/ConfiguredIntegrationItem.tsx b/frontend/src/views/IntegrationsPage/components/IntegrationsSection/ConfiguredIntegrationItem.tsx index 29901e35a..32ce21192 100644 --- a/frontend/src/views/IntegrationsPage/components/IntegrationsSection/ConfiguredIntegrationItem.tsx +++ b/frontend/src/views/IntegrationsPage/components/IntegrationsSection/ConfiguredIntegrationItem.tsx @@ -53,7 +53,7 @@ export const ConfiguredIntegrationItem = ({ {integration.secretPath}
-
+
@@ -107,6 +107,7 @@ export const ConfiguredIntegrationItem = ({ label={ (integration.integration === "qovery" && integration?.scope) || (integration.integration === "circleci" && "Project") || + (integration.integration === "bitbucket" && "Repository") || (integration.integration === "aws-secret-manager" && "Secret") || (["aws-parameter-store", "rundeck"].includes(integration.integration) && "Path") || (integration?.integration === "terraform-cloud" && "Project") || @@ -133,7 +134,6 @@ export const ConfiguredIntegrationItem = ({ integration.integration === "railway" || integration.integration === "gitlab" || integration.integration === "teamcity" || - integration.integration === "bitbucket" || (integration.integration === "github" && integration.scope === "github-env")) && (
@@ -142,6 +142,24 @@ export const ConfiguredIntegrationItem = ({
)} + {integration.integration === "bitbucket" && ( + <> + {integration.targetServiceId && ( +
+ +
+ {integration.targetService || integration.targetServiceId} +
+
+ )} +
+ +
+ {integration.targetEnvironment || integration.targetEnvironmentId} +
+
+ + )} {integration.integration === "checkly" && integration.targetService && (