Skip to content

Commit

Permalink
Use Azure cli or PowerShell credentials for devops and oss service (#…
Browse files Browse the repository at this point in the history
…8298)

* Use Azure cli or PowerShell credentials for devops and oss service
* Move alias caching into GitHubToAADConverter and update pipelines
* Stop retrying on 404s
* Use new app ID for OSS portal
* Skip tests for notification tool
  • Loading branch information
hallipr authored May 24, 2024
1 parent b6035ed commit c59ab4e
Show file tree
Hide file tree
Showing 18 changed files with 140 additions and 286 deletions.
27 changes: 12 additions & 15 deletions eng/pipelines/notifications.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,23 +54,20 @@ stages:
arguments: 'install --global --add-source "$(DotNetDevOpsFeed)" --version "$(NotificationsCreatorVersion)" "Azure.Sdk.Tools.NotificationConfiguration"'
workingDirectory: '$(Agent.BuildDirectory)'

- pwsh: |
notification-creator `
--organization $(Organization) `
--project $(Project) `
--path-prefix "\$(PathPrefix)" `
--token-variable-name DEVOPS_TOKEN `
--aad-app-id-var OPENSOURCE_AAD_APP_ID `
--aad-app-secret-var OPENSOURCE_AAD_APP_SECRET `
--aad-tenant-var OPENSOURCE_AAD_TENANT_ID `
--selection-strategy Scheduled `
$(AdditionalParameters)
- task: AzureCLI@2
displayName: 'Run Team/Notification Creator'
inputs:
azureSubscription: 'opensource-api-connection'
scriptType: pscore
scriptLocation: inlineScript
inlineScript:
notification-creator `
--organization $(Organization) `
--project $(Project) `
--path-prefix "\$(PathPrefix)" `
--selection-strategy Scheduled `
$(AdditionalParameters)
env:
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1
DOTNET_CLI_TELEMETRY_OPTOUT: 1
DOTNET_MULTILEVEL_LOOKUP: 0
DEVOPS_TOKEN: $(azure-sdk-notification-tools-pat)
OPENSOURCE_AAD_APP_ID: $(opensource-aad-app-id)
OPENSOURCE_AAD_APP_SECRET: $(opensource-aad-secret)
OPENSOURCE_AAD_TENANT_ID: $(opensource-aad-tenant-id)
2 changes: 1 addition & 1 deletion eng/pipelines/pipeline-owners-extraction.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ stages:
- task: AzureCLI@2
displayName: Run Pipeline Owners Extractor
inputs:
azureSubscription: 'Azure SDK Engineering System'
azureSubscription: 'opensource-api-connection'
scriptType: pscore
scriptLocation: inlineScript
inlineScript: pipeline-owners-extractor --output "$(OutputPath)"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,10 @@ private static string GetUrlContents(string url)
using HttpClient client = new HttpClient();
while (attempts <= maxRetries)
{
HttpResponseMessage response = null;
try
{
HttpResponseMessage response = client.GetAsync(url).ConfigureAwait(false).GetAwaiter().GetResult();
response = client.GetAsync(url).ConfigureAwait(false).GetAwaiter().GetResult();
if (response.StatusCode == HttpStatusCode.OK)
{
// This writeline is probably unnecessary but good to have if there are previous attempts that failed
Expand All @@ -76,17 +77,22 @@ private static string GetUrlContents(string url)
// HttpRequestException means the request failed due to an underlying issue such as network connectivity,
// DNS failure, server certificate validation or timeout.
Console.WriteLine($"GetUrlContents attempt number {attempts}. HttpRequestException trying to fetch {url}. Exception message = {httpReqEx.Message}");
if (attempts == maxRetries)
{
// At this point the retries have been exhausted, let this rethrow
throw;
}
}
System.Threading.Thread.Sleep(delayTimeInMs);

// Skip retries on a NotFound response
if (response?.StatusCode == HttpStatusCode.NotFound)
{
break;
}

if (attempts < maxRetries)
{
System.Threading.Thread.Sleep(delayTimeInMs);
}
attempts++;
}
// This will only get hit if the final retry is non-OK status code
throw new FileLoadException($"Unable to fetch {url} after {maxRetries}. See above for status codes for each attempt.");
throw new FileLoadException($"Unable to fetch {url} after {attempts} attempts. See above for status codes for each attempt.");
}
}
}
111 changes: 61 additions & 50 deletions tools/identity-resolution/Helpers/GitHubToAADConverter.cs
Original file line number Diff line number Diff line change
@@ -1,96 +1,107 @@
using System;
using System.Net;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Azure.Core;
using Azure.Identity;
using Microsoft.Extensions.Logging;
using Models.OpenSourcePortal;
using Newtonsoft.Json;

