diff --git a/build/ci-build.yml b/build/ci-build.yml index 38152bbb..fd113d28 100644 --- a/build/ci-build.yml +++ b/build/ci-build.yml @@ -106,11 +106,6 @@ stages: inputs: artifact: 'Build' path: '$(Build.SourcesDirectory)' - - template: 'templates/download-hashicorp-vault.yml' - parameters: - targetFolder: '$(Build.SourcesDirectory)' - version: $(HashiCorp.Vault.Version) - vaultBinVariableName: 'Arcus.HashiCorp.VaultBin' - template: templates/run-integration-tests.yml parameters: dockerProjectName: '$(Project).Tests.Runtimes.AzureFunctions' diff --git a/build/nuget-release.yml b/build/nuget-release.yml index af5d1c44..cbf4a070 100644 --- a/build/nuget-release.yml +++ b/build/nuget-release.yml @@ -92,11 +92,6 @@ stages: inputs: artifact: 'Build' path: '$(Build.SourcesDirectory)' - - template: 'templates/download-hashicorp-vault.yml' - parameters: - targetFolder: '$(Build.SourcesDirectory)' - version: $(HashiCorp.Vault.Version) - vaultBinVariableName: 'Arcus.HashiCorp.VaultBin' - template: templates/run-integration-tests.yml parameters: dockerProjectName: '$(Project).Tests.Runtimes.AzureFunctions' diff --git a/build/templates/download-dapr.yml b/build/templates/download-dapr.yml new file mode 100644 index 00000000..c696bfac --- /dev/null +++ b/build/templates/download-dapr.yml @@ -0,0 +1,16 @@ +parameters: + - name: targetFolder + type: string + default: '$(Build.SourcesDirectory)' + - name: daprBinVariableName + type: string + default: 'Arcus.Dapr.DaprBin' + +steps: + - bash: | + wget -q https://raw.githubusercontent.com/dapr/cli/master/install/install.sh -O - | /bin/bash + dapr init + dapr -h + echo "##vso[task.setvariable variable=Arcus.Dapr.DaprBin]dapr" + workingDirectory: ${{ parameters.targetFolder }} + displayName: 'Download Dapr' diff --git a/build/templates/run-integration-tests.yml b/build/templates/run-integration-tests.yml index c32cf21d..c4b0dc05 100644 --- a/build/templates/run-integration-tests.yml +++ b/build/templates/run-integration-tests.yml @@ -29,6 +29,15 @@ steps: imageName: '${{ parameters.dockerProjectName }}:$(Build.BuildId)' containerName: '${{ parameters.dockerProjectName }}' ports: '$(Arcus.AzureFunctions.HttpPort):80' + - template: 'download-dapr.yml' + parameters: + targetFolder: '$(Build.SourcesDirectory)' + daprBinVariableName: 'Arcus.Dapr.DaprBin' + - template: 'download-hashicorp-vault.yml' + parameters: + targetFolder: '$(Build.SourcesDirectory)' + version: $(HashiCorp.Vault.Version) + vaultBinVariableName: 'Arcus.HashiCorp.VaultBin' - template: test/run-integration-tests.yml@templates parameters: dotnetSdkVersion: '$(DotNet.Sdk.Version)' @@ -41,4 +50,4 @@ steps: docker logs ${{ parameters.dockerProjectName }} failOnStderr: true displayName: Show ${{ parameters.dockerProjectName }} logs - condition: always() \ No newline at end of file + condition: always() diff --git a/docs/preview/03-Features/secret-store/provider/dapr-secret-store.md b/docs/preview/03-Features/secret-store/provider/dapr-secret-store.md new file mode 100644 index 00000000..80c9feaf --- /dev/null +++ b/docs/preview/03-Features/secret-store/provider/dapr-secret-store.md @@ -0,0 +1,154 @@ +--- +title: "Dapr secret provider" +layout: default +--- + +# Dapr secret provider +Dapr secret provider brings secrets from the Dapr secret store to your application. Dapr is commonly used in Kubernetes environments where there is usually not the same network capabilities as other application environments. +By using this secret provider, you still benefit from all the Arcus secret store features, while still using Dapr as your external secret source. + +⛔ Does not support [synchronous secret retrieval](../../secrets/general.md). + +## Installation +Using the Dapr secrets building block with Arcus requires the following package: + +```shell +PM > Install-Package Arcus.Security.Providers.Dapr +``` + +## Configuration +After installing the package, the extensions methods for using the Dapr components becomes available when building the secret store. + +```csharp +using Microsoft.Extensions.Hosting; + +public class Program +{ + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) + { + return Host.CreateDefaultBuilder(args).ConfigureSecretStore((context, config, builder) => + { + // Adding the Dapr secret provider with the built-in overloads. + builder.AddDaprSecretStore( + // Name of the secret store where Dapr gets its secrets. + secretStore: "mycustomsecretstore", + // Following defaults can be overridden: + configureOptions: options => + { + // The URI endpoint to use for gRPC calls to the Dapr runtime. + // The default value will be http://127.0.0.1:DAPR_GRPC_PORT where DAPR_GRPC_PORT represents the value of the DAPR_GRPC_PORT environment variable. + options.GrpcEndpoint = "http://127.0.0.1:5001/"; + + // The URI endpoint to use for HTTP calls to the Dapr runtime. + // The default value will be http://127.0.0.1:DAPR_HTTP_PORT where DAPR_HTTP_PORT represents the value of the DAPR_HTTP_PORT environment variable. + options.HttpEndpoint = "http://127.0.0.1:5002"; + + // The API token on every request to the Dapr runtime (added to the request's headers). + options.DaprApiToken = "my-api-key"; + + // Tracking the Dapr secret store dependency which works well together with Application Insights (default: `false`). + // See https://observability.arcus-azure.net/features/writing-different-telemetry-types#measuring-custom-dependencies for more information. + options.TrackDependency = true; + + // Additional metadata entry which will be sent to the Dapr secret store on every request. + options.AddMetadata("my-dapr-key", "my-dapr-value"); + }); + }); + } +} +``` + +### Custom implementation +We allow custom implementations of the Dapr secret provider. +This can come in handy when you want to perform additional actions during the secret retrieval. + +**Example** +In this example we'll create a custom implementation for the local Dapr secret store that allows multi-valued secrets. +First, we'll implement the `DaprSecretProvider`: + +```csharp +using Arcus.Security.Providers.Dapr; + +public class MultiValuedLocalDaprSecretProvider : DaprSecretProvider +{ + public MultiValuedLocalDaprSecretProvider( + string secretStore, + DaprSecretProviderOptions options, + ILogger logger) : base(secretStore, options, logger) + { + } +} +``` + +👀 Notice that we require to take in the name of the Dapr secret store and the additional user-defined options which can be configured during the registration of the secret provider. + +To control how Dapr secrets be retrieved, we need to implement the `DetermineDaprSecretName` method which takes in the secret name like it comes into the secret provider, and implement the multi-valued implementation: + +```csharp +using Arcus.Security.Providers.Dapr; + +public class MultiValuedLocalDaprSecretProvider : DaprSecretProvider +{ + // Constructor truncated... + + /// + /// Determine the Dapr secret key and section based on the user passed-in . + /// + /// + /// The key of the secret in the Dapr secret store can be the same as the section for single-valued Dapr secrets, but is different in multi-valued Dapr secrets. + /// Therefore, make sure to split the into the required (key, section) pair for your use-case. + /// + /// The user passed-in secret which gets translated to a Dapr secret key and section. + protected override (string daprSecretKey, string daprSecretSection) DetermineDaprSecretName(string secretName) + { + const string nestedSeparator = ":"; + + string[] subKeys = secretName.Split(nestedSeparator, StringSplitOptions.RemoveEmptyEntries); + if (subKeys.Length >= 2) + { + string remaining = string.Join(nestedSeparator, subKeys.Skip(1)); + return (subKeys[0], remaining); + } + + return (secretName, secretName); + } +} +``` + +> 💡 Dapr allows for multi-valued secrets for the local Dapr secret store. This means that while single-valued secrets have the same 'key' as 'section' in the returned dictionary, multi-valued secrets are retrieved differently. For more information on the Dapr .NET SDK, see [their official documentation](https://docs.dapr.io/developing-applications/sdks/dotnet/). + +Such a custom implementation can easily be registered with a dedicated extension on the secret store: + +```csharp +using Microsoft.Extensions.Hosting; + +public class Program +{ + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) + { + return Host.CreateDefaultBuilder(args).ConfigureSecretStore((config, context, stores) => + { + stores.AddDaprSecretStore( + (IServiceProvider provider, DaprSecretProviderOptions options) => + { + var logger = provider.GetService>(); + return new MultiValuedLocalDaprSecretProvider("mycustomsecretstore", options, logger); + }, + (DaprSecretProviderOptions options) => + { + // Configure additional options which can be passed in the implementation factory function of the custom implementation. + }); + }); + } +} +``` \ No newline at end of file diff --git a/src/Arcus.Security.Providers.Dapr/Arcus.Security.Providers.Dapr.csproj b/src/Arcus.Security.Providers.Dapr/Arcus.Security.Providers.Dapr.csproj new file mode 100644 index 00000000..42aec4b5 --- /dev/null +++ b/src/Arcus.Security.Providers.Dapr/Arcus.Security.Providers.Dapr.csproj @@ -0,0 +1,34 @@ + + + + net6.0 + Arcus + Provides support for Dapr Secrets with Arcus Secret Store + Copyright (c) Arcus + https://security.arcus-azure.net/ + https://github.com/arcus-azure/arcus.security + LICENSE + icon.png + Git + README.md + Kubernetes;Secrets;Dapr + true + true + true + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Arcus.Security.Providers.Dapr/DaprSecretProvider.cs b/src/Arcus.Security.Providers.Dapr/DaprSecretProvider.cs new file mode 100644 index 00000000..cc4782e0 --- /dev/null +++ b/src/Arcus.Security.Providers.Dapr/DaprSecretProvider.cs @@ -0,0 +1,137 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Arcus.Observability.Telemetry.Core; +using Arcus.Security.Core; +using Dapr.Client; +using GuardNet; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; + +namespace Arcus.Security.Providers.Dapr +{ + /// + /// Represents an retrieving secrets from the Dapr secret store. + /// + public class DaprSecretProvider : ISecretProvider + { + /// + /// Initializes a new instance of the class. + /// + /// The name of the Dapr secret store from which the secrets should be retrieved from. + /// The optional set of options to manipulate the basic behavior of how the secrets should be retrieved. + /// The logger instance to write diagnostic trace messages during the retrieval of the Dapr secrets. + /// Thrown when the is blank. + public DaprSecretProvider(string secretStore, DaprSecretProviderOptions options, ILogger logger) + { + Guard.NotNullOrWhitespace(secretStore, nameof(secretStore)); + + SecretStore = secretStore; + Options = options ?? new DaprSecretProviderOptions(); + Logger = logger ?? NullLogger.Instance; + } + + /// + /// Gets the name of the Dapr secret store for which this secret provider is configured. + /// + protected string SecretStore { get; } + + /// + /// Gets the optional set of configured options to manipulated the basic behavior of how the secrets should be retrieved. + /// + /// + /// Options set when configuring this secret provider in the secret store. + /// + protected DaprSecretProviderOptions Options { get; } + + + /// + /// Gets the logger instance to write diagnostic trace messages during the Dapr secret retrieval. + /// + protected ILogger Logger { get; } + + /// + /// Retrieves the secret value, based on the given name. + /// + /// The name of the secret key. + /// Returns a that contains the secret key. + /// The must not be empty. + /// The must not be null. + /// The secret was not found, using the given name. + public async Task GetSecretAsync(string secretName) + { + Guard.NotNullOrWhitespace(secretName, nameof(secretName)); + + string secretValue = await GetRawSecretAsync(secretName); + return new Secret(secretValue); + } + + /// + /// Retrieves the secret value, based on the given name. + /// + /// The name of the secret key. + /// Returns the secret key. + /// The must not be empty. + /// The must not be null. + /// The secret was not found, using the given name. + public async Task GetRawSecretAsync(string secretName) + { + Guard.NotNullOrWhitespace(secretName, nameof(secretName)); + + Logger.LogTrace("Getting a secret '{SecretName}' from Dapr secret store '{StoreName}'...", secretName, SecretStore); + + (string daprSecretName, string daprSecretSection) = DetermineDaprSecretName(secretName); + string secretValue = await GetDaprSecretValueAsync(daprSecretName, daprSecretSection); + + Logger.LogTrace("Got secret '{SecretName}' from from Dapr secret store '{StoreName}'", secretName, SecretStore); + return secretValue; + } + + /// + /// Determine the Dapr secret key and section based on the user passed-in . + /// + /// + /// The key of the secret in the Dapr secret store can be the same as the section for single-valued Dapr secrets, but is different in multi-valued Dapr secrets. + /// Therefore, make sure to split the into the required (key, section) pair for your use-case. + /// + /// The user passed-in secret which gets translated to a Dapr secret key and section. + protected virtual (string daprSecretKey, string daprSecretSection) DetermineDaprSecretName(string secretName) + { + return (secretName, secretName); + } + + private async Task GetDaprSecretValueAsync(string daprSecretName, string daprSecretSection) + { + Guard.NotNullOrWhitespace(daprSecretName, nameof(daprSecretName)); + Guard.NotNullOrWhitespace(daprSecretSection, nameof(daprSecretSection)); + + using var measurement = DurationMeasurement.Start(); + bool isSuccessful = false; + + try + { + using DaprClient client = Options.CreateClient(); + Dictionary daprSecrets = await client.GetSecretAsync(SecretStore, daprSecretName); + + if (!daprSecrets.TryGetValue(daprSecretSection, out string secretValue)) + { + throw new SecretNotFoundException(daprSecretSection); + } + + isSuccessful = true; + return secretValue; + } + finally + { + if (Options.TrackDependency) + { + Logger.LogDependency("Dapr secret store", daprSecretName, isSuccessful, measurement, new Dictionary + { + ["SecretStore"] = SecretStore, + ["SecretSection"] = daprSecretSection + }); + } + } + } + } +} diff --git a/src/Arcus.Security.Providers.Dapr/DaprSecretProviderOptions.cs b/src/Arcus.Security.Providers.Dapr/DaprSecretProviderOptions.cs new file mode 100644 index 00000000..66230e4c --- /dev/null +++ b/src/Arcus.Security.Providers.Dapr/DaprSecretProviderOptions.cs @@ -0,0 +1,125 @@ +using System; +using System.Collections.Generic; +using Dapr.Client; +using GuardNet; + +namespace Arcus.Security.Providers.Dapr +{ + /// + /// Represents the available options for the . + /// + public class DaprSecretProviderOptions + { + private string _grpcEndpoint, _httpEndpoint, _daprApiToken; + + /// + /// Gets the optional metadata to be sent together with the Dapr runtime upon each secret retrieval. + /// + internal IDictionary Metadata { get; } = new Dictionary(); + + /// + /// Overrides the gRPC endpoint used by for communicating with the Dapr runtime. + /// + /// + /// The URI endpoint to use for gRPC calls to the Dapr runtime. The default value will be + /// http://127.0.0.1:DAPR_GRPC_PORT where DAPR_GRPC_PORT represents the value of the + /// DAPR_GRPC_PORT environment variable. + /// + /// Thrown when the is blank. + public string GrpcEndpoint + { + get => _grpcEndpoint; + set + { + Guard.NotNullOrWhitespace(value, nameof(value)); + _grpcEndpoint = value; + } + } + + /// + /// Overrides the HTTP endpoint used by for communicating with the Dapr runtime. + /// + /// + /// The URI endpoint to use for HTTP calls to the Dapr runtime. The default value will be + /// http://127.0.0.1:DAPR_HTTP_PORT where DAPR_HTTP_PORT represents the value of the + /// DAPR_HTTP_PORT environment variable. + /// + /// Thrown when the is blank. + public string HttpEndpoint + { + get => _httpEndpoint; + set + { + Guard.NotNullOrWhitespace(value, nameof(value)); + _httpEndpoint = value; + } + } + + /// + /// Adds a API token on every request to the Dapr runtime. + /// + /// Thrown when the is blank. + public string DaprApiToken + { + get => _daprApiToken; + set + { + Guard.NotNullOrWhitespace(value, nameof(value)); + _daprApiToken = value; + } + } + + /// + /// Gets or sets the flag to indicate whether or not the should track the Dapr dependency. + /// + public bool TrackDependency { get; set; } = false; + + /// + /// Adds an optional metadata entry which will be sent to the Dapr secret store. + /// + /// The unique metadata key. + /// The metadata value for the . + /// + /// Thrown when the or is blank, + /// or when there already exists a metadata entry for the same . + /// + public void AddMetadata(string key, string value) + { + Guard.NotNullOrWhitespace(key, nameof(key)); + Guard.NotNullOrWhitespace(value, nameof(value)); + + if (Metadata.ContainsKey(key)) + { + throw new ArgumentException( + $"Cannot add metadata entry because there already exists an entry with key '{key}'", nameof(key)); + } + + Metadata.Add(key, value); + } + + /// + /// Creates an based on the previously configured options. + /// + internal DaprClient CreateClient() + { + var builder = new DaprClientBuilder(); + + if (!string.IsNullOrWhiteSpace(_grpcEndpoint)) + { + builder.UseGrpcEndpoint(_grpcEndpoint); + } + + if (!string.IsNullOrWhiteSpace(_daprApiToken)) + { + builder.UseDaprApiToken(_daprApiToken); + } + + if (!string.IsNullOrWhiteSpace(_httpEndpoint)) + { + builder.UseHttpEndpoint(_httpEndpoint); + } + + return builder.Build(); + } + } +} diff --git a/src/Arcus.Security.Providers.Dapr/Extensions/SecretStoreBuilderExtensions.cs b/src/Arcus.Security.Providers.Dapr/Extensions/SecretStoreBuilderExtensions.cs new file mode 100644 index 00000000..3099adf9 --- /dev/null +++ b/src/Arcus.Security.Providers.Dapr/Extensions/SecretStoreBuilderExtensions.cs @@ -0,0 +1,98 @@ +using System; +using Arcus.Security.Providers.Dapr; +using GuardNet; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +// ReSharper disable once CheckNamespace +namespace Microsoft.Extensions.Hosting +{ + /// + /// Extensions on the to add the secrets from the Dapr secret store. + /// + public static class SecretStoreBuilderExtensions + { + /// + /// Adds the secrets from the Dapr secret store. + /// + /// The builder instance to add the secret source to. + /// The name of the Dapr secret store to include in the secret store. + /// Thrown when the is null. + /// Thrown when the is blank. + public static SecretStoreBuilder AddDaprSecretStore( + this SecretStoreBuilder builder, + string secretStore) + { + Guard.NotNull(builder, nameof(builder)); + Guard.NotNullOrWhitespace(secretStore, nameof(secretStore)); + + return AddDaprSecretStore(builder, secretStore, configureOptions: null); + } + + /// + /// Adds the secrets from the Dapr secret store. + /// + /// The builder instance to add the secret source to. + /// The name of the Dapr secret store to include in the secret store. + /// The function to create an optional set of options to manipulate the basic behavior of how the secrets should be retrieved. + /// Thrown when the is null. + /// Thrown when the is blank. + public static SecretStoreBuilder AddDaprSecretStore( + this SecretStoreBuilder builder, + string secretStore, + Action configureOptions) + { + Guard.NotNull(builder, nameof(builder)); + Guard.NotNullOrWhitespace(secretStore, nameof(secretStore)); + + return AddDaprSecretStore(builder, (provider, options) => + { + var logger = provider.GetService>(); + return new DaprSecretProvider(secretStore, options, logger); + }, configureOptions); + } + + /// + /// Adds the secrets from the Dapr secret store. + /// + /// The custom implementation of the . + /// The builder instance to add the secret source to. + /// The function to create a custom instance of the . + /// Thrown when the is null. + public static SecretStoreBuilder AddDaprSecretStore( + this SecretStoreBuilder builder, + Func implementationFactory) + where TCustom : DaprSecretProvider + { + Guard.NotNull(builder, nameof(builder)); + Guard.NotNull(implementationFactory, nameof(implementationFactory)); + + return builder.AddProvider(implementationFactory, configureOptions: null); + } + + /// + /// Adds the secrets from the Dapr secret store. + /// + /// The custom implementation of the . + /// The builder instance to add the secret source to. + /// The function to create a custom instance of the . + /// The function to create an optional set of options to manipulate the basic behavior of how the secrets should be retrieved. + /// Thrown when the is null. + public static SecretStoreBuilder AddDaprSecretStore( + this SecretStoreBuilder builder, + Func implementationFactory, + Action configureOptions) + where TCustom : DaprSecretProvider + { + Guard.NotNull(builder, nameof(builder)); + Guard.NotNull(implementationFactory, nameof(implementationFactory)); + + var options = new DaprSecretProviderOptions(); + configureOptions?.Invoke(options); + + return builder.AddProvider( + provider => implementationFactory(provider, options), + configureOptions: null); + } + } +} diff --git a/src/Arcus.Security.Tests.Integration/Arcus.Security.Tests.Integration.csproj b/src/Arcus.Security.Tests.Integration/Arcus.Security.Tests.Integration.csproj index f5486c2f..13bb6634 100644 --- a/src/Arcus.Security.Tests.Integration/Arcus.Security.Tests.Integration.csproj +++ b/src/Arcus.Security.Tests.Integration/Arcus.Security.Tests.Integration.csproj @@ -9,6 +9,7 @@ + @@ -17,12 +18,14 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + + @@ -34,6 +37,16 @@ Always + + Always + + + Always + + + + + diff --git a/src/Arcus.Security.Tests.Integration/Dapr/DaprSecretProviderTests.cs b/src/Arcus.Security.Tests.Integration/Dapr/DaprSecretProviderTests.cs new file mode 100644 index 00000000..c625dece --- /dev/null +++ b/src/Arcus.Security.Tests.Integration/Dapr/DaprSecretProviderTests.cs @@ -0,0 +1,145 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Arcus.Security.Core; +using Arcus.Security.Providers.Dapr; +using Arcus.Security.Tests.Integration.Dapr.Hosting; +using Arcus.Security.Tests.Integration.Dapr.Resources.Local; +using Arcus.Security.Tests.Integration.KeyVault.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json.Linq; +using Xunit; +using Xunit.Abstractions; + +namespace Arcus.Security.Tests.Integration.Dapr +{ + public class DaprSecretProviderTests : IntegrationTest + { + /// + /// Initializes a new instance of the class. + /// + public DaprSecretProviderTests(ITestOutputHelper outputWriter) : base(outputWriter) + { + } + + [Fact] + public async Task Dapr_WithAzureKeyVault_Succeeds() + { + // Arrange + KeyVaultConfig keyVaultConfig = Configuration.GetKeyVaultConfig(); + await using DaprSidecarFixture sideCar = await StartSideCarAsync(opt => + { + opt.LoadKeyVault(keyVaultConfig); + }); + + ISecretProvider provider = sideCar.GetSecretProvider(); + Assert.NotNull(await provider.GetRawSecretAsync(keyVaultConfig.SecretName)); + Assert.NotNull((await provider.GetSecretAsync(keyVaultConfig.SecretName)).Value); + } + + [Fact] + public async Task Dapr_WithCustomMultiValuedLocal_Succeeds() + { + // Arrange + var secretStore = JObject.Parse(@"{ + ""redisPassword"": ""your redis password"", + ""connectionStrings"": { + ""mySql"": { + ""user"": ""your username"", + ""pass"": ""your password"" + } + } + }"); + + await using DaprSidecarFixture sideCar = await StartSideCarAsync(opt => + { + opt.LoadSecrets(secretStore); + }); + + ISecretProvider provider = sideCar.GetSecretProvider((serviceProvider, secretOptions, fixtureOptions) => + { + return new MultiValuedLocalDaprSecretProvider( + fixtureOptions.StoreName, + secretOptions, + serviceProvider.GetService>()); + }); + + // Act + string actual = await provider.GetRawSecretAsync("connectionStrings:mySql:pass"); + + // Assert + Assert.Equal("your password", actual); + } + + [Fact] + public async Task Dapr_WithKnownLocalSecretName_Succeeds() + { + // Arrange + var secretStore = JObject.Parse(@"{ + ""redisPassword"": ""your redis password"" + }"); + + await using DaprSidecarFixture sideCar = await StartSideCarAsync(opt => + { + opt.LoadSecrets(secretStore); + }); + + ISecretProvider provider = sideCar.GetSecretProvider(); + + // Act / Assert + string secretName = "redisPassword"; + string expected = "your redis password"; + Assert.Equal(expected, await provider.GetRawSecretAsync(secretName)); + Assert.Equal(expected, (await provider.GetSecretAsync(secretName)).Value); + } + + [Fact] + public async Task Dapr_WithUnknownLocalSecretName_Fails() + { + // Arrange + var secretStore = JObject.Parse(@"{ + ""redisPassword"": ""your redis password"" + }"); + + await using DaprSidecarFixture sideCar = await StartSideCarAsync(opt => + { + opt.LoadSecrets(secretStore); + }); + + ISecretProvider provider = sideCar.GetSecretProvider(); + + // Act / Assert + string secretName = "unknownPass"; + await Assert.ThrowsAsync(() => provider.GetSecretAsync(secretName)); + await Assert.ThrowsAsync(() => provider.GetRawSecretAsync(secretName)); + } + + [Theory] + [InlineData(false, 0)] + [InlineData(true, 1)] + public async Task Dapr_WithTrackDependency_Succeeds(bool trackDependency, int expectedDependencyTracked) + { + // Arrange + await using DaprSidecarFixture sideCar = await StartSideCarAsync(opt => + { + opt.LoadSecrets(JObject.Parse(@"{ ""myPass"": ""your password"" }")); + }); + + ISecretProvider provider = sideCar.GetSecretProvider(opt => opt.TrackDependency = trackDependency, SerilogLogger); + + // Act + string actual = await provider.GetRawSecretAsync("myPass"); + + // Assert + Assert.Equal("your password", actual); + Assert.Equal(expectedDependencyTracked, + InMemoryLogSink.CurrentLogEmits.Count(ev => ev.MessageTemplate.Text.Contains("Dependency"))); + } + + private async Task StartSideCarAsync(Action configureOptions) + { + return await DaprSidecarFixture.StartSideCarAsync(Configuration, Logger, configureOptions); + } + } +} diff --git a/src/Arcus.Security.Tests.Integration/Dapr/Hosting/DaprSidecarFixture.cs b/src/Arcus.Security.Tests.Integration/Dapr/Hosting/DaprSidecarFixture.cs new file mode 100644 index 00000000..79b2bdf4 --- /dev/null +++ b/src/Arcus.Security.Tests.Integration/Dapr/Hosting/DaprSidecarFixture.cs @@ -0,0 +1,255 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics; +using System.IO; +using System.Threading.Tasks; +using Arcus.Security.Core; +using Arcus.Security.Providers.Dapr; +using Arcus.Security.Tests.Integration.Fixture; +using Arcus.Testing.Logging; +using Dapr.Client; +using GuardNet; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Polly; +using Serilog; +using Xunit; +using ILogger = Microsoft.Extensions.Logging.ILogger; + +namespace Arcus.Security.Tests.Integration.Dapr.Hosting +{ + /// + /// Represents the available teardown options for the . + /// + [Flags] + public enum TearDownOptions + { + /// + /// De-activate any additional teardown functionality. + /// + None = 0, + + /// + /// Logs the standard output console of the Dapr Sidecar service to the test output during teardown. + /// + LogDaprOutput = 1 + } + + /// + /// Represents a test fixture that runs the Dapr Sidecar as a temporary service. + /// + public sealed class DaprSidecarFixture : IAsyncDisposable + { + private readonly Process _process; + private readonly DaprSidecarOptions _options; + private readonly ILogger _logger; + private readonly ICollection _disposables = new Collection(); + + private DaprSidecarFixture(Process process, int port, DaprSidecarOptions options, ILogger logger) + { + Guard.NotNull(process, nameof(process)); + Guard.NotLessThan(port, 0, nameof(port)); + + _process = process; + _options = options; + _logger = logger ?? NullLogger.Instance; + + Endpoint = new Uri($"http://127.0.0.1:{port}/"); + } + + /// + /// Gets the GRPC endpoint where the Dapr Sidecar is hosted. + /// + public Uri Endpoint { get; } + + /// + /// Gets or sets the options to manipulate the teardown process of the test fixture. + /// + public TearDownOptions TearDownOptions { get; set; } = TearDownOptions.LogDaprOutput; + + /// + /// Starts a new as a temporary service with the provided user-defined options. + /// + /// The integration test configuration to retrieve the required hosting-related values. + /// The logger instance to write diagnostic trace messages during the startup and teardown of the test fixture. + /// The user-defined options to configure the Dapr Sidecar. + /// Thrown when the is null. + public static async Task StartSideCarAsync(TestConfig configuration, ILogger logger, Action configureOptions) + { + Guard.NotNull(configuration, nameof(configuration)); + logger ??= NullLogger.Instance; + int port = 60002; + + var options = new DaprSidecarOptions(); + configureOptions?.Invoke(options); + options.WriteSecretStoreConfigToDisk(); + + Process process = CreateProcess(configuration, port, options); + var fixture = new DaprSidecarFixture(process, port, options, logger); + + await fixture.StartProcessAsync(); + return fixture; + } + + private static Process CreateProcess(TestConfig configuration, int port, DaprSidecarOptions options) + { + string daprExeFileName = configuration.GetDaprInstallationFileName(); + + string vaultArgs = string.Join(" ", + "run", + $"--resources-path {nameof(Dapr)}/Resources/{options.StoreType}", + $"--app-id arcus-security-dapr --app-port 6002 --dapr-http-port 3601 --dapr-grpc-port {port}", + "--log-level debug"); + + var startInfo = new ProcessStartInfo(daprExeFileName, vaultArgs) + { + WorkingDirectory = Directory.GetCurrentDirectory(), + UseShellExecute = false, + CreateNoWindow = true, + RedirectStandardOutput = true, + RedirectStandardError = true + }; + + return new Process { StartInfo = startInfo }; + } + + private async Task StartProcessAsync() + { + _logger.LogTrace("Starting Dapr Sidecar: {FileName} {Arguments}", _process.StartInfo.FileName, _process.StartInfo.Arguments); + + bool isStarted = _process.Start(); + if (!isStarted) + { + throw new InvalidOperationException( + "Cannot correctly start Dapr Sidecar process due to an unexpected failure, please make sure to check if the provided arguments are correct"); + } + + PolicyResult healthResult = + await Policy.TimeoutAsync(TimeSpan.FromSeconds(30)) + .WrapAsync(Policy.Handle() + .WaitAndRetryForeverAsync(_ => TimeSpan.FromMilliseconds(100))) + .ExecuteAndCaptureAsync(async () => + { + _logger.LogTrace("Checking Dapr Sidecar availability..."); + + using var client = new DaprClientBuilder().UseGrpcEndpoint(Endpoint.OriginalString).Build(); + await client.GetMetadataAsync(); + }); + + if (healthResult.Outcome is OutcomeType.Failure) + { + _logger.LogError("Failed to correctly start Dapr Sidecar"); + await StopProcessAsync(); + + throw new TimeoutException( + "Could not correctly start Dapr Sidecar because the sidecar did not respond with a healthy response in the expected time frame", healthResult.FinalException); + } + + await Task.Delay(TimeSpan.FromSeconds(2)); + _logger.LogTrace("Dapr Sidecar started at {Endpoint}!", Endpoint); + } + + /// + /// Gets a registered based on the current Dapr sidecar configuration. + /// + /// The function to configure additional values on the . + /// The optional logger to include while registering the secret provider. + public ISecretProvider GetSecretProvider( + Action configureProvider = null, + Serilog.ILogger logger = null) + { + return CreateSecretProvider(stores => stores.AddDaprSecretStore(_options.StoreName, options => + { + options.GrpcEndpoint = Endpoint.ToString(); + configureProvider?.Invoke(options); + }), logger); + } + + /// + /// Gets a registered based on the current Dapr sidecar configuration. + /// + /// The function to create a custom . + /// The function to configure additional values on the . + /// The optional logger to include while registering the secret provider. + public ISecretProvider GetSecretProvider( + Func implementationFactory, + Action configureProvider = null, + Serilog.ILogger logger = null) + where TCustom : DaprSecretProvider + { + return CreateSecretProvider(stores => + { + stores.AddDaprSecretStore( + (provider, opt) => implementationFactory(provider, opt, _options), + options => + { + options.GrpcEndpoint = Endpoint.ToString(); + configureProvider?.Invoke(options); + }); + }, logger); + } + + private ISecretProvider CreateSecretProvider( + Action configureSecretStore, + Serilog.ILogger logger) + { + var builder = new HostBuilder(); + if (logger != null) + { + builder.UseSerilog(logger); + } + + builder.ConfigureLogging(logging => + { + logging.SetMinimumLevel(LogLevel.Trace); + logging.AddProvider(new CustomLoggerProvider(_logger)); + }); + + builder.ConfigureSecretStore((_, stores) => + { + configureSecretStore(stores); + }); + + IHost host = builder.Build(); + _disposables.Add(host); + + IServiceProvider serviceProvider = host.Services; + return serviceProvider.GetRequiredService(); + } + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources asynchronously. + /// + /// A task that represents the asynchronous dispose operation. + public async ValueTask DisposeAsync() + { + await StopProcessAsync(); + Assert.All(_disposables, d => d.Dispose()); + } + + private async Task StopProcessAsync() + { + if (!_process.HasExited) + { + _process.Kill(entireProcessTree: true); + } + + if (TearDownOptions.HasFlag(TearDownOptions.LogDaprOutput)) + { + _logger.LogTrace("Dapr Sidecar read standard output..."); + string output = await _process.StandardOutput.ReadToEndAsync(); + _logger.LogDebug("Dapr Sidecar output: {Output}", output); + + _logger.LogTrace("Dapr Sidecar read standard error..."); + string error = await _process.StandardError.ReadToEndAsync(); + _logger.LogDebug("Dapr Sidecar error: {Error}", error); + } + + _logger.LogTrace("Stop Dapr Sidecar at {Endpoint}", Endpoint); + _process.Dispose(); + } + } +} diff --git a/src/Arcus.Security.Tests.Integration/Dapr/Hosting/DaprSidecarOptions.cs b/src/Arcus.Security.Tests.Integration/Dapr/Hosting/DaprSidecarOptions.cs new file mode 100644 index 00000000..a2ced2af --- /dev/null +++ b/src/Arcus.Security.Tests.Integration/Dapr/Hosting/DaprSidecarOptions.cs @@ -0,0 +1,107 @@ +using System; +using System.IO; +using Arcus.Security.Tests.Integration.Fixture; +using Arcus.Security.Tests.Integration.KeyVault.Configuration; +using GuardNet; +using Newtonsoft.Json.Linq; + +namespace Arcus.Security.Tests.Integration.Dapr.Hosting +{ + public enum DaprStoreType { None, Local, AzureKeyVault } + + /// + /// Represents the available user options for the . + /// + public class DaprSidecarOptions + { + private JObject _startObject; + private KeyVaultConfig _config; + + /// + /// Gets the type of the secret source from where the Dapr runtime will get its secrets. + /// + public DaprStoreType StoreType { get; private set; } + + /// + /// Gets the name of the Dapr secret store from where the Dapr runtime will gets its secrets. + /// + /// Thrown when no Dapr secret store type was configured. + public string StoreName + { + get + { + switch (StoreType) + { + case DaprStoreType.Local: return "localsecretstore"; + case DaprStoreType.AzureKeyVault: return "azurekeyvault"; + default: + throw new ArgumentOutOfRangeException(nameof(StoreType), StoreType, "Unknown Dapr store type"); + } + } + } + + /// + /// Use Azure Key Vault to load the secrets into the Dapr secret store. + /// + /// The integration test configuration to load the authentication values to interact with Azure Key Vault. + /// Thrown when the is null. + /// Thrown when this method is called after the Dapr Sidecar was already configured with a secret source. + public DaprSidecarOptions LoadKeyVault(KeyVaultConfig configuration) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.For(() => StoreType is not DaprStoreType.None, $"Cannot load Azure Key Vault secrets into the Dapr secret store because the Dapr secret store was already configured: {StoreType}"); + + _config = configuration; + StoreType = DaprStoreType.AzureKeyVault; + + return this; + } + + /// + /// Loads a JSON representing a local secret set of the Dapr secret store. + /// + /// The Dapr local secret store. + /// Thrown when the is null. + /// Thrown when this method is called after the Dapr Sidecar was already configured with a secret source. + public DaprSidecarOptions LoadSecrets(JObject secretStore) + { + Guard.NotNull(secretStore, nameof(secretStore)); + Guard.For(() => StoreType is not DaprStoreType.None, $"Cannot load Azure Key Vault secrets into the Dapr secret store because the Dapr secret store was already configured: {StoreType}"); + + _startObject = secretStore; + StoreType = DaprStoreType.Local; + + return this; + } + + /// + /// Writes the previously configured Dapr secret store configuration to disk. + /// + /// Thrown when no Dapr secret store type was configured. + internal void WriteSecretStoreConfigToDisk() + { + switch (StoreType) + { + case DaprStoreType.Local: + JObject json = _startObject ?? new JObject(); + string secretsPath = Path.Combine(Directory.GetCurrentDirectory(), "secrets.json"); + File.WriteAllText(secretsPath, json.ToString()); + break; + + case DaprStoreType.AzureKeyVault: + string storePath = Path.Combine(Directory.GetCurrentDirectory(), nameof(Dapr), "Resources", StoreType.ToString(), "az-keyvault-secret-store.yaml"); + string contents = File.ReadAllText(storePath); + + File.WriteAllText(storePath, + contents.Replace("[your_service_principal_tenant_id]", _config.Azure.TenantId) + .Replace("[your_service_principal_app_id]", _config.ServicePrincipal.ClientId) + .Replace("[your_keyvault_name]", _config.VaultName) + .Replace("[your_service_principal_app_secret]", _config.ServicePrincipal.ClientSecret)); + break; + + default: + throw new ArgumentOutOfRangeException(nameof(StoreType), StoreType, "Unknown Dapr store type"); + } + } + } +} \ No newline at end of file diff --git a/src/Arcus.Security.Tests.Integration/Dapr/Hosting/TestConfigExtensions.cs b/src/Arcus.Security.Tests.Integration/Dapr/Hosting/TestConfigExtensions.cs new file mode 100644 index 00000000..5f4cfcb5 --- /dev/null +++ b/src/Arcus.Security.Tests.Integration/Dapr/Hosting/TestConfigExtensions.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using System.IO; +using GuardNet; +using Microsoft.Extensions.Configuration; + +// ReSharper disable once CheckNamespace +namespace Arcus.Security.Tests.Integration.Fixture +{ + /// + /// Extensions on the for easier access to the Dapr-related information. + /// + public static class TestConfigExtensions + { + /// + /// Gets the local file path of the Dapr installation on this system where the tests are run. + /// + /// The integration test configuration. + /// Thrown when the is null. + /// Thrown when there is no Dapr installation file path present in the application settings. + /// Thrown when a Dapr installation on the system where the tests are running cannot be found. + public static string GetDaprInstallationFileName(this TestConfig configuration) + { + Guard.NotNull(configuration, nameof(configuration)); + + var key = "Arcus:Dapr:DaprBin"; + string fileName = configuration.GetValue(key); + + if (string.IsNullOrWhiteSpace(fileName)) + { + throw new KeyNotFoundException( + "Could not find the installation file path of the Dapr Sidecar in the local app settings" + + "please install the Dapr Sidecar on this machine (https://docs.dapr.io/getting-started/install-dapr-cli/) " + + $"and add the installation folder as configuration key '{key}' to your local app settings"); + } + + if (fileName.StartsWith("#{") && fileName.EndsWith("}#")) + { + throw new KeyNotFoundException( + $"Could not find the installation file path of the Dapr Sidecar in the local app settings because the appsettings token '{fileName}' is not yet replaced," + + "please install the Dapr Sidecar on this machine (https://docs.dapr.io/getting-started/install-dapr-cli/) " + + $"and add the installation folder as configuration key '{key}' to your local app settings"); + } + + return fileName; + } + } +} diff --git a/src/Arcus.Security.Tests.Integration/Dapr/Resources/AzureKeyVault/az-keyvault-secret-store.yaml b/src/Arcus.Security.Tests.Integration/Dapr/Resources/AzureKeyVault/az-keyvault-secret-store.yaml new file mode 100644 index 00000000..b8f44109 --- /dev/null +++ b/src/Arcus.Security.Tests.Integration/Dapr/Resources/AzureKeyVault/az-keyvault-secret-store.yaml @@ -0,0 +1,17 @@ +apiVersion: dapr.io/v1alpha1 +kind: Component +metadata: + name: azurekeyvault +spec: + type: secretstores.azure.keyvault + version: v1 + metadata: + - name: vaultName # Required + value: [your_keyvault_name] + # See authentication section below for all options + - name: azureTenantId + value: "[your_service_principal_tenant_id]" + - name: azureClientId + value: "[your_service_principal_app_id]" + - name: azureClientSecret + value: "[your_service_principal_app_secret]" diff --git a/src/Arcus.Security.Tests.Integration/Dapr/Resources/Local/MultiValuedLocalDaprSecretProvider.cs b/src/Arcus.Security.Tests.Integration/Dapr/Resources/Local/MultiValuedLocalDaprSecretProvider.cs new file mode 100644 index 00000000..f31f1756 --- /dev/null +++ b/src/Arcus.Security.Tests.Integration/Dapr/Resources/Local/MultiValuedLocalDaprSecretProvider.cs @@ -0,0 +1,44 @@ +using System; +using System.Linq; +using Arcus.Security.Providers.Dapr; +using Microsoft.Extensions.Logging; + +namespace Arcus.Security.Tests.Integration.Dapr.Resources.Local +{ + /// + /// Represents a local variant of the that supports multi-valued secrets. + /// + public class MultiValuedLocalDaprSecretProvider : DaprSecretProvider + { + /// + public MultiValuedLocalDaprSecretProvider( + string secretStore, + DaprSecretProviderOptions options, + ILogger logger) + : base(secretStore, options, logger) + { + } + + /// + /// Determine the Dapr secret key and section based on the user passed-in . + /// + /// + /// The key of the secret in the Dapr secret store can be the same as the section for single-valued Dapr secrets, but is different in multi-valued Dapr secrets. + /// Therefore, make sure to split the into the required (key, section) pair for your use-case. + /// + /// The user passed-in secret which gets translated to a Dapr secret key and section. + protected override (string daprSecretKey, string daprSecretSection) DetermineDaprSecretName(string secretName) + { + const string nestedSeparator = ":"; + + string[] subKeys = secretName.Split(nestedSeparator, StringSplitOptions.RemoveEmptyEntries); + if (subKeys.Length >= 2) + { + string remaining = string.Join(nestedSeparator, subKeys.Skip(1)); + return (subKeys[0], remaining); + } + + return (secretName, secretName); + } + } +} diff --git a/src/Arcus.Security.Tests.Integration/Dapr/Resources/Local/local-secret-store.yaml b/src/Arcus.Security.Tests.Integration/Dapr/Resources/Local/local-secret-store.yaml new file mode 100644 index 00000000..320f2e89 --- /dev/null +++ b/src/Arcus.Security.Tests.Integration/Dapr/Resources/Local/local-secret-store.yaml @@ -0,0 +1,15 @@ +apiVersion: dapr.io/v1alpha1 +kind: Component +metadata: + name: localsecretstore + namespace: default +spec: + type: secretstores.local.file + version: v1 + metadata: + - name: secretsFile + value: secrets.json + - name: nestedSeparator + value: ":" + - name: multiValued + value: true \ No newline at end of file diff --git a/src/Arcus.Security.Tests.Integration/HashiCorp/SecretStoreBuilderExtensionsTests.cs b/src/Arcus.Security.Tests.Integration/HashiCorp/SecretStoreBuilderExtensionsTests.cs index 1a2a0bf4..60a2ece1 100644 --- a/src/Arcus.Security.Tests.Integration/HashiCorp/SecretStoreBuilderExtensionsTests.cs +++ b/src/Arcus.Security.Tests.Integration/HashiCorp/SecretStoreBuilderExtensionsTests.cs @@ -52,7 +52,7 @@ public async Task AuthenticateWithInvalidUserPassPasswordKeyValue_GetSecret_Fail const string policyName = "my-policy"; var builder = new HostBuilder(); - builder.UseSerilog(Logger); + builder.UseSerilog(SerilogLogger); using (var server = await HashiCorpVaultTestServer.StartServerAsync(_config, _logger)) { @@ -104,7 +104,7 @@ public async Task AuthenticateWithUnauthorizedUserPassUserKeyValue_GetSecret_Fai const string policyName = "my-policy"; var builder = new HostBuilder(); - builder.UseSerilog(Logger); + builder.UseSerilog(SerilogLogger); using (var server = await HashiCorpVaultTestServer.StartServerAsync(_config, _logger)) { diff --git a/src/Arcus.Security.Tests.Integration/IntegrationTest.cs b/src/Arcus.Security.Tests.Integration/IntegrationTest.cs index 22202c39..0397b3ca 100644 --- a/src/Arcus.Security.Tests.Integration/IntegrationTest.cs +++ b/src/Arcus.Security.Tests.Integration/IntegrationTest.cs @@ -3,6 +3,7 @@ using Arcus.Testing.Logging.Extensions; using Arcus.Testing.Logging; using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; using Serilog; using Serilog.Configuration; using Serilog.Core; @@ -15,7 +16,8 @@ public class IntegrationTest : IDisposable private bool _disposed; protected TestConfig Configuration { get; } - protected Logger Logger { get; } + protected Microsoft.Extensions.Logging.ILogger Logger { get; } + protected Logger SerilogLogger { get; } protected InMemoryLogSink InMemoryLogSink { get; } public IntegrationTest(ITestOutputHelper testOutput) @@ -25,11 +27,13 @@ public IntegrationTest(ITestOutputHelper testOutput) InMemoryLogSink = new InMemoryLogSink(); var configuration = new LoggerConfiguration() + .MinimumLevel.Verbose() .WriteTo.XunitTestLogging(testOutput) .WriteTo.Sink(InMemoryLogSink) .WriteTo.AzureApplicationInsights(Configuration.GetValue("Arcus:ApplicationInsights:InstrumentationKey")); - Logger = configuration.CreateLogger(); + Logger = new XunitTestLogger(testOutput); + SerilogLogger = configuration.CreateLogger(); } /// @@ -53,7 +57,7 @@ public void Dispose() /// protected virtual void Dispose(bool disposing) { - Logger.Dispose(); + SerilogLogger.Dispose(); } } } \ No newline at end of file diff --git a/src/Arcus.Security.Tests.Integration/KeyVault/Configuration/KeyVaultConfig.cs b/src/Arcus.Security.Tests.Integration/KeyVault/Configuration/KeyVaultConfig.cs new file mode 100644 index 00000000..78958902 --- /dev/null +++ b/src/Arcus.Security.Tests.Integration/KeyVault/Configuration/KeyVaultConfig.cs @@ -0,0 +1,81 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Arcus.Security.Tests.Integration.Fixture; +using Google.Api; +using Google.Protobuf.WellKnownTypes; + +namespace Arcus.Security.Tests.Integration.KeyVault.Configuration +{ + public static class TestConfigExtensions + { + public static KeyVaultConfig GetKeyVaultConfig(this TestConfig configuration) + { + return new KeyVaultConfig( + configuration.GetRequiredValue("Arcus:KeyVault:Uri"), + configuration.GetRequiredValue("Arcus:KeyVault:TestKeyName"), + new AzureEnvironmentConfig( + configuration.GetTenantId()), + new ServicePrincipalConfig( + configuration.GetServicePrincipalClientId(), + configuration.GetServicePrincipalClientSecret())); + } + } + + public class KeyVaultConfig + { + /// + /// Initializes a new instance of the class. + /// + public KeyVaultConfig( + string vaultUri, + string secretName, + AzureEnvironmentConfig environment, + ServicePrincipalConfig servicePrincipal) + { + VaultUri = vaultUri; + VaultName = new Uri(VaultUri).Host.Replace(".vault.azure.net", ""); + SecretName = secretName; + + Azure = environment; + ServicePrincipal = servicePrincipal; + } + + public string VaultName { get; } + public string VaultUri { get; } + public string SecretName { get; } + + public AzureEnvironmentConfig Azure { get; } + public ServicePrincipalConfig ServicePrincipal { get; } + } + + public class AzureEnvironmentConfig + { + /// + /// Initializes a new instance of the class. + /// + public AzureEnvironmentConfig(string tenantId) + { + TenantId = tenantId; + } + + public string TenantId { get; } + } + + public class ServicePrincipalConfig + { + /// + /// Initializes a new instance of the class. + /// + public ServicePrincipalConfig(string clientId, string clientSecret) + { + ClientId = clientId; + ClientSecret = clientSecret; + } + + public string ClientId { get; } + public string ClientSecret { get; } + } +} diff --git a/src/Arcus.Security.Tests.Integration/KeyVault/KeyVaultCachedSecretProviderTests.cs b/src/Arcus.Security.Tests.Integration/KeyVault/KeyVaultCachedSecretProviderTests.cs index e3fe5323..525fb56b 100644 --- a/src/Arcus.Security.Tests.Integration/KeyVault/KeyVaultCachedSecretProviderTests.cs +++ b/src/Arcus.Security.Tests.Integration/KeyVault/KeyVaultCachedSecretProviderTests.cs @@ -5,6 +5,7 @@ using Arcus.Security.Providers.AzureKeyVault.Authentication; using Arcus.Security.Providers.AzureKeyVault.Configuration; using Arcus.Security.Tests.Core.Fixture; +using Arcus.Security.Tests.Integration.KeyVault.Configuration; using Azure.Core; using Azure.Identity; using Azure.Security.KeyVault.Secrets; @@ -28,23 +29,23 @@ public KeyVaultCachedSecretProviderTests(ITestOutputHelper testOutput) : base(te public async Task KeyVaultSecretProvider_StoreSecret_Succeeds() { // Arrange - var keyVaultUri = Configuration.GetValue("Arcus:KeyVault:Uri"); - string tenantId = Configuration.GetTenantId(); - string clientId = Configuration.GetServicePrincipalClientId(); - string clientKey = Configuration.GetServicePrincipalClientSecret(); - + var keyVault = Configuration.GetKeyVaultConfig(); + string clientId = keyVault.ServicePrincipal.ClientId; + var secretName = $"Test-Secret-{Guid.NewGuid()}"; var secretValue = Guid.NewGuid().ToString(); - using (TemporaryEnvironmentVariable.Create(Constants.AzureTenantIdEnvironmentVariable, tenantId)) + using (TemporaryEnvironmentVariable.Create(Constants.AzureTenantIdEnvironmentVariable, keyVault.Azure.TenantId)) using (TemporaryEnvironmentVariable.Create(Constants.AzureServicePrincipalClientIdVariable, clientId)) - using (TemporaryEnvironmentVariable.Create(Constants.AzureServicePrincipalClientSecretVariable, clientKey)) + using (TemporaryEnvironmentVariable.Create(Constants.AzureServicePrincipalClientSecretVariable, keyVault.ServicePrincipal.ClientSecret)) { - var tokenCredential = new ChainedTokenCredential(new ManagedIdentityCredential(clientId), new EnvironmentCredential()); - var keyVaultSecretProvider = new KeyVaultSecretProvider( - tokenCredential: tokenCredential, - vaultConfiguration: new KeyVaultConfiguration(keyVaultUri)); - var cachedSecretProvider = new KeyVaultCachedSecretProvider(keyVaultSecretProvider); + var tokenCredential = new ChainedTokenCredential( + new ManagedIdentityCredential(clientId), + new EnvironmentCredential()); + + var cachedSecretProvider = new KeyVaultCachedSecretProvider( + new KeyVaultSecretProvider( + tokenCredential, new KeyVaultConfiguration(keyVault.VaultUri))); try { @@ -62,7 +63,7 @@ public async Task KeyVaultSecretProvider_StoreSecret_Succeeds() } finally { - var client = new SecretClient(new Uri(keyVaultUri), tokenCredential); + var client = new SecretClient(new Uri(keyVault.VaultUri), tokenCredential); await client.StartDeleteSecretAsync(secretName); } } diff --git a/src/Arcus.Security.Tests.Integration/KeyVault/KeyVaultSecretProviderTests.cs b/src/Arcus.Security.Tests.Integration/KeyVault/KeyVaultSecretProviderTests.cs index 826b8f8e..9754535b 100644 --- a/src/Arcus.Security.Tests.Integration/KeyVault/KeyVaultSecretProviderTests.cs +++ b/src/Arcus.Security.Tests.Integration/KeyVault/KeyVaultSecretProviderTests.cs @@ -7,6 +7,7 @@ using Arcus.Security.Providers.AzureKeyVault.Authentication; using Arcus.Security.Providers.AzureKeyVault.Configuration; using Arcus.Security.Tests.Core.Fixture; +using Arcus.Security.Tests.Integration.KeyVault.Configuration; using Azure.Identity; using Azure.Security.KeyVault.Secrets; using Microsoft.Extensions.Configuration; @@ -27,11 +28,12 @@ public KeyVaultSecretProviderTests(ITestOutputHelper testOutput) : base(testOutp { } - private string TenantId => Configuration.GetTenantId(); - private string ClientId => Configuration.GetRequiredValue("Arcus:ServicePrincipal:ApplicationId"); - private string ClientSecret => Configuration.GetRequiredValue("Arcus:ServicePrincipal:AccessKey"); - private string TestSecretName => Configuration.GetRequiredValue("Arcus:KeyVault:TestKeyName"); - private string VaultUri => Configuration.GetRequiredValue("Arcus:KeyVault:Uri"); + private KeyVaultConfig KeyVault => Configuration.GetKeyVaultConfig(); + private string TenantId => KeyVault.Azure.TenantId; + private string ClientId => KeyVault.ServicePrincipal.ClientId; + private string ClientSecret => KeyVault.ServicePrincipal.ClientSecret; + private string TestSecretName => KeyVault.SecretName; + private string VaultUri => KeyVault.VaultUri; private string TestSecretVersion => Configuration.GetRequiredValue("Arcus:KeyVault:TestKeyVersion"); [Fact] diff --git a/src/Arcus.Security.Tests.Integration/KeyVault/SecretStoreBuilderExtensionsTests.cs b/src/Arcus.Security.Tests.Integration/KeyVault/SecretStoreBuilderExtensionsTests.cs index 3062cd91..1ba0e60f 100644 --- a/src/Arcus.Security.Tests.Integration/KeyVault/SecretStoreBuilderExtensionsTests.cs +++ b/src/Arcus.Security.Tests.Integration/KeyVault/SecretStoreBuilderExtensionsTests.cs @@ -1,13 +1,13 @@ -using System; -using System.Collections.Generic; -using System.Linq; +using System.Linq; using System.Net; using System.Threading.Tasks; using Arcus.Security.Core; using Arcus.Security.Tests.Core.Fixture; using Arcus.Security.Tests.Integration.Fixture; +using Arcus.Security.Tests.Integration.KeyVault.Configuration; using Azure; using Azure.Identity; +using Microsoft.AspNetCore.Http; using Microsoft.Azure.KeyVault.Models; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -26,22 +26,25 @@ public SecretStoreBuilderExtensionsTests(ITestOutputHelper testOutput) : base(te { } + private KeyVaultConfig KeyVault => Configuration.GetKeyVaultConfig(); + private string TenantId => KeyVault.Azure.TenantId; + private string ClientId => KeyVault.ServicePrincipal.ClientId; + private string ClientSecret => KeyVault.ServicePrincipal.ClientSecret; + private string TestSecretName => KeyVault.SecretName; + private string VaultUri => KeyVault.VaultUri; + private string MsiConnectionString => Configuration.GetValue("Arcus:MSI:AzureServicesAuth:ConnectionString"); + [Fact] public async Task AddAzureKeyVault_WithServicePrincipal_GetSecretSucceeds() { // Arrange - string applicationId = Configuration.GetValue("Arcus:ServicePrincipal:ApplicationId"); - var clientKey = Configuration.GetValue("Arcus:ServicePrincipal:AccessKey"); - var keyVaultUri = Configuration.GetValue("Arcus:KeyVault:Uri"); - var keyName = Configuration.GetValue("Arcus:KeyVault:TestKeyName"); - var builder = new HostBuilder(); - builder.UseSerilog(Logger, dispose: true); + builder.UseSerilog(SerilogLogger, dispose: true); // Act - builder.ConfigureSecretStore((config, stores) => + builder.ConfigureSecretStore((_, stores) => { - stores.AddAzureKeyVaultWithServicePrincipal(keyVaultUri, applicationId, clientKey); + stores.AddAzureKeyVaultWithServicePrincipal(VaultUri, ClientId, ClientSecret); }); // Assert @@ -49,8 +52,8 @@ public async Task AddAzureKeyVault_WithServicePrincipal_GetSecretSucceeds() { var provider = host.Services.GetRequiredService(); - AssertNotNullSecret(await provider.GetSecretAsync(keyName)); - AssertNotNullSecret(await provider.GetRawSecretAsync(keyName)); + AssertNotNullSecret(await provider.GetSecretAsync(TestSecretName)); + AssertNotNullSecret(await provider.GetRawSecretAsync(TestSecretName)); } } @@ -72,18 +75,13 @@ private void AssertNotNullSecret(string secretValue) public async Task AddAzureKeyVaultWithDependencyTracking_WithServicePrincipal_GetSecretSucceeds(bool trackDependency, int expectedTrackedDependencies) { // Arrange - string applicationId = Configuration.GetValue("Arcus:ServicePrincipal:ApplicationId"); - var clientKey = Configuration.GetValue("Arcus:ServicePrincipal:AccessKey"); - var keyVaultUri = Configuration.GetValue("Arcus:KeyVault:Uri"); - var keyName = Configuration.GetValue("Arcus:KeyVault:TestKeyName"); - var builder = new HostBuilder(); - builder.UseSerilog(Logger, dispose: true); + builder.UseSerilog(SerilogLogger, dispose: true); // Act - builder.ConfigureSecretStore((config, stores) => + builder.ConfigureSecretStore((_, stores) => { - stores.AddAzureKeyVaultWithServicePrincipalWithOptions(keyVaultUri, applicationId, clientKey, + stores.AddAzureKeyVaultWithServicePrincipalWithOptions(VaultUri, ClientId, ClientSecret, configureOptions: options => options.TrackDependency = trackDependency); }); @@ -92,8 +90,8 @@ public async Task AddAzureKeyVaultWithDependencyTracking_WithServicePrincipal_Ge { var provider = host.Services.GetRequiredService(); - AssertNotNullSecret(await provider.GetSecretAsync(keyName)); - AssertNotNullSecret(await provider.GetRawSecretAsync(keyName)); + AssertNotNullSecret(await provider.GetSecretAsync(TestSecretName)); + AssertNotNullSecret(await provider.GetRawSecretAsync(TestSecretName)); } AssertTrackedAzureKeyVaultDependency(expectedTrackedDependencies); @@ -103,22 +101,19 @@ public async Task AddAzureKeyVaultWithDependencyTracking_WithServicePrincipal_Ge public async Task AddAzureKeyVault_WithServicePrincipal_GetSecretFails() { // Arrange - string applicationId = Configuration.GetValue("Arcus:ServicePrincipal:ApplicationId"); - var clientKey = Configuration.GetValue("Arcus:ServicePrincipal:AccessKey"); - var keyVaultUri = Configuration.GetValue("Arcus:KeyVault:Uri"); - var keyName = "UnknownSecretName"; - var builder = new HostBuilder(); // Act - builder.ConfigureSecretStore((config, stores) => + builder.ConfigureSecretStore((_, stores) => { - stores.AddAzureKeyVaultWithServicePrincipal(keyVaultUri, applicationId, clientKey); + stores.AddAzureKeyVaultWithServicePrincipal(VaultUri, ClientId, ClientSecret); }); // Assert using IHost host = builder.Build(); var provider = host.Services.GetRequiredService(); + + var keyName = "UnknownSecretName"; Assert.Throws(() => provider.GetSecret(keyName)); Assert.Throws(() => provider.GetRawSecret(keyName)); await Assert.ThrowsAsync(() => provider.GetSecretAsync(keyName)); @@ -131,18 +126,13 @@ public async Task AddAzureKeyVault_WithServicePrincipal_GetSecretFails() public async Task AddAzureKeyVaultWithDependencyTracking_WithServicePrincipal_GetSecretFails(bool trackDependency, int expectedTrackedDependencies) { // Arrange - string applicationId = Configuration.GetValue("Arcus:ServicePrincipal:ApplicationId"); - var clientKey = Configuration.GetValue("Arcus:ServicePrincipal:AccessKey"); - var keyVaultUri = Configuration.GetValue("Arcus:KeyVault:Uri"); - var keyName = "UnknownSecretName"; - var builder = new HostBuilder(); - builder.UseSerilog(Logger, dispose: true); + builder.UseSerilog(SerilogLogger, dispose: true); // Act - builder.ConfigureSecretStore((config, stores) => + builder.ConfigureSecretStore((_, stores) => { - stores.AddAzureKeyVaultWithServicePrincipalWithOptions(keyVaultUri, applicationId, clientKey, + stores.AddAzureKeyVaultWithServicePrincipalWithOptions(VaultUri, ClientId, ClientSecret, configureOptions: options => options.TrackDependency = trackDependency); }); @@ -150,6 +140,8 @@ public async Task AddAzureKeyVaultWithDependencyTracking_WithServicePrincipal_Ge using (IHost host = builder.Build()) { var provider = host.Services.GetRequiredService(); + + var keyName = "UnknownSecretName"; await Assert.ThrowsAsync(() => provider.GetSecretAsync(keyName)); await Assert.ThrowsAsync(() => provider.GetRawSecretAsync(keyName)); } @@ -162,25 +154,20 @@ public async Task AddAzureKeyVaultWithDependencyTracking_WithServicePrincipal_Ge public async Task AddAzureKeyVault_WithServicePrincipalToDots_GetsSecretSucceeds() { // Arrange - string applicationId = Configuration.GetValue("Arcus:ServicePrincipal:ApplicationId"); - var clientKey = Configuration.GetValue("Arcus:ServicePrincipal:AccessKey"); - var keyVaultUri = Configuration.GetValue("Arcus:KeyVault:Uri"); - var keyName = Configuration.GetValue("Arcus:KeyVault:TestKeyName"); - var builder = new HostBuilder(); // Act - builder.ConfigureSecretStore((config, stores) => + builder.ConfigureSecretStore((_, stores) => { stores.AddAzureKeyVaultWithServicePrincipalWithOptions( - keyVaultUri, applicationId, clientKey, mutateSecretName: secretName => secretName.Remove(0, 5)); + VaultUri, ClientId, ClientSecret, mutateSecretName: secretName => secretName.Remove(0, 5)); }); // Assert using IHost host = builder.Build(); var provider = host.Services.GetRequiredService(); - string appendedKeyName = "Test-" + keyName; + string appendedKeyName = "Test-" + TestSecretName; AssertNotNullSecret(await provider.GetSecretAsync(appendedKeyName)); AssertNotNullSecret(await provider.GetRawSecretAsync(appendedKeyName)); } @@ -189,74 +176,61 @@ public async Task AddAzureKeyVault_WithServicePrincipalToDots_GetsSecretSucceeds public async Task AddAzureKeyVault_WithServicePrincipalWrongMutation_GetsSecretsFails() { // Arrange - string applicationId = Configuration.GetValue("Arcus:ServicePrincipal:ApplicationId"); - var clientKey = Configuration.GetValue("Arcus:ServicePrincipal:AccessKey"); - var keyVaultUri = Configuration.GetValue("Arcus:KeyVault:Uri"); - var keyName = Configuration.GetValue("Arcus:KeyVault:TestKeyName"); - var builder = new HostBuilder(); // Act - builder.ConfigureSecretStore((config, stores) => + builder.ConfigureSecretStore((_, stores) => { stores.AddAzureKeyVaultWithServicePrincipalWithOptions( - keyVaultUri, applicationId, clientKey, mutateSecretName: secretName => "SOMETHING-WRONG-" + secretName, allowCaching: false); + VaultUri, ClientId, ClientSecret, mutateSecretName: secretName => "SOMETHING-WRONG-" + secretName, allowCaching: false); }); // Assert using IHost host = builder.Build(); var provider = host.Services.GetRequiredService(); - Assert.Throws(() => provider.GetSecret(keyName)); - Assert.Throws(() => provider.GetRawSecret(keyName)); - await Assert.ThrowsAsync(() => provider.GetSecretAsync(keyName)); - await Assert.ThrowsAsync(() => provider.GetRawSecretAsync(keyName)); + Assert.Throws(() => provider.GetSecret(TestSecretName)); + Assert.Throws(() => provider.GetRawSecret(TestSecretName)); + await Assert.ThrowsAsync(() => provider.GetSecretAsync(TestSecretName)); + await Assert.ThrowsAsync(() => provider.GetRawSecretAsync(TestSecretName)); } [Fact] public async Task AddAzureKeyVault_WithCachedServicePrincipal_GetSecretSucceeds() { // Arrange - string applicationId = Configuration.GetValue("Arcus:ServicePrincipal:ApplicationId"); - var clientKey = Configuration.GetValue("Arcus:ServicePrincipal:AccessKey"); - var keyVaultUri = Configuration.GetValue("Arcus:KeyVault:Uri"); - var keyName = Configuration.GetValue("Arcus:KeyVault:TestKeyName"); - var builder = new HostBuilder(); // Act - builder.ConfigureSecretStore((config, stores) => + builder.ConfigureSecretStore((_, stores) => { - stores.AddAzureKeyVaultWithServicePrincipal(keyVaultUri, applicationId, clientKey, allowCaching: true); + stores.AddAzureKeyVaultWithServicePrincipal(VaultUri, ClientId, ClientSecret, allowCaching: true); }); // Assert using IHost host = builder.Build(); var provider = host.Services.GetRequiredService(); - AssertNotNullSecret(await provider.GetSecretAsync(keyName)); - AssertNotNullSecret(await provider.GetRawSecretAsync(keyName)); + AssertNotNullSecret(await provider.GetSecretAsync(TestSecretName)); + AssertNotNullSecret(await provider.GetRawSecretAsync(TestSecretName)); } [Fact] public async Task AddAzureKeyVault_WithCachedServicePrincipal_GetSecretFails() { // Arrange - string applicationId = Configuration.GetValue("Arcus:ServicePrincipal:ApplicationId"); - var clientKey = Configuration.GetValue("Arcus:ServicePrincipal:AccessKey"); - var keyVaultUri = Configuration.GetValue("Arcus:KeyVault:Uri"); - var keyName = "UnknownSecretName"; - var builder = new HostBuilder(); // Act - builder.ConfigureSecretStore((config, stores) => + builder.ConfigureSecretStore((_, stores) => { - stores.AddAzureKeyVaultWithServicePrincipal(keyVaultUri, applicationId, clientKey, allowCaching: true); + stores.AddAzureKeyVaultWithServicePrincipal(VaultUri, ClientId, ClientSecret, allowCaching: true); }); // Assert using IHost host = builder.Build(); var provider = host.Services.GetRequiredService(); + + var keyName = "UnknownSecretName"; Assert.Throws(() => provider.GetSecret(keyName)); Assert.Throws(() => provider.GetRawSecret(keyName)); await Assert.ThrowsAsync(() => provider.GetSecretAsync(keyName)); @@ -267,25 +241,20 @@ public async Task AddAzureKeyVault_WithCachedServicePrincipal_GetSecretFails() public async Task AddAzureKeyVault_WithCachedServicePrincipalRemovesPrefix_GetsSecretsSucceeds() { // Arrange - string applicationId = Configuration.GetValue("Arcus:ServicePrincipal:ApplicationId"); - var clientKey = Configuration.GetValue("Arcus:ServicePrincipal:AccessKey"); - var keyVaultUri = Configuration.GetValue("Arcus:KeyVault:Uri"); - var keyName = Configuration.GetValue("Arcus:KeyVault:TestKeyName"); - var builder = new HostBuilder(); // Act - builder.ConfigureSecretStore((config, stores) => + builder.ConfigureSecretStore((_, stores) => { stores.AddAzureKeyVaultWithServicePrincipalWithOptions( - keyVaultUri, applicationId, clientKey, allowCaching: true, mutateSecretName: secretName => secretName.Remove(0, 5)); + VaultUri, ClientId, ClientSecret, allowCaching: true, mutateSecretName: secretName => secretName.Remove(0, 5)); }); // Assert using IHost host = builder.Build(); var provider = host.Services.GetRequiredService(); - string appendedKeyName = "Test-" + keyName; + string appendedKeyName = "Test-" + TestSecretName; AssertNotNullSecret(await provider.GetSecretAsync(appendedKeyName)); AssertNotNullSecret(await provider.GetRawSecretAsync(appendedKeyName)); } @@ -294,48 +263,39 @@ public async Task AddAzureKeyVault_WithCachedServicePrincipalRemovesPrefix_GetsS public async Task AddAzureKeyVault_WithCachedServicePrincipalWrongMutation_GetsSecretFails() { // Arrange - string applicationId = Configuration.GetValue("Arcus:ServicePrincipal:ApplicationId"); - var clientKey = Configuration.GetValue("Arcus:ServicePrincipal:AccessKey"); - var keyVaultUri = Configuration.GetValue("Arcus:KeyVault:Uri"); - var keyName = Configuration.GetValue("Arcus:KeyVault:TestKeyName"); - var builder = new HostBuilder(); // Act - builder.ConfigureSecretStore((config, stores) => + builder.ConfigureSecretStore((_, stores) => { stores.AddAzureKeyVaultWithServicePrincipalWithOptions( - keyVaultUri, applicationId, clientKey, allowCaching: true, mutateSecretName: secretName => "SOMETHING-WRONG-" + secretName); + VaultUri, ClientId, ClientSecret, allowCaching: true, mutateSecretName: secretName => "SOMETHING-WRONG-" + secretName); }); // Assert using IHost host = builder.Build(); var provider = host.Services.GetRequiredService(); - Assert.Throws(() => provider.GetSecret(keyName)); - Assert.Throws(() => provider.GetRawSecret(keyName)); - await Assert.ThrowsAsync(() => provider.GetSecretAsync(keyName)); - await Assert.ThrowsAsync(() => provider.GetRawSecretAsync(keyName)); + Assert.Throws(() => provider.GetSecret(TestSecretName)); + Assert.Throws(() => provider.GetRawSecret(TestSecretName)); + await Assert.ThrowsAsync(() => provider.GetSecretAsync(TestSecretName)); + await Assert.ThrowsAsync(() => provider.GetRawSecretAsync(TestSecretName)); } [Fact] public async Task AddAzureKeyVault_WithManagedServiceIdentity_GetSecretSucceeds() { // Arrange - var keyVaultUri = Configuration.GetValue("Arcus:KeyVault:Uri"); - var connectionString = Configuration.GetValue("Arcus:MSI:AzureServicesAuth:ConnectionString"); - var keyName = Configuration.GetValue("Arcus:KeyVault:TestKeyName"); - var builder = new HostBuilder(); // Act - builder.ConfigureSecretStore((config, stores) => stores.AddAzureKeyVaultWithManagedServiceIdentity(keyVaultUri, connectionString)); + builder.ConfigureSecretStore((_, stores) => stores.AddAzureKeyVaultWithManagedServiceIdentity(VaultUri, MsiConnectionString)); // Assert using IHost host = builder.Build(); var provider = host.Services.GetRequiredService(); - AssertNotNullSecret(await provider.GetSecretAsync(keyName)); - AssertNotNullSecret(await provider.GetRawSecretAsync(keyName)); + AssertNotNullSecret(await provider.GetSecretAsync(TestSecretName)); + AssertNotNullSecret(await provider.GetRawSecretAsync(TestSecretName)); } [Theory] @@ -344,15 +304,11 @@ public async Task AddAzureKeyVault_WithManagedServiceIdentity_GetSecretSucceeds( public async Task AddAzureKeyVaultWithOptions_WithManagedServiceIdentity_GetSecretSucceeds(bool trackDependency, int expectedTrackedDependencies) { // Arrange - var keyVaultUri = Configuration.GetValue("Arcus:KeyVault:Uri"); - var connectionString = Configuration.GetValue("Arcus:MSI:AzureServicesAuth:ConnectionString"); - var keyName = Configuration.GetValue("Arcus:KeyVault:TestKeyName"); - var builder = new HostBuilder(); - builder.UseSerilog(Logger, dispose: true); + builder.UseSerilog(SerilogLogger, dispose: true); // Act - builder.ConfigureSecretStore((config, stores) => stores.AddAzureKeyVaultWithManagedServiceIdentityWithOptions(keyVaultUri, connectionString, + builder.ConfigureSecretStore((_, stores) => stores.AddAzureKeyVaultWithManagedServiceIdentityWithOptions(VaultUri, MsiConnectionString, configureOptions: options => options.TrackDependency = trackDependency)); // Assert @@ -360,8 +316,8 @@ public async Task AddAzureKeyVaultWithOptions_WithManagedServiceIdentity_GetSecr { var provider = host.Services.GetRequiredService(); - AssertNotNullSecret(await provider.GetSecretAsync(keyName)); - AssertNotNullSecret(await provider.GetRawSecretAsync(keyName)); + AssertNotNullSecret(await provider.GetSecretAsync(TestSecretName)); + AssertNotNullSecret(await provider.GetRawSecretAsync(TestSecretName)); } AssertTrackedAzureKeyVaultDependency(expectedTrackedDependencies); @@ -371,22 +327,19 @@ public async Task AddAzureKeyVaultWithOptions_WithManagedServiceIdentity_GetSecr public async Task AddAzureKeyVault_WithManagedServiceIdentity_GetSecretFails() { // Arrange - var keyVaultUri = Configuration.GetValue("Arcus:KeyVault:Uri"); - var connectionString = Configuration.GetValue("Arcus:MSI:AzureServicesAuth:ConnectionString"); - var keyName = "UnknownSecretName"; - var builder = new HostBuilder(); // Act - builder.ConfigureSecretStore((config, stores) => + builder.ConfigureSecretStore((_, stores) => { - stores.AddAzureKeyVaultWithManagedServiceIdentity(keyVaultUri, connectionString); + stores.AddAzureKeyVaultWithManagedServiceIdentity(VaultUri, MsiConnectionString); }); // Assert using IHost host = builder.Build(); var provider = host.Services.GetRequiredService(); + var keyName = "UnknownSecretName"; await Assert.ThrowsAsync(() => provider.GetSecretAsync(keyName)); await Assert.ThrowsAsync(() => provider.GetRawSecretAsync(keyName)); } @@ -395,24 +348,20 @@ public async Task AddAzureKeyVault_WithManagedServiceIdentity_GetSecretFails() public async Task AddAzureKeyVault_WithManagedServiceIdentityRemovesPrefix_GetsSecretSucceeds() { // Arrange - var keyVaultUri = Configuration.GetValue("Arcus:KeyVault:Uri"); - var connectionString = Configuration.GetValue("Arcus:MSI:AzureServicesAuth:ConnectionString"); - var keyName = Configuration.GetValue("Arcus:KeyVault:TestKeyName"); - var builder = new HostBuilder(); // Act - builder.ConfigureSecretStore((config, stores) => + builder.ConfigureSecretStore((_, stores) => { stores.AddAzureKeyVaultWithManagedServiceIdentityWithOptions( - keyVaultUri, connectionString, mutateSecretName: secretName => secretName.Remove(0, 5)); + VaultUri, MsiConnectionString, mutateSecretName: secretName => secretName.Remove(0, 5)); }); // Assert using IHost host = builder.Build(); var provider = host.Services.GetRequiredService(); - string appendedKeyName = "Test-" + keyName; + string appendedKeyName = "Test-" + TestSecretName; AssertNotNullSecret(await provider.GetSecretAsync(appendedKeyName)); AssertNotNullSecret(await provider.GetRawSecretAsync(appendedKeyName)); } @@ -421,52 +370,45 @@ public async Task AddAzureKeyVault_WithManagedServiceIdentityRemovesPrefix_GetsS public async Task AddAzureKeyVault_WithManagedServiceIdentityWrongMutation_GetsSecretFails() { // Arrange - var keyVaultUri = Configuration.GetValue("Arcus:KeyVault:Uri"); - var connectionString = Configuration.GetValue("Arcus:MSI:AzureServicesAuth:ConnectionString"); - var keyName = Configuration.GetValue("Arcus:KeyVault:TestKeyName"); - var builder = new HostBuilder(); // Act - builder.ConfigureSecretStore((config, stores) => + builder.ConfigureSecretStore((_, stores) => { stores.AddAzureKeyVaultWithManagedServiceIdentityWithOptions( - keyVaultUri, connectionString, mutateSecretName: secretName => "SOMETHING-WRONG-" + secretName); + VaultUri, MsiConnectionString, mutateSecretName: secretName => "SOMETHING-WRONG-" + secretName); }); // Assert using IHost host = builder.Build(); var provider = host.Services.GetRequiredService(); - Assert.Throws(() => provider.GetSecret(keyName)); - Assert.Throws(() => provider.GetRawSecret(keyName)); - await Assert.ThrowsAsync(() => provider.GetSecretAsync(keyName)); - await Assert.ThrowsAsync(() => provider.GetRawSecretAsync(keyName)); + Assert.Throws(() => provider.GetSecret(TestSecretName)); + Assert.Throws(() => provider.GetRawSecret(TestSecretName)); + await Assert.ThrowsAsync(() => provider.GetSecretAsync(TestSecretName)); + await Assert.ThrowsAsync(() => provider.GetRawSecretAsync(TestSecretName)); } [Fact] public async Task AddAzureKeyVault_WithCachedManagedServiceIdentity_GetSecretSucceeds() { // Arrange - var keyVaultUri = Configuration.GetValue("Arcus:KeyVault:Uri"); - var connectionString = Configuration.GetValue("Arcus:MSI:AzureServicesAuth:ConnectionString"); - var keyName = Configuration.GetValue("Arcus:KeyVault:TestKeyName"); var builder = new HostBuilder(); var cacheConfiguration = new SpyCacheConfiguration(); // Act - builder.ConfigureSecretStore((config, stores) => + builder.ConfigureSecretStore((_, stores) => { - stores.AddAzureKeyVaultWithManagedServiceIdentity(keyVaultUri, connectionString: connectionString, cacheConfiguration: cacheConfiguration); + stores.AddAzureKeyVaultWithManagedServiceIdentity(VaultUri, connectionString: MsiConnectionString, cacheConfiguration: cacheConfiguration); }); // Assert using IHost host = builder.Build(); var provider = host.Services.GetRequiredService(); - AssertNotNullSecret(await provider.GetSecretAsync(keyName)); - AssertNotNullSecret(await provider.GetRawSecretAsync(keyName)); + AssertNotNullSecret(await provider.GetSecretAsync(TestSecretName)); + AssertNotNullSecret(await provider.GetRawSecretAsync(TestSecretName)); Assert.True(cacheConfiguration.IsCalled); } @@ -474,23 +416,20 @@ public async Task AddAzureKeyVault_WithCachedManagedServiceIdentity_GetSecretSuc public async Task AddAzureKeyVault_WithCachedManagedServiceIdentity_GetSecretFails() { // Arrange - var keyVaultUri = Configuration.GetValue("Arcus:KeyVault:Uri"); - var connectionString = Configuration.GetValue("Arcus:MSI:AzureServicesAuth:ConnectionString"); - var keyName = "UnknownSecretName"; - var builder = new HostBuilder(); var cacheConfiguration = new SpyCacheConfiguration(); // Act - builder.ConfigureSecretStore((config, stores) => + builder.ConfigureSecretStore((_, stores) => { - stores.AddAzureKeyVaultWithManagedServiceIdentity(keyVaultUri, connectionString: connectionString, cacheConfiguration: cacheConfiguration); + stores.AddAzureKeyVaultWithManagedServiceIdentity(VaultUri, connectionString: MsiConnectionString, cacheConfiguration: cacheConfiguration); }); // Assert using IHost host = builder.Build(); var provider = host.Services.GetRequiredService(); + var keyName = "UnknownSecretName"; Assert.Throws(() => provider.GetSecret(keyName)); Assert.Throws(() => provider.GetRawSecret(keyName)); await Assert.ThrowsAsync(() => provider.GetSecretAsync(keyName)); @@ -501,25 +440,21 @@ public async Task AddAzureKeyVault_WithCachedManagedServiceIdentity_GetSecretFai public async Task AddAzureKeyVault_WithCachedManagedServiceIdentityRemovesPrefix_GetsSecretSucceeds() { // Arrange - var keyVaultUri = Configuration.GetValue("Arcus:KeyVault:Uri"); - var connectionString = Configuration.GetValue("Arcus:MSI:AzureServicesAuth:ConnectionString"); - var keyName = Configuration.GetValue("Arcus:KeyVault:TestKeyName"); - var builder = new HostBuilder(); var cacheConfiguration = new SpyCacheConfiguration(); // Act - builder.ConfigureSecretStore((config, stores) => + builder.ConfigureSecretStore((_, stores) => { stores.AddAzureKeyVaultWithManagedServiceIdentityWithOptions( - keyVaultUri, connectionString: connectionString, cacheConfiguration: cacheConfiguration, mutateSecretName: secretName => secretName.Remove(0, 5)); + VaultUri, connectionString: MsiConnectionString, cacheConfiguration: cacheConfiguration, mutateSecretName: secretName => secretName.Remove(0, 5)); }); // Assert using IHost host = builder.Build(); var provider = host.Services.GetRequiredService(); - string appendedKeyName = "Test-" + keyName; + string appendedKeyName = "Test-" + TestSecretName; AssertNotNullSecret(await provider.GetSecretAsync(appendedKeyName)); AssertNotNullSecret(await provider.GetRawSecretAsync(appendedKeyName)); Assert.True(cacheConfiguration.IsCalled); @@ -529,51 +464,44 @@ public async Task AddAzureKeyVault_WithCachedManagedServiceIdentityRemovesPrefix public async Task AddAzureKeyVault_WithCachedManagedServiceIdentityWrongMutation_GetsSecretFails() { // Arrange - var keyVaultUri = Configuration.GetValue("Arcus:KeyVault:Uri"); - var connectionString = Configuration.GetValue("Arcus:MSI:AzureServicesAuth:ConnectionString"); - var keyName = Configuration.GetValue("Arcus:KeyVault:TestKeyName"); - var builder = new HostBuilder(); var cacheConfiguration = new SpyCacheConfiguration(); // Act - builder.ConfigureSecretStore((config, stores) => + builder.ConfigureSecretStore((_, stores) => { stores.AddAzureKeyVaultWithManagedServiceIdentityWithOptions( - keyVaultUri, connectionString: connectionString, cacheConfiguration: cacheConfiguration, mutateSecretName: secretName => "SOMETHING-WRONG-" + secretName); + VaultUri, connectionString: MsiConnectionString, cacheConfiguration: cacheConfiguration, mutateSecretName: secretName => "SOMETHING-WRONG-" + secretName); }); // Assert using IHost host = builder.Build(); var provider = host.Services.GetRequiredService(); - Assert.Throws(() => provider.GetSecret(keyName)); - Assert.Throws(() => provider.GetRawSecret(keyName)); - await Assert.ThrowsAsync(() => provider.GetSecretAsync(keyName)); - await Assert.ThrowsAsync(() => provider.GetRawSecretAsync(keyName)); + Assert.Throws(() => provider.GetSecret(TestSecretName)); + Assert.Throws(() => provider.GetRawSecret(TestSecretName)); + await Assert.ThrowsAsync(() => provider.GetSecretAsync(TestSecretName)); + await Assert.ThrowsAsync(() => provider.GetRawSecretAsync(TestSecretName)); } [Fact] public async Task AddAzureKeyVault_WithWrongServicePrincipalCredentials_Throws() { // Arrange - var keyVaultUri = Configuration.GetValue("Arcus:KeyVault:Uri"); - var keyName = Configuration.GetValue("Arcus:KeyVault:TestKeyName"); - var builder = new HostBuilder(); // Act - builder.ConfigureSecretStore((config, stores) => + builder.ConfigureSecretStore((_, stores) => { - stores.AddAzureKeyVaultWithServicePrincipal(keyVaultUri, "wrong-app-id", "wrong-access-key"); + stores.AddAzureKeyVaultWithServicePrincipal(VaultUri, "wrong-app-id", "wrong-access-key"); }); // Assert using IHost host = builder.Build(); var provider = host.Services.GetRequiredService(); - var exceptionFromSecretAsync = await Assert.ThrowsAsync(() => provider.GetSecretAsync(keyName)); - var exceptionFromRawSecretAsync = await Assert.ThrowsAsync(() => provider.GetRawSecretAsync(keyName)); + var exceptionFromSecretAsync = await Assert.ThrowsAsync(() => provider.GetSecretAsync(TestSecretName)); + var exceptionFromRawSecretAsync = await Assert.ThrowsAsync(() => provider.GetRawSecretAsync(TestSecretName)); var errorCode = "unauthorized_client"; Assert.Equal(errorCode, exceptionFromSecretAsync.ErrorCode); Assert.Equal(errorCode, exceptionFromRawSecretAsync.ErrorCode); @@ -585,23 +513,21 @@ public async Task AddAzureKeyVault_WithWrongUnauthorizedServicePrincipal_Throws( // Arrange string applicationId = Configuration.GetValue("Arcus:UnauthorizedServicePrincipal:ApplicationId"); var clientKey = Configuration.GetValue("Arcus:UnauthorizedServicePrincipal:AccessKey"); - var keyVaultUri = Configuration.GetValue("Arcus:KeyVault:Uri"); - var keyName = Configuration.GetValue("Arcus:KeyVault:TestKeyName"); var builder = new HostBuilder(); // Act - builder.ConfigureSecretStore((config, stores) => + builder.ConfigureSecretStore((_, stores) => { - stores.AddAzureKeyVaultWithServicePrincipal(keyVaultUri, applicationId, clientKey); + stores.AddAzureKeyVaultWithServicePrincipal(VaultUri, applicationId, clientKey); }); // Assert using IHost host = builder.Build(); var provider = host.Services.GetRequiredService(); - var exceptionFromSecretAsync = await Assert.ThrowsAsync(() => provider.GetSecretAsync(keyName)); - var exceptionFromRawSecretAsync = await Assert.ThrowsAsync(() => provider.GetRawSecretAsync(keyName)); + var exceptionFromSecretAsync = await Assert.ThrowsAsync(() => provider.GetSecretAsync(TestSecretName)); + var exceptionFromRawSecretAsync = await Assert.ThrowsAsync(() => provider.GetRawSecretAsync(TestSecretName)); Assert.Equal(HttpStatusCode.Forbidden, exceptionFromSecretAsync.Response.StatusCode); Assert.Equal(HttpStatusCode.Forbidden, exceptionFromRawSecretAsync.Response.StatusCode); } @@ -610,52 +536,40 @@ public async Task AddAzureKeyVault_WithWrongUnauthorizedServicePrincipal_Throws( public async Task AddAzureKeyVaultWithTenantSimple_WithServicePrincipal_GetSecretSucceeds() { // Arrange - string tenantId = Configuration.GetTenantId(); - string applicationId = Configuration.GetValue("Arcus:ServicePrincipal:ApplicationId"); - var clientKey = Configuration.GetValue("Arcus:ServicePrincipal:AccessKey"); - var keyVaultUri = Configuration.GetValue("Arcus:KeyVault:Uri"); - var keyName = Configuration.GetValue("Arcus:KeyVault:TestKeyName"); - var builder = new HostBuilder(); // Act - builder.ConfigureSecretStore((config, stores) => + builder.ConfigureSecretStore((_, stores) => { - stores.AddAzureKeyVaultWithServicePrincipal(keyVaultUri, tenantId, applicationId, clientKey); + stores.AddAzureKeyVaultWithServicePrincipal(VaultUri, TenantId, ClientId, ClientSecret); }); // Assert using IHost host = builder.Build(); var provider = host.Services.GetRequiredService(); - AssertNotNullSecret(provider.GetSecret(keyName)); - AssertNotNullSecret(provider.GetRawSecret(keyName)); - AssertNotNullSecret(await provider.GetSecretAsync(keyName)); - AssertNotNullSecret(await provider.GetRawSecretAsync(keyName)); + AssertNotNullSecret(provider.GetSecret(TestSecretName)); + AssertNotNullSecret(provider.GetRawSecret(TestSecretName)); + AssertNotNullSecret(await provider.GetSecretAsync(TestSecretName)); + AssertNotNullSecret(await provider.GetRawSecretAsync(TestSecretName)); } [Fact] public async Task AddAzureKeyVaultWithTenant_WithServicePrincipal_GetSecretSucceeds() { // Arrange - string tenantId = Configuration.GetTenantId(); - string applicationId = Configuration.GetValue("Arcus:ServicePrincipal:ApplicationId"); - var clientKey = Configuration.GetValue("Arcus:ServicePrincipal:AccessKey"); - var keyVaultUri = Configuration.GetValue("Arcus:KeyVault:Uri"); - var keyName = Configuration.GetValue("Arcus:KeyVault:TestKeyName"); string prefix = "Test-"; - var builder = new HostBuilder(); // Act - builder.ConfigureSecretStore((config, stores) => + builder.ConfigureSecretStore((_, stores) => { stores.AddAzureKeyVaultWithServicePrincipal( - keyVaultUri, - tenantId, - applicationId, - clientKey, - configureOptions: options => { }, + VaultUri, + TenantId, + ClientId, + ClientSecret, + configureOptions: _ => { }, name: "Azure Key Vault", mutateSecretName: secretName => secretName.Remove(0, prefix.Length)); }); @@ -664,7 +578,7 @@ public async Task AddAzureKeyVaultWithTenant_WithServicePrincipal_GetSecretSucce using IHost host = builder.Build(); var provider = host.Services.GetRequiredService(); - string appendedKeyName = "Test-" + keyName; + string appendedKeyName = prefix + TestSecretName; AssertNotNullSecret(provider.GetSecret(appendedKeyName)); AssertNotNullSecret(provider.GetRawSecret(appendedKeyName)); AssertNotNullSecret(await provider.GetSecretAsync(appendedKeyName)); @@ -677,19 +591,13 @@ public async Task AddAzureKeyVaultWithTenant_WithServicePrincipal_GetSecretSucce public async Task AddAzureKeyVaultWithTenantWithDependencyTracking_WithServicePrincipal_GetSecretSucceeds(bool trackDependency, int expectedTrackedDependencies) { // Arrange - string tenantId = Configuration.GetTenantId(); - string applicationId = Configuration.GetValue("Arcus:ServicePrincipal:ApplicationId"); - var clientKey = Configuration.GetValue("Arcus:ServicePrincipal:AccessKey"); - var keyVaultUri = Configuration.GetValue("Arcus:KeyVault:Uri"); - var keyName = Configuration.GetValue("Arcus:KeyVault:TestKeyName"); - var builder = new HostBuilder(); - builder.UseSerilog(Logger, dispose: true); + builder.UseSerilog(SerilogLogger, dispose: true); // Act - builder.ConfigureSecretStore((config, stores) => + builder.ConfigureSecretStore((_, stores) => { - stores.AddAzureKeyVaultWithServicePrincipal(keyVaultUri, tenantId, applicationId, clientKey, + stores.AddAzureKeyVaultWithServicePrincipal(VaultUri, TenantId, ClientId, ClientSecret, configureOptions: options => options.TrackDependency = trackDependency, name: null, mutateSecretName: null); }); @@ -698,8 +606,8 @@ public async Task AddAzureKeyVaultWithTenantWithDependencyTracking_WithServicePr { var provider = host.Services.GetRequiredService(); - AssertNotNullSecret(await provider.GetSecretAsync(keyName)); - AssertNotNullSecret(await provider.GetRawSecretAsync(keyName)); + AssertNotNullSecret(await provider.GetSecretAsync(TestSecretName)); + AssertNotNullSecret(await provider.GetRawSecretAsync(TestSecretName)); } AssertTrackedAzureKeyVaultDependency(expectedTrackedDependencies); @@ -709,24 +617,19 @@ public async Task AddAzureKeyVaultWithTenantWithDependencyTracking_WithServicePr public async Task AddAzureKeyVaultWithTenant_WithServicePrincipal_GetSecretFails() { // Arrange - string tenantId = Configuration.GetTenantId(); - string applicationId = Configuration.GetValue("Arcus:ServicePrincipal:ApplicationId"); - var clientKey = Configuration.GetValue("Arcus:ServicePrincipal:AccessKey"); - var keyVaultUri = Configuration.GetValue("Arcus:KeyVault:Uri"); - var keyName = "UnknownSecretName"; - var builder = new HostBuilder(); // Act - builder.ConfigureSecretStore((config, stores) => + builder.ConfigureSecretStore((_, stores) => { - stores.AddAzureKeyVaultWithServicePrincipal(keyVaultUri, tenantId, applicationId, clientKey, configureOptions: null, name: null, mutateSecretName: null); + stores.AddAzureKeyVaultWithServicePrincipal(VaultUri, TenantId, ClientId, ClientSecret, configureOptions: null, name: null, mutateSecretName: null); }); // Assert using IHost host = builder.Build(); var provider = host.Services.GetRequiredService(); + var keyName = "UnknownSecretName"; Assert.Throws(() => provider.GetSecret(keyName)); Assert.Throws(() => provider.GetRawSecret(keyName)); await Assert.ThrowsAsync(() => provider.GetSecretAsync(keyName)); @@ -739,19 +642,13 @@ public async Task AddAzureKeyVaultWithTenant_WithServicePrincipal_GetSecretFails public async Task AddAzureKeyVaultWithTenantWithDependencyTracking_WithServicePrincipal_GetSecretFails(bool trackDependency, int expectedTrackedDependencies) { // Arrange - string tenantId = Configuration.GetTenantId(); - string applicationId = Configuration.GetValue("Arcus:ServicePrincipal:ApplicationId"); - var clientKey = Configuration.GetValue("Arcus:ServicePrincipal:AccessKey"); - var keyVaultUri = Configuration.GetValue("Arcus:KeyVault:Uri"); - var keyName = "UnknownSecretName"; - var builder = new HostBuilder(); - builder.UseSerilog(Logger, dispose: true); + builder.UseSerilog(SerilogLogger, dispose: true); // Act - builder.ConfigureSecretStore((config, stores) => + builder.ConfigureSecretStore((_, stores) => { - stores.AddAzureKeyVaultWithServicePrincipal(keyVaultUri, tenantId, applicationId, clientKey, + stores.AddAzureKeyVaultWithServicePrincipal(VaultUri, TenantId, ClientId, ClientSecret, configureOptions: options => options.TrackDependency = trackDependency, name: null, mutateSecretName: null); }); @@ -759,6 +656,8 @@ public async Task AddAzureKeyVaultWithTenantWithDependencyTracking_WithServicePr using (IHost host = builder.Build()) { var provider = host.Services.GetRequiredService(); + + var keyName = "UnknownSecretName"; await Assert.ThrowsAsync(() => provider.GetSecretAsync(keyName)); await Assert.ThrowsAsync(() => provider.GetRawSecretAsync(keyName)); } @@ -770,19 +669,13 @@ public async Task AddAzureKeyVaultWithTenantWithDependencyTracking_WithServicePr public async Task AddAzureKeyVaultWithTenant_WithServicePrincipalToDots_GetsSecretSucceeds() { // Arrange - string tenantId = Configuration.GetTenantId(); - string applicationId = Configuration.GetValue("Arcus:ServicePrincipal:ApplicationId"); - var clientKey = Configuration.GetValue("Arcus:ServicePrincipal:AccessKey"); - var keyVaultUri = Configuration.GetValue("Arcus:KeyVault:Uri"); - var keyName = Configuration.GetValue("Arcus:KeyVault:TestKeyName"); - var builder = new HostBuilder(); // Act - builder.ConfigureSecretStore((config, stores) => + builder.ConfigureSecretStore((_, stores) => { stores.AddAzureKeyVaultWithServicePrincipal( - keyVaultUri, tenantId, applicationId, clientKey, + VaultUri, TenantId, ClientId, ClientSecret, mutateSecretName: secretName => secretName.Remove(0, 5), name: null, configureOptions: null); @@ -792,7 +685,7 @@ public async Task AddAzureKeyVaultWithTenant_WithServicePrincipalToDots_GetsSecr using IHost host = builder.Build(); var provider = host.Services.GetRequiredService(); - string appendedKeyName = "Test-" + keyName; + string appendedKeyName = "Test-" + TestSecretName; AssertNotNullSecret(provider.GetSecret(appendedKeyName)); AssertNotNullSecret(provider.GetRawSecret(appendedKeyName)); AssertNotNullSecret(await provider.GetSecretAsync(appendedKeyName)); @@ -803,19 +696,13 @@ public async Task AddAzureKeyVaultWithTenant_WithServicePrincipalToDots_GetsSecr public async Task AddAzureKeyVaultWithTenant_WithServicePrincipalWrongMutation_GetsSecretsFails() { // Arrange - string tenantId = Configuration.GetTenantId(); - string applicationId = Configuration.GetValue("Arcus:ServicePrincipal:ApplicationId"); - var clientKey = Configuration.GetValue("Arcus:ServicePrincipal:AccessKey"); - var keyVaultUri = Configuration.GetValue("Arcus:KeyVault:Uri"); - var keyName = Configuration.GetValue("Arcus:KeyVault:TestKeyName"); - var builder = new HostBuilder(); // Act - builder.ConfigureSecretStore((config, stores) => + builder.ConfigureSecretStore((_, stores) => { stores.AddAzureKeyVaultWithServicePrincipal( - keyVaultUri, tenantId, applicationId, clientKey, + VaultUri, TenantId, ClientId, ClientSecret, mutateSecretName: secretName => "SOMETHING-WRONG-" + secretName, configureOptions: null, name: null); @@ -825,62 +712,51 @@ public async Task AddAzureKeyVaultWithTenant_WithServicePrincipalWrongMutation_G using IHost host = builder.Build(); var provider = host.Services.GetRequiredService(); - Assert.Throws(() => provider.GetSecret(keyName)); - Assert.Throws(() => provider.GetRawSecret(keyName)); - await Assert.ThrowsAsync(() => provider.GetSecretAsync(keyName)); - await Assert.ThrowsAsync(() => provider.GetSecretAsync(keyName)); + Assert.Throws(() => provider.GetSecret(TestSecretName)); + Assert.Throws(() => provider.GetRawSecret(TestSecretName)); + await Assert.ThrowsAsync(() => provider.GetSecretAsync(TestSecretName)); + await Assert.ThrowsAsync(() => provider.GetSecretAsync(TestSecretName)); } [Fact] public async Task AddAzureKeyVaultWithTenant_WithCachedServicePrincipal_GetSecretSucceeds() { // Arrange - string tenantId = Configuration.GetTenantId(); - string applicationId = Configuration.GetValue("Arcus:ServicePrincipal:ApplicationId"); - var clientKey = Configuration.GetValue("Arcus:ServicePrincipal:AccessKey"); - var keyVaultUri = Configuration.GetValue("Arcus:KeyVault:Uri"); - var keyName = Configuration.GetValue("Arcus:KeyVault:TestKeyName"); - var builder = new HostBuilder(); // Act - builder.ConfigureSecretStore((config, stores) => + builder.ConfigureSecretStore((_, stores) => { - stores.AddAzureKeyVaultWithServicePrincipal(keyVaultUri, tenantId, applicationId, clientKey, allowCaching: true, configureOptions: null, name: null, mutateSecretName: null); + stores.AddAzureKeyVaultWithServicePrincipal(VaultUri, TenantId, ClientId, ClientSecret, allowCaching: true, configureOptions: null, name: null, mutateSecretName: null); }); // Assert using IHost host = builder.Build(); var provider = host.Services.GetRequiredService(); - AssertNotNullSecret(provider.GetSecret(keyName)); - AssertNotNullSecret(provider.GetRawSecret(keyName)); - AssertNotNullSecret(await provider.GetSecretAsync(keyName)); - AssertNotNullSecret(await provider.GetRawSecretAsync(keyName)); + AssertNotNullSecret(provider.GetSecret(TestSecretName)); + AssertNotNullSecret(provider.GetRawSecret(TestSecretName)); + AssertNotNullSecret(await provider.GetSecretAsync(TestSecretName)); + AssertNotNullSecret(await provider.GetRawSecretAsync(TestSecretName)); } [Fact] public async Task AddAzureKeyVaultWithTenant_WithCachedServicePrincipal_GetSecretFails() { // Arrange - string applicationId = Configuration.GetValue("Arcus:ServicePrincipal:ApplicationId"); - var clientKey = Configuration.GetValue("Arcus:ServicePrincipal:AccessKey"); - var keyVaultUri = Configuration.GetValue("Arcus:KeyVault:Uri"); - string tenantId = Configuration.GetTenantId(); - var keyName = "UnknownSecretName"; - var builder = new HostBuilder(); // Act - builder.ConfigureSecretStore((config, stores) => + builder.ConfigureSecretStore((_, stores) => { - stores.AddAzureKeyVaultWithServicePrincipal(keyVaultUri, tenantId, applicationId, clientKey, allowCaching: true, configureOptions: null, name: null, mutateSecretName: null); + stores.AddAzureKeyVaultWithServicePrincipal(VaultUri, TenantId, ClientId, ClientSecret, allowCaching: true, configureOptions: null, name: null, mutateSecretName: null); }); // Assert using IHost host = builder.Build(); var provider = host.Services.GetRequiredService(); + var keyName = "UnknownSecretName"; Assert.Throws(() => provider.GetSecret(keyName)); Assert.Throws(() => provider.GetRawSecret(keyName)); await Assert.ThrowsAsync(() => provider.GetSecretAsync(keyName)); @@ -891,19 +767,13 @@ public async Task AddAzureKeyVaultWithTenant_WithCachedServicePrincipal_GetSecre public async Task AddAzureKeyVaultWithTenant_WithCachedServicePrincipalRemovesPrefix_GetsSecretsSucceeds() { // Arrange - string tenantId = Configuration.GetTenantId(); - string applicationId = Configuration.GetValue("Arcus:ServicePrincipal:ApplicationId"); - var clientKey = Configuration.GetValue("Arcus:ServicePrincipal:AccessKey"); - var keyVaultUri = Configuration.GetValue("Arcus:KeyVault:Uri"); - var keyName = Configuration.GetValue("Arcus:KeyVault:TestKeyName"); - var builder = new HostBuilder(); // Act - builder.ConfigureSecretStore((config, stores) => + builder.ConfigureSecretStore((_, stores) => { stores.AddAzureKeyVaultWithServicePrincipal( - keyVaultUri, tenantId, applicationId, clientKey, allowCaching: true, + VaultUri, TenantId, ClientId, ClientSecret, allowCaching: true, mutateSecretName: secretName => secretName.Remove(0, 5), configureOptions: null, name: null); @@ -913,7 +783,7 @@ public async Task AddAzureKeyVaultWithTenant_WithCachedServicePrincipalRemovesPr using IHost host = builder.Build(); var provider = host.Services.GetRequiredService(); - string appendedKeyName = "Test-" + keyName; + string appendedKeyName = "Test-" + TestSecretName; AssertNotNullSecret(provider.GetSecret(appendedKeyName)); AssertNotNullSecret(provider.GetRawSecret(appendedKeyName)); AssertNotNullSecret(await provider.GetSecretAsync(appendedKeyName)); @@ -924,19 +794,13 @@ public async Task AddAzureKeyVaultWithTenant_WithCachedServicePrincipalRemovesPr public async Task AddAzureKeyVaultWithTenant_WithCachedServicePrincipalWrongMutation_GetsSecretFails() { // Arrange - string tenantId = Configuration.GetTenantId(); - string applicationId = Configuration.GetValue("Arcus:ServicePrincipal:ApplicationId"); - var clientKey = Configuration.GetValue("Arcus:ServicePrincipal:AccessKey"); - var keyVaultUri = Configuration.GetValue("Arcus:KeyVault:Uri"); - var keyName = Configuration.GetValue("Arcus:KeyVault:TestKeyName"); - var builder = new HostBuilder(); // Act - builder.ConfigureSecretStore((config, stores) => + builder.ConfigureSecretStore((_, stores) => { stores.AddAzureKeyVaultWithServicePrincipal( - keyVaultUri, tenantId, applicationId, clientKey, allowCaching: true, + VaultUri, TenantId, ClientId, ClientSecret, allowCaching: true, mutateSecretName: secretName => "SOMETHING-WRONG-" + secretName, configureOptions: null, name: null); @@ -946,39 +810,33 @@ public async Task AddAzureKeyVaultWithTenant_WithCachedServicePrincipalWrongMuta using IHost host = builder.Build(); var provider = host.Services.GetRequiredService(); - Assert.Throws(() => provider.GetSecret(keyName)); - Assert.Throws(() => provider.GetRawSecret(keyName)); - await Assert.ThrowsAsync(() => provider.GetSecretAsync(keyName)); - await Assert.ThrowsAsync(() => provider.GetRawSecretAsync(keyName)); + Assert.Throws(() => provider.GetSecret(TestSecretName)); + Assert.Throws(() => provider.GetRawSecret(TestSecretName)); + await Assert.ThrowsAsync(() => provider.GetSecretAsync(TestSecretName)); + await Assert.ThrowsAsync(() => provider.GetRawSecretAsync(TestSecretName)); } [Fact] public async Task AddAzureKeyVault_WithManagedIdentity_GetSecretSucceeds() { // Arrange - var keyVaultUri = Configuration.GetValue("Arcus:KeyVault:Uri"); - string tenantId = Configuration.GetTenantId(); - string clientId = Configuration.GetServicePrincipalClientId(); - var clientKey = Configuration.GetServicePrincipalClientSecret(); - var keyName = Configuration.GetValue("Arcus:KeyVault:TestKeyName"); - var builder = new HostBuilder(); // Act - builder.ConfigureSecretStore((config, stores) => stores.AddAzureKeyVaultWithManagedIdentity(keyVaultUri, clientId)); + builder.ConfigureSecretStore((_, stores) => stores.AddAzureKeyVaultWithManagedIdentity(VaultUri, ClientId)); // Assert - using (TemporaryEnvironmentVariable.Create(Constants.AzureTenantIdEnvironmentVariable, tenantId)) - using (TemporaryEnvironmentVariable.Create(Constants.AzureServicePrincipalClientIdVariable, clientId)) - using (TemporaryEnvironmentVariable.Create(Constants.AzureServicePrincipalClientSecretVariable, clientKey)) + using (TemporaryEnvironmentVariable.Create(Constants.AzureTenantIdEnvironmentVariable, TenantId)) + using (TemporaryEnvironmentVariable.Create(Constants.AzureServicePrincipalClientIdVariable, ClientId)) + using (TemporaryEnvironmentVariable.Create(Constants.AzureServicePrincipalClientSecretVariable, ClientSecret)) { using IHost host = builder.Build(); var provider = host.Services.GetRequiredService(); - AssertNotNullSecret(provider.GetSecret(keyName)); - AssertNotNullSecret(provider.GetRawSecret(keyName)); - AssertNotNullSecret(await provider.GetSecretAsync(keyName)); - AssertNotNullSecret(await provider.GetRawSecretAsync(keyName)); + AssertNotNullSecret(provider.GetSecret(TestSecretName)); + AssertNotNullSecret(provider.GetRawSecret(TestSecretName)); + AssertNotNullSecret(await provider.GetSecretAsync(TestSecretName)); + AssertNotNullSecret(await provider.GetRawSecretAsync(TestSecretName)); } } @@ -986,35 +844,28 @@ public async Task AddAzureKeyVault_WithManagedIdentity_GetSecretSucceeds() public async Task AddAzureKeyVaultSimple_WithManagedIdentity_GetSecretSucceeds() { // Arrange - var keyVaultUri = Configuration.GetValue("Arcus:KeyVault:Uri"); - string tenantId = Configuration.GetTenantId(); - string clientId = Configuration.GetServicePrincipalClientId(); - var clientKey = Configuration.GetServicePrincipalClientSecret(); - var keyName = Configuration.GetValue("Arcus:KeyVault:TestKeyName"); string prefix = "Test-"; - var builder = new HostBuilder(); // Act - builder.ConfigureSecretStore((config, stores) => + builder.ConfigureSecretStore((_, stores) => { stores.AddAzureKeyVaultWithManagedIdentity( - keyVaultUri, - clientId, - configureOptions: options => { }, + VaultUri, ClientId, + configureOptions: _ => { }, name: "Azure Key Vault", mutateSecretName: secretName => secretName.Remove(0, prefix.Length)); }); // Assert - using (TemporaryEnvironmentVariable.Create(Constants.AzureTenantIdEnvironmentVariable, tenantId)) - using (TemporaryEnvironmentVariable.Create(Constants.AzureServicePrincipalClientIdVariable, clientId)) - using (TemporaryEnvironmentVariable.Create(Constants.AzureServicePrincipalClientSecretVariable, clientKey)) + using (TemporaryEnvironmentVariable.Create(Constants.AzureTenantIdEnvironmentVariable, TenantId)) + using (TemporaryEnvironmentVariable.Create(Constants.AzureServicePrincipalClientIdVariable, ClientId)) + using (TemporaryEnvironmentVariable.Create(Constants.AzureServicePrincipalClientSecretVariable, ClientSecret)) { using IHost host = builder.Build(); var provider = host.Services.GetRequiredService(); - string appendedKeyName = "Test-" + keyName; + string appendedKeyName = "Test-" + TestSecretName; AssertNotNullSecret(provider.GetSecret(appendedKeyName)); AssertNotNullSecret(provider.GetRawSecret(appendedKeyName)); AssertNotNullSecret(await provider.GetSecretAsync(appendedKeyName)); @@ -1028,31 +879,25 @@ public async Task AddAzureKeyVaultSimple_WithManagedIdentity_GetSecretSucceeds() public async Task AddAzureKeyVaultWithDependencyTracking_WithManagedIdentity_GetSecretSucceeds(bool trackDependency, int expectedTrackedDependencies) { // Arrange - var keyVaultUri = Configuration.GetValue("Arcus:KeyVault:Uri"); - string tenantId = Configuration.GetTenantId(); - string clientId = Configuration.GetServicePrincipalClientId(); - var clientKey = Configuration.GetServicePrincipalClientSecret(); - var keyName = Configuration.GetValue("Arcus:KeyVault:TestKeyName"); - var builder = new HostBuilder(); - builder.UseSerilog(Logger, dispose: true); + builder.UseSerilog(SerilogLogger, dispose: true); // Act - builder.ConfigureSecretStore((config, stores) => + builder.ConfigureSecretStore((_, stores) => { - stores.AddAzureKeyVaultWithManagedIdentity(keyVaultUri, clientId, configureOptions: options => options.TrackDependency = trackDependency, name: null, mutateSecretName: null); + stores.AddAzureKeyVaultWithManagedIdentity(VaultUri, ClientId, configureOptions: options => options.TrackDependency = trackDependency, name: null, mutateSecretName: null); }); // Assert - using (TemporaryEnvironmentVariable.Create(Constants.AzureTenantIdEnvironmentVariable, tenantId)) - using (TemporaryEnvironmentVariable.Create(Constants.AzureServicePrincipalClientIdVariable, clientId)) - using (TemporaryEnvironmentVariable.Create(Constants.AzureServicePrincipalClientSecretVariable, clientKey)) + using (TemporaryEnvironmentVariable.Create(Constants.AzureTenantIdEnvironmentVariable, TenantId)) + using (TemporaryEnvironmentVariable.Create(Constants.AzureServicePrincipalClientIdVariable, ClientId)) + using (TemporaryEnvironmentVariable.Create(Constants.AzureServicePrincipalClientSecretVariable, ClientSecret)) using (IHost host = builder.Build()) { var provider = host.Services.GetRequiredService(); - AssertNotNullSecret(await provider.GetSecretAsync(keyName)); - AssertNotNullSecret(await provider.GetRawSecretAsync(keyName)); + AssertNotNullSecret(await provider.GetSecretAsync(TestSecretName)); + AssertNotNullSecret(await provider.GetRawSecretAsync(TestSecretName)); } AssertTrackedAzureKeyVaultDependency(expectedTrackedDependencies); @@ -1062,33 +907,27 @@ public async Task AddAzureKeyVaultWithDependencyTracking_WithManagedIdentity_Get public async Task AddAzureKeyVault_WithManagedIdentityRemovesPrefix_GetsSecretSucceeds() { // Arrange - var keyVaultUri = Configuration.GetValue("Arcus:KeyVault:Uri"); - string tenantId = Configuration.GetTenantId(); - string clientId = Configuration.GetServicePrincipalClientId(); - string clientKey = Configuration.GetServicePrincipalClientSecret(); - var keyName = Configuration.GetValue("Arcus:KeyVault:TestKeyName"); - var builder = new HostBuilder(); // Act - builder.ConfigureSecretStore((config, stores) => + builder.ConfigureSecretStore((_, stores) => { stores.AddAzureKeyVaultWithManagedIdentity( - keyVaultUri, clientId, + VaultUri, ClientId, mutateSecretName: secretName => secretName.Remove(0, 5), configureOptions: null, name: null); }); // Assert - using (TemporaryEnvironmentVariable.Create(Constants.AzureTenantIdEnvironmentVariable, tenantId)) - using (TemporaryEnvironmentVariable.Create(Constants.AzureServicePrincipalClientIdVariable, clientId)) - using (TemporaryEnvironmentVariable.Create(Constants.AzureServicePrincipalClientSecretVariable, clientKey)) + using (TemporaryEnvironmentVariable.Create(Constants.AzureTenantIdEnvironmentVariable, TenantId)) + using (TemporaryEnvironmentVariable.Create(Constants.AzureServicePrincipalClientIdVariable, ClientId)) + using (TemporaryEnvironmentVariable.Create(Constants.AzureServicePrincipalClientSecretVariable, ClientSecret)) { using IHost host = builder.Build(); var provider = host.Services.GetRequiredService(); - string appendedKeyName = "Test-" + keyName; + string appendedKeyName = "Test-" + TestSecretName; AssertNotNullSecret(provider.GetSecret(appendedKeyName)); AssertNotNullSecret(provider.GetRawSecret(appendedKeyName)); AssertNotNullSecret(await provider.GetSecretAsync(appendedKeyName)); @@ -1100,36 +939,30 @@ public async Task AddAzureKeyVault_WithManagedIdentityRemovesPrefix_GetsSecretSu public async Task AddAzureKeyVault_WithManagedIdentityWrongMutation_GetsSecretFails() { // Arrange - var keyVaultUri = Configuration.GetValue("Arcus:KeyVault:Uri"); - string tenantId = Configuration.GetTenantId(); - string clientId = Configuration.GetServicePrincipalClientId(); - string clientKey = Configuration.GetServicePrincipalClientSecret(); - var keyName = Configuration.GetValue("Arcus:KeyVault:TestKeyName"); - var builder = new HostBuilder(); // Act - builder.ConfigureSecretStore((config, stores) => + builder.ConfigureSecretStore((_, stores) => { stores.AddAzureKeyVaultWithManagedIdentity( - keyVaultUri, clientId, + VaultUri, ClientId, mutateSecretName: secretName => "SOMETHING-WRONG-" + secretName, configureOptions: null, name: null); }); // Assert - using (TemporaryEnvironmentVariable.Create(Constants.AzureTenantIdEnvironmentVariable, tenantId)) - using (TemporaryEnvironmentVariable.Create(Constants.AzureServicePrincipalClientIdVariable, clientId)) - using (TemporaryEnvironmentVariable.Create(Constants.AzureServicePrincipalClientSecretVariable, clientKey)) + using (TemporaryEnvironmentVariable.Create(Constants.AzureTenantIdEnvironmentVariable, TenantId)) + using (TemporaryEnvironmentVariable.Create(Constants.AzureServicePrincipalClientIdVariable, ClientId)) + using (TemporaryEnvironmentVariable.Create(Constants.AzureServicePrincipalClientSecretVariable, ClientSecret)) { using IHost host = builder.Build(); var provider = host.Services.GetRequiredService(); - Assert.Throws(() => provider.GetSecret(keyName)); - Assert.Throws(() => provider.GetRawSecret(keyName)); - await Assert.ThrowsAsync(() => provider.GetSecretAsync(keyName)); - await Assert.ThrowsAsync(() => provider.GetRawSecretAsync(keyName)); + Assert.Throws(() => provider.GetSecret(TestSecretName)); + Assert.Throws(() => provider.GetRawSecret(TestSecretName)); + await Assert.ThrowsAsync(() => provider.GetSecretAsync(TestSecretName)); + await Assert.ThrowsAsync(() => provider.GetRawSecretAsync(TestSecretName)); } } @@ -1139,35 +972,29 @@ public async Task AddAzureKeyVault_WithManagedIdentityWrongMutation_GetsSecretFa public async Task AddAzureKeyVaultWithDependencyTracking_WithManagedIdentityWrongMutation_GetsSecretFails(bool trackDependency, int expectedTrackedDependencies) { // Arrange - var keyVaultUri = Configuration.GetValue("Arcus:KeyVault:Uri"); - string tenantId = Configuration.GetTenantId(); - string clientId = Configuration.GetServicePrincipalClientId(); - string clientKey = Configuration.GetServicePrincipalClientSecret(); - var keyName = Configuration.GetValue("Arcus:KeyVault:TestKeyName"); - var builder = new HostBuilder(); - builder.UseSerilog(Logger, dispose: true); + builder.UseSerilog(SerilogLogger, dispose: true); // Act - builder.ConfigureSecretStore((config, stores) => + builder.ConfigureSecretStore((_, stores) => { stores.AddAzureKeyVaultWithManagedIdentity( - keyVaultUri, clientId, + VaultUri, ClientId, configureOptions: options => options.TrackDependency = trackDependency, mutateSecretName: secretName => "SOMETHING-WRONG-" + secretName, name: null); }); // Assert - using (TemporaryEnvironmentVariable.Create(Constants.AzureTenantIdEnvironmentVariable, tenantId)) - using (TemporaryEnvironmentVariable.Create(Constants.AzureServicePrincipalClientIdVariable, clientId)) - using (TemporaryEnvironmentVariable.Create(Constants.AzureServicePrincipalClientSecretVariable, clientKey)) + using (TemporaryEnvironmentVariable.Create(Constants.AzureTenantIdEnvironmentVariable, TenantId)) + using (TemporaryEnvironmentVariable.Create(Constants.AzureServicePrincipalClientIdVariable, ClientId)) + using (TemporaryEnvironmentVariable.Create(Constants.AzureServicePrincipalClientSecretVariable, ClientSecret)) { using (IHost host = builder.Build()) { var provider = host.Services.GetRequiredService(); - await Assert.ThrowsAsync(() => provider.GetSecretAsync(keyName)); - await Assert.ThrowsAsync(() => provider.GetRawSecretAsync(keyName)); + await Assert.ThrowsAsync(() => provider.GetSecretAsync(TestSecretName)); + await Assert.ThrowsAsync(() => provider.GetRawSecretAsync(TestSecretName)); } } @@ -1178,33 +1005,27 @@ public async Task AddAzureKeyVaultWithDependencyTracking_WithManagedIdentityWron public async Task AddAzureKeyVault_WithCachedManagedIdentity_GetSecretSucceeds() { // Arrange - var keyVaultUri = Configuration.GetValue("Arcus:KeyVault:Uri"); - string tenantId = Configuration.GetTenantId(); - string clientId = Configuration.GetServicePrincipalClientId(); - string clientKey = Configuration.GetServicePrincipalClientSecret(); - var keyName = Configuration.GetValue("Arcus:KeyVault:TestKeyName"); - var builder = new HostBuilder(); var cacheConfiguration = new SpyCacheConfiguration(); // Act - builder.ConfigureSecretStore((config, stores) => + builder.ConfigureSecretStore((_, stores) => { - stores.AddAzureKeyVaultWithManagedIdentity(keyVaultUri, cacheConfiguration, clientId, configureOptions: null, name: null, mutateSecretName: null); + stores.AddAzureKeyVaultWithManagedIdentity(VaultUri, cacheConfiguration, ClientId, configureOptions: null, name: null, mutateSecretName: null); }); // Assert - using (TemporaryEnvironmentVariable.Create(Constants.AzureTenantIdEnvironmentVariable, tenantId)) - using (TemporaryEnvironmentVariable.Create(Constants.AzureServicePrincipalClientIdVariable, clientId)) - using (TemporaryEnvironmentVariable.Create(Constants.AzureServicePrincipalClientSecretVariable, clientKey)) + using (TemporaryEnvironmentVariable.Create(Constants.AzureTenantIdEnvironmentVariable, TenantId)) + using (TemporaryEnvironmentVariable.Create(Constants.AzureServicePrincipalClientIdVariable, ClientId)) + using (TemporaryEnvironmentVariable.Create(Constants.AzureServicePrincipalClientSecretVariable, ClientSecret)) { using IHost host = builder.Build(); var provider = host.Services.GetRequiredService(); - AssertNotNullSecret(provider.GetSecret(keyName)); - AssertNotNullSecret(provider.GetRawSecret(keyName)); - AssertNotNullSecret(await provider.GetSecretAsync(keyName)); - AssertNotNullSecret(await provider.GetRawSecretAsync(keyName)); + AssertNotNullSecret(provider.GetSecret(TestSecretName)); + AssertNotNullSecret(provider.GetRawSecret(TestSecretName)); + AssertNotNullSecret(await provider.GetSecretAsync(TestSecretName)); + AssertNotNullSecret(await provider.GetRawSecretAsync(TestSecretName)); Assert.True(cacheConfiguration.IsCalled); } } @@ -1213,29 +1034,24 @@ public async Task AddAzureKeyVault_WithCachedManagedIdentity_GetSecretSucceeds() public async Task AddAzureKeyVault_WithCachedManagedIdentity_GetSecretFails() { // Arrange - var keyVaultUri = Configuration.GetValue("Arcus:KeyVault:Uri"); - string tenantId = Configuration.GetTenantId(); - string clientId = Configuration.GetServicePrincipalClientId(); - string clientKey = Configuration.GetServicePrincipalClientSecret(); - var keyName = "UnknownSecretName"; - var builder = new HostBuilder(); var cacheConfiguration = new SpyCacheConfiguration(); // Act - builder.ConfigureSecretStore((config, stores) => + builder.ConfigureSecretStore((_, stores) => { - stores.AddAzureKeyVaultWithManagedIdentity(keyVaultUri, clientId: clientId, cacheConfiguration: cacheConfiguration, configureOptions: null, name: null, mutateSecretName: null); + stores.AddAzureKeyVaultWithManagedIdentity(VaultUri, clientId: ClientId, cacheConfiguration: cacheConfiguration, configureOptions: null, name: null, mutateSecretName: null); }); // Assert - using (TemporaryEnvironmentVariable.Create(Constants.AzureTenantIdEnvironmentVariable, tenantId)) - using (TemporaryEnvironmentVariable.Create(Constants.AzureServicePrincipalClientIdVariable, clientId)) - using (TemporaryEnvironmentVariable.Create(Constants.AzureServicePrincipalClientSecretVariable, clientKey)) + using (TemporaryEnvironmentVariable.Create(Constants.AzureTenantIdEnvironmentVariable, TenantId)) + using (TemporaryEnvironmentVariable.Create(Constants.AzureServicePrincipalClientIdVariable, ClientId)) + using (TemporaryEnvironmentVariable.Create(Constants.AzureServicePrincipalClientSecretVariable, ClientSecret)) { using IHost host = builder.Build(); var provider = host.Services.GetRequiredService(); + var keyName = "UnknownSecretName"; Assert.Throws(() => provider.GetSecret(keyName)); Assert.Throws(() => provider.GetRawSecret(keyName)); await Assert.ThrowsAsync(() => provider.GetSecretAsync(keyName)); @@ -1247,34 +1063,28 @@ public async Task AddAzureKeyVault_WithCachedManagedIdentity_GetSecretFails() public async Task AddAzureKeyVault_WithCachedManagedIdentityRemovesPrefix_GetsSecretSucceeds() { // Arrange - var keyVaultUri = Configuration.GetValue("Arcus:KeyVault:Uri"); - string tenantId = Configuration.GetTenantId(); - string clientId = Configuration.GetServicePrincipalClientId(); - string clientKey = Configuration.GetServicePrincipalClientSecret(); - var keyName = Configuration.GetValue("Arcus:KeyVault:TestKeyName"); - var builder = new HostBuilder(); var cacheConfiguration = new SpyCacheConfiguration(); // Act - builder.ConfigureSecretStore((config, stores) => + builder.ConfigureSecretStore((_, stores) => { stores.AddAzureKeyVaultWithManagedIdentity( - keyVaultUri, clientId: clientId, cacheConfiguration: cacheConfiguration, + VaultUri, clientId: ClientId, cacheConfiguration: cacheConfiguration, mutateSecretName: secretName => secretName.Remove(0, 5), configureOptions: null, name: null); }); // Assert - using (TemporaryEnvironmentVariable.Create(Constants.AzureTenantIdEnvironmentVariable, tenantId)) - using (TemporaryEnvironmentVariable.Create(Constants.AzureServicePrincipalClientIdVariable, clientId)) - using (TemporaryEnvironmentVariable.Create(Constants.AzureServicePrincipalClientSecretVariable, clientKey)) + using (TemporaryEnvironmentVariable.Create(Constants.AzureTenantIdEnvironmentVariable, TenantId)) + using (TemporaryEnvironmentVariable.Create(Constants.AzureServicePrincipalClientIdVariable, ClientId)) + using (TemporaryEnvironmentVariable.Create(Constants.AzureServicePrincipalClientSecretVariable, ClientSecret)) { using IHost host = builder.Build(); var provider = host.Services.GetRequiredService(); - string appendedKeyName = "Test-" + keyName; + string appendedKeyName = "Test-" + TestSecretName; AssertNotNullSecret(provider.GetSecret(appendedKeyName)); AssertNotNullSecret(provider.GetRawSecret(appendedKeyName)); AssertNotNullSecret(await provider.GetSecretAsync(appendedKeyName)); @@ -1287,37 +1097,31 @@ public async Task AddAzureKeyVault_WithCachedManagedIdentityRemovesPrefix_GetsSe public async Task AddAzureKeyVault_WithCachedManagedIdentityWrongMutation_GetsSecretFails() { // Arrange - var keyVaultUri = Configuration.GetValue("Arcus:KeyVault:Uri"); - string tenantId = Configuration.GetTenantId(); - string clientId = Configuration.GetServicePrincipalClientId(); - string clientKey = Configuration.GetServicePrincipalClientSecret(); - var keyName = Configuration.GetValue("Arcus:KeyVault:TestKeyName"); - var builder = new HostBuilder(); var cacheConfiguration = new SpyCacheConfiguration(); // Act - builder.ConfigureSecretStore((config, stores) => + builder.ConfigureSecretStore((_, stores) => { stores.AddAzureKeyVaultWithManagedIdentity( - keyVaultUri, clientId: clientId, cacheConfiguration: cacheConfiguration, + VaultUri, clientId: ClientId, cacheConfiguration: cacheConfiguration, mutateSecretName: secretName => "SOMETHING-WRONG-" + secretName, configureOptions: null, name: null); }); // Assert - using (TemporaryEnvironmentVariable.Create(Constants.AzureTenantIdEnvironmentVariable, tenantId)) - using (TemporaryEnvironmentVariable.Create(Constants.AzureServicePrincipalClientIdVariable, clientId)) - using (TemporaryEnvironmentVariable.Create(Constants.AzureServicePrincipalClientSecretVariable, clientKey)) + using (TemporaryEnvironmentVariable.Create(Constants.AzureTenantIdEnvironmentVariable, TenantId)) + using (TemporaryEnvironmentVariable.Create(Constants.AzureServicePrincipalClientIdVariable, ClientId)) + using (TemporaryEnvironmentVariable.Create(Constants.AzureServicePrincipalClientSecretVariable, ClientSecret)) { using IHost host = builder.Build(); var provider = host.Services.GetRequiredService(); - Assert.Throws(() => provider.GetSecret(keyName)); - Assert.Throws(() => provider.GetRawSecret(keyName)); - await Assert.ThrowsAsync(() => provider.GetSecretAsync(keyName)); - await Assert.ThrowsAsync(() => provider.GetRawSecretAsync(keyName)); + Assert.Throws(() => provider.GetSecret(TestSecretName)); + Assert.Throws(() => provider.GetRawSecret(TestSecretName)); + await Assert.ThrowsAsync(() => provider.GetSecretAsync(TestSecretName)); + await Assert.ThrowsAsync(() => provider.GetRawSecretAsync(TestSecretName)); } } @@ -1325,59 +1129,50 @@ public async Task AddAzureKeyVault_WithCachedManagedIdentityWrongMutation_GetsSe public async Task AddAzureKeyVaultWithTenant_WithWrongServicePrincipalCredentials_Throws() { // Arrange - var keyVaultUri = Configuration.GetValue("Arcus:KeyVault:Uri"); - string tenantId = Configuration.GetTenantId(); - var keyName = Configuration.GetValue("Arcus:KeyVault:TestKeyName"); - var builder = new HostBuilder(); // Act - builder.ConfigureSecretStore((config, stores) => + builder.ConfigureSecretStore((_, stores) => { - stores.AddAzureKeyVaultWithServicePrincipal(keyVaultUri, tenantId, "wrong-app-id", "wrong-access-key", configureOptions: null, name: null, mutateSecretName: null); + stores.AddAzureKeyVaultWithServicePrincipal(VaultUri, TenantId, "wrong-app-id", "wrong-access-key", configureOptions: null, name: null, mutateSecretName: null); }); // Assert using IHost host = builder.Build(); var provider = host.Services.GetRequiredService(); - Assert.Throws(() => provider.GetSecret(keyName)); - Assert.Throws(() => provider.GetRawSecret(keyName)); - await Assert.ThrowsAsync(() => provider.GetSecretAsync(keyName)); - await Assert.ThrowsAsync(() => provider.GetRawSecretAsync(keyName)); + Assert.Throws(() => provider.GetSecret(TestSecretName)); + Assert.Throws(() => provider.GetRawSecret(TestSecretName)); + await Assert.ThrowsAsync(() => provider.GetSecretAsync(TestSecretName)); + await Assert.ThrowsAsync(() => provider.GetRawSecretAsync(TestSecretName)); } [Fact] public async Task AddAzureKeyVaultWithTenant_WithWrongUnauthorizedServicePrincipal_Throws() { // Arrange - string tenantId = Configuration.GetTenantId(); - string applicationId = Configuration.GetValue("Arcus:UnauthorizedServicePrincipal:ApplicationId"); - var clientKey = Configuration.GetValue("Arcus:UnauthorizedServicePrincipal:AccessKey"); - var keyVaultUri = Configuration.GetValue("Arcus:KeyVault:Uri"); - var keyName = Configuration.GetValue("Arcus:KeyVault:TestKeyName"); - var builder = new HostBuilder(); - builder.UseSerilog(Logger, dispose: true); + builder.UseSerilog(SerilogLogger, dispose: true); + string clientId = Configuration.GetRequiredValue("Arcus:UnauthorizedServicePrincipal:ApplicationId"); + string clientSecret = Configuration.GetRequiredValue("Arcus:UnauthorizedServicePrincipal:AccessKey"); // Act - builder.ConfigureSecretStore((config, stores) => + builder.ConfigureSecretStore((_, stores) => { - stores.AddAzureKeyVaultWithServicePrincipal(keyVaultUri, tenantId, applicationId, clientKey, configureOptions: null, name: null, mutateSecretName: null); + stores.AddAzureKeyVaultWithServicePrincipal(VaultUri, TenantId, clientId, clientSecret, configureOptions: null, name: null, mutateSecretName: null); }); // Assert using IHost host = builder.Build(); var provider = host.Services.GetRequiredService(); - var exceptionFromSecretSync = Assert.Throws(() => provider.GetSecret(keyName)); - var exceptionFromRawSecretSync = Assert.Throws(() => provider.GetRawSecret(keyName)); - var exceptionFromSecretAsync = await Assert.ThrowsAsync(() => provider.GetSecretAsync(keyName)); - var exceptionFromRawSecretAsync = await Assert.ThrowsAsync(() => provider.GetRawSecretAsync(keyName)); - Assert.Equal((int) HttpStatusCode.Forbidden, exceptionFromSecretSync.Status); - Assert.Equal((int) HttpStatusCode.Forbidden, exceptionFromRawSecretSync.Status); - Assert.Equal((int) HttpStatusCode.Forbidden, exceptionFromSecretAsync.Status); - Assert.Equal((int) HttpStatusCode.Forbidden, exceptionFromRawSecretAsync.Status); + Assert.All(new[] + { + Assert.Throws(() => provider.GetSecret(TestSecretName)), + Assert.Throws(() => provider.GetRawSecret(TestSecretName)), + await Assert.ThrowsAsync(() => provider.GetSecretAsync(TestSecretName)), + await Assert.ThrowsAsync(() => provider.GetRawSecretAsync(TestSecretName)) + }, exception => Assert.Equal(StatusCodes.Status403Forbidden, exception.Status)); } private void AssertTrackedAzureKeyVaultDependency(int expectedTrackedDependencyCount) diff --git a/src/Arcus.Security.Tests.Integration/appsettings.json b/src/Arcus.Security.Tests.Integration/appsettings.json index 79b21c98..dbd427c7 100644 --- a/src/Arcus.Security.Tests.Integration/appsettings.json +++ b/src/Arcus.Security.Tests.Integration/appsettings.json @@ -23,8 +23,11 @@ "VaultBin": "#{Arcus.HashiCorp.VaultBin}#", "UserPass": { "UserName": "arcus", - "Password": "123" - } + "Password": "123" + } + }, + "Dapr": { + "DaprBin": "#{Arcus.Dapr.DaprBin}#" }, "ApplicationInsights": { "InstrumentationKey": "#{Arcus.ApplicationInsights.InstrumentationKey}#" diff --git a/src/Arcus.Security.Tests.Unit/Arcus.Security.Tests.Unit.csproj b/src/Arcus.Security.Tests.Unit/Arcus.Security.Tests.Unit.csproj index 31a1b9c2..24d08520 100644 --- a/src/Arcus.Security.Tests.Unit/Arcus.Security.Tests.Unit.csproj +++ b/src/Arcus.Security.Tests.Unit/Arcus.Security.Tests.Unit.csproj @@ -32,6 +32,7 @@ + diff --git a/src/Arcus.Security.Tests.Unit/Dapr/DaprSecretProviderOptionsTests.cs b/src/Arcus.Security.Tests.Unit/Dapr/DaprSecretProviderOptionsTests.cs new file mode 100644 index 00000000..10488f78 --- /dev/null +++ b/src/Arcus.Security.Tests.Unit/Dapr/DaprSecretProviderOptionsTests.cs @@ -0,0 +1,80 @@ +using System; +using Arcus.Security.Providers.Dapr; +using Xunit; + +namespace Arcus.Security.Tests.Unit.Dapr +{ + public class DaprSecretProviderOptionsTests + { + [Theory] + [ClassData(typeof(Blanks))] + public void Set_WithoutGrpcEndpoint_Fails(string grpcEndpoint) + { + // Arrange + var options = new DaprSecretProviderOptions(); + + // Act / Assert + Assert.ThrowsAny(() => options.GrpcEndpoint = grpcEndpoint); + } + + [Theory] + [ClassData(typeof(Blanks))] + public void Set_WithoutHttpEndpoint_Fails(string httpEndpoint) + { + // Arrange + var options = new DaprSecretProviderOptions(); + + // Act / Assert + Assert.ThrowsAny(() => options.HttpEndpoint = httpEndpoint); + } + + [Theory] + [ClassData(typeof(Blanks))] + public void Set_WithoutDaprApiToken_Fails(string apiToken) + { + // Arrange + var options = new DaprSecretProviderOptions(); + + // Act / Assert + Assert.ThrowsAny(() => options.DaprApiToken = apiToken); + } + + [Theory] + [ClassData(typeof(Blanks))] + public void AddMetadata_WithoutKey_Fails(string key) + { + // Arrange + var options = new DaprSecretProviderOptions(); + var value = Guid.NewGuid().ToString(); + + // Act / Assert + Assert.ThrowsAny(() => options.AddMetadata(key, value)); + } + + [Theory] + [ClassData(typeof(Blanks))] + public void AddMetadata_WithoutValue_Fails(string value) + { + // Arrange + var options = new DaprSecretProviderOptions(); + var key = Guid.NewGuid().ToString(); + + // Act / Assert + Assert.ThrowsAny(() => options.AddMetadata(key, value)); + } + + [Fact] + public void AddMetadata_WithSameKey_Fails() + { + // Arrange + var options = new DaprSecretProviderOptions(); + var key = Guid.NewGuid().ToString(); + var value1 = Guid.NewGuid().ToString(); + var value2 = Guid.NewGuid().ToString(); + options.AddMetadata(key, value1); + + // Act / Assert + Assert.ThrowsAny(() => options.AddMetadata(key, value2)); + } + } +} diff --git a/src/Arcus.Security.Tests.Unit/Dapr/DaprSecretProviderTests.cs b/src/Arcus.Security.Tests.Unit/Dapr/DaprSecretProviderTests.cs new file mode 100644 index 00000000..7b33db67 --- /dev/null +++ b/src/Arcus.Security.Tests.Unit/Dapr/DaprSecretProviderTests.cs @@ -0,0 +1,17 @@ +using System; +using Arcus.Security.Providers.Dapr; +using Microsoft.Extensions.Logging.Abstractions; +using Xunit; + +namespace Arcus.Security.Tests.Unit.Dapr +{ + public class DaprSecretProviderTests + { + [Theory] + [ClassData(typeof(Blanks))] + public void Create_WithoutStoreName_Fails(string storeName) + { + Assert.ThrowsAny(() => new DaprSecretProvider(storeName, new DaprSecretProviderOptions(), NullLogger.Instance)); + } + } +} diff --git a/src/Arcus.Security.Tests.Unit/Dapr/Extensions/SecretStoreExtensionsTests.cs b/src/Arcus.Security.Tests.Unit/Dapr/Extensions/SecretStoreExtensionsTests.cs new file mode 100644 index 00000000..be901d3f --- /dev/null +++ b/src/Arcus.Security.Tests.Unit/Dapr/Extensions/SecretStoreExtensionsTests.cs @@ -0,0 +1,32 @@ +using System; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Xunit; + +namespace Arcus.Security.Tests.Unit.Dapr.Extensions +{ + public class SecretStoreExtensionsTests + { + [Theory] + [ClassData(typeof(Blanks))] + public void AddDaprSecretStore_WithoutSecretStore_Fails(string secretStore) + { + // Arrange + var services = new ServiceCollection(); + + // Act / Assert + Assert.ThrowsAny(() => services.AddSecretStore(stores => stores.AddDaprSecretStore(secretStore))); + } + + [Theory] + [ClassData(typeof(Blanks))] + public void AddDaprSecretStoreWithConfig_WithoutSecretStore_Fails(string secretStore) + { + // Arrange + var services = new ServiceCollection(); + + // Act / Assert + Assert.ThrowsAny(() => services.AddSecretStore(stores => stores.AddDaprSecretStore(secretStore, opt => { }))); + } + } +} diff --git a/src/Arcus.Security.sln b/src/Arcus.Security.sln index 68868bbc..24163ba3 100644 --- a/src/Arcus.Security.sln +++ b/src/Arcus.Security.sln @@ -29,6 +29,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Arcus.Security.Providers.Co EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Arcus.Security.Tests.Runtimes.AzureFunctions", "Arcus.Security.Tests.Runtimes.AzureFunctions\Arcus.Security.Tests.Runtimes.AzureFunctions.csproj", "{B571C9E1-67D9-414D-9204-C5167B01A0E0}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Arcus.Security.Providers.Dapr", "Arcus.Security.Providers.Dapr\Arcus.Security.Providers.Dapr.csproj", "{EDF973BE-D1F5-497E-A47E-EF81ABABEFC7}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -79,6 +81,10 @@ Global {B571C9E1-67D9-414D-9204-C5167B01A0E0}.Debug|Any CPU.Build.0 = Debug|Any CPU {B571C9E1-67D9-414D-9204-C5167B01A0E0}.Release|Any CPU.ActiveCfg = Release|Any CPU {B571C9E1-67D9-414D-9204-C5167B01A0E0}.Release|Any CPU.Build.0 = Release|Any CPU + {EDF973BE-D1F5-497E-A47E-EF81ABABEFC7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EDF973BE-D1F5-497E-A47E-EF81ABABEFC7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EDF973BE-D1F5-497E-A47E-EF81ABABEFC7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EDF973BE-D1F5-497E-A47E-EF81ABABEFC7}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -93,6 +99,7 @@ Global {EBCA7E71-5F09-43B3-BC44-73B1980477C7} = {DD66B1E8-3676-4C13-8C37-AEA80E1C21B7} {D6749062-90BB-4828-86ED-FD8F89ACE8AB} = {DD66B1E8-3676-4C13-8C37-AEA80E1C21B7} {B571C9E1-67D9-414D-9204-C5167B01A0E0} = {C42FDAC3-5A31-4F39-BD92-C57C2CDAB2D2} + {EDF973BE-D1F5-497E-A47E-EF81ABABEFC7} = {DD66B1E8-3676-4C13-8C37-AEA80E1C21B7} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {D3310D31-C37D-47F4-A6D3-325EE3806698}