diff --git a/Source/VaultSharp.Extensions.Configuration/VaultConfigurationProvider.cs b/Source/VaultSharp.Extensions.Configuration/VaultConfigurationProvider.cs index 2fc6ddb..0d7fb09 100644 --- a/Source/VaultSharp.Extensions.Configuration/VaultConfigurationProvider.cs +++ b/Source/VaultSharp.Extensions.Configuration/VaultConfigurationProvider.cs @@ -127,6 +127,9 @@ public override void Load() private async Task LoadVaultDataAsync(IVaultClient vaultClient) { var hasChanges = false; + var currentKeys = new HashSet(); + var keysToUpdate = new Dictionary(); + await foreach (var secretData in this.ReadKeysAsync(vaultClient, this.ConfigurationSource.BasePath)) { this.logger?.LogDebug($"VaultConfigurationProvider: got Vault data with key `{secretData.Key}`"); @@ -146,8 +149,9 @@ private async Task LoadVaultDataAsync(IVaultClient vaultClient) { key = this.ConfigurationSource.Options.KeyPrefix + ":" + key; } - } + + currentKeys.Add(key); var data = secretData.SecretData.Data; var shouldSetValue = true; @@ -160,9 +164,26 @@ private async Task LoadVaultDataAsync(IVaultClient vaultClient) if (shouldSetValue) { - this.SetData(data, this.ConfigurationSource.Options.OmitVaultKeyName ? string.Empty : key); - hasChanges = true; - this.versionsCache[key] = secretData.SecretData.Metadata.Version; + keysToUpdate[key] = (data, secretData.SecretData.Metadata.Version); + } + } + + var keysToRemove = this.versionsCache.Keys.Where(k => !currentKeys.Contains(k)).ToList(); + if (keysToUpdate.Count > 0 || keysToRemove.Count > 0) + { + this.Data.Clear(); + hasChanges = true; + + foreach (var kvp in keysToUpdate) + { + this.SetData((IDictionary)kvp.Value.Data, this.ConfigurationSource.Options.OmitVaultKeyName ? string.Empty : kvp.Key); + this.versionsCache[kvp.Key] = kvp.Value.Version; + } + + foreach (var removedKey in keysToRemove) + { + this.versionsCache.Remove(removedKey); + this.logger?.LogDebug($"VaultConfigurationProvider: key `{removedKey}` was removed from Vault"); } } diff --git a/Source/VaultSharp.Extensions.Configuration/VaultSharp.Extensions.Configuration.csproj b/Source/VaultSharp.Extensions.Configuration/VaultSharp.Extensions.Configuration.csproj index 4205b9d..3cbb179 100644 --- a/Source/VaultSharp.Extensions.Configuration/VaultSharp.Extensions.Configuration.csproj +++ b/Source/VaultSharp.Extensions.Configuration/VaultSharp.Extensions.Configuration.csproj @@ -3,7 +3,7 @@ default True - SA1633;SA1028;SA1309;CA1303;CS1591;IDE0057 + SA1633;SA1028;SA1309;CA1303;CS1591;IDE0057;IDE0032 VaultSharp.Extensions.Configuration MIT enable diff --git a/Tests/VaultSharp.Extensions.Configuration.Test/IntegrationTests.cs b/Tests/VaultSharp.Extensions.Configuration.Test/IntegrationTests.cs index c0ec754..ba70ba2 100644 --- a/Tests/VaultSharp.Extensions.Configuration.Test/IntegrationTests.cs +++ b/Tests/VaultSharp.Extensions.Configuration.Test/IntegrationTests.cs @@ -3,8 +3,10 @@ namespace VaultSharp.Extensions.Configuration.Test using System; using System.Collections.Generic; using System.IO; + using System.Linq; using System.Net; using System.Net.Http; + using System.Reflection; using System.Threading; using System.Threading.Tasks; using DotNet.Testcontainers.Builders; @@ -999,6 +1001,63 @@ public async Task Success_TokenNoAuthMethod() await container.DisposeAsync().ConfigureAwait(false); } } + + [Fact] + public async Task Success_DeletedKeyRemovedFromVersionsCache() + { + // arrange + var values = new Dictionary>> + { + { "test", new[] { new KeyValuePair("option1", "value1") } }, + { "test/subsection", new[] { new KeyValuePair("option2", "value2") } }, + }; + + var container = this.PrepareVaultContainer(); + try + { + await container.StartAsync(); + await this.LoadDataAsync("http://localhost:8200", values); + + var builder = new ConfigurationBuilder(); + builder.AddVaultConfiguration( + () => new VaultOptions("http://localhost:8200", "root"), + "test", + "secret", + this.logger); + var configurationRoot = builder.Build(); + + // assert initial state + configurationRoot.GetValue("option1").Should().Be("value1"); + configurationRoot.GetSection("subsection").GetValue("option2").Should().Be("value2"); + + var provider = configurationRoot.Providers.OfType().First(); + + var versionsCacheField = typeof(VaultConfigurationProvider) + .GetField("versionsCache", BindingFlags.NonPublic | BindingFlags.Instance); + var versionsCache = (Dictionary)versionsCacheField!.GetValue(provider)!; + + // delete test/subsection from Vault + var vaultClientSettings = new VaultClientSettings("http://localhost:8200", new TokenAuthMethodInfo("root")) + { + SecretsEngineMountPoints = { KeyValueV2 = "secret" } + }; + IVaultClient vaultClient = new VaultClient(vaultClientSettings); + await vaultClient.V1.Secrets.KeyValue.V2.DeleteSecretAsync("test/subsection", "secret"); + + // reload the configuration provider + provider.Load(); + + // assert the deleted key is removed from versionsCache + versionsCache.Should().NotContainKey("subsection"); + + // assert the deleted key's value is no longer present in configuration + configurationRoot.GetSection("subsection").GetValue("option2").Should().BeNull(); + } + finally + { + await container.DisposeAsync(); + } + } } public class TestConfigObject