diff --git a/eng/pipelines/notifications.yml b/eng/pipelines/notifications.yml index 028d4af6528..572200b096f 100644 --- a/eng/pipelines/notifications.yml +++ b/eng/pipelines/notifications.yml @@ -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) diff --git a/eng/pipelines/pipeline-owners-extraction.yml b/eng/pipelines/pipeline-owners-extraction.yml index 16b63891ab3..bbe6e5621e5 100644 --- a/eng/pipelines/pipeline-owners-extraction.yml +++ b/eng/pipelines/pipeline-owners-extraction.yml @@ -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)" diff --git a/tools/codeowners-utils/Azure.Sdk.Tools.CodeownersUtils/Utils/FileHelpers.cs b/tools/codeowners-utils/Azure.Sdk.Tools.CodeownersUtils/Utils/FileHelpers.cs index 5109598f437..66eb754e117 100644 --- a/tools/codeowners-utils/Azure.Sdk.Tools.CodeownersUtils/Utils/FileHelpers.cs +++ b/tools/codeowners-utils/Azure.Sdk.Tools.CodeownersUtils/Utils/FileHelpers.cs @@ -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 @@ -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."); } } } diff --git a/tools/identity-resolution/Helpers/GitHubToAADConverter.cs b/tools/identity-resolution/Helpers/GitHubToAADConverter.cs index 6b8698a49b3..9e8129492a1 100644 --- a/tools/identity-resolution/Helpers/GitHubToAADConverter.cs +++ b/tools/identity-resolution/Helpers/GitHubToAADConverter.cs @@ -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 { + /// + /// Utility class for converting GitHub usernames to AAD user principal names. + /// + /// + /// 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. + /// public class GitHubToAADConverter { + private readonly TokenCredential credential; + private readonly ILogger logger; + private readonly SemaphoreSlim cacheLock = new(1); + private Dictionary lookupCache; + /// /// GitHubToAadConverter constructor for generating new token, and initialize http client. /// /// The aad token auth class. /// Logger - public GitHubToAADConverter( - ClientSecretCredential credential, - ILogger logger) + public GitHubToAADConverter(TokenCredential credential, ILogger logger) { + this.credential = credential; this.logger = logger; - var opsAuthToken = ""; + + } + + public async Task 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 logger; - - /// - /// Get the user principal name from github. User principal name is in format of ms email. - /// - /// github user name - /// Aad user principal name - public string GetUserPrincipalNameFromGithub(string githubUserName) + private async Task GetPeopleLinksAsync() { - return GetUserPrincipalNameFromGithubAsync(githubUserName).Result; - } + AccessToken opsAuthToken; - public async Task 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 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(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(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; } } } diff --git a/tools/identity-resolution/Services/AzureDevOpsService.cs b/tools/identity-resolution/Services/AzureDevOpsService.cs index 2ee8b37a289..8981963cb8b 100644 --- a/tools/identity-resolution/Services/AzureDevOpsService.cs +++ b/tools/identity-resolution/Services/AzureDevOpsService.cs @@ -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 { @@ -27,9 +28,9 @@ public class AzureDevOpsService private Dictionary clientCache = new Dictionary(); private SemaphoreSlim clientCacheSemaphore = new SemaphoreSlim(1); - public static AzureDevOpsService CreateAzureDevOpsService(string token, string url, ILogger logger) + public static AzureDevOpsService CreateAzureDevOpsService(TokenCredential tokenCredential, string url, ILogger 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); diff --git a/tools/identity-resolution/Services/GitHubService.cs b/tools/identity-resolution/Services/GitHubService.cs index c59e527771b..fdd2a280670 100644 --- a/tools/identity-resolution/Services/GitHubService.cs +++ b/tools/identity-resolution/Services/GitHubService.cs @@ -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; diff --git a/tools/identity-resolution/identity-resolution.csproj b/tools/identity-resolution/identity-resolution.csproj index 24e592eb1aa..2c61094a460 100644 --- a/tools/identity-resolution/identity-resolution.csproj +++ b/tools/identity-resolution/identity-resolution.csproj @@ -7,8 +7,9 @@ - - + + + diff --git a/tools/notification-configuration/ci.yml b/tools/notification-configuration/ci.yml index ae36769a7dc..c32e6bc0a19 100644 --- a/tools/notification-configuration/ci.yml +++ b/tools/notification-configuration/ci.yml @@ -29,3 +29,4 @@ extends: template: /eng/pipelines/templates/stages/archetype-sdk-tool-dotnet.yml parameters: ToolDirectory: tools/notification-configuration + TestMatrix: {} diff --git a/tools/notification-configuration/notification-creator.Tests/ProgramTests.cs b/tools/notification-configuration/notification-creator.Tests/ProgramTests.cs index e7a92d50636..8e9f80a1929 100644 --- a/tools/notification-configuration/notification-creator.Tests/ProgramTests.cs +++ b/tools/notification-configuration/notification-creator.Tests/ProgramTests.cs @@ -9,9 +9,6 @@ public class ProgramTests [Test] public void ThrowsVssUnauthorizedException() { - Environment.SetEnvironmentVariable("aadAppIdVar", "aadAppIdVarValue"); - Environment.SetEnvironmentVariable("aadAppSecretVar", "aadAppSecretVarValue"); - Environment.SetEnvironmentVariable("aadTenantVar", "aadTenantVarValue"); Assert.ThrowsAsync( async () => // Act @@ -19,10 +16,6 @@ await Program.Main( organization: "fooOrg", project: "barProj", pathPrefix: "qux", - tokenVariableName: "token", - aadAppIdVar: "aadAppIdVar", - aadAppSecretVar: "aadAppSecretVar", - aadTenantVar: "aadTenantVar", selectionStrategy: PipelineSelectionStrategy.Scheduled, dryRun: true) ); diff --git a/tools/notification-configuration/notification-creator/NotificationConfigurator.cs b/tools/notification-configuration/notification-creator/NotificationConfigurator.cs index 4e0bef8308f..097a3205850 100644 --- a/tools/notification-configuration/notification-creator/NotificationConfigurator.cs +++ b/tools/notification-configuration/notification-creator/NotificationConfigurator.cs @@ -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); diff --git a/tools/notification-configuration/notification-creator/Program.cs b/tools/notification-configuration/notification-creator/Program.cs index dbc343717f6..e53750f77a5 100644 --- a/tools/notification-configuration/notification-creator/Program.cs +++ b/tools/notification-configuration/notification-creator/Program.cs @@ -1,11 +1,12 @@ using System; using System.Threading.Tasks; using Microsoft.Extensions.Logging; -using Microsoft.VisualStudio.Services.Common; using Microsoft.VisualStudio.Services.WebApi; using Azure.Sdk.Tools.NotificationConfiguration.Services; using Azure.Sdk.Tools.NotificationConfiguration.Helpers; using Azure.Identity; +using Azure.Core; +using Microsoft.VisualStudio.Services.Client; namespace Azure.Sdk.Tools.NotificationConfiguration; @@ -23,10 +24,6 @@ public static class Program /// Azure DevOps Organization /// Name of the DevOps project /// Path prefix to include pipelines (e.g. "\net") - /// Environment variable token name (e.g. "SYSTEM_ACCESSTOKEN") - /// AAD App ID environment variable name (OpensourceAPI access) - /// AAD App Secret environment variable name (OpensourceAPI access) - /// AAD Tenant environment variable name (OpensourceAPI access) /// Pipeline selection strategy /// Prints changes but does not alter any objects /// @@ -34,10 +31,6 @@ public static async Task Main( string organization, string project, string pathPrefix, - string tokenVariableName, - string aadAppIdVar, - string aadAppSecretVar, - string aadTenantVar, PipelineSelectionStrategy selectionStrategy = PipelineSelectionStrategy.Scheduled, bool dryRun = false) { @@ -45,54 +38,55 @@ public static async Task Main( { builder.AddSimpleConsole(config => { config.IncludeScopes = true; }); }); + var logger = loggerFactory.CreateLogger(nameof(Program)); + logger.LogInformation( "Executing Azure.Sdk.Tools.NotificationConfiguration.Program.Main with following arguments: " + "organization: '{organization}' " + "project: '{project}' " + "pathPrefix: '{pathPrefix}' " - + "tokenVariableName: '{tokenVariableName}' " - + "aadAppIdVar: '{aadAppIdVar}' " - + "aadAppSecretVar: '{aadAppSecretVar}' " - + "aadTenantVar: '{aadTenantVar}' " + "selectionStrategy: '{selectionStrategy}' " + "dryRun: '{dryRun}' " , organization , project , pathPrefix - , tokenVariableName - , aadAppIdVar - , aadAppSecretVar - , aadTenantVar , selectionStrategy , dryRun); + var azureCredential = new ChainedTokenCredential( + new AzureCliCredential(), + new AzurePowerShellCredential() + ); + var notificationConfigurator = new NotificationConfigurator( - AzureDevOpsService(organization, tokenVariableName, loggerFactory), + AzureDevOpsService(organization, azureCredential, loggerFactory), GitHubService(loggerFactory), loggerFactory.CreateLogger()); await notificationConfigurator.ConfigureNotifications( project, pathPrefix, - GitHubToAADConverter(aadTenantVar, aadAppIdVar, aadAppSecretVar, loggerFactory), + GitHubToAADConverter(azureCredential, loggerFactory), persistChanges: !dryRun, strategy: selectionStrategy); } private static AzureDevOpsService AzureDevOpsService( string organization, - string tokenVariableName, + TokenCredential azureCredential, ILoggerFactory loggerFactory) { - var devOpsToken = Environment.GetEnvironmentVariable(tokenVariableName); - var devOpsCreds = new VssBasicCredential("nobody", devOpsToken); + var devOpsCreds = new VssAzureIdentityCredential(azureCredential); + var devOpsConnection = new VssConnection( new Uri($"https://dev.azure.com/{organization}/"), devOpsCreds); + var devOpsService = new AzureDevOpsService( devOpsConnection, loggerFactory.CreateLogger()); + return devOpsService; } @@ -100,20 +94,14 @@ private static GitHubService GitHubService(ILoggerFactory loggerFactory) => new GitHubService(loggerFactory.CreateLogger()); private static GitHubToAADConverter GitHubToAADConverter( - string aadTenantVar, - string aadAppIdVar, - string aadAppSecretVar, + TokenCredential azureCredential, ILoggerFactory loggerFactory) { - var credential = new ClientSecretCredential( - Environment.GetEnvironmentVariable(aadTenantVar), - Environment.GetEnvironmentVariable(aadAppIdVar), - Environment.GetEnvironmentVariable(aadAppSecretVar)); - var githubToAadResolver = new GitHubToAADConverter( - credential, + azureCredential, loggerFactory.CreateLogger() ); + return githubToAadResolver; } } diff --git a/tools/pipeline-owners-extractor/Azure.Sdk.Tools.PipelineOwnersExtractor/Configuration/ISecretClientProvider.cs b/tools/pipeline-owners-extractor/Azure.Sdk.Tools.PipelineOwnersExtractor/Configuration/ISecretClientProvider.cs deleted file mode 100644 index bf56d88701c..00000000000 --- a/tools/pipeline-owners-extractor/Azure.Sdk.Tools.PipelineOwnersExtractor/Configuration/ISecretClientProvider.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System; - -using Azure.Security.KeyVault.Secrets; - -namespace Azure.Sdk.Tools.PipelineOwnersExtractor.Configuration -{ - public interface ISecretClientProvider - { - SecretClient GetSecretClient(Uri vaultUri); - } -} \ No newline at end of file diff --git a/tools/pipeline-owners-extractor/Azure.Sdk.Tools.PipelineOwnersExtractor/Configuration/PipelineOwnerSettings.cs b/tools/pipeline-owners-extractor/Azure.Sdk.Tools.PipelineOwnersExtractor/Configuration/PipelineOwnerSettings.cs index 2cd0d05cd5a..883186446ee 100644 --- a/tools/pipeline-owners-extractor/Azure.Sdk.Tools.PipelineOwnersExtractor/Configuration/PipelineOwnerSettings.cs +++ b/tools/pipeline-owners-extractor/Azure.Sdk.Tools.PipelineOwnersExtractor/Configuration/PipelineOwnerSettings.cs @@ -8,12 +8,6 @@ public class PipelineOwnerSettings public string OpenSourceAadAppId { get; set; } - public string OpenSourceAadSecret { get; set; } - - public string OpenSourceAadTenantId { get; set; } - - public string AzureDevOpsPat { get; set; } - public string Output { get; set; } } } diff --git a/tools/pipeline-owners-extractor/Azure.Sdk.Tools.PipelineOwnersExtractor/Configuration/PostConfigureKeyVaultSettings.cs b/tools/pipeline-owners-extractor/Azure.Sdk.Tools.PipelineOwnersExtractor/Configuration/PostConfigureKeyVaultSettings.cs deleted file mode 100644 index 63437d3fb03..00000000000 --- a/tools/pipeline-owners-extractor/Azure.Sdk.Tools.PipelineOwnersExtractor/Configuration/PostConfigureKeyVaultSettings.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System; -using System.Linq; -using System.Text.RegularExpressions; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; - -namespace Azure.Sdk.Tools.PipelineOwnersExtractor.Configuration -{ - public class PostConfigureKeyVaultSettings : IPostConfigureOptions where T : class - { - private static readonly Regex secretRegex = new Regex(@"(?https://[a-zA-Z0-9-]+\.vault\.azure\.net)/secrets/(?.*)", RegexOptions.Compiled, TimeSpan.FromSeconds(5)); - private readonly ILogger logger; - private readonly ISecretClientProvider secretClientProvider; - - public PostConfigureKeyVaultSettings(ILogger> logger, ISecretClientProvider secretClientProvider) - { - this.logger = logger; - this.secretClientProvider = secretClientProvider; - } - - public void PostConfigure(string name, T options) - { - var stringProperties = typeof(T) - .GetProperties() - .Where(x => x.PropertyType == typeof(string)); - - foreach (var property in stringProperties) - { - var value = (string)property.GetValue(options); - - if (value != null) - { - var match = secretRegex.Match(value); - - if (match.Success) - { - var vaultUrl = match.Groups["vault"].Value; - var secretName = match.Groups["secret"].Value; - - try - { - var secretClient = this.secretClientProvider.GetSecretClient(new Uri(vaultUrl)); - this.logger.LogInformation("Replacing setting property {PropertyName} with value from secret {SecretUrl}", property.Name, value); - - var response = secretClient.GetSecret(secretName); - var secret = response.Value; - - property.SetValue(options, secret.Value); - } - catch (Exception exception) - { - this.logger.LogError(exception, "Unable to read secret {SecretName} from vault {VaultUrl}", secretName, vaultUrl); - } - } - } - } - } - } -} diff --git a/tools/pipeline-owners-extractor/Azure.Sdk.Tools.PipelineOwnersExtractor/Configuration/SecretClientProvider.cs b/tools/pipeline-owners-extractor/Azure.Sdk.Tools.PipelineOwnersExtractor/Configuration/SecretClientProvider.cs deleted file mode 100644 index 7cb566f8275..00000000000 --- a/tools/pipeline-owners-extractor/Azure.Sdk.Tools.PipelineOwnersExtractor/Configuration/SecretClientProvider.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; - -using Azure.Core; -using Azure.Security.KeyVault.Secrets; - -namespace Azure.Sdk.Tools.PipelineOwnersExtractor.Configuration -{ - public class SecretClientProvider : ISecretClientProvider - { - private readonly TokenCredential tokenCredential; - - public SecretClientProvider(TokenCredential tokenCredential) - { - this.tokenCredential = tokenCredential; - } - - public SecretClient GetSecretClient(Uri vaultUri) - { - return new SecretClient(vaultUri, this.tokenCredential); - } - } -} diff --git a/tools/pipeline-owners-extractor/Azure.Sdk.Tools.PipelineOwnersExtractor/Processor.cs b/tools/pipeline-owners-extractor/Azure.Sdk.Tools.PipelineOwnersExtractor/Processor.cs index b096740c922..7ff53acb4cc 100644 --- a/tools/pipeline-owners-extractor/Azure.Sdk.Tools.PipelineOwnersExtractor/Processor.cs +++ b/tools/pipeline-owners-extractor/Azure.Sdk.Tools.PipelineOwnersExtractor/Processor.cs @@ -14,7 +14,6 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.TeamFoundation.Build.WebApi; -using Models.OpenSourcePortal; using Newtonsoft.Json; namespace Azure.Sdk.Tools.PipelineOwnersExtractor @@ -76,17 +75,9 @@ await File.WriteAllTextAsync( IEnumerable pipelines, Dictionary> codeownersEntriesByRepository) { - UserLink[] linkedGithubUsers = await githubToAadResolver.GetPeopleLinksAsync(); - - Dictionary microsoftAliasMap = linkedGithubUsers.ToDictionary( - x => x.GitHub.Login, - x => x.Aad.UserPrincipalName, - StringComparer.OrdinalIgnoreCase); - - List<(BuildDefinition Pipeline, List Owners)> microsoftPipelineOwners = - new List<(BuildDefinition Pipeline, List Owners)>(); + List<(BuildDefinition Pipeline, List Owners)> microsoftPipelineOwners = new (); - HashSet unrecognizedGitHubAliases = new HashSet(); + HashSet unrecognizedGitHubAliases = new (); foreach (BuildDefinition pipeline in pipelines) { @@ -135,7 +126,8 @@ await File.WriteAllTextAsync( foreach (string githubOwner in githubOwners) { - if (microsoftAliasMap.TryGetValue(githubOwner, out string microsoftOwner)) + string microsoftOwner = await this.githubToAadResolver.GetUserPrincipalNameFromGithubAsync(githubOwner); + if (!string.IsNullOrEmpty(microsoftOwner)) { microsoftOwners.Add(microsoftOwner); } diff --git a/tools/pipeline-owners-extractor/Azure.Sdk.Tools.PipelineOwnersExtractor/Program.cs b/tools/pipeline-owners-extractor/Azure.Sdk.Tools.PipelineOwnersExtractor/Program.cs index 6ccafe9a312..42eb8ac41a9 100644 --- a/tools/pipeline-owners-extractor/Azure.Sdk.Tools.PipelineOwnersExtractor/Program.cs +++ b/tools/pipeline-owners-extractor/Azure.Sdk.Tools.PipelineOwnersExtractor/Program.cs @@ -12,7 +12,7 @@ using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Microsoft.VisualStudio.Services.Common; +using Microsoft.VisualStudio.Services.Client; using Microsoft.VisualStudio.Services.WebApi; namespace Azure.Sdk.Tools.PipelineOwnersExtractor @@ -28,13 +28,8 @@ public static async Task Main(string[] args) .UseContentRoot(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)) .ConfigureServices((context, services) => { - services.AddSingleton( - DefaultAzureCredentialWithoutManagedIdentity); - services.AddSingleton(); + services.AddSingleton(BuildAzureCredential); services.Configure(context.Configuration); - services - .AddSingleton, - PostConfigureKeyVaultSettings>(); services.AddSingleton(); services.AddSingleton(CreateGithubAadConverter); services.AddSingleton(CreateAzureDevOpsService); @@ -48,45 +43,21 @@ public static async Task Main(string[] args) } /// - /// Instead of using DefaultAzureCredential [1] we use ChainedTokenCredential [2] which works - /// as DefaultAzureCredential, but most importantly, it excludes ManagedIdentityCredential. - /// We do so because there is an undesired managed identity available when we run this - /// code in CI/CD pipelines, which takes priority over the desired AzureCliCredential coming - /// from the calling AzureCLI@2 task. - /// - /// Besides, the returned ChainedTokenCredential also excludes following credentials: - /// - /// - SharedTokenCredential, as it appears to fail on linux with following error: - /// SharedTokenCacheCredential authentication failed: Persistence check failed. Data was written but it could not be read. Possible cause: on Linux, LibSecret is installed but D-Bus isn't running because it cannot be started over SSH. - /// - /// - VisualStudioCodeCredential, as it doesn't work, as explained here: - /// https://learn.microsoft.com/en-us/dotnet/api/overview/azure/identity-readme?view=azure-dotnet#defaultazurecredential - /// - /// The remaining credentials are in the same order as in DefaultAzureCredential. - /// - /// For debugging aids helping determine which credential is used and how, - /// please see the following tags in azure-sdk-tools repo: - /// - kojamroz_debug_aid_default_azure_credentials - /// Code from @hallipr showing how to get credential data using Microsoft Graph and JwtSecurityToken - /// - kojamroz_debug_aid_diag_log_on_creds - /// Code from kojamroz showing how to use Azure.Identity diagnostic output to get information on which - /// credential ends up being in use (additional flags must be set to see the full info [3]) - /// + /// Build a TokenCredential supporting and + /// + /// /// Full context provided here, on internal Azure SDK Engineering System Teams channel: /// https://teams.microsoft.com/l/message/19:59dbfadafb5e41c4890e2cd3d74cc7ba@thread.skype/1675713800408?tenantId=72f988bf-86f1-41af-91ab-2d7cd011db47&groupId=3e17dcb0-4257-4a30-b843-77f47f1d4121&parentMessageId=1675713800408&teamName=Azure%20SDK&channelName=Engineering%20System%20%F0%9F%9B%A0%EF%B8%8F&createdTime=1675713800408 /// - /// [1] https://learn.microsoft.com/en-us/dotnet/api/overview/azure/identity-readme?view=azure-dotnet#defaultazurecredential - /// [2] https://learn.microsoft.com/en-us/dotnet/api/overview/azure/identity-readme?view=azure-dotnet#define-a-custom-authentication-flow-with-chainedtokencredential - /// [3] https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/identity/Azure.Identity/README.md#logging - /// - private static Func DefaultAzureCredentialWithoutManagedIdentity - => _ - => new ChainedTokenCredential( - new EnvironmentCredential(), - new VisualStudioCredential(), - new AzureCliCredential(), - new AzurePowerShellCredential(), - new InteractiveBrowserCredential()); + /// [1] https://learn.microsoft.com/en-us/dotnet/api/overview/azure/identity-readme?view=azure-dotnet#define-a-custom-authentication-flow-with-chainedtokencredential + /// [2] https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/identity/Azure.Identity/README.md#logging + /// + private static TokenCredential BuildAzureCredential(IServiceProvider provider) { + return new ChainedTokenCredential( + new AzureCliCredential(), + new AzurePowerShellCredential() + ); + } private static AzureDevOpsService CreateAzureDevOpsService(IServiceProvider provider) { @@ -94,8 +65,10 @@ private static AzureDevOpsService CreateAzureDevOpsService(IServiceProvider prov var settings = provider.GetRequiredService>().Value; var uri = new Uri($"https://dev.azure.com/{settings.Account}"); - var credentials = new VssBasicCredential("pat", settings.AzureDevOpsPat); - var connection = new VssConnection(uri, credentials); + + var azureCredential = provider.GetRequiredService(); + var devopsCredential = new VssAzureIdentityCredential(azureCredential); + var connection = new VssConnection(uri, devopsCredential); return new AzureDevOpsService(connection, logger); } @@ -103,14 +76,9 @@ private static AzureDevOpsService CreateAzureDevOpsService(IServiceProvider prov private static GitHubToAADConverter CreateGithubAadConverter(IServiceProvider provider) { var logger = provider.GetRequiredService>(); - var settings = provider.GetRequiredService>().Value; - - var credential = new ClientSecretCredential( - settings.OpenSourceAadTenantId, - settings.OpenSourceAadAppId, - settings.OpenSourceAadSecret); + var azureCredential = provider.GetRequiredService(); - return new GitHubToAADConverter(credential, logger); + return new GitHubToAADConverter(azureCredential, logger); } } } diff --git a/tools/pipeline-owners-extractor/Azure.Sdk.Tools.PipelineOwnersExtractor/appsettings.json b/tools/pipeline-owners-extractor/Azure.Sdk.Tools.PipelineOwnersExtractor/appsettings.json index c5bd54b8af4..8982ece9d56 100644 --- a/tools/pipeline-owners-extractor/Azure.Sdk.Tools.PipelineOwnersExtractor/appsettings.json +++ b/tools/pipeline-owners-extractor/Azure.Sdk.Tools.PipelineOwnersExtractor/appsettings.json @@ -9,9 +9,5 @@ }, "Account": "azure-sdk", "Projects": "internal,public", - "OpenSourceAadAppId": "https://AzureSDKEngKeyVault.vault.azure.net/secrets/opensource-aad-app-id", - "OpenSourceAadSecret": "https://AzureSDKEngKeyVault.vault.azure.net/secrets/opensource-aad-secret", - "OpenSourceAadTenantId": "https://AzureSDKEngKeyVault.vault.azure.net/secrets/opensource-aad-tenant-id", - "AzureDevOpsPat": "https://AzureSDKEngKeyVault.vault.azure.net/secrets/azure-sdk-notification-tools-pat", "Output": "pipelineOwners.json" }