Skip to content

Commit 30cfada

Browse files
authored
Make AzureKeyVaultConfigurationProvider public (Azure#19788)
1 parent ac27033 commit 30cfada

File tree

8 files changed

+162
-107
lines changed

8 files changed

+162
-107
lines changed

sdk/extensions/Azure.Extensions.AspNetCore.Configuration.Secrets/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
## 1.1.0-beta.1 (Unreleased)
44

5+
### Added
6+
7+
- The `AzureKeyVaultConfigurationProvider` was made public.
8+
- The `KeyVaultSecretManager.GetData` method was added to allow customizing the way secrets are turned into configuration values.
59

610
## 1.0.2 (2020-11-10)
711

sdk/extensions/Azure.Extensions.AspNetCore.Configuration.Secrets/api/Azure.Extensions.AspNetCore.Configuration.Secrets.netstandard2.0.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,17 @@ public AzureKeyVaultConfigurationOptions() { }
66
public Azure.Extensions.AspNetCore.Configuration.Secrets.KeyVaultSecretManager Manager { get { throw null; } set { } }
77
public System.TimeSpan? ReloadInterval { get { throw null; } set { } }
88
}
9+
public partial class AzureKeyVaultConfigurationProvider : Microsoft.Extensions.Configuration.ConfigurationProvider, System.IDisposable
10+
{
11+
public AzureKeyVaultConfigurationProvider(Azure.Security.KeyVault.Secrets.SecretClient client, Azure.Extensions.AspNetCore.Configuration.Secrets.AzureKeyVaultConfigurationOptions options = null) { }
12+
public void Dispose() { }
13+
protected virtual void Dispose(bool disposing) { }
14+
public override void Load() { }
15+
}
916
public partial class KeyVaultSecretManager
1017
{
1118
public KeyVaultSecretManager() { }
19+
public virtual System.Collections.Generic.Dictionary<string, string> GetData(System.Collections.Generic.IEnumerable<Azure.Security.KeyVault.Secrets.KeyVaultSecret> secrets) { throw null; }
1220
public virtual string GetKey(Azure.Security.KeyVault.Secrets.KeyVaultSecret secret) { throw null; }
1321
public virtual bool Load(Azure.Security.KeyVault.Secrets.SecretProperties secret) { throw null; }
1422
}

sdk/extensions/Azure.Extensions.AspNetCore.Configuration.Secrets/src/AzureKeyVaultConfigurationExtensions.cs

Lines changed: 5 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,8 @@ public static IConfigurationBuilder AddAzureKeyVault(
4444
TokenCredential credential,
4545
KeyVaultSecretManager manager)
4646
{
47-
return AddAzureKeyVault(configurationBuilder, new AzureKeyVaultConfigurationOptions
47+
return AddAzureKeyVault(configurationBuilder, new SecretClient(vaultUri, credential), new AzureKeyVaultConfigurationOptions
4848
{
49-
Client = new SecretClient(vaultUri, credential),
5049
Manager = manager
5150
});
5251
}
@@ -63,11 +62,10 @@ public static IConfigurationBuilder AddAzureKeyVault(
6362
SecretClient client,
6463
KeyVaultSecretManager manager)
6564
{
66-
return configurationBuilder.Add(new AzureKeyVaultConfigurationSource(new AzureKeyVaultConfigurationOptions()
65+
return AddAzureKeyVault(configurationBuilder, client, new AzureKeyVaultConfigurationOptions()
6766
{
68-
Client = client,
6967
Manager = manager
70-
}));
68+
});
7169
}
7270

7371
/// <summary>
@@ -98,25 +96,13 @@ public static IConfigurationBuilder AddAzureKeyVault(
9896
this IConfigurationBuilder configurationBuilder,
9997
SecretClient client,
10098
AzureKeyVaultConfigurationOptions options)
101-
{
102-
options.Client = client;
103-
return configurationBuilder.AddAzureKeyVault(options);
104-
}
105-
106-
/// <summary>
107-
/// Adds an <see cref="IConfigurationProvider"/> that reads configuration values from the Azure KeyVault.
108-
/// </summary>
109-
/// <param name="configurationBuilder">The <see cref="IConfigurationBuilder"/> to add to.</param>
110-
/// <param name="options">The <see cref="AzureKeyVaultConfigurationOptions"/> to use.</param>
111-
/// <returns>The <see cref="IConfigurationBuilder"/>.</returns>
112-
internal static IConfigurationBuilder AddAzureKeyVault(this IConfigurationBuilder configurationBuilder, AzureKeyVaultConfigurationOptions options)
11399
{
114100
Argument.AssertNotNull(configurationBuilder, nameof(configurationBuilder));
115101
Argument.AssertNotNull(options, nameof(configurationBuilder));
116-
Argument.AssertNotNull(options.Client, $"{nameof(options)}.{nameof(options.Client)}");
102+
Argument.AssertNotNull(client, nameof(client));
117103
Argument.AssertNotNull(options.Manager, $"{nameof(options)}.{nameof(options.Manager)}");
118104

119-
configurationBuilder.Add(new AzureKeyVaultConfigurationSource(options));
105+
configurationBuilder.Add(new AzureKeyVaultConfigurationSource(client, options));
120106

121107
return configurationBuilder;
122108
}

