Skip to content

Commit ae87113

Browse files
fix: Make lazy-loading of secrets atomic and handle updateValue with unloaded secrets
Use ConcurrentMap.compute() in loadSecretIfNeeded() to ensure the sentinel check and secret retrieval are performed atomically, avoiding race conditions with concurrent access. Add loadSecretIfNeeded() call at the beginning of updateValue() to ensure secrets are loaded before update logic runs, preventing the NOT_LOADED_SENTINEL from being treated as a plain value store. Signed-off-by: Sumit Bhattacharya <sumit4739@gmail.com>
1 parent 7360f59 commit ae87113

2 files changed

Lines changed: 55 additions & 8 deletions

File tree

data-prepper-plugins/aws-plugin/src/main/java/org/opensearch/dataprepper/plugins/aws/AwsSecretsSupplier.java

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -123,18 +123,21 @@ public Object retrieveValue(String secretId) {
123123

124124
/**
125125
* Loads a secret if it was skipped on start (lazy-loading).
126-
*
126+
* Uses {@link ConcurrentMap#compute} to ensure atomicity of the sentinel check and refresh.
127+
*
127128
* @param secretId The secret configuration ID
128129
* @return The loaded secret value
129130
*/
130131
private Object loadSecretIfNeeded(String secretId) {
131-
Object value = secretIdToValue.get(secretId);
132-
if (value == NOT_LOADED_SENTINEL) {
133-
LOG.info("Secret {} was not loaded on start, loading now on first access.", secretId);
134-
refresh(secretId);
135-
value = secretIdToValue.get(secretId);
136-
}
137-
return value;
132+
return secretIdToValue.compute(secretId, (key, currentValue) -> {
133+
if (currentValue == NOT_LOADED_SENTINEL) {
134+
LOG.info("Secret {} was not loaded on start, loading now on first access.", key);
135+
final AwsSecretManagerConfiguration config = awsSecretManagerConfigurationMap.get(key);
136+
final SecretsManagerClient client = secretsManagerClientMap.get(key);
137+
return retrieveSecretsFromSecretManager(config, client);
138+
}
139+
return currentValue;
140+
});
138141
}
139142

140143

@@ -184,6 +187,8 @@ public String updateValue(String secretId, Object newValue) {
184187

185188
@Override
186189
public String updateValue(String secretId, String keyToUpdate, Object newValue) {
190+
// Ensure the secret is loaded before attempting to update
191+
loadSecretIfNeeded(secretId);
187192
Object currentSecretStore = secretIdToValue.get(secretId);
188193
if (currentSecretStore instanceof Map) {
189194
if (keyToUpdate == null) {

data-prepper-plugins/aws-plugin/src/test/java/org/opensearch/dataprepper/plugins/aws/AwsSecretsSupplierLazyLoadTest.java

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,15 @@
2020
import software.amazon.awssdk.services.secretsmanager.SecretsManagerClient;
2121
import software.amazon.awssdk.services.secretsmanager.model.GetSecretValueRequest;
2222
import software.amazon.awssdk.services.secretsmanager.model.GetSecretValueResponse;
23+
import software.amazon.awssdk.services.secretsmanager.model.PutSecretValueRequest;
24+
import software.amazon.awssdk.services.secretsmanager.model.PutSecretValueResponse;
2325

2426
import java.util.Map;
2527
import java.util.UUID;
2628

2729
import static org.hamcrest.CoreMatchers.equalTo;
2830
import static org.hamcrest.MatcherAssert.assertThat;
31+
import static org.mockito.ArgumentMatchers.any;
2932
import static org.mockito.ArgumentMatchers.eq;
3033
import static org.mockito.Mockito.never;
3134
import static org.mockito.Mockito.times;
@@ -61,6 +64,12 @@ class AwsSecretsSupplierLazyLoadTest {
6164
@Mock
6265
private GetSecretValueResponse getSecretValueResponse;
6366

67+
@Mock
68+
private PutSecretValueRequest putSecretValueRequest;
69+
70+
@Mock
71+
private PutSecretValueResponse putSecretValueResponse;
72+
6473
@Mock
6574
private AwsCredentialsSupplier awsCredentialsSupplier;
6675

@@ -131,4 +140,37 @@ void testSecretWithSkipValidationOnStartFalse_LoadsAtConstruction() throws JsonP
131140
verify(secretsManagerClient, times(1)).getSecretValue(eq(getSecretValueRequest));
132141
assertThat(value, equalTo(testValue));
133142
}
143+
144+
@Test
145+
void testUpdateValue_withSkipValidationOnStart_loadsSecretBeforeUpdate() throws JsonProcessingException {
146+
// Given: Secret configured with skip_validation_on_start=true
147+
when(awsSecretPluginConfig.getAwsSecretManagerConfigurationMap()).thenReturn(
148+
Map.of(testSecretId, awsSecretManagerConfiguration)
149+
);
150+
when(awsSecretManagerConfiguration.isSkipValidationOnStart()).thenReturn(true);
151+
when(awsSecretManagerConfiguration.createSecretManagerClient(awsCredentialsSupplier)).thenReturn(secretsManagerClient);
152+
when(awsSecretManagerConfiguration.createGetSecretValueRequest()).thenReturn(getSecretValueRequest);
153+
when(secretValueDecoder.decode(eq(getSecretValueResponse))).thenReturn(objectMapper.writeValueAsString(
154+
Map.of(testKey, testValue)
155+
));
156+
when(secretsManagerClient.getSecretValue(eq(getSecretValueRequest))).thenReturn(getSecretValueResponse);
157+
when(awsSecretManagerConfiguration.putSecretValueRequest(any())).thenReturn(putSecretValueRequest);
158+
when(secretsManagerClient.putSecretValue(eq(putSecretValueRequest))).thenReturn(putSecretValueResponse);
159+
final String newVersionId = UUID.randomUUID().toString();
160+
when(putSecretValueResponse.versionId()).thenReturn(newVersionId);
161+
162+
final AwsSecretsSupplier supplier = new AwsSecretsSupplier(
163+
secretValueDecoder, awsSecretPluginConfig, objectMapper, awsCredentialsSupplier
164+
);
165+
166+
// Then: Secret is NOT retrieved at construction time
167+
verify(secretsManagerClient, never()).getSecretValue(eq(getSecretValueRequest));
168+
169+
// When: updateValue is called before any retrieveValue
170+
final String versionId = supplier.updateValue(testSecretId, testKey, "newValue");
171+
172+
// Then: Secret was loaded on-demand and update succeeded
173+
verify(secretsManagerClient, times(1)).getSecretValue(eq(getSecretValueRequest));
174+
assertThat(versionId, equalTo(newVersionId));
175+
}
134176
}

0 commit comments

Comments
 (0)