diff --git a/CHANGELOG.md b/CHANGELOG.md index 403cda7e6..cb87fb251 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Added generated OpenAPI documentation for the public search, repo, and file browsing API surface. [#996](https://github.com/sourcebot-dev/sourcebot/pull/996) +### Fixed +- [EE] Fixed account-driven permission sync silently wiping all Bitbucket Server repository permissions when the OAuth token expires on instances with anonymous access enabled. [#998](https://github.com/sourcebot-dev/sourcebot/pull/998) + ## [4.15.5] - 2026-03-12 ### Added diff --git a/packages/backend/src/bitbucket.ts b/packages/backend/src/bitbucket.ts index 708f55bc3..af606108d 100644 --- a/packages/backend/src/bitbucket.ts +++ b/packages/backend/src/bitbucket.ts @@ -701,11 +701,30 @@ export const getReposForAuthenticatedBitbucketCloudUser = async ( * Returns the IDs of all repositories accessible to the authenticated Bitbucket Server user. * Used for account-driven permission syncing. * - * @see https://developer.atlassian.com/server/bitbucket/rest/v906/api-group-repository/#api-rest-api-latest-repos-get + * @see https://developer.atlassian.com/server/bitbucket/rest/v906/api-group-repository/#api-api-latest-repos-get */ export const getReposForAuthenticatedBitbucketServerUser = async ( client: BitbucketClient, ): Promise> => { + + /** + * @note We need to explicitly check if the user is authenticated here because + * /rest/api/1.0/repos?permission=REPO_READ will return an empty list if the + * following conditions are met: + * 1. Anonymous access is enabled via `feature.public.access` + * 2. The token is expired or invalid. + * + * This check ensures we will not hit this condition and instead fail with a + * explicit error. + * + * @see https://developer.atlassian.com/server/bitbucket/rest/v906/api-group-repository/#api-api-latest-repos-get + * @see https://confluence.atlassian.com/bitbucketserver/configuration-properties-776640155.html + */ + const isAuthenticated = await isBitbucketServerUserAuthenticated(client); + if (!isAuthenticated) { + throw new Error(`Bitbucket Server authentication check failed. The OAuth token may be expired and the server may be treating the request as anonymous. Please re-authenticate with Bitbucket Server.`); + } + const repos = await fetchWithRetry(() => getPaginatedServer<{ id: number }>( `/rest/api/1.0/repos` as ServerGetRequestPath, async (url, start) => { @@ -761,4 +780,29 @@ export const getUserPermissionsForServerRepo = async ( return repoUsers .filter(entry => entry.user?.id != null) .map(entry => ({ userId: String(entry.user.id) })); +}; + +/** + * Returns true if the Bitbucket Server client is authenticated as a real user, + * false if the token is expired, invalid, or the request is being treated as anonymous. + */ +export const isBitbucketServerUserAuthenticated = async ( + client: BitbucketClient, +): Promise => { + try { + const { error, response } = await client.apiClient.GET(`/rest/api/1.0/profile/recent/repos` as ServerGetRequestPath, {}); + if (error) { + if (response.status === 401 || response.status === 403) { + return false; + } + throw new Error(`Unexpected error when verifying Bitbucket Server authentication status: ${JSON.stringify(error)}`); + } + return true; + } catch (e: any) { + // Handle the case where openapi-fetch throws directly for auth errors + if (e?.status === 401 || e?.status === 403) { + return false; + } + throw e; + } }; \ No newline at end of file diff --git a/packages/backend/src/ee/accountPermissionSyncer.ts b/packages/backend/src/ee/accountPermissionSyncer.ts index bddf10df7..87a49cc42 100644 --- a/packages/backend/src/ee/accountPermissionSyncer.ts +++ b/packages/backend/src/ee/accountPermissionSyncer.ts @@ -330,6 +330,8 @@ export class AccountPermissionSyncer { }); repos.forEach(repo => aggregatedRepoIds.add(repo.id)); + } else { + throw new Error(`Unsupported code host type: ${account.provider}`); } return Array.from(aggregatedRepoIds); diff --git a/packages/backend/src/ee/repoPermissionSyncer.ts b/packages/backend/src/ee/repoPermissionSyncer.ts index 406dbba45..a6f42bf17 100644 --- a/packages/backend/src/ee/repoPermissionSyncer.ts +++ b/packages/backend/src/ee/repoPermissionSyncer.ts @@ -338,9 +338,7 @@ export class RepoPermissionSyncer { } } - return { - accountIds: [], - } + throw new Error(`Unsupported code host type: ${repo.external_codeHostType}`); })(); await this.db.$transaction([