Skip to content

Commit 13629e9

Browse files
fix(worker): guard against anonymous Bitbucket Server token fallback in account permission sync (#998)
* fix(worker): guard against anonymous Bitbucket Server token fallback in account permission sync Bitbucket Server instances with anonymous access enabled silently treat expired/invalid OAuth tokens as anonymous rather than returning a 401. This caused account-driven permission syncing to receive an empty repo list (200 OK) and wipe all AccountToRepoPermission records. Added isBitbucketServerUserAuthenticated() which calls /rest/api/1.0/profile/recent/repos — an endpoint that always requires authentication even when anonymous access is enabled — to detect this condition before fetching repos. Also added explicit throws for unsupported provider/code host types instead of silently returning empty results. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: update CHANGELOG for #998 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * nit * feedback --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 93199aa commit 13629e9

File tree

4 files changed

+51
-4
lines changed

4 files changed

+51
-4
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
### Added
1111
- Added generated OpenAPI documentation for the public search, repo, and file browsing API surface. [#996](https://github.com/sourcebot-dev/sourcebot/pull/996)
1212

13+
### Fixed
14+
- [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)
15+
1316
## [4.15.5] - 2026-03-12
1417

1518
### Added

packages/backend/src/bitbucket.ts

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -701,11 +701,30 @@ export const getReposForAuthenticatedBitbucketCloudUser = async (
701701
* Returns the IDs of all repositories accessible to the authenticated Bitbucket Server user.
702702
* Used for account-driven permission syncing.
703703
*
704-
* @see https://developer.atlassian.com/server/bitbucket/rest/v906/api-group-repository/#api-rest-api-latest-repos-get
704+
* @see https://developer.atlassian.com/server/bitbucket/rest/v906/api-group-repository/#api-api-latest-repos-get
705705
*/
706706
export const getReposForAuthenticatedBitbucketServerUser = async (
707707
client: BitbucketClient,
708708
): Promise<Array<{ id: string }>> => {
709+
710+
/**
711+
* @note We need to explicitly check if the user is authenticated here because
712+
* /rest/api/1.0/repos?permission=REPO_READ will return an empty list if the
713+
* following conditions are met:
714+
* 1. Anonymous access is enabled via `feature.public.access`
715+
* 2. The token is expired or invalid.
716+
*
717+
* This check ensures we will not hit this condition and instead fail with a
718+
* explicit error.
719+
*
720+
* @see https://developer.atlassian.com/server/bitbucket/rest/v906/api-group-repository/#api-api-latest-repos-get
721+
* @see https://confluence.atlassian.com/bitbucketserver/configuration-properties-776640155.html
722+
*/
723+
const isAuthenticated = await isBitbucketServerUserAuthenticated(client);
724+
if (!isAuthenticated) {
725+
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.`);
726+
}
727+
709728
const repos = await fetchWithRetry(() => getPaginatedServer<{ id: number }>(
710729
`/rest/api/1.0/repos` as ServerGetRequestPath,
711730
async (url, start) => {
@@ -761,4 +780,29 @@ export const getUserPermissionsForServerRepo = async (
761780
return repoUsers
762781
.filter(entry => entry.user?.id != null)
763782
.map(entry => ({ userId: String(entry.user.id) }));
783+
};
784+
785+
/**
786+
* Returns true if the Bitbucket Server client is authenticated as a real user,
787+
* false if the token is expired, invalid, or the request is being treated as anonymous.
788+
*/
789+
export const isBitbucketServerUserAuthenticated = async (
790+
client: BitbucketClient,
791+
): Promise<boolean> => {
792+
try {
793+
const { error, response } = await client.apiClient.GET(`/rest/api/1.0/profile/recent/repos` as ServerGetRequestPath, {});
794+
if (error) {
795+
if (response.status === 401 || response.status === 403) {
796+
return false;
797+
}
798+
throw new Error(`Unexpected error when verifying Bitbucket Server authentication status: ${JSON.stringify(error)}`);
799+
}
800+
return true;
801+
} catch (e: any) {
802+
// Handle the case where openapi-fetch throws directly for auth errors
803+
if (e?.status === 401 || e?.status === 403) {
804+
return false;
805+
}
806+
throw e;
807+
}
764808
};

packages/backend/src/ee/accountPermissionSyncer.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,8 @@ export class AccountPermissionSyncer {
330330
});
331331

332332
repos.forEach(repo => aggregatedRepoIds.add(repo.id));
333+
} else {
334+
throw new Error(`Unsupported code host type: ${account.provider}`);
333335
}
334336

335337
return Array.from(aggregatedRepoIds);

packages/backend/src/ee/repoPermissionSyncer.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -338,9 +338,7 @@ export class RepoPermissionSyncer {
338338
}
339339
}
340340

341-
return {
342-
accountIds: [],
343-
}
341+
throw new Error(`Unsupported code host type: ${repo.external_codeHostType}`);
344342
})();
345343

346344
await this.db.$transaction([

0 commit comments

Comments
 (0)