diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 836f7e0..ce873e0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -36,6 +36,10 @@ To send us a pull request, please: 5. Send us a pull request, answering any default questions in the pull request interface. 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. +Please enable the removal of trailing whitespaces in your IDE. In JetBrains IDEs, enable the following settings under `Settings -> Editor -> General`: + +![JetBrains settings](https://private-user-images.githubusercontent.com/66992519/430139096-944e54df-a5a3-4655-bf10-93265149ec0b.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NDQ3NDg3MjksIm5iZiI6MTc0NDc0ODQyOSwicGF0aCI6Ii82Njk5MjUxOS80MzAxMzkwOTYtOTQ0ZTU0ZGYtYTVhMy00NjU1LWJmMTAtOTMyNjUxNDllYzBiLnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTA0MTUlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwNDE1VDIwMjAyOVomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPWEwNzdkNGFkMDljOWUyZmQwN2E2YmMyNzhlYTI3YmQxODQ4ZGY4YzRmMDExM2E4NzFlN2RiYjYzZTNjMTc2ZDQmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.SPKnCA3FbZUQAcbedzcos2VJdjL95NuSIUFKrfyH5MY) + GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). diff --git a/src/Amazon.SecretsManager.Extensions.Caching/ISecretsManagerCache.cs b/src/Amazon.SecretsManager.Extensions.Caching/ISecretsManagerCache.cs index c728808..7fb4cab 100644 --- a/src/Amazon.SecretsManager.Extensions.Caching/ISecretsManagerCache.cs +++ b/src/Amazon.SecretsManager.Extensions.Caching/ISecretsManagerCache.cs @@ -31,14 +31,26 @@ public interface ISecretsManagerCache : IDisposable SecretCacheItem GetCachedSecret(string secretId); /// - /// Asynchronously retrieves the specified SecretBinary after calling . + /// Asynchronously retrieves the specified SecretBinary after calling . + /// If both versionId and versionStage are specified, versionId takes precedence. /// - Task GetSecretBinary(string secretId, CancellationToken cancellationToken = default); + /// The secret identifier. This can be the full ARN or the friendly name for the secret. + /// The version identifier. + /// The version stage. + /// The cancellation token used for the Secrets Manager API call. + /// The SecretBinary. + Task GetSecretBinary(string secretId, string versionId = null, string versionStage = null, CancellationToken cancellationToken = default); /// - /// Asynchronously retrieves the specified SecretString after calling . + /// Asynchronously retrieves the specified SecretString after calling . + /// If both versionId and versionStage are specified, versionId takes precedence. /// - Task GetSecretString(string secretId, CancellationToken cancellationToken = default); + /// The secret identifier. This can be the full ARN or the friendly name for the secret. + /// The version identifier. + /// The version stage. + /// The cancellation token used for the Secrets Manager API call. + /// The SecretString. + Task GetSecretString(string secretId, string versionId = null, string versionStage = null, CancellationToken cancellationToken = default); /// /// Requests the secret value from SecretsManager asynchronously and updates the cache entry with any changes. @@ -47,4 +59,4 @@ public interface ISecretsManagerCache : IDisposable /// Task RefreshNowAsync(string secretId, CancellationToken cancellationToken = default); } -} \ No newline at end of file +} diff --git a/src/Amazon.SecretsManager.Extensions.Caching/SecretCacheItem.cs b/src/Amazon.SecretsManager.Extensions.Caching/SecretCacheItem.cs index 29dbd36..c71c2f6 100755 --- a/src/Amazon.SecretsManager.Extensions.Caching/SecretCacheItem.cs +++ b/src/Amazon.SecretsManager.Extensions.Caching/SecretCacheItem.cs @@ -25,15 +25,15 @@ namespace Amazon.SecretsManager.Extensions.Caching /// public class SecretCacheItem : SecretCacheObject { - /// The cached secret value versions for this cached secret. + /// The cached secret value versions for this cached secret. private readonly MemoryCache versions = new MemoryCache(new MemoryCacheOptions()); private const ushort MAX_VERSIONS_CACHE_SIZE = 10; - + public SecretCacheItem(String secretId, IAmazonSecretsManager client, SecretCacheConfiguration config) : base(secretId, client, config) { } - + /// /// Asynchronously retrieves the most current DescribeSecretResponse from Secrets Manager /// as part of the Refresh operation. @@ -46,9 +46,9 @@ protected override async Task ExecuteRefreshAsync(Cancel /// /// Asynchronously retrieves the GetSecretValueResponse from the proper SecretCacheVersion. /// - protected override async Task GetSecretValueAsync(DescribeSecretResponse result, CancellationToken cancellationToken = default) + protected override async Task GetSecretValueAsync(DescribeSecretResponse result, string versionId = null, string versionStage = null, CancellationToken cancellationToken = default) { - SecretCacheVersion version = GetVersion(result); + SecretCacheVersion version = GetVersion(result, versionId, versionStage); if (version == null) { return null; @@ -75,13 +75,19 @@ public override bool Equals(object obj) /// Retrieves the SecretCacheVersion corresponding to the Version Stage /// specified by the SecretCacheConfiguration. /// - private SecretCacheVersion GetVersion(DescribeSecretResponse describeResult) + private SecretCacheVersion GetVersion(DescribeSecretResponse describeResult, string versionId = null, string versionStage = null) { if (null == describeResult?.VersionIdsToStages) return null; String currentVersionId = null; foreach (KeyValuePair> entry in describeResult.VersionIdsToStages) { - if (entry.Value.Contains(config.VersionStage)) + if (versionId != null && entry.Key.Equals(versionId)) + { + currentVersionId = versionId; + break; + } + + if ((versionStage != null && entry.Value.Contains(versionStage)) || entry.Value.Contains(config.VersionStage)) { currentVersionId = entry.Key; break; @@ -108,4 +114,4 @@ private void TrimCacheToSizeLimit() versions.Compact((double)(versions.Count - config.MaxCacheSize) / versions.Count); } } -} \ No newline at end of file +} diff --git a/src/Amazon.SecretsManager.Extensions.Caching/SecretCacheObject.cs b/src/Amazon.SecretsManager.Extensions.Caching/SecretCacheObject.cs index 776446e..45217de 100755 --- a/src/Amazon.SecretsManager.Extensions.Caching/SecretCacheObject.cs +++ b/src/Amazon.SecretsManager.Extensions.Caching/SecretCacheObject.cs @@ -22,20 +22,20 @@ namespace Amazon.SecretsManager.Extensions.Caching public abstract class SecretCacheObject { - /// The number of milliseconds to wait after an exception. + /// The number of milliseconds to wait after an exception. private const long EXCEPTION_BACKOFF = 1000; - /// The growth factor of the backoff duration. + /// The growth factor of the backoff duration. private const long EXCEPTION_BACKOFF_GROWTH_FACTOR = 2; - + /// The maximum number of milliseconds to wait before retrying a failed /// request. private const long BACKOFF_PLATEAU = 128 * EXCEPTION_BACKOFF; - private JitteredDelay EXCEPTION_JITTERED_DELAY = new JitteredDelay(TimeSpan.FromMilliseconds(EXCEPTION_BACKOFF), - TimeSpan.FromMilliseconds(EXCEPTION_BACKOFF), + private JitteredDelay EXCEPTION_JITTERED_DELAY = new JitteredDelay(TimeSpan.FromMilliseconds(EXCEPTION_BACKOFF), + TimeSpan.FromMilliseconds(EXCEPTION_BACKOFF), TimeSpan.FromMilliseconds(BACKOFF_PLATEAU)); - + /// When forcing a refresh using the refreshNow method, a random sleep /// will be performed using this value. This helps prevent code from /// executing a refreshNow in a continuous loop without waiting. @@ -45,35 +45,35 @@ public abstract class SecretCacheObject private JitteredDelay FORCE_REFRESH_JITTERED_DELAY = new JitteredDelay(TimeSpan.FromMilliseconds(FORCE_REFRESH_JITTER_BASE_INCREMENT), TimeSpan.FromMilliseconds(FORCE_REFRESH_JITTER_VARIANCE)); - /// The secret identifier for this cached object. + /// The secret identifier for this cached object. protected String secretId; - /// A private object to synchronize access to certain methods. + /// A private object to synchronize access to certain methods. protected static readonly SemaphoreSlim Lock = new SemaphoreSlim(1,1); - /// The AWS Secrets Manager client to use for requesting secrets. + /// The AWS Secrets Manager client to use for requesting secrets. protected IAmazonSecretsManager client; /// The Secret Cache Configuration. protected SecretCacheConfiguration config; - /// A flag to indicate a refresh is needed. + /// A flag to indicate a refresh is needed. private bool refreshNeeded = true; /// The result of the last AWS Secrets Manager request for this item. private Object data = null; - + /// If the last request to AWS Secrets Manager resulted in an exception, /// that exception will be thrown back to the caller when requesting /// secret data. protected Exception exception = null; - + /// The number of exceptions encountered since the last successfully /// AWS Secrets Manager request. This is used to calculate an exponential /// backoff. private long exceptionCount = 0; - + /// The time to wait before retrying a failed AWS Secrets Manager request. private long nextRetryTime = 0; @@ -93,10 +93,10 @@ public SecretCacheObject(String secretId, IAmazonSecretsManager client, SecretCa this.client = client; this.config = config; } - + protected abstract Task ExecuteRefreshAsync(CancellationToken cancellationToken = default); - protected abstract Task GetSecretValueAsync(T result, CancellationToken cancellationToken = default); + protected abstract Task GetSecretValueAsync(T result, string versionId = null, string versionStage = null, CancellationToken cancellationToken = default); /// /// Return the typed result object. @@ -209,7 +209,7 @@ public async Task RefreshNowAsync(CancellationToken cancellationToken = de /// If the secret is due for a refresh, the refresh will occur before the result is returned. /// If the refresh fails, the cached result is returned, or the cached exception is thrown. /// - public async Task GetSecretValue(CancellationToken cancellationToken) + public async Task GetSecretValue(CancellationToken cancellationToken, string versionId = null, string versionStage = null) { bool success = false; await Lock.WaitAsync(cancellationToken); @@ -226,7 +226,7 @@ public async Task GetSecretValue(CancellationToken cance { System.Runtime.ExceptionServices.ExceptionDispatchInfo.Capture(exception).Throw(); } - return await GetSecretValueAsync(GetResult(), cancellationToken); + return await GetSecretValueAsync(GetResult(), versionId, versionStage, cancellationToken); } } } diff --git a/src/Amazon.SecretsManager.Extensions.Caching/SecretCacheVersion.cs b/src/Amazon.SecretsManager.Extensions.Caching/SecretCacheVersion.cs index 8725544..0943acc 100755 --- a/src/Amazon.SecretsManager.Extensions.Caching/SecretCacheVersion.cs +++ b/src/Amazon.SecretsManager.Extensions.Caching/SecretCacheVersion.cs @@ -56,7 +56,7 @@ protected override async Task ExecuteRefreshAsync(Cancel return await this.client.GetSecretValueAsync(new GetSecretValueRequest { SecretId = this.secretId, VersionId = this.versionId }, cancellationToken); } - protected override Task GetSecretValueAsync(GetSecretValueResponse result, CancellationToken cancellationToken = default) + protected override Task GetSecretValueAsync(GetSecretValueResponse result, string versionId = null, string versionStage = null, CancellationToken cancellationToken = default) { return Task.FromResult(result); } diff --git a/src/Amazon.SecretsManager.Extensions.Caching/SecretsManagerCache.cs b/src/Amazon.SecretsManager.Extensions.Caching/SecretsManagerCache.cs index ce77a6d..c5529ab 100755 --- a/src/Amazon.SecretsManager.Extensions.Caching/SecretsManagerCache.cs +++ b/src/Amazon.SecretsManager.Extensions.Caching/SecretsManagerCache.cs @@ -86,24 +86,36 @@ public void Dispose() } /// - /// Asynchronously retrieves the specified SecretString after calling . + /// Asynchronously retrieves the specified SecretString after calling . + /// If both versionId and versionStage are specified, versionId takes precedence. /// - public async Task GetSecretString(String secretId, CancellationToken cancellationToken = default) + /// The secret identifier. This can be the full ARN or the friendly name for the secret. + /// The version identifier. + /// The version stage. + /// The cancellation token used for the Secrets Manager API call. + /// The SecretString. + public async Task GetSecretString(String secretId, string versionId = null, string versionStage = null, CancellationToken cancellationToken = default) { SecretCacheItem secret = GetCachedSecret(secretId); GetSecretValueResponse response = null; - response = await secret.GetSecretValue(cancellationToken); + response = await secret.GetSecretValue(cancellationToken, versionId, versionStage); return response?.SecretString; } /// - /// Asynchronously retrieves the specified SecretBinary after calling . + /// Asynchronously retrieves the specified SecretBinary after calling . + /// If both versionId and versionStage are specified, versionId takes precedence. /// - public async Task GetSecretBinary(String secretId, CancellationToken cancellationToken = default) + /// The secret identifier. This can be the full ARN or the friendly name for the secret. + /// The version identifier. + /// The version stage. + /// The cancellation token used for the Secrets Manager API call. + /// The SecretBinary. + public async Task GetSecretBinary(String secretId, string versionId = null, string versionStage = null, CancellationToken cancellationToken = default) { SecretCacheItem secret = GetCachedSecret(secretId); GetSecretValueResponse response = null; - response = await secret.GetSecretValue(cancellationToken); + response = await secret.GetSecretValue(cancellationToken, versionId, versionStage); return response?.SecretBinary?.ToArray(); } @@ -137,4 +149,4 @@ public SecretCacheItem GetCachedSecret(string secretId) return secret; } } -} \ No newline at end of file +} diff --git a/test/Amazon.SecretsManager.Extensions.Caching.UnitTests/CacheTests.cs b/test/Amazon.SecretsManager.Extensions.Caching.UnitTests/CacheTests.cs index 3f83e64..3f86857 100755 --- a/test/Amazon.SecretsManager.Extensions.Caching.UnitTests/CacheTests.cs +++ b/test/Amazon.SecretsManager.Extensions.Caching.UnitTests/CacheTests.cs @@ -28,6 +28,7 @@ public class CacheTests { private const string AWSCURRENT_VERSIONID_1 = "01234567890123456789012345678901"; private const string AWSCURRENT_VERSIONID_2 = "12345678901234567890123456789012"; + private const string AWSCURRENT_VERSIONID_3 = "23456789012345678901234567890123"; private readonly GetSecretValueResponse secretStringResponse1 = new GetSecretValueResponse { @@ -57,6 +58,13 @@ public class CacheTests SecretString = "AnotherSecretValue" }; + private readonly GetSecretValueResponse secretStringResponse5 = new GetSecretValueResponse + { + Name = "MySecretString", + VersionId = AWSCURRENT_VERSIONID_3, + SecretString = "MySecretValue5", + }; + private readonly GetSecretValueResponse binaryResponse1 = new GetSecretValueResponse { Name = "MyBinarySecret", @@ -71,10 +79,18 @@ public class CacheTests SecretBinary = new MemoryStream(Enumerable.Repeat((byte)0x30, 10).ToArray()) }; + private readonly GetSecretValueResponse binaryResponse3 = new GetSecretValueResponse + { + Name = "MyBinarySecret", + VersionId = AWSCURRENT_VERSIONID_3, + SecretBinary = new MemoryStream(Enumerable.Repeat((byte)0x20, 10).ToArray()) + }; + private readonly DescribeSecretResponse describeSecretResponse1 = new DescribeSecretResponse() { VersionIdsToStages = new Dictionary> { - { AWSCURRENT_VERSIONID_1, new List { "AWSCURRENT" } } + { AWSCURRENT_VERSIONID_1, new List { "AWSCURRENT" } }, + { AWSCURRENT_VERSIONID_3, new List { "AWSPREVIOUS" } } } }; @@ -112,6 +128,38 @@ public async Task GetSecretStringTest() Assert.Equal(first, secretStringResponse1.SecretString); } + [Fact] + public async Task GetSecretStringVersionIdTest() + { + Mock secretsManager = new Mock(MockBehavior.Strict); + secretsManager.SetupSequence(i => i.GetSecretValueAsync(It.Is(j => j.SecretId == secretStringResponse1.Name), default(CancellationToken))) + .ReturnsAsync(secretStringResponse1) + .ThrowsAsync(new AmazonSecretsManagerException("This should not be called")); + secretsManager.SetupSequence(i => i.DescribeSecretAsync(It.Is(j => j.SecretId == secretStringResponse1.Name), default(CancellationToken))) + .ReturnsAsync(describeSecretResponse1) + .ThrowsAsync(new AmazonSecretsManagerException("This should not be called")); + + SecretsManagerCache cache = new SecretsManagerCache(secretsManager.Object); + string first = await cache.GetSecretString(secretStringResponse1.Name, secretStringResponse1.VersionId); + Assert.Equal(first, secretStringResponse1.SecretString); + } + + [Fact] + public async Task GetSecretStringVersionStageTest() + { + Mock secretsManager = new Mock(MockBehavior.Strict); + secretsManager.SetupSequence(i => i.GetSecretValueAsync(It.Is(j => j.SecretId == secretStringResponse5.Name), default(CancellationToken))) + .ReturnsAsync(secretStringResponse5) + .ThrowsAsync(new AmazonSecretsManagerException("This should not be called")); + secretsManager.SetupSequence(i => i.DescribeSecretAsync(It.Is(j => j.SecretId == secretStringResponse5.Name), default(CancellationToken))) + .ReturnsAsync(describeSecretResponse1) + .ThrowsAsync(new AmazonSecretsManagerException("This should not be called")); + + SecretsManagerCache cache = new SecretsManagerCache(secretsManager.Object); + string first = await cache.GetSecretString(secretStringResponse5.Name, "", "AWSPREVIOUS"); + Assert.Equal(first, secretStringResponse5.SecretString); + } + [Fact] public async Task NoSecretStringPresentTest() { @@ -140,11 +188,45 @@ public async Task GetSecretBinaryTest() .ThrowsAsync(new AmazonSecretsManagerException("This should not be called")); SecretsManagerCache cache = new SecretsManagerCache(secretsManager.Object); - + byte[] first = await cache.GetSecretBinary(binaryResponse1.Name); Assert.Equal(first, binaryResponse1.SecretBinary.ToArray()); } + [Fact] + public async Task GetSecretBinaryVersionIdTest() + { + Mock secretsManager = new Mock(MockBehavior.Strict); + secretsManager.SetupSequence(i => i.GetSecretValueAsync(It.Is(j => j.SecretId == binaryResponse1.Name), default(CancellationToken))) + .ReturnsAsync(binaryResponse1) + .ThrowsAsync(new AmazonSecretsManagerException("This should not be called")); + secretsManager.SetupSequence(i => i.DescribeSecretAsync(It.Is(j => j.SecretId == binaryResponse1.Name), default(CancellationToken))) + .ReturnsAsync(describeSecretResponse1) + .ThrowsAsync(new AmazonSecretsManagerException("This should not be called")); + + SecretsManagerCache cache = new SecretsManagerCache(secretsManager.Object); + + byte[] first = await cache.GetSecretBinary(binaryResponse1.Name, binaryResponse1.VersionId); + Assert.Equal(first, binaryResponse1.SecretBinary.ToArray()); + } + + [Fact] + public async Task GetSecretBinaryVersionStageTest() + { + Mock secretsManager = new Mock(MockBehavior.Strict); + secretsManager.SetupSequence(i => i.GetSecretValueAsync(It.Is(j => j.SecretId == binaryResponse3.Name), default(CancellationToken))) + .ReturnsAsync(binaryResponse3) + .ThrowsAsync(new AmazonSecretsManagerException("This should not be called")); + secretsManager.SetupSequence(i => i.DescribeSecretAsync(It.Is(j => j.SecretId == binaryResponse3.Name), default(CancellationToken))) + .ReturnsAsync(describeSecretResponse1) + .ThrowsAsync(new AmazonSecretsManagerException("This should not be called")); + + SecretsManagerCache cache = new SecretsManagerCache(secretsManager.Object); + + byte[] first = await cache.GetSecretBinary(binaryResponse3.Name, "", "AWSPREVIOUS"); + Assert.Equal(first, binaryResponse3.SecretBinary.ToArray()); + } + [Fact] public async Task NoSecretBinaryPresentTest() { @@ -174,14 +256,14 @@ public async Task GetSecretBinaryMultipleTest() .ThrowsAsync(new AmazonSecretsManagerException("This should not be called")); SecretsManagerCache cache = new SecretsManagerCache(secretsManager.Object); - + byte[] first = null; for (int i = 0; i < 10; i++) { first = await cache.GetSecretBinary(binaryResponse1.Name); } Assert.Equal(first, binaryResponse1.SecretBinary.ToArray()); - + } [Fact] @@ -196,11 +278,11 @@ public async Task BasicSecretCacheTest() .ThrowsAsync(new AmazonSecretsManagerException("This should not be called")); SecretsManagerCache cache = new SecretsManagerCache(secretsManager.Object); - + String first = await cache.GetSecretString(secretStringResponse1.Name); String second = await cache.GetSecretString(secretStringResponse1.Name); Assert.Equal(first, second); - + } [Fact] @@ -280,7 +362,7 @@ public async Task BasicSecretCacheTTLRefreshTest() .ThrowsAsync(new AmazonSecretsManagerException("This should not be called")); SecretsManagerCache cache = new SecretsManagerCache(secretsManager.Object, new SecretCacheConfiguration { CacheItemTTL = 1000 }); - + String first = await cache.GetSecretString(secretStringResponse1.Name); String second = await cache.GetSecretString(secretStringResponse1.Name); Assert.Equal(first, second); @@ -434,7 +516,7 @@ public async Task HookSecretCacheTest() .ReturnsAsync(describeSecretResponse1) .ReturnsAsync(describeSecretResponse1) .ThrowsAsync(new AmazonSecretsManagerException("This should not be called")); - + TestHook testHook = new TestHook(); SecretsManagerCache cache = new SecretsManagerCache(secretsManager.Object, new SecretCacheConfiguration { CacheHook = testHook }); @@ -451,4 +533,4 @@ public async Task HookSecretCacheTest() Assert.Equal(4, testHook.GetCount()); } } -} \ No newline at end of file +}