Skip to content

Commit 0ff91d3

Browse files
authored
Add option to set a property prefix on Secrets Manager property sources (#630)
Fixes #621
1 parent 99655eb commit 0ff91d3

File tree

3 files changed

+82
-11
lines changed

3 files changed

+82
-11
lines changed

docs/src/main/asciidoc/secrets-manager.adoc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,9 @@ import. The table below provides examples of supported property values and descr
109109
|`spring.config.import=aws-secretsmanager:secret-key;other-secret-key`
110110
|Importing secrets by individual keys
111111

112+
|`spring.config.import=aws-secretsmanager:secret-key?prefix=db.`
113+
|To avoid property keys collisions it is possible to configure property key prefix that gets added to each resolved property from a secret
114+
112115
|`spring.config.import=optional:aws-secretsmanager:secret-key;other-secret-key`
113116
|When `optional` is used the application will start even if the specified secret is not found.
114117

spring-cloud-aws-secrets-manager-config/src/main/java/io/awspring/cloud/secretsmanager/AwsSecretsManagerPropertySource.java

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import org.apache.commons.logging.LogFactory;
3232

3333
import org.springframework.core.env.EnumerablePropertySource;
34+
import org.springframework.lang.Nullable;
3435

3536
/**
3637
* Retrieves secret value under the given context / path from the AWS Secrets Manager
@@ -44,15 +45,25 @@ public class AwsSecretsManagerPropertySource extends EnumerablePropertySource<AW
4445

4546
private static Log LOG = LogFactory.getLog(AwsSecretsManagerPropertySource.class);
4647

48+
private static final String PREFIX_PART = "?prefix=";
49+
4750
private final ObjectMapper jsonMapper = new ObjectMapper();
4851

49-
private final String context;
52+
private final String secretId;
53+
54+
/**
55+
* Prefix that gets added to resolved property keys. Useful same property keys are
56+
* returned by multiple property sources.
57+
*/
58+
@Nullable
59+
private final String prefix;
5060

5161
private final Map<String, Object> properties = new LinkedHashMap<>();
5262

5363
public AwsSecretsManagerPropertySource(String context, AWSSecretsManager smClient) {
5464
super(context, smClient);
55-
this.context = context;
65+
this.secretId = resolveSecretId(context);
66+
this.prefix = resolvePrefix(context);
5667
}
5768

5869
/**
@@ -61,7 +72,7 @@ public AwsSecretsManagerPropertySource(String context, AWSSecretsManager smClien
6172
* database.
6273
*/
6374
public void init() {
64-
readSecretValue(new GetSecretValueRequest().withSecretId(context));
75+
readSecretValue(new GetSecretValueRequest().withSecretId(secretId));
6576
}
6677

6778
@Override
@@ -84,12 +95,30 @@ private void readSecretValue(GetSecretValueRequest secretValueRequest) {
8495

8596
for (Map.Entry<String, Object> secretEntry : secretMap.entrySet()) {
8697
LOG.debug("Populating property retrieved from AWS Secrets Manager: " + secretEntry.getKey());
87-
properties.put(secretEntry.getKey(), secretEntry.getValue());
98+
String propertyKey = prefix != null ? prefix + secretEntry.getKey() : secretEntry.getKey();
99+
properties.put(propertyKey, secretEntry.getValue());
88100
}
89101
}
90102
catch (JsonProcessingException e) {
91103
throw new RuntimeException(e);
92104
}
93105
}
94106

107+
@Nullable
108+
private static String resolvePrefix(String context) {
109+
int prefixIndex = context.indexOf(PREFIX_PART);
110+
if (prefixIndex != -1) {
111+
return context.substring(prefixIndex + PREFIX_PART.length());
112+
}
113+
return null;
114+
}
115+
116+
private static String resolveSecretId(String context) {
117+
int prefixIndex = context.indexOf(PREFIX_PART);
118+
if (prefixIndex != -1) {
119+
return context.substring(0, prefixIndex);
120+
}
121+
return context;
122+
}
123+
95124
}

spring-cloud-aws-secrets-manager-config/src/test/java/io/awspring/cloud/secretsmanager/AwsSecretsManagerPropertySourceTest.java

Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,10 @@
2020
import com.amazonaws.services.secretsmanager.model.GetSecretValueRequest;
2121
import com.amazonaws.services.secretsmanager.model.GetSecretValueResult;
2222
import com.amazonaws.services.secretsmanager.model.ResourceNotFoundException;
23+
import com.fasterxml.jackson.core.JsonProcessingException;
24+
import org.junit.jupiter.api.BeforeEach;
2325
import org.junit.jupiter.api.Test;
26+
import org.mockito.ArgumentCaptor;
2427