sdk/extensions/Azure.Extensions.AspNetCore.Configuration.Secrets/src/AzureKeyVaultConfigurationOptions.cs

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,6 @@ public AzureKeyVaultConfigurationOptions()
2020
Manager = KeyVaultSecretManager.Instance;
2121
}
2222

23-
/// <summary>
24-
/// Gets or sets the <see cref="SecretClient"/> to use for retrieving values.
25-
/// </summary>
26-
internal SecretClient Client { get; set; }
27-
2823
/// <summary>
2924
/// Gets or sets the <see cref="KeyVaultSecretManager"/> instance used to control secret loading.
3025
/// </summary>

sdk/extensions/Azure.Extensions.AspNetCore.Configuration.Secrets/src/AzureKeyVaultConfigurationProvider.cs

Lines changed: 41 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -15,36 +15,38 @@ namespace Azure.Extensions.AspNetCore.Configuration.Secrets
1515
/// <summary>
1616
/// An AzureKeyVault based <see cref="ConfigurationProvider"/>.
1717
/// </summary>
18-
internal class AzureKeyVaultConfigurationProvider : ConfigurationProvider, IDisposable
18+
public class AzureKeyVaultConfigurationProvider : ConfigurationProvider, IDisposable
1919
{
2020
private readonly TimeSpan? _reloadInterval;
2121
private readonly SecretClient _client;
2222
private readonly KeyVaultSecretManager _manager;
23-
private Dictionary<string, LoadedSecret> _loadedSecrets;
23+
private Dictionary<string, KeyVaultSecret> _loadedSecrets;
2424
private Task _pollingTask;
2525
private readonly CancellationTokenSource _cancellationToken;
2626

2727
/// <summary>
2828
/// Creates a new instance of <see cref="AzureKeyVaultConfigurationProvider"/>.
2929
/// </summary>
3030
/// <param name="client">The <see cref="SecretClient"/> to use for retrieving values.</param>
31-
/// <param name="manager">The <see cref="KeyVaultSecretManager"/> to use in managing values.</param>
32-
/// <param name="reloadInterval">The timespan to wait in between each attempt at polling the Azure Key Vault for changes. Default is null which indicates no reloading.</param>
33-
public AzureKeyVaultConfigurationProvider(SecretClient client, KeyVaultSecretManager manager, TimeSpan? reloadInterval = null)
31+
/// <param name="options">The <see cref="AzureKeyVaultConfigurationOptions"/> to configure provider behaviors.</param>
32+
/// <exception cref="ArgumentNullException">When either <paramref name="client"/> or <see cref="AzureKeyVaultConfigurationOptions.Manager"/> is <code>null</code>.</exception>
33+
/// <exception cref="ArgumentOutOfRangeException">When either <see cref="AzureKeyVaultConfigurationOptions.ReloadInterval"/> is not positive or <code>null</code>.</exception>
34+
public AzureKeyVaultConfigurationProvider(SecretClient client, AzureKeyVaultConfigurationOptions options = null)
3435
{
36+
options ??= new AzureKeyVaultConfigurationOptions();
3537
Argument.AssertNotNull(client, nameof(client));
36-
Argument.AssertNotNull(manager, nameof(manager));
38+
Argument.AssertNotNull(options.Manager, $"{nameof(options)}.{nameof(options.Manager)}");
3739

3840
_client = client;
39-
_manager = manager;
40-
if (reloadInterval != null && reloadInterval.Value <= TimeSpan.Zero)
41+
if (options.ReloadInterval != null && options.ReloadInterval.Value <= TimeSpan.Zero)
4142
{
42-
throw new ArgumentOutOfRangeException(nameof(reloadInterval), reloadInterval, nameof(reloadInterval) + " must be positive.");
43+
throw new ArgumentOutOfRangeException(nameof(options.ReloadInterval), options.ReloadInterval, nameof(options.ReloadInterval) + " must be positive.");
4344
}
4445

4546
_pollingTask = null;
4647
_cancellationToken = new CancellationTokenSource();
47-
_reloadInterval = reloadInterval;
48+
_reloadInterval = options.ReloadInterval;
49+
_manager = options.Manager;
4850
}
4951

5052
/// <summary>
@@ -68,7 +70,7 @@ private async Task PollForSecretChangesAsync()
6870
}
6971
}
7072

