Added support for {org}.visualstudio.com domains used by Azure DevOps
Signed-off-by: Andre Wanlin <awanlin@spotify.com> Address CodeQL comments Signed-off-by: Andre Wanlin <awanlin@spotify.com> Another correction Signed-off-by: Andre Wanlin <awanlin@spotify.com> Fixed casing Signed-off-by: Andre Wanlin <awanlin@spotify.com> Adjusted to be more secure based on feedback Signed-off-by: Andre Wanlin <awanlin@spotify.com> Tighten up endsWith Signed-off-by: Andre Wanlin <awanlin@spotify.com> Corrections to TSDoc comment Signed-off-by: Andre Wanlin <awanlin@spotify.com> Changes based on feedback Signed-off-by: Andre Wanlin <awanlin@spotify.com> Correct URL for discovery Signed-off-by: Andre Wanlin <awanlin@spotify.com> Updated docs Signed-off-by: Andre Wanlin <awanlin@spotify.com> Updated changeset Signed-off-by: Andre Wanlin <awanlin@spotify.com>
This commit is contained in:
@@ -270,4 +270,53 @@ describe('AzureUrl', () => {
|
||||
),
|
||||
).toThrow('Azure URL must point to a git repository');
|
||||
});
|
||||
|
||||
it('should work with the old visualstudio long URL', () => {
|
||||
const url = AzureUrl.fromRepoUrl(
|
||||
'https://my-org.visualstudio.com/my-project/_git/my-repo',
|
||||
);
|
||||
|
||||
expect(url.getOwner()).toBe('my-org');
|
||||
expect(url.getProject()).toBe('my-project');
|
||||
expect(url.getRepo()).toBe('my-repo');
|
||||
expect(url.getRef()).toBeUndefined();
|
||||
expect(url.getPath()).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should work with the old visualstudio long URL form with a path', () => {
|
||||
const url = AzureUrl.fromRepoUrl(
|
||||
'https://my-org.visualstudio.com/my-project/_git/my-repo?path=%2Ftest.yaml',
|
||||
);
|
||||
|
||||
expect(url.getOwner()).toBe('my-org');
|
||||
expect(url.getProject()).toBe('my-project');
|
||||
expect(url.getRepo()).toBe('my-repo');
|
||||
expect(url.getRef()).toBeUndefined();
|
||||
expect(url.getPath()).toBe('/test.yaml');
|
||||
|
||||
expect(url.toRepoUrl()).toBe(
|
||||
'https://my-org.visualstudio.com/my-project/_git/my-repo?path=%2Ftest.yaml',
|
||||
);
|
||||
expect(url.toFileUrl()).toBe(
|
||||
'https://my-org.visualstudio.com/my-project/_apis/git/repositories/my-repo/items?api-version=6.0&path=%2Ftest.yaml',
|
||||
);
|
||||
expect(url.toArchiveUrl()).toBe(
|
||||
'https://my-org.visualstudio.com/my-project/_apis/git/repositories/my-repo/items?recursionLevel=full&download=true&api-version=6.0&scopePath=%2Ftest.yaml',
|
||||
);
|
||||
expect(url.toCommitsUrl()).toBe(
|
||||
'https://my-org.visualstudio.com/my-project/_apis/git/repositories/my-repo/commits?api-version=6.0',
|
||||
);
|
||||
});
|
||||
|
||||
it('should work with the old visualstudio long URL form with a path and ref', () => {
|
||||
const url = AzureUrl.fromRepoUrl(
|
||||
'https://my-org.visualstudio.com/my-project/_git/my-repo?path=%2Ffolder&version=GBtest-branch',
|
||||
);
|
||||
|
||||
expect(url.getOwner()).toBe('my-org');
|
||||
expect(url.getProject()).toBe('my-project');
|
||||
expect(url.getRepo()).toBe('my-repo');
|
||||
expect(url.getRef()).toBe('test-branch');
|
||||
expect(url.getPath()).toBe('/folder');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { isVisualStudioDomain } from './core';
|
||||
|
||||
export class AzureUrl {
|
||||
/**
|
||||
* Parses an azure URL as copied from the browser address bar.
|
||||
@@ -28,7 +30,12 @@ export class AzureUrl {
|
||||
let repo;
|
||||
|
||||
const parts = url.pathname.split('/').map(part => decodeURIComponent(part));
|
||||
if (parts[2] === '_git') {
|
||||
|
||||
if (isVisualStudioDomain(url.origin) && parts[2] === '_git') {
|
||||
owner = url.host.split('.')[0];
|
||||
project = parts[1];
|
||||
repo = parts[3];
|
||||
} else if (parts[2] === '_git') {
|
||||
owner = parts[1];
|
||||
project = repo = parts[3];
|
||||
} else if (parts[3] === '_git') {
|
||||
@@ -102,7 +109,9 @@ export class AzureUrl {
|
||||
*/
|
||||
toRepoUrl(): string {
|
||||
let url;
|
||||
if (this.#project === this.#repo) {
|
||||
if (isVisualStudioDomain(this.#origin)) {
|
||||
url = this.#baseUrl(this.#project, '_git', this.#repo);
|
||||
} else if (this.#project === this.#repo) {
|
||||
url = this.#baseUrl(this.#owner, '_git', this.#repo);
|
||||
} else {
|
||||
url = this.#baseUrl(this.#owner, this.#project, '_git', this.#repo);
|
||||
@@ -130,15 +139,28 @@ export class AzureUrl {
|
||||
);
|
||||
}
|
||||
|
||||
const url = this.#baseUrl(
|
||||
this.#owner,
|
||||
this.#project,
|
||||
'_apis',
|
||||
'git',
|
||||
'repositories',
|
||||
this.#repo,
|
||||
'items',
|
||||
);
|
||||
let url;
|
||||
if (isVisualStudioDomain(this.#origin)) {
|
||||
url = this.#baseUrl(
|
||||
this.#project,
|
||||
'_apis',
|
||||
'git',
|
||||
'repositories',
|
||||
this.#repo,
|
||||
'items',
|
||||
);
|
||||
} else {
|
||||
url = this.#baseUrl(
|
||||
this.#owner,
|
||||
this.#project,
|
||||
'_apis',
|
||||
'git',
|
||||
'repositories',
|
||||
this.#repo,
|
||||
'items',
|
||||
);
|
||||
}
|
||||
|
||||
url.searchParams.set('api-version', '6.0');
|
||||
url.searchParams.set('path', this.#path);
|
||||
|
||||
@@ -155,15 +177,28 @@ export class AzureUrl {
|
||||
* Throws an error if the URL does not point to a repo.
|
||||
*/
|
||||
toArchiveUrl(): string {
|
||||
const url = this.#baseUrl(
|
||||
this.#owner,
|
||||
this.#project,
|
||||
'_apis',
|
||||
'git',
|
||||
'repositories',
|
||||
this.#repo,
|
||||
'items',
|
||||
);
|
||||
let url;
|
||||
if (isVisualStudioDomain(this.#origin)) {
|
||||
url = this.#baseUrl(
|
||||
this.#project,
|
||||
'_apis',
|
||||
'git',
|
||||
'repositories',
|
||||
this.#repo,
|
||||
'items',
|
||||
);
|
||||
} else {
|
||||
url = this.#baseUrl(
|
||||
this.#owner,
|
||||
this.#project,
|
||||
'_apis',
|
||||
'git',
|
||||
'repositories',
|
||||
this.#repo,
|
||||
'items',
|
||||
);
|
||||
}
|
||||
|
||||
url.searchParams.set('recursionLevel', 'full');
|
||||
url.searchParams.set('download', 'true');
|
||||
url.searchParams.set('api-version', '6.0');
|
||||
@@ -184,15 +219,28 @@ export class AzureUrl {
|
||||
* Throws an error if the URL does not point to a commit.
|
||||
*/
|
||||
toCommitsUrl(): string {
|
||||
const url = this.#baseUrl(
|
||||
this.#owner,
|
||||
this.#project,
|
||||
'_apis',
|
||||
'git',
|
||||
'repositories',
|
||||
this.#repo,
|
||||
'commits',
|
||||
);
|
||||
let url;
|
||||
if (isVisualStudioDomain(this.#origin)) {
|
||||
url = this.#baseUrl(
|
||||
this.#project,
|
||||
'_apis',
|
||||
'git',
|
||||
'repositories',
|
||||
this.#repo,
|
||||
'commits',
|
||||
);
|
||||
} else {
|
||||
url = this.#baseUrl(
|
||||
this.#owner,
|
||||
this.#project,
|
||||
'_apis',
|
||||
'git',
|
||||
'repositories',
|
||||
this.#repo,
|
||||
'commits',
|
||||
);
|
||||
}
|
||||
|
||||
url.searchParams.set('api-version', '6.0');
|
||||
|
||||
if (this.#ref) {
|
||||
|
||||
@@ -445,5 +445,25 @@ describe('DefaultAzureDevOpsCredentialProvider', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('Azure DevOps (visualstudio.com)', () => {
|
||||
it('Should return a token when a credential with the same organization is specified', async () => {
|
||||
const provider = buildProvider([
|
||||
{
|
||||
host: 'org1.visualstudio.com',
|
||||
credentials: [
|
||||
{
|
||||
personalAccessToken: 'pat',
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
const credentials = provider.getCredentials({
|
||||
url: 'https://org1.visualstudio.com/project1',
|
||||
});
|
||||
|
||||
expect(credentials).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
import { CachedAzureDevOpsCredentialsProvider } from './CachedAzureDevOpsCredentialsProvider';
|
||||
import { ScmIntegrationRegistry } from '../registry';
|
||||
import { DefaultAzureCredential } from '@azure/identity';
|
||||
import { isVisualStudioDomain } from './core';
|
||||
|
||||
/**
|
||||
* Default implementation of AzureDevOpsCredentialsProvider.
|
||||
@@ -110,6 +111,17 @@ export class DefaultAzureDevOpsCredentialsProvider
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private forVisualStudioOrganization(
|
||||
url: URL,
|
||||
): AzureDevOpsCredentialsProvider | undefined {
|
||||
const parts = url.host.split('.');
|
||||
if (isVisualStudioDomain(url.origin) && parts.length > 0) {
|
||||
return this.providers.get(url.host);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private forHost(url: URL): AzureDevOpsCredentialsProvider | undefined {
|
||||
return this.providers.get(url.host);
|
||||
}
|
||||
@@ -121,6 +133,7 @@ export class DefaultAzureDevOpsCredentialsProvider
|
||||
const provider =
|
||||
this.forAzureDevOpsOrganization(url) ??
|
||||
this.forAzureDevOpsServerOrganization(url) ??
|
||||
this.forVisualStudioOrganization(url) ??
|
||||
this.forHost(url);
|
||||
|
||||
if (provider === undefined) {
|
||||
|
||||
@@ -53,3 +53,14 @@ export function getAzureDownloadUrl(url: string): string {
|
||||
export function getAzureCommitsUrl(url: string): string {
|
||||
return AzureUrl.fromRepoUrl(url).toCommitsUrl();
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a URL, return true if it contains `.visualstudio.com` and false if it does not
|
||||
* URLs can be in these two formats: `dev.azure.com/{org}` or the legacy `{org}.visualstudio.com`
|
||||
*
|
||||
* @param origin - A URL origin string pointing to an Azure DevOps instance
|
||||
* @public
|
||||
*/
|
||||
export function isVisualStudioDomain(origin: string): boolean {
|
||||
return origin.endsWith('.visualstudio.com');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user