-
Notifications
You must be signed in to change notification settings - Fork 12
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: add dapr secret provider #412
Open
stijnmoreels
wants to merge
20
commits into
arcus-azure:main
Choose a base branch
from
stijnmoreels:feature/dapr-secret-provider
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
20 commits
Select commit
Hold shift + click to select a range
193f8f8
feat: add dapr secret provider
stijnmoreels 85baf0a
pr-fix: unit tests
stijnmoreels 4a619d1
Merge branch 'main' into feature/dapr-secret-provider
stijnmoreels 68a69be
pr-fix: remove `/templates` path in `run-integration-tests.yml`
stijnmoreels 7a50705
pr-fix: use `dapr` directly in Linux environment
stijnmoreels 2089256
pr-style: update w/ correct exception messages
stijnmoreels f639949
pr-fix: use cross-platform dapr installation
stijnmoreels c05179f
pr-remove: out-of-context unit test
stijnmoreels dcad110
Merge branch 'feature/dapr-secret-provider' of https://github.com/sti…
stijnmoreels 09d6cbb
pr-fix: update with correct file name in exception message
stijnmoreels d8ffedb
pr-fix: add exception logging
stijnmoreels 03f9ca1
pr-fix: increase time-out
stijnmoreels 6539ed3
pr-fix: correct with dapr logging
stijnmoreels 9ebb6ae
pr-fix: update w/o app-logging
stijnmoreels b1a1d51
Update download-dapr.yml
stijnmoreels 944f3dd
pr-fix: use correct unauthorized service principal
stijnmoreels a264c0f
Merge branch 'feature/dapr-secret-provider' of https://github.com/sti…
stijnmoreels 013f10b
Update docs/preview/03-Features/secret-store/provider/dapr-secret-sto…
stijnmoreels e4c9174
Update docs/preview/03-Features/secret-store/provider/dapr-secret-sto…
stijnmoreels 013009f
Merge branch 'main' into feature/dapr-secret-provider
stijnmoreels File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
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
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,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' |
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
154 changes: 154 additions & 0 deletions
154
docs/preview/03-Features/secret-store/provider/dapr-secret-store.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,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<DaprSecretProvider> 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... | ||
|
||
/// <summary> | ||
/// Determine the Dapr secret key and section based on the user passed-in <paramref name="secretName"/>. | ||
/// </summary> | ||
/// <remarks> | ||
/// 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 <paramref name="secretName"/> into the required (key, section) pair for your use-case. | ||
/// </remarks> | ||
/// <param name="secretName">The user passed-in secret which gets translated to a Dapr secret key and section.</param> | ||
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<ILogger<DaprSecretProvider>>(); | ||
return new MultiValuedLocalDaprSecretProvider("mycustomsecretstore", options, logger); | ||
}, | ||
(DaprSecretProviderOptions options) => | ||
{ | ||
// Configure additional options which can be passed in the implementation factory function of the custom implementation. | ||
}); | ||
}); | ||
} | ||
} | ||
``` |
34 changes: 34 additions & 0 deletions
34
src/Arcus.Security.Providers.Dapr/Arcus.Security.Providers.Dapr.csproj
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,34 @@ | ||
<Project Sdk="Microsoft.NET.Sdk"> | ||
|
||
<PropertyGroup> | ||
<TargetFramework>net6.0</TargetFramework> | ||
<Authors>Arcus</Authors> | ||
<Description>Provides support for Dapr Secrets with Arcus Secret Store</Description> | ||
<Copyright>Copyright (c) Arcus</Copyright> | ||
<PackageProjectUrl>https://security.arcus-azure.net/</PackageProjectUrl> | ||
<RepositoryUrl>https://github.com/arcus-azure/arcus.security</RepositoryUrl> | ||
<PackageLicenseFile>LICENSE</PackageLicenseFile> | ||
<PackageIcon>icon.png</PackageIcon> | ||
<RepositoryType>Git</RepositoryType> | ||
<PackageReadmeFile>README.md</PackageReadmeFile> | ||
<PackageTags>Kubernetes;Secrets;Dapr</PackageTags> | ||
<GeneratePackageOnBuild>true</GeneratePackageOnBuild> | ||
<GenerateDocumentationFile>true</GenerateDocumentationFile> | ||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors> | ||
</PropertyGroup> | ||
|
||
<ItemGroup> | ||
<None Include="..\..\README.md" Pack="true" PackagePath="\" /> | ||
<None Include="..\..\LICENSE" Pack="true" PackagePath="\" /> | ||
<None Include="..\..\docs\static\img\icon.png" Pack="true" PackagePath="\" /> | ||
</ItemGroup> | ||
|
||
<ItemGroup> | ||
<PackageReference Include="Dapr.Client" Version="1.10.0" /> | ||
</ItemGroup> | ||
|
||
<ItemGroup> | ||
<ProjectReference Include="..\Arcus.Security.Core\Arcus.Security.Core.csproj" /> | ||
</ItemGroup> | ||
|
||
</Project> |
137 changes: 137 additions & 0 deletions
137
src/Arcus.Security.Providers.Dapr/DaprSecretProvider.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,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 | ||
{ | ||
/// <summary> | ||
/// Represents an <see cref="ISecretProvider"/> retrieving secrets from the Dapr secret store. | ||
/// </summary> | ||
public class DaprSecretProvider : ISecretProvider | ||
{ | ||
/// <summary> | ||
/// Initializes a new instance of the <see cref="DaprSecretProvider" /> class. | ||
/// </summary> | ||
/// <param name="secretStore">The name of the Dapr secret store from which the secrets should be retrieved from.</param> | ||
/// <param name="options">The optional set of options to manipulate the basic behavior of how the secrets should be retrieved.</param> | ||
/// <param name="logger">The logger instance to write diagnostic trace messages during the retrieval of the Dapr secrets.</param> | ||
/// <exception cref="ArgumentException">Thrown when the <paramref name="secretStore"/> is blank.</exception> | ||
public DaprSecretProvider(string secretStore, DaprSecretProviderOptions options, ILogger<DaprSecretProvider> logger) | ||
{ | ||
Guard.NotNullOrWhitespace(secretStore, nameof(secretStore)); | ||
|
||
SecretStore = secretStore; | ||
Options = options ?? new DaprSecretProviderOptions(); | ||
Logger = logger ?? NullLogger<DaprSecretProvider>.Instance; | ||
} | ||
|
||
/// <summary> | ||
/// Gets the name of the Dapr secret store for which this secret provider is configured. | ||
/// </summary> | ||
protected string SecretStore { get; } | ||
|
||
/// <summary> | ||
/// Gets the optional set of configured options to manipulated the basic behavior of how the secrets should be retrieved. | ||
/// </summary> | ||
/// <remarks> | ||
/// Options set when configuring this secret provider in the secret store. | ||
/// </remarks> | ||
protected DaprSecretProviderOptions Options { get; } | ||
|
||
|
||
/// <summary> | ||
/// Gets the logger instance to write diagnostic trace messages during the Dapr secret retrieval. | ||
/// </summary> | ||
protected ILogger Logger { get; } | ||
|
||
/// <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 <paramref name="secretName"/> must not be empty.</exception> | ||
/// <exception cref="ArgumentNullException">The <paramref name="secretName"/> must not be null.</exception> | ||
/// <exception cref="SecretNotFoundException">The secret was not found, using the given name.</exception> | ||
public async Task<Secret> GetSecretAsync(string secretName) | ||
{ | ||
Guard.NotNullOrWhitespace(secretName, nameof(secretName)); | ||
|
||
string secretValue = await GetRawSecretAsync(secretName); | ||
return new Secret(secretValue); | ||
} | ||
|
||
/// <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 <paramref name="secretName"/> must not be empty.</exception> | ||
/// <exception cref="ArgumentNullException">The <paramref name="secretName"/> must not be null.</exception> | ||
/// <exception cref="SecretNotFoundException">The secret was not found, using the given name.</exception> | ||
public async Task<string> 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; | ||
} | ||
|
||
/// <summary> | ||
/// Determine the Dapr secret key and section based on the user passed-in <paramref name="secretName"/>. | ||
/// </summary> | ||
/// <remarks> | ||
/// 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 <paramref name="secretName"/> into the required (key, section) pair for your use-case. | ||
/// </remarks> | ||
/// <param name="secretName">The user passed-in secret which gets translated to a Dapr secret key and section.</param> | ||
protected virtual (string daprSecretKey, string daprSecretSection) DetermineDaprSecretName(string secretName) | ||
{ | ||
return (secretName, secretName); | ||
} | ||
|
||
private async Task<string> 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<string, string> 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<string, object> | ||
{ | ||
["SecretStore"] = SecretStore, | ||
["SecretSection"] = daprSecretSection | ||
}); | ||
} | ||
} | ||
} | ||
} | ||
} |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Dapr is not really a secret provider. It has the capabilities to connect to a secret provider and ads a layer of abstraction.
People that are looking for using Dapr will probably understand this, but I'm not sure on how we can improve this text
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe we should change it to
Dapr secret management brings external secrets to your application.
? As they talk about 'secret management' in their docs?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is the suggestion here, @fgheysels better?