From 43bbb5a371babc39ad41fd6b875ac3051a388742 Mon Sep 17 00:00:00 2001 From: David Fowler Date: Sat, 12 Apr 2025 23:37:28 -0700 Subject: [PATCH 1/2] Fixed resolving secrets for keyvault refernces in run mode - Moved token credential resolution out of the AzureProvisioner so it can shared. - Configure the secret resolving in both configure and provision cases. --- .../AzureProvisionerExtensions.cs | 2 + .../Provisioners/AzureProvisioner.cs | 39 ++------------- .../Provisioners/BicepProvisioner.cs | 34 +++++++++---- .../Provisioners/TokenCredentialHolder.cs | 50 +++++++++++++++++++ 4 files changed, 80 insertions(+), 45 deletions(-) create mode 100644 src/Aspire.Hosting.Azure/Provisioning/Provisioners/TokenCredentialHolder.cs diff --git a/src/Aspire.Hosting.Azure/Provisioning/AzureProvisionerExtensions.cs b/src/Aspire.Hosting.Azure/Provisioning/AzureProvisionerExtensions.cs index 980484a57bc..d11eecd0b5b 100644 --- a/src/Aspire.Hosting.Azure/Provisioning/AzureProvisionerExtensions.cs +++ b/src/Aspire.Hosting.Azure/Provisioning/AzureProvisionerExtensions.cs @@ -29,6 +29,8 @@ public static IDistributedApplicationBuilder AddAzureProvisioning(this IDistribu .ValidateDataAnnotations() .ValidateOnStart(); + builder.Services.AddSingleton(); + builder.AddAzureProvisioner(); return builder; diff --git a/src/Aspire.Hosting.Azure/Provisioning/Provisioners/AzureProvisioner.cs b/src/Aspire.Hosting.Azure/Provisioning/Provisioners/AzureProvisioner.cs index ca731a0634a..3c45b7a7436 100644 --- a/src/Aspire.Hosting.Azure/Provisioning/Provisioners/AzureProvisioner.cs +++ b/src/Aspire.Hosting.Azure/Provisioning/Provisioners/AzureProvisioner.cs @@ -13,7 +13,6 @@ using Aspire.Hosting.Lifecycle; using Azure; using Azure.Core; -using Azure.Identity; using Azure.ResourceManager; using Azure.ResourceManager.Resources; using Microsoft.Extensions.Configuration; @@ -35,7 +34,8 @@ internal sealed class AzureProvisioner( IServiceProvider serviceProvider, ResourceNotificationService notificationService, ResourceLoggerService loggerService, - IDistributedApplicationEventing eventing + IDistributedApplicationEventing eventing, + TokenCredentialHolder tokenCredentialHolder ) : IDistributedApplicationLifecycleHook { internal const string AspireResourceNameTag = "aspire-resource-name"; @@ -219,7 +219,7 @@ private async Task ProvisionAzureResources( var userSecretsLazy = new Lazy>(() => GetUserSecretsAsync(userSecretsPath, cancellationToken)); // Make resources wait on the same provisioning context - var provisioningContextLazy = new Lazy>(() => GetProvisioningContextAsync(userSecretsLazy, cancellationToken)); + var provisioningContextLazy = new Lazy>(() => GetProvisioningContextAsync(tokenCredentialHolder.Credential, userSecretsLazy, cancellationToken)); var tasks = new List(); @@ -366,39 +366,8 @@ async Task PublishConnectionStringAvailableEventAsync() } } - private async Task GetProvisioningContextAsync(Lazy> userSecretsLazy, CancellationToken cancellationToken) + private async Task GetProvisioningContextAsync(TokenCredential credential, Lazy> userSecretsLazy, CancellationToken cancellationToken) { - // Optionally configured in AppHost appSettings under "Azure" : { "CredentialSource": "AzureCli" } - var credentialSetting = _options.CredentialSource; - - TokenCredential credential = credentialSetting switch - { - "AzureCli" => new AzureCliCredential(), - "AzurePowerShell" => new AzurePowerShellCredential(), - "VisualStudio" => new VisualStudioCredential(), - "VisualStudioCode" => new VisualStudioCodeCredential(), - "AzureDeveloperCli" => new AzureDeveloperCliCredential(), - "InteractiveBrowser" => new InteractiveBrowserCredential(), - _ => new DefaultAzureCredential(new DefaultAzureCredentialOptions() - { - ExcludeManagedIdentityCredential = true, - ExcludeWorkloadIdentityCredential = true, - ExcludeAzurePowerShellCredential = true, - CredentialProcessTimeout = TimeSpan.FromSeconds(15) - }) - }; - - if (credential.GetType() == typeof(DefaultAzureCredential)) - { - logger.LogInformation( - "Using DefaultAzureCredential for provisioning. This may not work in all environments. " + - "See https://aka.ms/azsdk/net/identity/credential-chains#defaultazurecredential-overview for more information."); - } - else - { - logger.LogInformation("Using {credentialType} for provisioning.", credential.GetType().Name); - } - var subscriptionId = _options.SubscriptionId ?? throw new MissingConfigurationException("An Azure subscription id is required. Set the Azure:SubscriptionId configuration value."); var armClient = new ArmClient(credential, subscriptionId); diff --git a/src/Aspire.Hosting.Azure/Provisioning/Provisioners/BicepProvisioner.cs b/src/Aspire.Hosting.Azure/Provisioning/Provisioners/BicepProvisioner.cs index d4a0c90d5cb..673d345dca6 100644 --- a/src/Aspire.Hosting.Azure/Provisioning/Provisioners/BicepProvisioner.cs +++ b/src/Aspire.Hosting.Azure/Provisioning/Provisioners/BicepProvisioner.cs @@ -19,7 +19,8 @@ namespace Aspire.Hosting.Azure.Provisioning; internal sealed class BicepProvisioner( ResourceNotificationService notificationService, - ResourceLoggerService loggerService) : AzureResourceProvisioner + ResourceLoggerService loggerService, + TokenCredentialHolder tokenCredentialHolder) : AzureResourceProvisioner { public override bool ShouldProvision(IConfiguration configuration, AzureBicepResource resource) => !resource.IsContainer(); @@ -67,6 +68,12 @@ public override async Task ConfigureResourceAsync(IConfiguration configura } } + if (resource is IAzureKeyVaultResource kvr) + { + ConfigureSecretResolver(kvr); + } + + // Populate secret outputs from key vault (if any) foreach (var item in section.GetSection("SecretOutputs").GetChildren()) { resource.SecretOutputs[item.Key] = item.Value; @@ -280,15 +287,7 @@ await notificationService.PublishUpdateAsync(resource, state => // Populate secret outputs from key vault (if any) if (resource is IAzureKeyVaultResource kvr) { - var vaultUri = resource.Outputs[kvr.VaultUriOutputReference.Name] as string ?? throw new InvalidOperationException($"{kvr.VaultUriOutputReference.Name} not found in outputs."); - - // Set the client for resolving secrets at runtime - var client = new SecretClient(new(vaultUri), context.Credential); - kvr.SecretResolver = async (secretRef, ct) => - { - var secret = await client.GetSecretAsync(secretRef.SecretName, cancellationToken: ct).ConfigureAwait(false); - return secret.Value.Value; - }; + ConfigureSecretResolver(kvr); } await notificationService.PublishUpdateAsync(resource, state => @@ -308,6 +307,21 @@ await notificationService.PublishUpdateAsync(resource, state => .ConfigureAwait(false); } + private void ConfigureSecretResolver(IAzureKeyVaultResource kvr) + { + var resource = (AzureBicepResource)kvr; + + var vaultUri = resource.Outputs[kvr.VaultUriOutputReference.Name] as string ?? throw new InvalidOperationException($"{kvr.VaultUriOutputReference.Name} not found in outputs."); + + // Set the client for resolving secrets at runtime + var client = new SecretClient(new(vaultUri), tokenCredentialHolder.Credential); + kvr.SecretResolver = async (secretRef, ct) => + { + var secret = await client.GetSecretAsync(secretRef.SecretName, cancellationToken: ct).ConfigureAwait(false); + return secret.Value.Value; + }; + } + private static void PopulateWellKnownParameters(AzureBicepResource resource, ProvisioningContext context) { if (resource.Parameters.TryGetValue(AzureBicepResource.KnownParameters.PrincipalId, out var principalId) && principalId is null) diff --git a/src/Aspire.Hosting.Azure/Provisioning/Provisioners/TokenCredentialHolder.cs b/src/Aspire.Hosting.Azure/Provisioning/Provisioners/TokenCredentialHolder.cs new file mode 100644 index 00000000000..0be1e300005 --- /dev/null +++ b/src/Aspire.Hosting.Azure/Provisioning/Provisioners/TokenCredentialHolder.cs @@ -0,0 +1,50 @@ +// 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; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace Aspire.Hosting.Azure.Provisioning; + +internal class TokenCredentialHolder +{ + public TokenCredentialHolder(ILogger logger, IOptions options) + { + // Optionally configured in AppHost appSettings under "Azure" : { "CredentialSource": "AzureCli" } + var credentialSetting = options.Value.CredentialSource; + + TokenCredential credential = credentialSetting switch + { + "AzureCli" => new AzureCliCredential(), + "AzurePowerShell" => new AzurePowerShellCredential(), + "VisualStudio" => new VisualStudioCredential(), + "VisualStudioCode" => new VisualStudioCodeCredential(), + "AzureDeveloperCli" => new AzureDeveloperCliCredential(), + "InteractiveBrowser" => new InteractiveBrowserCredential(), + _ => new DefaultAzureCredential(new DefaultAzureCredentialOptions() + { + ExcludeManagedIdentityCredential = true, + ExcludeWorkloadIdentityCredential = true, + ExcludeAzurePowerShellCredential = true, + CredentialProcessTimeout = TimeSpan.FromSeconds(15) + }) + }; + + if (credential.GetType() == typeof(DefaultAzureCredential)) + { + logger.LogInformation( + "Using DefaultAzureCredential for provisioning. This may not work in all environments. " + + "See https://aka.ms/azsdk/net/identity/credential-chains#defaultazurecredential-overview for more information."); + } + else + { + logger.LogInformation("Using {credentialType} for provisioning.", credential.GetType().Name); + } + + Credential = credential; + } + + public TokenCredential Credential { get; } +} From 9c5ee79056fa86e3b26823bc60248ba615b374fb Mon Sep 17 00:00:00 2001 From: David Fowler Date: Mon, 14 Apr 2025 20:44:16 -0700 Subject: [PATCH 2/2] Update src/Aspire.Hosting.Azure/Provisioning/Provisioners/TokenCredentialHolder.cs --- .../Provisioning/Provisioners/TokenCredentialHolder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Aspire.Hosting.Azure/Provisioning/Provisioners/TokenCredentialHolder.cs b/src/Aspire.Hosting.Azure/Provisioning/Provisioners/TokenCredentialHolder.cs index 0be1e300005..60169f08202 100644 --- a/src/Aspire.Hosting.Azure/Provisioning/Provisioners/TokenCredentialHolder.cs +++ b/src/Aspire.Hosting.Azure/Provisioning/Provisioners/TokenCredentialHolder.cs @@ -10,7 +10,7 @@ namespace Aspire.Hosting.Azure.Provisioning; internal class TokenCredentialHolder { - public TokenCredentialHolder(ILogger logger, IOptions options) + public TokenCredentialHolder(ILogger logger, IOptions options) { // Optionally configured in AppHost appSettings under "Azure" : { "CredentialSource": "AzureCli" } var credentialSetting = options.Value.CredentialSource;