Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ public class AzureAppConfigurationKeyVaultOptions
internal TokenCredential Credential;
internal List<SecretClient> SecretClients = new List<SecretClient>();
internal Func<Uri, ValueTask<string>> SecretResolver;
internal Dictionary<string, TimeSpan> SecretRefreshIntervals = new Dictionary<string, TimeSpan>();
internal TimeSpan? DefaultSecretRefreshInterval = null;

/// <summary>
/// Sets the credentials used to authenticate to key vaults that have no registered <see cref="SecretClient"/>.
Expand Down Expand Up @@ -52,5 +54,33 @@ public AzureAppConfigurationKeyVaultOptions SetSecretResolver(Func<Uri, ValueTas
SecretResolver = secretResolver;
return this;
}

/// <summary>
/// Sets the refresh interval for periodically reloading a secret from Key Vault.
/// Any refresh operation triggered using <see cref="IConfigurationRefresher"/> will not update the value for a Key Vault secret until the cached value for that secret has expired.
/// </summary>
/// <param name="secretReferenceKey">Key of the Key Vault reference in Azure App Configuration.</param>
/// <param name="refreshInterval">Minimum time that must elapse before the secret is reloaded from Key Vault.</param>
public AzureAppConfigurationKeyVaultOptions SetSecretRefreshInterval(string secretReferenceKey, TimeSpan refreshInterval)
Comment thread
avanigupta marked this conversation as resolved.
{
if (string.IsNullOrEmpty(secretReferenceKey))
{
throw new ArgumentNullException(nameof(secretReferenceKey));
}

SecretRefreshIntervals[secretReferenceKey] = refreshInterval;
return this;
}

