support passing in token manually in auth header

This commit is contained in:
msukkari 2025-09-27 15:51:50 -07:00
parent 7a97d4ee06
commit 2b46e8a9d6
16 changed files with 102 additions and 39 deletions

View file

@ -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"
}

View file

@ -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"
}

View file

@ -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
}

View file

@ -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
},

View file

@ -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
},

View file

@ -1,16 +1,19 @@
import { CheckRepoActions, GitConfigScope, simpleGit, SimpleGitProgressEvent } from 'simple-git';
import { mkdir } from 'node:fs/promises';
import { env } from './env.js';
import { doesHaveEmbeddedToken } from './utils.js';
type onProgressFn = (event: SimpleGitProgressEvent) => void;
export const cloneRepository = async (
{
cloneUrl,
authHeader,
path,
onProgress,
}: {
cloneUrl: string,
authHeader?: string,
path: string,
onProgress?: onProgressFn
}
@ -24,13 +27,29 @@ export const cloneRepository = async (
path,
})
await git.clone(
cloneUrl,
path,
[
"--bare",
]
);
if (authHeader) {
if (doesHaveEmbeddedToken(cloneUrl)) {
throw new Error("Cannot use auth header when clone URL has embedded token");
}
await git.clone(
cloneUrl,
path,
[
"--bare",
"-c",
`http.extraHeader=${authHeader}`,
]
)
} else {
await git.clone(
cloneUrl,
path,
[
"--bare",
]
)
}
await unsetGitConfig(path, ["remote.origin.url"]);
} catch (error: unknown) {
@ -50,10 +69,12 @@ export const cloneRepository = async (
export const fetchRepository = async (
{
cloneUrl,
authHeader,
path,
onProgress,
}: {
cloneUrl: string,
authHeader?: string,
path: string,
onProgress?: onProgressFn
}
@ -65,6 +86,14 @@ export const fetchRepository = async (
path: path,
})
if (authHeader) {
if (doesHaveEmbeddedToken(cloneUrl)) {
throw new Error("Cannot use auth header when clone URL has embedded token");
}
await git.addConfig("http.extraHeader", authHeader);
}
await git.fetch([
cloneUrl,
"+refs/heads/*:refs/heads/*",

View file

@ -175,6 +175,7 @@ export class RepoManager {
const credentials = await getAuthCredentialsForRepo(repo, this.db);
const cloneUrlMaybeWithToken = credentials?.cloneUrlWithToken ?? repo.cloneUrl;
const authHeader = credentials?.authToken ?? 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}`)

View file

@ -59,5 +59,6 @@ export type RepoWithConnections = Repo & { connections: (RepoToConnection & { co
export type RepoAuthCredentials = {
hostUrl?: string;
token: string;
cloneUrlWithToken: string;
cloneUrlWithToken?: string;
authToken?: string;
}

View file

@ -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 authToken field here
if (config.deploymentType === 'server') {
return {
hostUrl: config.url,
token,
authToken: "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,9 @@ const createGitCloneUrlWithToken = (cloneUrl: string, credentials: { username?:
url.password = credentials.password;
}
return url.toString();
}
export const doesHaveEmbeddedToken = (cloneUrl: string) => {
const url = new URL(cloneUrl);
return url.username || url.password;
}

View file

@ -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;

View file

@ -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/...).
*/

View file

@ -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
},

View file

@ -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/...).
*/

View file

@ -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
},

View file

@ -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/...).
*/

View file

@ -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
}