71-
protected virtual Task WaitForReload()
73+
internal virtual Task WaitForReload()
7274
{
7375
// WaitForReload is only called when the _reloadInterval has a value.
7476
return Task.Delay(_reloadInterval.Value, _cancellationToken.Token);
@@ -79,7 +81,7 @@ private async Task LoadAsync()
7981
var secretPages = _client.GetPropertiesOfSecretsAsync();
8082

8183
using var secretLoader = new ParallelSecretLoader(_client);
82-
var newLoadedSecrets = new Dictionary<string, LoadedSecret>();
84+
var newLoadedSecrets = new Dictionary<string, KeyVaultSecret>();
8385
var oldLoadedSecrets = Interlocked.Exchange(ref _loadedSecrets, null);
8486

8587
await foreach (var secret in secretPages.ConfigureAwait(false))
@@ -92,7 +94,7 @@ private async Task LoadAsync()
9294
var secretId = secret.Name;
9395
if (oldLoadedSecrets != null &&
9496
oldLoadedSecrets.TryGetValue(secretId, out var existingSecret) &&
95-
existingSecret.IsUpToDate(secret.UpdatedOn))
97+
IsUpToDate(existingSecret, secret))
9698
{
9799
oldLoadedSecrets.Remove(secretId);
98100
newLoadedSecrets.Add(secretId, existingSecret);
@@ -106,7 +108,7 @@ private async Task LoadAsync()
106108
var loadedSecret = await secretLoader.WaitForAll().ConfigureAwait(false);
107109
foreach (var secretBundle in loadedSecret)
108110
{
109-
newLoadedSecrets.Add(secretBundle.Value.Name, new LoadedSecret(_manager.GetKey(secretBundle), secretBundle.Value.Value, secretBundle.Value.Properties.UpdatedOn));
111+
newLoadedSecrets.Add(secretBundle.Value.Name, secretBundle);
110112
}
111113

112114
_loadedSecrets = newLoadedSecrets;
@@ -115,7 +117,11 @@ private async Task LoadAsync()
115117
// secret that was loaded previously is not available anymore
116118
if (loadedSecret.Any() || oldLoadedSecrets?.Any() == true)
117119
{
118-
SetData(_loadedSecrets, fireToken: oldLoadedSecrets != null);
120+
Data = _manager.GetData(newLoadedSecrets.Values);
121+
if (oldLoadedSecrets != null)
122+
{
123+
OnReload();
124+
}
119125
}
120126

121127
// schedule a polling task only if none exists and a valid delay is specified
@@ -125,68 +131,36 @@ private async Task LoadAsync()
125131
}
126132
}
127133

128-
private void SetData(Dictionary<string, LoadedSecret> loadedSecrets, bool fireToken)
129-
{
130-
var data = new Dictionary<string, LoadedSecret>(loadedSecrets.Count, StringComparer.OrdinalIgnoreCase);
131-
132-
// The loadesSecrets dictionary contains LoadedSecrets objects that has
133-
// the configuration value and the configuration key (created by the
134-
// KeyVaultSecretManager object). It is possible that multiple
135-
// LoadedSecrets objects uses the same configuration key. This loop
136-
// takes the latest updated value for each key.
137-
foreach (var secretItem in loadedSecrets)
138-
{
139-
string key = secretItem.Value.Key;
140-
141-
if (data.TryGetValue(key, out LoadedSecret currentSecret))
142-
{
143-
if (secretItem.Value.Updated > currentSecret.Updated)
144-
{
145-
data[key] = secretItem.Value;
146-
}
147-
}
148-
else
149-
{
150-
data.Add(key, secretItem.Value);
151-
}
152-
}
153-
154-
Data = data.ToDictionary(d => d.Key, v => v.Value.Value);
155-
if (fireToken)
156-
{
157-
OnReload();
158-
}
159-
}
160-
161-
/// <inheritdoc/>
134+
/// <summary>
135+
/// Frees resources held by the <see cref="AzureKeyVaultConfigurationProvider"/> object.
136+
/// </summary>
162137
public void Dispose()
163138
{
164-
_cancellationToken.Cancel();
165-
_cancellationToken.Dispose();
139+
Dispose(true);
140+
GC.SuppressFinalize(this);
166141
}
167142

168-
private readonly struct LoadedSecret
143+
/// <summary>
144+
/// Frees resources held by the <see cref="AzureKeyVaultConfigurationProvider"/> object.
145+
/// </summary>
146+
/// <param name="disposing">true if called from <see cref="Dispose()"/>, otherwise false.</param>
147+
protected virtual void Dispose(bool disposing)
169148
{
170-
public LoadedSecret(string key, string value, DateTimeOffset? updated)
149+
if (disposing)
171150
{
172-
Key = key;
173-
Value = value;
174-
Updated = updated;
151+
_cancellationToken.Cancel();
152+
_cancellationToken.Dispose();
175153
}
154+
}
176155

