Skip to content

Commit

Permalink
FEAT: add 'ISecretProvider' with '.AddAzureKeyVault' configuratio… (#71)
Browse files Browse the repository at this point in the history
* 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
stijnmoreels authored and tomkerkhove committed Aug 2, 2019
1 parent c84647d commit 2aecbd4
Show file tree
Hide file tree
Showing 7 changed files with 318 additions and 0 deletions.
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>();
```
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
<PackageReference Include="Microsoft.Azure.KeyVault.Core" Version="3.0.1" />
<PackageReference Include="Microsoft.Azure.KeyVault.WebKey" Version="3.0.2" />
<PackageReference Include="Microsoft.Azure.Services.AppAuthentication" Version="1.0.3" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="2.2.0" />
<PackageReference Include="Microsoft.IdentityModel.Clients.ActiveDirectory" Version="4.4.2" />
</ItemGroup>

Expand Down
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;
}
}
}
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);
}
}
}
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 src/Arcus.Security.Tests.Unit/Core/Stubs/InMemorySecretProvider.cs
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);
}
}
}
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);
}
}
}

0 comments on commit 2aecbd4

Please sign in to comment.