diff --git a/CHANGELOG.md b/CHANGELOG.md index cb87fb251..c1efc4102 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### 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) +- [EE] Fixed Bitbucket Server repos being incorrectly treated as public in Sourcebot when the instance-level `feature.public.access` flag is disabled but per-repo public flags were not reset. [#999](https://github.com/sourcebot-dev/sourcebot/pull/999) ## [4.15.5] - 2026-03-12 diff --git a/packages/backend/src/bitbucket.ts b/packages/backend/src/bitbucket.ts index af606108d..2e10f57d0 100644 --- a/packages/backend/src/bitbucket.ts +++ b/packages/backend/src/bitbucket.ts @@ -782,6 +782,33 @@ export const getUserPermissionsForServerRepo = async ( .map(entry => ({ userId: String(entry.user.id) })); }; +/** + * Checks if the Bitbucket Server instance has the `feature.public.access` flag enabled + * by making a single unauthenticated request to a repo that the API reports as public. + */ +export const isBitbucketServerPublicAccessEnabled = async ( + serverUrl: string, + publicRepo: ServerRepository, +): Promise => { + const projectKey = publicRepo.project?.key; + const repoSlug = publicRepo.slug; + if (!projectKey || !repoSlug) { + return false; + } + + const url = `${serverUrl}/rest/api/1.0/projects/${projectKey}/repos/${repoSlug}`; + try { + const response = await fetch(url, { + headers: { Accept: 'application/json' }, + // Intentionally no Authorization header - we want to test anonymous access + }); + return response.ok; + } catch (e) { + logger.warn(`Failed to probe public access for ${projectKey}/${repoSlug}: ${e}`); + return false; + } +}; + /** * 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. diff --git a/packages/backend/src/repoCompileUtils.ts b/packages/backend/src/repoCompileUtils.ts index ab8794e68..a3c9fc053 100644 --- a/packages/backend/src/repoCompileUtils.ts +++ b/packages/backend/src/repoCompileUtils.ts @@ -3,7 +3,7 @@ import { getGitHubReposFromConfig, OctokitRepository } from "./github.js"; import { getGitLabReposFromConfig } from "./gitlab.js"; import { getGiteaReposFromConfig } from "./gitea.js"; import { getGerritReposFromConfig } from "./gerrit.js"; -import { BitbucketRepository, getBitbucketReposFromConfig } from "./bitbucket.js"; +import { BitbucketRepository, getBitbucketReposFromConfig, isBitbucketServerPublicAccessEnabled } from "./bitbucket.js"; import { getAzureDevOpsReposFromConfig } from "./azuredevops.js"; import { SchemaRestRepository as BitbucketServerRepository } from "@coderabbitai/bitbucket/server/openapi"; import { SchemaRepository as BitbucketCloudRepository } from "@coderabbitai/bitbucket/cloud/openapi"; @@ -401,6 +401,22 @@ export const compileBitbucketConfig = async ( .toString() .replace(/^https?:\/\//, ''); + // For Bitbucket Server, verify that the instance-level `feature.public.access` flag is + // actually enabled. When it is disabled, per-repo `public` flags may still be stale + // (i.e., remain `true` from before the flag was turned off) but repos are no longer + // anonymously accessible. We detect this by making a single unauthenticated probe + // request to one of the repos the API reports as public. + let isServerPublicAccessEnabled = true; + if (config.deploymentType === 'server') { + const firstPublicRepo = bitbucketRepos.find(repo => (repo as BitbucketServerRepository).public === true); + if (firstPublicRepo) { + isServerPublicAccessEnabled = await isBitbucketServerPublicAccessEnabled(hostUrl, firstPublicRepo as BitbucketServerRepository); + if (!isServerPublicAccessEnabled) { + logger.warn(`Bitbucket Server at ${hostUrl} has repos marked as public but they are not anonymously accessible. The feature.public.access flag may be disabled. Treating all repos as private.`); + } + } + } + const getCloneUrl = (repo: BitbucketRepository) => { if (!repo.links) { throw new Error(`No clone links found for server repo ${repo.name}`); @@ -467,7 +483,9 @@ export const compileBitbucketConfig = async ( } })(); const externalId = isServer ? (repo as BitbucketServerRepository).id!.toString() : (repo as BitbucketCloudRepository).uuid!; - const isPublic = isServer ? (repo as BitbucketServerRepository).public : (repo as BitbucketCloudRepository).is_private === false; + const isPublic = isServer + ? (isServerPublicAccessEnabled && (repo as BitbucketServerRepository).public === true) + : (repo as BitbucketCloudRepository).is_private === false; const isArchived = isServer ? (repo as BitbucketServerRepository).archived === true : false; const isFork = isServer ? (repo as BitbucketServerRepository).origin !== undefined : (repo as BitbucketCloudRepository).parent !== undefined; const repoName = path.join(repoNameRoot, displayName);