177-
public string Key { get; }
178-
public string Value { get; }
179-
public DateTimeOffset? Updated { get; }
180-
181-
public bool IsUpToDate(DateTimeOffset? updated)
156+
private static bool IsUpToDate(KeyVaultSecret current, SecretProperties updated)
157+
{
158+
if (updated.UpdatedOn.HasValue != current.Properties.UpdatedOn.HasValue)
182159
{
183-
if (updated.HasValue != Updated.HasValue)
184-
{
185-
return false;
186-
}
187-
188-
return updated.GetValueOrDefault() == Updated.GetValueOrDefault();
160+
return false;
189161
}
162+
163+
return updated.UpdatedOn.GetValueOrDefault() == current.Properties.UpdatedOn.GetValueOrDefault();
190164
}
191165
}
192166
}

sdk/extensions/Azure.Extensions.AspNetCore.Configuration.Secrets/src/AzureKeyVaultConfigurationSource.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT License.
33

4+
using Azure.Security.KeyVault.Secrets;
45
using Microsoft.Extensions.Configuration;
56

67
namespace Azure.Extensions.AspNetCore.Configuration.Secrets
@@ -11,16 +12,18 @@ namespace Azure.Extensions.AspNetCore.Configuration.Secrets
1112
internal class AzureKeyVaultConfigurationSource : IConfigurationSource
1213
{
1314
private readonly AzureKeyVaultConfigurationOptions _options;
15+
private readonly SecretClient _client;
1416

15-
public AzureKeyVaultConfigurationSource(AzureKeyVaultConfigurationOptions options)
17+
public AzureKeyVaultConfigurationSource(SecretClient client, AzureKeyVaultConfigurationOptions options)
1618
{
1719
_options = options;
20+
_client = client;
1821
}
1922

2023
/// <inheritdoc />
2124
public IConfigurationProvider Build(IConfigurationBuilder builder)
2225
{
23-
return new AzureKeyVaultConfigurationProvider(_options.Client, _options.Manager, _options.ReloadInterval);
26+
return new AzureKeyVaultConfigurationProvider(_client, _options);
2427
}
2528
}
2629
}

sdk/extensions/Azure.Extensions.AspNetCore.Configuration.Secrets/src/KeyVaultSecretManager.cs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT License.
33

4+
using System;
5+
using System.Collections.Generic;
6+
using System.Linq;
7+
using Azure.Core;
48
using Azure.Security.KeyVault.Secrets;
59
using Microsoft.Extensions.Configuration;
610

@@ -24,6 +28,42 @@ public virtual string GetKey(KeyVaultSecret secret)
2428
return secret.Name.Replace("--", ConfigurationPath.KeyDelimiter);
2529
}
2630

31+
/// <summary>
32+
/// Converts a loaded list of secrets into a corresponding set of configuration key-value pairs.
33+
/// </summary>
34+
/// <param name="secrets">A set of secrets retrieved during <see cref="AzureKeyVaultConfigurationProvider.Load"/> call.</param>
35+
/// <returns>The dictionary of configuration key-value pairs that would be assigned to the <see cref="ConfigurationProvider.Data"/>
36+
/// and exposed from the <see cref="IConfiguration"/>.</returns>
37+
/// <exception cref="ArgumentNullException">When <paramref name="secrets"/> is <code>null</code>.</exception>
38+
public virtual Dictionary<string, string> GetData(IEnumerable<KeyVaultSecret> secrets)
39+
{
40+
Argument.AssertNotNull(secrets, nameof(secrets));
41+
42+
var data = new Dictionary<string, KeyVaultSecret>(StringComparer.OrdinalIgnoreCase);
43+
44+
foreach (var secret in secrets)
45+
{
46+
string key = GetKey(secret);
47+
48+
// It is possible that multiple
49+
// LoadedSecrets objects uses the same configuration key. This loop
50+
// takes the latest updated value for each key.
51+
if (data.TryGetValue(key, out KeyVaultSecret currentSecret))
52+
{
53+
if (secret.Properties.UpdatedOn > currentSecret.Properties.UpdatedOn)
54+
{
55+
data[key] = secret;
56+
}
57+
}
58+
else
59+
{
60+
data.Add(key, secret);
61+
}
62+
}
63+
64+
return data.ToDictionary(d => d.Key, v => v.Value.Value);
65+
}
66+
2767
/// <summary>
2868
/// Checks if <see cref="KeyVaultSecret"/> value should be retrieved.
2969
/// </summary>

0 commit comments

Comments
 (0)