From 569d006d9c529f93c7f25384dcaa0c7a3a8105ea Mon Sep 17 00:00:00 2001 From: msukkari Date: Thu, 21 May 2026 16:00:37 -0700 Subject: [PATCH 1/3] fix(worker): replace Bitbucket Cloud user-repos endpoint removed by CHANGE-2770 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Atlassian removed GET /2.0/user/permissions/repositories on 2026-02-27 (CHANGE-2770) and the endpoint now returns HTTP 410 Gone for every caller. Bitbucket Cloud account-driven permission sync was the only caller of this endpoint, so it has been hard-failing on every cycle since then. Atlassian's stated guidance is that there is no direct replacement, and recommends a two-step pattern instead: 1. GET /2.0/user/workspaces — list the workspaces the user is a member of. 2. GET /2.0/repositories/{workspace}?role=member — list the repos the user can see in each workspace. `role=member` returns repos where the user has at least explicit read access, including access inherited from workspace admin, project membership, or group membership. Bitbucket enforces the filter server-side, so a workspace admin gets the full workspace, while a user with no grant on a repo gets nothing back for it — neither over-grants nor under-grants. Public-repo handling: the call is intentionally not filtered with q=is_private=true. Sourcebot's read-side prisma extension already short-circuits public repos to org-wide access, so an ACCOUNT_DRIVEN row on a public repo is harmless overlay; but not filtering means that during a public<->private visibility flip, the user never has a window where they're missing an ACCOUNT_DRIVEN row for a repo they can see upstream. Co-Authored-By: Claude Opus 4.7 (1M context) --- CHANGELOG.md | 1 + packages/backend/src/bitbucket.ts | 85 +++++++++++++++++++++++++------ 2 files changed, 71 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 55683269b..0b163b92e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Fixed issue where repo permissions could go stale when authentication or token refresh related errors occured. [#1215](https://github.com/sourcebot-dev/sourcebot/pull/1215) +- [EE] Fixed Bitbucket Cloud account-driven permission sync after Atlassian's CHANGE-2770 removed `GET /2.0/user/permissions/repositories`. ## [4.17.2] - 2026-05-16 diff --git a/packages/backend/src/bitbucket.ts b/packages/backend/src/bitbucket.ts index 241a79a8d..bc2daaef3 100644 --- a/packages/backend/src/bitbucket.ts +++ b/packages/backend/src/bitbucket.ts @@ -9,7 +9,6 @@ import micromatch from "micromatch"; import { SchemaRepository as CloudRepository, SchemaRepositoryUserPermission as CloudRepositoryUserPermission, - SchemaRepositoryPermission as CloudRepositoryPermission, } from "@coderabbitai/bitbucket/cloud/openapi"; import { SchemaRestRepository as ServerRepository } from "@coderabbitai/bitbucket/server/openapi"; import { processPromiseResults } from "./connectionUtils.js"; @@ -658,26 +657,82 @@ export const getExplicitUserPermissionsForCloudRepo = async ( }; /** - * Returns the UUIDs of all private repositories accessible to the authenticated Bitbucket Cloud user. + * Returns the UUIDs of all repositories accessible to the authenticated Bitbucket Cloud user. * Used for account-driven permission syncing. - * - * @see https://developer.atlassian.com/cloud/bitbucket/rest/api-group-repositories/#api-user-permissions-repositories-get + * + * @note Atlassian's CHANGE-2770 removed `GET /2.0/user/permissions/repositories` + * (the previous single-call cross-workspace endpoint) and has stated there is no + * direct replacement. The recommended path is two-step: enumerate the workspaces + * the user is a member of, then enumerate the repositories the user can see + * within each workspace. + * + * `?role=member` returns repositories where the user has at least explicit read + * access (including access inherited from workspace admin, project membership, + * or group membership). Bitbucket treats workspace admins as having implicit + * read on every repo in the workspace, so this query naturally returns the + * admin's full set without needing a special case. Conversely, a user with no + * grant on a given repo gets nothing back for it, even if the workspace is + * shared — Bitbucket enforces the access filter server-side. + * + * Visibility (public vs. private) is intentionally not filtered server-side. + * Sourcebot's read-side prisma extension already short-circuits public repos to + * org-wide access, so an ACCOUNT_DRIVEN row on a public repo is harmless; but + * not filtering means that during a public↔private visibility flip, the user + * never has a window where they're missing an ACCOUNT_DRIVEN row for a repo + * they can see upstream. + * + * @see https://developer.atlassian.com/cloud/bitbucket/changelog/#CHANGE-2770 + * @see https://developer.atlassian.com/cloud/bitbucket/rest/api-group-workspaces/#api-user-workspaces-get + * @see https://developer.atlassian.com/cloud/bitbucket/rest/api-group-repositories/#api-repositories-workspace-get */ export const getReposForAuthenticatedBitbucketCloudUser = async ( client: BitbucketClient, ): Promise> => { - const path = `/user/permissions/repositories` as CloudGetRequestPath; - - const permissions = await fetchWithRetry(() => getPaginatedCloud(path, async (p, query) => { - const { data } = await client.apiClient.GET(p, { - params: { query }, - }); - return data; - }), 'user repository permissions', logger); + // The `/user/workspaces` path is not in the openapi-fetch typed surface + // (the package's bundled types still describe the deprecated paths under + // /user/permissions/*), so we cast to CloudGetRequestPath and narrow the + // response shape locally to just the fields we use. + interface CloudUserWorkspaceAccess { + readonly workspace?: { readonly slug?: string }; + } - return permissions - .filter(p => p.repository?.uuid != null) - .map(p => ({ uuid: p.repository!.uuid as string })); + const memberships = await fetchWithRetry( + () => getPaginatedCloud( + `/user/workspaces` as CloudGetRequestPath, + async (path, query) => { + const { data } = await client.apiClient.GET(path, { params: { query } }); + return data; + }, + ), + 'user workspace memberships', + logger, + ); + + const slugs = memberships + .map(m => m.workspace?.slug) + .filter((slug): slug is string => typeof slug === 'string'); + + const reposByWorkspace = await Promise.all(slugs.map(workspace => fetchWithRetry( + () => getPaginatedCloud( + `/repositories/${workspace}` as CloudGetRequestPath, + async (path, query) => { + const { data } = await client.apiClient.GET(path, { + params: { + path: { workspace }, + query: { role: 'member', ...query }, + }, + }); + return data; + }, + ), + `repos for workspace ${workspace}`, + logger, + ))); + + return reposByWorkspace + .flat() + .filter(repo => repo.uuid != null) + .map(repo => ({ uuid: repo.uuid as string })); }; /** From 8b6556496e50c101c349eaa3757ee9be69d1a584 Mon Sep 17 00:00:00 2001 From: msukkari Date: Thu, 21 May 2026 16:01:26 -0700 Subject: [PATCH 2/3] chore: link CHANGELOG entry to #1217 Co-Authored-By: Claude Opus 4.7 (1M context) --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b163b92e..670b1d27d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Fixed issue where repo permissions could go stale when authentication or token refresh related errors occured. [#1215](https://github.com/sourcebot-dev/sourcebot/pull/1215) -- [EE] Fixed Bitbucket Cloud account-driven permission sync after Atlassian's CHANGE-2770 removed `GET /2.0/user/permissions/repositories`. +- [EE] Fixed Bitbucket Cloud account-driven permission sync after Atlassian's CHANGE-2770 removed `GET /2.0/user/permissions/repositories`. [#1217](https://github.com/sourcebot-dev/sourcebot/pull/1217) ## [4.17.2] - 2026-05-16 From c810e7c575498bf819cb18ef7a1c80a0b7025aa9 Mon Sep 17 00:00:00 2001 From: msukkari Date: Thu, 21 May 2026 16:50:10 -0700 Subject: [PATCH 3/3] fix(worker): only include private repos in BB Cloud user-repo list Matches the GitHub and GitLab branches of accountPermissionSyncer, which both pass visibility='private' for the same reason: public repos are gated by the read-side prisma filter via isPublic, so no AccountToRepoPermission row is needed for them. Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/backend/src/bitbucket.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/backend/src/bitbucket.ts b/packages/backend/src/bitbucket.ts index 1a10ff1cd..9051164a7 100644 --- a/packages/backend/src/bitbucket.ts +++ b/packages/backend/src/bitbucket.ts @@ -657,7 +657,7 @@ export const getExplicitUserPermissionsForCloudRepo = async ( }; /** - * Returns the UUIDs of all repositories accessible to the authenticated Bitbucket Cloud user. + * Returns the UUIDs of all private repositories accessible to the authenticated Bitbucket Cloud user. * Used for account-driven permission syncing. * * @see https://developer.atlassian.com/cloud/bitbucket/rest/api-group-workspaces/#api-user-workspaces-get @@ -693,7 +693,7 @@ export const getReposForAuthenticatedBitbucketCloudUser = async ( const { data } = await client.apiClient.GET(path, { params: { path: { workspace }, - query: { role: 'member', ...query }, + query: { role: 'member', q: 'is_private=true', ...query }, }, }); return data;