-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathAwsSecretsConfigurationProvider.cs
166 lines (145 loc) · 5.81 KB
/
AwsSecretsConfigurationProvider.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Text.Json;
using Amazon.SecretsManager;
using Amazon.SecretsManager.Model;
using Microsoft.Extensions.Configuration;
namespace Skyward.Aspnet.Configuration
{
public class AwsSecretsConfigurationProviderOptions
{
public string SecretArn { get; set; }
public AwsSecretsConfigurationProviderOptions(string secretArn)
{
SecretArn = secretArn ?? throw new ArgumentNullException(nameof(secretArn));
}
}
public class AwsSecretsConfigurationProvider : ConfigurationProvider
{
private readonly AwsSecretsConfigurationProviderOptions _options;
private readonly Timer _timer;
private readonly object _lockObject = new object();
private DateTime? _lastUpdate;
private readonly int _maxRefreshSeconds = 60;
public AwsSecretsConfigurationProvider(AwsSecretsConfigurationProviderOptions options)
{
_options = options;
// Set up a timer to fetch secrets periodically (every minute in this case)
_timer = new Timer
(
callback: _ =>
{
Load();
OnReload();
},
dueTime: TimeSpan.FromSeconds(_maxRefreshSeconds * 2),
period: TimeSpan.FromSeconds(_maxRefreshSeconds),
state: null
);
}
public override void Load()
{
// Skip trying to load if the object is locked.
if (!Monitor.TryEnter(_lockObject))
{
return;
}
try
{
// Only refresh at specified intervals.
if (_lastUpdate != null && (DateTime.Now - _lastUpdate.Value).TotalSeconds < _maxRefreshSeconds)
{
return;
}
Data = FetchSecretAsync().GetAwaiter().GetResult();
_lastUpdate = DateTime.Now;
}
catch (Exception e)
{
// Ignore all exceptions.
// Cannot use logger because the logger might not be initialized yet.
Console.WriteLine("An exception occured while loading secrets:");
Console.WriteLine(e.Message);
}
finally
{
Monitor.Exit(_lockObject);
}
}
/// <summary>
/// Make key retrieval case insensitive.
/// </summary>
public override bool TryGet(string key, out string? value)
{
lock (_lockObject)
{
if (Data.ContainsKey(key.ToLower()))
{
value = Data[key.ToLower()];
return true;
}
value = null;
return false;
}
}
/// <summary>
/// Fetches AWS secret and mangles the value into a format ASPnet configuration can figure out.
/// </summary>
private async Task<Dictionary<string, string?>> FetchSecretAsync()
{
// https://github.com/awsdocs/aws-doc-sdk-examples/blob/main/dotnetv3/SecretsManager/GetSecretValue/GetSecretValueExample/GetSecretValue.cs
var client = new AmazonSecretsManagerClient();
var secret = await client.GetSecretValueAsync(new GetSecretValueRequest
{
SecretId = _options.SecretArn
}).ConfigureAwait(false);
var overrides = JsonSerializer.Deserialize<Dictionary<string, string?>>(secret.SecretString);
if (overrides == null)
{
return new Dictionary<string, string?>();
}
// Convert to a format that ASPnet configuration can understand.
// The keys must be lowercase, otherwise the override will not be applied.
var corrected = overrides.ToDictionary(kv => kv.Key.Replace("__", ":").ToLower(), kv => kv.Value);
return corrected;
}
}
public class AwsSecretsConfigurationProviderSource : IConfigurationSource
{
private readonly AwsSecretsConfigurationProviderOptions _options;
public AwsSecretsConfigurationProviderSource(AwsSecretsConfigurationProviderOptions options)
{
_options = options;
}
private static AwsSecretsConfigurationProvider? provider;
private static AwsSecretsConfigurationProvider GetProvider(AwsSecretsConfigurationProviderOptions options)
{
provider ??= new AwsSecretsConfigurationProvider(options);
return provider;
}
public IConfigurationProvider Build(IConfigurationBuilder builder)
{
return GetProvider(_options);
}
}
public static class AwsSecretsConfigurationExtensions
{
public static IConfigurationBuilder AddAwsSecretsConfiguration(this IConfigurationBuilder builder, AwsSecretsConfigurationProviderOptions? options = null)
{
if (options != null)
{
return builder.Add(new AwsSecretsConfigurationProviderSource(options));
}
var secretArn = Environment.GetEnvironmentVariable("APPSETTINGS_OVERRIDE_SECRET_ARN");
if (secretArn == null)
{
Console.WriteLine("No AWS secret provided. AWS configuration overrides disabled.");
return builder;
}
return builder.Add(new AwsSecretsConfigurationProviderSource(new AwsSecretsConfigurationProviderOptions(secretArn)));
}
}
}