From 29ee021f3e1031cb9d7f5c6b96d48e8dc5c06684 Mon Sep 17 00:00:00 2001 From: James Gould Date: Thu, 27 Mar 2025 23:19:45 +0000 Subject: [PATCH 01/24] Builder pattern introduced for Secret, Key and Certificate clients --- Directory.Packages.props | 4 +- .../Aspire.Azure.Security.KeyVault.csproj | 2 + .../AspireKeyVaultExtensions.cs | 118 +------------ .../AzureKeyVaultClientBuilder.cs | 26 +++ ...VaultClientBuilderCertificateExtensions.cs | 57 ++++++ ...AzureKeyVaultClientBuilderKeyExtensions.cs | 57 ++++++ ...reKeyVaultClientBuilderSecretExtensions.cs | 166 ++++++++++++++++++ .../AzureKeyVaultComponentConstants.cs | 9 + 8 files changed, 328 insertions(+), 111 deletions(-) create mode 100644 src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultClientBuilder.cs create mode 100644 src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultClientBuilderCertificateExtensions.cs create mode 100644 src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultClientBuilderKeyExtensions.cs create mode 100644 src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultClientBuilderSecretExtensions.cs create mode 100644 src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultComponentConstants.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index eca24c6aae8..a20a8cca540 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -22,6 +22,8 @@ + + @@ -237,4 +239,4 @@ - \ No newline at end of file + diff --git a/src/Components/Aspire.Azure.Security.KeyVault/Aspire.Azure.Security.KeyVault.csproj b/src/Components/Aspire.Azure.Security.KeyVault/Aspire.Azure.Security.KeyVault.csproj index 2a4dbb03da4..5f9ec339ce2 100644 --- a/src/Components/Aspire.Azure.Security.KeyVault/Aspire.Azure.Security.KeyVault.csproj +++ b/src/Components/Aspire.Azure.Security.KeyVault/Aspire.Azure.Security.KeyVault.csproj @@ -19,6 +19,8 @@ + + diff --git a/src/Components/Aspire.Azure.Security.KeyVault/AspireKeyVaultExtensions.cs b/src/Components/Aspire.Azure.Security.KeyVault/AspireKeyVaultExtensions.cs index 1b2bffb2b80..00eeebf465a 100644 --- a/src/Components/Aspire.Azure.Security.KeyVault/AspireKeyVaultExtensions.cs +++ b/src/Components/Aspire.Azure.Security.KeyVault/AspireKeyVaultExtensions.cs @@ -1,18 +1,11 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Aspire.Azure.Common; using Aspire.Azure.Security.KeyVault; -using Azure.Core; using Azure.Core.Extensions; -using Azure.Extensions.AspNetCore.Configuration.Secrets; -using Azure.Identity; using Azure.Security.KeyVault.Secrets; -using HealthChecks.Azure.KeyVault.Secrets; using Microsoft.Extensions.Azure; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Diagnostics.HealthChecks; namespace Microsoft.Extensions.Hosting; @@ -21,8 +14,6 @@ namespace Microsoft.Extensions.Hosting; /// public static class AspireKeyVaultExtensions { - internal const string DefaultConfigSectionName = "Aspire:Azure:Security:KeyVault"; - /// /// Registers as a singleton in the services provided by the . /// Enables retries, corresponding health check, logging and telemetry. @@ -32,8 +23,9 @@ public static class AspireKeyVaultExtensions /// An optional method that can be used for customizing the . It's invoked after the settings are read from the configuration. /// An optional method that can be used for customizing the . /// Reads the configuration from "Aspire:Azure:Security:KeyVault" section. + /// An instance of the allowing for further Key Vault Clients to be registered. /// Thrown when mandatory is not provided. - public static void AddAzureKeyVaultClient( + public static AzureKeyVaultClientBuilder AddAzureKeyVaultClient( this IHostApplicationBuilder builder, string connectionName, Action? configureSettings = null, @@ -42,7 +34,8 @@ public static void AddAzureKeyVaultClient( ArgumentNullException.ThrowIfNull(builder); ArgumentException.ThrowIfNullOrEmpty(connectionName); - new KeyVaultComponent().AddClient(builder, DefaultConfigSectionName, configureSettings, configureClientBuilder, connectionName, serviceKey: null); + return new AzureKeyVaultClientBuilder(builder, connectionName, configureSettings) + .AddSecretClient(configureClientBuilder); } /// @@ -54,8 +47,9 @@ public static void AddAzureKeyVaultClient( /// An optional method that can be used for customizing the . It's invoked after the settings are read from the configuration. /// An optional method that can be used for customizing the . /// Reads the configuration from "Aspire:Azure:Security:KeyVault:{name}" section. + /// /// An instance of the allowing for further Key Vault Clients to be registered. /// Thrown when mandatory is not provided. - public static void AddKeyedAzureKeyVaultClient( + public static AzureKeyVaultClientBuilder AddKeyedAzureKeyVaultClient( this IHostApplicationBuilder builder, string name, Action? configureSettings = null, @@ -64,103 +58,7 @@ public static void AddKeyedAzureKeyVaultClient( ArgumentNullException.ThrowIfNull(builder); ArgumentException.ThrowIfNullOrEmpty(name); - new KeyVaultComponent().AddClient(builder, DefaultConfigSectionName, configureSettings, configureClientBuilder, connectionName: name, serviceKey: name); - } - - /// - /// Adds the Azure KeyVault secrets to be configuration values in the . - /// - /// The to add the secrets to. - /// A name used to retrieve the connection string from the ConnectionStrings configuration section. - /// An optional method that can be used for customizing the . It's invoked after the settings are read from the configuration. - /// An optional method that can be used for customizing the . - /// An optional instance to configure the behavior of the configuration provider. - public static void AddAzureKeyVaultSecrets( - this IConfigurationManager configurationManager, - string connectionName, - Action? configureSettings = null, - Action? configureClientOptions = null, - AzureKeyVaultConfigurationOptions? options = null) - { - ArgumentNullException.ThrowIfNull(configurationManager); - ArgumentException.ThrowIfNullOrEmpty(connectionName); - - var client = configurationManager.GetSecretClient(connectionName, configureSettings, configureClientOptions); - configurationManager.AddAzureKeyVault(client, options ?? new AzureKeyVaultConfigurationOptions()); - } - - private static SecretClient GetSecretClient( - this IConfiguration configuration, - string connectionName, - Action? configureSettings, - Action? configureOptions) - { - var configSection = configuration.GetSection(DefaultConfigSectionName); - - var settings = new AzureSecurityKeyVaultSettings(); - configSection.Bind(settings); - - if (configuration.GetConnectionString(connectionName) is string connectionString) - { - ((IConnectionStringSettings)settings).ParseConnectionString(connectionString); - } - - configureSettings?.Invoke(settings); - - var clientOptions = new SecretClientOptions(); - configSection.GetSection("ClientOptions").Bind(clientOptions); - configureOptions?.Invoke(clientOptions); - - if (settings.VaultUri is null) - { - throw new InvalidOperationException($"VaultUri is missing. It should be provided in 'ConnectionStrings:{connectionName}' or under the 'VaultUri' key in the '{DefaultConfigSectionName}' configuration section."); - } - - return new SecretClient(settings.VaultUri, settings.Credential ?? new DefaultAzureCredential(), clientOptions); - } - - private sealed class KeyVaultComponent : AzureComponent - { - protected override IAzureClientBuilder AddClient( - AzureClientFactoryBuilder azureFactoryBuilder, AzureSecurityKeyVaultSettings settings, - string connectionName, string configurationSectionName) - { - return azureFactoryBuilder.AddClient((options, cred, _) => - { - if (settings.VaultUri is null) - { - throw new InvalidOperationException($"VaultUri is missing. It should be provided in 'ConnectionStrings:{connectionName}' or under the 'VaultUri' key in the '{configurationSectionName}' configuration section."); - } - - return new SecretClient(settings.VaultUri, cred, options); - }); - } - - protected override IHealthCheck CreateHealthCheck(SecretClient client, AzureSecurityKeyVaultSettings settings) - => new AzureKeyVaultSecretsHealthCheck(client, new AzureKeyVaultSecretsHealthCheckOptions()); - - protected override void BindClientOptionsToConfiguration(IAzureClientBuilder clientBuilder, IConfiguration configuration) - { -#pragma warning disable IDE0200 // Remove unnecessary lambda expression - needed so the ConfigBinder Source Generator works - clientBuilder.ConfigureOptions(options => configuration.Bind(options)); -#pragma warning restore IDE0200 - } - - protected override void BindSettingsToConfiguration(AzureSecurityKeyVaultSettings settings, IConfiguration configuration) - { - configuration.Bind(settings); - } - - protected override bool GetHealthCheckEnabled(AzureSecurityKeyVaultSettings settings) - => !settings.DisableHealthChecks; - - protected override TokenCredential? GetTokenCredential(AzureSecurityKeyVaultSettings settings) - => settings.Credential; - - protected override bool GetMetricsEnabled(AzureSecurityKeyVaultSettings settings) - => false; - - protected override bool GetTracingEnabled(AzureSecurityKeyVaultSettings settings) - => !settings.DisableTracing; + return new AzureKeyVaultClientBuilder(builder, name, configureSettings) + .AddKeyedSecretClient(serviceKey: name, configureClientBuilder); } } diff --git a/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultClientBuilder.cs b/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultClientBuilder.cs new file mode 100644 index 00000000000..55ce991f642 --- /dev/null +++ b/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultClientBuilder.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Hosting; + +namespace Aspire.Azure.Security.KeyVault; + +/// +/// +/// +/// +/// +/// +public class AzureKeyVaultClientBuilder( + IHostApplicationBuilder host, + string connectionName, + Action? configureSettings ) +{ + internal string DefaultConfigSectionName { get; } = AzureKeyVaultComponentConstants.s_defaultConfigSectionName; + + internal IHostApplicationBuilder HostBuilder { get; } = host; + + internal string ConnectionName { get; } = connectionName; + + internal Action? ConfigureSettings { get; } = configureSettings; +} diff --git a/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultClientBuilderCertificateExtensions.cs b/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultClientBuilderCertificateExtensions.cs new file mode 100644 index 00000000000..375e862ba21 --- /dev/null +++ b/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultClientBuilderCertificateExtensions.cs @@ -0,0 +1,57 @@ +// 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.Extensions; +using Azure.Security.KeyVault.Certificates; + +namespace Aspire.Azure.Security.KeyVault; + +/// +/// +/// +public static class AzureKeyVaultClientBuilderCertificateExtensions +{ + /// + /// + /// + /// + /// + /// + public static AzureKeyVaultClientBuilder AddCertificateClient( + this AzureKeyVaultClientBuilder builder, + Action>? configureClientBuilder = null) + { + return builder.InnerAddCertificateClient(configureClientBuilder); + } + + /// + /// + /// + /// + /// + /// + /// + public static AzureKeyVaultClientBuilder AddKeyedCertificateClient( + this AzureKeyVaultClientBuilder builder, + string name, + Action>? configureClientBuilder = null) + { + return builder.InnerAddCertificateClient(configureClientBuilder, name); + } + + /// + /// + /// + /// + /// + /// + /// + /// + private static AzureKeyVaultClientBuilder InnerAddCertificateClient( + this AzureKeyVaultClientBuilder builder, + Action>? configureClientBuilder = null, + string? serviceKey = null) + { + throw new NotImplementedException(); + } +} diff --git a/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultClientBuilderKeyExtensions.cs b/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultClientBuilderKeyExtensions.cs new file mode 100644 index 00000000000..f7e6ba5e07a --- /dev/null +++ b/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultClientBuilderKeyExtensions.cs @@ -0,0 +1,57 @@ +// 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.Extensions; +using Azure.Security.KeyVault.Keys; + +namespace Aspire.Azure.Security.KeyVault; + +/// +/// +/// +public static class AzureKeyVaultClientBuilderKeyExtensions +{ + /// + /// + /// + /// + /// + /// + public static AzureKeyVaultClientBuilder AddKeyClient( + this AzureKeyVaultClientBuilder builder, + Action>? configureClientBuilder = null) + { + return builder.InnerAddKeyClient(configureClientBuilder); + } + + /// + /// + /// + /// + /// + /// + /// + public static AzureKeyVaultClientBuilder AddKeyedKeyClient( + this AzureKeyVaultClientBuilder builder, + string name, + Action>? configureClientBuilder = null) + { + return builder.InnerAddKeyClient(configureClientBuilder, name); + } + + /// + /// + /// + /// + /// + /// + /// + /// + private static AzureKeyVaultClientBuilder InnerAddKeyClient( + this AzureKeyVaultClientBuilder builder, + Action>? configureClientBuilder = null, + string? serviceKey = null) + { + throw new NotImplementedException(); + } +} diff --git a/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultClientBuilderSecretExtensions.cs b/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultClientBuilderSecretExtensions.cs new file mode 100644 index 00000000000..7a2ab6f6ee3 --- /dev/null +++ b/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultClientBuilderSecretExtensions.cs @@ -0,0 +1,166 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Aspire.Azure.Common; +using Azure.Core; +using Azure.Core.Extensions; +using Azure.Extensions.AspNetCore.Configuration.Secrets; +using Azure.Identity; +using Azure.Security.KeyVault.Secrets; +using HealthChecks.Azure.KeyVault.Secrets; +using Microsoft.Extensions.Azure; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Diagnostics.HealthChecks; + +namespace Aspire.Azure.Security.KeyVault; + +/// +/// +/// +public static class AzureKeyVaultClientBuilderSecretExtensions +{ + /// + /// + /// + /// + /// + /// + public static AzureKeyVaultClientBuilder AddSecretClient( + this AzureKeyVaultClientBuilder builder, + Action>? configureClientBuilder = null) + { + return builder.InnerAddSecretClient(configureClientBuilder); + } + + /// + /// + /// + /// + /// + /// + /// + public static AzureKeyVaultClientBuilder AddKeyedSecretClient( + this AzureKeyVaultClientBuilder builder, + string serviceKey, + Action>? configureClientBuilder = null) + { + return builder.InnerAddSecretClient(configureClientBuilder, serviceKey); + } + + /// + /// Adds the Azure KeyVault secrets to be configuration values in the . + /// + /// The to add the secrets to. + /// A name used to retrieve the connection string from the ConnectionStrings configuration section. + /// An optional method that can be used for customizing the . It's invoked after the settings are read from the configuration. + /// An optional method that can be used for customizing the . + /// An optional instance to configure the behavior of the configuration provider. + public static void AddAzureKeyVaultSecrets( + this IConfigurationManager configurationManager, + string connectionName, + Action? configureSettings = null, + Action? configureClientOptions = null, + AzureKeyVaultConfigurationOptions? options = null) + { + ArgumentNullException.ThrowIfNull(configurationManager); + ArgumentException.ThrowIfNullOrEmpty(connectionName); + + var client = configurationManager.GetSecretClient(connectionName, configureSettings, configureClientOptions); + configurationManager.AddAzureKeyVault(client, options ?? new AzureKeyVaultConfigurationOptions()); + } + + private static SecretClient GetSecretClient( + this IConfiguration configuration, + string connectionName, + Action? configureSettings, + Action? configureOptions) + { + var configSection = configuration.GetSection(AzureKeyVaultComponentConstants.s_defaultConfigSectionName); + + var settings = new AzureSecurityKeyVaultSettings(); + configSection.Bind(settings); + + if (configuration.GetConnectionString(connectionName) is string connectionString) + { + ((IConnectionStringSettings)settings).ParseConnectionString(connectionString); + } + + configureSettings?.Invoke(settings); + + var clientOptions = new SecretClientOptions(); + configSection.GetSection("ClientOptions").Bind(clientOptions); + configureOptions?.Invoke(clientOptions); + + if (settings.VaultUri is null) + { + throw new InvalidOperationException($"VaultUri is missing. It should be provided in 'ConnectionStrings:{connectionName}' or under the 'VaultUri' key in the '{AzureKeyVaultComponentConstants.s_defaultConfigSectionName}' configuration section."); + } + + return new SecretClient(settings.VaultUri, settings.Credential ?? new DefaultAzureCredential(), clientOptions); + } + + /// + /// + /// + /// + /// + /// + /// + /// + private static AzureKeyVaultClientBuilder InnerAddSecretClient( + this AzureKeyVaultClientBuilder builder, + Action>? configureClientBuilder = null, + string? serviceKey = null) + { + new KeyVaultSecretsComponent() + .AddClient(builder.HostBuilder, builder.DefaultConfigSectionName, builder.ConfigureSettings, + configureClientBuilder, builder.ConnectionName, serviceKey); + + return builder; + } + + private sealed class KeyVaultSecretsComponent : AzureComponent + { + protected override IAzureClientBuilder AddClient( + AzureClientFactoryBuilder azureFactoryBuilder, AzureSecurityKeyVaultSettings settings, + string connectionName, string configurationSectionName) + { + return azureFactoryBuilder.AddClient((options, cred, _) => + { + if (settings.VaultUri is null) + { + throw new InvalidOperationException($"VaultUri is missing. It should be provided in 'ConnectionStrings:{connectionName}' or under the 'VaultUri' key in the '{configurationSectionName}' configuration section."); + } + + return new SecretClient(settings.VaultUri, cred, options); + }); + } + + protected override IHealthCheck CreateHealthCheck(SecretClient client, AzureSecurityKeyVaultSettings settings) + => new AzureKeyVaultSecretsHealthCheck(client, new AzureKeyVaultSecretsHealthCheckOptions()); + + protected override void BindClientOptionsToConfiguration(IAzureClientBuilder clientBuilder, IConfiguration configuration) + { +#pragma warning disable IDE0200 // Remove unnecessary lambda expression - needed so the ConfigBinder Source Generator works + clientBuilder.ConfigureOptions(options => configuration.Bind(options)); +#pragma warning restore IDE0200 + } + + protected override void BindSettingsToConfiguration(AzureSecurityKeyVaultSettings settings, IConfiguration configuration) + { + configuration.Bind(settings); + } + + protected override bool GetHealthCheckEnabled(AzureSecurityKeyVaultSettings settings) + => !settings.DisableHealthChecks; + + protected override TokenCredential? GetTokenCredential(AzureSecurityKeyVaultSettings settings) + => settings.Credential; + + protected override bool GetMetricsEnabled(AzureSecurityKeyVaultSettings settings) + => false; + + protected override bool GetTracingEnabled(AzureSecurityKeyVaultSettings settings) + => !settings.DisableTracing; + } +} diff --git a/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultComponentConstants.cs b/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultComponentConstants.cs new file mode 100644 index 00000000000..d10b2f79a14 --- /dev/null +++ b/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultComponentConstants.cs @@ -0,0 +1,9 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Aspire.Azure.Security.KeyVault; + +internal static class AzureKeyVaultComponentConstants +{ + internal static string s_defaultConfigSectionName = "Aspire:Azure:Security:KeyVault"; +} From 18632223c741fc7cfce68001f634124145a7961e Mon Sep 17 00:00:00 2001 From: James Gould Date: Thu, 27 Mar 2025 23:23:16 +0000 Subject: [PATCH 02/24] Migrated argument exceptions to keep pattern consistent --- .../AspireKeyVaultExtensions.cs | 2 -- .../AzureKeyVaultClientBuilderCertificateExtensions.cs | 8 +++++--- .../AzureKeyVaultClientBuilderKeyExtensions.cs | 8 +++++--- .../AzureKeyVaultClientBuilderSecretExtensions.cs | 2 ++ 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/Components/Aspire.Azure.Security.KeyVault/AspireKeyVaultExtensions.cs b/src/Components/Aspire.Azure.Security.KeyVault/AspireKeyVaultExtensions.cs index 00eeebf465a..b8e3ed6d273 100644 --- a/src/Components/Aspire.Azure.Security.KeyVault/AspireKeyVaultExtensions.cs +++ b/src/Components/Aspire.Azure.Security.KeyVault/AspireKeyVaultExtensions.cs @@ -32,7 +32,6 @@ public static AzureKeyVaultClientBuilder AddAzureKeyVaultClient( Action>? configureClientBuilder = null) { ArgumentNullException.ThrowIfNull(builder); - ArgumentException.ThrowIfNullOrEmpty(connectionName); return new AzureKeyVaultClientBuilder(builder, connectionName, configureSettings) .AddSecretClient(configureClientBuilder); @@ -56,7 +55,6 @@ public static AzureKeyVaultClientBuilder AddKeyedAzureKeyVaultClient( Action>? configureClientBuilder = null) { ArgumentNullException.ThrowIfNull(builder); - ArgumentException.ThrowIfNullOrEmpty(name); return new AzureKeyVaultClientBuilder(builder, name, configureSettings) .AddKeyedSecretClient(serviceKey: name, configureClientBuilder); diff --git a/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultClientBuilderCertificateExtensions.cs b/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultClientBuilderCertificateExtensions.cs index 375e862ba21..af2e30e04f3 100644 --- a/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultClientBuilderCertificateExtensions.cs +++ b/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultClientBuilderCertificateExtensions.cs @@ -28,15 +28,17 @@ public static AzureKeyVaultClientBuilder AddCertificateClient( /// /// /// - /// + /// /// /// public static AzureKeyVaultClientBuilder AddKeyedCertificateClient( this AzureKeyVaultClientBuilder builder, - string name, + string serviceKey, Action>? configureClientBuilder = null) { - return builder.InnerAddCertificateClient(configureClientBuilder, name); + ArgumentException.ThrowIfNullOrEmpty(serviceKey); + + return builder.InnerAddCertificateClient(configureClientBuilder, serviceKey); } /// diff --git a/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultClientBuilderKeyExtensions.cs b/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultClientBuilderKeyExtensions.cs index f7e6ba5e07a..bb9fbde28c6 100644 --- a/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultClientBuilderKeyExtensions.cs +++ b/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultClientBuilderKeyExtensions.cs @@ -28,15 +28,17 @@ public static AzureKeyVaultClientBuilder AddKeyClient( /// /// /// - /// + /// /// /// public static AzureKeyVaultClientBuilder AddKeyedKeyClient( this AzureKeyVaultClientBuilder builder, - string name, + string serviceKey, Action>? configureClientBuilder = null) { - return builder.InnerAddKeyClient(configureClientBuilder, name); + ArgumentException.ThrowIfNullOrEmpty(serviceKey); + + return builder.InnerAddKeyClient(configureClientBuilder, serviceKey); } /// diff --git a/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultClientBuilderSecretExtensions.cs b/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultClientBuilderSecretExtensions.cs index 7a2ab6f6ee3..865e26dad63 100644 --- a/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultClientBuilderSecretExtensions.cs +++ b/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultClientBuilderSecretExtensions.cs @@ -44,6 +44,8 @@ public static AzureKeyVaultClientBuilder AddKeyedSecretClient( string serviceKey, Action>? configureClientBuilder = null) { + ArgumentException.ThrowIfNullOrEmpty(serviceKey); + return builder.InnerAddSecretClient(configureClientBuilder, serviceKey); } From 57865d0fb3e6aebe665dd8d2c75062ccffee9509 Mon Sep 17 00:00:00 2001 From: James Gould Date: Fri, 28 Mar 2025 11:56:19 +0000 Subject: [PATCH 03/24] Added base implementation of healthchecks to component library, feels misplaced. --- ...VaultClientBuilderCertificateExtensions.cs | 45 +++++++++++++++++++ .../AzureKeyVaultCertificatesHealthCheck.cs | 18 ++++++++ .../AzureKeyVaultKeysHealthCheck.cs | 18 ++++++++ 3 files changed, 81 insertions(+) create mode 100644 src/Components/Aspire.Azure.Security.KeyVault/HealthChecks/AzureKeyVaultCertificatesHealthCheck.cs create mode 100644 src/Components/Aspire.Azure.Security.KeyVault/HealthChecks/AzureKeyVaultKeysHealthCheck.cs diff --git a/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultClientBuilderCertificateExtensions.cs b/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultClientBuilderCertificateExtensions.cs index af2e30e04f3..600ab50b51f 100644 --- a/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultClientBuilderCertificateExtensions.cs +++ b/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultClientBuilderCertificateExtensions.cs @@ -1,8 +1,14 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Aspire.Azure.Common; +using Aspire.Azure.Security.KeyVault.HealthChecks; +using Azure.Core; using Azure.Core.Extensions; using Azure.Security.KeyVault.Certificates; +using Microsoft.Extensions.Azure; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Diagnostics.HealthChecks; namespace Aspire.Azure.Security.KeyVault; @@ -56,4 +62,43 @@ private static AzureKeyVaultClientBuilder InnerAddCertificateClient( { throw new NotImplementedException(); } + + private class KeyVaultCertificateComponent : AzureComponent + { + protected override IAzureClientBuilder AddClient(AzureClientFactoryBuilder azureFactoryBuilder, AzureSecurityKeyVaultSettings settings, string connectionName, string configurationSectionName) + { + throw new NotImplementedException(); + } + + protected override void BindClientOptionsToConfiguration(IAzureClientBuilder clientBuilder, IConfiguration configuration) +#pragma warning disable IDE0200 // Remove unnecessary lambda expression - needed so the ConfigBinder Source Generator works + => clientBuilder.ConfigureOptions(options => configuration.Bind(options)); +#pragma warning restore IDE0200 + + protected override void BindSettingsToConfiguration(AzureSecurityKeyVaultSettings settings, IConfiguration configuration) + => configuration.Bind(settings); + + protected override IHealthCheck CreateHealthCheck(CertificateClient client, AzureSecurityKeyVaultSettings settings) + => new AzureKeyVaultCertificatesHealthCheck(client, settings); + + protected override bool GetHealthCheckEnabled(AzureSecurityKeyVaultSettings settings) + { + throw new NotImplementedException(); + } + + protected override bool GetMetricsEnabled(AzureSecurityKeyVaultSettings settings) + { + throw new NotImplementedException(); + } + + protected override TokenCredential? GetTokenCredential(AzureSecurityKeyVaultSettings settings) + { + throw new NotImplementedException(); + } + + protected override bool GetTracingEnabled(AzureSecurityKeyVaultSettings settings) + { + throw new NotImplementedException(); + } + } } diff --git a/src/Components/Aspire.Azure.Security.KeyVault/HealthChecks/AzureKeyVaultCertificatesHealthCheck.cs b/src/Components/Aspire.Azure.Security.KeyVault/HealthChecks/AzureKeyVaultCertificatesHealthCheck.cs new file mode 100644 index 00000000000..8e8f0fbe39f --- /dev/null +++ b/src/Components/Aspire.Azure.Security.KeyVault/HealthChecks/AzureKeyVaultCertificatesHealthCheck.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Azure.Security.KeyVault.Certificates; +using Microsoft.Extensions.Diagnostics.HealthChecks; + +namespace Aspire.Azure.Security.KeyVault.HealthChecks; + +internal class AzureKeyVaultCertificatesHealthCheck(CertificateClient client, AzureSecurityKeyVaultSettings settings) : IHealthCheck +{ + internal CertificateClient CertificateClient => client; + internal AzureSecurityKeyVaultSettings Settings => settings; + + public Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } +} diff --git a/src/Components/Aspire.Azure.Security.KeyVault/HealthChecks/AzureKeyVaultKeysHealthCheck.cs b/src/Components/Aspire.Azure.Security.KeyVault/HealthChecks/AzureKeyVaultKeysHealthCheck.cs new file mode 100644 index 00000000000..d486a0e3957 --- /dev/null +++ b/src/Components/Aspire.Azure.Security.KeyVault/HealthChecks/AzureKeyVaultKeysHealthCheck.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Azure.Security.KeyVault.Keys; +using Microsoft.Extensions.Diagnostics.HealthChecks; + +namespace Aspire.Azure.Security.KeyVault.HealthChecks; + +internal class AzureKeyVaultKeysHealthCheck(KeyClient client, AzureSecurityKeyVaultSettings settings) : IHealthCheck +{ + internal KeyClient KeyClient => client; + internal AzureSecurityKeyVaultSettings Settings => settings; + + public Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } +} From d4f760cc6515f1b2e599a032eb034b5d4d8e1046 Mon Sep 17 00:00:00 2001 From: James Gould Date: Fri, 28 Mar 2025 12:30:17 +0000 Subject: [PATCH 04/24] Created an AbstractKeyVaultComponent to enhance reusability/testing --- .../AbstractKeyVaultComponent.cs | 61 +++++++++++++++++++ ...reKeyVaultClientBuilderSecretExtensions.cs | 43 +------------ 2 files changed, 64 insertions(+), 40 deletions(-) create mode 100644 src/Components/Aspire.Azure.Security.KeyVault/AbstractKeyVaultComponent.cs diff --git a/src/Components/Aspire.Azure.Security.KeyVault/AbstractKeyVaultComponent.cs b/src/Components/Aspire.Azure.Security.KeyVault/AbstractKeyVaultComponent.cs new file mode 100644 index 00000000000..bf52540d96b --- /dev/null +++ b/src/Components/Aspire.Azure.Security.KeyVault/AbstractKeyVaultComponent.cs @@ -0,0 +1,61 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using Aspire.Azure.Common; +using Azure.Core; +using Azure.Core.Extensions; +using Microsoft.Extensions.Azure; +using Microsoft.Extensions.Configuration; + +namespace Aspire.Azure.Security.KeyVault; + +/// +/// Abstracts the common configuration binding required by +/// Deriving type implements KeyVaultClient specific item: +/// +/// +/// The KeyVaultClient type for this component. +/// The associated configuration for the +internal abstract class AbstractKeyVaultComponent + : AzureComponent + where TClient : class + where TOptions : class +{ + internal abstract TClient CreateComponentClient(Uri vaultUri, TOptions options, TokenCredential cred); + + protected override IAzureClientBuilder AddClient(AzureClientFactoryBuilder azureFactoryBuilder, AzureSecurityKeyVaultSettings settings, string connectionName, string configurationSectionName) + { + return azureFactoryBuilder.AddClient((options, cred, _) => + { + if (settings.VaultUri is null) + { + throw new InvalidOperationException($"VaultUri is missing. It should be provided in 'ConnectionStrings:{connectionName}' or under the 'VaultUri' key in the '{configurationSectionName}' configuration section."); + } + + return CreateComponentClient(settings.VaultUri, options, cred); + }); + } + + protected override void BindClientOptionsToConfiguration(IAzureClientBuilder clientBuilder, IConfiguration configuration) +#pragma warning disable IDE0200 // Remove unnecessary lambda expression - needed so the ConfigBinder Source Generator works + => clientBuilder.ConfigureOptions(options => configuration.Bind(options)); +#pragma warning restore IDE0200 + + protected override void BindSettingsToConfiguration(AzureSecurityKeyVaultSettings settings, IConfiguration configuration) + { + configuration.Bind(settings); + } + + protected override bool GetHealthCheckEnabled(AzureSecurityKeyVaultSettings settings) + => !settings.DisableHealthChecks; + + protected override TokenCredential? GetTokenCredential(AzureSecurityKeyVaultSettings settings) + => settings.Credential; + + protected override bool GetMetricsEnabled(AzureSecurityKeyVaultSettings settings) + => false; + + protected override bool GetTracingEnabled(AzureSecurityKeyVaultSettings settings) + => !settings.DisableTracing; +} diff --git a/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultClientBuilderSecretExtensions.cs b/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultClientBuilderSecretExtensions.cs index 865e26dad63..4800dcf3a3e 100644 --- a/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultClientBuilderSecretExtensions.cs +++ b/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultClientBuilderSecretExtensions.cs @@ -8,7 +8,6 @@ using Azure.Identity; using Azure.Security.KeyVault.Secrets; using HealthChecks.Azure.KeyVault.Secrets; -using Microsoft.Extensions.Azure; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Diagnostics.HealthChecks; @@ -121,48 +120,12 @@ private static AzureKeyVaultClientBuilder InnerAddSecretClient( return builder; } - private sealed class KeyVaultSecretsComponent : AzureComponent + private sealed class KeyVaultSecretsComponent : AbstractKeyVaultComponent { - protected override IAzureClientBuilder AddClient( - AzureClientFactoryBuilder azureFactoryBuilder, AzureSecurityKeyVaultSettings settings, - string connectionName, string configurationSectionName) - { - return azureFactoryBuilder.AddClient((options, cred, _) => - { - if (settings.VaultUri is null) - { - throw new InvalidOperationException($"VaultUri is missing. It should be provided in 'ConnectionStrings:{connectionName}' or under the 'VaultUri' key in the '{configurationSectionName}' configuration section."); - } - - return new SecretClient(settings.VaultUri, cred, options); - }); - } - protected override IHealthCheck CreateHealthCheck(SecretClient client, AzureSecurityKeyVaultSettings settings) => new AzureKeyVaultSecretsHealthCheck(client, new AzureKeyVaultSecretsHealthCheckOptions()); - protected override void BindClientOptionsToConfiguration(IAzureClientBuilder clientBuilder, IConfiguration configuration) - { -#pragma warning disable IDE0200 // Remove unnecessary lambda expression - needed so the ConfigBinder Source Generator works - clientBuilder.ConfigureOptions(options => configuration.Bind(options)); -#pragma warning restore IDE0200 - } - - protected override void BindSettingsToConfiguration(AzureSecurityKeyVaultSettings settings, IConfiguration configuration) - { - configuration.Bind(settings); - } - - protected override bool GetHealthCheckEnabled(AzureSecurityKeyVaultSettings settings) - => !settings.DisableHealthChecks; - - protected override TokenCredential? GetTokenCredential(AzureSecurityKeyVaultSettings settings) - => settings.Credential; - - protected override bool GetMetricsEnabled(AzureSecurityKeyVaultSettings settings) - => false; - - protected override bool GetTracingEnabled(AzureSecurityKeyVaultSettings settings) - => !settings.DisableTracing; + internal override SecretClient CreateComponentClient(Uri vaultUri, SecretClientOptions options, TokenCredential cred) + => new SecretClient(vaultUri, cred, options); } } From 5b1206ad2df6a3c3dc133aa1df93b1fc28d48519 Mon Sep 17 00:00:00 2001 From: James Gould Date: Fri, 28 Mar 2025 13:47:48 +0000 Subject: [PATCH 05/24] SecretClientExtensions documentation added --- ...reKeyVaultClientBuilderSecretExtensions.cs | 58 ++++++++++--------- 1 file changed, 31 insertions(+), 27 deletions(-) diff --git a/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultClientBuilderSecretExtensions.cs b/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultClientBuilderSecretExtensions.cs index 4800dcf3a3e..9827f532e6b 100644 --- a/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultClientBuilderSecretExtensions.cs +++ b/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultClientBuilderSecretExtensions.cs @@ -19,11 +19,11 @@ namespace Aspire.Azure.Security.KeyVault; public static class AzureKeyVaultClientBuilderSecretExtensions { /// - /// + /// Registers a as a singleton into the services provided by the . /// - /// - /// - /// + /// Used to register AzureKeyVault clients. + /// Optional configuration for the . + /// A to configure further clients. public static AzureKeyVaultClientBuilder AddSecretClient( this AzureKeyVaultClientBuilder builder, Action>? configureClientBuilder = null) @@ -32,12 +32,13 @@ public static AzureKeyVaultClientBuilder AddSecretClient( } /// - /// + /// Registers a keyed as a singleton into the services provided by the . /// - /// - /// - /// - /// + /// Used to register AzureKeyVault clients. + /// The name to call the singleton service. + /// Optional configuration for the . + /// A to configure further clients. + /// Thrown if mandatory is null or empty. public static AzureKeyVaultClientBuilder AddKeyedSecretClient( this AzureKeyVaultClientBuilder builder, string serviceKey, @@ -48,6 +49,26 @@ public static AzureKeyVaultClientBuilder AddKeyedSecretClient( return builder.InnerAddSecretClient(configureClientBuilder, serviceKey); } + /// + /// Implements the creation of a as an + /// + /// Used to register AzureKeyVault clients. + /// The name to call the singleton service. + /// Optional configuration for the . + /// A to configure further clients. + /// + private static AzureKeyVaultClientBuilder InnerAddSecretClient( + this AzureKeyVaultClientBuilder builder, + Action>? configureClientBuilder = null, + string? serviceKey = null) + { + new KeyVaultSecretsComponent() + .AddClient(builder.HostBuilder, builder.DefaultConfigSectionName, builder.ConfigureSettings, + configureClientBuilder, builder.ConnectionName, serviceKey); + + return builder; + } + /// /// Adds the Azure KeyVault secrets to be configuration values in the . /// @@ -101,25 +122,8 @@ private static SecretClient GetSecretClient( } /// - /// + /// Representation of an , configured as a /// - /// - /// - /// - /// - /// - private static AzureKeyVaultClientBuilder InnerAddSecretClient( - this AzureKeyVaultClientBuilder builder, - Action>? configureClientBuilder = null, - string? serviceKey = null) - { - new KeyVaultSecretsComponent() - .AddClient(builder.HostBuilder, builder.DefaultConfigSectionName, builder.ConfigureSettings, - configureClientBuilder, builder.ConnectionName, serviceKey); - - return builder; - } - private sealed class KeyVaultSecretsComponent : AbstractKeyVaultComponent { protected override IHealthCheck CreateHealthCheck(SecretClient client, AzureSecurityKeyVaultSettings settings) From a3cd2a182eee7933703354ded4124d568014b56a Mon Sep 17 00:00:00 2001 From: James Gould Date: Fri, 28 Mar 2025 15:41:32 +0000 Subject: [PATCH 06/24] Full implementation and documentation completed for all 3 Client types --- .../AbstractKeyVaultComponent.cs | 12 +-- .../Aspire.Azure.Security.KeyVault.csproj | 2 +- .../AspireKeyVaultExtensions.cs | 3 +- .../AzureKeyVaultClientBuilder.cs | 26 ++++-- ...VaultClientBuilderCertificateExtensions.cs | 85 ++++++++----------- ...AzureKeyVaultClientBuilderKeyExtensions.cs | 69 +++++++++++---- ...reKeyVaultClientBuilderSecretExtensions.cs | 17 ++-- .../AzureKeyVaultComponentConstants.cs | 2 +- .../AzureKeyVaultCertificatesHealthCheck.cs | 52 ++++++++++-- ...AzureKeyVaultExtendedHealthCheckOptions.cs | 38 +++++++++ .../AzureKeyVaultKeysHealthCheck.cs | 50 +++++++++-- 11 files changed, 254 insertions(+), 102 deletions(-) create mode 100644 src/Components/Aspire.Azure.Security.KeyVault/HealthChecks/AzureKeyVaultExtendedHealthCheckOptions.cs diff --git a/src/Components/Aspire.Azure.Security.KeyVault/AbstractKeyVaultComponent.cs b/src/Components/Aspire.Azure.Security.KeyVault/AbstractKeyVaultComponent.cs index bf52540d96b..3dd3ef50125 100644 --- a/src/Components/Aspire.Azure.Security.KeyVault/AbstractKeyVaultComponent.cs +++ b/src/Components/Aspire.Azure.Security.KeyVault/AbstractKeyVaultComponent.cs @@ -6,7 +6,7 @@ using Azure.Core; using Azure.Core.Extensions; using Microsoft.Extensions.Azure; -using Microsoft.Extensions.Configuration; +//using Microsoft.Extensions.Configuration; namespace Aspire.Azure.Security.KeyVault; @@ -37,16 +37,6 @@ protected override IAzureClientBuilder AddClient(AzureClientF }); } - protected override void BindClientOptionsToConfiguration(IAzureClientBuilder clientBuilder, IConfiguration configuration) -#pragma warning disable IDE0200 // Remove unnecessary lambda expression - needed so the ConfigBinder Source Generator works - => clientBuilder.ConfigureOptions(options => configuration.Bind(options)); -#pragma warning restore IDE0200 - - protected override void BindSettingsToConfiguration(AzureSecurityKeyVaultSettings settings, IConfiguration configuration) - { - configuration.Bind(settings); - } - protected override bool GetHealthCheckEnabled(AzureSecurityKeyVaultSettings settings) => !settings.DisableHealthChecks; diff --git a/src/Components/Aspire.Azure.Security.KeyVault/Aspire.Azure.Security.KeyVault.csproj b/src/Components/Aspire.Azure.Security.KeyVault/Aspire.Azure.Security.KeyVault.csproj index 5f9ec339ce2..b8847101beb 100644 --- a/src/Components/Aspire.Azure.Security.KeyVault/Aspire.Azure.Security.KeyVault.csproj +++ b/src/Components/Aspire.Azure.Security.KeyVault/Aspire.Azure.Security.KeyVault.csproj @@ -6,7 +6,7 @@ $(ComponentAzurePackageTags) keyvault secrets A client for Azure Key Vault that integrates with Aspire, including health checks, logging and telemetry. $(SharedDir)AzureKeyVault_256x.png - $(NoWarn);SYSLIB1100;SYSLIB1101 + $(NoWarn);SYSLIB1100;SYSLIB1101;IDE0200 diff --git a/src/Components/Aspire.Azure.Security.KeyVault/AspireKeyVaultExtensions.cs b/src/Components/Aspire.Azure.Security.KeyVault/AspireKeyVaultExtensions.cs index b8e3ed6d273..184696fdcdf 100644 --- a/src/Components/Aspire.Azure.Security.KeyVault/AspireKeyVaultExtensions.cs +++ b/src/Components/Aspire.Azure.Security.KeyVault/AspireKeyVaultExtensions.cs @@ -4,7 +4,6 @@ using Aspire.Azure.Security.KeyVault; using Azure.Core.Extensions; using Azure.Security.KeyVault.Secrets; -using Microsoft.Extensions.Azure; using Microsoft.Extensions.DependencyInjection; namespace Microsoft.Extensions.Hosting; @@ -32,6 +31,7 @@ public static AzureKeyVaultClientBuilder AddAzureKeyVaultClient( Action>? configureClientBuilder = null) { ArgumentNullException.ThrowIfNull(builder); + ArgumentException.ThrowIfNullOrEmpty(connectionName); return new AzureKeyVaultClientBuilder(builder, connectionName, configureSettings) .AddSecretClient(configureClientBuilder); @@ -55,6 +55,7 @@ public static AzureKeyVaultClientBuilder AddKeyedAzureKeyVaultClient( Action>? configureClientBuilder = null) { ArgumentNullException.ThrowIfNull(builder); + ArgumentException.ThrowIfNullOrEmpty(name); return new AzureKeyVaultClientBuilder(builder, name, configureSettings) .AddKeyedSecretClient(serviceKey: name, configureClientBuilder); diff --git a/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultClientBuilder.cs b/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultClientBuilder.cs index 55ce991f642..35ca4ea0fd6 100644 --- a/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultClientBuilder.cs +++ b/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultClientBuilder.cs @@ -1,26 +1,38 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Microsoft.Extensions.Hosting; +using Aspire.Azure.Security.KeyVault; -namespace Aspire.Azure.Security.KeyVault; +namespace Microsoft.Extensions.Hosting; /// -/// +/// A builder used for creating one or more Key Vault Clients, registered into the . /// -/// -/// -/// +/// The to register the clients to as singletons. +/// The name used to retrieve the VaultUri from ConnectionStrings in the configuration provider. +/// An optional configuration point for the overall applied to each Key Vault Client. public class AzureKeyVaultClientBuilder( IHostApplicationBuilder host, string connectionName, - Action? configureSettings ) + Action? configureSettings) { + /// + /// The default name of the configuration section for Key Vault. + /// internal string DefaultConfigSectionName { get; } = AzureKeyVaultComponentConstants.s_defaultConfigSectionName; + /// + /// The to register Key Vault Clients into as singletons. + /// internal IHostApplicationBuilder HostBuilder { get; } = host; + /// + /// @The name used to retrieve the VaultUri from ConnectionStrings in the configuration provider. + /// internal string ConnectionName { get; } = connectionName; + /// + /// An optional configuration point for the overall applied to each Key Vault Client. + /// internal Action? ConfigureSettings { get; } = configureSettings; } diff --git a/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultClientBuilderCertificateExtensions.cs b/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultClientBuilderCertificateExtensions.cs index 600ab50b51f..476d60beb9e 100644 --- a/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultClientBuilderCertificateExtensions.cs +++ b/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultClientBuilderCertificateExtensions.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using Aspire.Azure.Common; +using Aspire.Azure.Security.KeyVault; using Aspire.Azure.Security.KeyVault.HealthChecks; using Azure.Core; using Azure.Core.Extensions; @@ -10,95 +11,83 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Diagnostics.HealthChecks; -namespace Aspire.Azure.Security.KeyVault; +namespace Microsoft.Extensions.Hosting; /// -/// +/// Extends the for optionally registering a . /// public static class AzureKeyVaultClientBuilderCertificateExtensions { /// - /// + /// Registers a as a singleton into the services provided by the /// - /// - /// - /// + /// The being used to register Key Vault Clients. + /// Optional configuration for the . + /// A to configure further clients. + /// Thrown if the mandatory is null. public static AzureKeyVaultClientBuilder AddCertificateClient( this AzureKeyVaultClientBuilder builder, Action>? configureClientBuilder = null) { + ArgumentNullException.ThrowIfNull(builder); + return builder.InnerAddCertificateClient(configureClientBuilder); } /// - /// + /// Registers a keyed as a singleton into the services provided by the /// - /// - /// - /// - /// + /// The being used to register Key Vault Clients. + /// The name to call the singleton service. + /// Optional configuration for the + /// A to configure further clients. + /// Thrown if mandatory is null. + /// Thrown if the mandatory is null or empty. public static AzureKeyVaultClientBuilder AddKeyedCertificateClient( this AzureKeyVaultClientBuilder builder, string serviceKey, Action>? configureClientBuilder = null) { + ArgumentNullException.ThrowIfNull(builder); ArgumentException.ThrowIfNullOrEmpty(serviceKey); return builder.InnerAddCertificateClient(configureClientBuilder, serviceKey); } /// - /// + /// Implements the creation of a as an /// - /// - /// - /// - /// - /// + /// Used to register AzureKeyVault clients. + /// The name to call the singleton service. + /// Optional configuration for the . + /// A to configure further clients. private static AzureKeyVaultClientBuilder InnerAddCertificateClient( this AzureKeyVaultClientBuilder builder, Action>? configureClientBuilder = null, string? serviceKey = null) { - throw new NotImplementedException(); + new KeyVaultCertificateComponent() + .AddClient(builder.HostBuilder, builder.DefaultConfigSectionName, builder.ConfigureSettings, + configureClientBuilder, builder.ConnectionName, serviceKey); + + return builder; } - private class KeyVaultCertificateComponent : AzureComponent + /// + /// Representation of an configured as a + /// + private sealed class KeyVaultCertificateComponent : AbstractKeyVaultComponent { - protected override IAzureClientBuilder AddClient(AzureClientFactoryBuilder azureFactoryBuilder, AzureSecurityKeyVaultSettings settings, string connectionName, string configurationSectionName) - { - throw new NotImplementedException(); - } + internal override CertificateClient CreateComponentClient(Uri vaultUri, CertificateClientOptions options, TokenCredential cred) + => new(vaultUri, cred, options); + + protected override IHealthCheck CreateHealthCheck(CertificateClient client, AzureSecurityKeyVaultSettings settings) + => new AzureKeyVaultCertificatesHealthCheck(client, new AzureKeyVaultCertificatesHealthCheckOptions()); protected override void BindClientOptionsToConfiguration(IAzureClientBuilder clientBuilder, IConfiguration configuration) -#pragma warning disable IDE0200 // Remove unnecessary lambda expression - needed so the ConfigBinder Source Generator works => clientBuilder.ConfigureOptions(options => configuration.Bind(options)); -#pragma warning restore IDE0200 protected override void BindSettingsToConfiguration(AzureSecurityKeyVaultSettings settings, IConfiguration configuration) => configuration.Bind(settings); - - protected override IHealthCheck CreateHealthCheck(CertificateClient client, AzureSecurityKeyVaultSettings settings) - => new AzureKeyVaultCertificatesHealthCheck(client, settings); - - protected override bool GetHealthCheckEnabled(AzureSecurityKeyVaultSettings settings) - { - throw new NotImplementedException(); - } - - protected override bool GetMetricsEnabled(AzureSecurityKeyVaultSettings settings) - { - throw new NotImplementedException(); - } - - protected override TokenCredential? GetTokenCredential(AzureSecurityKeyVaultSettings settings) - { - throw new NotImplementedException(); - } - - protected override bool GetTracingEnabled(AzureSecurityKeyVaultSettings settings) - { - throw new NotImplementedException(); - } } } diff --git a/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultClientBuilderKeyExtensions.cs b/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultClientBuilderKeyExtensions.cs index bb9fbde28c6..97dce4a7cac 100644 --- a/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultClientBuilderKeyExtensions.cs +++ b/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultClientBuilderKeyExtensions.cs @@ -1,59 +1,94 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Aspire.Azure.Common; +using Aspire.Azure.Security.KeyVault; +using Aspire.Azure.Security.KeyVault.HealthChecks; +using Azure.Core; using Azure.Core.Extensions; using Azure.Security.KeyVault.Keys; +using Microsoft.Extensions.Azure; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Diagnostics.HealthChecks; -namespace Aspire.Azure.Security.KeyVault; +namespace Microsoft.Extensions.Hosting; /// -/// +/// Extends the for optionally registering a . /// public static class AzureKeyVaultClientBuilderKeyExtensions { /// - /// + /// Registers a as a singleton into the services provided by the . /// - /// - /// - /// + /// The being used to register Key Vault Clients. + /// Optional configuration for the . + /// A to configure further clients. + /// Thrown if the mandatory is null. public static AzureKeyVaultClientBuilder AddKeyClient( this AzureKeyVaultClientBuilder builder, Action>? configureClientBuilder = null) { + ArgumentNullException.ThrowIfNull(builder); + return builder.InnerAddKeyClient(configureClientBuilder); } /// - /// + /// Registers a keyed as a singleton into the services provided by the . /// - /// - /// - /// + /// The being used to register Key Vault Clients. + /// The name to call the singleton service. + /// Optional configuration for the + /// A to configure further clients. + /// Thrown if mandatory is null. + /// Thrown if the mandatory is null or empty. /// public static AzureKeyVaultClientBuilder AddKeyedKeyClient( this AzureKeyVaultClientBuilder builder, string serviceKey, Action>? configureClientBuilder = null) { + ArgumentNullException.ThrowIfNull(builder); ArgumentException.ThrowIfNullOrEmpty(serviceKey); return builder.InnerAddKeyClient(configureClientBuilder, serviceKey); } /// - /// + /// Implements the creation of a as an /// - /// - /// - /// - /// - /// + /// Used to register AzureKeyVault clients. + /// The name to call the singleton service. + /// Optional configuration for the . + /// A to configure further clients. private static AzureKeyVaultClientBuilder InnerAddKeyClient( this AzureKeyVaultClientBuilder builder, Action>? configureClientBuilder = null, string? serviceKey = null) { - throw new NotImplementedException(); + new KeyVaultKeyComponent() + .AddClient(builder.HostBuilder, builder.DefaultConfigSectionName, builder.ConfigureSettings, + configureClientBuilder, builder.ConnectionName, serviceKey); + + return builder; + } + + /// + /// Representation of an configured as a + /// + private sealed class KeyVaultKeyComponent : AbstractKeyVaultComponent + { + protected override IHealthCheck CreateHealthCheck(KeyClient client, AzureSecurityKeyVaultSettings settings) + => new AzureKeyVaultKeysHealthCheck(client, new AzureKeyVaultKeysHealthCheckOptions()); + + internal override KeyClient CreateComponentClient(Uri vaultUri, KeyClientOptions options, TokenCredential cred) + => new(vaultUri, cred, options); + + protected override void BindClientOptionsToConfiguration(IAzureClientBuilder clientBuilder, IConfiguration configuration) + => clientBuilder.ConfigureOptions(options => configuration.Bind(options)); + + protected override void BindSettingsToConfiguration(AzureSecurityKeyVaultSettings settings, IConfiguration configuration) + => configuration.Bind(settings); } } diff --git a/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultClientBuilderSecretExtensions.cs b/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultClientBuilderSecretExtensions.cs index 9827f532e6b..a76feb9613b 100644 --- a/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultClientBuilderSecretExtensions.cs +++ b/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultClientBuilderSecretExtensions.cs @@ -2,16 +2,18 @@ // The .NET Foundation licenses this file to you under the MIT license. using Aspire.Azure.Common; +using Aspire.Azure.Security.KeyVault; using Azure.Core; using Azure.Core.Extensions; using Azure.Extensions.AspNetCore.Configuration.Secrets; using Azure.Identity; using Azure.Security.KeyVault.Secrets; using HealthChecks.Azure.KeyVault.Secrets; +using Microsoft.Extensions.Azure; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Diagnostics.HealthChecks; -namespace Aspire.Azure.Security.KeyVault; +namespace Microsoft.Extensions.Hosting; /// /// @@ -44,8 +46,6 @@ public static AzureKeyVaultClientBuilder AddKeyedSecretClient( string serviceKey, Action>? configureClientBuilder = null) { - ArgumentException.ThrowIfNullOrEmpty(serviceKey); - return builder.InnerAddSecretClient(configureClientBuilder, serviceKey); } @@ -56,7 +56,6 @@ public static AzureKeyVaultClientBuilder AddKeyedSecretClient( /// The name to call the singleton service. /// Optional configuration for the . /// A to configure further clients. - /// private static AzureKeyVaultClientBuilder InnerAddSecretClient( this AzureKeyVaultClientBuilder builder, Action>? configureClientBuilder = null, @@ -122,14 +121,20 @@ private static SecretClient GetSecretClient( } /// - /// Representation of an , configured as a + /// Representation of an configured as a /// private sealed class KeyVaultSecretsComponent : AbstractKeyVaultComponent { + protected override void BindClientOptionsToConfiguration(IAzureClientBuilder clientBuilder, IConfiguration configuration) + => clientBuilder.ConfigureOptions(options => configuration.Bind(options)); + + protected override void BindSettingsToConfiguration(AzureSecurityKeyVaultSettings settings, IConfiguration configuration) + => configuration.Bind(settings); + protected override IHealthCheck CreateHealthCheck(SecretClient client, AzureSecurityKeyVaultSettings settings) => new AzureKeyVaultSecretsHealthCheck(client, new AzureKeyVaultSecretsHealthCheckOptions()); internal override SecretClient CreateComponentClient(Uri vaultUri, SecretClientOptions options, TokenCredential cred) - => new SecretClient(vaultUri, cred, options); + => new(vaultUri, cred, options); } } diff --git a/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultComponentConstants.cs b/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultComponentConstants.cs index d10b2f79a14..8863ebe6a4b 100644 --- a/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultComponentConstants.cs +++ b/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultComponentConstants.cs @@ -1,7 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Aspire.Azure.Security.KeyVault; +namespace Microsoft.Extensions.Hosting; internal static class AzureKeyVaultComponentConstants { diff --git a/src/Components/Aspire.Azure.Security.KeyVault/HealthChecks/AzureKeyVaultCertificatesHealthCheck.cs b/src/Components/Aspire.Azure.Security.KeyVault/HealthChecks/AzureKeyVaultCertificatesHealthCheck.cs index 8e8f0fbe39f..e920c8960aa 100644 --- a/src/Components/Aspire.Azure.Security.KeyVault/HealthChecks/AzureKeyVaultCertificatesHealthCheck.cs +++ b/src/Components/Aspire.Azure.Security.KeyVault/HealthChecks/AzureKeyVaultCertificatesHealthCheck.cs @@ -1,18 +1,60 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Azure; using Azure.Security.KeyVault.Certificates; using Microsoft.Extensions.Diagnostics.HealthChecks; namespace Aspire.Azure.Security.KeyVault.HealthChecks; -internal class AzureKeyVaultCertificatesHealthCheck(CertificateClient client, AzureSecurityKeyVaultSettings settings) : IHealthCheck +/// +/// Creates a basic health check targeting an Azure Key Vault +/// +/// The configured to use for the health check. +/// +internal sealed class AzureKeyVaultCertificatesHealthCheck(CertificateClient client, AzureKeyVaultCertificatesHealthCheckOptions options) : IHealthCheck { - internal CertificateClient CertificateClient => client; - internal AzureSecurityKeyVaultSettings Settings => settings; + internal CertificateClient Client => client; + internal AzureKeyVaultCertificatesHealthCheckOptions Options => options; - public Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) + /// + /// Executes a health check using the options provided via . + /// + /// The context in which to perform the health check. + /// The token to cancel the . + /// A representing the status of the connection. + public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) { - throw new NotImplementedException(); + var certificateName = Options.ItemName; + + try + { + await Client.GetCertificateAsync(certificateName, cancellationToken).ConfigureAwait(false); + + return new HealthCheckResult(HealthStatus.Healthy); + } + catch (RequestFailedException azureEx) when (azureEx.Status == 404) // based on https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/core/Azure.Core/README.md#reporting-errors-requestfailedexception + { + // Retaining structure to mimic -> AspNetCore.HealthChecks.Azure.KeyVault.Secrets + if (Options.CreateWhenNotFound) + { + throw new NotImplementedException(); + } + + return new HealthCheckResult(HealthStatus.Healthy); + } + catch (Exception ex) + { + return new HealthCheckResult(context.Registration.FailureStatus, exception: ex); + } } } + +internal sealed class AzureKeyVaultCertificatesHealthCheckOptions + : AzureKeyVaultExtendedHealthCheckOptions +{ + /// + /// CreateCertificate{Async} starts a long running process, inappropriate for a Health Check. + /// + public AzureKeyVaultCertificatesHealthCheckOptions() => CreateWhenNotFound = false; +} diff --git a/src/Components/Aspire.Azure.Security.KeyVault/HealthChecks/AzureKeyVaultExtendedHealthCheckOptions.cs b/src/Components/Aspire.Azure.Security.KeyVault/HealthChecks/AzureKeyVaultExtendedHealthCheckOptions.cs new file mode 100644 index 00000000000..5b9ec8f7f84 --- /dev/null +++ b/src/Components/Aspire.Azure.Security.KeyVault/HealthChecks/AzureKeyVaultExtendedHealthCheckOptions.cs @@ -0,0 +1,38 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Azure; +using Microsoft.Extensions.Diagnostics.HealthChecks; + +namespace Aspire.Azure.Security.KeyVault.HealthChecks; + +/// +/// Basis for Key Vault Client options. +/// +/// +internal class AzureKeyVaultExtendedHealthCheckOptions +{ + /// + /// Default naming of the reference item used for the Health Check. + /// + private string _itemName = nameof(TKeyVaultClientHealthCheck); + + /// + /// The name of the item held in Key Vault to be used for the Health Check. + /// + public string ItemName + { + get => _itemName; + set => _itemName = value ?? throw new ArgumentNullException(nameof(value)); + } + + /// + /// A boolean value that indicates whether the secret should be created when it's not found. + /// by default. + /// + /// + /// Enabling it requires secret set permissions and can be used to improve performance + /// (secret not found is signaled via ). + /// + public bool CreateWhenNotFound { get; set; } +} diff --git a/src/Components/Aspire.Azure.Security.KeyVault/HealthChecks/AzureKeyVaultKeysHealthCheck.cs b/src/Components/Aspire.Azure.Security.KeyVault/HealthChecks/AzureKeyVaultKeysHealthCheck.cs index d486a0e3957..67f44f69abb 100644 --- a/src/Components/Aspire.Azure.Security.KeyVault/HealthChecks/AzureKeyVaultKeysHealthCheck.cs +++ b/src/Components/Aspire.Azure.Security.KeyVault/HealthChecks/AzureKeyVaultKeysHealthCheck.cs @@ -1,18 +1,58 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Azure; using Azure.Security.KeyVault.Keys; using Microsoft.Extensions.Diagnostics.HealthChecks; namespace Aspire.Azure.Security.KeyVault.HealthChecks; -internal class AzureKeyVaultKeysHealthCheck(KeyClient client, AzureSecurityKeyVaultSettings settings) : IHealthCheck +/// +/// Creates a basic health check targeting an Azure Key Vault . +/// +/// The configured to use for the health check. +/// The configuration options for the health check. +internal sealed class AzureKeyVaultKeysHealthCheck(KeyClient client, AzureKeyVaultKeysHealthCheckOptions options) : IHealthCheck { - internal KeyClient KeyClient => client; - internal AzureSecurityKeyVaultSettings Settings => settings; + internal KeyClient Client => client; + internal AzureKeyVaultKeysHealthCheckOptions Options => options; - public Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) + /// + /// Executes a health check using the options provided via . + /// + /// The context in which to perform the health check. + /// The token to cancel the . + /// A representing the status of the connection. + public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) { - throw new NotImplementedException(); + var keyName = Options.ItemName; + + try + { + await Client.GetKeyAsync(keyName, cancellationToken: cancellationToken).ConfigureAwait(false); + + return new HealthCheckResult(HealthStatus.Healthy); + } + catch (RequestFailedException azureEx) when (azureEx.Status == 404) // based on https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/core/Azure.Core/README.md#reporting-errors-requestfailedexception + { + if (Options.CreateWhenNotFound) + { + // When this call fails, the exception is caught by upper layer. + // From https://learn.microsoft.com/aspnet/core/host-and-deploy/health-checks#create-health-checks: + // "If CheckHealthAsync throws an exception during the check, a new HealthReportEntry is returned with its HealthReportEntry.Status set to the FailureStatus." + await Client.CreateKeyAsync(keyName, KeyType.Rsa, cancellationToken: cancellationToken).ConfigureAwait(false); + } + + // The secret was not found, but it's fine as all we care about is whether it's possible to connect. + return new HealthCheckResult(HealthStatus.Healthy); + } + catch (Exception ex) + { + return new HealthCheckResult(context.Registration.FailureStatus, exception: ex); + } } } + +internal sealed class AzureKeyVaultKeysHealthCheckOptions + : AzureKeyVaultExtendedHealthCheckOptions +{ } From a11ef4aab9f482ed99ce5a7d23decb13bd339db8 Mon Sep 17 00:00:00 2001 From: James Gould Date: Sat, 29 Mar 2025 17:28:23 +0000 Subject: [PATCH 07/24] Creating multiple keyed clients of different types will now respect different ConnectionStrings configuration keys --- .../AzureKeyVaultClientBuilder.cs | 5 +- ...VaultClientBuilderCertificateExtensions.cs | 3 + ...AzureKeyVaultClientBuilderKeyExtensions.cs | 3 + .../AspireKeyVaultExtensionsTests.cs | 70 +++++++++++++++++++ 4 files changed, 79 insertions(+), 2 deletions(-) diff --git a/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultClientBuilder.cs b/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultClientBuilder.cs index 35ca4ea0fd6..be66b578a96 100644 --- a/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultClientBuilder.cs +++ b/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultClientBuilder.cs @@ -27,9 +27,10 @@ public class AzureKeyVaultClientBuilder( internal IHostApplicationBuilder HostBuilder { get; } = host; /// - /// @The name used to retrieve the VaultUri from ConnectionStrings in the configuration provider. + /// The name used to retrieve the VaultUri from ConnectionStrings in the configuration provider. + /// Setting the value after the initial creation allows for keyed clients of different types to have separate ConnectionStrings configuration names. /// - internal string ConnectionName { get; } = connectionName; + internal string ConnectionName { get; set; } = connectionName; /// /// An optional configuration point for the overall applied to each Key Vault Client. diff --git a/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultClientBuilderCertificateExtensions.cs b/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultClientBuilderCertificateExtensions.cs index 476d60beb9e..6dff9630e3e 100644 --- a/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultClientBuilderCertificateExtensions.cs +++ b/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultClientBuilderCertificateExtensions.cs @@ -51,6 +51,9 @@ public static AzureKeyVaultClientBuilder AddKeyedCertificateClient( ArgumentNullException.ThrowIfNull(builder); ArgumentException.ThrowIfNullOrEmpty(serviceKey); + // Overwrite previous builder.ConnectionName to KeyedCertificateClient builder.ConnectionName + builder.ConnectionName = serviceKey; + return builder.InnerAddCertificateClient(configureClientBuilder, serviceKey); } diff --git a/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultClientBuilderKeyExtensions.cs b/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultClientBuilderKeyExtensions.cs index 97dce4a7cac..c2939c6f4eb 100644 --- a/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultClientBuilderKeyExtensions.cs +++ b/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultClientBuilderKeyExtensions.cs @@ -52,6 +52,9 @@ public static AzureKeyVaultClientBuilder AddKeyedKeyClient( ArgumentNullException.ThrowIfNull(builder); ArgumentException.ThrowIfNullOrEmpty(serviceKey); + // Overwrite previous builder.ConnectionName to KeyedKeyClient builder.ConnectionName + builder.ConnectionName = serviceKey; + return builder.InnerAddKeyClient(configureClientBuilder, serviceKey); } diff --git a/tests/Aspire.Azure.Security.KeyVault.Tests/AspireKeyVaultExtensionsTests.cs b/tests/Aspire.Azure.Security.KeyVault.Tests/AspireKeyVaultExtensionsTests.cs index e9cf7d23eb2..89569193b97 100644 --- a/tests/Aspire.Azure.Security.KeyVault.Tests/AspireKeyVaultExtensionsTests.cs +++ b/tests/Aspire.Azure.Security.KeyVault.Tests/AspireKeyVaultExtensionsTests.cs @@ -4,6 +4,8 @@ using System.Globalization; using System.Text; using Azure.Core; +using Azure.Security.KeyVault.Certificates; +using Azure.Security.KeyVault.Keys; using Azure.Security.KeyVault.Secrets; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -201,4 +203,72 @@ public void CanAddMultipleKeyedServices() Assert.Equal(new Uri("https://aspiretests2.vault.azure.net/"), client2.VaultUri); Assert.Equal(new Uri("https://aspiretests3.vault.azure.net/"), client3.VaultUri); } + + [Fact] + public void CanUseBuilderToAddMultipleClients() + { + var builder = Host.CreateEmptyApplicationBuilder(null); + + var connectionName = "keyVaultMultipleClients"; + + builder.Configuration.AddInMemoryCollection([ + new KeyValuePair($"ConnectionStrings:{connectionName}", ConformanceTests.VaultUri) + ]); + + builder + .AddAzureKeyVaultClient(connectionName) + .AddKeyClient() + .AddCertificateClient(); + + using var host = builder.Build(); + + var secretClient = host.Services.GetRequiredService(); + var keyClient = host.Services.GetRequiredService(); + var certClient = host.Services.GetRequiredService(); + + var vaultUri = new Uri(ConformanceTests.VaultUri); + + Assert.Equal(vaultUri, secretClient.VaultUri); + Assert.Equal(vaultUri, keyClient.VaultUri); + Assert.Equal(vaultUri, certClient.VaultUri); + } + + [Fact] + public void CanUseBuilderToAddMultipleKeyedClients() + { + var builder = Host.CreateEmptyApplicationBuilder(null); + + var secretClientName = "secret-client"; + var secretClientUri = "https://aspiretests1.vault.azure.net/"; + + var keyClientName = "key-client"; + var keyClientUri = "https://aspiretests2.vault.azure.net/"; + + var certClientName = "cert-client"; + var certClientUri = "https://aspiretests3.vault.azure.net/"; + + builder.Configuration.AddInMemoryCollection([ + new KeyValuePair($"ConnectionStrings:{secretClientName}", secretClientUri), + new KeyValuePair($"ConnectionStrings:{keyClientName}", keyClientUri), + new KeyValuePair($"ConnectionStrings:{certClientName}", certClientUri) + ]); + + builder + .AddKeyedAzureKeyVaultClient(secretClientName) + .AddKeyedKeyClient(keyClientName) + .AddKeyedCertificateClient(certClientName); + + using var host = builder.Build(); + + var secretClient = host.Services.GetRequiredKeyedService(secretClientName); + var keyClient = host.Services.GetRequiredKeyedService(keyClientName); + var certClient = host.Services.GetRequiredKeyedService(certClientName); + + Assert.NotEqual(secretClient.VaultUri, keyClient.VaultUri); + Assert.NotEqual(keyClient.VaultUri, certClient.VaultUri); + + Assert.Equal(secretClient.VaultUri, new Uri(secretClientUri)); + Assert.Equal(keyClient.VaultUri, new Uri(keyClientUri)); + Assert.Equal(certClient.VaultUri, new Uri(certClientUri)); + } } From fb11e580d6a2d9c45990110bf3a87cdca9eca3ba Mon Sep 17 00:00:00 2001 From: James Gould Date: Sat, 29 Mar 2025 17:31:35 +0000 Subject: [PATCH 08/24] Updated docs on AzureKeyVaultClientBuilder to provide an example of overwriting ConnectionName --- .../Aspire.Azure.Security.KeyVault/AzureKeyVaultClientBuilder.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultClientBuilder.cs b/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultClientBuilder.cs index be66b578a96..53d54803712 100644 --- a/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultClientBuilder.cs +++ b/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultClientBuilder.cs @@ -29,6 +29,7 @@ public class AzureKeyVaultClientBuilder( /// /// The name used to retrieve the VaultUri from ConnectionStrings in the configuration provider. /// Setting the value after the initial creation allows for keyed clients of different types to have separate ConnectionStrings configuration names. + /// For example: ConnectionStrings.MyKeyedSecretClient in previous builder stage will become ConnectionStrings.MyKeyedKeyClient. /// internal string ConnectionName { get; set; } = connectionName; From 70cc770a61bb026b7f85ed27c957a82aa3d94ad5 Mon Sep 17 00:00:00 2001 From: James Gould Date: Sat, 29 Mar 2025 18:07:22 +0000 Subject: [PATCH 09/24] Conformance tests now added for all clients; old "catch all" tests renamed to SecretClientConformanceTests --- .../AspireKeyVaultExtensionsTests.cs | 16 +-- .../CertificateClientConformanceTests.cs | 131 +++++++++++++++++ .../ConformanceConstants.cs | 9 ++ .../KeyClientConformanceTests.cs | 132 ++++++++++++++++++ ...sts.cs => SecretClientConformanceTests.cs} | 4 +- 5 files changed, 282 insertions(+), 10 deletions(-) create mode 100644 tests/Aspire.Azure.Security.KeyVault.Tests/CertificateClientConformanceTests.cs create mode 100644 tests/Aspire.Azure.Security.KeyVault.Tests/ConformanceConstants.cs create mode 100644 tests/Aspire.Azure.Security.KeyVault.Tests/KeyClientConformanceTests.cs rename tests/Aspire.Azure.Security.KeyVault.Tests/{ConformanceTests.cs => SecretClientConformanceTests.cs} (96%) diff --git a/tests/Aspire.Azure.Security.KeyVault.Tests/AspireKeyVaultExtensionsTests.cs b/tests/Aspire.Azure.Security.KeyVault.Tests/AspireKeyVaultExtensionsTests.cs index 89569193b97..3c7ee20effe 100644 --- a/tests/Aspire.Azure.Security.KeyVault.Tests/AspireKeyVaultExtensionsTests.cs +++ b/tests/Aspire.Azure.Security.KeyVault.Tests/AspireKeyVaultExtensionsTests.cs @@ -21,7 +21,7 @@ public class AspireKeyVaultExtensionsTests [InlineData(false)] public void VaultUriCanBeSetInCode(bool useKeyed) { - var vaultUri = new Uri(ConformanceTests.VaultUri); + var vaultUri = new Uri(ConformanceConstants.VaultUri); var builder = Host.CreateEmptyApplicationBuilder(null); builder.Configuration.AddInMemoryCollection([ @@ -54,8 +54,8 @@ public void ConnectionNameWinsOverConfigSection(bool useKeyed) var key = useKeyed ? "secrets" : null; builder.Configuration.AddInMemoryCollection([ - new KeyValuePair(ConformanceTests.CreateConfigKey("Aspire:Azure:Security:KeyVault", key, "VaultUri"), "unused"), - new KeyValuePair("ConnectionStrings:secrets", ConformanceTests.VaultUri) + new KeyValuePair("Aspire:Azure:Security:KeyVault:{key}:VaultUri", "unused"), + new KeyValuePair("ConnectionStrings:secrets", ConformanceConstants.VaultUri) ]); if (useKeyed) @@ -72,7 +72,7 @@ public void ConnectionNameWinsOverConfigSection(bool useKeyed) host.Services.GetRequiredKeyedService("secrets") : host.Services.GetRequiredService(); - Assert.Equal(new Uri(ConformanceTests.VaultUri), client.VaultUri); + Assert.Equal(new Uri(ConformanceConstants.VaultUri), client.VaultUri); } [Fact] @@ -80,7 +80,7 @@ public void AddsKeyVaultSecretsToConfig() { var builder = Host.CreateEmptyApplicationBuilder(null); builder.Configuration.AddInMemoryCollection([ - new KeyValuePair("ConnectionStrings:secrets", ConformanceTests.VaultUri) + new KeyValuePair("ConnectionStrings:secrets", ConformanceConstants.VaultUri) ]); builder.Configuration.AddAzureKeyVaultSecrets("secrets", configureClientOptions: o => @@ -179,7 +179,7 @@ public void CanAddMultipleKeyedServices() { var builder = Host.CreateEmptyApplicationBuilder(null); builder.Configuration.AddInMemoryCollection([ - new KeyValuePair("ConnectionStrings:secrets1", ConformanceTests.VaultUri), + new KeyValuePair("ConnectionStrings:secrets1", ConformanceConstants.VaultUri), new KeyValuePair("ConnectionStrings:secrets2", "https://aspiretests2.vault.azure.net/"), new KeyValuePair("ConnectionStrings:secrets3", "https://aspiretests3.vault.azure.net/") ]); @@ -212,7 +212,7 @@ public void CanUseBuilderToAddMultipleClients() var connectionName = "keyVaultMultipleClients"; builder.Configuration.AddInMemoryCollection([ - new KeyValuePair($"ConnectionStrings:{connectionName}", ConformanceTests.VaultUri) + new KeyValuePair($"ConnectionStrings:{connectionName}", ConformanceConstants.VaultUri) ]); builder @@ -226,7 +226,7 @@ public void CanUseBuilderToAddMultipleClients() var keyClient = host.Services.GetRequiredService(); var certClient = host.Services.GetRequiredService(); - var vaultUri = new Uri(ConformanceTests.VaultUri); + var vaultUri = new Uri(ConformanceConstants.VaultUri); Assert.Equal(vaultUri, secretClient.VaultUri); Assert.Equal(vaultUri, keyClient.VaultUri); diff --git a/tests/Aspire.Azure.Security.KeyVault.Tests/CertificateClientConformanceTests.cs b/tests/Aspire.Azure.Security.KeyVault.Tests/CertificateClientConformanceTests.cs new file mode 100644 index 00000000000..c79bb5b67ba --- /dev/null +++ b/tests/Aspire.Azure.Security.KeyVault.Tests/CertificateClientConformanceTests.cs @@ -0,0 +1,131 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Aspire.Components.ConformanceTests; +using Azure.Identity; +using Azure.Security.KeyVault.Certificates; +using Azure.Security.KeyVault.Keys; +using Microsoft.DotNet.RemoteExecutor; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Xunit; + +namespace Aspire.Azure.Security.KeyVault.Tests; +public class CertificateClientConformanceTests : ConformanceTests +{ + // Roles: Key Vault Certificate User (pending) + private const string VaultUri = ConformanceConstants.VaultUri; + + private static readonly Lazy s_canConnectToServer = new(GetCanConnect); + + protected override ServiceLifetime ServiceLifetime => ServiceLifetime.Singleton; + + protected override string ActivitySourceName => "Azure.Security.KeyVault.Keys.KeyClient"; + + protected override string[] RequiredLogCategories => new string[] { "Azure.Core" }; + + protected override bool SupportsKeyedRegistrations => true; + + protected override bool CanConnectToServer => s_canConnectToServer.Value; + + protected override string ValidJsonConfig => """ + { + "Aspire": { + "Azure": { + "Security": { + "KeyVault": { + "VaultUri": "http://YOUR_URI", + "DisableHealthChecks": true, + "DisableTracing": false, + "ClientOptions": { + "DisableChallengeResourceVerification": true, + "Retry": { + "Mode": "Exponential", + "Delay": "00:03" + } + } + } + } + } + } + } + """; + + protected override (string json, string error)[] InvalidJsonToErrorMessage => new[] + { + ("""{"Aspire": { "Azure": { "Security":{ "KeyVault": { "VaultUri": "YOUR_URI"}}}}}""", "Value does not match format \"uri\""), + ("""{"Aspire": { "Azure": { "Security":{ "KeyVault": { "VaultUri": "http://YOUR_URI", "DisableHealthChecks": "true"}}}}}""", "Value is \"string\" but should be \"boolean\""), + ("""{"Aspire": { "Azure": { "Security":{ "KeyVault": { "VaultUri": "http://YOUR_URI", "ClientOptions": {"Retry": {"Mode": "Fast"}}}}}}}""", "Value should match one of the values specified by the enum"), + ("""{"Aspire": { "Azure": { "Security":{ "KeyVault": { "VaultUri": "http://YOUR_URI", "ClientOptions": {"Retry": {"NetworkTimeout": "3S"}}}}}}}""", "The string value is not a match for the indicated regular expression") + }; + + protected override void PopulateConfiguration(ConfigurationManager configuration, string? key = null) + => configuration.AddInMemoryCollection(new KeyValuePair[] + { + new(CreateConfigKey("Aspire:Azure:Security:KeyVault", key, "VaultUri"), VaultUri), + new(CreateConfigKey("Aspire:Azure:Security:KeyVault", key, "ClientOptions:Retry:MaxRetries"), "0") + }); + + protected override void RegisterComponent(HostApplicationBuilder builder, Action? configure = null, string? key = null) + { + if (key is null) + { + builder.AddAzureKeyVaultClient("secrets", ConfigureCredentials) + .AddKeyedKeyClient("keys"); + } + else + { + builder.AddKeyedAzureKeyVaultClient(key, ConfigureCredentials); + } + + void ConfigureCredentials(AzureSecurityKeyVaultSettings settings) + { + if (CanConnectToServer) + { + settings.Credential = new DefaultAzureCredential(); + } + configure?.Invoke(settings); + } + } + + protected override void SetHealthCheck(AzureSecurityKeyVaultSettings options, bool enabled) + // Disable Key Vault health check tests until https://github.com/Xabaril/AspNetCore.Diagnostics.HealthChecks/issues/2279 is fixed + // => options.DisableHealthChecks = !enabled; + => throw new NotImplementedException(); + + protected override void SetMetrics(AzureSecurityKeyVaultSettings options, bool enabled) + => throw new NotImplementedException(); + + protected override void SetTracing(AzureSecurityKeyVaultSettings options, bool enabled) + => options.DisableTracing = !enabled; + + protected override void TriggerActivity(CertificateClient service) + => service.GetCertificate("IsAlive"); + + [Fact] + public void TracingEnablesTheRightActivitySource() + => RemoteExecutor.Invoke(() => ActivitySourceTest(key: null)).Dispose(); + + [Fact] + public void TracingEnablesTheRightActivitySource_Keyed() + => RemoteExecutor.Invoke(() => ActivitySourceTest(key: "key")).Dispose(); + + private static bool GetCanConnect() + { + CertificateClientOptions clientOptions = new(); + clientOptions.Retry.MaxRetries = 0; // don't enable retries (test runs few times faster) + CertificateClient certClient = new(new Uri(VaultUri), new DefaultAzureCredential(), clientOptions); + + try + { + return certClient.GetCertificate("IsAlive").Value.Name.Equals("IsAlive", StringComparison.CurrentCultureIgnoreCase); + } + catch (Exception) + { + // Requires real key inside of hosted aspiretesting Vault! + // Revert this to false if/when that key is provided to enable conformance testing fail case + return true; + } + } +} diff --git a/tests/Aspire.Azure.Security.KeyVault.Tests/ConformanceConstants.cs b/tests/Aspire.Azure.Security.KeyVault.Tests/ConformanceConstants.cs new file mode 100644 index 00000000000..8720fa5efdf --- /dev/null +++ b/tests/Aspire.Azure.Security.KeyVault.Tests/ConformanceConstants.cs @@ -0,0 +1,9 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Aspire.Azure.Security.KeyVault.Tests; + +public sealed class ConformanceConstants +{ + public const string VaultUri = "https://aspiretests.vault.azure.net/"; +} diff --git a/tests/Aspire.Azure.Security.KeyVault.Tests/KeyClientConformanceTests.cs b/tests/Aspire.Azure.Security.KeyVault.Tests/KeyClientConformanceTests.cs new file mode 100644 index 00000000000..051121ce235 --- /dev/null +++ b/tests/Aspire.Azure.Security.KeyVault.Tests/KeyClientConformanceTests.cs @@ -0,0 +1,132 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Aspire.Components.ConformanceTests; +using Azure.Identity; +using Azure.Security.KeyVault.Keys; +using Microsoft.DotNet.RemoteExecutor; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Xunit; + +namespace Aspire.Azure.Security.KeyVault.Tests; + +public class KeyClientConformanceTests : ConformanceTests +{ + + // Roles: Key Vault Certificate User (pending) + private const string VaultUri = ConformanceConstants.VaultUri; + + private static readonly Lazy s_canConnectToServer = new(GetCanConnect); + + protected override ServiceLifetime ServiceLifetime => ServiceLifetime.Singleton; + + protected override string ActivitySourceName => "Azure.Security.KeyVault.Keys.KeyClient"; + + protected override string[] RequiredLogCategories => new string[] { "Azure.Core" }; + + protected override bool SupportsKeyedRegistrations => true; + + protected override bool CanConnectToServer => s_canConnectToServer.Value; + + protected override string ValidJsonConfig => """ + { + "Aspire": { + "Azure": { + "Security": { + "KeyVault": { + "VaultUri": "http://YOUR_URI", + "DisableHealthChecks": true, + "DisableTracing": false, + "ClientOptions": { + "DisableChallengeResourceVerification": true, + "Retry": { + "Mode": "Exponential", + "Delay": "00:03" + } + } + } + } + } + } + } + """; + + protected override (string json, string error)[] InvalidJsonToErrorMessage => new[] + { + ("""{"Aspire": { "Azure": { "Security":{ "KeyVault": { "VaultUri": "YOUR_URI"}}}}}""", "Value does not match format \"uri\""), + ("""{"Aspire": { "Azure": { "Security":{ "KeyVault": { "VaultUri": "http://YOUR_URI", "DisableHealthChecks": "true"}}}}}""", "Value is \"string\" but should be \"boolean\""), + ("""{"Aspire": { "Azure": { "Security":{ "KeyVault": { "VaultUri": "http://YOUR_URI", "ClientOptions": {"Retry": {"Mode": "Fast"}}}}}}}""", "Value should match one of the values specified by the enum"), + ("""{"Aspire": { "Azure": { "Security":{ "KeyVault": { "VaultUri": "http://YOUR_URI", "ClientOptions": {"Retry": {"NetworkTimeout": "3S"}}}}}}}""", "The string value is not a match for the indicated regular expression") + }; + + protected override void PopulateConfiguration(ConfigurationManager configuration, string? key = null) + => configuration.AddInMemoryCollection(new KeyValuePair[] + { + new(CreateConfigKey("Aspire:Azure:Security:KeyVault", key, "VaultUri"), VaultUri), + new(CreateConfigKey("Aspire:Azure:Security:KeyVault", key, "ClientOptions:Retry:MaxRetries"), "0") + }); + + protected override void RegisterComponent(HostApplicationBuilder builder, Action? configure = null, string? key = null) + { + if (key is null) + { + builder.AddAzureKeyVaultClient("secrets", ConfigureCredentials) + .AddKeyedKeyClient("keys"); + } + else + { + builder.AddKeyedAzureKeyVaultClient(key, ConfigureCredentials); + } + + void ConfigureCredentials(AzureSecurityKeyVaultSettings settings) + { + if (CanConnectToServer) + { + settings.Credential = new DefaultAzureCredential(); + } + configure?.Invoke(settings); + } + } + + protected override void SetHealthCheck(AzureSecurityKeyVaultSettings options, bool enabled) + // Disable Key Vault health check tests until https://github.com/Xabaril/AspNetCore.Diagnostics.HealthChecks/issues/2279 is fixed + // => options.DisableHealthChecks = !enabled; + => throw new NotImplementedException(); + + protected override void SetMetrics(AzureSecurityKeyVaultSettings options, bool enabled) + => throw new NotImplementedException(); + + protected override void SetTracing(AzureSecurityKeyVaultSettings options, bool enabled) + => options.DisableTracing = !enabled; + + protected override void TriggerActivity(KeyClient service) + => service.GetKey("IsAlive"); + + [Fact] + public void TracingEnablesTheRightActivitySource() + => RemoteExecutor.Invoke(() => ActivitySourceTest(key: null)).Dispose(); + + [Fact] + public void TracingEnablesTheRightActivitySource_Keyed() + => RemoteExecutor.Invoke(() => ActivitySourceTest(key: "key")).Dispose(); + + private static bool GetCanConnect() + { + KeyClientOptions clientOptions = new(); + clientOptions.Retry.MaxRetries = 0; // don't enable retries (test runs few times faster) + KeyClient keyClient = new(new Uri(VaultUri), new DefaultAzureCredential(), clientOptions); + + try + { + return keyClient.GetKey("IsAlive").Value.Name.Equals("IsAlive", StringComparison.CurrentCultureIgnoreCase); + } + catch (Exception) + { + // Requires real key inside of hosted aspiretesting Vault! + // Revert this to false if/when that key is provided to enable conformance testing fail case + return true; + } + } +} diff --git a/tests/Aspire.Azure.Security.KeyVault.Tests/ConformanceTests.cs b/tests/Aspire.Azure.Security.KeyVault.Tests/SecretClientConformanceTests.cs similarity index 96% rename from tests/Aspire.Azure.Security.KeyVault.Tests/ConformanceTests.cs rename to tests/Aspire.Azure.Security.KeyVault.Tests/SecretClientConformanceTests.cs index 24cf646b333..0fb9c51150e 100644 --- a/tests/Aspire.Azure.Security.KeyVault.Tests/ConformanceTests.cs +++ b/tests/Aspire.Azure.Security.KeyVault.Tests/SecretClientConformanceTests.cs @@ -12,10 +12,10 @@ namespace Aspire.Azure.Security.KeyVault.Tests; -public class ConformanceTests : ConformanceTests +public class SecretClientConformanceTests : ConformanceTests { // Roles: Key Vault Secrets User - public const string VaultUri = "https://aspiretests.vault.azure.net/"; + private const string VaultUri = ConformanceConstants.VaultUri; private static readonly Lazy s_canConnectToServer = new(GetCanConnect); From c2236bb2b67ed13e50a5ce0cb14e13fca170d230 Mon Sep 17 00:00:00 2001 From: James Gould Date: Sat, 29 Mar 2025 19:25:26 +0000 Subject: [PATCH 10/24] Tests created (and passing) - Certificate and Key missing from hosted Vault so no connection can be made yet. --- ...VaultClientBuilderCertificateExtensions.cs | 12 ++-- .../AspireKeyVaultExtensionsTests.cs | 56 +++++++++++++++++++ .../CertificateClientConformanceTests.cs | 12 ++-- .../KeyClientConformanceTests.cs | 9 ++- 4 files changed, 71 insertions(+), 18 deletions(-) diff --git a/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultClientBuilderCertificateExtensions.cs b/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultClientBuilderCertificateExtensions.cs index 6dff9630e3e..828c9d98cb6 100644 --- a/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultClientBuilderCertificateExtensions.cs +++ b/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultClientBuilderCertificateExtensions.cs @@ -38,23 +38,23 @@ public static AzureKeyVaultClientBuilder AddCertificateClient( /// Registers a keyed as a singleton into the services provided by the /// /// The being used to register Key Vault Clients. - /// The name to call the singleton service. + /// The name to call the singleton service. /// Optional configuration for the /// A to configure further clients. /// Thrown if mandatory is null. - /// Thrown if the mandatory is null or empty. + /// Thrown if the mandatory is null or empty. public static AzureKeyVaultClientBuilder AddKeyedCertificateClient( this AzureKeyVaultClientBuilder builder, - string serviceKey, + string name, Action>? configureClientBuilder = null) { ArgumentNullException.ThrowIfNull(builder); - ArgumentException.ThrowIfNullOrEmpty(serviceKey); + ArgumentException.ThrowIfNullOrEmpty(name); // Overwrite previous builder.ConnectionName to KeyedCertificateClient builder.ConnectionName - builder.ConnectionName = serviceKey; + builder.ConnectionName = name; - return builder.InnerAddCertificateClient(configureClientBuilder, serviceKey); + return builder.InnerAddCertificateClient(configureClientBuilder, name); } /// diff --git a/tests/Aspire.Azure.Security.KeyVault.Tests/AspireKeyVaultExtensionsTests.cs b/tests/Aspire.Azure.Security.KeyVault.Tests/AspireKeyVaultExtensionsTests.cs index 3c7ee20effe..87a80d8cd58 100644 --- a/tests/Aspire.Azure.Security.KeyVault.Tests/AspireKeyVaultExtensionsTests.cs +++ b/tests/Aspire.Azure.Security.KeyVault.Tests/AspireKeyVaultExtensionsTests.cs @@ -271,4 +271,60 @@ public void CanUseBuilderToAddMultipleKeyedClients() Assert.Equal(keyClient.VaultUri, new Uri(keyClientUri)); Assert.Equal(certClient.VaultUri, new Uri(certClientUri)); } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void AddingUnnamedKeyedSecretClientShouldThrow(bool isNull) + { + var builder = Host.CreateEmptyApplicationBuilder(null); + + var name = isNull ? null! : string.Empty; + + var action = () => builder.AddKeyedAzureKeyVaultClient(name); + + var exception = isNull + ? Assert.Throws(action) + : Assert.Throws(action); + + Assert.Equal(nameof(name), exception.ParamName); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void AddingUnnamedKeyedKeyClientShouldThrow(bool isNull) + { + var builder = Host.CreateEmptyApplicationBuilder(null); + + var name = isNull ? null! : string.Empty; + + var action = () => builder.AddKeyedAzureKeyVaultClient("secrets") + .AddKeyedCertificateClient(name); + + var exception = isNull + ? Assert.Throws(action) + : Assert.Throws(action); + + Assert.Equal(nameof(name), exception.ParamName); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void AddingUnnamedKeyedCertificateClientShouldThrow(bool isNull) + { + var builder = Host.CreateEmptyApplicationBuilder(null); + + var name = isNull ? null! : string.Empty; + + var action = () => builder.AddKeyedAzureKeyVaultClient("secrets") + .AddKeyedCertificateClient(name); + + var exception = isNull + ? Assert.Throws(action) + : Assert.Throws(action); + + Assert.Equal(nameof(name), exception.ParamName); + } } diff --git a/tests/Aspire.Azure.Security.KeyVault.Tests/CertificateClientConformanceTests.cs b/tests/Aspire.Azure.Security.KeyVault.Tests/CertificateClientConformanceTests.cs index c79bb5b67ba..6342fa4f8c3 100644 --- a/tests/Aspire.Azure.Security.KeyVault.Tests/CertificateClientConformanceTests.cs +++ b/tests/Aspire.Azure.Security.KeyVault.Tests/CertificateClientConformanceTests.cs @@ -4,7 +4,6 @@ using Aspire.Components.ConformanceTests; using Azure.Identity; using Azure.Security.KeyVault.Certificates; -using Azure.Security.KeyVault.Keys; using Microsoft.DotNet.RemoteExecutor; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -21,7 +20,7 @@ public class CertificateClientConformanceTests : ConformanceTests ServiceLifetime.Singleton; - protected override string ActivitySourceName => "Azure.Security.KeyVault.Keys.KeyClient"; + protected override string ActivitySourceName => "Azure.Security.KeyVault.Certificates.CertificateClient"; protected override string[] RequiredLogCategories => new string[] { "Azure.Core" }; @@ -72,11 +71,12 @@ protected override void RegisterComponent(HostApplicationBuilder builder, Action if (key is null) { builder.AddAzureKeyVaultClient("secrets", ConfigureCredentials) - .AddKeyedKeyClient("keys"); + .AddCertificateClient(); } else { - builder.AddKeyedAzureKeyVaultClient(key, ConfigureCredentials); + builder.AddKeyedAzureKeyVaultClient(key, ConfigureCredentials) + .AddKeyedCertificateClient(key); } void ConfigureCredentials(AzureSecurityKeyVaultSettings settings) @@ -123,9 +123,7 @@ private static bool GetCanConnect() } catch (Exception) { - // Requires real key inside of hosted aspiretesting Vault! - // Revert this to false if/when that key is provided to enable conformance testing fail case - return true; + return false; } } } diff --git a/tests/Aspire.Azure.Security.KeyVault.Tests/KeyClientConformanceTests.cs b/tests/Aspire.Azure.Security.KeyVault.Tests/KeyClientConformanceTests.cs index 051121ce235..ae40f627a99 100644 --- a/tests/Aspire.Azure.Security.KeyVault.Tests/KeyClientConformanceTests.cs +++ b/tests/Aspire.Azure.Security.KeyVault.Tests/KeyClientConformanceTests.cs @@ -73,11 +73,12 @@ protected override void RegisterComponent(HostApplicationBuilder builder, Action if (key is null) { builder.AddAzureKeyVaultClient("secrets", ConfigureCredentials) - .AddKeyedKeyClient("keys"); + .AddKeyClient(); } else { - builder.AddKeyedAzureKeyVaultClient(key, ConfigureCredentials); + builder.AddKeyedAzureKeyVaultClient(key, ConfigureCredentials) + .AddKeyedKeyClient(key); } void ConfigureCredentials(AzureSecurityKeyVaultSettings settings) @@ -124,9 +125,7 @@ private static bool GetCanConnect() } catch (Exception) { - // Requires real key inside of hosted aspiretesting Vault! - // Revert this to false if/when that key is provided to enable conformance testing fail case - return true; + return false; } } } From a1d8071a0e82e12cadc8f0b87b982ff1816570d7 Mon Sep 17 00:00:00 2001 From: James Gould Date: Sat, 29 Mar 2025 19:51:01 +0000 Subject: [PATCH 11/24] Updated README to include new API supported methods --- .../Aspire.Azure.Security.KeyVault/README.md | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/src/Components/Aspire.Azure.Security.KeyVault/README.md b/src/Components/Aspire.Azure.Security.KeyVault/README.md index 8f3d20f01d5..ead0aa1ea2a 100644 --- a/src/Components/Aspire.Azure.Security.KeyVault/README.md +++ b/src/Components/Aspire.Azure.Security.KeyVault/README.md @@ -57,6 +57,42 @@ public ProductsController(SecretClient client) See the [Azure.Security.KeyVault.Secrets documentation](https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/keyvault/Azure.Security.KeyVault.Secrets/README.md) for examples on using the `SecretClient`. +### Optionally include KeyClient and CertificateClient + +You can also optionally dependency inject a `KeyClient` and/or `CertificateClient` too: + +```csharp +builder.AddAzureKeyVaultClient("secrets") + .AddKeyClient() + .AddCertificateClient(); +``` + +Which can then be retrieved in the same way the `SecretClient` is. For example , to retrieve a `KeyClient` from a Web API controller: + +```csharp +private readonly KeyClient _client; + +public ProductsController(KeyClient client) +{ + _client = client; +} +``` + +Alternatively to retrieve a `CertificateClient` from a Web API controller: + +```csharp +private readonly CertificateClient _client; + +public ProductsController(CertificateClient client) +{ + _client = client; +} +``` + +See the [Azure.Security.KeyVault.Keys documentation](https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/keyvault/Azure.Security.KeyVault.Keys/README.md) for examples on using the `KeyClient`. + +See the [Azure.Security.KeyVault.Certificates documentation](https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/keyvault/Azure.Security.KeyVault.Certificates/README.md) for examples on using the `CertificateClient`. + ## Configuration The .NET Aspire Azure Key Vault library provides multiple options to configure the Azure Key Vault connection based on the requirements and conventions of your project. Note that the `VaultUri` is required to be supplied. From 8ef9254e5018754a49dcff6ff3bfd9edc77d78f6 Mon Sep 17 00:00:00 2001 From: James Gould Date: Fri, 4 Apr 2025 18:11:36 +0100 Subject: [PATCH 12/24] Reintroduced original signature causing breaking change, added additional Extended extension method instead --- .../AspireKeyVaultExtensions.cs | 50 ++++++++++++++++++- .../AspireKeyVaultExtensionsTests.cs | 8 +-- .../CertificateClientConformanceTests.cs | 4 +- .../KeyClientConformanceTests.cs | 4 +- 4 files changed, 56 insertions(+), 10 deletions(-) diff --git a/src/Components/Aspire.Azure.Security.KeyVault/AspireKeyVaultExtensions.cs b/src/Components/Aspire.Azure.Security.KeyVault/AspireKeyVaultExtensions.cs index 184696fdcdf..6c17ec3c5eb 100644 --- a/src/Components/Aspire.Azure.Security.KeyVault/AspireKeyVaultExtensions.cs +++ b/src/Components/Aspire.Azure.Security.KeyVault/AspireKeyVaultExtensions.cs @@ -13,6 +13,52 @@ namespace Microsoft.Extensions.Hosting; /// public static class AspireKeyVaultExtensions { + /// + /// Registers as a singleton in the services provided by the . + /// Enables retries, corresponding health check, logging and telemetry. + /// + /// The to read config from and add services to. + /// A name used to retrieve the connection string from the ConnectionStrings configuration section. + /// An optional method that can be used for customizing the . It's invoked after the settings are read from the configuration. + /// An optional method that can be used for customizing the . + /// Reads the configuration from "Aspire:Azure:Security:KeyVault" section. + /// Thrown when mandatory is not provided. + public static void AddAzureKeyVaultClient( + this IHostApplicationBuilder builder, + string connectionName, + Action? configureSettings = null, + Action>? configureClientBuilder = null) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentException.ThrowIfNullOrEmpty(connectionName); + + new AzureKeyVaultClientBuilder(builder, connectionName, configureSettings) + .AddSecretClient(configureClientBuilder); + } + + /// + /// Registers as a singleton for given in the services provided by the . + /// Enables retries, corresponding health check, logging and telemetry. + /// + /// The to read config from and add services to. + /// The name of the component, which is used as the of the service and also to retrieve the connection information from the ConnectionStrings configuration section. + /// An optional method that can be used for customizing the . It's invoked after the settings are read from the configuration. + /// An optional method that can be used for customizing the . + /// Reads the configuration from "Aspire:Azure:Security:KeyVault:{name}" section. + /// Thrown when mandatory is not provided. + public static void AddKeyedAzureKeyVaultClient( + this IHostApplicationBuilder builder, + string name, + Action? configureSettings = null, + Action>? configureClientBuilder = null) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentException.ThrowIfNullOrEmpty(name); + + new AzureKeyVaultClientBuilder(builder, name, configureSettings) + .AddKeyedSecretClient(serviceKey: name, configureClientBuilder); + } + /// /// Registers as a singleton in the services provided by the . /// Enables retries, corresponding health check, logging and telemetry. @@ -24,7 +70,7 @@ public static class AspireKeyVaultExtensions /// Reads the configuration from "Aspire:Azure:Security:KeyVault" section. /// An instance of the allowing for further Key Vault Clients to be registered. /// Thrown when mandatory is not provided. - public static AzureKeyVaultClientBuilder AddAzureKeyVaultClient( + public static AzureKeyVaultClientBuilder AddExtendedAzureKeyVaultClient( this IHostApplicationBuilder builder, string connectionName, Action? configureSettings = null, @@ -48,7 +94,7 @@ public static AzureKeyVaultClientBuilder AddAzureKeyVaultClient( /// Reads the configuration from "Aspire:Azure:Security:KeyVault:{name}" section. /// /// An instance of the allowing for further Key Vault Clients to be registered. /// Thrown when mandatory is not provided. - public static AzureKeyVaultClientBuilder AddKeyedAzureKeyVaultClient( + public static AzureKeyVaultClientBuilder AddExtendedKeyedAzureKeyVaultClient( this IHostApplicationBuilder builder, string name, Action? configureSettings = null, diff --git a/tests/Aspire.Azure.Security.KeyVault.Tests/AspireKeyVaultExtensionsTests.cs b/tests/Aspire.Azure.Security.KeyVault.Tests/AspireKeyVaultExtensionsTests.cs index 87a80d8cd58..e3e8f8e5429 100644 --- a/tests/Aspire.Azure.Security.KeyVault.Tests/AspireKeyVaultExtensionsTests.cs +++ b/tests/Aspire.Azure.Security.KeyVault.Tests/AspireKeyVaultExtensionsTests.cs @@ -216,7 +216,7 @@ public void CanUseBuilderToAddMultipleClients() ]); builder - .AddAzureKeyVaultClient(connectionName) + .AddExtendedAzureKeyVaultClient(connectionName) .AddKeyClient() .AddCertificateClient(); @@ -254,7 +254,7 @@ public void CanUseBuilderToAddMultipleKeyedClients() ]); builder - .AddKeyedAzureKeyVaultClient(secretClientName) + .AddExtendedKeyedAzureKeyVaultClient(secretClientName) .AddKeyedKeyClient(keyClientName) .AddKeyedCertificateClient(certClientName); @@ -299,7 +299,7 @@ public void AddingUnnamedKeyedKeyClientShouldThrow(bool isNull) var name = isNull ? null! : string.Empty; - var action = () => builder.AddKeyedAzureKeyVaultClient("secrets") + var action = () => builder.AddExtendedKeyedAzureKeyVaultClient("secrets") .AddKeyedCertificateClient(name); var exception = isNull @@ -318,7 +318,7 @@ public void AddingUnnamedKeyedCertificateClientShouldThrow(bool isNull) var name = isNull ? null! : string.Empty; - var action = () => builder.AddKeyedAzureKeyVaultClient("secrets") + var action = () => builder.AddExtendedKeyedAzureKeyVaultClient("secrets") .AddKeyedCertificateClient(name); var exception = isNull diff --git a/tests/Aspire.Azure.Security.KeyVault.Tests/CertificateClientConformanceTests.cs b/tests/Aspire.Azure.Security.KeyVault.Tests/CertificateClientConformanceTests.cs index 6342fa4f8c3..233a01f4883 100644 --- a/tests/Aspire.Azure.Security.KeyVault.Tests/CertificateClientConformanceTests.cs +++ b/tests/Aspire.Azure.Security.KeyVault.Tests/CertificateClientConformanceTests.cs @@ -70,12 +70,12 @@ protected override void RegisterComponent(HostApplicationBuilder builder, Action { if (key is null) { - builder.AddAzureKeyVaultClient("secrets", ConfigureCredentials) + builder.AddExtendedAzureKeyVaultClient("secrets", ConfigureCredentials) .AddCertificateClient(); } else { - builder.AddKeyedAzureKeyVaultClient(key, ConfigureCredentials) + builder.AddExtendedKeyedAzureKeyVaultClient(key, ConfigureCredentials) .AddKeyedCertificateClient(key); } diff --git a/tests/Aspire.Azure.Security.KeyVault.Tests/KeyClientConformanceTests.cs b/tests/Aspire.Azure.Security.KeyVault.Tests/KeyClientConformanceTests.cs index ae40f627a99..7d49543c117 100644 --- a/tests/Aspire.Azure.Security.KeyVault.Tests/KeyClientConformanceTests.cs +++ b/tests/Aspire.Azure.Security.KeyVault.Tests/KeyClientConformanceTests.cs @@ -72,12 +72,12 @@ protected override void RegisterComponent(HostApplicationBuilder builder, Action { if (key is null) { - builder.AddAzureKeyVaultClient("secrets", ConfigureCredentials) + builder.AddExtendedAzureKeyVaultClient("secrets", ConfigureCredentials) .AddKeyClient(); } else { - builder.AddKeyedAzureKeyVaultClient(key, ConfigureCredentials) + builder.AddExtendedKeyedAzureKeyVaultClient(key, ConfigureCredentials) .AddKeyedKeyClient(key); } From 1070dd1134875b468976ed1ebce3efb73cf6f450 Mon Sep 17 00:00:00 2001 From: James Gould Date: Fri, 4 Apr 2025 18:45:02 +0100 Subject: [PATCH 13/24] Migrated AddAzureKeyVaultSecrets back to AspireKeyVaultExtensions --- .../AspireKeyVaultExtensions.cs | 56 +++++++++++++++++++ ...reKeyVaultClientBuilderSecretExtensions.cs | 54 ------------------ 2 files changed, 56 insertions(+), 54 deletions(-) diff --git a/src/Components/Aspire.Azure.Security.KeyVault/AspireKeyVaultExtensions.cs b/src/Components/Aspire.Azure.Security.KeyVault/AspireKeyVaultExtensions.cs index 6c17ec3c5eb..b1933d64416 100644 --- a/src/Components/Aspire.Azure.Security.KeyVault/AspireKeyVaultExtensions.cs +++ b/src/Components/Aspire.Azure.Security.KeyVault/AspireKeyVaultExtensions.cs @@ -1,9 +1,13 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Aspire.Azure.Common; using Aspire.Azure.Security.KeyVault; using Azure.Core.Extensions; +using Azure.Extensions.AspNetCore.Configuration.Secrets; +using Azure.Identity; using Azure.Security.KeyVault.Secrets; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; namespace Microsoft.Extensions.Hosting; @@ -59,6 +63,58 @@ public static void AddKeyedAzureKeyVaultClient( .AddKeyedSecretClient(serviceKey: name, configureClientBuilder); } + /// + /// Adds the Azure KeyVault secrets to be configuration values in the . + /// + /// The to add the secrets to. + /// A name used to retrieve the connection string from the ConnectionStrings configuration section. + /// An optional method that can be used for customizing the . It's invoked after the settings are read from the configuration. + /// An optional method that can be used for customizing the . + /// An optional instance to configure the behavior of the configuration provider. + public static void AddAzureKeyVaultSecrets( + this IConfigurationManager configurationManager, + string connectionName, + Action? configureSettings = null, + Action? configureClientOptions = null, + AzureKeyVaultConfigurationOptions? options = null) + { + ArgumentNullException.ThrowIfNull(configurationManager); + ArgumentException.ThrowIfNullOrEmpty(connectionName); + + var client = configurationManager.GetSecretClient(connectionName, configureSettings, configureClientOptions); + configurationManager.AddAzureKeyVault(client, options ?? new AzureKeyVaultConfigurationOptions()); + } + + private static SecretClient GetSecretClient( + this IConfiguration configuration, + string connectionName, + Action? configureSettings, + Action? configureOptions) + { + var configSection = configuration.GetSection(AzureKeyVaultComponentConstants.s_defaultConfigSectionName); + + var settings = new AzureSecurityKeyVaultSettings(); + configSection.Bind(settings); + + if (configuration.GetConnectionString(connectionName) is string connectionString) + { + ((IConnectionStringSettings)settings).ParseConnectionString(connectionString); + } + + configureSettings?.Invoke(settings); + + var clientOptions = new SecretClientOptions(); + configSection.GetSection("ClientOptions").Bind(clientOptions); + configureOptions?.Invoke(clientOptions); + + if (settings.VaultUri is null) + { + throw new InvalidOperationException($"VaultUri is missing. It should be provided in 'ConnectionStrings:{connectionName}' or under the 'VaultUri' key in the '{AzureKeyVaultComponentConstants.s_defaultConfigSectionName}' configuration section."); + } + + return new SecretClient(settings.VaultUri, settings.Credential ?? new DefaultAzureCredential(), clientOptions); + } + /// /// Registers as a singleton in the services provided by the . /// Enables retries, corresponding health check, logging and telemetry. diff --git a/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultClientBuilderSecretExtensions.cs b/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultClientBuilderSecretExtensions.cs index a76feb9613b..59a095e2cac 100644 --- a/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultClientBuilderSecretExtensions.cs +++ b/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultClientBuilderSecretExtensions.cs @@ -5,8 +5,6 @@ using Aspire.Azure.Security.KeyVault; using Azure.Core; using Azure.Core.Extensions; -using Azure.Extensions.AspNetCore.Configuration.Secrets; -using Azure.Identity; using Azure.Security.KeyVault.Secrets; using HealthChecks.Azure.KeyVault.Secrets; using Microsoft.Extensions.Azure; @@ -68,58 +66,6 @@ private static AzureKeyVaultClientBuilder InnerAddSecretClient( return builder; } - /// - /// Adds the Azure KeyVault secrets to be configuration values in the . - /// - /// The to add the secrets to. - /// A name used to retrieve the connection string from the ConnectionStrings configuration section. - /// An optional method that can be used for customizing the . It's invoked after the settings are read from the configuration. - /// An optional method that can be used for customizing the . - /// An optional instance to configure the behavior of the configuration provider. - public static void AddAzureKeyVaultSecrets( - this IConfigurationManager configurationManager, - string connectionName, - Action? configureSettings = null, - Action? configureClientOptions = null, - AzureKeyVaultConfigurationOptions? options = null) - { - ArgumentNullException.ThrowIfNull(configurationManager); - ArgumentException.ThrowIfNullOrEmpty(connectionName); - - var client = configurationManager.GetSecretClient(connectionName, configureSettings, configureClientOptions); - configurationManager.AddAzureKeyVault(client, options ?? new AzureKeyVaultConfigurationOptions()); - } - - private static SecretClient GetSecretClient( - this IConfiguration configuration, - string connectionName, - Action? configureSettings, - Action? configureOptions) - { - var configSection = configuration.GetSection(AzureKeyVaultComponentConstants.s_defaultConfigSectionName); - - var settings = new AzureSecurityKeyVaultSettings(); - configSection.Bind(settings); - - if (configuration.GetConnectionString(connectionName) is string connectionString) - { - ((IConnectionStringSettings)settings).ParseConnectionString(connectionString); - } - - configureSettings?.Invoke(settings); - - var clientOptions = new SecretClientOptions(); - configSection.GetSection("ClientOptions").Bind(clientOptions); - configureOptions?.Invoke(clientOptions); - - if (settings.VaultUri is null) - { - throw new InvalidOperationException($"VaultUri is missing. It should be provided in 'ConnectionStrings:{connectionName}' or under the 'VaultUri' key in the '{AzureKeyVaultComponentConstants.s_defaultConfigSectionName}' configuration section."); - } - - return new SecretClient(settings.VaultUri, settings.Credential ?? new DefaultAzureCredential(), clientOptions); - } - /// /// Representation of an configured as a /// From 61316309f94fea183ae68a325c256bfb320d708a Mon Sep 17 00:00:00 2001 From: James Gould Date: Tue, 29 Apr 2025 16:43:14 +0100 Subject: [PATCH 14/24] Removed healthchecks for keys and certs --- .../AzureKeyVaultCertificatesHealthCheck.cs | 60 ------------------- ...AzureKeyVaultExtendedHealthCheckOptions.cs | 38 ------------ .../AzureKeyVaultKeysHealthCheck.cs | 58 ------------------ 3 files changed, 156 deletions(-) delete mode 100644 src/Components/Aspire.Azure.Security.KeyVault/HealthChecks/AzureKeyVaultCertificatesHealthCheck.cs delete mode 100644 src/Components/Aspire.Azure.Security.KeyVault/HealthChecks/AzureKeyVaultExtendedHealthCheckOptions.cs delete mode 100644 src/Components/Aspire.Azure.Security.KeyVault/HealthChecks/AzureKeyVaultKeysHealthCheck.cs diff --git a/src/Components/Aspire.Azure.Security.KeyVault/HealthChecks/AzureKeyVaultCertificatesHealthCheck.cs b/src/Components/Aspire.Azure.Security.KeyVault/HealthChecks/AzureKeyVaultCertificatesHealthCheck.cs deleted file mode 100644 index e920c8960aa..00000000000 --- a/src/Components/Aspire.Azure.Security.KeyVault/HealthChecks/AzureKeyVaultCertificatesHealthCheck.cs +++ /dev/null @@ -1,60 +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; -using Azure.Security.KeyVault.Certificates; -using Microsoft.Extensions.Diagnostics.HealthChecks; - -namespace Aspire.Azure.Security.KeyVault.HealthChecks; - -/// -/// Creates a basic health check targeting an Azure Key Vault -/// -/// The configured to use for the health check. -/// -internal sealed class AzureKeyVaultCertificatesHealthCheck(CertificateClient client, AzureKeyVaultCertificatesHealthCheckOptions options) : IHealthCheck -{ - internal CertificateClient Client => client; - internal AzureKeyVaultCertificatesHealthCheckOptions Options => options; - - /// - /// Executes a health check using the options provided via . - /// - /// The context in which to perform the health check. - /// The token to cancel the . - /// A representing the status of the connection. - public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) - { - var certificateName = Options.ItemName; - - try - { - await Client.GetCertificateAsync(certificateName, cancellationToken).ConfigureAwait(false); - - return new HealthCheckResult(HealthStatus.Healthy); - } - catch (RequestFailedException azureEx) when (azureEx.Status == 404) // based on https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/core/Azure.Core/README.md#reporting-errors-requestfailedexception - { - // Retaining structure to mimic -> AspNetCore.HealthChecks.Azure.KeyVault.Secrets - if (Options.CreateWhenNotFound) - { - throw new NotImplementedException(); - } - - return new HealthCheckResult(HealthStatus.Healthy); - } - catch (Exception ex) - { - return new HealthCheckResult(context.Registration.FailureStatus, exception: ex); - } - } -} - -internal sealed class AzureKeyVaultCertificatesHealthCheckOptions - : AzureKeyVaultExtendedHealthCheckOptions -{ - /// - /// CreateCertificate{Async} starts a long running process, inappropriate for a Health Check. - /// - public AzureKeyVaultCertificatesHealthCheckOptions() => CreateWhenNotFound = false; -} diff --git a/src/Components/Aspire.Azure.Security.KeyVault/HealthChecks/AzureKeyVaultExtendedHealthCheckOptions.cs b/src/Components/Aspire.Azure.Security.KeyVault/HealthChecks/AzureKeyVaultExtendedHealthCheckOptions.cs deleted file mode 100644 index 5b9ec8f7f84..00000000000 --- a/src/Components/Aspire.Azure.Security.KeyVault/HealthChecks/AzureKeyVaultExtendedHealthCheckOptions.cs +++ /dev/null @@ -1,38 +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; -using Microsoft.Extensions.Diagnostics.HealthChecks; - -namespace Aspire.Azure.Security.KeyVault.HealthChecks; - -/// -/// Basis for Key Vault Client options. -/// -/// -internal class AzureKeyVaultExtendedHealthCheckOptions -{ - /// - /// Default naming of the reference item used for the Health Check. - /// - private string _itemName = nameof(TKeyVaultClientHealthCheck); - - /// - /// The name of the item held in Key Vault to be used for the Health Check. - /// - public string ItemName - { - get => _itemName; - set => _itemName = value ?? throw new ArgumentNullException(nameof(value)); - } - - /// - /// A boolean value that indicates whether the secret should be created when it's not found. - /// by default. - /// - /// - /// Enabling it requires secret set permissions and can be used to improve performance - /// (secret not found is signaled via ). - /// - public bool CreateWhenNotFound { get; set; } -} diff --git a/src/Components/Aspire.Azure.Security.KeyVault/HealthChecks/AzureKeyVaultKeysHealthCheck.cs b/src/Components/Aspire.Azure.Security.KeyVault/HealthChecks/AzureKeyVaultKeysHealthCheck.cs deleted file mode 100644 index 67f44f69abb..00000000000 --- a/src/Components/Aspire.Azure.Security.KeyVault/HealthChecks/AzureKeyVaultKeysHealthCheck.cs +++ /dev/null @@ -1,58 +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; -using Azure.Security.KeyVault.Keys; -using Microsoft.Extensions.Diagnostics.HealthChecks; - -namespace Aspire.Azure.Security.KeyVault.HealthChecks; - -/// -/// Creates a basic health check targeting an Azure Key Vault . -/// -/// The configured to use for the health check. -/// The configuration options for the health check. -internal sealed class AzureKeyVaultKeysHealthCheck(KeyClient client, AzureKeyVaultKeysHealthCheckOptions options) : IHealthCheck -{ - internal KeyClient Client => client; - internal AzureKeyVaultKeysHealthCheckOptions Options => options; - - /// - /// Executes a health check using the options provided via . - /// - /// The context in which to perform the health check. - /// The token to cancel the . - /// A representing the status of the connection. - public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) - { - var keyName = Options.ItemName; - - try - { - await Client.GetKeyAsync(keyName, cancellationToken: cancellationToken).ConfigureAwait(false); - - return new HealthCheckResult(HealthStatus.Healthy); - } - catch (RequestFailedException azureEx) when (azureEx.Status == 404) // based on https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/core/Azure.Core/README.md#reporting-errors-requestfailedexception - { - if (Options.CreateWhenNotFound) - { - // When this call fails, the exception is caught by upper layer. - // From https://learn.microsoft.com/aspnet/core/host-and-deploy/health-checks#create-health-checks: - // "If CheckHealthAsync throws an exception during the check, a new HealthReportEntry is returned with its HealthReportEntry.Status set to the FailureStatus." - await Client.CreateKeyAsync(keyName, KeyType.Rsa, cancellationToken: cancellationToken).ConfigureAwait(false); - } - - // The secret was not found, but it's fine as all we care about is whether it's possible to connect. - return new HealthCheckResult(HealthStatus.Healthy); - } - catch (Exception ex) - { - return new HealthCheckResult(context.Registration.FailureStatus, exception: ex); - } - } -} - -internal sealed class AzureKeyVaultKeysHealthCheckOptions - : AzureKeyVaultExtendedHealthCheckOptions -{ } From 0b92db334f831d1035c556adbb1c309bc679cf73 Mon Sep 17 00:00:00 2001 From: James Gould Date: Tue, 29 Apr 2025 16:43:36 +0100 Subject: [PATCH 15/24] Fixed formatting --- .../AspireKeyVaultExtensions.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Components/Aspire.Azure.Security.KeyVault/AspireKeyVaultExtensions.cs b/src/Components/Aspire.Azure.Security.KeyVault/AspireKeyVaultExtensions.cs index b1933d64416..20706700a83 100644 --- a/src/Components/Aspire.Azure.Security.KeyVault/AspireKeyVaultExtensions.cs +++ b/src/Components/Aspire.Azure.Security.KeyVault/AspireKeyVaultExtensions.cs @@ -28,10 +28,10 @@ public static class AspireKeyVaultExtensions /// Reads the configuration from "Aspire:Azure:Security:KeyVault" section. /// Thrown when mandatory is not provided. public static void AddAzureKeyVaultClient( - this IHostApplicationBuilder builder, - string connectionName, - Action? configureSettings = null, - Action>? configureClientBuilder = null) + this IHostApplicationBuilder builder, + string connectionName, + Action? configureSettings = null, + Action>? configureClientBuilder = null) { ArgumentNullException.ThrowIfNull(builder); ArgumentException.ThrowIfNullOrEmpty(connectionName); From 3837592f81147e50a36be31c9015c14475f4d55a Mon Sep 17 00:00:00 2001 From: James Gould Date: Tue, 29 Apr 2025 16:47:28 +0100 Subject: [PATCH 16/24] Project level surpression moved to source and healthcheck create defaulted to null --- .../Aspire.Azure.Security.KeyVault.csproj | 2 +- .../AzureKeyVaultClientBuilderCertificateExtensions.cs | 5 +++-- .../AzureKeyVaultClientBuilderKeyExtensions.cs | 5 +++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/Components/Aspire.Azure.Security.KeyVault/Aspire.Azure.Security.KeyVault.csproj b/src/Components/Aspire.Azure.Security.KeyVault/Aspire.Azure.Security.KeyVault.csproj index b8847101beb..69a81e90b9e 100644 --- a/src/Components/Aspire.Azure.Security.KeyVault/Aspire.Azure.Security.KeyVault.csproj +++ b/src/Components/Aspire.Azure.Security.KeyVault/Aspire.Azure.Security.KeyVault.csproj @@ -6,7 +6,7 @@ $(ComponentAzurePackageTags) keyvault secrets A client for Azure Key Vault that integrates with Aspire, including health checks, logging and telemetry. $(SharedDir)AzureKeyVault_256x.png - $(NoWarn);SYSLIB1100;SYSLIB1101;IDE0200 + $(NoWarn);SYSLIB1100;SYSLIB1101; diff --git a/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultClientBuilderCertificateExtensions.cs b/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultClientBuilderCertificateExtensions.cs index 828c9d98cb6..726acea2203 100644 --- a/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultClientBuilderCertificateExtensions.cs +++ b/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultClientBuilderCertificateExtensions.cs @@ -3,7 +3,6 @@ using Aspire.Azure.Common; using Aspire.Azure.Security.KeyVault; -using Aspire.Azure.Security.KeyVault.HealthChecks; using Azure.Core; using Azure.Core.Extensions; using Azure.Security.KeyVault.Certificates; @@ -85,10 +84,12 @@ internal override CertificateClient CreateComponentClient(Uri vaultUri, Certific => new(vaultUri, cred, options); protected override IHealthCheck CreateHealthCheck(CertificateClient client, AzureSecurityKeyVaultSettings settings) - => new AzureKeyVaultCertificatesHealthCheck(client, new AzureKeyVaultCertificatesHealthCheckOptions()); + => null!; protected override void BindClientOptionsToConfiguration(IAzureClientBuilder clientBuilder, IConfiguration configuration) +#pragma warning disable IDE0200 // Remove unnecessary lambda expression => clientBuilder.ConfigureOptions(options => configuration.Bind(options)); +#pragma warning restore IDE0200 // Remove unnecessary lambda expression protected override void BindSettingsToConfiguration(AzureSecurityKeyVaultSettings settings, IConfiguration configuration) => configuration.Bind(settings); diff --git a/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultClientBuilderKeyExtensions.cs b/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultClientBuilderKeyExtensions.cs index c2939c6f4eb..1208992929d 100644 --- a/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultClientBuilderKeyExtensions.cs +++ b/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultClientBuilderKeyExtensions.cs @@ -3,7 +3,6 @@ using Aspire.Azure.Common; using Aspire.Azure.Security.KeyVault; -using Aspire.Azure.Security.KeyVault.HealthChecks; using Azure.Core; using Azure.Core.Extensions; using Azure.Security.KeyVault.Keys; @@ -83,13 +82,15 @@ private static AzureKeyVaultClientBuilder InnerAddKeyClient( private sealed class KeyVaultKeyComponent : AbstractKeyVaultComponent { protected override IHealthCheck CreateHealthCheck(KeyClient client, AzureSecurityKeyVaultSettings settings) - => new AzureKeyVaultKeysHealthCheck(client, new AzureKeyVaultKeysHealthCheckOptions()); + => default!; internal override KeyClient CreateComponentClient(Uri vaultUri, KeyClientOptions options, TokenCredential cred) => new(vaultUri, cred, options); protected override void BindClientOptionsToConfiguration(IAzureClientBuilder clientBuilder, IConfiguration configuration) +#pragma warning disable IDE0200 // Remove unnecessary lambda expression => clientBuilder.ConfigureOptions(options => configuration.Bind(options)); +#pragma warning restore IDE0200 // Remove unnecessary lambda expression protected override void BindSettingsToConfiguration(AzureSecurityKeyVaultSettings settings, IConfiguration configuration) => configuration.Bind(settings); From 6fe9df6c951850e6768ee198d6deee92d7af5ca1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Ros?= Date: Tue, 29 Apr 2025 09:23:09 -0700 Subject: [PATCH 17/24] Remove unnecessary lambda --- .../AzureKeyVaultClientBuilderSecretExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultClientBuilderSecretExtensions.cs b/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultClientBuilderSecretExtensions.cs index 59a095e2cac..9499e27db63 100644 --- a/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultClientBuilderSecretExtensions.cs +++ b/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultClientBuilderSecretExtensions.cs @@ -72,7 +72,7 @@ private static AzureKeyVaultClientBuilder InnerAddSecretClient( private sealed class KeyVaultSecretsComponent : AbstractKeyVaultComponent { protected override void BindClientOptionsToConfiguration(IAzureClientBuilder clientBuilder, IConfiguration configuration) - => clientBuilder.ConfigureOptions(options => configuration.Bind(options)); + => clientBuilder.ConfigureOptions(configuration.Bind); protected override void BindSettingsToConfiguration(AzureSecurityKeyVaultSettings settings, IConfiguration configuration) => configuration.Bind(settings); From 5941be1e5b1698630e2c812a60bede5947bc93f6 Mon Sep 17 00:00:00 2001 From: James Gould Date: Tue, 29 Apr 2025 17:31:56 +0100 Subject: [PATCH 18/24] Removed builder pattern and added extensions for each Client type --- ...t.cs => AbstractAzureKeyVaultComponent.cs} | 2 +- .../AspireKeyVaultExtensions.cs | 152 +++++++++++------- .../AzureKeyVaultCertificatesComponent.cs | 33 ++++ .../AzureKeyVaultClientBuilder.cs | 40 ----- ...VaultClientBuilderCertificateExtensions.cs | 97 ----------- ...AzureKeyVaultClientBuilderKeyExtensions.cs | 98 ----------- ...reKeyVaultClientBuilderSecretExtensions.cs | 86 ---------- .../AzureKeyVaultComponentConstants.cs | 9 -- .../AzureKeyVaultKeysComponent.cs | 29 ++++ .../AzureKeyVaultSecretsComponent.cs | 30 ++++ 10 files changed, 191 insertions(+), 385 deletions(-) rename src/Components/Aspire.Azure.Security.KeyVault/{AbstractKeyVaultComponent.cs => AbstractAzureKeyVaultComponent.cs} (93%) create mode 100644 src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultCertificatesComponent.cs delete mode 100644 src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultClientBuilder.cs delete mode 100644 src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultClientBuilderCertificateExtensions.cs delete mode 100644 src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultClientBuilderKeyExtensions.cs delete mode 100644 src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultClientBuilderSecretExtensions.cs delete mode 100644 src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultComponentConstants.cs create mode 100644 src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultKeysComponent.cs create mode 100644 src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultSecretsComponent.cs diff --git a/src/Components/Aspire.Azure.Security.KeyVault/AbstractKeyVaultComponent.cs b/src/Components/Aspire.Azure.Security.KeyVault/AbstractAzureKeyVaultComponent.cs similarity index 93% rename from src/Components/Aspire.Azure.Security.KeyVault/AbstractKeyVaultComponent.cs rename to src/Components/Aspire.Azure.Security.KeyVault/AbstractAzureKeyVaultComponent.cs index 3dd3ef50125..4a27173f475 100644 --- a/src/Components/Aspire.Azure.Security.KeyVault/AbstractKeyVaultComponent.cs +++ b/src/Components/Aspire.Azure.Security.KeyVault/AbstractAzureKeyVaultComponent.cs @@ -17,7 +17,7 @@ namespace Aspire.Azure.Security.KeyVault; /// /// The KeyVaultClient type for this component. /// The associated configuration for the -internal abstract class AbstractKeyVaultComponent +internal abstract class AbstractAzureKeyVaultComponent : AzureComponent where TClient : class where TOptions : class diff --git a/src/Components/Aspire.Azure.Security.KeyVault/AspireKeyVaultExtensions.cs b/src/Components/Aspire.Azure.Security.KeyVault/AspireKeyVaultExtensions.cs index 20706700a83..7e87c0a2147 100644 --- a/src/Components/Aspire.Azure.Security.KeyVault/AspireKeyVaultExtensions.cs +++ b/src/Components/Aspire.Azure.Security.KeyVault/AspireKeyVaultExtensions.cs @@ -6,6 +6,8 @@ using Azure.Core.Extensions; using Azure.Extensions.AspNetCore.Configuration.Secrets; using Azure.Identity; +using Azure.Security.KeyVault.Certificates; +using Azure.Security.KeyVault.Keys; using Azure.Security.KeyVault.Secrets; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -17,6 +19,8 @@ namespace Microsoft.Extensions.Hosting; /// public static class AspireKeyVaultExtensions { + internal const string DefaultConfigSectionName = "Aspire:Azure:Security:KeyVault"; + /// /// Registers as a singleton in the services provided by the . /// Enables retries, corresponding health check, logging and telemetry. @@ -36,8 +40,8 @@ public static void AddAzureKeyVaultClient( ArgumentNullException.ThrowIfNull(builder); ArgumentException.ThrowIfNullOrEmpty(connectionName); - new AzureKeyVaultClientBuilder(builder, connectionName, configureSettings) - .AddSecretClient(configureClientBuilder); + new AzureKeyVaultSecretsComponent() + .AddClient(builder, DefaultConfigSectionName, configureSettings, configureClientBuilder, connectionName, serviceKey: null); } /// @@ -59,8 +63,96 @@ public static void AddKeyedAzureKeyVaultClient( ArgumentNullException.ThrowIfNull(builder); ArgumentException.ThrowIfNullOrEmpty(name); - new AzureKeyVaultClientBuilder(builder, name, configureSettings) - .AddKeyedSecretClient(serviceKey: name, configureClientBuilder); + new AzureKeyVaultSecretsComponent() + .AddClient(builder, DefaultConfigSectionName, configureSettings, configureClientBuilder, connectionName: name, serviceKey: name); + } + + /// + /// Registers as a singleton in the services provided by the . + /// + /// The to read config from and add services to. + /// A name used to retrieve the connection string from the ConnectionStrings configuration section. + /// An optional method that can be used for customizing the . It's invoked after the settings are read from the configuration. + /// An optional method that can be used for customizing the . + /// Reads the configuration from "Aspire:Azure:Security:KeyVault:{name}" section. + /// Thrown when mandatory is not provided. + public static void AddAzureKeyVaultCertificateClient( + this IHostApplicationBuilder builder, + string connectionName, + Action? configureSettings = null, + Action>? configureClientBuilder = null) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentException.ThrowIfNullOrEmpty(connectionName); + + new AzureKeyVaultCertificatesComponent() + .AddClient(builder, DefaultConfigSectionName, configureSettings, configureClientBuilder, connectionName, serviceKey: null); + } + + /// + /// Registers as a singleton for given in the services provided by the . + /// + /// The to read config from and add services to. + /// The name of the component, which is used as the of the service and also to retrieve the connection information from the ConnectionStrings configuration section. + /// An optional method that can be used for customizing the . It's invoked after the settings are read from the configuration. + /// An optional method that can be used for customizing the . + /// Reads the configuration from "Aspire:Azure:Security:KeyVault:{name}" section. + /// Thrown when mandatory is not provided. + public static void AddKeyedAzureKeyVaultCertificateClient( + this IHostApplicationBuilder builder, + string name, + Action? configureSettings = null, + Action>? configureClientBuilder = null) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentException.ThrowIfNullOrEmpty(name); + + new AzureKeyVaultCertificatesComponent() + .AddClient(builder, DefaultConfigSectionName, configureSettings, configureClientBuilder, connectionName: name, serviceKey: name); + } + + /// + /// Registers as a singleton in the services provided by the . + /// + /// The to read config from and add services to. + /// A name used to retrieve the connection string from the ConnectionStrings configuration section. + /// An optional method that can be used for customizing the . It's invoked after the settings are read from the configuration. + /// An optional method that can be used for customizing the . + /// Reads the configuration from "Aspire:Azure:Security:KeyVault:{name}" section. + /// Thrown when mandatory is not provided. + public static void AddAzureKeyVaultKeyClient( + this IHostApplicationBuilder builder, + string connectionName, + Action? configureSettings = null, + Action>? configureClientBuilder = null) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentException.ThrowIfNullOrEmpty(connectionName); + + new AzureKeyVaultKeysComponent() + .AddClient(builder, DefaultConfigSectionName, configureSettings, configureClientBuilder, connectionName, serviceKey: null); + } + + /// + /// Registers as a singleton for given in the services provided by the . + /// + /// The to read config from and add services to. + /// The name of the component, which is used as the of the service and also to retrieve the connection information from the ConnectionStrings configuration section. + /// An optional method that can be used for customizing the . It's invoked after the settings are read from the configuration. + /// An optional method that can be used for customizing the . + /// Reads the configuration from "Aspire:Azure:Security:KeyVault:{name}" section. + /// Thrown when mandatory is not provided. + public static void AddKeyedAzureKeyVaultKeyClient( + this IHostApplicationBuilder builder, + string name, + Action? configureSettings = null, + Action>? configureClientBuilder = null) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentException.ThrowIfNullOrEmpty(name); + + new AzureKeyVaultKeysComponent() + .AddClient(builder, DefaultConfigSectionName, configureSettings, configureClientBuilder, connectionName: name, serviceKey: name); } /// @@ -91,7 +183,7 @@ private static SecretClient GetSecretClient( Action? configureSettings, Action? configureOptions) { - var configSection = configuration.GetSection(AzureKeyVaultComponentConstants.s_defaultConfigSectionName); + var configSection = configuration.GetSection(DefaultConfigSectionName); var settings = new AzureSecurityKeyVaultSettings(); configSection.Bind(settings); @@ -109,57 +201,9 @@ private static SecretClient GetSecretClient( if (settings.VaultUri is null) { - throw new InvalidOperationException($"VaultUri is missing. It should be provided in 'ConnectionStrings:{connectionName}' or under the 'VaultUri' key in the '{AzureKeyVaultComponentConstants.s_defaultConfigSectionName}' configuration section."); + throw new InvalidOperationException($"VaultUri is missing. It should be provided in 'ConnectionStrings:{connectionName}' or under the 'VaultUri' key in the '{DefaultConfigSectionName}' configuration section."); } return new SecretClient(settings.VaultUri, settings.Credential ?? new DefaultAzureCredential(), clientOptions); } - - /// - /// Registers as a singleton in the services provided by the . - /// Enables retries, corresponding health check, logging and telemetry. - /// - /// The to read config from and add services to. - /// A name used to retrieve the connection string from the ConnectionStrings configuration section. - /// An optional method that can be used for customizing the . It's invoked after the settings are read from the configuration. - /// An optional method that can be used for customizing the . - /// Reads the configuration from "Aspire:Azure:Security:KeyVault" section. - /// An instance of the allowing for further Key Vault Clients to be registered. - /// Thrown when mandatory is not provided. - public static AzureKeyVaultClientBuilder AddExtendedAzureKeyVaultClient( - this IHostApplicationBuilder builder, - string connectionName, - Action? configureSettings = null, - Action>? configureClientBuilder = null) - { - ArgumentNullException.ThrowIfNull(builder); - ArgumentException.ThrowIfNullOrEmpty(connectionName); - - return new AzureKeyVaultClientBuilder(builder, connectionName, configureSettings) - .AddSecretClient(configureClientBuilder); - } - - /// - /// Registers as a singleton for given in the services provided by the . - /// Enables retries, corresponding health check, logging and telemetry. - /// - /// The to read config from and add services to. - /// The name of the component, which is used as the of the service and also to retrieve the connection information from the ConnectionStrings configuration section. - /// An optional method that can be used for customizing the . It's invoked after the settings are read from the configuration. - /// An optional method that can be used for customizing the . - /// Reads the configuration from "Aspire:Azure:Security:KeyVault:{name}" section. - /// /// An instance of the allowing for further Key Vault Clients to be registered. - /// Thrown when mandatory is not provided. - public static AzureKeyVaultClientBuilder AddExtendedKeyedAzureKeyVaultClient( - this IHostApplicationBuilder builder, - string name, - Action? configureSettings = null, - Action>? configureClientBuilder = null) - { - ArgumentNullException.ThrowIfNull(builder); - ArgumentException.ThrowIfNullOrEmpty(name); - - return new AzureKeyVaultClientBuilder(builder, name, configureSettings) - .AddKeyedSecretClient(serviceKey: name, configureClientBuilder); - } } diff --git a/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultCertificatesComponent.cs b/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultCertificatesComponent.cs new file mode 100644 index 00000000000..099d9c10670 --- /dev/null +++ b/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultCertificatesComponent.cs @@ -0,0 +1,33 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Aspire.Azure.Common; +using Aspire.Azure.Security.KeyVault; +using Azure.Core; +using Azure.Core.Extensions; +using Azure.Security.KeyVault.Certificates; +using Microsoft.Extensions.Azure; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Diagnostics.HealthChecks; + +namespace Microsoft.Extensions.Hosting; + +/// +/// Representation of an configured as a +/// +internal sealed class AzureKeyVaultCertificatesComponent : AbstractAzureKeyVaultComponent +{ + internal override CertificateClient CreateComponentClient(Uri vaultUri, CertificateClientOptions options, TokenCredential cred) + => new(vaultUri, cred, options); + + protected override IHealthCheck CreateHealthCheck(CertificateClient client, AzureSecurityKeyVaultSettings settings) + => throw new NotImplementedException(); + + protected override void BindClientOptionsToConfiguration(IAzureClientBuilder clientBuilder, IConfiguration configuration) +#pragma warning disable IDE0200 // Remove unnecessary lambda expression + => clientBuilder.ConfigureOptions(options => configuration.Bind(options)); +#pragma warning restore IDE0200 // Remove unnecessary lambda expression + + protected override void BindSettingsToConfiguration(AzureSecurityKeyVaultSettings settings, IConfiguration configuration) + => configuration.Bind(settings); +} diff --git a/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultClientBuilder.cs b/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultClientBuilder.cs deleted file mode 100644 index 53d54803712..00000000000 --- a/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultClientBuilder.cs +++ /dev/null @@ -1,40 +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 Aspire.Azure.Security.KeyVault; - -namespace Microsoft.Extensions.Hosting; - -/// -/// A builder used for creating one or more Key Vault Clients, registered into the . -/// -/// The to register the clients to as singletons. -/// The name used to retrieve the VaultUri from ConnectionStrings in the configuration provider. -/// An optional configuration point for the overall applied to each Key Vault Client. -public class AzureKeyVaultClientBuilder( - IHostApplicationBuilder host, - string connectionName, - Action? configureSettings) -{ - /// - /// The default name of the configuration section for Key Vault. - /// - internal string DefaultConfigSectionName { get; } = AzureKeyVaultComponentConstants.s_defaultConfigSectionName; - - /// - /// The to register Key Vault Clients into as singletons. - /// - internal IHostApplicationBuilder HostBuilder { get; } = host; - - /// - /// The name used to retrieve the VaultUri from ConnectionStrings in the configuration provider. - /// Setting the value after the initial creation allows for keyed clients of different types to have separate ConnectionStrings configuration names. - /// For example: ConnectionStrings.MyKeyedSecretClient in previous builder stage will become ConnectionStrings.MyKeyedKeyClient. - /// - internal string ConnectionName { get; set; } = connectionName; - - /// - /// An optional configuration point for the overall applied to each Key Vault Client. - /// - internal Action? ConfigureSettings { get; } = configureSettings; -} diff --git a/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultClientBuilderCertificateExtensions.cs b/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultClientBuilderCertificateExtensions.cs deleted file mode 100644 index 726acea2203..00000000000 --- a/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultClientBuilderCertificateExtensions.cs +++ /dev/null @@ -1,97 +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 Aspire.Azure.Common; -using Aspire.Azure.Security.KeyVault; -using Azure.Core; -using Azure.Core.Extensions; -using Azure.Security.KeyVault.Certificates; -using Microsoft.Extensions.Azure; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Diagnostics.HealthChecks; - -namespace Microsoft.Extensions.Hosting; - -/// -/// Extends the for optionally registering a . -/// -public static class AzureKeyVaultClientBuilderCertificateExtensions -{ - /// - /// Registers a as a singleton into the services provided by the - /// - /// The being used to register Key Vault Clients. - /// Optional configuration for the . - /// A to configure further clients. - /// Thrown if the mandatory is null. - public static AzureKeyVaultClientBuilder AddCertificateClient( - this AzureKeyVaultClientBuilder builder, - Action>? configureClientBuilder = null) - { - ArgumentNullException.ThrowIfNull(builder); - - return builder.InnerAddCertificateClient(configureClientBuilder); - } - - /// - /// Registers a keyed as a singleton into the services provided by the - /// - /// The being used to register Key Vault Clients. - /// The name to call the singleton service. - /// Optional configuration for the - /// A to configure further clients. - /// Thrown if mandatory is null. - /// Thrown if the mandatory is null or empty. - public static AzureKeyVaultClientBuilder AddKeyedCertificateClient( - this AzureKeyVaultClientBuilder builder, - string name, - Action>? configureClientBuilder = null) - { - ArgumentNullException.ThrowIfNull(builder); - ArgumentException.ThrowIfNullOrEmpty(name); - - // Overwrite previous builder.ConnectionName to KeyedCertificateClient builder.ConnectionName - builder.ConnectionName = name; - - return builder.InnerAddCertificateClient(configureClientBuilder, name); - } - - /// - /// Implements the creation of a as an - /// - /// Used to register AzureKeyVault clients. - /// The name to call the singleton service. - /// Optional configuration for the . - /// A to configure further clients. - private static AzureKeyVaultClientBuilder InnerAddCertificateClient( - this AzureKeyVaultClientBuilder builder, - Action>? configureClientBuilder = null, - string? serviceKey = null) - { - new KeyVaultCertificateComponent() - .AddClient(builder.HostBuilder, builder.DefaultConfigSectionName, builder.ConfigureSettings, - configureClientBuilder, builder.ConnectionName, serviceKey); - - return builder; - } - - /// - /// Representation of an configured as a - /// - private sealed class KeyVaultCertificateComponent : AbstractKeyVaultComponent - { - internal override CertificateClient CreateComponentClient(Uri vaultUri, CertificateClientOptions options, TokenCredential cred) - => new(vaultUri, cred, options); - - protected override IHealthCheck CreateHealthCheck(CertificateClient client, AzureSecurityKeyVaultSettings settings) - => null!; - - protected override void BindClientOptionsToConfiguration(IAzureClientBuilder clientBuilder, IConfiguration configuration) -#pragma warning disable IDE0200 // Remove unnecessary lambda expression - => clientBuilder.ConfigureOptions(options => configuration.Bind(options)); -#pragma warning restore IDE0200 // Remove unnecessary lambda expression - - protected override void BindSettingsToConfiguration(AzureSecurityKeyVaultSettings settings, IConfiguration configuration) - => configuration.Bind(settings); - } -} diff --git a/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultClientBuilderKeyExtensions.cs b/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultClientBuilderKeyExtensions.cs deleted file mode 100644 index 1208992929d..00000000000 --- a/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultClientBuilderKeyExtensions.cs +++ /dev/null @@ -1,98 +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 Aspire.Azure.Common; -using Aspire.Azure.Security.KeyVault; -using Azure.Core; -using Azure.Core.Extensions; -using Azure.Security.KeyVault.Keys; -using Microsoft.Extensions.Azure; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Diagnostics.HealthChecks; - -namespace Microsoft.Extensions.Hosting; - -/// -/// Extends the for optionally registering a . -/// -public static class AzureKeyVaultClientBuilderKeyExtensions -{ - /// - /// Registers a as a singleton into the services provided by the . - /// - /// The being used to register Key Vault Clients. - /// Optional configuration for the . - /// A to configure further clients. - /// Thrown if the mandatory is null. - public static AzureKeyVaultClientBuilder AddKeyClient( - this AzureKeyVaultClientBuilder builder, - Action>? configureClientBuilder = null) - { - ArgumentNullException.ThrowIfNull(builder); - - return builder.InnerAddKeyClient(configureClientBuilder); - } - - /// - /// Registers a keyed as a singleton into the services provided by the . - /// - /// The being used to register Key Vault Clients. - /// The name to call the singleton service. - /// Optional configuration for the - /// A to configure further clients. - /// Thrown if mandatory is null. - /// Thrown if the mandatory is null or empty. - /// - public static AzureKeyVaultClientBuilder AddKeyedKeyClient( - this AzureKeyVaultClientBuilder builder, - string serviceKey, - Action>? configureClientBuilder = null) - { - ArgumentNullException.ThrowIfNull(builder); - ArgumentException.ThrowIfNullOrEmpty(serviceKey); - - // Overwrite previous builder.ConnectionName to KeyedKeyClient builder.ConnectionName - builder.ConnectionName = serviceKey; - - return builder.InnerAddKeyClient(configureClientBuilder, serviceKey); - } - - /// - /// Implements the creation of a as an - /// - /// Used to register AzureKeyVault clients. - /// The name to call the singleton service. - /// Optional configuration for the . - /// A to configure further clients. - private static AzureKeyVaultClientBuilder InnerAddKeyClient( - this AzureKeyVaultClientBuilder builder, - Action>? configureClientBuilder = null, - string? serviceKey = null) - { - new KeyVaultKeyComponent() - .AddClient(builder.HostBuilder, builder.DefaultConfigSectionName, builder.ConfigureSettings, - configureClientBuilder, builder.ConnectionName, serviceKey); - - return builder; - } - - /// - /// Representation of an configured as a - /// - private sealed class KeyVaultKeyComponent : AbstractKeyVaultComponent - { - protected override IHealthCheck CreateHealthCheck(KeyClient client, AzureSecurityKeyVaultSettings settings) - => default!; - - internal override KeyClient CreateComponentClient(Uri vaultUri, KeyClientOptions options, TokenCredential cred) - => new(vaultUri, cred, options); - - protected override void BindClientOptionsToConfiguration(IAzureClientBuilder clientBuilder, IConfiguration configuration) -#pragma warning disable IDE0200 // Remove unnecessary lambda expression - => clientBuilder.ConfigureOptions(options => configuration.Bind(options)); -#pragma warning restore IDE0200 // Remove unnecessary lambda expression - - protected override void BindSettingsToConfiguration(AzureSecurityKeyVaultSettings settings, IConfiguration configuration) - => configuration.Bind(settings); - } -} diff --git a/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultClientBuilderSecretExtensions.cs b/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultClientBuilderSecretExtensions.cs deleted file mode 100644 index 59a095e2cac..00000000000 --- a/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultClientBuilderSecretExtensions.cs +++ /dev/null @@ -1,86 +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 Aspire.Azure.Common; -using Aspire.Azure.Security.KeyVault; -using Azure.Core; -using Azure.Core.Extensions; -using Azure.Security.KeyVault.Secrets; -using HealthChecks.Azure.KeyVault.Secrets; -using Microsoft.Extensions.Azure; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Diagnostics.HealthChecks; - -namespace Microsoft.Extensions.Hosting; - -/// -/// -/// -public static class AzureKeyVaultClientBuilderSecretExtensions -{ - /// - /// Registers a as a singleton into the services provided by the . - /// - /// Used to register AzureKeyVault clients. - /// Optional configuration for the . - /// A to configure further clients. - public static AzureKeyVaultClientBuilder AddSecretClient( - this AzureKeyVaultClientBuilder builder, - Action>? configureClientBuilder = null) - { - return builder.InnerAddSecretClient(configureClientBuilder); - } - - /// - /// Registers a keyed as a singleton into the services provided by the . - /// - /// Used to register AzureKeyVault clients. - /// The name to call the singleton service. - /// Optional configuration for the . - /// A to configure further clients. - /// Thrown if mandatory is null or empty. - public static AzureKeyVaultClientBuilder AddKeyedSecretClient( - this AzureKeyVaultClientBuilder builder, - string serviceKey, - Action>? configureClientBuilder = null) - { - return builder.InnerAddSecretClient(configureClientBuilder, serviceKey); - } - - /// - /// Implements the creation of a as an - /// - /// Used to register AzureKeyVault clients. - /// The name to call the singleton service. - /// Optional configuration for the . - /// A to configure further clients. - private static AzureKeyVaultClientBuilder InnerAddSecretClient( - this AzureKeyVaultClientBuilder builder, - Action>? configureClientBuilder = null, - string? serviceKey = null) - { - new KeyVaultSecretsComponent() - .AddClient(builder.HostBuilder, builder.DefaultConfigSectionName, builder.ConfigureSettings, - configureClientBuilder, builder.ConnectionName, serviceKey); - - return builder; - } - - /// - /// Representation of an configured as a - /// - private sealed class KeyVaultSecretsComponent : AbstractKeyVaultComponent - { - protected override void BindClientOptionsToConfiguration(IAzureClientBuilder clientBuilder, IConfiguration configuration) - => clientBuilder.ConfigureOptions(options => configuration.Bind(options)); - - protected override void BindSettingsToConfiguration(AzureSecurityKeyVaultSettings settings, IConfiguration configuration) - => configuration.Bind(settings); - - protected override IHealthCheck CreateHealthCheck(SecretClient client, AzureSecurityKeyVaultSettings settings) - => new AzureKeyVaultSecretsHealthCheck(client, new AzureKeyVaultSecretsHealthCheckOptions()); - - internal override SecretClient CreateComponentClient(Uri vaultUri, SecretClientOptions options, TokenCredential cred) - => new(vaultUri, cred, options); - } -} diff --git a/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultComponentConstants.cs b/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultComponentConstants.cs deleted file mode 100644 index 8863ebe6a4b..00000000000 --- a/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultComponentConstants.cs +++ /dev/null @@ -1,9 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.Extensions.Hosting; - -internal static class AzureKeyVaultComponentConstants -{ - internal static string s_defaultConfigSectionName = "Aspire:Azure:Security:KeyVault"; -} diff --git a/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultKeysComponent.cs b/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultKeysComponent.cs new file mode 100644 index 00000000000..33525d55950 --- /dev/null +++ b/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultKeysComponent.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 Aspire.Azure.Security.KeyVault; +using Azure.Core; +using Azure.Core.Extensions; +using Azure.Security.KeyVault.Keys; +using Microsoft.Extensions.Azure; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Diagnostics.HealthChecks; + +namespace Microsoft.Extensions.Hosting; + +internal sealed class AzureKeyVaultKeysComponent : AbstractAzureKeyVaultComponent +{ + protected override IHealthCheck CreateHealthCheck(KeyClient client, AzureSecurityKeyVaultSettings settings) + => throw new NotImplementedException(); + + internal override KeyClient CreateComponentClient(Uri vaultUri, KeyClientOptions options, TokenCredential cred) + => new(vaultUri, cred, options); + + protected override void BindClientOptionsToConfiguration(IAzureClientBuilder clientBuilder, IConfiguration configuration) +#pragma warning disable IDE0200 // Remove unnecessary lambda expression + => clientBuilder.ConfigureOptions(options => configuration.Bind(options)); +#pragma warning restore IDE0200 // Remove unnecessary lambda expression + + protected override void BindSettingsToConfiguration(AzureSecurityKeyVaultSettings settings, IConfiguration configuration) + => configuration.Bind(settings); +} diff --git a/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultSecretsComponent.cs b/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultSecretsComponent.cs new file mode 100644 index 00000000000..d8c2d876e7c --- /dev/null +++ b/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultSecretsComponent.cs @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Aspire.Azure.Security.KeyVault; +using Azure.Core; +using Azure.Core.Extensions; +using Azure.Security.KeyVault.Secrets; +using HealthChecks.Azure.KeyVault.Secrets; +using Microsoft.Extensions.Azure; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Diagnostics.HealthChecks; + +namespace Microsoft.Extensions.Hosting; + +internal sealed class AzureKeyVaultSecretsComponent : AbstractAzureKeyVaultComponent +{ + protected override void BindClientOptionsToConfiguration(IAzureClientBuilder clientBuilder, IConfiguration configuration) +#pragma warning disable IDE0200 // Remove unnecessary lambda expression + => clientBuilder.ConfigureOptions(options => configuration.Bind(options)); +#pragma warning restore IDE0200 // Remove unnecessary lambda expression + + protected override void BindSettingsToConfiguration(AzureSecurityKeyVaultSettings settings, IConfiguration configuration) + => configuration.Bind(settings); + + protected override IHealthCheck CreateHealthCheck(SecretClient client, AzureSecurityKeyVaultSettings settings) + => new AzureKeyVaultSecretsHealthCheck(client, new AzureKeyVaultSecretsHealthCheckOptions()); + + internal override SecretClient CreateComponentClient(Uri vaultUri, SecretClientOptions options, TokenCredential cred) + => new(vaultUri, cred, options); +} From 4dcf0c9936ba780b935fbaac534c084eb096fac3 Mon Sep 17 00:00:00 2001 From: James Gould Date: Tue, 29 Apr 2025 17:37:00 +0100 Subject: [PATCH 19/24] Tests corrected after removal of builder pattern --- .../AspireKeyVaultExtensionsTests.cs | 24 ++++++++----------- .../CertificateClientConformanceTests.cs | 6 ++--- .../KeyClientConformanceTests.cs | 6 ++--- 3 files changed, 14 insertions(+), 22 deletions(-) diff --git a/tests/Aspire.Azure.Security.KeyVault.Tests/AspireKeyVaultExtensionsTests.cs b/tests/Aspire.Azure.Security.KeyVault.Tests/AspireKeyVaultExtensionsTests.cs index e3e8f8e5429..682386d3fc9 100644 --- a/tests/Aspire.Azure.Security.KeyVault.Tests/AspireKeyVaultExtensionsTests.cs +++ b/tests/Aspire.Azure.Security.KeyVault.Tests/AspireKeyVaultExtensionsTests.cs @@ -205,7 +205,7 @@ public void CanAddMultipleKeyedServices() } [Fact] - public void CanUseBuilderToAddMultipleClients() + public void CanAddMultipleClientTypes() { var builder = Host.CreateEmptyApplicationBuilder(null); @@ -215,10 +215,9 @@ public void CanUseBuilderToAddMultipleClients() new KeyValuePair($"ConnectionStrings:{connectionName}", ConformanceConstants.VaultUri) ]); - builder - .AddExtendedAzureKeyVaultClient(connectionName) - .AddKeyClient() - .AddCertificateClient(); + builder.AddAzureKeyVaultClient(connectionName); + builder.AddAzureKeyVaultKeyClient(connectionName); + builder.AddAzureKeyVaultCertificateClient(connectionName); using var host = builder.Build(); @@ -234,7 +233,7 @@ public void CanUseBuilderToAddMultipleClients() } [Fact] - public void CanUseBuilderToAddMultipleKeyedClients() + public void CanAddMultipleKeyedClients() { var builder = Host.CreateEmptyApplicationBuilder(null); @@ -253,10 +252,9 @@ public void CanUseBuilderToAddMultipleKeyedClients() new KeyValuePair($"ConnectionStrings:{certClientName}", certClientUri) ]); - builder - .AddExtendedKeyedAzureKeyVaultClient(secretClientName) - .AddKeyedKeyClient(keyClientName) - .AddKeyedCertificateClient(certClientName); + builder.AddKeyedAzureKeyVaultClient(secretClientName); + builder.AddKeyedAzureKeyVaultKeyClient(keyClientName); + builder.AddKeyedAzureKeyVaultCertificateClient(certClientName); using var host = builder.Build(); @@ -299,8 +297,7 @@ public void AddingUnnamedKeyedKeyClientShouldThrow(bool isNull) var name = isNull ? null! : string.Empty; - var action = () => builder.AddExtendedKeyedAzureKeyVaultClient("secrets") - .AddKeyedCertificateClient(name); + var action = () => builder.AddKeyedAzureKeyVaultKeyClient(name); var exception = isNull ? Assert.Throws(action) @@ -318,8 +315,7 @@ public void AddingUnnamedKeyedCertificateClientShouldThrow(bool isNull) var name = isNull ? null! : string.Empty; - var action = () => builder.AddExtendedKeyedAzureKeyVaultClient("secrets") - .AddKeyedCertificateClient(name); + var action = () => builder.AddKeyedAzureKeyVaultCertificateClient(name); var exception = isNull ? Assert.Throws(action) diff --git a/tests/Aspire.Azure.Security.KeyVault.Tests/CertificateClientConformanceTests.cs b/tests/Aspire.Azure.Security.KeyVault.Tests/CertificateClientConformanceTests.cs index 233a01f4883..eb9029e8255 100644 --- a/tests/Aspire.Azure.Security.KeyVault.Tests/CertificateClientConformanceTests.cs +++ b/tests/Aspire.Azure.Security.KeyVault.Tests/CertificateClientConformanceTests.cs @@ -70,13 +70,11 @@ protected override void RegisterComponent(HostApplicationBuilder builder, Action { if (key is null) { - builder.AddExtendedAzureKeyVaultClient("secrets", ConfigureCredentials) - .AddCertificateClient(); + builder.AddAzureKeyVaultCertificateClient("certificates", ConfigureCredentials); } else { - builder.AddExtendedKeyedAzureKeyVaultClient(key, ConfigureCredentials) - .AddKeyedCertificateClient(key); + builder.AddKeyedAzureKeyVaultCertificateClient(key, ConfigureCredentials); } void ConfigureCredentials(AzureSecurityKeyVaultSettings settings) diff --git a/tests/Aspire.Azure.Security.KeyVault.Tests/KeyClientConformanceTests.cs b/tests/Aspire.Azure.Security.KeyVault.Tests/KeyClientConformanceTests.cs index 7d49543c117..141604c562b 100644 --- a/tests/Aspire.Azure.Security.KeyVault.Tests/KeyClientConformanceTests.cs +++ b/tests/Aspire.Azure.Security.KeyVault.Tests/KeyClientConformanceTests.cs @@ -72,13 +72,11 @@ protected override void RegisterComponent(HostApplicationBuilder builder, Action { if (key is null) { - builder.AddExtendedAzureKeyVaultClient("secrets", ConfigureCredentials) - .AddKeyClient(); + builder.AddAzureKeyVaultKeyClient("keys", ConfigureCredentials); } else { - builder.AddExtendedKeyedAzureKeyVaultClient(key, ConfigureCredentials) - .AddKeyedKeyClient(key); + builder.AddKeyedAzureKeyVaultKeyClient(key, ConfigureCredentials); } void ConfigureCredentials(AzureSecurityKeyVaultSettings settings) From 5a5539b95d0eebca5ceaf87ec4992b534e3a24ec Mon Sep 17 00:00:00 2001 From: James Gould Date: Tue, 29 Apr 2025 17:54:54 +0100 Subject: [PATCH 20/24] Added context to IDE0200 warning disable --- .../AzureKeyVaultCertificatesComponent.cs | 2 +- .../AzureKeyVaultKeysComponent.cs | 2 +- .../AzureKeyVaultSecretsComponent.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultCertificatesComponent.cs b/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultCertificatesComponent.cs index 099d9c10670..d0ea01aeb7a 100644 --- a/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultCertificatesComponent.cs +++ b/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultCertificatesComponent.cs @@ -24,7 +24,7 @@ protected override IHealthCheck CreateHealthCheck(CertificateClient client, Azur => throw new NotImplementedException(); protected override void BindClientOptionsToConfiguration(IAzureClientBuilder clientBuilder, IConfiguration configuration) -#pragma warning disable IDE0200 // Remove unnecessary lambda expression +#pragma warning disable IDE0200 // Remove unnecessary lambda expression - needed so the ConfigBinder Source Generator works => clientBuilder.ConfigureOptions(options => configuration.Bind(options)); #pragma warning restore IDE0200 // Remove unnecessary lambda expression diff --git a/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultKeysComponent.cs b/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultKeysComponent.cs index 33525d55950..9318c7986e8 100644 --- a/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultKeysComponent.cs +++ b/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultKeysComponent.cs @@ -20,7 +20,7 @@ internal override KeyClient CreateComponentClient(Uri vaultUri, KeyClientOptions => new(vaultUri, cred, options); protected override void BindClientOptionsToConfiguration(IAzureClientBuilder clientBuilder, IConfiguration configuration) -#pragma warning disable IDE0200 // Remove unnecessary lambda expression +#pragma warning disable IDE0200 // Remove unnecessary lambda expression - needed so the ConfigBinder Source Generator works => clientBuilder.ConfigureOptions(options => configuration.Bind(options)); #pragma warning restore IDE0200 // Remove unnecessary lambda expression diff --git a/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultSecretsComponent.cs b/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultSecretsComponent.cs index d8c2d876e7c..9e56132a53c 100644 --- a/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultSecretsComponent.cs +++ b/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultSecretsComponent.cs @@ -15,7 +15,7 @@ namespace Microsoft.Extensions.Hosting; internal sealed class AzureKeyVaultSecretsComponent : AbstractAzureKeyVaultComponent { protected override void BindClientOptionsToConfiguration(IAzureClientBuilder clientBuilder, IConfiguration configuration) -#pragma warning disable IDE0200 // Remove unnecessary lambda expression +#pragma warning disable IDE0200 // Remove unnecessary lambda expression - needed so the ConfigBinder Source Generator works => clientBuilder.ConfigureOptions(options => configuration.Bind(options)); #pragma warning restore IDE0200 // Remove unnecessary lambda expression From 951e331112403b9d781dcacd7ea83d7c148c2888 Mon Sep 17 00:00:00 2001 From: James Gould Date: Tue, 29 Apr 2025 18:15:07 +0100 Subject: [PATCH 21/24] Update src/Components/Aspire.Azure.Security.KeyVault/AbstractAzureKeyVaultComponent.cs Co-authored-by: Eric Erhardt --- .../AbstractAzureKeyVaultComponent.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Components/Aspire.Azure.Security.KeyVault/AbstractAzureKeyVaultComponent.cs b/src/Components/Aspire.Azure.Security.KeyVault/AbstractAzureKeyVaultComponent.cs index 4a27173f475..ffc8662641b 100644 --- a/src/Components/Aspire.Azure.Security.KeyVault/AbstractAzureKeyVaultComponent.cs +++ b/src/Components/Aspire.Azure.Security.KeyVault/AbstractAzureKeyVaultComponent.cs @@ -6,7 +6,6 @@ using Azure.Core; using Azure.Core.Extensions; using Microsoft.Extensions.Azure; -//using Microsoft.Extensions.Configuration; namespace Aspire.Azure.Security.KeyVault; From 2d5bd2fb23c6e1a4bee5f45cfffcf6b7857f1e24 Mon Sep 17 00:00:00 2001 From: James Gould Date: Tue, 29 Apr 2025 18:15:45 +0100 Subject: [PATCH 22/24] Update src/Components/Aspire.Azure.Security.KeyVault/Aspire.Azure.Security.KeyVault.csproj Co-authored-by: Eric Erhardt --- .../Aspire.Azure.Security.KeyVault.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Components/Aspire.Azure.Security.KeyVault/Aspire.Azure.Security.KeyVault.csproj b/src/Components/Aspire.Azure.Security.KeyVault/Aspire.Azure.Security.KeyVault.csproj index 69a81e90b9e..5f9ec339ce2 100644 --- a/src/Components/Aspire.Azure.Security.KeyVault/Aspire.Azure.Security.KeyVault.csproj +++ b/src/Components/Aspire.Azure.Security.KeyVault/Aspire.Azure.Security.KeyVault.csproj @@ -6,7 +6,7 @@ $(ComponentAzurePackageTags) keyvault secrets A client for Azure Key Vault that integrates with Aspire, including health checks, logging and telemetry. $(SharedDir)AzureKeyVault_256x.png - $(NoWarn);SYSLIB1100;SYSLIB1101; + $(NoWarn);SYSLIB1100;SYSLIB1101 From 4c0d3e1c09618c237e37afb512bd13ba15dbdca4 Mon Sep 17 00:00:00 2001 From: James Gould Date: Tue, 29 Apr 2025 18:19:55 +0100 Subject: [PATCH 23/24] README updated with correct API after changes --- src/Components/Aspire.Azure.Security.KeyVault/README.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/Components/Aspire.Azure.Security.KeyVault/README.md b/src/Components/Aspire.Azure.Security.KeyVault/README.md index ead0aa1ea2a..667c2c6214d 100644 --- a/src/Components/Aspire.Azure.Security.KeyVault/README.md +++ b/src/Components/Aspire.Azure.Security.KeyVault/README.md @@ -59,12 +59,11 @@ See the [Azure.Security.KeyVault.Secrets documentation](https://github.com/Azure ### Optionally include KeyClient and CertificateClient -You can also optionally dependency inject a `KeyClient` and/or `CertificateClient` too: +You can also dependency inject a `KeyClient` and/or `CertificateClient` too: ```csharp -builder.AddAzureKeyVaultClient("secrets") - .AddKeyClient() - .AddCertificateClient(); +builder.AddAzureKeyVaultKeyClient("keys"); +builder.AddAzureKeyVaultCertificateClient("certificates"); ``` Which can then be retrieved in the same way the `SecretClient` is. For example , to retrieve a `KeyClient` from a Web API controller: @@ -78,7 +77,7 @@ public ProductsController(KeyClient client) } ``` -Alternatively to retrieve a `CertificateClient` from a Web API controller: +Or to retrieve a `CertificateClient` from a Web API controller: ```csharp private readonly CertificateClient _client; From 526e23de371a415534b6d589bef95bdb023c3858 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Ros?= Date: Mon, 5 May 2025 09:42:53 -0700 Subject: [PATCH 24/24] Disable health checks for new components --- .../AzureKeyVaultCertificatesComponent.cs | 3 +++ .../AzureKeyVaultKeysComponent.cs | 3 +++ 2 files changed, 6 insertions(+) diff --git a/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultCertificatesComponent.cs b/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultCertificatesComponent.cs index d0ea01aeb7a..33e85dc84c8 100644 --- a/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultCertificatesComponent.cs +++ b/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultCertificatesComponent.cs @@ -20,6 +20,9 @@ internal sealed class AzureKeyVaultCertificatesComponent : AbstractAzureKeyVault internal override CertificateClient CreateComponentClient(Uri vaultUri, CertificateClientOptions options, TokenCredential cred) => new(vaultUri, cred, options); + protected override bool GetHealthCheckEnabled(AzureSecurityKeyVaultSettings settings) + => false; + protected override IHealthCheck CreateHealthCheck(CertificateClient client, AzureSecurityKeyVaultSettings settings) => throw new NotImplementedException(); diff --git a/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultKeysComponent.cs b/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultKeysComponent.cs index 9318c7986e8..2688f890b11 100644 --- a/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultKeysComponent.cs +++ b/src/Components/Aspire.Azure.Security.KeyVault/AzureKeyVaultKeysComponent.cs @@ -13,6 +13,9 @@ namespace Microsoft.Extensions.Hosting; internal sealed class AzureKeyVaultKeysComponent : AbstractAzureKeyVaultComponent { + protected override bool GetHealthCheckEnabled(AzureSecurityKeyVaultSettings settings) + => false; + protected override IHealthCheck CreateHealthCheck(KeyClient client, AzureSecurityKeyVaultSettings settings) => throw new NotImplementedException();