2528
import static org.assertj.core.api.Assertions.assertThat;
2629
import static org.assertj.core.api.Assertions.assertThatThrownBy;
@@ -30,31 +33,67 @@
3033

3134
class AwsSecretsManagerPropertySourceTest {
3235

33-
private AWSSecretsManager client = mock(AWSSecretsManager.class);
36+
private AWSSecretsManager client;
3437

35-
private AwsSecretsManagerPropertySource propertySource = new AwsSecretsManagerPropertySource("/config/myservice",
36-
client);
38+
private AwsSecretsManagerPropertySource propertySource;
39+
40+
private ArgumentCaptor<GetSecretValueRequest> secretValueRequestArgumentCaptor;
41+
42+
@BeforeEach
43+
void setUp() {
44+
client = mock(AWSSecretsManager.class);
45+
secretValueRequestArgumentCaptor = ArgumentCaptor.forClass(GetSecretValueRequest.class);
46+
propertySource = new AwsSecretsManagerPropertySource("/config/myservice", client);
47+
}
3748

3849
@Test
3950
void shouldParseSecretValue() {
40-
GetSecretValueResult secretValueResult = new GetSecretValueResult();
41-
secretValueResult.setSecretString("{\"key1\": \"value1\", \"key2\": \"value2\"}");
51+
GetSecretValueResult secretValueResult = new GetSecretValueResult()
52+
.withSecretString("{\"key1\": \"value1\", \"key2\": \"value2\"}");
4253

43-
when(client.getSecretValue(any(GetSecretValueRequest.class))).thenReturn(secretValueResult);
54+
when(client.getSecretValue(secretValueRequestArgumentCaptor.capture())).thenReturn(secretValueResult);
4455

4556
propertySource.init();
4657

58+
assertThat(secretValueRequestArgumentCaptor.getValue().getSecretId()).isEqualTo("/config/myservice");
4759
assertThat(propertySource.getPropertyNames()).containsExactly("key1", "key2");
4860
assertThat(propertySource.getProperty("key1")).isEqualTo("value1");
4961
assertThat(propertySource.getProperty("key2")).isEqualTo("value2");
5062
}
5163

64+
@Test
65+
void shouldAppendPrefixIfPrefixConfigured() {
66+
propertySource = new AwsSecretsManagerPropertySource("/config/myservice2?prefix=service2.", client);
67+
GetSecretValueResult secretValueResult = new GetSecretValueResult()
68+
.withSecretString("{\"key1\": \"value1\", \"key2\": \"value2\"}");
69+
70+
when(client.getSecretValue(secretValueRequestArgumentCaptor.capture())).thenReturn(secretValueResult);
71+
72+
propertySource.init();
73+
74+
assertThat(secretValueRequestArgumentCaptor.getValue().getSecretId()).isEqualTo("/config/myservice2");
75+
assertThat(propertySource.getPropertyNames()).containsExactly("service2.key1", "service2.key2");
76+
assertThat(propertySource.getProperty("service2.key1")).isEqualTo("value1");
77+
assertThat(propertySource.getProperty("service2.key2")).isEqualTo("value2");
78+
}
79+
5280
@Test
5381
void throwsExceptionWhenSecretNotFound() {
5482
when(client.getSecretValue(any(GetSecretValueRequest.class)))
5583
.thenThrow(new ResourceNotFoundException("secret not found"));
5684

57-
assertThatThrownBy(() -> propertySource.init()).isInstanceOf(ResourceNotFoundException.class);
85+
assertThatThrownBy(() -> propertySource.init()).isInstanceOf(ResourceNotFoundException.class)
86+
.hasMessageContaining("secret not found");
87+
}
88+
89+
@Test
90+
void throwsExceptionWhenSecretIsNotJsonSecret() {
91+
GetSecretValueResult secretValueResult = new GetSecretValueResult()
92+
.withSecretString("plain text secret string, not json secret");
93+
when(client.getSecretValue(any(GetSecretValueRequest.class))).thenReturn(secretValueResult);
94+
95+
assertThatThrownBy(() -> propertySource.init()).isInstanceOf(RuntimeException.class)
96+
.extracting(Throwable::getCause).isInstanceOf(JsonProcessingException.class);
5897
}
5998

6099
}

0 commit comments

Comments
 (0)