diff --git a/CHANGELOG.md b/CHANGELOG.md index c54d1c341..1cc25277c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed +- Manually pass auth token for ado server deployments. [#543](https://github.com/sourcebot-dev/sourcebot/pull/543) + ## [4.7.2] - 2025-09-22 ### Fixed diff --git a/docs/docs/connections/ado-cloud.mdx b/docs/docs/connections/ado-cloud.mdx index d4e51bd61..cc20c7450 100644 --- a/docs/docs/connections/ado-cloud.mdx +++ b/docs/docs/connections/ado-cloud.mdx @@ -15,6 +15,7 @@ If you're not familiar with Sourcebot [connections](/docs/connections/overview), ```json { "type": "azuredevops", + "deploymentType": "cloud", "repos": [ "organizationName/projectName/repoName", "organizationName/projectName/repoName2 @@ -26,6 +27,7 @@ If you're not familiar with Sourcebot [connections](/docs/connections/overview), ```json { "type": "azuredevops", + "deploymentType": "cloud", "orgs": [ "organizationName", "organizationName2 @@ -37,6 +39,7 @@ If you're not familiar with Sourcebot [connections](/docs/connections/overview), ```json { "type": "azuredevops", + "deploymentType": "cloud", "projects": [ "organizationName/projectName", "organizationName/projectName2" @@ -48,6 +51,7 @@ If you're not familiar with Sourcebot [connections](/docs/connections/overview), ```json { "type": "azuredevops", + "deploymentType": "cloud", // Include all repos in my-org... "orgs": [ "my-org" @@ -91,6 +95,7 @@ Next, provide the access token via the `token` property, either as an environmen ```json { "type": "azuredevops", + "deploymentType": "cloud", "token": { // note: this env var can be named anything. It // doesn't need to be `ADO_TOKEN`. @@ -121,6 +126,7 @@ Next, provide the access token via the `token` property, either as an environmen ```json { "type": "azuredevops", + "deploymentType": "cloud", "token": { "secret": "mysecret" } diff --git a/docs/docs/connections/ado-server.mdx b/docs/docs/connections/ado-server.mdx index b2586056f..1cfc02525 100644 --- a/docs/docs/connections/ado-server.mdx +++ b/docs/docs/connections/ado-server.mdx @@ -16,7 +16,8 @@ If you're not familiar with Sourcebot [connections](/docs/connections/overview), ```json { "type": "azuredevops", - "useTfsPath": true + "deploymentType": "server", + "useTfsPath": true, "repos": [ "organizationName/projectName/repoName", "organizationName/projectName/repoName2 @@ -28,6 +29,7 @@ If you're not familiar with Sourcebot [connections](/docs/connections/overview), ```json { "type": "azuredevops", + "deploymentType": "server", "repos": [ "organizationName/projectName/repoName", "organizationName/projectName/repoName2 @@ -39,6 +41,7 @@ If you're not familiar with Sourcebot [connections](/docs/connections/overview), ```json { "type": "azuredevops", + "deploymentType": "server", "orgs": [ "collectionName", "collectionName2" @@ -50,6 +53,7 @@ If you're not familiar with Sourcebot [connections](/docs/connections/overview), ```json { "type": "azuredevops", + "deploymentType": "server", "projects": [ "collectionName/projectName", "collectionName/projectName2" @@ -61,6 +65,7 @@ If you're not familiar with Sourcebot [connections](/docs/connections/overview), ```json { "type": "azuredevops", + "deploymentType": "server", // Include all repos in my-org... "orgs": [ "my-org" @@ -104,6 +109,7 @@ Next, provide the access token via the `token` property, either as an environmen ```json { "type": "azuredevops", + "deploymentType": "server", "token": { // note: this env var can be named anything. It // doesn't need to be `ADO_TOKEN`. @@ -134,6 +140,7 @@ Next, provide the access token via the `token` property, either as an environmen ```json { "type": "azuredevops", + "deploymentType": "server", "token": { "secret": "mysecret" } diff --git a/docs/snippets/schemas/v3/azuredevops.schema.mdx b/docs/snippets/schemas/v3/azuredevops.schema.mdx index a36132ff7..fab796de2 100644 --- a/docs/snippets/schemas/v3/azuredevops.schema.mdx +++ b/docs/snippets/schemas/v3/azuredevops.schema.mdx @@ -62,7 +62,6 @@ "cloud", "server" ], - "default": "cloud", "description": "The type of Azure DevOps deployment" }, "useTfsPath": { @@ -199,7 +198,8 @@ }, "required": [ "type", - "token" + "token", + "deploymentType" ], "additionalProperties": false } diff --git a/docs/snippets/schemas/v3/connection.schema.mdx b/docs/snippets/schemas/v3/connection.schema.mdx index 3203cadd5..48750ae18 100644 --- a/docs/snippets/schemas/v3/connection.schema.mdx +++ b/docs/snippets/schemas/v3/connection.schema.mdx @@ -931,7 +931,6 @@ "cloud", "server" ], - "default": "cloud", "description": "The type of Azure DevOps deployment" }, "useTfsPath": { @@ -1068,7 +1067,8 @@ }, "required": [ "type", - "token" + "token", + "deploymentType" ], "additionalProperties": false }, diff --git a/docs/snippets/schemas/v3/index.schema.mdx b/docs/snippets/schemas/v3/index.schema.mdx index 82e18d114..9df06a9f1 100644 --- a/docs/snippets/schemas/v3/index.schema.mdx +++ b/docs/snippets/schemas/v3/index.schema.mdx @@ -1214,7 +1214,6 @@ "cloud", "server" ], - "default": "cloud", "description": "The type of Azure DevOps deployment" }, "useTfsPath": { @@ -1351,7 +1350,8 @@ }, "required": [ "type", - "token" + "token", + "deploymentType" ], "additionalProperties": false }, diff --git a/packages/backend/src/git.ts b/packages/backend/src/git.ts index 3f24b00e7..c11106259 100644 --- a/packages/backend/src/git.ts +++ b/packages/backend/src/git.ts @@ -7,10 +7,12 @@ type onProgressFn = (event: SimpleGitProgressEvent) => void; export const cloneRepository = async ( { cloneUrl, + authHeader, path, onProgress, }: { cloneUrl: string, + authHeader?: string, path: string, onProgress?: onProgressFn } @@ -24,13 +26,12 @@ export const cloneRepository = async ( path, }) - await git.clone( - cloneUrl, - path, - [ - "--bare", - ] - ); + const cloneArgs = [ + "--bare", + ...(authHeader ? ["-c", `http.extraHeader=${authHeader}`] : []) + ]; + + await git.clone(cloneUrl, path, cloneArgs); await unsetGitConfig(path, ["remote.origin.url"]); } catch (error: unknown) { @@ -50,10 +51,12 @@ export const cloneRepository = async ( export const fetchRepository = async ( { cloneUrl, + authHeader, path, onProgress, }: { cloneUrl: string, + authHeader?: string, path: string, onProgress?: onProgressFn } @@ -65,6 +68,10 @@ export const fetchRepository = async ( path: path, }) + if (authHeader) { + await git.addConfig("http.extraHeader", authHeader); + } + await git.fetch([ cloneUrl, "+refs/heads/*:refs/heads/*", @@ -81,6 +88,16 @@ export const fetchRepository = async ( } else { throw new Error(`${baseLog}. Error: ${error}`); } + } finally { + if (authHeader) { + const git = simpleGit({ + progress: onProgress, + }).cwd({ + path: path, + }) + + await git.raw(["config", "--unset", "http.extraHeader", authHeader]); + } } } diff --git a/packages/backend/src/repoManager.ts b/packages/backend/src/repoManager.ts index 12bf71731..89e41673e 100644 --- a/packages/backend/src/repoManager.ts +++ b/packages/backend/src/repoManager.ts @@ -175,6 +175,7 @@ export class RepoManager { const credentials = await getAuthCredentialsForRepo(repo, this.db); const cloneUrlMaybeWithToken = credentials?.cloneUrlWithToken ?? repo.cloneUrl; + const authHeader = credentials?.authHeader ?? undefined; if (existsSync(repoPath) && !isReadOnly) { // @NOTE: in #483, we changed the cloning method s.t., we _no longer_ @@ -188,6 +189,7 @@ export class RepoManager { logger.info(`Fetching ${repo.displayName}...`); const { durationMs } = await measure(() => fetchRepository({ cloneUrl: cloneUrlMaybeWithToken, + authHeader, path: repoPath, onProgress: ({ method, stage, progress }) => { logger.debug(`git.${method} ${stage} stage ${progress}% complete for ${repo.displayName}`) @@ -203,6 +205,7 @@ export class RepoManager { const { durationMs } = await measure(() => cloneRepository({ cloneUrl: cloneUrlMaybeWithToken, + authHeader, path: repoPath, onProgress: ({ method, stage, progress }) => { logger.debug(`git.${method} ${stage} stage ${progress}% complete for ${repo.displayName}`) diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts index 0a9e76ff6..2ea42d04b 100644 --- a/packages/backend/src/types.ts +++ b/packages/backend/src/types.ts @@ -59,5 +59,6 @@ export type RepoWithConnections = Repo & { connections: (RepoToConnection & { co export type RepoAuthCredentials = { hostUrl?: string; token: string; - cloneUrlWithToken: string; + cloneUrlWithToken?: string; + authHeader?: string; } \ No newline at end of file diff --git a/packages/backend/src/utils.ts b/packages/backend/src/utils.ts index 6ea8e5ff3..e6ac5f935 100644 --- a/packages/backend/src/utils.ts +++ b/packages/backend/src/utils.ts @@ -193,19 +193,31 @@ export const getAuthCredentialsForRepo = async (repo: RepoWithConnections, db: P const config = connection.config as unknown as AzureDevOpsConnectionConfig; if (config.token) { const token = await getTokenFromConfig(config.token, connection.orgId, db, logger); - return { - hostUrl: config.url, - token, - cloneUrlWithToken: createGitCloneUrlWithToken( - repo.cloneUrl, - { - // @note: If we don't provide a username, the password will be set as the username. This seems to work - // for ADO cloud but not for ADO server. To fix this, we set a placeholder username to ensure the password - // is set correctly - username: 'user', - password: token - } - ), + + // For ADO server, multiple auth schemes may be supported. If the ADO deployment supports NTLM, the git clone will default + // to this over basic auth. As a result, we cannot embed the token in the clone URL and must force basic auth by passing in the token + // appropriately in the header. To do this, we set the authHeader field here + if (config.deploymentType === 'server') { + return { + hostUrl: config.url, + token, + authHeader: "Authorization: Basic " + Buffer.from(`:${token}`).toString('base64') + } + } else { + return { + hostUrl: config.url, + token, + cloneUrlWithToken: createGitCloneUrlWithToken( + repo.cloneUrl, + { + // @note: If we don't provide a username, the password will be set as the username. This seems to work + // for ADO cloud but not for ADO server. To fix this, we set a placeholder username to ensure the password + // is set correctly + username: 'user', + password: token + } + ), + } } } } @@ -228,4 +240,4 @@ const createGitCloneUrlWithToken = (cloneUrl: string, credentials: { username?: url.password = credentials.password; } return url.toString(); -} \ No newline at end of file +} diff --git a/packages/schemas/src/v3/azuredevops.schema.ts b/packages/schemas/src/v3/azuredevops.schema.ts index 02bc989df..3b36fbed1 100644 --- a/packages/schemas/src/v3/azuredevops.schema.ts +++ b/packages/schemas/src/v3/azuredevops.schema.ts @@ -61,7 +61,6 @@ const schema = { "cloud", "server" ], - "default": "cloud", "description": "The type of Azure DevOps deployment" }, "useTfsPath": { @@ -198,7 +197,8 @@ const schema = { }, "required": [ "type", - "token" + "token", + "deploymentType" ], "additionalProperties": false } as const; diff --git a/packages/schemas/src/v3/azuredevops.type.ts b/packages/schemas/src/v3/azuredevops.type.ts index eec963bc1..b6ef68da7 100644 --- a/packages/schemas/src/v3/azuredevops.type.ts +++ b/packages/schemas/src/v3/azuredevops.type.ts @@ -28,7 +28,7 @@ export interface AzureDevOpsConnectionConfig { /** * The type of Azure DevOps deployment */ - deploymentType?: "cloud" | "server"; + deploymentType: "cloud" | "server"; /** * Use legacy TFS path format (/tfs) in API URLs. Required for older TFS installations (TFS 2018 and earlier). When true, API URLs will include /tfs in the path (e.g., https://server/tfs/collection/_apis/...). */ diff --git a/packages/schemas/src/v3/connection.schema.ts b/packages/schemas/src/v3/connection.schema.ts index cb8676943..e0bcbc48a 100644 --- a/packages/schemas/src/v3/connection.schema.ts +++ b/packages/schemas/src/v3/connection.schema.ts @@ -930,7 +930,6 @@ const schema = { "cloud", "server" ], - "default": "cloud", "description": "The type of Azure DevOps deployment" }, "useTfsPath": { @@ -1067,7 +1066,8 @@ const schema = { }, "required": [ "type", - "token" + "token", + "deploymentType" ], "additionalProperties": false }, diff --git a/packages/schemas/src/v3/connection.type.ts b/packages/schemas/src/v3/connection.type.ts index 7922ef208..df31c465c 100644 --- a/packages/schemas/src/v3/connection.type.ts +++ b/packages/schemas/src/v3/connection.type.ts @@ -340,7 +340,7 @@ export interface AzureDevOpsConnectionConfig { /** * The type of Azure DevOps deployment */ - deploymentType?: "cloud" | "server"; + deploymentType: "cloud" | "server"; /** * Use legacy TFS path format (/tfs) in API URLs. Required for older TFS installations (TFS 2018 and earlier). When true, API URLs will include /tfs in the path (e.g., https://server/tfs/collection/_apis/...). */ diff --git a/packages/schemas/src/v3/index.schema.ts b/packages/schemas/src/v3/index.schema.ts index c8fe48e89..38ec2f0a0 100644 --- a/packages/schemas/src/v3/index.schema.ts +++ b/packages/schemas/src/v3/index.schema.ts @@ -1213,7 +1213,6 @@ const schema = { "cloud", "server" ], - "default": "cloud", "description": "The type of Azure DevOps deployment" }, "useTfsPath": { @@ -1350,7 +1349,8 @@ const schema = { }, "required": [ "type", - "token" + "token", + "deploymentType" ], "additionalProperties": false }, diff --git a/packages/schemas/src/v3/index.type.ts b/packages/schemas/src/v3/index.type.ts index 2bea94538..4e8982dc0 100644 --- a/packages/schemas/src/v3/index.type.ts +++ b/packages/schemas/src/v3/index.type.ts @@ -473,7 +473,7 @@ export interface AzureDevOpsConnectionConfig { /** * The type of Azure DevOps deployment */ - deploymentType?: "cloud" | "server"; + deploymentType: "cloud" | "server"; /** * Use legacy TFS path format (/tfs) in API URLs. Required for older TFS installations (TFS 2018 and earlier). When true, API URLs will include /tfs in the path (e.g., https://server/tfs/collection/_apis/...). */ diff --git a/schemas/v3/azuredevops.json b/schemas/v3/azuredevops.json index f9b6bd81d..6cc278335 100644 --- a/schemas/v3/azuredevops.json +++ b/schemas/v3/azuredevops.json @@ -30,7 +30,6 @@ "deploymentType": { "type": "string", "enum": ["cloud", "server"], - "default": "cloud", "description": "The type of Azure DevOps deployment" }, "useTfsPath": { @@ -129,7 +128,8 @@ }, "required": [ "type", - "token" + "token", + "deploymentType" ], "additionalProperties": false } \ No newline at end of file