Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use Azure cli or PowerShell credentials for devops and oss service #8298

Merged
merged 9 commits into from
May 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
hallipr marked this conversation as resolved.
Show resolved Hide resolved
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