-
Notifications
You must be signed in to change notification settings - Fork 13
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
FEAT: add 'ISecretProvider' with '.AddAzureKeyVault' configuratio… (#71)
* FEAT: add 'ISecretProvider' with '.AddAzureKeyVault' configuration builder Adds an extension to the IConfigurationBuilder so we can pass-in our own ISecretProvider when replacing configuration tokens with secret values. * PR-DOC: update correct package name
- Loading branch information
1 parent
c84647d
commit 2aecbd4
Showing
7 changed files
with
318 additions
and
0 deletions.
There are no files selected for viewing
27 changes: 27 additions & 0 deletions
27
docs/features/config/replace-configuration-tokens-with-isecretprovider.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
--- | ||
title: "Replace configuration tokens with ISecretProvider" | ||
layout: default | ||
--- | ||
|
||
The `Arcus.Security.Providers.AzureKeyVault` package provides a mechanism to use your own `ISecretProvider` implementation when building your configuration for your application. | ||
|
||
### Usage | ||
|
||
When building your IConfiguration, you can use the extension .AddAzureKeyVault to pass in your ISecretProvider instead of using the built-in [Azure Key Vault provider](https://docs.microsoft.com/en-us/aspnet/core/security/key-vault-configuration?view=aspnetcore-2.2#packages). | ||
|
||
Example how the configuration builder is used inside a web application: | ||
|
||
```csharp | ||
var vaultAuthenticator = new ManagedServiceIdentityAuthenticator(); | ||
var vaultConfiguration = new KeyVaultConfiguration(keyVaultUri); | ||
var yourSecretProvider = new KeyVaultSecretProvider(vaultAuthenticator, vaultConfiguration); | ||
|
||
var config = new ConfigurationBuilder() | ||
.AddAzureKeyVault(yourSecretProvider) | ||
.Build(); | ||
|
||
var host = new WebHostBuilder() | ||
.UseConfiguration(config) | ||
.UseKestrel() | ||
.UseStartup<Startup>(); | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
47 changes: 47 additions & 0 deletions
47
src/Arcus.Security.Providers.AzureKeyVault/Configuration/ArcusConfigurationProvider.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
using System.Threading.Tasks; | ||
using Microsoft.Extensions.Configuration; | ||
using GuardNet; | ||
using Arcus.Security.Secrets.Core.Interfaces; | ||
|
||
namespace Arcus.Security.Providers.AzureKeyVault.Configuration | ||
{ | ||
/// <summary> | ||
/// Provider to retrieve configuration tokens via an <see cref="ISecretProvider"/> implementation. | ||
/// </summary> | ||
internal class ArcusConfigurationProvider : ConfigurationProvider | ||
{ | ||
private readonly ISecretProvider _secretProvider; | ||
|
||
/// <summary> | ||
/// Initializes a new instance of the <see cref="ArcusConfigurationProvider"/> class. | ||
/// </summary> | ||
/// <param name="secretProvider">The provder to retrieve secret values for configuration tokens.</param> | ||
internal ArcusConfigurationProvider(ISecretProvider secretProvider) | ||
{ | ||
Guard.NotNull(secretProvider, nameof(secretProvider), $"Requires a {nameof(ISecretProvider)} instance"); | ||
|
||
_secretProvider = secretProvider; | ||
} | ||
|
||
/// <summary> | ||
/// Attempts to find a value with the given key, returns true if one is found, false otherwise. | ||
/// </summary> | ||
/// <param name="key">The key to lookup.</param> | ||
/// <param name="value">The value found at key if one is found.</param> | ||
/// <returns>True if key has a value, false otherwise.</returns> | ||
public override bool TryGet(string key, out string value) | ||
{ | ||
Task<string> getSecretValueAsync = _secretProvider.Get(key); | ||
if (getSecretValueAsync != null) | ||
{ | ||
string secretValue = getSecretValueAsync.ConfigureAwait(false).GetAwaiter().GetResult(); | ||
|
||
value = secretValue; | ||
return secretValue != null; | ||
} | ||
|
||
value = null; | ||
return false; | ||
} | ||
} | ||
} |
37 changes: 37 additions & 0 deletions
37
src/Arcus.Security.Providers.AzureKeyVault/Configuration/ArcusConfigurationSource.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
using Arcus.Security.Secrets.Core.Interfaces; | ||
using GuardNet; | ||
using Microsoft.Extensions.Configuration; | ||
|
||
namespace Arcus.Security.Providers.AzureKeyVault.Configuration | ||
{ | ||
/// <summary> | ||
/// Represents the configuration source to provide configuration key/values for an application. | ||
/// </summary> | ||
internal class ArcusConfigurationSource : IConfigurationSource | ||
{ | ||
private readonly ISecretProvider _secretProvider; | ||
|
||
/// <summary> | ||
/// Initializes a new instance of the <see cref="ArcusConfigurationSource"/> class. | ||
/// </summary> | ||
/// <param name="secretProvider">The provider to retrieve secret values for configuration tokens.</param> | ||
internal ArcusConfigurationSource(ISecretProvider secretProvider) | ||
{ | ||
Guard.NotNull(secretProvider, nameof(secretProvider), $"Requires an {nameof(ISecretProvider)} instance"); | ||
|
||
_secretProvider = secretProvider; | ||
} | ||
|
||
/// <summary> | ||
/// Builds the Microsoft.Extensions.Configuration.IConfigurationProvider for this source. | ||
/// </summary> | ||
/// <param name="builder">The Microsoft.Extensions.Configuration.IConfigurationBuilder.</param> | ||
/// <returns>An Microsoft.Extensions.Configuration.IConfigurationProvider</returns> | ||
public IConfigurationProvider Build(IConfigurationBuilder builder) | ||
{ | ||
Guard.NotNull(builder, nameof(builder), $"Requires and {nameof(IConfigurationBuilder)} instance"); | ||
|
||
return new ArcusConfigurationProvider(_secretProvider); | ||
} | ||
} | ||
} |
26 changes: 26 additions & 0 deletions
26
...ity.Providers.AzureKeyVault/Configuration/SecretProviderConfigurationBuilderExtensions.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
using Arcus.Security.Secrets.Core.Interfaces; | ||
using GuardNet; | ||
using Microsoft.Extensions.Configuration; | ||
|
||
namespace Arcus.Security.Providers.AzureKeyVault.Configuration | ||
{ | ||
/// <summary> | ||
/// Provide extensions to use an <see cref="ISecretProvider"/> implementation to retrieve secret values from configuration tokens. | ||
/// </summary> | ||
public static class SecretProviderConfigurationBuilderExtensions | ||
{ | ||
/// <summary> | ||
/// Adds an <see cref="IConfigurationProvider"/> that reads configuration values from the Azure KeyVault with an <see cref="ISecretProvider"/> implementation. | ||
/// </summary> | ||
/// <param name="configurationBuilder">The <see cref="IConfigurationBuilder"/> to add to.</param> | ||
/// <param name="secretProvider">The provider to retrieve the secret values for configuration tokens.</param> | ||
/// <returns>The <see cref="IConfigurationBuilder"/>.</returns> | ||
public static IConfigurationBuilder AddAzureKeyVault(this IConfigurationBuilder configurationBuilder, ISecretProvider secretProvider) | ||
{ | ||
Guard.NotNull(configurationBuilder, nameof(configurationBuilder), $"Requires an {nameof(IConfigurationBuilder)} instance"); | ||
Guard.NotNull(secretProvider, nameof(secretProvider), $"Requires an {nameof(ISecretProvider)} instance"); | ||
|
||
return configurationBuilder.Add(new ArcusConfigurationSource(secretProvider)); | ||
} | ||
} | ||
} |
125 changes: 125 additions & 0 deletions
125
src/Arcus.Security.Tests.Unit/Core/Stubs/InMemorySecretProvider.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
using System.Threading.Tasks; | ||
using Arcus.Security.Secrets.Core.Interfaces; | ||
using Arcus.Security.Secrets.Core.Models; | ||
using GuardNet; | ||
|
||
namespace Arcus.Security.Tests.Unit.Core.Stubs | ||
{ | ||
/// <summary> | ||
/// <see cref="ISecretProvider"/> implementation that provides an in-memory storage of secrets by name. | ||
/// </summary> | ||
public class InMemorySecretProvider : ISecretProvider | ||
{ | ||
private readonly IDictionary<string, string> _secretValueByName; | ||
|
||
/// <summary> | ||
/// Initializes a new instance of the <see cref="InMemorySecretProvider"/> class. | ||
/// </summary> | ||
/// <param name="secretValueByName">The sequence of combinations of secret names and values.</param> | ||
public InMemorySecretProvider(params (string name, string value)[] secretValueByName) | ||
{ | ||
Guard.NotNull(secretValueByName, "Secret name/value combinations cannot be 'null'"); | ||
|
||
_secretValueByName = secretValueByName.ToDictionary(t => t.name, t => t.value); | ||
} | ||
|
||
/// <summary> | ||
/// Initializes a new instance of the <see cref="InMemorySecretProvider"/> class. | ||
/// </summary> | ||
/// <param name="secretValueByName">The sequence of combinations of secret names and values.</param> | ||
public InMemorySecretProvider(IDictionary<string, string> secretValueByName) | ||
{ | ||
Guard.NotNull(secretValueByName, "Secret name/value combinations cannot be 'null'"); | ||
|
||
_secretValueByName = secretValueByName; | ||
} | ||
|
||
/// <summary> | ||
/// Retrieves the secret value, based on the given name | ||
/// </summary> | ||
/// <param name="secretName">The name of the secret key</param> | ||
/// <returns>Returns the secret key</returns> | ||
/// <exception cref="ArgumentException">The name must not be empty</exception> | ||
/// <exception cref="ArgumentNullException">The name must not be null</exception> | ||
/// <exception cref="SecretNotFoundException">The secret was not found, using the given name</exception> | ||
[Obsolete("Use the " + nameof(GetRawSecret) + " method instead")] | ||
public async Task<string> Get(string secretName) | ||
{ | ||
return await GetRawSecretAsync(secretName); | ||
} | ||
|
||
/// <summary> | ||
/// Retrieves the secret value, based on the given name | ||
/// </summary> | ||
/// <param name="secretName">The name of the secret key</param> | ||
/// <returns>Returns a <see cref="Secret"/> that contains the secret key</returns> | ||
/// <exception cref="ArgumentException">The name must not be empty</exception> | ||
/// <exception cref="ArgumentNullException">The name must not be null</exception> | ||
/// <exception cref="SecretNotFoundException">The secret was not found, using the given name</exception> | ||
[Obsolete("Use the " + nameof(GetSecretAsync) + " method instead")] | ||
public async Task<Secret> GetSecret(string secretName) | ||
{ | ||
return await GetSecretAsync(secretName); | ||
} | ||
|
||
/// <summary> | ||
/// Retrieves the secret value, based on the given name | ||
/// </summary> | ||
/// <param name="secretName">The name of the secret key</param> | ||
/// <returns>Returns a <see cref="Secret"/> that contains the secret key</returns> | ||
/// <exception cref="ArgumentException">The name must not be empty</exception> | ||
/// <exception cref="ArgumentNullException">The name must not be null</exception> | ||
/// <exception cref="SecretNotFoundException">The secret was not found, using the given name</exception> | ||
public Task<Secret> GetSecretAsync(string secretName) | ||
{ | ||
Guard.NotNull(secretName, "Secret name cannot be 'null'"); | ||
|
||
if (_secretValueByName.TryGetValue(secretName, out string secretValue)) | ||
{ | ||
var secret = new Secret(secretValue, version: $"v-{Guid.NewGuid()}"); | ||
return Task.FromResult(secret); | ||
} | ||
|
||
return Task.FromResult<Secret>(null); | ||
} | ||
|
||
/// <summary> | ||
/// Retrieves the secret value, based on the given name | ||
/// </summary> | ||
/// <param name="secretName">The name of the secret key</param> | ||
/// <returns>Returns the secret key.</returns> | ||
/// <exception cref="ArgumentException">The name must not be empty</exception> | ||
/// <exception cref="ArgumentNullException">The name must not be null</exception> | ||
/// <exception cref="SecretNotFoundException">The secret was not found, using the given name</exception> | ||
[Obsolete("Use the " + nameof(GetRawSecretAsync) + " method instead")] | ||
public async Task<string> GetRawSecret(string secretName) | ||
{ | ||
return await GetRawSecretAsync(secretName); | ||
} | ||
|
||
|
||
|
||
/// <summary> | ||
/// Retrieves the secret value, based on the given name | ||
/// </summary> | ||
/// <param name="secretName">The name of the secret key</param> | ||
/// <returns>Returns the secret key.</returns> | ||
/// <exception cref="ArgumentException">The name must not be empty</exception> | ||
/// <exception cref="ArgumentNullException">The name must not be null</exception> | ||
/// <exception cref="SecretNotFoundException">The secret was not found, using the given name</exception> | ||
public Task<string> GetRawSecretAsync(string secretName) | ||
{ | ||
Guard.NotNull(secretName, "Secret name cannot be 'null'"); | ||
|
||
if (_secretValueByName.TryGetValue(secretName, out string secretValue)) | ||
{ | ||
return Task.FromResult(secretValue); | ||
} | ||
|
||
return Task.FromResult<string>(null); | ||
} | ||
} | ||
} |
55 changes: 55 additions & 0 deletions
55
...ty.Tests.Unit/KeyVault/Configuration/SecretProviderConfigurationBuilderExtensionsTests.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
using System.Collections.Generic; | ||
using Microsoft.Extensions.Configuration; | ||
using Arcus.Security.Providers.AzureKeyVault.Configuration; | ||
using Xunit; | ||
using Arcus.Security.Tests.Unit.Core.Stubs; | ||
|
||
namespace Arcus.Security.Tests.Unit.KeyVault.Configuration | ||
{ | ||
public class SecretProviderConfigurationBuilderExtensionsTests | ||
{ | ||
[Fact] | ||
public void AddAzureKeyVault_WithSecretWithConfigurationKey_AccessesSecretProviderForSecretValuesFromConfigurationTokens_ResolvesConfigurationToken() | ||
{ | ||
// Arrange | ||
const string configurationKey = "Connection_String"; | ||
const string expected = "connection to somewhere"; | ||
|
||
var stubProvider = new InMemorySecretProvider((configurationKey, expected)); | ||
|
||
var configuration = | ||
new ConfigurationBuilder() | ||
.AddInMemoryCollection(new [] { new KeyValuePair<string, string>("ConnectionString", $"#{{{configurationKey}}}#") }) | ||
.AddAzureKeyVault(stubProvider) | ||
.Build(); | ||
|
||
// Act | ||
IConfigurationSection section = configuration.GetSection(configurationKey); | ||
|
||
// Assert | ||
Assert.Equal(expected, section.Value); | ||
} | ||
|
||
[Fact] | ||
public void AddAzureKeyVault_WithoutSecretWithConfigurationKey_AccessesSecretProviderForSecretValuesFromConfigurationTokens_ButDontResolveConfigurationToken() | ||
{ | ||
// Arrange | ||
const string configurationKey = "ConnectionString"; | ||
const string configurationToken = "#{ConnectionString}#"; | ||
|
||
var stubProvider = new InMemorySecretProvider(("Some other secret key name", "Some other secret value")); | ||
|
||
var configuration = | ||
new ConfigurationBuilder() | ||
.AddInMemoryCollection(new [] { new KeyValuePair<string, string>(configurationKey, configurationToken) }) | ||
.AddAzureKeyVault(stubProvider) | ||
.Build(); | ||
|
||
// Act | ||
IConfigurationSection section = configuration.GetSection(configurationKey); | ||
|
||
// Assert | ||
Assert.Equal(configurationToken, section.Value); | ||
} | ||
} | ||
} |