Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
32 changes: 31 additions & 1 deletion packages/backend/src/bitbucket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Array<{ id: string }>> => {

/**
* @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) => {
Expand Down Expand Up @@ -761,4 +780,15 @@ 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<boolean> => {
const { error } = await client.apiClient.GET(`/rest/api/1.0/profile/recent/repos` as ServerGetRequestPath, {});
return !error;
};
Comment thread
brendan-kellam marked this conversation as resolved.
2 changes: 2 additions & 0 deletions packages/backend/src/ee/accountPermissionSyncer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
4 changes: 1 addition & 3 deletions packages/backend/src/ee/repoPermissionSyncer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -338,9 +338,7 @@ export class RepoPermissionSyncer {
}
}

return {
accountIds: [],
}
throw new Error(`Unsupported code host type: ${repo.external_codeHostType}`);
})();

await this.db.$transaction([
Expand Down
Loading