From 1f8617efa7bc05d10f78a59ef0a4dceca907b13f Mon Sep 17 00:00:00 2001 From: Angad Misra Date: Thu, 10 Apr 2025 14:54:32 -0700 Subject: [PATCH 1/4] feat: get secrets by `versionId` or `versionStage` (#19) --- .../secretsmanager/caching/SecretCache.java | 40 +++++++++++++ .../caching/cache/SecretCacheItem.java | 57 ++++++++++++++++--- .../caching/cache/SecretCacheObject.java | 33 +++++++++++ .../caching/cache/SecretCacheVersion.java | 14 +++++ .../caching/SecretCacheTest.java | 55 ++++++++++++++++++ 5 files changed, 190 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/amazonaws/secretsmanager/caching/SecretCache.java b/src/main/java/com/amazonaws/secretsmanager/caching/SecretCache.java index d0c2672..d7911d7 100644 --- a/src/main/java/com/amazonaws/secretsmanager/caching/SecretCache.java +++ b/src/main/java/com/amazonaws/secretsmanager/caching/SecretCache.java @@ -136,6 +136,26 @@ public String getSecretString(final String secretId) { return gsv.secretString(); } + /** + * Retrieve and cache a secret string from AWS Secrets Manager. + * + * @param secretId the secret ID of the desired secret. + * @param versionId the version ID of the desired secret (optional, can be null). + * @param versionStage the version stage of the desired secret (optional, can be null). + * + * @return The secret string for the desired secret. + */ + public String getSecretString(final String secretId, final String versionId, final String versionStage) { + SecretCacheItem secret = this.getCachedSecret(secretId); + GetSecretValueResponse gsv = secret.getSecretValue(versionId, versionStage); + + if (gsv == null) { + return null; + } + + return gsv.secretString(); + } + /** * Method to retrieve a binary secret from AWS Secrets Manager. * @@ -151,6 +171,26 @@ public ByteBuffer getSecretBinary(final String secretId) { return gsv.secretBinary().asByteBuffer(); } + /** + * Retrieve and cache a secret binary from AWS Secrets Manager. + * + * @param secretId the secret ID of the desired secret. + * @param versionId the version ID of the desired secret (optional, can be null). + * @param versionStage the version stage of the desired secret (optional, can be null). + * + * @return The secret binary for the desired secret. + */ + public ByteBuffer getSecretBinary(final String secretId, final String versionId, final String versionStage) { + SecretCacheItem secret = this.getCachedSecret(secretId); + GetSecretValueResponse gsv = secret.getSecretValue(versionId, versionStage); + + if (gsv == null) { + return null; + } + + return gsv.secretBinary().asByteBuffer(); + } + /** * Method to force the refresh of a cached secret state. * diff --git a/src/main/java/com/amazonaws/secretsmanager/caching/cache/SecretCacheItem.java b/src/main/java/com/amazonaws/secretsmanager/caching/cache/SecretCacheItem.java index 9c0ed78..2eb43a2 100644 --- a/src/main/java/com/amazonaws/secretsmanager/caching/cache/SecretCacheItem.java +++ b/src/main/java/com/amazonaws/secretsmanager/caching/cache/SecretCacheItem.java @@ -13,6 +13,8 @@ package com.amazonaws.secretsmanager.caching.cache; +import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.concurrent.ThreadLocalRandom; @@ -115,16 +117,32 @@ protected DescribeSecretResponse executeRefresh() { * The result of the Describe Secret request to AWS Secrets Manager. * @return The cached secret version. */ - private SecretCacheVersion getVersion(DescribeSecretResponse describeResponse) { + private SecretCacheVersion getVersion(DescribeSecretResponse describeResponse, String versionId, String versionStage) { if (null == describeResponse) { return null; } if (null == describeResponse.versionIdsToStages()) { return null; } - Optional currentVersionId = describeResponse.versionIdsToStages().entrySet() - .stream() - .filter(Objects::nonNull) - .filter(x -> x.getValue() != null) - .filter(x -> x.getValue().contains(this.config.getVersionStage())) - .map(x -> x.getKey()) - .findFirst(); + + Optional currentVersionId = Optional.empty(); + + for (Map.Entry> entry : describeResponse.versionIdsToStages().entrySet()) { + if (entry == null) { + continue; + } + + if (entry.getValue() == null) { + continue; + } + + if (versionId != null && entry.getKey() == versionId) { + currentVersionId = Optional.of(versionId); + break; + } + + if ((versionStage != null && entry.getValue().contains(versionStage)) || entry.getValue().contains(config.getVersionStage())) { + currentVersionId = Optional.of(entry.getKey()); + break; + } + } + if (currentVersionId.isPresent()) { SecretCacheVersion version = versions.get(currentVersionId.get()); if (null == version) { @@ -134,6 +152,7 @@ private SecretCacheVersion getVersion(DescribeSecretResponse describeResponse) { } return version; } + return null; } @@ -146,9 +165,29 @@ private SecretCacheVersion getVersion(DescribeSecretResponse describeResponse) { */ @Override protected GetSecretValueResponse getSecretValue(DescribeSecretResponse describeResponse) { - SecretCacheVersion version = getVersion(describeResponse); + SecretCacheVersion version = getVersion(describeResponse, null, null); if (null == version) { return null; } return version.getSecretValue(); } + /** + * Return the cached GetSecretValue result. + * + * @param describeResponse the DescribeSecret result. + * @param versionId the version ID of the desired secret (optional, can be null). + * @param versionStage the version stage of the desired secret (optional, can be null). + * + * @return The cached GetSecretValue result. + */ + @Override + protected GetSecretValueResponse getSecretValue(DescribeSecretResponse describeResponse, String versionId, String versionStage) { + SecretCacheVersion version = getVersion(describeResponse, versionId, versionStage); + + if (version == null) { + return null; + } + + return version.getSecretValue(); + } + } diff --git a/src/main/java/com/amazonaws/secretsmanager/caching/cache/SecretCacheObject.java b/src/main/java/com/amazonaws/secretsmanager/caching/cache/SecretCacheObject.java index 3d626c4..ee8eb1b 100644 --- a/src/main/java/com/amazonaws/secretsmanager/caching/cache/SecretCacheObject.java +++ b/src/main/java/com/amazonaws/secretsmanager/caching/cache/SecretCacheObject.java @@ -111,6 +111,17 @@ public SecretCacheObject(final String secretId, */ protected abstract GetSecretValueResponse getSecretValue(T result); + /** + * Execute the actual refresh of the cached secret state. + * + * @param result the GetSecretValue or DescribeSecret result. + * @param versionId the version ID of the desired secret (optional, can be null). + * @param versionStage the version stage of the desired secret (optional, can be null). + * + * @return The cached GetSecretValue result based on the current cached state. + */ + protected abstract GetSecretValueResponse getSecretValue(T result, String versionId, String versionStage);; + public abstract boolean equals(Object obj); public abstract int hashCode(); public abstract String toString(); @@ -246,4 +257,26 @@ public GetSecretValueResponse getSecretValue() { } } + /** + * Return the cached GetSecretValue result. + * + * @param versionId the version ID of the desired secret (optional, can be null). + * @param versionStage the version stage of the desired secret (optional, can be null). + * + * @return The cached GetSecretValue result. + */ + public GetSecretValueResponse getSecretValue(String versionId, String versionStage) { + synchronized (lock) { + refresh(); + + if (this.data == null) { + if (this.exception != null) { + throw this.exception; + } + } + + return this.getSecretValue(this.getResult(), versionId, versionStage); + } + } + } diff --git a/src/main/java/com/amazonaws/secretsmanager/caching/cache/SecretCacheVersion.java b/src/main/java/com/amazonaws/secretsmanager/caching/cache/SecretCacheVersion.java index ec98dbf..498a58f 100644 --- a/src/main/java/com/amazonaws/secretsmanager/caching/cache/SecretCacheVersion.java +++ b/src/main/java/com/amazonaws/secretsmanager/caching/cache/SecretCacheVersion.java @@ -98,4 +98,18 @@ protected GetSecretValueResponse getSecretValue(GetSecretValueResponse gsvResult return gsvResult; } + /** + * Return the cached GetSecretValue result. + * + * @param gsvResult the GetSecretValue or DescribeSecret result. + * @param versionId the version ID of the desired secret (optional, can be null). + * @param versionStage the version stage of the desired secret (optional, can be null). + * + * @return The cached GetSecretValue result. + */ + @Override + protected GetSecretValueResponse getSecretValue(GetSecretValueResponse gsvResult, String versionId, String versionStage) { + return gsvResult; + } + } diff --git a/src/test/java/com/amazonaws/secretsmanager/caching/SecretCacheTest.java b/src/test/java/com/amazonaws/secretsmanager/caching/SecretCacheTest.java index 4f4e93b..13f008e 100644 --- a/src/test/java/com/amazonaws/secretsmanager/caching/SecretCacheTest.java +++ b/src/test/java/com/amazonaws/secretsmanager/caching/SecretCacheTest.java @@ -135,6 +135,61 @@ public void basicSecretCacheTest() { sc.close(); } + @Test + public void basicSecretCacheVersionIdTest() { + final String secret = "basicSecretCacheTest"; + Map> versionMap = new HashMap>(); + versionMap.put("versionId", Arrays.asList("AWSCURRENT")); + versionMap.put("otherVersionId", Arrays.asList("AWSCURRENT")); + Mockito.when(describeSecretResponse.versionIdsToStages()).thenReturn(versionMap); + GetSecretValueResponse.Builder resBuilder = GetSecretValueResponse.builder().secretString(secret) + .secretBinary(SdkBytes.fromByteArray(secret.getBytes())); + getSecretValueResponse = resBuilder.build(); + + Mockito.when(asm.describeSecret(Mockito.any(DescribeSecretRequest.class))).thenReturn(describeSecretResponse); + Mockito.when(asm.getSecretValue(Mockito.any(GetSecretValueRequest.class))).thenReturn(getSecretValueResponse); + + SecretCache sc = new SecretCache(asm); + + // Request the secret multiple times and verify the correct result + repeat(10, n -> Assert.assertEquals(sc.getSecretString("", "otherVersionId", null), secret)); + + // Verify that multiple requests did not call the API + Mockito.verify(asm, Mockito.times(1)).describeSecret(Mockito.any(DescribeSecretRequest.class)); + Mockito.verify(asm, Mockito.times(1)).getSecretValue(Mockito.any(GetSecretValueRequest.class)); + + repeat(10, n -> Assert.assertEquals(sc.getSecretBinary("", "otherVersionId", null), + ByteBuffer.wrap(secret.getBytes()))); + sc.close(); + } + + @Test + public void basicSecretCacheVersionStageTest() { + final String secret = "basicSecretCacheTest"; + Map> versionMap = new HashMap>(); + versionMap.put("versionId", Arrays.asList("AWSCURRENT")); + Mockito.when(describeSecretResponse.versionIdsToStages()).thenReturn(versionMap); + GetSecretValueResponse.Builder resBuilder = GetSecretValueResponse.builder().secretString(secret) + .secretBinary(SdkBytes.fromByteArray(secret.getBytes())); + getSecretValueResponse = resBuilder.build(); + + Mockito.when(asm.describeSecret(Mockito.any(DescribeSecretRequest.class))).thenReturn(describeSecretResponse); + Mockito.when(asm.getSecretValue(Mockito.any(GetSecretValueRequest.class))).thenReturn(getSecretValueResponse); + + SecretCache sc = new SecretCache(asm); + + // Request the secret multiple times and verify the correct result + repeat(10, n -> Assert.assertEquals(sc.getSecretString("", null, "AWSCURRENT"), secret)); + + // Verify that multiple requests did not call the API + Mockito.verify(asm, Mockito.times(1)).describeSecret(Mockito.any(DescribeSecretRequest.class)); + Mockito.verify(asm, Mockito.times(1)).getSecretValue(Mockito.any(GetSecretValueRequest.class)); + + repeat(10, n -> Assert.assertEquals(sc.getSecretBinary("", null, "AWSCURRENT"), + ByteBuffer.wrap(secret.getBytes()))); + sc.close(); + } + @Test public void hookSecretCacheTest() { final String secret = "hookSecretCacheTest"; From bed46a3dc6b76756f3cbe015c9fd18fb8a9d5f15 Mon Sep 17 00:00:00 2001 From: Angad Misra Date: Thu, 10 Apr 2025 14:57:49 -0700 Subject: [PATCH 2/4] fix: remove extraneous semicolon --- .../secretsmanager/caching/cache/SecretCacheObject.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/amazonaws/secretsmanager/caching/cache/SecretCacheObject.java b/src/main/java/com/amazonaws/secretsmanager/caching/cache/SecretCacheObject.java index ee8eb1b..113531a 100644 --- a/src/main/java/com/amazonaws/secretsmanager/caching/cache/SecretCacheObject.java +++ b/src/main/java/com/amazonaws/secretsmanager/caching/cache/SecretCacheObject.java @@ -120,7 +120,7 @@ public SecretCacheObject(final String secretId, * * @return The cached GetSecretValue result based on the current cached state. */ - protected abstract GetSecretValueResponse getSecretValue(T result, String versionId, String versionStage);; + protected abstract GetSecretValueResponse getSecretValue(T result, String versionId, String versionStage); public abstract boolean equals(Object obj); public abstract int hashCode(); From 1f3549133d3bdfdb88ed0e9928b0f4fe22cb99b4 Mon Sep 17 00:00:00 2001 From: Angad Misra Date: Thu, 10 Apr 2025 15:07:56 -0700 Subject: [PATCH 3/4] fix: SpotBugs errors --- .../com/amazonaws/secretsmanager/caching/SecretCache.java | 8 ++++---- .../secretsmanager/caching/cache/SecretCacheItem.java | 6 +++--- .../secretsmanager/caching/cache/SecretCacheObject.java | 8 ++++---- .../secretsmanager/caching/cache/SecretCacheVersion.java | 4 ++-- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/main/java/com/amazonaws/secretsmanager/caching/SecretCache.java b/src/main/java/com/amazonaws/secretsmanager/caching/SecretCache.java index d7911d7..ee5bd3e 100644 --- a/src/main/java/com/amazonaws/secretsmanager/caching/SecretCache.java +++ b/src/main/java/com/amazonaws/secretsmanager/caching/SecretCache.java @@ -140,8 +140,8 @@ public String getSecretString(final String secretId) { * Retrieve and cache a secret string from AWS Secrets Manager. * * @param secretId the secret ID of the desired secret. - * @param versionId the version ID of the desired secret (optional, can be null). - * @param versionStage the version stage of the desired secret (optional, can be null). + * @param versionId the version ID of the desired secret (optional, can be null). + * @param versionStage the version stage of the desired secret (optional, can be null). * * @return The secret string for the desired secret. */ @@ -175,8 +175,8 @@ public ByteBuffer getSecretBinary(final String secretId) { * Retrieve and cache a secret binary from AWS Secrets Manager. * * @param secretId the secret ID of the desired secret. - * @param versionId the version ID of the desired secret (optional, can be null). - * @param versionStage the version stage of the desired secret (optional, can be null). + * @param versionId the version ID of the desired secret (optional, can be null). + * @param versionStage the version stage of the desired secret (optional, can be null). * * @return The secret binary for the desired secret. */ diff --git a/src/main/java/com/amazonaws/secretsmanager/caching/cache/SecretCacheItem.java b/src/main/java/com/amazonaws/secretsmanager/caching/cache/SecretCacheItem.java index 2eb43a2..08964f8 100644 --- a/src/main/java/com/amazonaws/secretsmanager/caching/cache/SecretCacheItem.java +++ b/src/main/java/com/amazonaws/secretsmanager/caching/cache/SecretCacheItem.java @@ -132,7 +132,7 @@ private SecretCacheVersion getVersion(DescribeSecretResponse describeResponse, S continue; } - if (versionId != null && entry.getKey() == versionId) { + if (versionId != null && versionId.equals(entry.getKey())) { currentVersionId = Optional.of(versionId); break; } @@ -174,8 +174,8 @@ protected GetSecretValueResponse getSecretValue(DescribeSecretResponse describeR * Return the cached GetSecretValue result. * * @param describeResponse the DescribeSecret result. - * @param versionId the version ID of the desired secret (optional, can be null). - * @param versionStage the version stage of the desired secret (optional, can be null). + * @param versionId the version ID of the desired secret (optional, can be null). + * @param versionStage the version stage of the desired secret (optional, can be null). * * @return The cached GetSecretValue result. */ diff --git a/src/main/java/com/amazonaws/secretsmanager/caching/cache/SecretCacheObject.java b/src/main/java/com/amazonaws/secretsmanager/caching/cache/SecretCacheObject.java index 113531a..c5564c8 100644 --- a/src/main/java/com/amazonaws/secretsmanager/caching/cache/SecretCacheObject.java +++ b/src/main/java/com/amazonaws/secretsmanager/caching/cache/SecretCacheObject.java @@ -115,8 +115,8 @@ public SecretCacheObject(final String secretId, * Execute the actual refresh of the cached secret state. * * @param result the GetSecretValue or DescribeSecret result. - * @param versionId the version ID of the desired secret (optional, can be null). - * @param versionStage the version stage of the desired secret (optional, can be null). + * @param versionId the version ID of the desired secret (optional, can be null). + * @param versionStage the version stage of the desired secret (optional, can be null). * * @return The cached GetSecretValue result based on the current cached state. */ @@ -260,8 +260,8 @@ public GetSecretValueResponse getSecretValue() { /** * Return the cached GetSecretValue result. * - * @param versionId the version ID of the desired secret (optional, can be null). - * @param versionStage the version stage of the desired secret (optional, can be null). + * @param versionId the version ID of the desired secret (optional, can be null). + * @param versionStage the version stage of the desired secret (optional, can be null). * * @return The cached GetSecretValue result. */ diff --git a/src/main/java/com/amazonaws/secretsmanager/caching/cache/SecretCacheVersion.java b/src/main/java/com/amazonaws/secretsmanager/caching/cache/SecretCacheVersion.java index 498a58f..20db42b 100644 --- a/src/main/java/com/amazonaws/secretsmanager/caching/cache/SecretCacheVersion.java +++ b/src/main/java/com/amazonaws/secretsmanager/caching/cache/SecretCacheVersion.java @@ -102,8 +102,8 @@ protected GetSecretValueResponse getSecretValue(GetSecretValueResponse gsvResult * Return the cached GetSecretValue result. * * @param gsvResult the GetSecretValue or DescribeSecret result. - * @param versionId the version ID of the desired secret (optional, can be null). - * @param versionStage the version stage of the desired secret (optional, can be null). + * @param versionId the version ID of the desired secret (optional, can be null). + * @param versionStage the version stage of the desired secret (optional, can be null). * * @return The cached GetSecretValue result. */ From 53a271bc36a8904329674f2640f5aa44137189a6 Mon Sep 17 00:00:00 2001 From: Angad Misra Date: Tue, 15 Apr 2025 09:56:25 -0700 Subject: [PATCH 4/4] fix: use method overloads --- .../secretsmanager/caching/SecretCache.java | 14 ++------------ .../caching/cache/SecretCacheItem.java | 4 +--- .../caching/cache/SecretCacheObject.java | 9 +-------- 3 files changed, 4 insertions(+), 23 deletions(-) diff --git a/src/main/java/com/amazonaws/secretsmanager/caching/SecretCache.java b/src/main/java/com/amazonaws/secretsmanager/caching/SecretCache.java index ee5bd3e..e0d5986 100644 --- a/src/main/java/com/amazonaws/secretsmanager/caching/SecretCache.java +++ b/src/main/java/com/amazonaws/secretsmanager/caching/SecretCache.java @@ -128,12 +128,7 @@ private SecretCacheItem getCachedSecret(final String secretId) { * @return The string secret */ public String getSecretString(final String secretId) { - SecretCacheItem secret = this.getCachedSecret(secretId); - GetSecretValueResponse gsv = secret.getSecretValue(); - if (null == gsv) { - return null; - } - return gsv.secretString(); + return getSecretString(secretId, null, null); } /** @@ -163,12 +158,7 @@ public String getSecretString(final String secretId, final String versionId, fin * @return The binary secret */ public ByteBuffer getSecretBinary(final String secretId) { - SecretCacheItem secret = this.getCachedSecret(secretId); - GetSecretValueResponse gsv = secret.getSecretValue(); - if (null == gsv) { - return null; - } - return gsv.secretBinary().asByteBuffer(); + return getSecretBinary(secretId, null, null); } /** diff --git a/src/main/java/com/amazonaws/secretsmanager/caching/cache/SecretCacheItem.java b/src/main/java/com/amazonaws/secretsmanager/caching/cache/SecretCacheItem.java index 08964f8..74a0331 100644 --- a/src/main/java/com/amazonaws/secretsmanager/caching/cache/SecretCacheItem.java +++ b/src/main/java/com/amazonaws/secretsmanager/caching/cache/SecretCacheItem.java @@ -165,9 +165,7 @@ private SecretCacheVersion getVersion(DescribeSecretResponse describeResponse, S */ @Override protected GetSecretValueResponse getSecretValue(DescribeSecretResponse describeResponse) { - SecretCacheVersion version = getVersion(describeResponse, null, null); - if (null == version) { return null; } - return version.getSecretValue(); + return getSecretValue(describeResponse, null, null); } /** diff --git a/src/main/java/com/amazonaws/secretsmanager/caching/cache/SecretCacheObject.java b/src/main/java/com/amazonaws/secretsmanager/caching/cache/SecretCacheObject.java index c5564c8..c7bc995 100644 --- a/src/main/java/com/amazonaws/secretsmanager/caching/cache/SecretCacheObject.java +++ b/src/main/java/com/amazonaws/secretsmanager/caching/cache/SecretCacheObject.java @@ -247,14 +247,7 @@ public boolean refreshNow() throws InterruptedException { * @return The cached GetSecretValue result. */ public GetSecretValueResponse getSecretValue() { - synchronized (lock) { - refresh(); - if (null == this.data) { - if (null != this.exception) { throw this.exception; } - } - - return this.getSecretValue(this.getResult()); - } + return getSecretValue(null, null); } /**