/// <summary>
/// Sets the refresh interval for periodically reloading all those secrets which do not have individual refresh intervals.
/// Any refresh operation triggered using <see cref="IConfigurationRefresher"/> will not update the value for a Key Vault secret until the cached value for that secret has expired.
/// </summary>
/// <param name="refreshInterval">Minimum time that must elapse before the secrets are reloaded from Key Vault.</param>
public AzureAppConfigurationKeyVaultOptions SetSecretRefreshInterval(TimeSpan refreshInterval)
{
DefaultSecretRefreshInterval = refreshInterval;
return this;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -336,7 +336,7 @@ public AzureAppConfigurationOptions ConfigureKeyVault(Action<AzureAppConfigurati
}

_adapters.RemoveAll(a => a is AzureKeyVaultKeyValueAdapter);
_adapters.Add(new AzureKeyVaultKeyValueAdapter(new AzureKeyVaultSecretProvider(keyVaultOptions.Credential, keyVaultOptions.SecretClients, keyVaultOptions.SecretResolver)));
_adapters.Add(new AzureKeyVaultKeyValueAdapter(new AzureKeyVaultSecretProvider(keyVaultOptions)));

return this;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ public async Task RefreshAsync()

await RefreshIndividualKeyValues().ConfigureAwait(false);
await RefreshKeyValueCollections().ConfigureAwait(false);
await RefreshKeyValueAdapters().ConfigureAwait(false);
}
finally
{
Expand Down Expand Up @@ -294,6 +295,12 @@ await CallWithRequestTracing(async () =>

if (data != null)
{
// Invalidate all the cached KeyVault secrets
foreach (IKeyValueAdapter adapter in _options.Adapters)
{
adapter.InvalidateCache();
}

await SetData(data, ignoreFailures).ConfigureAwait(false);

// Set the cache expiration time for all refresh registered settings
Expand Down Expand Up @@ -429,11 +436,17 @@ await TracingUtils.CallWithRequestTracing(_requestTracingEnabled, RequestType.Wa

hasChanged = true;

// Add the key-value if it is not loaded, or update it if it was loaded with a different label
_applicationSettings[watchedKey] = watchedKv;
_watchedSettings[watchedKeyLabel] = watchedKv;
// Add the key-value if it is not loaded, or update it if it was loaded with a different label
_applicationSettings[watchedKey] = watchedKv;
_watchedSettings[watchedKeyLabel] = watchedKv;

// Invalidate the cached Key Vault secret (if any) for this ConfigurationSetting
foreach (IKeyValueAdapter adapter in _options.Adapters)
{
adapter.InvalidateCache(watchedKv);
Comment thread
avanigupta marked this conversation as resolved.
}
}
}
}

if (hasChanged)
{
Expand All @@ -448,6 +461,14 @@ await TracingUtils.CallWithRequestTracing(_requestTracingEnabled, RequestType.Wa
}
}

private async Task RefreshKeyValueAdapters()
{
if (_options.Adapters.Any(adapter => adapter.NeedsRefresh()))
{
SetData(_applicationSettings);
}
}

private async Task RefreshKeyValueCollections()
{
foreach (KeyValueWatcher changeWatcher in _options.MultiKeyWatchers)
Expand Down Expand Up @@ -581,6 +602,12 @@ private void ProcessChanges(IEnumerable<KeyValueChange> changes)
{
_applicationSettings[change.Key] = change.Current;
}

// Invalidate the cached Key Vault secret (if any) for this ConfigurationSetting
foreach (IKeyValueAdapter adapter in _options.Adapters)
{
adapter.InvalidateCache(change.Current);
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ public async Task<IEnumerable<KeyValuePair<string, string>>> ProcessKeyValue(Con

try
{
secret = await _secretProvider.GetSecretValue(secretUri, cancellationToken).ConfigureAwait(false);
secret = await _secretProvider.GetSecretValue(secretUri, setting.Key, cancellationToken).ConfigureAwait(false);
}
catch (Exception e) when (e is UnauthorizedAccessException || (e.Source?.Equals(AzureIdentityAssemblyName, StringComparison.OrdinalIgnoreCase) ?? false))
{
Expand Down Expand Up @@ -84,5 +84,22 @@ public bool CanProcess(ConfigurationSetting setting)
string contentType = setting?.ContentType?.Split(';')[0].Trim();
return string.Equals(contentType, KeyVaultConstants.ContentType);
}

public void InvalidateCache(ConfigurationSetting setting = null)
{
if (setting == null)
{
_secretProvider.ClearCache();
}
else
{
_secretProvider.RemoveSecretFromCache(setting.Key);
}
}

public bool NeedsRefresh()
{
return _secretProvider.ShouldRefreshKeyVaultSecrets();
}
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
//
using Azure.Core;
using Azure.Security.KeyVault.Secrets;
using System;
using System.Collections.Generic;
Expand All @@ -13,47 +12,52 @@ namespace Microsoft.Extensions.Configuration.AzureAppConfiguration.AzureKeyVault
{
internal class AzureKeyVaultSecretProvider
{
private readonly AzureAppConfigurationKeyVaultOptions _keyVaultOptions;
private readonly IDictionary<string, SecretClient> _secretClients;
private readonly TokenCredential _credential;
private readonly Func<Uri, ValueTask<string>> _secretResolver;
private readonly Dictionary<string, CachedKeyVaultSecret> _cachedKeyVaultSecrets;
private string _nextRefreshKey;
private DateTimeOffset? _nextRefreshTime;

public AzureKeyVaultSecretProvider(TokenCredential credential = null, IEnumerable<SecretClient> secretClients = null, Func<Uri, ValueTask<string>> secretResolver = null)
public AzureKeyVaultSecretProvider(AzureAppConfigurationKeyVaultOptions keyVaultOptions = null)
{
_credential = credential;
_keyVaultOptions = keyVaultOptions ?? new AzureAppConfigurationKeyVaultOptions();
_cachedKeyVaultSecrets = new Dictionary<string, CachedKeyVaultSecret>(StringComparer.OrdinalIgnoreCase);
_secretClients = new Dictionary<string, SecretClient>(StringComparer.OrdinalIgnoreCase);
_secretResolver = secretResolver;

if (secretClients != null)
if (_keyVaultOptions.SecretClients != null)
{
foreach (SecretClient client in secretClients)
foreach (SecretClient client in _keyVaultOptions.SecretClients)
{
string keyVaultId = client.VaultUri.Host;
_secretClients[keyVaultId] = client;
}
}
}

public async Task<string> GetSecretValue(Uri secretUri, CancellationToken cancellationToken)
public async Task<string> GetSecretValue(Uri secretUri, string key, CancellationToken cancellationToken)
Comment thread
avanigupta marked this conversation as resolved.
{
if (secretUri == null)
{
throw new ArgumentNullException(nameof(secretUri));
Comment thread
avanigupta marked this conversation as resolved.
}

string secretName = secretUri?.Segments?.ElementAtOrDefault(2)?.TrimEnd('/');
string secretVersion = secretUri?.Segments?.ElementAtOrDefault(3)?.TrimEnd('/');
string secretValue;

SecretClient client = GetSecretClient(secretUri);

if (client != null)
if (_cachedKeyVaultSecrets.TryGetValue(key, out CachedKeyVaultSecret cachedSecret) &&
(!cachedSecret.RefreshAt.HasValue || DateTimeOffset.UtcNow < cachedSecret.RefreshAt.Value))
{
KeyVaultSecret secret = await client.GetSecretAsync(secretName, secretVersion, cancellationToken).ConfigureAwait(false);
secretValue = cachedSecret.SecretValue;
}
else if (client != null)
{
KeyVaultSecret secret;
secret = await client.GetSecretAsync(secretName, secretVersion, cancellationToken).ConfigureAwait(false);
secretValue = secret?.Value;
SetSecretInCache(key, secretValue);
}
else if (_secretResolver != null)
else if (_keyVaultOptions.SecretResolver != null)
{
secretValue = await _secretResolver(secretUri).ConfigureAwait(false);
secretValue = await _keyVaultOptions.SecretResolver(secretUri).ConfigureAwait(false);
Comment thread
jimmyca15 marked this conversation as resolved.
SetSecretInCache(key, secretValue);
}
else
{
Expand All @@ -63,6 +67,28 @@ public async Task<string> GetSecretValue(Uri secretUri, CancellationToken cancel
return secretValue;
}

public bool ShouldRefreshKeyVaultSecrets()
{
return _nextRefreshTime.HasValue && _nextRefreshTime.Value < DateTimeOffset.UtcNow;
}

public void ClearCache()
{
_cachedKeyVaultSecrets.Clear();
_nextRefreshKey = null;
_nextRefreshTime = null;
}

public void RemoveSecretFromCache(string key)
{
_cachedKeyVaultSecrets.Remove(key);

if (key == _nextRefreshKey)
{
UpdateNextRefreshableSecretFromCache();
}
}

private SecretClient GetSecretClient(Uri secretUri)
{
string keyVaultId = secretUri.Host;
Expand All @@ -72,14 +98,61 @@ private SecretClient GetSecretClient(Uri secretUri)
return client;
}

if (_credential == null)
if (_keyVaultOptions.Credential == null)
{
return null;
}

client = new SecretClient(new Uri(secretUri.GetLeftPart(UriPartial.Authority)), _credential);
client = new SecretClient(new Uri(secretUri.GetLeftPart(UriPartial.Authority)), _keyVaultOptions.Credential);
_secretClients.Add(keyVaultId, client);
return client;
}

private void SetSecretInCache(string key, string secretValue)
{
DateTimeOffset? refreshSecretAt = null;

if (_keyVaultOptions.SecretRefreshIntervals.TryGetValue(key, out TimeSpan refreshInterval))
{
refreshSecretAt = DateTimeOffset.UtcNow.Add(refreshInterval);
}
else if (_keyVaultOptions.DefaultSecretRefreshInterval.HasValue)
{
refreshSecretAt = DateTimeOffset.UtcNow.Add(_keyVaultOptions.DefaultSecretRefreshInterval.Value);
}

_cachedKeyVaultSecrets[key] = new CachedKeyVaultSecret(secretValue, refreshSecretAt);

if (key == _nextRefreshKey)
{
UpdateNextRefreshableSecretFromCache();
}
else if ((refreshSecretAt.HasValue && _nextRefreshTime.HasValue && refreshSecretAt.Value < _nextRefreshTime.Value)
|| (refreshSecretAt.HasValue && !_nextRefreshTime.HasValue))
{
_nextRefreshKey = key;
_nextRefreshTime = refreshSecretAt.Value;
}
}

private void UpdateNextRefreshableSecretFromCache()
{
_nextRefreshKey = null;
_nextRefreshTime = DateTimeOffset.MaxValue;

foreach (KeyValuePair<string, CachedKeyVaultSecret> secret in _cachedKeyVaultSecrets)
{
if (secret.Value.RefreshAt.HasValue && secret.Value.RefreshAt.Value < _nextRefreshTime)
{
_nextRefreshTime = secret.Value.RefreshAt;
_nextRefreshKey = secret.Key;
}
}

if (_nextRefreshTime == DateTimeOffset.MaxValue)
{
_nextRefreshTime = null;
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
//
using System;

namespace Microsoft.Extensions.Configuration.AzureAppConfiguration.AzureKeyVault
{
internal class CachedKeyVaultSecret
{
///// <summary>
///// The value of the Key Vault secret.
///// </summary>
public string SecretValue { get; set; }

/// <summary>
/// The time when this secret should be reloaded from Key Vault.
/// </summary>
public DateTimeOffset? RefreshAt { get; set; }

public CachedKeyVaultSecret(string secretValue, DateTimeOffset? refreshAt)
{
SecretValue = secretValue;
RefreshAt = refreshAt;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -89,5 +89,15 @@ public bool CanProcess(ConfigurationSetting setting)
return string.Equals(contentType, FeatureManagementConstants.ContentType) ||
setting.Key.StartsWith(FeatureManagementConstants.FeatureFlagMarker);
}

public void InvalidateCache(ConfigurationSetting setting = null)
{
return;
}

public bool NeedsRefresh()
{
return false;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,9 @@ internal interface IKeyValueAdapter
Task<IEnumerable<KeyValuePair<string, string>>> ProcessKeyValue(ConfigurationSetting setting, CancellationToken cancellationToken);

bool CanProcess(ConfigurationSetting setting);

void InvalidateCache(ConfigurationSetting setting = null);

bool NeedsRefresh();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -89,5 +89,15 @@ public bool CanProcess(ConfigurationSetting setting)

return false;
}

public void InvalidateCache(ConfigurationSetting setting = null)
{
return;
}

public bool NeedsRefresh()
{
return false;
}
}
}
Loading