-
Notifications
You must be signed in to change notification settings - Fork 43
Resolve key vault references concurrently #736
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,8 +4,8 @@ | |
| using Azure.Security.KeyVault.Secrets; | ||
| using Microsoft.Extensions.Configuration.AzureAppConfiguration.Extensions; | ||
| using System; | ||
| using System.Collections.Concurrent; | ||
| using System.Collections.Generic; | ||
| using System.Linq; | ||
| using System.Threading; | ||
| using System.Threading.Tasks; | ||
|
|
||
|
|
@@ -14,16 +14,14 @@ namespace Microsoft.Extensions.Configuration.AzureAppConfiguration.AzureKeyVault | |
| internal class AzureKeyVaultSecretProvider | ||
| { | ||
| private readonly AzureAppConfigurationKeyVaultOptions _keyVaultOptions; | ||
| private readonly IDictionary<string, SecretClient> _secretClients; | ||
| private readonly Dictionary<Uri, CachedKeyVaultSecret> _cachedKeyVaultSecrets; | ||
| private Uri _nextRefreshSourceId; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The reason we remove the The benefit of maintaining the |
||
| private DateTimeOffset? _nextRefreshTime; | ||
| private readonly ConcurrentDictionary<string, SecretClient> _secretClients; | ||
| private readonly ConcurrentDictionary<Uri, CachedKeyVaultSecret> _cachedKeyVaultSecrets; | ||
|
|
||
| public AzureKeyVaultSecretProvider(AzureAppConfigurationKeyVaultOptions keyVaultOptions = null) | ||
| { | ||
| _keyVaultOptions = keyVaultOptions ?? new AzureAppConfigurationKeyVaultOptions(); | ||
| _cachedKeyVaultSecrets = new Dictionary<Uri, CachedKeyVaultSecret>(); | ||
| _secretClients = new Dictionary<string, SecretClient>(StringComparer.OrdinalIgnoreCase); | ||
| _cachedKeyVaultSecrets = new ConcurrentDictionary<Uri, CachedKeyVaultSecret>(); | ||
| _secretClients = new ConcurrentDictionary<string, SecretClient>(StringComparer.OrdinalIgnoreCase); | ||
|
|
||
| if (_keyVaultOptions.SecretClients != null) | ||
| { | ||
|
|
@@ -52,6 +50,7 @@ public async Task<string> GetSecretValue(KeyVaultSecretIdentifier secretIdentifi | |
| throw new UnauthorizedAccessException("No key vault credential or secret resolver callback configured, and no matching secret client could be found."); | ||
| } | ||
|
|
||
| CachedKeyVaultSecret updatedCachedSecret = null; | ||
| bool success = false; | ||
|
|
||
| try | ||
|
|
@@ -68,55 +67,44 @@ public async Task<string> GetSecretValue(KeyVaultSecretIdentifier secretIdentifi | |
| secretValue = await _keyVaultOptions.SecretResolver(secretIdentifier.SourceId).ConfigureAwait(false); | ||
| } | ||
|
|
||
| cachedSecret = new CachedKeyVaultSecret(secretValue, secretIdentifier.SourceId); | ||
| updatedCachedSecret = new CachedKeyVaultSecret(secretValue, secretIdentifier.SourceId); | ||
| success = true; | ||
| } | ||
| finally | ||
| { | ||
| SetSecretInCache(secretIdentifier.SourceId, key, cachedSecret, success); | ||
| SetSecretInCache(secretIdentifier.SourceId, key, updatedCachedSecret, success); | ||
| } | ||
|
|
||
| return secretValue; | ||
| } | ||
|
|
||
| public bool ShouldRefreshKeyVaultSecrets() | ||
| { | ||
| return _nextRefreshTime.HasValue && _nextRefreshTime.Value < DateTimeOffset.UtcNow; | ||
| } | ||
|
|
||
| public void ClearCache() | ||
| { | ||
| var sourceIdsToRemove = new List<Uri>(); | ||
|
|
||
| var utcNow = DateTimeOffset.UtcNow; | ||
|
|
||
| foreach (KeyValuePair<Uri, CachedKeyVaultSecret> secret in _cachedKeyVaultSecrets) | ||
| { | ||
| if (secret.Value.LastRefreshTime + RefreshConstants.MinimumSecretRefreshInterval < utcNow) | ||
| if (secret.Value.RefreshAt.HasValue && secret.Value.RefreshAt.Value < DateTimeOffset.UtcNow) | ||
| { | ||
| sourceIdsToRemove.Add(secret.Key); | ||
| return true; | ||
| } | ||
| } | ||
|
|
||
| foreach (Uri sourceId in sourceIdsToRemove) | ||
| { | ||
| _cachedKeyVaultSecrets.Remove(sourceId); | ||
| } | ||
| return false; | ||
| } | ||
|
linglingye001 marked this conversation as resolved.
|
||
|
|
||
| if (_cachedKeyVaultSecrets.Any()) | ||
| public void ClearCache() | ||
| { | ||
| foreach (KeyValuePair<Uri, CachedKeyVaultSecret> secret in _cachedKeyVaultSecrets) | ||
| { | ||
| UpdateNextRefreshableSecretFromCache(); | ||
| if (secret.Value.LastRefreshTime + RefreshConstants.MinimumSecretRefreshInterval < DateTimeOffset.UtcNow) | ||
| { | ||
| _cachedKeyVaultSecrets.TryRemove(secret.Key, out _); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| public void RemoveSecretFromCache(Uri sourceId) | ||
| { | ||
| _cachedKeyVaultSecrets.Remove(sourceId); | ||
|
|
||
| if (sourceId == _nextRefreshSourceId) | ||
| { | ||
| UpdateNextRefreshableSecretFromCache(); | ||
| } | ||
| _cachedKeyVaultSecrets.TryRemove(sourceId, out _); | ||
| } | ||
|
|
||
| private SecretClient GetSecretClient(Uri secretUri) | ||
|
|
@@ -133,14 +121,12 @@ private SecretClient GetSecretClient(Uri secretUri) | |
| return null; | ||
| } | ||
|
|
||
| client = new SecretClient( | ||
| new Uri(secretUri.GetLeftPart(UriPartial.Authority)), | ||
| _keyVaultOptions.Credential, | ||
| _keyVaultOptions.ClientOptions); | ||
|
|
||
| _secretClients.Add(keyVaultId, client); | ||
|
|
||
| return client; | ||
| return _secretClients.GetOrAdd( | ||
| keyVaultId, | ||
| _ => new SecretClient( | ||
| new Uri(secretUri.GetLeftPart(UriPartial.Authority)), | ||
| _keyVaultOptions.Credential, | ||
| _keyVaultOptions.ClientOptions)); | ||
| } | ||
|
|
||
| private void SetSecretInCache(Uri sourceId, string key, CachedKeyVaultSecret cachedSecret, bool success = true) | ||
|
|
@@ -152,37 +138,6 @@ private void SetSecretInCache(Uri sourceId, string key, CachedKeyVaultSecret cac | |
|
|
||
| UpdateCacheExpirationTimeForSecret(key, cachedSecret, success); | ||
| _cachedKeyVaultSecrets[sourceId] = cachedSecret; | ||
|
|
||
| if (sourceId == _nextRefreshSourceId) | ||
| { | ||
| UpdateNextRefreshableSecretFromCache(); | ||
| } | ||
| else if ((cachedSecret.RefreshAt.HasValue && _nextRefreshTime.HasValue && cachedSecret.RefreshAt.Value < _nextRefreshTime.Value) | ||
| || (cachedSecret.RefreshAt.HasValue && !_nextRefreshTime.HasValue)) | ||
| { | ||
| _nextRefreshSourceId = sourceId; | ||
| _nextRefreshTime = cachedSecret.RefreshAt.Value; | ||
| } | ||
| } | ||
|
|
||
| private void UpdateNextRefreshableSecretFromCache() | ||
| { | ||
| _nextRefreshSourceId = null; | ||
| _nextRefreshTime = DateTimeOffset.MaxValue; | ||
|
|
||
| foreach (KeyValuePair<Uri, CachedKeyVaultSecret> secret in _cachedKeyVaultSecrets) | ||
| { | ||
| if (secret.Value.RefreshAt.HasValue && secret.Value.RefreshAt.Value < _nextRefreshTime) | ||
| { | ||
| _nextRefreshTime = secret.Value.RefreshAt; | ||
| _nextRefreshSourceId = secret.Key; | ||
| } | ||
| } | ||
|
|
||
| if (_nextRefreshTime == DateTimeOffset.MaxValue) | ||
| { | ||
| _nextRefreshTime = null; | ||
| } | ||
| } | ||
|
|
||
| private void UpdateCacheExpirationTimeForSecret(string key, CachedKeyVaultSecret cachedSecret, bool success) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| // Copyright (c) Microsoft Corporation. | ||
| // Licensed under the MIT license. | ||
| // | ||
| using Azure.Data.AppConfiguration; | ||
| using Microsoft.Extensions.Configuration.AzureAppConfiguration.AzureKeyVault; | ||
| using System.Net.Mime; | ||
|
|
||
| namespace Microsoft.Extensions.Configuration.AzureAppConfiguration.Extensions | ||
| { | ||
| internal static class ConfigurationSettingExtensions | ||
| { | ||
| public static bool IsKeyVaultReference(this ConfigurationSetting setting) | ||
| { | ||
| return setting != null | ||
| && setting.ContentType.TryParseContentType(out ContentType contentType) | ||
| && contentType.IsKeyVaultReference(); | ||
| } | ||
| } | ||
| } |
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.