Skip to content
Open
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
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,12 @@ You can get the latest release from Maven:
<dependency>
<groupId>com.amazonaws.secretsmanager</groupId>
<artifactId>aws-secretsmanager-caching-java</artifactId>
<version>2.2.0</version>
<version>x.y.z</version>
</dependency>
```

Replace `x.y.z` with the latest version, which you can find on [Maven Central](https://central.sonatype.com/artifact/com.amazonaws.secretsmanager/aws-secretsmanager-caching-java).

Don't forget to enable the download of snapshot jars from Maven:

```xml
Expand Down
9 changes: 9 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,15 @@
</dependency>
</dependencies>
<build>
<resources>
<!-- Filter version.properties so ${project.version} is substituted at build time.
This makes pom.xml the single source of truth for the version reported in the
SDK UserAgent header (read at runtime by VersionInfo). -->
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,23 +68,49 @@ public SecretCache() {
* using the
* provided builder.
*
* <p>
* Any UserAgent suffix already configured on the builder is preserved: the
* caching client identifier ({@code AwsSecretCache/<version>}) is appended to
* it rather than overwriting it.
*
* @param builder The builder to use for creating the AWS Secrets Manager
* client.
*/
@SuppressFBWarnings(value = "CT_CONSTRUCTOR_THROW",
justification = "Delegates to constructor that validates before field initialization")
public SecretCache(SecretsManagerClientBuilder builder) {
this(new SecretCacheConfiguration().withClient(builder
.overrideConfiguration(
builder.overrideConfiguration().toBuilder()
.putAdvancedOption(SdkAdvancedClientOption.USER_AGENT_SUFFIX, VersionInfo.USER_AGENT)
.build())
.build()));
this(new SecretCacheConfiguration().withClient(buildClientWithUserAgent(builder)));
}

/**
* Builds a client from the provided builder, appending the caching client
* UserAgent identifier while preserving any suffix the caller already set.
*
* @param builder The caller-provided builder.
* @return The built AWS Secrets Manager client.
*/
private static SecretsManagerClient buildClientWithUserAgent(SecretsManagerClientBuilder builder) {
ClientOverrideConfiguration existingOverride = builder.overrideConfiguration();
String callerSuffix = existingOverride
.advancedOption(SdkAdvancedClientOption.USER_AGENT_SUFFIX).orElse(null);
ClientOverrideConfiguration mergedOverride = existingOverride.toBuilder()
.putAdvancedOption(SdkAdvancedClientOption.USER_AGENT_SUFFIX,
VersionInfo.userAgentSuffix(callerSuffix))
.build();
return builder.overrideConfiguration(mergedOverride).build();
}

/**
* Constructs a new secret cache using the provided AWS Secrets Manager client.
*
* <p>
* Note: a pre-built {@link SecretsManagerClient} is immutable, so the caching
* client UserAgent identifier ({@code AwsSecretCache/<version>}) cannot be
* appended to it here. Callers who want the caching identifier reported in
* service logs should instead pass a {@link SecretsManagerClientBuilder} (via
* {@link #SecretCache(SecretsManagerClientBuilder)}), which preserves any
* existing suffix and appends the caching identifier.
*
* @param client The AWS Secrets Manager client to use for requesting secret
* values.
*/
Expand Down Expand Up @@ -117,7 +143,8 @@ public SecretCache(SecretCacheConfiguration config) {
this.cache = new LRUCache<String, SecretCacheItem>(config.getMaxCacheSize());
this.config = config;
ClientOverrideConfiguration defaultOverride = ClientOverrideConfiguration.builder()
.putAdvancedOption(SdkAdvancedClientOption.USER_AGENT_SUFFIX, VersionInfo.USER_AGENT).build();
.putAdvancedOption(SdkAdvancedClientOption.USER_AGENT_SUFFIX,
VersionInfo.userAgentSuffix(null)).build();

if (config.getClient() != null) {
this.client = config.getClient();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,107 @@

package com.amazonaws.secretsmanager.caching.cache.internal;

import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;

/**
* This class specifies the versioning system for the AWS SecretsManager caching
* client.
*
* <p>
* The library version is defined in a single place ({@code pom.xml}) and made
* available at runtime through the {@code version.properties} classpath resource,
* whose {@code ${project.version}} placeholder is substituted at build time via
* Maven resource filtering. This avoids the version drift that occurs when the
* version has to be maintained in more than one place.
*/
public class VersionInfo {
public final class VersionInfo {

/** Placeholder returned when the version cannot be resolved at runtime. */
public static final String UNKNOWN_VERSION = "unknown";

/** Prefix identifying this caching client in the SDK UserAgent header. */
public static final String USER_AGENT_PREFIX = "AwsSecretCache/";

/** Name of the filtered classpath resource holding the version. */
static final String VERSION_RESOURCE = "version.properties";

/** Properties key under which the version is stored. */
static final String VERSION_KEY = "version";

/**
* Library version number
* Library version number, resolved at runtime from {@code pom.xml} via the
* filtered {@code version.properties} resource. Falls back to
* {@link #UNKNOWN_VERSION} when the resource is unavailable.
*/
public static final String RELEASE_VERSION = "2.1.0";
public static final String RELEASE_VERSION = resolveVersion();

/**
* User agent for AWS Secrets Manager API calls.
*/
public static final String USER_AGENT = "AwsSecretCache/" + RELEASE_VERSION;
public static final String USER_AGENT = USER_AGENT_PREFIX + RELEASE_VERSION;

private VersionInfo() {
}
}

/**
* Resolves the library version from the filtered {@code version.properties}
* classpath resource. Never throws; returns {@link #UNKNOWN_VERSION} if the
* resource is missing or unreadable, so that a failure to resolve the version
* can never break secret retrieval.
*
* @return the resolved version, or {@link #UNKNOWN_VERSION}.
*/
private static String resolveVersion() {
return readVersion(VersionInfo.class.getResourceAsStream(VERSION_RESOURCE));
}

/**
* Parses the {@code version} property from the given stream, closing the
* stream before returning.
*
* @param in the properties stream (may be {@code null}); this method takes
* ownership and closes it.
* @return the version value, or {@link #UNKNOWN_VERSION} when the stream is
* {@code null}, unreadable, missing the key, blank, or still contains
* an unsubstituted Maven placeholder.
*/
public static String readVersion(final InputStream in) {
if (in == null) {
return UNKNOWN_VERSION;
}
try (InputStream stream = in) {
Properties properties = new Properties();
properties.load(stream);
String version = properties.getProperty(VERSION_KEY);
if (version == null) {
return UNKNOWN_VERSION;
}
version = version.trim();
if (version.isEmpty() || version.contains("${")) {
return UNKNOWN_VERSION;
}
return version;
} catch (IOException e) {
return UNKNOWN_VERSION;
}
}

/**
* Builds the UserAgent suffix for the caching client, preserving any suffix a
* caller has already configured. When a caller suffix is present, the caching
* identifier is appended to it ({@code "<caller> AwsSecretCache/<version>"});
* otherwise the caching identifier is returned on its own.
*
* @param callerSuffix the caller-provided UserAgent suffix (may be {@code null}
* or blank).
* @return the combined UserAgent suffix.
*/
public static String userAgentSuffix(final String callerSuffix) {
if (callerSuffix == null || callerSuffix.trim().isEmpty()) {
return USER_AGENT;
}
return callerSuffix + " " + USER_AGENT;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
version=${project.version}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@

import org.mockito.ArgumentMatcher;
import org.mockito.ArgumentMatchers;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
Expand All @@ -32,13 +33,17 @@
import org.testng.annotations.Test;

import software.amazon.awssdk.core.SdkBytes;
import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration;
import software.amazon.awssdk.core.client.config.SdkAdvancedClientOption;
import software.amazon.awssdk.services.secretsmanager.SecretsManagerClient;
import software.amazon.awssdk.services.secretsmanager.SecretsManagerClientBuilder;
import software.amazon.awssdk.services.secretsmanager.model.DescribeSecretRequest;
import software.amazon.awssdk.services.secretsmanager.model.DescribeSecretResponse;
import software.amazon.awssdk.services.secretsmanager.model.GetSecretValueRequest;
import software.amazon.awssdk.services.secretsmanager.model.GetSecretValueResponse;

import com.amazonaws.secretsmanager.caching.cache.internal.VersionInfo;

/**
* SecretCacheTest.
*/
Expand Down Expand Up @@ -526,4 +531,41 @@ public void testSecretCacheWithPQTLSEnabledNoClient() {
}
}
}

@Test
public void builderPathPreservesAndAppendsUserAgent() {
// A caller who already configured their own UserAgent suffix on the builder.
SecretsManagerClientBuilder builder = Mockito.mock(SecretsManagerClientBuilder.class);
ClientOverrideConfiguration existing = ClientOverrideConfiguration.builder()
.putAdvancedOption(SdkAdvancedClientOption.USER_AGENT_SUFFIX, "MyApp/9.9").build();
Mockito.when(builder.overrideConfiguration()).thenReturn(existing);
Mockito.when(builder.overrideConfiguration(Mockito.any(ClientOverrideConfiguration.class))).thenReturn(builder);
Mockito.when(builder.build()).thenReturn(asm);

new SecretCache(builder).close();

ArgumentCaptor<ClientOverrideConfiguration> captor = ArgumentCaptor.forClass(ClientOverrideConfiguration.class);
Mockito.verify(builder).overrideConfiguration(captor.capture());
String suffix = captor.getValue().advancedOption(SdkAdvancedClientOption.USER_AGENT_SUFFIX).orElse(null);

Assert.assertEquals(suffix, "MyApp/9.9 " + VersionInfo.USER_AGENT);
}

@Test
public void builderPathSetsOursWhenNoCallerSuffix() {
// A caller who did not configure any UserAgent suffix.
SecretsManagerClientBuilder builder = Mockito.mock(SecretsManagerClientBuilder.class);
ClientOverrideConfiguration existing = ClientOverrideConfiguration.builder().build();
Mockito.when(builder.overrideConfiguration()).thenReturn(existing);
Mockito.when(builder.overrideConfiguration(Mockito.any(ClientOverrideConfiguration.class))).thenReturn(builder);
Mockito.when(builder.build()).thenReturn(asm);

new SecretCache(builder).close();

ArgumentCaptor<ClientOverrideConfiguration> captor = ArgumentCaptor.forClass(ClientOverrideConfiguration.class);
Mockito.verify(builder).overrideConfiguration(captor.capture());
String suffix = captor.getValue().advancedOption(SdkAdvancedClientOption.USER_AGENT_SUFFIX).orElse(null);

Assert.assertEquals(suffix, VersionInfo.USER_AGENT);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
package com.amazonaws.secretsmanager.caching.internal;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.regex.Pattern;

import org.testng.Assert;
Expand All @@ -8,11 +12,80 @@
import com.amazonaws.secretsmanager.caching.cache.internal.VersionInfo;

public class VersionInfoTest {

@Test
public void versionInfoIsValid() {
String ua = VersionInfo.USER_AGENT;
Pattern p = Pattern.compile("AwsSecretCache/\\d+.\\d+.\\d+");

Assert.assertTrue(p.matcher(ua).matches(), "User agent " + ua + " is not valid");
}

@Test
public void releaseVersionResolvesToSemver() {
Pattern p = Pattern.compile("\\d+\\.\\d+\\.\\d+");

Assert.assertTrue(p.matcher(VersionInfo.RELEASE_VERSION).matches(),
"RELEASE_VERSION should resolve to a semver but was " + VersionInfo.RELEASE_VERSION);
}

@Test
public void readVersionParsesValidStream() {
InputStream in = new ByteArrayInputStream("version=1.2.3".getBytes(StandardCharsets.UTF_8));

Assert.assertEquals(VersionInfo.readVersion(in), "1.2.3");
}

@Test
public void readVersionFallsBackOnNullStream() {
Assert.assertEquals(VersionInfo.readVersion(null), VersionInfo.UNKNOWN_VERSION);
}

@Test
public void readVersionFallsBackWhenKeyMissing() {
InputStream in = new ByteArrayInputStream("foo=bar".getBytes(StandardCharsets.UTF_8));

Assert.assertEquals(VersionInfo.readVersion(in), VersionInfo.UNKNOWN_VERSION);
}

@Test
public void readVersionFallsBackOnUnfilteredPlaceholder() {
InputStream in = new ByteArrayInputStream("version=${project.version}".getBytes(StandardCharsets.UTF_8));

Assert.assertEquals(VersionInfo.readVersion(in), VersionInfo.UNKNOWN_VERSION);
}

@Test
public void readVersionFallsBackOnEmptyValue() {
InputStream in = new ByteArrayInputStream("version=".getBytes(StandardCharsets.UTF_8));

Assert.assertEquals(VersionInfo.readVersion(in), VersionInfo.UNKNOWN_VERSION);
}

@Test
public void readVersionFallsBackOnIoException() {
InputStream in = new InputStream() {
@Override
public int read() throws IOException {
throw new IOException("simulated read failure");
}
};

Assert.assertEquals(VersionInfo.readVersion(in), VersionInfo.UNKNOWN_VERSION);
}

@Test
public void userAgentSuffixAppendsCallerSuffix() {
Assert.assertEquals(VersionInfo.userAgentSuffix("MyApp/9.9"), "MyApp/9.9 " + VersionInfo.USER_AGENT);
}

@Test
public void userAgentSuffixWithNullCaller() {
Assert.assertEquals(VersionInfo.userAgentSuffix(null), VersionInfo.USER_AGENT);
}

@Test
public void userAgentSuffixWithBlankCaller() {
Assert.assertEquals(VersionInfo.userAgentSuffix(" "), VersionInfo.USER_AGENT);
}
}
Loading