Skip to content

Commit aa62847

Browse files
authored
fix(ado): Manually pass token through http header for ado server (#543)
* support passing in token manually in auth header * remove unneeded PAT embed check * cleanup authheader usage * changelog * var name typo * unset auth header in fetch * move unset to finally in fetch
1 parent 7a97d4e commit aa62847

File tree

17 files changed

+89
-40
lines changed

17 files changed

+89
-40
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Fixed
11+
- Manually pass auth token for ado server deployments. [#543](https://github.com/sourcebot-dev/sourcebot/pull/543)
12+
1013
## [4.7.2] - 2025-09-22
1114

1215
### Fixed

docs/docs/connections/ado-cloud.mdx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ If you're not familiar with Sourcebot [connections](/docs/connections/overview),
1515
```json
1616
{
1717
"type": "azuredevops",
18+
"deploymentType": "cloud",
1819
"repos": [
1920
"organizationName/projectName/repoName",
2021
"organizationName/projectName/repoName2
@@ -26,6 +27,7 @@ If you're not familiar with Sourcebot [connections](/docs/connections/overview),
2627
```json
2728
{
2829
"type": "azuredevops",
30+
"deploymentType": "cloud",
2931
"orgs": [
3032
"organizationName",
3133
"organizationName2
@@ -37,6 +39,7 @@ If you're not familiar with Sourcebot [connections](/docs/connections/overview),
3739
```json
3840
{
3941
"type": "azuredevops",
42+
"deploymentType": "cloud",
4043
"projects": [
4144
"organizationName/projectName",
4245
"organizationName/projectName2"
@@ -48,6 +51,7 @@ If you're not familiar with Sourcebot [connections](/docs/connections/overview),
4851
```json
4952
{
5053
"type": "azuredevops",
54+
"deploymentType": "cloud",
5155
// Include all repos in my-org...
5256
"orgs": [
5357
"my-org"
@@ -91,6 +95,7 @@ Next, provide the access token via the `token` property, either as an environmen
9195
```json
9296
{
9397
"type": "azuredevops",
98+
"deploymentType": "cloud",
9499
"token": {
95100
// note: this env var can be named anything. It
96101
// doesn't need to be `ADO_TOKEN`.
@@ -121,6 +126,7 @@ Next, provide the access token via the `token` property, either as an environmen
121126
```json
122127
{
123128
"type": "azuredevops",
129+
"deploymentType": "cloud",
124130
"token": {
125131
"secret": "mysecret"
126132
}

docs/docs/connections/ado-server.mdx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ If you're not familiar with Sourcebot [connections](/docs/connections/overview),
1616
```json
1717
{
1818
"type": "azuredevops",
19-
"useTfsPath": true
19+
"deploymentType": "server",
20+
"useTfsPath": true,
2021
"repos": [
2122
"organizationName/projectName/repoName",
2223
"organizationName/projectName/repoName2
@@ -28,6 +29,7 @@ If you're not familiar with Sourcebot [connections](/docs/connections/overview),
2829
```json
2930
{
3031
"type": "azuredevops",
32+
"deploymentType": "server",
3133
"repos": [
3234
"organizationName/projectName/repoName",
3335
"organizationName/projectName/repoName2
@@ -39,6 +41,7 @@ If you're not familiar with Sourcebot [connections](/docs/connections/overview),
3941
```json
4042
{
4143
"type": "azuredevops",
44+
"deploymentType": "server",
4245
"orgs": [
4346
"collectionName",
4447
"collectionName2"
@@ -50,6 +53,7 @@ If you're not familiar with Sourcebot [connections](/docs/connections/overview),
5053
```json
5154
{
5255
"type": "azuredevops",
56+
"deploymentType": "server",
5357
"projects": [
5458
"collectionName/projectName",
5559
"collectionName/projectName2"
@@ -61,6 +65,7 @@ If you're not familiar with Sourcebot [connections](/docs/connections/overview),
6165
```json
6266
{
6367
"type": "azuredevops",
68+
"deploymentType": "server",
6469
// Include all repos in my-org...
6570
"orgs": [
6671
"my-org"
@@ -104,6 +109,7 @@ Next, provide the access token via the `token` property, either as an environmen
104109
```json
105110
{
106111
"type": "azuredevops",
112+
"deploymentType": "server",
107113
"token": {
108114
// note: this env var can be named anything. It
109115
// doesn't need to be `ADO_TOKEN`.
@@ -134,6 +140,7 @@ Next, provide the access token via the `token` property, either as an environmen
134140
```json
135141
{
136142
"type": "azuredevops",
143+
"deploymentType": "server",
137144
"token": {
138145
"secret": "mysecret"
139146
}

docs/snippets/schemas/v3/azuredevops.schema.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,6 @@
6262
"cloud",
6363
"server"
6464
],
65-
"default": "cloud",
6665
"description": "The type of Azure DevOps deployment"
6766
},
6867
"useTfsPath": {
@@ -199,7 +198,8 @@
199198
},
200199
"required": [
201200
"type",
202-
"token"
201+
"token",
202+
"deploymentType"
203203
],
204204
"additionalProperties": false
205205
}

docs/snippets/schemas/v3/connection.schema.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -931,7 +931,6 @@
931931
"cloud",
932932
"server"
933933
],
934-
"default": "cloud",
935934
"description": "The type of Azure DevOps deployment"
936935
},
937936
"useTfsPath": {
@@ -1068,7 +1067,8 @@
10681067
},
10691068
"required": [
10701069
"type",
1071-
"token"
1070+
"token",
1071+
"deploymentType"
10721072
],
10731073
"additionalProperties": false
10741074
},

docs/snippets/schemas/v3/index.schema.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1214,7 +1214,6 @@
12141214
"cloud",
12151215
"server"
12161216
],
1217-
"default": "cloud",
12181217
"description": "The type of Azure DevOps deployment"
12191218
},
12201219
"useTfsPath": {
@@ -1351,7 +1350,8 @@
13511350
},
13521351
"required": [
13531352
"type",
1354-
"token"
1353+
"token",
1354+
"deploymentType"
13551355
],
13561356
"additionalProperties": false
13571357
},

packages/backend/src/git.ts

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@ type onProgressFn = (event: SimpleGitProgressEvent) => void;
77
export const cloneRepository = async (
88
{
99
cloneUrl,
10+
authHeader,
1011
path,
1112
onProgress,
1213
}: {
1314
cloneUrl: string,
15+
authHeader?: string,
1416
path: string,
1517
onProgress?: onProgressFn
1618
}
@@ -24,13 +26,12 @@ export const cloneRepository = async (
2426
path,
2527
})
2628

27-
await git.clone(
28-
cloneUrl,
29-
path,
30-
[
31-
"--bare",
32-
]
33-
);
29+
const cloneArgs = [
30+
"--bare",
31+
...(authHeader ? ["-c", `http.extraHeader=${authHeader}`] : [])
32+
];
33+
34+
await git.clone(cloneUrl, path, cloneArgs);
3435

3536
await unsetGitConfig(path, ["remote.origin.url"]);
3637
} catch (error: unknown) {
@@ -50,10 +51,12 @@ export const cloneRepository = async (
5051
export const fetchRepository = async (
5152
{
5253
cloneUrl,
54+
authHeader,
5355
path,
5456
onProgress,
5557
}: {
5658
cloneUrl: string,
59+
authHeader?: string,
5760
path: string,
5861
onProgress?: onProgressFn
5962
}
@@ -65,6 +68,10 @@ export const fetchRepository = async (
6568
path: path,
6669
})
6770

71+
if (authHeader) {
72+
await git.addConfig("http.extraHeader", authHeader);
73+
}
74+
6875
await git.fetch([
6976
cloneUrl,
7077
"+refs/heads/*:refs/heads/*",
@@ -81,6 +88,16 @@ export const fetchRepository = async (
8188
} else {
8289
throw new Error(`${baseLog}. Error: ${error}`);
8390
}
91+
} finally {
92+
if (authHeader) {
93+
const git = simpleGit({
94+
progress: onProgress,
95+
}).cwd({
96+
path: path,
97+
})
98+
99+
await git.raw(["config", "--unset", "http.extraHeader", authHeader]);
100+
}
84101
}
85102
}
86103

packages/backend/src/repoManager.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@ export class RepoManager {
175175

176176
const credentials = await getAuthCredentialsForRepo(repo, this.db);
177177
const cloneUrlMaybeWithToken = credentials?.cloneUrlWithToken ?? repo.cloneUrl;
178+
const authHeader = credentials?.authHeader ?? undefined;
178179

179180
if (existsSync(repoPath) && !isReadOnly) {
180181
// @NOTE: in #483, we changed the cloning method s.t., we _no longer_
@@ -188,6 +189,7 @@ export class RepoManager {
188189
logger.info(`Fetching ${repo.displayName}...`);
189190
const { durationMs } = await measure(() => fetchRepository({
190191
cloneUrl: cloneUrlMaybeWithToken,
192+
authHeader,
191193
path: repoPath,
192194
onProgress: ({ method, stage, progress }) => {
193195
logger.debug(`git.${method} ${stage} stage ${progress}% complete for ${repo.displayName}`)
@@ -203,6 +205,7 @@ export class RepoManager {
203205

204206
const { durationMs } = await measure(() => cloneRepository({
205207
cloneUrl: cloneUrlMaybeWithToken,
208+
authHeader,
206209
path: repoPath,
207210
onProgress: ({ method, stage, progress }) => {
208211
logger.debug(`git.${method} ${stage} stage ${progress}% complete for ${repo.displayName}`)

packages/backend/src/types.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,5 +59,6 @@ export type RepoWithConnections = Repo & { connections: (RepoToConnection & { co
5959
export type RepoAuthCredentials = {
6060
hostUrl?: string;
6161
token: string;
62-
cloneUrlWithToken: string;
62+
cloneUrlWithToken?: string;
63+
authHeader?: string;
6364
}

packages/backend/src/utils.ts

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -193,19 +193,31 @@ export const getAuthCredentialsForRepo = async (repo: RepoWithConnections, db: P
193193
const config = connection.config as unknown as AzureDevOpsConnectionConfig;
194194
if (config.token) {
195195
const token = await getTokenFromConfig(config.token, connection.orgId, db, logger);
196-
return {
197-
hostUrl: config.url,
198-
token,
199-
cloneUrlWithToken: createGitCloneUrlWithToken(
200-
repo.cloneUrl,
201-
{
202-
// @note: If we don't provide a username, the password will be set as the username. This seems to work
203-
// for ADO cloud but not for ADO server. To fix this, we set a placeholder username to ensure the password
204-
// is set correctly
205-
username: 'user',
206-
password: token
207-
}
208-
),
196+
197+
// For ADO server, multiple auth schemes may be supported. If the ADO deployment supports NTLM, the git clone will default
198+
// 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
199+
// appropriately in the header. To do this, we set the authHeader field here
200+
if (config.deploymentType === 'server') {
201+
return {
202+
hostUrl: config.url,
203+
token,
204+
authHeader: "Authorization: Basic " + Buffer.from(`:${token}`).toString('base64')
205+
}
206+
} else {
207+
return {
208+
hostUrl: config.url,
209+
token,
210+
cloneUrlWithToken: createGitCloneUrlWithToken(
211+
repo.cloneUrl,
212+
{
213+
// @note: If we don't provide a username, the password will be set as the username. This seems to work
214+
// for ADO cloud but not for ADO server. To fix this, we set a placeholder username to ensure the password
215+
// is set correctly
216+
username: 'user',
217+
password: token
218+
}
219+
),
220+
}
209221
}
210222
}
211223
}
@@ -228,4 +240,4 @@ const createGitCloneUrlWithToken = (cloneUrl: string, credentials: { username?:
228240
url.password = credentials.password;
229241
}
230242
return url.toString();
231-
}
243+
}

0 commit comments

Comments
 (0)