namespace Azure.Sdk.Tools.NotificationConfiguration.Helpers
{
/// <summary>
/// Utility class for converting GitHub usernames to AAD user principal names.
/// </summary>
/// <remarks>
/// A map of GitHub usernames to AAD user principal names is cached in memory to avoid making multiple calls to the
/// OpenSource portal API. The cache is initialized with the full alias list on the first call to
/// GetUserPrincipalNameFromGithubAsync.
/// </remarks>
public class GitHubToAADConverter
{
private readonly TokenCredential credential;
private readonly ILogger<GitHubToAADConverter> logger;
private readonly SemaphoreSlim cacheLock = new(1);
private Dictionary<string, string> lookupCache;

/// <summary>
/// GitHubToAadConverter constructor for generating new token, and initialize http client.
/// </summary>
/// <param name="credential">The aad token auth class.</param>
/// <param name="logger">Logger</param>
public GitHubToAADConverter(
ClientSecretCredential credential,
ILogger<GitHubToAADConverter> logger)
public GitHubToAADConverter(TokenCredential credential, ILogger<GitHubToAADConverter> logger)
{
this.credential = credential;
this.logger = logger;
var opsAuthToken = "";

}

public async Task<string> GetUserPrincipalNameFromGithubAsync(string gitHubUserName)
{
await EnsureCacheExistsAsync();

if (this.lookupCache.TryGetValue(gitHubUserName, out string aadUserPrincipalName))
{
return aadUserPrincipalName;
}

return null;
}

public async Task EnsureCacheExistsAsync()
{
await this.cacheLock.WaitAsync();
try
{
// This is aad scope of opensource rest API.
string[] scopes = new string[]
if (this.lookupCache == null)
{
"api://2789159d-8d8b-4d13-b90b-ca29c1707afd/.default"
};
opsAuthToken = credential.GetToken(new TokenRequestContext(scopes)).Token;
var peopleLinks = await GetPeopleLinksAsync();
this.lookupCache = peopleLinks.ToDictionary(
x => x.GitHub.Login,
x => x.Aad.UserPrincipalName,
StringComparer.OrdinalIgnoreCase);
}
}
catch (Exception ex)
finally
{
logger.LogError("Failed to generate aad token. " + ex.Message);
this.cacheLock.Release();
}
client = new HttpClient();
client.DefaultRequestHeaders.Add("content_type", "application/json");
client.DefaultRequestHeaders.Add("api-version", "2019-10-01");
client.DefaultRequestHeaders.Add("Authorization", $"Bearer {opsAuthToken}");
}

private readonly HttpClient client;
private readonly ILogger<GitHubToAADConverter> logger;

/// <summary>
/// Get the user principal name from github. User principal name is in format of ms email.
/// </summary>
/// <param name="githubUserName">github user name</param>
/// <returns>Aad user principal name</returns>
public string GetUserPrincipalNameFromGithub(string githubUserName)
private async Task<UserLink[]> GetPeopleLinksAsync()
{
return GetUserPrincipalNameFromGithubAsync(githubUserName).Result;
}
AccessToken opsAuthToken;

public async Task<string> GetUserPrincipalNameFromGithubAsync(string githubUserName)
{
try
{
var responseJsonString = await client.GetStringAsync($"https://repos.opensource.microsoft.com/api/people/links/github/{githubUserName}");
dynamic contentJson = JsonConvert.DeserializeObject(responseJsonString);
return contentJson.aad.userPrincipalName;
}
catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.NotFound)
{
logger.LogWarning("Github username {Username} not found", githubUserName);
// This is aad scope of opensource rest API.
string[] scopes = new [] { "api://66b6ea26-954d-4b68-8f48-71e3faec7ad1/.default" };
opsAuthToken = await credential.GetTokenAsync(new TokenRequestContext(scopes), CancellationToken.None);
}
catch (Exception ex)
{
logger.LogError(ex.Message);
this.logger.LogError("Failed to generate aad token. {ExceptionMessage}", ex.Message);
throw;
}

return null;
}

