Skip to content
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

Allow for a list and no searching for keys #46

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,23 @@ builder.AddSecretsManager(configurator: options =>

You can see an example [here](/samples/Sample4).

### Defining list of secrets in advance (no list secrets permission required)
Security best practices sometimes prevent the listing of secrets in a production environment. As a result, a defined list of `ARN` can be provided in lieu of a secrets filter. In this case, the library will only retrieve the secrets whose `ARN` are given. The `.SecretFilter` is ignored.

```csharp
var acceptedARNs = new[]
{
"MySecretARN1",
"MySecretARN2",
"MySecretARN3",
};

builder.AddSecretsManager(configurator: options =>
{
options.AcceptedSecretArns = acceptedARNs;
});
```

### Altering how the values are added to the Configuration

Sometimes we are not in control of the full system. Maybe we are forced to use secrets defined by someone else that uses a different convention.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public class SecretsManagerConfigurationProvider : ConfigurationProvider, IDispo

public IAmazonSecretsManager Client { get; }

private HashSet<(string, string)> _loadedValues = new HashSet<(string, string)>();
private HashSet<(string, string)> _loadedValues = new();
private Task? _pollingTask;
private CancellationTokenSource? _cancellationToken;

Expand All @@ -37,7 +37,7 @@ public Task ForceReloadAsync(CancellationToken cancellationToken)
return ReloadAsync(cancellationToken);
}

async Task LoadAsync()
private async Task LoadAsync()
{
_loadedValues = await FetchConfigurationAsync(default).ConfigureAwait(false);
SetData(_loadedValues, triggerReload: false);
Expand All @@ -49,7 +49,7 @@ async Task LoadAsync()
}
}

