mirror of
https://github.com/sourcebot-dev/sourcebot.git
synced 2025-12-12 04:15:30 +00:00
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
This commit is contained in:
parent
7a97d4ee06
commit
aa62847143
17 changed files with 89 additions and 40 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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}`)
|
||||
|
|
|
|||
|
|
@ -59,5 +59,6 @@ export type RepoWithConnections = Repo & { connections: (RepoToConnection & { co
|
|||
export type RepoAuthCredentials = {
|
||||
hostUrl?: string;
|
||||
token: string;
|
||||
cloneUrlWithToken: string;
|
||||
cloneUrlWithToken?: string;
|
||||
authHeader?: string;
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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/...).
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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/...).
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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/...).
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
Loading…
Reference in a new issue