Skip to content

Commit e51e1a4

Browse files
authored
azure-repos: fix org name in userinfo (#1522)
Fix a bug in the Azure Repos host provider that prevented the Azure DevOps organisation name from being pulled from the userinfo part of the remote URL. When creating the remote URL from Git input in multiple places we had not been preserving the userinfo part that was subsequently passed to the `CreateOrganizationUri` helper method to extract the org URL. Also add an additional test for PAT mode to cover this use case. Fixes #1520
2 parents 541712a + 4041a64 commit e51e1a4

File tree

2 files changed

+54
-15
lines changed

2 files changed

+54
-15
lines changed

src/shared/Microsoft.AzureRepos.Tests/AzureReposHostProviderTests.cs

+44-5
Original file line numberDiff line numberDiff line change
@@ -239,17 +239,14 @@ public async Task AzureReposProvider_GetCredentialAsync_JwtMode_CachedAuthority_
239239
[Fact]
240240
public async Task AzureReposProvider_GetCredentialAsync_JwtMode_CachedAuthority_DevAzureUrlOrgName_ReturnsCredential()
241241
{
242-
243242
var input = new InputArguments(new Dictionary<string, string>
244243
{
245244
["protocol"] = "https",
246245
["host"] = "dev.azure.com",
247-
["path"] = "org/project/_git/repo",
248246
["username"] = "org"
249247
});
250248

251249
var expectedOrgUri = new Uri("https://dev.azure.com/org");
252-
var remoteUri = new Uri("https://dev.azure.com/org/project/_git/repo");
253250
var authorityUrl = "https://login.microsoftonline.com/common";
254251
var expectedClientId = AzureDevOpsConstants.AadClientId;
255252
var expectedRedirectUri = AzureDevOpsConstants.AadRedirectUri;
@@ -385,7 +382,6 @@ public async Task AzureReposProvider_GetCredentialAsync_JwtMode_CachedAuthority_
385382
[Fact]
386383
public async Task AzureReposProvider_GetCredentialAsync_JwtMode_NoCachedAuthority_NoUser_ReturnsCredential()
387384
{
388-
389385
var input = new InputArguments(new Dictionary<string, string>
390386
{
391387
["protocol"] = "https",
@@ -433,9 +429,52 @@ public async Task AzureReposProvider_GetCredentialAsync_JwtMode_NoCachedAuthorit
433429
}
434430

435431
[Fact]
436-
public async Task AzureReposProvider_GetCredentialAsync_PatMode_NoExistingPat_GeneratesCredential()
432+
public async Task AzureReposProvider_GetCredentialAsync_PatMode_OrgInUserName_NoExistingPat_GeneratesCredential()
437433
{
434+
var input = new InputArguments(new Dictionary<string, string>
435+
{
436+
["protocol"] = "https",
437+
["host"] = "dev.azure.com",
438+
["username"] = "org"
439+
});
440+
441+
var expectedOrgUri = new Uri("https://dev.azure.com/org");
442+
var authorityUrl = "https://login.microsoftonline.com/common";
443+
var expectedClientId = AzureDevOpsConstants.AadClientId;
444+
var expectedRedirectUri = AzureDevOpsConstants.AadRedirectUri;
445+
var expectedScopes = AzureDevOpsConstants.AzureDevOpsDefaultScopes;
446+
var accessToken = "ACCESS-TOKEN";
447+
var personalAccessToken = "PERSONAL-ACCESS-TOKEN";
448+
var account = "john.doe";
449+
var authResult = CreateAuthResult(account, accessToken);
450+
451+
var context = new TestCommandContext();
452+
453+
var azDevOpsMock = new Mock<IAzureDevOpsRestApi>(MockBehavior.Strict);
454+
azDevOpsMock.Setup(x => x.GetAuthorityAsync(expectedOrgUri)).ReturnsAsync(authorityUrl);
455+
azDevOpsMock.Setup(x => x.CreatePersonalAccessTokenAsync(expectedOrgUri, accessToken, It.IsAny<IEnumerable<string>>()))
456+
.ReturnsAsync(personalAccessToken);
457+
458+
var msAuthMock = new Mock<IMicrosoftAuthentication>(MockBehavior.Strict);
459+
msAuthMock.Setup(x => x.GetTokenForUserAsync(authorityUrl, expectedClientId, expectedRedirectUri, expectedScopes, null, true))
460+
.ReturnsAsync(authResult);
461+
462+
var authorityCacheMock = new Mock<IAzureDevOpsAuthorityCache>(MockBehavior.Strict);
463+
464+
var userMgrMock = new Mock<IAzureReposBindingManager>(MockBehavior.Strict);
438465

466+
var provider = new AzureReposHostProvider(context, azDevOpsMock.Object, msAuthMock.Object, authorityCacheMock.Object, userMgrMock.Object);
467+
468+
ICredential credential = await provider.GetCredentialAsync(input);
469+
470+
Assert.NotNull(credential);
471+
Assert.Equal(account, credential.Account);
472+
Assert.Equal(personalAccessToken, credential.Password);
473+
}
474+
475+
[Fact]
476+
public async Task AzureReposProvider_GetCredentialAsync_PatMode_NoExistingPat_GeneratesCredential()
477+
{
439478
var input = new InputArguments(new Dictionary<string, string>
440479
{
441480
["protocol"] = "https",

src/shared/Microsoft.AzureRepos/AzureReposHostProvider.cs

+10-10
Original file line numberDiff line numberDiff line change
@@ -92,8 +92,8 @@ public async Task<ICredential> GetCredentialAsync(InputArguments input)
9292

9393
if (UsePersonalAccessTokens())
9494
{
95-
Uri remoteUri = input.GetRemoteUri();
96-
string service = GetServiceName(remoteUri);
95+
Uri remoteWithUserUri = input.GetRemoteUri(includeUser: true);
96+
string service = GetServiceName(remoteWithUserUri);
9797
string account = GetAccountNameForCredentialQuery(input);
9898

9999
_context.Trace.WriteLine($"Looking for existing credential in store with service={service} account={account}...");
@@ -219,8 +219,8 @@ private async Task<ICredential> GeneratePersonalAccessTokenAsync(InputArguments
219219
"Unencrypted HTTP is not supported for Azure Repos. Ensure the repository remote URL is using HTTPS.");
220220
}
221221

222-
Uri remoteUri = input.GetRemoteUri();
223-
Uri orgUri = UriHelpers.CreateOrganizationUri(remoteUri, out _);
222+
Uri remoteUserUri = input.GetRemoteUri(includeUser: true);
223+
Uri orgUri = UriHelpers.CreateOrganizationUri(remoteUserUri, out _);
224224

225225
// Determine the MS authentication authority for this organization
226226
_context.Trace.WriteLine("Determining Microsoft Authentication Authority...");
@@ -257,17 +257,17 @@ private async Task<ICredential> GeneratePersonalAccessTokenAsync(InputArguments
257257

258258
private async Task<IMicrosoftAuthenticationResult> GetAzureAccessTokenAsync(InputArguments input)
259259
{
260-
Uri remoteUri = input.GetRemoteUri();
260+
Uri remoteWithUserUri = input.GetRemoteUri(includeUser: true);
261261
string userName = input.UserName;
262262

263263
// We should not allow unencrypted communication and should inform the user
264-
if (StringComparer.OrdinalIgnoreCase.Equals(remoteUri.Scheme, "http"))
264+
if (StringComparer.OrdinalIgnoreCase.Equals(remoteWithUserUri.Scheme, "http"))
265265
{
266266
throw new Trace2Exception(_context.Trace2,
267267
"Unencrypted HTTP is not supported for Azure Repos. Ensure the repository remote URL is using HTTPS.");
268268
}
269269

270-
Uri orgUri = UriHelpers.CreateOrganizationUri(remoteUri, out string orgName);
270+
Uri orgUri = UriHelpers.CreateOrganizationUri(remoteWithUserUri, out string orgName);
271271

272272
_context.Trace.WriteLine($"Determining Microsoft Authentication authority for Azure DevOps organization '{orgName}'...");
273273
if (TryGetAuthorityFromHeaders(input.WwwAuth, out string authAuthority))
@@ -306,8 +306,8 @@ private async Task<IMicrosoftAuthenticationResult> GetAzureAccessTokenAsync(Inpu
306306
//
307307
var icmp = StringComparer.OrdinalIgnoreCase;
308308
if (!string.IsNullOrWhiteSpace(userName) &&
309-
(UriHelpers.IsVisualStudioComHost(remoteUri.Host) ||
310-
(UriHelpers.IsAzureDevOpsHost(remoteUri.Host) && !icmp.Equals(orgName, userName))))
309+
(UriHelpers.IsVisualStudioComHost(remoteWithUserUri.Host) ||
310+
(UriHelpers.IsAzureDevOpsHost(remoteWithUserUri.Host) && !icmp.Equals(orgName, userName))))
311311
{
312312
_context.Trace.WriteLine("Using username as specified in remote.");
313313
}
@@ -422,7 +422,7 @@ private static string GetServiceName(Uri remoteUri)
422422
{
423423
// If we're given the full path for an older *.visualstudio.com-style URL then we should
424424
// respect that in the service name.
425-
return remoteUri.AbsoluteUri.TrimEnd('/');
425+
return remoteUri.WithoutUserInfo().AbsoluteUri.TrimEnd('/');
426426
}
427427

428428
throw new InvalidOperationException("Host is not Azure DevOps.");

0 commit comments

Comments
 (0)