async Task PollForChangesAsync(TimeSpan interval, CancellationToken cancellationToken)
private async Task PollForChangesAsync(TimeSpan interval, CancellationToken cancellationToken)
{
while (!cancellationToken.IsCancellationRequested)
{
Expand All @@ -64,7 +64,7 @@ async Task PollForChangesAsync(TimeSpan interval, CancellationToken cancellation
}
}

async Task ReloadAsync(CancellationToken cancellationToken)
private async Task ReloadAsync(CancellationToken cancellationToken)
{
var oldValues = _loadedValues;
var newValues = await FetchConfigurationAsync(cancellationToken).ConfigureAwait(false);
Expand All @@ -78,7 +78,7 @@ async Task ReloadAsync(CancellationToken cancellationToken)

private static bool IsJson(string str) => str.StartsWith("[") || str.StartsWith("{");

IEnumerable<(string key, string value)> ExtractValues(JToken token, string prefix)
private static IEnumerable<(string key, string value)> ExtractValues(JToken token, string prefix)
{
switch (token)
{
Expand Down Expand Up @@ -130,7 +130,7 @@ async Task ReloadAsync(CancellationToken cancellationToken)
}
}

void SetData(IEnumerable<(string, string)> values, bool triggerReload)
private void SetData(IEnumerable<(string, string)> values, bool triggerReload)
{
Data = values.ToDictionary(x => x.Item1, x => x.Item2, StringComparer.InvariantCultureIgnoreCase);
if (triggerReload)
Expand All @@ -139,17 +139,23 @@ void SetData(IEnumerable<(string, string)> values, bool triggerReload)
}
}

async Task<IReadOnlyList<SecretListEntry>> FetchAllSecretsAsync(CancellationToken cancellationToken)
private async Task<IReadOnlyList<SecretListEntry>> FetchAllSecretsAsync(CancellationToken cancellationToken)
{
var response = default(ListSecretsResponse);

var result = new List<SecretListEntry>();

if (Options.AcceptedSecretArns.Count > 0)
{
result.AddRange(Options.AcceptedSecretArns.Select(x => new SecretListEntry(){ARN = x}));
return result;
}

do
{
var nextToken = response?.NextToken;

var request = new ListSecretsRequest() {NextToken = nextToken};
var request = new ListSecretsRequest {NextToken = nextToken};

response = await Client.ListSecretsAsync(request, cancellationToken).ConfigureAwait(false);

Expand All @@ -158,8 +164,8 @@ async Task<IReadOnlyList<SecretListEntry>> FetchAllSecretsAsync(CancellationToke

return result;
}
async Task<HashSet<(string, string)>> FetchConfigurationAsync(CancellationToken cancellationToken)

private async Task<HashSet<(string, string)>> FetchConfigurationAsync(CancellationToken cancellationToken)
{
var secrets = await FetchAllSecretsAsync(cancellationToken).ConfigureAwait(false);
var configuration = new HashSet<(string, string)>();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
using System;
using System.Collections.Generic;
using Amazon.SecretsManager;
using Amazon.SecretsManager.Model;

namespace Kralizek.Extensions.Configuration.Internal
{
public class SecretsManagerConfigurationProviderOptions
{
public Func<SecretListEntry, bool> SecretFilter { get; set; } = secret => true;
public List<string> AcceptedSecretArns { get; set; } = new();

public Func<SecretListEntry, bool> SecretFilter { get; set; } = _ => true;

public Func<SecretListEntry, string, string> KeyGenerator { get; set; } = (secret, key) => key;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
Expand Down Expand Up @@ -119,6 +120,26 @@ public void Secrets_can_be_filtered_out_via_options([Frozen] SecretListEntry tes

Assert.That(sut.Get(testEntry.Name), Is.Null);
}

[Test, CustomAutoData]
public void Secrets_can_be_listed_explicitly_and_not_searched([Frozen] SecretListEntry testEntry, ListSecretsResponse listSecretsResponse, GetSecretValueResponse getSecretValueResponse, [Frozen] IAmazonSecretsManager secretsManager, [Frozen] SecretsManagerConfigurationProviderOptions options, SecretsManagerConfigurationProvider sut, IFixture fixture)
{
const string secretKey = "KEY";
var firstSecretArn = listSecretsResponse.SecretList.Select(x => x.ARN).First();
Mock.Get(secretsManager).Setup(p => p.GetSecretValueAsync(It.Is<GetSecretValueRequest>(x => x.SecretId.Equals(firstSecretArn)), It.IsAny<CancellationToken>())).ReturnsAsync(getSecretValueResponse);

options.SecretFilter = entry => true;
options.AcceptedSecretArns = new List<string> {firstSecretArn};
options.KeyGenerator = (entry, key) => secretKey;

sut.Load();

Mock.Get(secretsManager).Verify(p => p.GetSecretValueAsync(It.Is<GetSecretValueRequest>(x => !x.SecretId.Equals(firstSecretArn)), It.IsAny<CancellationToken>()), Times.Never);
Mock.Get(secretsManager).Verify(p => p.ListSecretsAsync(It.IsAny<ListSecretsRequest>(), It.IsAny<CancellationToken>()), Times.Never);

Assert.That(sut.Get(testEntry.Name), Is.Null);
Assert.That(sut.Get(secretKey), Is.EqualTo(getSecretValueResponse.SecretString));
}

[Test, CustomAutoData]
public void Keys_can_be_customized_via_options([Frozen] SecretListEntry testEntry, ListSecretsResponse listSecretsResponse, GetSecretValueResponse getSecretValueResponse, string newKey, [Frozen] IAmazonSecretsManager secretsManager, [Frozen] SecretsManagerConfigurationProviderOptions options, SecretsManagerConfigurationProvider sut, IFixture fixture)
Expand Down Expand Up @@ -156,7 +177,7 @@ public void Should_throw_on_missing_secret_value([Frozen] SecretListEntry testEn

Mock.Get(secretsManager).Setup(p => p.GetSecretValueAsync(It.IsAny<GetSecretValueRequest>(), It.IsAny<CancellationToken>())).Throws(new ResourceNotFoundException("Oops"));

Assert.That(() => sut.Load(), Throws.TypeOf<MissingSecretValueException>());
Assert.That(sut.Load, Throws.TypeOf<MissingSecretValueException>());
}

[Test, CustomAutoData]
Expand Down