public async Task<UserLink[]> GetPeopleLinksAsync()
{
try
{
logger.LogInformation("Calling GET https://repos.opensource.microsoft.com/api/people/links");
var responseJsonString = await client.GetStringAsync($"https://repos.opensource.microsoft.com/api/people/links");
var allLinks = JsonConvert.DeserializeObject<UserLink[]>(responseJsonString);
using HttpClient client = new ();
client.DefaultRequestHeaders.Add("content_type", "application/json");
client.DefaultRequestHeaders.Add("api-version", "2019-10-01");
client.DefaultRequestHeaders.Add("Authorization", $"Bearer {opsAuthToken.Token}");

return allLinks;
this.logger.LogInformation("Calling GET https://repos.opensource.microsoft.com/api/people/links");
string responseJsonString = await client.GetStringAsync($"https://repos.opensource.microsoft.com/api/people/links");
return JsonConvert.DeserializeObject<UserLink[]>(responseJsonString);
}
catch (Exception ex)
{
logger.LogError(ex.Message);
this.logger.LogError(ex, "Error getting people links from opensource.microsoft.com: {ExceptionMessage}", ex.Message);
throw;
}

return null;
}
}
}
7 changes: 4 additions & 3 deletions tools/identity-resolution/Services/AzureDevOpsService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@
using Microsoft.VisualStudio.Services.Notifications.WebApi;
using System;
using Microsoft.Extensions.Logging;
using Microsoft.VisualStudio.Services.Common;
using Microsoft.VisualStudio.Services.Client;
using Microsoft.VisualStudio.Services.Identity.Client;
using System.Threading;
using System.Linq;
using MicrosoftIdentityAlias = Microsoft.VisualStudio.Services.Identity;
using Azure.Core;

namespace Azure.Sdk.Tools.NotificationConfiguration.Services
{
Expand All @@ -27,9 +28,9 @@ public class AzureDevOpsService
private Dictionary<Type, VssHttpClientBase> clientCache = new Dictionary<Type, VssHttpClientBase>();
private SemaphoreSlim clientCacheSemaphore = new SemaphoreSlim(1);

public static AzureDevOpsService CreateAzureDevOpsService(string token, string url, ILogger<AzureDevOpsService> logger)
public static AzureDevOpsService CreateAzureDevOpsService(TokenCredential tokenCredential, string url, ILogger<AzureDevOpsService> logger)
{
var devOpsCreds = new VssBasicCredential("nobody", token);
var devOpsCreds = new VssAzureIdentityCredential(tokenCredential);
var devOpsConnection = new VssConnection(new Uri(url), devOpsCreds);
var result = new AzureDevOpsService(devOpsConnection, logger);

Expand Down
2 changes: 0 additions & 2 deletions tools/identity-resolution/Services/GitHubService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using Azure.Sdk.Tools.CodeownersUtils.Parsing;
using System.IO;

Expand Down
5 changes: 3 additions & 2 deletions tools/identity-resolution/identity-resolution.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging" Version="7.0.0" />
<PackageReference Include="Microsoft.TeamFoundationServer.Client" Version="16.170.0" />
<PackageReference Include="Microsoft.VisualStudio.Services.Notifications.WebApi" Version="16.170.0" />
<PackageReference Include="Microsoft.TeamFoundationServer.Client" Version="19.239.0-preview" />
<PackageReference Include="Microsoft.VisualStudio.Services.Notifications.WebApi" Version="19.239.0-preview" />
<PackageReference Include="Microsoft.VisualStudio.Services.InteractiveClient" Version="19.239.0-preview" />
<PackageReference Include="YamlDotNet" Version="6.1.1" />
<PackageReference Include="Azure.Identity" Version="1.10.2" />
</ItemGroup>
Expand Down
1 change: 1 addition & 0 deletions tools/notification-configuration/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,4 @@ extends:
template: /eng/pipelines/templates/stages/archetype-sdk-tool-dotnet.yml
parameters:
ToolDirectory: tools/notification-configuration
TestMatrix: {}
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,13 @@ public class ProgramTests
[Test]
public void ThrowsVssUnauthorizedException()
{
Environment.SetEnvironmentVariable("aadAppIdVar", "aadAppIdVarValue");
Environment.SetEnvironmentVariable("aadAppSecretVar", "aadAppSecretVarValue");
Environment.SetEnvironmentVariable("aadTenantVar", "aadTenantVarValue");
Assert.ThrowsAsync<Microsoft.VisualStudio.Services.Common.VssUnauthorizedException>(
async () =>
// Act
await Program.Main(
organization: "fooOrg",
project: "barProj",
pathPrefix: "qux",
tokenVariableName: "token",
aadAppIdVar: "aadAppIdVar",
aadAppSecretVar: "aadAppSecretVar",
aadTenantVar: "aadTenantVar",
selectionStrategy: PipelineSelectionStrategy.Scheduled,
dryRun: true)
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ private async Task SyncTeamWithCodeownersFile(
if (!contactsCache.ContainsKey(contact))
{
// TODO: Better to have retry if no success on this call.
var userPrincipal = gitHubToAADConverter.GetUserPrincipalNameFromGithub(contact);
var userPrincipal = await gitHubToAADConverter.GetUserPrincipalNameFromGithubAsync(contact);
if (!string.IsNullOrEmpty(userPrincipal))
{
contactsCache[contact] = await service.GetDescriptorForPrincipal(userPrincipal);
Expand Down
Loading

0 comments on commit c59ab4e

Please sign in to comment.