From a7a549d73e9e4c11914b583b327dfeff4ac18905 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C5=99emek=20Vysok=C3=BD?= Date: Tue, 18 Jun 2024 17:46:17 +0200 Subject: [PATCH 1/9] Improve error message when setting repository policies (#3646) --- .../Operations/SetRepositoryMergePoliciesOperation.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/Microsoft.DotNet.Darc/Darc/Operations/SetRepositoryMergePoliciesOperation.cs b/src/Microsoft.DotNet.Darc/Darc/Operations/SetRepositoryMergePoliciesOperation.cs index 4ddf751cd3..0d08020ba9 100644 --- a/src/Microsoft.DotNet.Darc/Darc/Operations/SetRepositoryMergePoliciesOperation.cs +++ b/src/Microsoft.DotNet.Darc/Darc/Operations/SetRepositoryMergePoliciesOperation.cs @@ -11,6 +11,7 @@ using Microsoft.DotNet.Darc.Models.PopUps; using Microsoft.DotNet.Darc.Options; using Microsoft.DotNet.DarcLib; +using Microsoft.DotNet.DarcLib.Helpers; using Microsoft.DotNet.Maestro.Client; using Microsoft.DotNet.Maestro.Client.Models; using Microsoft.Extensions.DependencyInjection; @@ -39,6 +40,13 @@ public override async Task ExecuteAsync() return Constants.ErrorCode; } + var repoType = GitRepoUrlParser.ParseTypeFromUri(_options.Repository); + if (repoType == GitRepoType.Local || repoType == GitRepoType.None) + { + Console.WriteLine("Please specify full repository URL (GitHub or AzDO)"); + return Constants.ErrorCode; + } + // Parse the merge policies List mergePolicies = []; From 320e2b2ec081d64f1cac2b7de452d859c65b3ceb Mon Sep 17 00:00:00 2001 From: Andrii Patsula Date: Wed, 19 Jun 2024 13:47:10 +0200 Subject: [PATCH 2/9] Bump the Azure.Identity package version (#3653) --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 6fb906911b..85ed3abd0d 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -21,7 +21,7 @@ - + From f3e675fb30f1634751e05ccb79cfe709fe08cc70 Mon Sep 17 00:00:00 2001 From: "dotnet-maestro[bot]" <42748379+dotnet-maestro[bot]@users.noreply.github.com> Date: Wed, 19 Jun 2024 14:13:42 +0200 Subject: [PATCH 3/9] [main] Update dependencies from dotnet/dnceng-shared (#3650) Co-authored-by: dotnet-maestro[bot] Co-authored-by: Andrii Patsula --- eng/Version.Details.xml | 88 ++++++++++++++++++++--------------------- eng/Versions.props | 44 ++++++++++----------- 2 files changed, 66 insertions(+), 66 deletions(-) diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index 9669fbad76..e913c4943c 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -1,93 +1,93 @@ - + https://github.com/dotnet/dnceng-shared - 5180a73b783b58369a0589c64bc343d0f8043d89 + 77c5d1699eba47a9fd73a111cabc56a0f4bb7c3f - + https://github.com/dotnet/dnceng-shared - 5180a73b783b58369a0589c64bc343d0f8043d89 + 77c5d1699eba47a9fd73a111cabc56a0f4bb7c3f - + https://github.com/dotnet/dnceng-shared - 5180a73b783b58369a0589c64bc343d0f8043d89 + 77c5d1699eba47a9fd73a111cabc56a0f4bb7c3f - + https://github.com/dotnet/dnceng-shared - 5180a73b783b58369a0589c64bc343d0f8043d89 + 77c5d1699eba47a9fd73a111cabc56a0f4bb7c3f - + https://github.com/dotnet/dnceng-shared - 5180a73b783b58369a0589c64bc343d0f8043d89 + 77c5d1699eba47a9fd73a111cabc56a0f4bb7c3f - + https://github.com/dotnet/dnceng-shared - 5180a73b783b58369a0589c64bc343d0f8043d89 + 77c5d1699eba47a9fd73a111cabc56a0f4bb7c3f - + https://github.com/dotnet/dnceng-shared - 5180a73b783b58369a0589c64bc343d0f8043d89 + 77c5d1699eba47a9fd73a111cabc56a0f4bb7c3f - + https://github.com/dotnet/dnceng-shared - 5180a73b783b58369a0589c64bc343d0f8043d89 + 77c5d1699eba47a9fd73a111cabc56a0f4bb7c3f - + https://github.com/dotnet/dnceng-shared - 5180a73b783b58369a0589c64bc343d0f8043d89 + 77c5d1699eba47a9fd73a111cabc56a0f4bb7c3f - + https://github.com/dotnet/dnceng-shared - 5180a73b783b58369a0589c64bc343d0f8043d89 + 77c5d1699eba47a9fd73a111cabc56a0f4bb7c3f - + https://github.com/dotnet/dnceng-shared - 5180a73b783b58369a0589c64bc343d0f8043d89 + 77c5d1699eba47a9fd73a111cabc56a0f4bb7c3f - + https://github.com/dotnet/dnceng-shared - 5180a73b783b58369a0589c64bc343d0f8043d89 + 77c5d1699eba47a9fd73a111cabc56a0f4bb7c3f - + https://github.com/dotnet/dnceng-shared - 5180a73b783b58369a0589c64bc343d0f8043d89 + 77c5d1699eba47a9fd73a111cabc56a0f4bb7c3f - + https://github.com/dotnet/dnceng-shared - 5180a73b783b58369a0589c64bc343d0f8043d89 + 77c5d1699eba47a9fd73a111cabc56a0f4bb7c3f - + https://github.com/dotnet/dnceng-shared - 5180a73b783b58369a0589c64bc343d0f8043d89 + 77c5d1699eba47a9fd73a111cabc56a0f4bb7c3f - + https://github.com/dotnet/dnceng-shared - 5180a73b783b58369a0589c64bc343d0f8043d89 + 77c5d1699eba47a9fd73a111cabc56a0f4bb7c3f - + https://github.com/dotnet/dnceng-shared - 5180a73b783b58369a0589c64bc343d0f8043d89 + 77c5d1699eba47a9fd73a111cabc56a0f4bb7c3f - + https://github.com/dotnet/dnceng-shared - 5180a73b783b58369a0589c64bc343d0f8043d89 + 77c5d1699eba47a9fd73a111cabc56a0f4bb7c3f - + https://github.com/dotnet/dnceng-shared - 5180a73b783b58369a0589c64bc343d0f8043d89 + 77c5d1699eba47a9fd73a111cabc56a0f4bb7c3f - + https://github.com/dotnet/dnceng-shared - 5180a73b783b58369a0589c64bc343d0f8043d89 + 77c5d1699eba47a9fd73a111cabc56a0f4bb7c3f - + https://github.com/dotnet/dnceng-shared - 5180a73b783b58369a0589c64bc343d0f8043d89 + 77c5d1699eba47a9fd73a111cabc56a0f4bb7c3f - + https://github.com/dotnet/dnceng-shared - 5180a73b783b58369a0589c64bc343d0f8043d89 + 77c5d1699eba47a9fd73a111cabc56a0f4bb7c3f diff --git a/eng/Versions.props b/eng/Versions.props index 0716f99b1d..019ce58d75 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -15,28 +15,28 @@ 8.0.0-beta.24311.3 8.0.0-beta.24311.3 17.4.1 - 1.1.0-beta.24303.1 - 1.1.0-beta.24303.1 - 1.1.0-beta.24303.1 - 1.1.0-beta.24303.1 - 1.1.0-beta.24303.1 - 1.1.0-beta.24303.1 - 1.1.0-beta.24303.1 - 1.1.0-beta.24303.1 - 1.1.0-beta.24303.1 - 1.1.0-beta.24303.1 - 1.1.0-beta.24303.1 - 1.1.0-beta.24303.1 - 1.1.0-beta.24303.1 - 1.1.0-beta.24303.1 - 1.1.0-beta.24303.1 - 1.1.0-beta.24303.1 - 1.1.0-beta.24303.1 - 1.1.0-beta.24303.1 - 1.1.0-beta.24303.1 - 1.1.0-beta.24303.1 - 1.1.0-beta.24303.1 - 1.1.0-beta.24303.1 + 1.1.0-beta.24319.1 + 1.1.0-beta.24319.1 + 1.1.0-beta.24319.1 + 1.1.0-beta.24319.1 + 1.1.0-beta.24319.1 + 1.1.0-beta.24319.1 + 1.1.0-beta.24319.1 + 1.1.0-beta.24319.1 + 1.1.0-beta.24319.1 + 1.1.0-beta.24319.1 + 1.1.0-beta.24319.1 + 1.1.0-beta.24319.1 + 1.1.0-beta.24319.1 + 1.1.0-beta.24319.1 + 1.1.0-beta.24319.1 + 1.1.0-beta.24319.1 + 1.1.0-beta.24319.1 + 1.1.0-beta.24319.1 + 1.1.0-beta.24319.1 + 1.1.0-beta.24319.1 + 1.1.0-beta.24319.1 + 1.1.0-beta.24319.1 1.1.0-beta.24313.1 1.1.0-beta.24313.1 From 31d55092d74defa7da825940b9625a0164b66546 Mon Sep 17 00:00:00 2001 From: Oleksandr Didyk <106967057+oleksandr-didyk@users.noreply.github.com> Date: Wed, 19 Jun 2024 14:51:23 +0200 Subject: [PATCH 4/9] Fixup darc command used for auth test (#3654) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Přemek Vysoký --- eng/templates/stages/deploy.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/eng/templates/stages/deploy.yaml b/eng/templates/stages/deploy.yaml index d94d69f46a..31500bbc24 100644 --- a/eng/templates/stages/deploy.yaml +++ b/eng/templates/stages/deploy.yaml @@ -197,11 +197,11 @@ stages: - powershell: az login --service-principal -u "$(GetAuthInfo.ServicePrincipalId)" --federated-token "$(GetAuthInfo.FederatedToken)" --tenant "$(GetAuthInfo.TenantId)" --allow-no-subscriptions - .\darc\darc.exe get-subscriptions --ci --source-enabled true --bar-uri "$(GetAuthInfo.BarUri)" --debug + .\darc\darc.exe get-default-channels --source-repo arcade-services --ci --bar-uri "$(GetAuthInfo.BarUri)" --debug displayName: Test Azure CLI authentication - powershell: - .\darc\darc.exe get-subscriptions -t "$(GetAuthInfo.FederatedToken)" --source-enabled true --bar-uri "$(GetAuthInfo.BarUri)" --debug + .\darc\darc.exe get-default-channels --source-repo arcade-services --ci -t "$(GetAuthInfo.FederatedToken)" --bar-uri "$(GetAuthInfo.BarUri)" --debug displayName: Test Federated token authentication - task: VSTest@2 From c89235f7c8a7e47edc72030d7e6c3c23d4eeda6d Mon Sep 17 00:00:00 2001 From: Oleksandr Didyk <106967057+oleksandr-didyk@users.noreply.github.com> Date: Wed, 19 Jun 2024 16:07:32 +0200 Subject: [PATCH 5/9] Unify app token authentication between Darc and PCS (#3649) --- arcade-services.sln | 15 ++ src/Maestro/Client/src/MaestroApi.cs | 55 ------ .../Client/src/MaestroApiCredential.cs | 175 ------------------ src/Maestro/Client/src/MaestroApiFactory.cs | 2 +- src/Maestro/Client/src/MaestroApiOptions.cs | 33 +++- .../Client/src/MaestroApiTokenCredential.cs | 34 ---- .../Microsoft.DotNet.Maestro.Client.csproj | 7 +- src/Maestro/Maestro.Common/AppCredential.cs | 157 ++++++++++++++++ .../Maestro.Common/AppCredentialResolver.cs | 55 ++++++ .../Maestro.Common/Maestro.Common.csproj | 15 ++ .../Maestro.Common/ResolvedCredential.cs | 29 +++ .../Darc/Helpers/LocalSettings.cs | 4 +- .../Darc/Microsoft.DotNet.Darc.csproj | 1 + .../Darc/Operations/AuthenticateOperation.cs | 8 +- .../ProductConstructionService.Api/Dockerfile | 2 + .../PcsApiCredential.cs | 75 -------- .../PcsApiTokenCredential.cs | 26 --- .../ProductConstructionService.Client.csproj | 6 +- .../ProductConstructionServiceApi.cs | 42 ----- .../ProductConstructionServiceApiOptions.cs | 25 ++- src/ProductConstructionService/Readme.md | 2 +- .../VmrTestsBase.cs | 2 +- 22 files changed, 345 insertions(+), 425 deletions(-) delete mode 100644 src/Maestro/Client/src/MaestroApiCredential.cs delete mode 100644 src/Maestro/Client/src/MaestroApiTokenCredential.cs create mode 100644 src/Maestro/Maestro.Common/AppCredential.cs create mode 100644 src/Maestro/Maestro.Common/AppCredentialResolver.cs create mode 100644 src/Maestro/Maestro.Common/Maestro.Common.csproj create mode 100644 src/Maestro/Maestro.Common/ResolvedCredential.cs delete mode 100644 src/ProductConstructionService/ProductConstructionService.Client/PcsApiCredential.cs delete mode 100644 src/ProductConstructionService/ProductConstructionService.Client/PcsApiTokenCredential.cs delete mode 100644 src/ProductConstructionService/ProductConstructionService.Client/ProductConstructionServiceApi.cs diff --git a/arcade-services.sln b/arcade-services.sln index 15cfb7377c..00ad56aa59 100644 --- a/arcade-services.sln +++ b/arcade-services.sln @@ -118,6 +118,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Maestro.Authentication", "s EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ProductConstructionService.Client", "src\ProductConstructionService\ProductConstructionService.Client\ProductConstructionService.Client.csproj", "{964FA796-358E-48AE-B75C-E42132600BCC}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Maestro.Common", "src\Maestro\Maestro.Common\Maestro.Common.csproj", "{16F086DD-8387-44BB-87D5-CD804355A110}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -524,6 +526,18 @@ Global {964FA796-358E-48AE-B75C-E42132600BCC}.Release|x64.Build.0 = Release|Any CPU {964FA796-358E-48AE-B75C-E42132600BCC}.Release|x86.ActiveCfg = Release|Any CPU {964FA796-358E-48AE-B75C-E42132600BCC}.Release|x86.Build.0 = Release|Any CPU + {16F086DD-8387-44BB-87D5-CD804355A110}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {16F086DD-8387-44BB-87D5-CD804355A110}.Debug|Any CPU.Build.0 = Debug|Any CPU + {16F086DD-8387-44BB-87D5-CD804355A110}.Debug|x64.ActiveCfg = Debug|Any CPU + {16F086DD-8387-44BB-87D5-CD804355A110}.Debug|x64.Build.0 = Debug|Any CPU + {16F086DD-8387-44BB-87D5-CD804355A110}.Debug|x86.ActiveCfg = Debug|Any CPU + {16F086DD-8387-44BB-87D5-CD804355A110}.Debug|x86.Build.0 = Debug|Any CPU + {16F086DD-8387-44BB-87D5-CD804355A110}.Release|Any CPU.ActiveCfg = Release|Any CPU + {16F086DD-8387-44BB-87D5-CD804355A110}.Release|Any CPU.Build.0 = Release|Any CPU + {16F086DD-8387-44BB-87D5-CD804355A110}.Release|x64.ActiveCfg = Release|Any CPU + {16F086DD-8387-44BB-87D5-CD804355A110}.Release|x64.Build.0 = Release|Any CPU + {16F086DD-8387-44BB-87D5-CD804355A110}.Release|x86.ActiveCfg = Release|Any CPU + {16F086DD-8387-44BB-87D5-CD804355A110}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -566,6 +580,7 @@ Global {BB8CC065-79D6-48CE-AD96-32EDDDC4D404} = {1A456CF0-C09A-4DE6-89CE-1110EED31180} {70D48B7A-DBFE-4762-A83F-4617ECF80838} = {AE791E26-78E1-4936-BCF4-1BF5152CBBD6} {964FA796-358E-48AE-B75C-E42132600BCC} = {243A4561-BF35-405A-AF12-AC57BB27796D} + {16F086DD-8387-44BB-87D5-CD804355A110} = {AE791E26-78E1-4936-BCF4-1BF5152CBBD6} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {32B9C883-432E-4FC8-A1BF-090EB033DD5B} diff --git a/src/Maestro/Client/src/MaestroApi.cs b/src/Maestro/Client/src/MaestroApi.cs index 61671ecfe2..cef81672ee 100644 --- a/src/Maestro/Client/src/MaestroApi.cs +++ b/src/Maestro/Client/src/MaestroApi.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Azure.Core; using Newtonsoft.Json.Linq; using System; using System.IO; @@ -25,14 +24,6 @@ public override bool IsRetriableException(Exception exception) => public partial class MaestroApi { - public const string ProductionBuildAssetRegistryBaseUri = "https://maestro.dot.net/"; - - public const string StagingBuildAssetRegistryBaseUri = "https://maestro.int-dot.net/"; - - public const string OldProductionBuildAssetRegistryBaseUri = "https://maestro-prod.westus2.cloudapp.azure.com/"; - - public const string OldStagingBuildAssetRegistryBaseUri = "https://maestro-int.westus2.cloudapp.azure.com/"; - // Special error handler to consumes the generated MaestroApi code. If this method returns without throwing a specific exception // then a generic RestApiException is thrown. partial void HandleFailedRequest(RestApiException ex) @@ -60,51 +51,5 @@ partial void HandleFailedRequest(RestApiException ex) "Please make sure the PAT you're using is valid."); } } - - /// - /// Creates a credential based on parameters provided. - /// - /// BAR API URI used to determine the right set of credentials (INT vs PROD) - /// Whether to include interactive login flows - /// Token to use for the call. If none supplied, will try other flows. - /// Federated token to use for the call. If none supplied, will try other flows. - /// Managed Identity to use for the auth - /// Credential that can be used to call the Maestro API - public static TokenCredential CreateApiCredential( - string barApiBaseUri, - bool disableInteractiveAuth, - string? barApiToken = null, - string? federatedToken = null, - string? managedIdentityId = null) - { - // 1. BAR or Entra token that can directly be used to authenticate against Maestro - if (!string.IsNullOrEmpty(barApiToken)) - { - return new MaestroApiTokenCredential(barApiToken!); - } - - barApiBaseUri ??= ProductionBuildAssetRegistryBaseUri; - - // 2. Federated token that can be used to fetch an app token (for CI scenarios) - if (!string.IsNullOrEmpty(federatedToken)) - { - return MaestroApiCredential.CreateFederatedCredential(barApiBaseUri, federatedToken!); - } - - // 3. Managed identity (for server-to-server scenarios - e.g. PCS->Maestro) - if (!string.IsNullOrEmpty(managedIdentityId)) - { - return MaestroApiCredential.CreateManagedIdentityCredential(barApiBaseUri, managedIdentityId!); - } - - // 4. Azure CLI authentication setup by the caller (for CI scenarios) - if (disableInteractiveAuth) - { - return MaestroApiCredential.CreateNonUserCredential(barApiBaseUri); - } - - // 5. Interactive login (user-based scenario) - return MaestroApiCredential.CreateUserCredential(barApiBaseUri); - } } } diff --git a/src/Maestro/Client/src/MaestroApiCredential.cs b/src/Maestro/Client/src/MaestroApiCredential.cs deleted file mode 100644 index 3c60dc5a80..0000000000 --- a/src/Maestro/Client/src/MaestroApiCredential.cs +++ /dev/null @@ -1,175 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Collections.Generic; -using System.IO; -using System.Threading; -using System.Threading.Tasks; -using Azure.Core; -using Azure.Identity; - -#nullable enable -namespace Microsoft.DotNet.Maestro.Client -{ - /// - /// A credential that first tries a user-based browser auth flow then falls back to a managed identity-based flow. - /// - internal class MaestroApiCredential : TokenCredential - { - private const string TENANT_ID = "72f988bf-86f1-41af-91ab-2d7cd011db47"; - private const string USER_SCOPE = "Maestro.User"; - - private const string AUTH_RECORD_PREFIX = ".auth-record"; - - private static readonly Dictionary EntraAppIds = new Dictionary - { - [MaestroApi.StagingBuildAssetRegistryBaseUri.TrimEnd('/')] = "baf98f1b-374e-487d-af42-aa33807f11e4", - [MaestroApi.OldStagingBuildAssetRegistryBaseUri.TrimEnd('/')] = "baf98f1b-374e-487d-af42-aa33807f11e4", - [MaestroApi.ProductionBuildAssetRegistryBaseUri.TrimEnd('/')] = "54c17f3d-7325-4eca-9db7-f090bfc765a8", - [MaestroApi.OldProductionBuildAssetRegistryBaseUri.TrimEnd('/')] = "54c17f3d-7325-4eca-9db7-f090bfc765a8", - }; - - private readonly TokenRequestContext _requestContext; - private readonly TokenCredential _tokenCredential; - - private MaestroApiCredential(TokenCredential credential, TokenRequestContext requestContext) - { - _requestContext = requestContext; - _tokenCredential = credential; - } - - public override AccessToken GetToken(TokenRequestContext _, CancellationToken cancellationToken) - { - // We hardcode the request context as we know which scopes we need to invoke in each scenario (user vs daemon) - return _tokenCredential.GetToken(_requestContext, cancellationToken); - } - - public override ValueTask GetTokenAsync(TokenRequestContext _, CancellationToken cancellationToken) - { - // We hardcode the request context as we know which scopes we need to invoke in each scenario (user vs daemon) - return _tokenCredential.GetTokenAsync(_requestContext, cancellationToken); - } - - /// - /// Use this for user-based flows (darc invocation from dev machines). - /// - internal static MaestroApiCredential CreateUserCredential(string barApiBaseUri) - { - string appId = EntraAppIds[barApiBaseUri.TrimEnd('/')]; - var requestContext = new TokenRequestContext(new string[] { $"api://{appId}/{USER_SCOPE}" }); - - string authRecordPath = Path.Combine(MaestroApiOptions.AUTH_CACHE, $"{AUTH_RECORD_PREFIX}-{appId}"); - var credential = GetInteractiveCredential(appId, requestContext, authRecordPath); - - return new MaestroApiCredential(credential, requestContext); - } - - /// - /// Create interactive credential from an authentication record stored in local cache - /// Authentication record is a set of app and user-specific metadata used by the library to authenticate - /// - private static InteractiveBrowserCredential GetInteractiveCredential( - string appId, - TokenRequestContext requestContext, - string authRecordPath) - { - // This is a usual configuration for a credential obtained against an entra app through a browser sign-in - var credentialOptions = new InteractiveBrowserCredentialOptions - { - TenantId = TENANT_ID, - ClientId = appId, - RedirectUri = new Uri("http://localhost"), - TokenCachePersistenceOptions = new TokenCachePersistenceOptions() - { - Name = "darc" - }, - }; - - - string authRecordDir = Path.GetDirectoryName(authRecordPath) ?? - throw new ArgumentException($"Cannot resolve cache dir from auth record: {authRecordPath}"); - - if (!Directory.Exists(authRecordDir)) - { - Directory.CreateDirectory(authRecordDir); - } - - if (File.Exists(authRecordPath)) - { - try - { - // Fetch existing authentication record to not prompt the user for consent - using var authRecordReadStream = new FileStream(authRecordPath, FileMode.Open, FileAccess.Read); - credentialOptions.AuthenticationRecord = AuthenticationRecord.Deserialize(authRecordReadStream); - } - catch - { - // We failed to read the authentication record, we should delete the invalid file and re-create it - File.Delete(authRecordPath); - - return GetInteractiveCredential(appId, requestContext, authRecordPath); - } - - return new InteractiveBrowserCredential(credentialOptions); - } - - var credential = new InteractiveBrowserCredential(credentialOptions); - - // Prompt the user for consent and save the resulting authentication record on disk - var authRecord = credential.Authenticate(requestContext); - - using var authRecordStream = new FileStream(authRecordPath, FileMode.Create, FileAccess.Write); - authRecord.Serialize(authRecordStream); - - return credential; - } - - /// - /// Use this for darc invocations from pipelines with a federated token - /// - internal static MaestroApiCredential CreateFederatedCredential(string barApiBaseUri, string federatedToken) - { - string appId = EntraAppIds[barApiBaseUri.TrimEnd('/')]; - - var credential = new ClientAssertionCredential( - TENANT_ID, - appId, - token => Task.FromResult(federatedToken)); - - var requestContext = new TokenRequestContext(new string[] { $"api://{appId}/.default" }); - return new MaestroApiCredential(credential, requestContext); - } - - /// - /// Use this for darc invocations from services using an MI. - /// ID can be "system" for system-assigned identity or GUID for a user assigned one. - /// - internal static MaestroApiCredential CreateManagedIdentityCredential(string barApiBaseUri, string managedIdentityId) - { - string appId = EntraAppIds[barApiBaseUri.TrimEnd('/')]; - - var miCredential = managedIdentityId == "system" - ? new ManagedIdentityCredential() - : new ManagedIdentityCredential(managedIdentityId); - - var appCredential = new ClientAssertionCredential( - TENANT_ID, - appId, - async (ct) => (await miCredential.GetTokenAsync(new TokenRequestContext(new string[] { "api://AzureADTokenExchange" }), ct)).Token); - - var requestContext = new TokenRequestContext(new string[] { $"api://{appId}/.default" }); - return new MaestroApiCredential(appCredential, requestContext); - } - - /// - /// Use this for darc invocations from pipelines without a token. - /// - internal static MaestroApiCredential CreateNonUserCredential(string barApiBaseUri) - { - var requestContext = new TokenRequestContext(new string[] { $"{EntraAppIds[barApiBaseUri.TrimEnd('/')]}/.default" }); - var credential = new AzureCliCredential(); - return new MaestroApiCredential(credential, requestContext); - } - } -} diff --git a/src/Maestro/Client/src/MaestroApiFactory.cs b/src/Maestro/Client/src/MaestroApiFactory.cs index 319fe16aef..71770fde56 100644 --- a/src/Maestro/Client/src/MaestroApiFactory.cs +++ b/src/Maestro/Client/src/MaestroApiFactory.cs @@ -45,7 +45,7 @@ public static IMaestroApi GetAuthenticated( bool disableInteractiveAuth) { return new MaestroApi(new MaestroApiOptions( - MaestroApi.StagingBuildAssetRegistryBaseUri, + MaestroApiOptions.StagingBuildAssetRegistryBaseUri, accessToken, managedIdentityId, federatedToken, diff --git a/src/Maestro/Client/src/MaestroApiOptions.cs b/src/Maestro/Client/src/MaestroApiOptions.cs index 0cf1536a53..ed7ef2bc88 100644 --- a/src/Maestro/Client/src/MaestroApiOptions.cs +++ b/src/Maestro/Client/src/MaestroApiOptions.cs @@ -2,16 +2,37 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; -using System.IO; +using System.Collections.Generic; using Azure.Core; using Azure.Core.Pipeline; +using Microsoft.DotNet.Maestro.Common; namespace Microsoft.DotNet.Maestro.Client { public partial class MaestroApiOptions { - public static readonly string AUTH_CACHE = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".darc"); + public const string ProductionBuildAssetRegistryBaseUri = "https://maestro.dot.net/"; + public const string OldProductionBuildAssetRegistryBaseUri = "https://maestro-prod.westus2.cloudapp.azure.com/"; + + // https://ms.portal.azure.com/#view/Microsoft_AAD_RegisteredApps/ApplicationMenuBlade/~/Overview/appId/54c17f3d-7325-4eca-9db7-f090bfc765a8/isMSAApp~/false + private const string MaestroProductionAppId = "54c17f3d-7325-4eca-9db7-f090bfc765a8"; + + public const string StagingBuildAssetRegistryBaseUri = "https://maestro.int-dot.net/"; + public const string OldStagingBuildAssetRegistryBaseUri = "https://maestro-int.westus2.cloudapp.azure.com/"; + + // https://ms.portal.azure.com/#view/Microsoft_AAD_RegisteredApps/ApplicationMenuBlade/~/Overview/appId/baf98f1b-374e-487d-af42-aa33807f11e4/isMSAApp~/false + private const string MaestroStagingAppId = "baf98f1b-374e-487d-af42-aa33807f11e4"; + + private const string APP_USER_SCOPE = "Maestro.User"; + + private static readonly Dictionary EntraAppIds = new Dictionary + { + [StagingBuildAssetRegistryBaseUri.TrimEnd('/')] = MaestroStagingAppId, + [OldStagingBuildAssetRegistryBaseUri.TrimEnd('/')] = MaestroStagingAppId, + [ProductionBuildAssetRegistryBaseUri.TrimEnd('/')] = MaestroProductionAppId, + [OldProductionBuildAssetRegistryBaseUri.TrimEnd('/')] = MaestroProductionAppId, + }; /// /// Creates a new instance of with the provided base URI. @@ -24,7 +45,13 @@ public partial class MaestroApiOptions public MaestroApiOptions(string baseUri, string accessToken, string managedIdentityId, string federatedToken, bool disableInteractiveAuth) : this( new Uri(baseUri), - MaestroApi.CreateApiCredential(baseUri, disableInteractiveAuth, accessToken, federatedToken, managedIdentityId)) + AppCredentialResolver.CreateCredential( + EntraAppIds[(baseUri ?? ProductionBuildAssetRegistryBaseUri).TrimEnd('/')], + disableInteractiveAuth, + accessToken, + federatedToken, + managedIdentityId, + APP_USER_SCOPE)) { } diff --git a/src/Maestro/Client/src/MaestroApiTokenCredential.cs b/src/Maestro/Client/src/MaestroApiTokenCredential.cs deleted file mode 100644 index 773ab668f1..0000000000 --- a/src/Maestro/Client/src/MaestroApiTokenCredential.cs +++ /dev/null @@ -1,34 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Threading; -using System.Threading.Tasks; -using Azure.Core; - -#nullable enable -namespace Microsoft.DotNet.Maestro.Client -{ - /// - /// Credential used to authenticate to the Maestro API using a specific token. - /// - internal class MaestroApiTokenCredential : TokenCredential - { - public MaestroApiTokenCredential(string token) - { - Token = token; - } - - public string Token { get; } - - public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken) - { - return new AccessToken(Token, DateTimeOffset.MaxValue); - } - - public override ValueTask GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken) - { - return new ValueTask(new AccessToken(Token, DateTimeOffset.MaxValue)); - } - } -} diff --git a/src/Maestro/Client/src/Microsoft.DotNet.Maestro.Client.csproj b/src/Maestro/Client/src/Microsoft.DotNet.Maestro.Client.csproj index 38a4212968..fae0c32b3d 100644 --- a/src/Maestro/Client/src/Microsoft.DotNet.Maestro.Client.csproj +++ b/src/Maestro/Client/src/Microsoft.DotNet.Maestro.Client.csproj @@ -1,4 +1,4 @@ - + true @@ -14,13 +14,16 @@ - + + + + diff --git a/src/Maestro/Maestro.Common/AppCredential.cs b/src/Maestro/Maestro.Common/AppCredential.cs new file mode 100644 index 0000000000..eb2146533e --- /dev/null +++ b/src/Maestro/Maestro.Common/AppCredential.cs @@ -0,0 +1,157 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Azure.Core; +using Azure.Identity; + +namespace Microsoft.DotNet.Maestro.Common; + +/// +/// A credential for authenticating against Azure applications. +/// +public class AppCredential : TokenCredential +{ + public static string AUTH_CACHE = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".darc"); + + private static readonly string AUTH_RECORD_PREFIX = ".auth-record"; + + private const string TENANT_ID = "72f988bf-86f1-41af-91ab-2d7cd011db47"; + + private readonly TokenRequestContext _requestContext; + private readonly TokenCredential _tokenCredential; + + private AppCredential(TokenCredential credential, TokenRequestContext requestContext) + { + _requestContext = requestContext; + _tokenCredential = credential; + } + + public override AccessToken GetToken(TokenRequestContext _, CancellationToken cancellationToken) + { + // We hardcode the request context as we know which scopes we need to invoke in each scenario (user vs daemon) + return _tokenCredential.GetToken(_requestContext, cancellationToken); + } + + public override ValueTask GetTokenAsync(TokenRequestContext _, CancellationToken cancellationToken) + { + // We hardcode the request context as we know which scopes we need to invoke in each scenario (user vs daemon) + return _tokenCredential.GetTokenAsync(_requestContext, cancellationToken); + } + + /// + /// Use this for user-based flows. + /// + public static AppCredential CreateUserCredential(string appId, string userScope = ".default") + { + var requestContext = new TokenRequestContext(new string[] { $"api://{appId}/{userScope}" }); + + string authRecordPath = Path.Combine(AUTH_CACHE, $"{AUTH_RECORD_PREFIX}-{appId}"); + var credential = GetInteractiveCredential(appId, requestContext, authRecordPath); + + return new AppCredential(credential, requestContext); + } + + /// + /// Creates an interactive credential. Checks local cache first for an authentication record. + /// Authentication record is a set of app and user-specific metadata used by the library to authenticate + /// + private static InteractiveBrowserCredential GetInteractiveCredential( + string appId, + TokenRequestContext requestContext, + string authRecordPath) + { + // This is a usual configuration for a credential obtained against an entra app through a browser sign-in + var credentialOptions = new InteractiveBrowserCredentialOptions + { + TenantId = TENANT_ID, + ClientId = appId, + RedirectUri = new Uri("http://localhost"), + // These options describe credential caching only during runtime + TokenCachePersistenceOptions = new TokenCachePersistenceOptions() + { + Name = "maestro" + }, + }; + + + string authRecordDir = Path.GetDirectoryName(authRecordPath) ?? + throw new ArgumentException($"Cannot resolve cache dir from auth record: {authRecordPath}"); + + if (!Directory.Exists(authRecordDir)) + { + Directory.CreateDirectory(authRecordDir); + } + + if (File.Exists(authRecordPath)) + { + try + { + // Fetch existing authentication record to not prompt the user for consent + using var authRecordReadStream = new FileStream(authRecordPath, FileMode.Open, FileAccess.Read); + credentialOptions.AuthenticationRecord = AuthenticationRecord.Deserialize(authRecordReadStream); + } + catch + { + // We failed to read the authentication record, we should delete the invalid file and re-create it + File.Delete(authRecordPath); + + return GetInteractiveCredential(appId, requestContext, authRecordPath); + } + + return new InteractiveBrowserCredential(credentialOptions); + } + + var credential = new InteractiveBrowserCredential(credentialOptions); + + // Prompt the user for consent and save the resulting authentication record on disk + var authRecord = credential.Authenticate(requestContext); + + using var authRecordStream = new FileStream(authRecordPath, FileMode.Create, FileAccess.Write); + authRecord.Serialize(authRecordStream); + + return credential; + } + + /// + /// Use this for invocations from pipelines with a federated token + /// + public static AppCredential CreateFederatedCredential(string appId, string federatedToken) + { + var credential = new ClientAssertionCredential( + TENANT_ID, + appId, + token => Task.FromResult(federatedToken)); + + var requestContext = new TokenRequestContext(new string[] { $"api://{appId}/.default" }); + return new AppCredential(credential, requestContext); + } + + /// + /// Use this for invocations from services using an MI. + /// ID can be "system" for system-assigned identity or GUID for a user assigned one. + /// + public static AppCredential CreateManagedIdentityCredential(string appId, string managedIdentityId) + { + var miCredential = managedIdentityId == "system" + ? new ManagedIdentityCredential() + : new ManagedIdentityCredential(managedIdentityId); + + var appCredential = new ClientAssertionCredential( + TENANT_ID, + appId, + async (ct) => (await miCredential.GetTokenAsync(new TokenRequestContext(new string[] { "api://AzureADTokenExchange" }), ct)).Token); + + var requestContext = new TokenRequestContext(new string[] { $"api://{appId}/.default" }); + return new AppCredential(appCredential, requestContext); + } + + /// + /// Use this for invocations from pipelines without a token. + /// + public static AppCredential CreateNonUserCredential(string appId) + { + var requestContext = new TokenRequestContext(new string[] { $"{appId}/.default" }); + var credential = new AzureCliCredential(); + return new AppCredential(credential, requestContext); + } +} diff --git a/src/Maestro/Maestro.Common/AppCredentialResolver.cs b/src/Maestro/Maestro.Common/AppCredentialResolver.cs new file mode 100644 index 0000000000..62b92a1202 --- /dev/null +++ b/src/Maestro/Maestro.Common/AppCredentialResolver.cs @@ -0,0 +1,55 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Azure.Core; + +namespace Microsoft.DotNet.Maestro.Common; + +public static class AppCredentialResolver +{ + /// + /// Creates a credential based on parameters provided. + /// + /// Client ID of the Azure application to request the token for + /// Whether to include interactive login flows + /// Token to use directly instead of authenticating. + /// Federated token to use for fetching the token. If none supplied, will try other flows. + /// Managed Identity to use for the auth + /// Credential that can be used to call the Maestro API + public static TokenCredential CreateCredential( + string appId, + bool disableInteractiveAuth, + string? token = null, + string? federatedToken = null, + string? managedIdentityId = null, + string userScope = ".default") + { + // 1. BAR or Entra token that can directly be used to authenticate against a service + if (!string.IsNullOrEmpty(token)) + { + return new ResolvedCredential(token!); + } + + // 2. Federated token that can be used to fetch an app token (for CI scenarios) + if (!string.IsNullOrEmpty(federatedToken)) + { + return AppCredential.CreateFederatedCredential(appId, federatedToken!); + } + + // 3. Managed identity (for server-to-server scenarios - e.g. PCS->Maestro) + if (!string.IsNullOrEmpty(managedIdentityId)) + { + return AppCredential.CreateManagedIdentityCredential(appId, managedIdentityId!); + } + + // 4. Azure CLI authentication setup by the caller (for CI scenarios) + if (disableInteractiveAuth) + { + return AppCredential.CreateNonUserCredential(appId); + } + + // 5. Interactive login (user-based scenario) + return AppCredential.CreateUserCredential(appId, userScope); + } + +} diff --git a/src/Maestro/Maestro.Common/Maestro.Common.csproj b/src/Maestro/Maestro.Common/Maestro.Common.csproj new file mode 100644 index 0000000000..ca5fb70831 --- /dev/null +++ b/src/Maestro/Maestro.Common/Maestro.Common.csproj @@ -0,0 +1,15 @@ + + + + enable + enable + netstandard2.0;net6.0 + + + + + + + + + diff --git a/src/Maestro/Maestro.Common/ResolvedCredential.cs b/src/Maestro/Maestro.Common/ResolvedCredential.cs new file mode 100644 index 0000000000..cc8ac16196 --- /dev/null +++ b/src/Maestro/Maestro.Common/ResolvedCredential.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Azure.Core; + +namespace Microsoft.DotNet.Maestro.Common; + +/// +/// Credential with a set token. +/// +public class ResolvedCredential : TokenCredential +{ + public ResolvedCredential(string token) + { + Token = token; + } + + public string Token { get; } + + public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken) + { + return new AccessToken(Token, DateTimeOffset.MaxValue); + } + + public override ValueTask GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken) + { + return new ValueTask(new AccessToken(Token, DateTimeOffset.MaxValue)); + } +} diff --git a/src/Microsoft.DotNet.Darc/Darc/Helpers/LocalSettings.cs b/src/Microsoft.DotNet.Darc/Darc/Helpers/LocalSettings.cs index 0a735dc0ce..1c7a64ef2a 100644 --- a/src/Microsoft.DotNet.Darc/Darc/Helpers/LocalSettings.cs +++ b/src/Microsoft.DotNet.Darc/Darc/Helpers/LocalSettings.cs @@ -25,7 +25,7 @@ internal class LocalSettings public string AzureDevOpsToken { get; set; } - public string BuildAssetRegistryBaseUri { get; set; } = MaestroApi.ProductionBuildAssetRegistryBaseUri; + public string BuildAssetRegistryBaseUri { get; set; } = MaestroApiOptions.ProductionBuildAssetRegistryBaseUri; /// /// Saves the settings in the settings files @@ -104,7 +104,7 @@ static string PreferOptionToSetting(string option, string localSetting) localSettings.BuildAssetRegistryToken = PreferOptionToSetting(options.BuildAssetRegistryToken, localSettings.BuildAssetRegistryToken); localSettings.BuildAssetRegistryBaseUri = options.BuildAssetRegistryBaseUri ?? localSettings.BuildAssetRegistryBaseUri - ?? MaestroApi.ProductionBuildAssetRegistryBaseUri; + ?? MaestroApiOptions.ProductionBuildAssetRegistryBaseUri; return localSettings; } diff --git a/src/Microsoft.DotNet.Darc/Darc/Microsoft.DotNet.Darc.csproj b/src/Microsoft.DotNet.Darc/Darc/Microsoft.DotNet.Darc.csproj index ac18fde1fc..ac064adbb4 100644 --- a/src/Microsoft.DotNet.Darc/Darc/Microsoft.DotNet.Darc.csproj +++ b/src/Microsoft.DotNet.Darc/Darc/Microsoft.DotNet.Darc.csproj @@ -12,6 +12,7 @@ + diff --git a/src/Microsoft.DotNet.Darc/Darc/Operations/AuthenticateOperation.cs b/src/Microsoft.DotNet.Darc/Darc/Operations/AuthenticateOperation.cs index 9d80284854..85eceb65b5 100644 --- a/src/Microsoft.DotNet.Darc/Darc/Operations/AuthenticateOperation.cs +++ b/src/Microsoft.DotNet.Darc/Darc/Operations/AuthenticateOperation.cs @@ -4,11 +4,11 @@ using Microsoft.DotNet.Darc.Helpers; using Microsoft.DotNet.Darc.Models; using Microsoft.DotNet.Darc.Options; -using Microsoft.DotNet.Maestro.Client; using Microsoft.Extensions.Logging; using System; using System.IO; using System.Threading.Tasks; +using Microsoft.DotNet.Maestro.Common; namespace Microsoft.DotNet.Darc.Operations; @@ -30,12 +30,12 @@ public override Task ExecuteAsync() if (_options.Clear) { // Clear directories before we re-create any settings file - if (Directory.Exists(MaestroApiOptions.AUTH_CACHE)) + if (Directory.Exists(AppCredential.AUTH_CACHE)) { try { - Directory.Delete(MaestroApiOptions.AUTH_CACHE, recursive: true); - Directory.CreateDirectory(MaestroApiOptions.AUTH_CACHE); + Directory.Delete(AppCredential.AUTH_CACHE, recursive: true); + Directory.CreateDirectory(AppCredential.AUTH_CACHE); } catch (Exception ex) { diff --git a/src/ProductConstructionService/ProductConstructionService.Api/Dockerfile b/src/ProductConstructionService/ProductConstructionService.Api/Dockerfile index 16df148a6c..7391e651b3 100644 --- a/src/ProductConstructionService/ProductConstructionService.Api/Dockerfile +++ b/src/ProductConstructionService/ProductConstructionService.Api/Dockerfile @@ -18,6 +18,7 @@ COPY ["src/Maestro/Client/src/Microsoft.DotNet.Maestro.Client.csproj", "./Maestr COPY ["src/Maestro/Maestro.Authentication/Maestro.Authentication.csproj", "./Maestro/Maestro.Authentication/"] COPY ["src/Maestro/Maestro.AzureDevOps/Maestro.AzureDevOps.csproj", "./Maestro/Maestro.AzureDevOps/"] COPY ["src/Maestro/Maestro.Data/Maestro.Data.csproj", "./Maestro/Maestro.Data/"] +COPY ["src/Maestro/Maestro.Common/Maestro.Common.csproj", "./Maestro/Maestro.Common/"] COPY ["src/Maestro/Maestro.DataProviders/Maestro.DataProviders.csproj", "./Maestro/Maestro.DataProviders/"] COPY ["src/Maestro/Maestro.MergePolicyEvaluation/Maestro.MergePolicyEvaluation.csproj", "./Maestro/Maestro.MergePolicyEvaluation/"] @@ -32,6 +33,7 @@ COPY ["src/Maestro/Client/src", "./Maestro/Client/src"] COPY ["src/Maestro/Maestro.Authentication", "./Maestro/Maestro.Authentication"] COPY ["src/Maestro/Maestro.AzureDevOps", "./Maestro/Maestro.AzureDevOps"] COPY ["src/Maestro/Maestro.Data", "./Maestro/Maestro.Data"] +COPY ["src/Maestro/Maestro.Common", "./Maestro/Maestro.Common"] COPY ["src/Maestro/Maestro.DataProviders", "./Maestro/Maestro.DataProviders"] COPY ["src/Maestro/Maestro.MergePolicyEvaluation", "./Maestro/Maestro.MergePolicyEvaluation"] diff --git a/src/ProductConstructionService/ProductConstructionService.Client/PcsApiCredential.cs b/src/ProductConstructionService/ProductConstructionService.Client/PcsApiCredential.cs deleted file mode 100644 index d023169310..0000000000 --- a/src/ProductConstructionService/ProductConstructionService.Client/PcsApiCredential.cs +++ /dev/null @@ -1,75 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Azure.Core; -using Azure.Identity; - -namespace ProductConstructionService.Client -{ - /// - /// A credential that first tries a user-based browser auth flow then falls back to a managed identity-based flow. - /// - internal class PcsApiCredential : TokenCredential - { - private const string TENANT_ID = "72f988bf-86f1-41af-91ab-2d7cd011db47"; - - private static readonly Dictionary EntraAppIds = new Dictionary - { - [ProductConstructionServiceApi.StagingPcsBaseUri.TrimEnd('/')] = "baf98f1b-374e-487d-af42-aa33807f11e4", - }; - - private readonly TokenRequestContext _requestContext; - private readonly TokenCredential _tokenCredential; - - private PcsApiCredential(TokenCredential credential, TokenRequestContext requestContext) - { - _requestContext = requestContext; - _tokenCredential = credential; - } - - public override AccessToken GetToken(TokenRequestContext _, CancellationToken cancellationToken) - { - // We hardcode the request context as we know which scopes we need to invoke in each scenario (user vs daemon) - return _tokenCredential.GetToken(_requestContext, cancellationToken); - } - - public override ValueTask GetTokenAsync(TokenRequestContext _, CancellationToken cancellationToken) - { - // We hardcode the request context as we know which scopes we need to invoke in each scenario (user vs daemon) - return _tokenCredential.GetTokenAsync(_requestContext, cancellationToken); - } - - /// - /// Use this for darc invocations from services using an MI - /// - internal static PcsApiCredential CreateManagedIdentityCredential(string barApiBaseUri, string managedIdentityId) - { - string appId = EntraAppIds[barApiBaseUri.TrimEnd('/')]; - - var miCredential = managedIdentityId == "system" - ? new ManagedIdentityCredential() - : new ManagedIdentityCredential(managedIdentityId); - - var appCredential = new ClientAssertionCredential( - TENANT_ID, - appId, - async (ct) => (await miCredential.GetTokenAsync(new TokenRequestContext(new string[] { "api://AzureADTokenExchange" }), ct)).Token); - - var requestContext = new TokenRequestContext(new string[] { $"api://{appId}/.default" }); - return new PcsApiCredential(appCredential, requestContext); - } - - /// - /// Use this for darc invocations from pipelines without a token. - /// - internal static PcsApiCredential CreateNonUserCredential(string barApiBaseUri) - { - var requestContext = new TokenRequestContext(new string[] { $"{EntraAppIds[barApiBaseUri.TrimEnd('/')]}/.default" }); - var credential = new AzureCliCredential(); - return new PcsApiCredential(credential, requestContext); - } - } -} diff --git a/src/ProductConstructionService/ProductConstructionService.Client/PcsApiTokenCredential.cs b/src/ProductConstructionService/ProductConstructionService.Client/PcsApiTokenCredential.cs deleted file mode 100644 index 75165ceb50..0000000000 --- a/src/ProductConstructionService/ProductConstructionService.Client/PcsApiTokenCredential.cs +++ /dev/null @@ -1,26 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Threading; -using System.Threading.Tasks; -using Azure.Core; - -namespace ProductConstructionService.Client -{ - public class PcsApiTokenCredential : TokenCredential - { - public PcsApiTokenCredential(string token) - { - Token = token; - } - - public string Token { get; } - - public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken) - => new AccessToken(Token, DateTimeOffset.MaxValue); - - public override ValueTask GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken) - => new ValueTask(new AccessToken(Token, DateTimeOffset.MaxValue)); - } -} diff --git a/src/ProductConstructionService/ProductConstructionService.Client/ProductConstructionService.Client.csproj b/src/ProductConstructionService/ProductConstructionService.Client/ProductConstructionService.Client.csproj index 6bf40167f9..1316a3cd26 100644 --- a/src/ProductConstructionService/ProductConstructionService.Client/ProductConstructionService.Client.csproj +++ b/src/ProductConstructionService/ProductConstructionService.Client/ProductConstructionService.Client.csproj @@ -1,4 +1,4 @@ - + true @@ -21,4 +21,8 @@ + + + + diff --git a/src/ProductConstructionService/ProductConstructionService.Client/ProductConstructionServiceApi.cs b/src/ProductConstructionService/ProductConstructionService.Client/ProductConstructionServiceApi.cs deleted file mode 100644 index ce99d83f97..0000000000 --- a/src/ProductConstructionService/ProductConstructionService.Client/ProductConstructionServiceApi.cs +++ /dev/null @@ -1,42 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Azure.Core; - -namespace ProductConstructionService.Client -{ - public partial class ProductConstructionServiceApi - { - public const string StagingPcsBaseUri = "https://product-construction-int.delightfuldune-c0f01ab0.westus2.azurecontainerapps.io/"; - - /// - /// Creates a credential based on parameters provided. - /// - /// BAR API URI used to determine the right set of credentials (INT vs PROD) - /// Token to use for the call. If none supplied, will try other flows. - /// Managed Identity to use for the auth - /// Credential that can be used to call the Maestro API - public static TokenCredential CreateApiCredential( - string barApiBaseUri, - string barApiToken = null, - string managedIdentityId = null) - { - // 1. BAR or Entra token that can directly be used to authenticate against Maestro - if (!string.IsNullOrEmpty(barApiToken)) - { - return new PcsApiTokenCredential(barApiToken!); - } - - barApiBaseUri ??= StagingPcsBaseUri; - - // 2. Managed identity (for server-to-server scenarios) - if (!string.IsNullOrEmpty(managedIdentityId)) - { - return PcsApiCredential.CreateManagedIdentityCredential(barApiBaseUri, managedIdentityId!); - } - - // 3. Azure CLI authentication (for CI scenarios) - return PcsApiCredential.CreateNonUserCredential(barApiBaseUri); - } - } -} diff --git a/src/ProductConstructionService/ProductConstructionService.Client/ProductConstructionServiceApiOptions.cs b/src/ProductConstructionService/ProductConstructionService.Client/ProductConstructionServiceApiOptions.cs index 7f436fdc2d..eabc3a0783 100644 --- a/src/ProductConstructionService/ProductConstructionService.Client/ProductConstructionServiceApiOptions.cs +++ b/src/ProductConstructionService/ProductConstructionService.Client/ProductConstructionServiceApiOptions.cs @@ -2,11 +2,20 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Collections.Generic; +using Microsoft.DotNet.Maestro.Common; namespace ProductConstructionService.Client { public partial class ProductConstructionServiceApiOptions { + public const string StagingPcsBaseUri = "https://product-construction-int.delightfuldune-c0f01ab0.westus2.azurecontainerapps.io/"; + + private static readonly Dictionary EntraAppIds = new Dictionary + { + [StagingPcsBaseUri.TrimEnd('/')] = "baf98f1b-374e-487d-af42-aa33807f11e4", + }; + /// /// Creates a new instance of with the provided base URI. /// @@ -16,7 +25,12 @@ public partial class ProductConstructionServiceApiOptions public ProductConstructionServiceApiOptions(string baseUri, string accessToken, string managedIdentityId) : this( new Uri(baseUri), - ProductConstructionServiceApi.CreateApiCredential(baseUri, accessToken, managedIdentityId)) + AppCredentialResolver.CreateCredential( + EntraAppIds[baseUri.TrimEnd('/')], + disableInteractiveAuth: true, // the client is only used in Maestro for now + token: accessToken, + federatedToken: null, + managedIdentityId: managedIdentityId)) { } @@ -28,8 +42,13 @@ public ProductConstructionServiceApiOptions(string baseUri, string accessToken, /// Managed Identity to use for the auth public ProductConstructionServiceApiOptions(string accessToken, string managedIdentityId) : this( - new Uri(ProductConstructionServiceApi.StagingPcsBaseUri), - ProductConstructionServiceApi.CreateApiCredential(ProductConstructionServiceApi.StagingPcsBaseUri, accessToken, managedIdentityId)) + new Uri(StagingPcsBaseUri), + AppCredentialResolver.CreateCredential( + EntraAppIds[StagingPcsBaseUri.TrimEnd('/')], + disableInteractiveAuth: true, // the client is only used in Maestro for now + token: accessToken, + federatedToken: null, + managedIdentityId: managedIdentityId)) { } } diff --git a/src/ProductConstructionService/Readme.md b/src/ProductConstructionService/Readme.md index f5b4a5d5c8..04d405a3ea 100644 --- a/src/ProductConstructionService/Readme.md +++ b/src/ProductConstructionService/Readme.md @@ -56,7 +56,7 @@ Once the resources are created and configured: - Copy the Client ID, and paste it in the correct appconfig.json, under `ManagedIdentityClientId` - Add this identity as a user to AzDo so it can get AzDo tokens (you'll need a saw for this). You might have to remove the old user identity before doing this - Update the `ProductConstructionServiceDeploymentProd` (or `ProductConstructionServiceDeploymentInt`) Service Connection with the new MI information (you'll also have to create a Federated Credential in the MI) - - Update the default PCS URI in `ProductConstructionServiceApi`. + - Update the default PCS URI in `ProductConstructionServiceApiOptions`. We're not able to configure a few Kusto things in bicep: - Give the PCS Managed Identity the permissions it needs: diff --git a/test/Microsoft.DotNet.Darc.VirtualMonoRepo.E2E.Tests/VmrTestsBase.cs b/test/Microsoft.DotNet.Darc.VirtualMonoRepo.E2E.Tests/VmrTestsBase.cs index c61d08c558..8ecb3c196b 100644 --- a/test/Microsoft.DotNet.Darc.VirtualMonoRepo.E2E.Tests/VmrTestsBase.cs +++ b/test/Microsoft.DotNet.Darc.VirtualMonoRepo.E2E.Tests/VmrTestsBase.cs @@ -87,7 +87,7 @@ public void DeleteCurrentTestDirectory() federatedToken: null, managedIdentityId: null, disableInteractiveAuth: true, - buildAssetRegistryBaseUri: MaestroApi.StagingBuildAssetRegistryBaseUri)); + buildAssetRegistryBaseUri: MaestroApiOptions.StagingBuildAssetRegistryBaseUri)); protected static List GetExpectedFilesInVmr( NativePath vmrPath, From cb4dbd98a4e6e931f8729d2fb992386f549e5573 Mon Sep 17 00:00:00 2001 From: Pavel Purma Date: Wed, 19 Jun 2024 16:41:57 +0200 Subject: [PATCH 6/9] Gather drop blob downloads without SAS tokens using Entra identity --- .../Darc/Operations/GatherDropOperation.cs | 48 +++++++++++++++---- .../Options/GatherDropCommandLineOptions.cs | 3 ++ .../DarcLib/Helpers/HttpRequestManager.cs | 10 +++- 3 files changed, 52 insertions(+), 9 deletions(-) diff --git a/src/Microsoft.DotNet.Darc/Darc/Operations/GatherDropOperation.cs b/src/Microsoft.DotNet.Darc/Darc/Operations/GatherDropOperation.cs index d9c607188f..36d240810b 100644 --- a/src/Microsoft.DotNet.Darc/Darc/Operations/GatherDropOperation.cs +++ b/src/Microsoft.DotNet.Darc/Darc/Operations/GatherDropOperation.cs @@ -12,6 +12,8 @@ using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; +using Azure.Core; +using Azure.Identity; using Microsoft.DotNet.Darc.Helpers; using Microsoft.DotNet.Darc.Options; using Microsoft.DotNet.DarcLib; @@ -22,7 +24,6 @@ using Microsoft.DotNet.Services.Utility; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using Microsoft.VisualStudio.Services.Common; using Newtonsoft.Json; namespace Microsoft.DotNet.Darc.Operations; @@ -36,10 +37,21 @@ internal class InputBuilds internal class GatherDropOperation : Operation { private readonly GatherDropCommandLineOptions _options; + private Lazy _defaultAzureCredential; + public GatherDropOperation(GatherDropCommandLineOptions options) : base(options) { _options = options; + _defaultAzureCredential = new Lazy(() => + new DefaultAzureCredential(new DefaultAzureCredentialOptions + { + ExcludeVisualStudioCodeCredential = true, + ExcludeVisualStudioCredential = true, + ExcludeAzureDeveloperCliCredential = true, + ExcludeInteractiveBrowserCredential = _options.IsCi + } + )); } private const string PackagesSubPath = "packages"; @@ -607,7 +619,7 @@ private async Task GatherDropForBuildAsync(Build build, string List mustDownloadAssets = []; string[] alwaysDownloadRegexes = _options.AlwaysDownloadAssetPatterns.Split(',', StringSplitOptions.RemoveEmptyEntries); - var assets = await barClient.GetAssetsAsync(buildId: build.Id, nonShipping: (!_options.IncludeNonShipping ? (bool?) false : null)); + var assets = await barClient.GetAssetsAsync(buildId: build.Id, nonShipping: (!_options.IncludeNonShipping ? (bool?)false : null)); if (!string.IsNullOrEmpty(_options.AssetFilter)) { assets = assets.Where(asset => Regex.IsMatch(asset.Name, _options.AssetFilter)); @@ -1190,12 +1202,12 @@ private async Task DownloadBlobAsync(HttpClient client, var unifiedFullTargetPath = Path.Combine(unifiedFullSubPath, normalizedAssetName); List targetFilePaths = []; - + if (_options.Separated) { targetFilePaths.Add(releaseFullTargetPath); } - + targetFilePaths.Add(unifiedFullTargetPath); var downloadedAsset = new DownloadedAsset() @@ -1378,12 +1390,15 @@ private async Task DownloadFileAsync(HttpClient client, } else { - // Append and attempt to use the suffixes that were passed in to download from the uri - foreach (string sasSuffix in _options.SASSuffixes) + if (!_options.UseAzureCredentialForBlobs) { - if (await DownloadFileImplAsync(client, $"{sourceUri}{sasSuffix}", authHeader, targetFilePaths, errors, downloadOutput, cancellationToken)) + // Append and attempt to use the suffixes that were passed in to download from the uri + foreach (string sasSuffix in _options.SASSuffixes) { - return true; + if (await DownloadFileImplAsync(client, $"{sourceUri}{sasSuffix}", authHeader, targetFilePaths, errors, downloadOutput, cancellationToken)) + { + return true; + } } } } @@ -1434,6 +1449,7 @@ private async Task DownloadFileImplAsync(HttpClient client, sourceUri, Logger, authHeader: authHeader, + adjustRequestMessage: AdjustRequestMessage, httpCompletionOption: HttpCompletionOption.ResponseHeadersRead); using (var response = await manager.ExecuteAsync()) @@ -1491,6 +1507,22 @@ private async Task DownloadFileImplAsync(HttpClient client, return false; } + private void AdjustRequestMessage(HttpRequestMessage request) + { + if (request.RequestUri.Host.Contains(".blob.core.windows.net", StringComparison.OrdinalIgnoreCase)) + { + // add API version to support Bearer token authentication + request.Headers.Add("x-ms-version", "2023-08-03"); + + if (_options.UseAzureCredentialForBlobs) + { + TokenRequestContext tokenRequest = new TokenRequestContext(["https://storage.azure.com/"]); + var token = _defaultAzureCredential.Value.GetToken(tokenRequest); + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token.Token); + } + } + } + /// /// Delete a file with retry. Sometimes when gathering a drop directly to a share /// we can get occasional deletion failures. All of them seen so far are UnauthorizedAccessExceptions diff --git a/src/Microsoft.DotNet.Darc/Darc/Options/GatherDropCommandLineOptions.cs b/src/Microsoft.DotNet.Darc/Darc/Options/GatherDropCommandLineOptions.cs index 2645905b8e..6ee1d61ec9 100644 --- a/src/Microsoft.DotNet.Darc/Darc/Options/GatherDropCommandLineOptions.cs +++ b/src/Microsoft.DotNet.Darc/Darc/Options/GatherDropCommandLineOptions.cs @@ -78,6 +78,9 @@ internal class GatherDropCommandLineOptions : CommandLineOptions [RedactFromLogging] public IEnumerable SASSuffixes { get; set; } + [Option("use-azure-credential-for-blobs", HelpText = "Use DefaultAzureCredential to acquire token for downloading assets from Blob storages")] + public bool UseAzureCredentialForBlobs { get; set; } + [Option("always-download-asset-filters", HelpText = "Comma-separated list of exact names or regexes which will always be downloaded. If not part of the usual payload, they will be downloaded to an 'extra-assets' folder.")] public string AlwaysDownloadAssetPatterns { get; set; } = ""; diff --git a/src/Microsoft.DotNet.Darc/DarcLib/Helpers/HttpRequestManager.cs b/src/Microsoft.DotNet.Darc/DarcLib/Helpers/HttpRequestManager.cs index 1f41564c33..640a0fbfd6 100644 --- a/src/Microsoft.DotNet.Darc/DarcLib/Helpers/HttpRequestManager.cs +++ b/src/Microsoft.DotNet.Darc/DarcLib/Helpers/HttpRequestManager.cs @@ -20,6 +20,7 @@ public class HttpRequestManager private readonly string _body; private readonly string _requestUri; private readonly AuthenticationHeaderValue _authHeader; + private readonly Action _adjustRequestMessage; private readonly HttpMethod _method; private readonly HttpCompletionOption _httpCompletionOption; @@ -32,6 +33,7 @@ public HttpRequestManager( string versionOverride = null, bool logFailure = true, AuthenticationHeaderValue authHeader = null, + Action adjustRequestMessage = null, HttpCompletionOption httpCompletionOption = HttpCompletionOption.ResponseContentRead) { _client = client; @@ -41,6 +43,7 @@ public HttpRequestManager( _requestUri = requestUri; _method = method; _authHeader = authHeader; + _adjustRequestMessage = adjustRequestMessage; _httpCompletionOption = httpCompletionOption; } @@ -75,6 +78,11 @@ public async Task ExecuteAsync(int retryCount = 3) message.Headers.Authorization = _authHeader; } + if (_adjustRequestMessage != null) + { + _adjustRequestMessage(message); + } + response = await _client.SendAsync(message, _httpCompletionOption); if (stopRetriesHttpStatusCodes.Contains(response.StatusCode)) @@ -104,7 +112,7 @@ public async Task ExecuteAsync(int retryCount = 3) response?.Dispose(); // For CLI users this will look normal, but translating to a DarcAuthenticationFailureException means it opts in to automated failure logging. - if (ex is HttpRequestException && ex.Message.Contains(((int) HttpStatusCode.Unauthorized).ToString())) + if (ex is HttpRequestException && ex.Message.Contains(((int)HttpStatusCode.Unauthorized).ToString())) { int queryParamIndex = _requestUri.IndexOf('?'); string sanitizedRequestUri = queryParamIndex < 0 ? _requestUri : $"{_requestUri.Substring(0, queryParamIndex)}?***"; From 5f9bac16cf6f283bfb624bcaa8c702fb7cee3e68 Mon Sep 17 00:00:00 2001 From: Pavel Purma Date: Wed, 19 Jun 2024 16:48:41 +0200 Subject: [PATCH 7/9] If else fix --- .../Darc/Operations/GatherDropOperation.cs | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/Microsoft.DotNet.Darc/Darc/Operations/GatherDropOperation.cs b/src/Microsoft.DotNet.Darc/Darc/Operations/GatherDropOperation.cs index 36d240810b..a8f7a14f63 100644 --- a/src/Microsoft.DotNet.Darc/Darc/Operations/GatherDropOperation.cs +++ b/src/Microsoft.DotNet.Darc/Darc/Operations/GatherDropOperation.cs @@ -1388,17 +1388,14 @@ private async Task DownloadFileAsync(HttpClient client, { return true; } - else + else if (!_options.UseAzureCredentialForBlobs) { - if (!_options.UseAzureCredentialForBlobs) + // Append and attempt to use the suffixes that were passed in to download from the uri + foreach (string sasSuffix in _options.SASSuffixes) { - // Append and attempt to use the suffixes that were passed in to download from the uri - foreach (string sasSuffix in _options.SASSuffixes) + if (await DownloadFileImplAsync(client, $"{sourceUri}{sasSuffix}", authHeader, targetFilePaths, errors, downloadOutput, cancellationToken)) { - if (await DownloadFileImplAsync(client, $"{sourceUri}{sasSuffix}", authHeader, targetFilePaths, errors, downloadOutput, cancellationToken)) - { - return true; - } + return true; } } } From 10f8c16a2073093599096bca9f5a3ad0ebbe0242 Mon Sep 17 00:00:00 2001 From: Pavel Purma Date: Wed, 19 Jun 2024 17:33:00 +0200 Subject: [PATCH 8/9] Update naming of lambda for request modification --- .../Darc/Operations/GatherDropOperation.cs | 4 ++-- .../DarcLib/Helpers/HttpRequestManager.cs | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Microsoft.DotNet.Darc/Darc/Operations/GatherDropOperation.cs b/src/Microsoft.DotNet.Darc/Darc/Operations/GatherDropOperation.cs index a8f7a14f63..72a7376c52 100644 --- a/src/Microsoft.DotNet.Darc/Darc/Operations/GatherDropOperation.cs +++ b/src/Microsoft.DotNet.Darc/Darc/Operations/GatherDropOperation.cs @@ -1446,7 +1446,7 @@ private async Task DownloadFileImplAsync(HttpClient client, sourceUri, Logger, authHeader: authHeader, - adjustRequestMessage: AdjustRequestMessage, + configureRequestMessage: ConfigureRequestMessage, httpCompletionOption: HttpCompletionOption.ResponseHeadersRead); using (var response = await manager.ExecuteAsync()) @@ -1504,7 +1504,7 @@ private async Task DownloadFileImplAsync(HttpClient client, return false; } - private void AdjustRequestMessage(HttpRequestMessage request) + private void ConfigureRequestMessage(HttpRequestMessage request) { if (request.RequestUri.Host.Contains(".blob.core.windows.net", StringComparison.OrdinalIgnoreCase)) { diff --git a/src/Microsoft.DotNet.Darc/DarcLib/Helpers/HttpRequestManager.cs b/src/Microsoft.DotNet.Darc/DarcLib/Helpers/HttpRequestManager.cs index 640a0fbfd6..bb7130e0b8 100644 --- a/src/Microsoft.DotNet.Darc/DarcLib/Helpers/HttpRequestManager.cs +++ b/src/Microsoft.DotNet.Darc/DarcLib/Helpers/HttpRequestManager.cs @@ -20,7 +20,7 @@ public class HttpRequestManager private readonly string _body; private readonly string _requestUri; private readonly AuthenticationHeaderValue _authHeader; - private readonly Action _adjustRequestMessage; + private readonly Action _configureRequestMessage; private readonly HttpMethod _method; private readonly HttpCompletionOption _httpCompletionOption; @@ -33,7 +33,7 @@ public HttpRequestManager( string versionOverride = null, bool logFailure = true, AuthenticationHeaderValue authHeader = null, - Action adjustRequestMessage = null, + Action configureRequestMessage = null, HttpCompletionOption httpCompletionOption = HttpCompletionOption.ResponseContentRead) { _client = client; @@ -43,7 +43,7 @@ public HttpRequestManager( _requestUri = requestUri; _method = method; _authHeader = authHeader; - _adjustRequestMessage = adjustRequestMessage; + _configureRequestMessage = configureRequestMessage; _httpCompletionOption = httpCompletionOption; } @@ -78,9 +78,9 @@ public async Task ExecuteAsync(int retryCount = 3) message.Headers.Authorization = _authHeader; } - if (_adjustRequestMessage != null) + if (_configureRequestMessage != null) { - _adjustRequestMessage(message); + _configureRequestMessage(message); } response = await _client.SendAsync(message, _httpCompletionOption); From 6660885f50810c4c1d5378ebef34cd45f905d32c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C5=99emek=20Vysok=C3=BD?= Date: Thu, 20 Jun 2024 13:36:15 +0200 Subject: [PATCH 9/9] Make local darc settings available in DI (#3658) --- .../Darc/Helpers/LocalSettings.cs | 2 +- .../Darc/Helpers/RemoteFactory.cs | 17 ++++++----------- .../Operations/AddBuildToChannelOperation.cs | 3 --- .../Darc/Operations/GatherDropOperation.cs | 6 ------ .../Darc/Operations/Operation.cs | 1 + .../Darc/Options/CommandLineOptions.cs | 11 +++++++++++ .../Darc/Options/ICommandLineOptions.cs | 6 ++++++ 7 files changed, 25 insertions(+), 21 deletions(-) diff --git a/src/Microsoft.DotNet.Darc/Darc/Helpers/LocalSettings.cs b/src/Microsoft.DotNet.Darc/Darc/Helpers/LocalSettings.cs index 1c7a64ef2a..7d2b688ea1 100644 --- a/src/Microsoft.DotNet.Darc/Darc/Helpers/LocalSettings.cs +++ b/src/Microsoft.DotNet.Darc/Darc/Helpers/LocalSettings.cs @@ -81,7 +81,7 @@ public static LocalSettings GetSettings(ICommandLineOptions options, ILogger log { if (!options.IsCi && options.OutputFormat != DarcOutputType.json) { - logger.LogWarning(e, $"Failed to load the darc settings file, may be corrupted"); + logger.LogInformation(e, $"Failed to load the darc settings file, may be corrupted"); } } diff --git a/src/Microsoft.DotNet.Darc/Darc/Helpers/RemoteFactory.cs b/src/Microsoft.DotNet.Darc/Darc/Helpers/RemoteFactory.cs index 5a71b6676e..96c081234b 100644 --- a/src/Microsoft.DotNet.Darc/Darc/Helpers/RemoteFactory.cs +++ b/src/Microsoft.DotNet.Darc/Darc/Helpers/RemoteFactory.cs @@ -26,15 +26,12 @@ public static IRemote GetRemote(ICommandLineOptions options, string repoUrl, ILo } public static IBarApiClient GetBarClient(ICommandLineOptions options, ILogger logger) - { - var settings = LocalSettings.GetSettings(options, logger); - return new BarApiClient( - settings?.BuildAssetRegistryToken, - options?.FederatedToken, + => new BarApiClient( + options.BuildAssetRegistryToken, + options.FederatedToken, managedIdentityId: null, options.IsCi, - settings?.BuildAssetRegistryBaseUri); - } + options.BuildAssetRegistryBaseUri); public Task GetRemoteAsync(string repoUrl, ILogger logger) => Task.FromResult(GetRemote(_options, repoUrl, logger)); @@ -47,8 +44,6 @@ public Task GetDependencyFileManagerAsync(string repoUrl private static IRemoteGitRepo GetRemoteGitClient(ICommandLineOptions options, string repoUrl, ILogger logger) { - var darcSettings = LocalSettings.GetSettings(options, logger); - string temporaryRepositoryRoot = Path.GetTempPath(); var repoType = GitRepoUrlParser.ParseTypeFromUri(repoUrl); @@ -58,7 +53,7 @@ private static IRemoteGitRepo GetRemoteGitClient(ICommandLineOptions options, st GitRepoType.GitHub => new GitHubClient( options.GitLocation, - darcSettings.GitHubToken, + options.GitHubPat, logger, temporaryRepositoryRoot, // Caching not in use for Darc local client. @@ -67,7 +62,7 @@ private static IRemoteGitRepo GetRemoteGitClient(ICommandLineOptions options, st GitRepoType.AzureDevOps => new AzureDevOpsClient( options.GitLocation, - darcSettings.AzureDevOpsToken, + options.AzureDevOpsPat, logger, temporaryRepositoryRoot), diff --git a/src/Microsoft.DotNet.Darc/Darc/Operations/AddBuildToChannelOperation.cs b/src/Microsoft.DotNet.Darc/Darc/Operations/AddBuildToChannelOperation.cs index 44c348770b..03d5a4c69b 100644 --- a/src/Microsoft.DotNet.Darc/Darc/Operations/AddBuildToChannelOperation.cs +++ b/src/Microsoft.DotNet.Darc/Darc/Operations/AddBuildToChannelOperation.cs @@ -213,9 +213,6 @@ private async Task PromoteBuildAsync(Build build, List targetChann return Constants.SuccessCode; } - LocalSettings localSettings = LocalSettings.GetSettings(_options, Logger); - _options.AzureDevOpsPat = (string.IsNullOrEmpty(_options.AzureDevOpsPat)) ? localSettings.AzureDevOpsToken : _options.AzureDevOpsPat; - if (string.IsNullOrEmpty(_options.AzureDevOpsPat)) { Console.WriteLine($"Promoting build {build.Id} with the given parameters would require starting the Build Promotion pipeline, however an AzDO PAT was not found."); diff --git a/src/Microsoft.DotNet.Darc/Darc/Operations/GatherDropOperation.cs b/src/Microsoft.DotNet.Darc/Darc/Operations/GatherDropOperation.cs index 72a7376c52..9ccfc5da07 100644 --- a/src/Microsoft.DotNet.Darc/Darc/Operations/GatherDropOperation.cs +++ b/src/Microsoft.DotNet.Darc/Darc/Operations/GatherDropOperation.cs @@ -1084,12 +1084,6 @@ private async Task DownloadAssetFromAzureDevOpsFeedAsync(HttpClient clie var packageContentUrl = $"https://pkgs.dev.azure.com/{feedAccount}/{feedProject}_apis/packaging/feeds/{feedName}/nuget/packages/{assetName}/versions/{asset.Version}/content"; - if (string.IsNullOrEmpty(_options.AzureDevOpsPat)) - { - var localSettings = LocalSettings.GetSettings(_options, Logger); - _options.AzureDevOpsPat = localSettings.AzureDevOpsToken; - } - var authHeader = new AuthenticationHeaderValue( "Basic", Convert.ToBase64String(Encoding.ASCII.GetBytes(string.Format("{0}:{1}", "", _options.AzureDevOpsPat)))); diff --git a/src/Microsoft.DotNet.Darc/Darc/Operations/Operation.cs b/src/Microsoft.DotNet.Darc/Darc/Operations/Operation.cs index 5dea802f77..7027f7be39 100644 --- a/src/Microsoft.DotNet.Darc/Darc/Operations/Operation.cs +++ b/src/Microsoft.DotNet.Darc/Darc/Operations/Operation.cs @@ -60,6 +60,7 @@ protected Operation(ICommandLineOptions options, IServiceCollection? services = Provider = services.BuildServiceProvider(); Logger = Provider.GetRequiredService>(); + options.InitializeFromSettings(Logger); } public abstract Task ExecuteAsync(); diff --git a/src/Microsoft.DotNet.Darc/Darc/Options/CommandLineOptions.cs b/src/Microsoft.DotNet.Darc/Darc/Options/CommandLineOptions.cs index 5a476ef5d7..1a4c26fc41 100644 --- a/src/Microsoft.DotNet.Darc/Darc/Options/CommandLineOptions.cs +++ b/src/Microsoft.DotNet.Darc/Darc/Options/CommandLineOptions.cs @@ -2,8 +2,10 @@ // The .NET Foundation licenses this file to you under the MIT license. using CommandLine; +using Microsoft.DotNet.Darc.Helpers; using Microsoft.DotNet.Darc.Operations; using Microsoft.DotNet.DarcLib; +using Microsoft.Extensions.Logging; namespace Microsoft.DotNet.Darc.Options; @@ -57,4 +59,13 @@ public RemoteConfiguration GetRemoteConfiguration() { return new RemoteConfiguration(GitHubPat, AzureDevOpsPat); } + + public void InitializeFromSettings(ILogger logger) + { + var localSettings = LocalSettings.GetSettings(this, logger); + AzureDevOpsPat ??= localSettings.AzureDevOpsToken; + GitHubPat ??= localSettings.GitHubToken; + BuildAssetRegistryBaseUri ??= localSettings.BuildAssetRegistryBaseUri; + BuildAssetRegistryToken ??= localSettings.BuildAssetRegistryToken; + } } diff --git a/src/Microsoft.DotNet.Darc/Darc/Options/ICommandLineOptions.cs b/src/Microsoft.DotNet.Darc/Darc/Options/ICommandLineOptions.cs index f41459226f..878c9c63bd 100644 --- a/src/Microsoft.DotNet.Darc/Darc/Options/ICommandLineOptions.cs +++ b/src/Microsoft.DotNet.Darc/Darc/Options/ICommandLineOptions.cs @@ -3,6 +3,7 @@ using Microsoft.DotNet.Darc.Operations; using Microsoft.DotNet.DarcLib; +using Microsoft.Extensions.Logging; namespace Microsoft.DotNet.Darc.Options; public interface ICommandLineOptions @@ -20,4 +21,9 @@ public interface ICommandLineOptions Operation GetOperation(); RemoteConfiguration GetRemoteConfiguration(); + + /// + /// Reads missing options from the local settings. + /// + void InitializeFromSettings(ILogger logger); }