Secret Store configuration provider implementation for Microsoft.Extensions.Configuration (#238)

* Implementing Dapr Secret Store configuration provider

* Improved exception message

Co-authored-by: Aman Bhardwaj <amanbha@users.noreply.github.com>
This commit is contained in:
Carlos Mendible 2020-03-18 18:43:19 +01:00 committed by GitHub
parent ae22eb28a5
commit f36b52107f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 370 additions and 10 deletions

View File

@ -18,6 +18,7 @@ This repo builds the following packages:
- Dapr.AspNetCore
- Dapr.Actors
- Dapr.Actors.AspNetCore
- Dapr.Extensions.Configuration
## Getting Started

14
all.sln
View File

@ -50,6 +50,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
.editorconfig = .editorconfig
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Extensions.Configuration", "src\Dapr.Extensions.Configuration\Dapr.Extensions.Configuration.csproj", "{3EFB6020-F928-4FF4-A795-6B426C3886A9}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Extensions.Configuration.Test", "test\Dapr.Extensions.Configuration.Test\Dapr.Extensions.Configuration.Test.csproj", "{E39DC087-2521-4BB3-933E-E8AFD74AFA6F}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -116,6 +120,14 @@ Global
{576B28F7-0D86-4C11-828A-B648E11863F6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{576B28F7-0D86-4C11-828A-B648E11863F6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{576B28F7-0D86-4C11-828A-B648E11863F6}.Release|Any CPU.Build.0 = Release|Any CPU
{3EFB6020-F928-4FF4-A795-6B426C3886A9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3EFB6020-F928-4FF4-A795-6B426C3886A9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3EFB6020-F928-4FF4-A795-6B426C3886A9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3EFB6020-F928-4FF4-A795-6B426C3886A9}.Release|Any CPU.Build.0 = Release|Any CPU
{E39DC087-2521-4BB3-933E-E8AFD74AFA6F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E39DC087-2521-4BB3-933E-E8AFD74AFA6F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E39DC087-2521-4BB3-933E-E8AFD74AFA6F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E39DC087-2521-4BB3-933E-E8AFD74AFA6F}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -139,6 +151,8 @@ Global
{DCBEAB71-0BDE-42F4-8187-F576942CB038} = {B2DB41EE-45F5-447B-95E8-38E1E8B70C4E}
{576B28F7-0D86-4C11-828A-B648E11863F6} = {9ED03812-9DDE-4DFE-9574-DF3671EECA25}
{9ED03812-9DDE-4DFE-9574-DF3671EECA25} = {B2DB41EE-45F5-447B-95E8-38E1E8B70C4E}
{3EFB6020-F928-4FF4-A795-6B426C3886A9} = {27C5D71D-0721-4221-9286-B94AB07B58CF}
{E39DC087-2521-4BB3-933E-E8AFD74AFA6F} = {DD020B34-460F-455F-8D17-CF4A949F100B}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {65220BF2-EAE1-4CB2-AA58-EBE80768CB40}

View File

@ -74,12 +74,6 @@ stages:
versioningScheme: 'off'
- task: DotNetCoreCLI@2
displayName: 'Package Dapr.Client.Grpc nuget - $(Configuration)'
inputs:
command: 'pack'
arguments: '--configuration $(Configuration)'
packagesToPack: 'src/Dapr.Client.Grpc/Dapr.Client.Grpc.csproj'
nobuild: true
versioningScheme: 'off'
- task: CopyFiles@2
condition: ne(variables['Build.Reason'], 'PullRequest')
inputs:

View File

@ -6,3 +6,4 @@
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Dapr.Client.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")]
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Dapr.AspNetCore.IntegrationTest, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")]
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Dapr.AspNetCore.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")]
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Dapr.Extensions.Configuration.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")]

View File

@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup>
<!-- Additional Nuget package properties. -->
<PropertyGroup>
<Description>Dapr Secret Store configuration provider implementation for Microsoft.Extensions.Configuration.</Description>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Dapr.Client\Dapr.Client.csproj" />
</ItemGroup>
<ItemGroup>
<Compile Include="..\Dapr.Client\ArgumentVerifier.cs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration" Version="3.1.2" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,42 @@
// ------------------------------------------------------------
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
// ------------------------------------------------------------
using System.Collections.Generic;
namespace Dapr.Extensions.Configuration
{
/// <summary>
/// Represents the name and metadata for a Secret.
/// </summary>
public class DaprSecretDescriptor
{
/// <summary>
/// Gets or sets the secret name.
/// </summary>
public string SecretName { get; }
/// <summary>
/// Gets or sets the secret's metadata.
/// </summary>
public Dictionary<string, string> Metadata { get; }
/// <summary>
/// Secret Descriptor Construcutor
/// </summary>
public DaprSecretDescriptor(string secretName) : this(secretName, new Dictionary<string, string>())
{
}
/// <summary>
/// Secret Descriptor Construcutor
/// </summary>
public DaprSecretDescriptor(string secretName, Dictionary<string, string> metadata)
{
SecretName = secretName;
Metadata = metadata;
}
}
}

View File

@ -0,0 +1,56 @@
// ------------------------------------------------------------
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
// ------------------------------------------------------------
using System;
using System.Collections.Generic;
using Dapr.Client;
using Microsoft.Extensions.Configuration;
using Dapr.Extensions.Configuration.DaprSecretStore;
namespace Dapr.Extensions.Configuration
{
/// <summary>
/// Extension methods for registering <see cref="DaprSecretStoreConfigurationProvider"/> with <see cref="IConfigurationBuilder"/>.
/// </summary>
public static class DaprSecretStoreConfigurationExtensions
{
/// <summary>
/// Adds an <see cref="IConfigurationProvider"/> that reads configuration values from the Dapr Secret Store.
/// </summary>
/// <param name="configurationBuilder">The <see cref="IConfigurationBuilder"/> to add to.</param>
/// <param name="store">Dapr secret store name.</param>
/// <param name="secretDescriptors">The secrets to retrieve.</param>
/// <param name="client">The Dapr client</param>
/// <returns>The <see cref="IConfigurationBuilder"/>.</returns>
public static IConfigurationBuilder AddDaprSecretStore(
this IConfigurationBuilder configurationBuilder,
string store,
IEnumerable<DaprSecretDescriptor> secretDescriptors,
DaprClient client)
{
ArgumentVerifier.ThrowIfNullOrEmpty(store, nameof(store));
ArgumentVerifier.ThrowIfNull(secretDescriptors, nameof(secretDescriptors));
ArgumentVerifier.ThrowIfNull(client, nameof(client));
configurationBuilder.Add(new DaprSecretStoreConfigurationSource()
{
Store = store,
SecretDescriptors = secretDescriptors,
Client = client
});
return configurationBuilder;
}
/// <summary>
/// Adds an <see cref="IConfigurationProvider"/> that reads configuration values from the command line.
/// </summary>
/// <param name="configurationBuilder">The <see cref="IConfigurationBuilder"/> to add to.</param>
/// <param name="configureSource">Configures the source.</param>
/// <returns>The <see cref="IConfigurationBuilder"/>.</returns>
public static IConfigurationBuilder AddDaprSecretStore(this IConfigurationBuilder configurationBuilder, Action<DaprSecretStoreConfigurationSource> configureSource)
=> configurationBuilder.Add(configureSource);
}
}

View File

@ -0,0 +1,65 @@
// ------------------------------------------------------------
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
// ------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Dapr.Client;
using Microsoft.Extensions.Configuration;
namespace Dapr.Extensions.Configuration.DaprSecretStore
{
/// <summary>
/// A Dapr Secret Store based <see cref="ConfigurationProvider"/>.
/// </summary>
internal class DaprSecretStoreConfigurationProvider : ConfigurationProvider
{
private readonly string store;
private readonly IEnumerable<DaprSecretDescriptor> secretDescriptors;
private readonly DaprClient client;
/// <summary>
/// Creates a new instance of <see cref="DaprSecretStoreConfigurationProvider"/>.
/// </summary>
/// <param name="store">Dapr Secre Store name.</param>
/// <param name="secretDescriptors">The secrets to retrieve.</param>
/// <param name="client">Dapr client used to retrieve Secrets</param>
public DaprSecretStoreConfigurationProvider(string store, IEnumerable<DaprSecretDescriptor> secretDescriptors, DaprClient client)
{
ArgumentVerifier.ThrowIfNullOrEmpty(store, nameof(store));
ArgumentVerifier.ThrowIfNull(secretDescriptors, nameof(secretDescriptors));
ArgumentVerifier.ThrowIfNull(client, nameof(client));
this.store = store;
this.secretDescriptors = secretDescriptors;
this.client = client;
}
public override void Load() => LoadAsync().ConfigureAwait(false).GetAwaiter().GetResult();
private async Task LoadAsync()
{
var data = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
foreach (var secretDescriptor in secretDescriptors)
{
var result = await client.GetSecretAsync(store, secretDescriptor.SecretName, secretDescriptor.Metadata).ConfigureAwait(false);
foreach (var key in result.Keys)
{
if (data.ContainsKey(key))
{
throw new InvalidOperationException($"A duplicate key '{key}' was found in the secret store '{store}'. Please remove any duplicates from your secret store.");
}
data.Add(key, result[key]);
}
}
Data = data;
}
}
}

View File

@ -0,0 +1,38 @@
// ------------------------------------------------------------
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
// ------------------------------------------------------------
using System.Collections.Generic;
using Dapr.Client;
using Microsoft.Extensions.Configuration;
namespace Dapr.Extensions.Configuration.DaprSecretStore
{
/// <summary>
/// Represents Dapr Secret Store as an <see cref="IConfigurationSource"/>.
/// </summary>
public class DaprSecretStoreConfigurationSource : IConfigurationSource
{
/// <summary>
/// Gets or sets the store name.
/// </summary>
public string Store { get; set; }
/// <summary>
/// Gets or sets the secret names.
/// </summary>
public IEnumerable<DaprSecretDescriptor> SecretDescriptors { get; set; }
/// <summary>
/// Gets or sets the http client.
/// </summary>
public DaprClient Client { get; set; }
/// <inheritdoc />
public IConfigurationProvider Build(IConfigurationBuilder builder)
{
return new DaprSecretStoreConfigurationProvider(Store, SecretDescriptors, Client);
}
}
}

View File

@ -11,6 +11,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapr.Client", "Dapr.Client\
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapr.AspNetCore", "Dapr.AspNetCore\Dapr.AspNetCore.csproj", "{08D602F6-7C11-4653-B70B-B56333BF6FD2}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Extensions.Configuration", "Dapr.Extensions.Configuration\Dapr.Extensions.Configuration.csproj", "{3757D4B4-D084-4938-88CF-6539B6E3D08C}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -33,6 +35,10 @@ Global
{08D602F6-7C11-4653-B70B-B56333BF6FD2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{08D602F6-7C11-4653-B70B-B56333BF6FD2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{08D602F6-7C11-4653-B70B-B56333BF6FD2}.Release|Any CPU.Build.0 = Release|Any CPU
{3757D4B4-D084-4938-88CF-6539B6E3D08C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3757D4B4-D084-4938-88CF-6539B6E3D08C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3757D4B4-D084-4938-88CF-6539B6E3D08C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3757D4B4-D084-4938-88CF-6539B6E3D08C}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

View File

@ -0,0 +1,30 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentAssertions" Version="5.9.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.3.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<Compile Include="..\Shared\TestHttpClient.cs" />
<Compile Include="..\Shared\GrpcUtils.cs" />
<Compile Include="..\Shared\ProtobufUtils.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Dapr.Extensions.Configuration\Dapr.Extensions.Configuration.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration" Version="3.1.2" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,87 @@
// ------------------------------------------------------------
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
// ------------------------------------------------------------
using System;
using System.Collections.Generic;
using Autogenerated = Dapr.Client.Autogen.Grpc;
using FluentAssertions;
using Xunit;
using Microsoft.Extensions.Configuration;
using Dapr.Client;
using Grpc.Net.Client;
using System.Net;
namespace Dapr.Extensions.Configuration.Test
{
public class DaprSecretStoreConfigurationProviderTest
{
[Fact]
public void LoadSecrets_FromSecretStoreThatReturnsOneValue()
{
// Configure Client
var httpClient = new TestHttpClient()
{
Handler = async (entry) =>
{
var secrets = new Dictionary<string, string>() { { "secretName", "secret" } };
var secretResponse = new Autogenerated.GetSecretResponseEnvelope();
secretResponse.Data.Add(secrets);
var streamContent = await GrpcUtils.CreateResponseContent(secretResponse);
var response = GrpcUtils.CreateResponse(HttpStatusCode.OK, streamContent);
entry.Completion.SetResult(response);
}
};
var daprClient = new DaprClientBuilder()
.UseGrpcChannelOptions(new GrpcChannelOptions { HttpClient = httpClient })
.Build();
var config = CreateBuilder()
.AddDaprSecretStore("store", new DaprSecretDescriptor[] { new DaprSecretDescriptor("secretName") }, daprClient)
.Build();
config["secretName"].Should().Be("secret");
}
[Fact]
public void LoadSecrets_FromSecretStoreThatCanReturnsMultipleValues()
{
// Configure Client
var httpClient = new TestHttpClient()
{
Handler = async (entry) =>
{
var secrets = new Dictionary<string, string>() {
{ "first_secret", "secret1" },
{ "second_secret", "secret2" }};
var secretResponse = new Autogenerated.GetSecretResponseEnvelope();
secretResponse.Data.Add(secrets);
var streamContent = await GrpcUtils.CreateResponseContent(secretResponse);
var response = GrpcUtils.CreateResponse(HttpStatusCode.OK, streamContent);
entry.Completion.SetResult(response);
}
};
var daprClient = new DaprClientBuilder()
.UseGrpcChannelOptions(new GrpcChannelOptions { HttpClient = httpClient })
.Build();
var config = CreateBuilder()
.AddDaprSecretStore("store", new DaprSecretDescriptor[] { new DaprSecretDescriptor("secretName") }, daprClient)
.Build();
config["first_secret"].Should().Be("secret1");
config["second_secret"].Should().Be("secret2");
}
private IConfigurationBuilder CreateBuilder()
{
return new ConfigurationBuilder();
}
}
}

View File

@ -25,6 +25,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapr.AspNetCore.Integration
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapr.AspNetCore.IntegrationTest.App", "Dapr.AspNetCore.IntegrationTest.App\Dapr.AspNetCore.IntegrationTest.App.csproj", "{342783B5-F75B-4752-A3E2-B8CB7D09C080}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Extensions.Configuration.Test", "Dapr.Extensions.Configuration.Test\Dapr.Extensions.Configuration.Test.csproj", "{F676A444-9099-4382-9399-16C6F9A02AD1}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -67,10 +69,10 @@ Global
{342783B5-F75B-4752-A3E2-B8CB7D09C080}.Debug|Any CPU.Build.0 = Debug|Any CPU
{342783B5-F75B-4752-A3E2-B8CB7D09C080}.Release|Any CPU.ActiveCfg = Release|Any CPU
{342783B5-F75B-4752-A3E2-B8CB7D09C080}.Release|Any CPU.Build.0 = Release|Any CPU
{BCE5A721-69C5-4510-97ED-931ADE587466}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BCE5A721-69C5-4510-97ED-931ADE587466}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BCE5A721-69C5-4510-97ED-931ADE587466}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BCE5A721-69C5-4510-97ED-931ADE587466}.Release|Any CPU.Build.0 = Release|Any CPU
{F676A444-9099-4382-9399-16C6F9A02AD1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F676A444-9099-4382-9399-16C6F9A02AD1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F676A444-9099-4382-9399-16C6F9A02AD1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F676A444-9099-4382-9399-16C6F9A02AD1}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE