Skip to content

Commit c372fcf

Browse files
Ryan Baxterryanjbaxter
Ryan Baxter
andauthoredApr 5, 2024··
Set the FailesafeTextEncryptor delegate if we can create a valid KeyProperties (#2399)
Fixes #2330 Co-authored-by: Ryan Baxter <[email protected]>
1 parent 8e7595b commit c372fcf

File tree

2 files changed

+120
-8
lines changed

2 files changed

+120
-8
lines changed
 

‎spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServerConfigDataLocationResolver.java

+52-8
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,16 @@
3737
import org.springframework.boot.context.properties.bind.Bindable;
3838
import org.springframework.boot.context.properties.bind.Binder;
3939
import org.springframework.boot.logging.DeferredLogFactory;
40+
import org.springframework.cloud.bootstrap.encrypt.KeyProperties;
41+
import org.springframework.cloud.bootstrap.encrypt.RsaProperties;
42+
import org.springframework.cloud.bootstrap.encrypt.TextEncryptorUtils;
4043
import org.springframework.cloud.client.ServiceInstance;
44+
import org.springframework.cloud.context.encrypt.EncryptorFactory;
4145
import org.springframework.core.Ordered;
4246
import org.springframework.core.log.LogMessage;
4347
import org.springframework.retry.support.RetryTemplate;
48+
import org.springframework.security.crypto.encrypt.TextEncryptor;
49+
import org.springframework.util.ClassUtils;
4450
import org.springframework.util.ObjectUtils;
4551
import org.springframework.util.StringUtils;
4652
import org.springframework.web.client.RestTemplate;
@@ -54,6 +60,8 @@ public class ConfigServerConfigDataLocationResolver
5460
* Prefix for Config Server imports.
5561
*/
5662
public static final String PREFIX = "configserver:";
63+
static final boolean RSA_IS_PRESENT = ClassUtils
64+
.isPresent("org.springframework.security.rsa.crypto.RsaSecretEncryptor", null);
5765

5866
private final Log log;
5967

@@ -66,6 +74,41 @@ public int getOrder() {
6674
return -1;
6775
}
6876

77+
/*
78+
* Depending on whether encrypt.key is set as an environment or system property we may
79+
* have created a TextEncryptor implementation or just created a
80+
* FailsafeTextEncryptor. This is because when TextEncryptorConfigBootstrapper runs we
81+
* have not yet loaded any configuration files (application.yaml | properties etc).
82+
* However, at this point when the ConfigServerConfigDataLocationResolver is resolving
83+
* configuration we would have resolved the configuration files on the classpath at
84+
* least so we can potentially create a properly configured TextEncryptor. So if the
85+
* FailsafeTextEncryptor is in the context and we can create a TextEncryptor then we
86+
* set the delegate in the FailsafeTextEncryptor so that we can decrypt any encrypted
87+
* properties at this point.
88+
*/
89+
protected void setTextEncryptorDelegate(ConfigDataLocationResolverContext context) {
90+
if (context.getBootstrapContext().isRegistered(TextEncryptor.class)) {
91+
Binder binder = context.getBinder();
92+
KeyProperties keyProperties = binder.bindOrCreate(KeyProperties.PREFIX, Bindable.of(KeyProperties.class));
93+
boolean textEncryptorRegistered = context.getBootstrapContext().isRegistered(TextEncryptor.class);
94+
if (TextEncryptorUtils.keysConfigured(keyProperties) && textEncryptorRegistered) {
95+
TextEncryptor textEncryptor = context.getBootstrapContext().get(TextEncryptor.class);
96+
if (textEncryptor instanceof TextEncryptorUtils.FailsafeTextEncryptor failsafeTextEncryptor) {
97+
TextEncryptor delegate;
98+
if (RSA_IS_PRESENT) {
99+
RsaProperties rsaProperties = binder.bindOrCreate(RsaProperties.PREFIX,
100+
Bindable.of(RsaProperties.class));
101+
delegate = TextEncryptorUtils.createTextEncryptor(keyProperties, rsaProperties);
102+
}
103+
else {
104+
delegate = new EncryptorFactory(keyProperties.getSalt()).create(keyProperties.getKey());
105+
}
106+
failsafeTextEncryptor.setDelegate(delegate);
107+
}
108+
}
109+
}
110+
}
111+
69112
protected PropertyHolder loadProperties(ConfigDataLocationResolverContext context, String uris) {
70113
Binder binder = context.getBinder();
71114
BindHandler bindHandler = getBindHandler(context);
@@ -182,6 +225,7 @@ public List<ConfigServerConfigDataResource> resolve(ConfigDataLocationResolverCo
182225
public List<ConfigServerConfigDataResource> resolveProfileSpecific(
183226
ConfigDataLocationResolverContext resolverContext, ConfigDataLocation location, Profiles profiles)
184227
throws ConfigDataLocationNotFoundException {
228+
setTextEncryptorDelegate(resolverContext);
185229
String uris = location.getNonPrefixedValue(getPrefix());
186230
PropertyHolder propertyHolder = loadProperties(resolverContext, uris);
187231
ConfigClientProperties properties = propertyHolder.properties;
@@ -269,14 +313,6 @@ public List<ServiceInstance> getConfigServerInstances(String serviceId) {
269313
return locations;
270314
}
271315

272-
private class PropertyHolder {
273-
274-
ConfigClientProperties properties;
275-
276-
RetryProperties retryProperties;
277-
278-
}
279-
280316
public static class PropertyResolver {
281317

282318
private final Binder binder;
@@ -302,4 +338,12 @@ public <T> T resolveOrCreateConfigurationProperties(String prefix, Class<T> type
302338

303339
}
304340

341+
private class PropertyHolder {
342+
343+
ConfigClientProperties properties;
344+
345+
RetryProperties retryProperties;
346+
347+
}
348+
305349
}

‎spring-cloud-config-client/src/test/java/org/springframework/cloud/config/client/ConfigServerConfigDataLocationResolverTests.java

+68
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,23 @@
2121

2222
import org.junit.jupiter.api.BeforeEach;
2323
import org.junit.jupiter.api.Test;
24+
import org.mockito.ArgumentCaptor;
2425

26+
import org.springframework.beans.factory.support.InstanceSupplier;
27+
import org.springframework.boot.BootstrapRegistry;
2528
import org.springframework.boot.ConfigurableBootstrapContext;
2629
import org.springframework.boot.context.config.ConfigDataLocation;
2730
import org.springframework.boot.context.config.ConfigDataLocationResolverContext;
2831
import org.springframework.boot.context.config.Profiles;
32+
import org.springframework.boot.context.properties.bind.BindHandler;
2933
import org.springframework.boot.context.properties.bind.Binder;
3034
import org.springframework.boot.logging.DeferredLog;
35+
import org.springframework.cloud.bootstrap.TextEncryptorBindHandler;
36+
import org.springframework.cloud.bootstrap.encrypt.KeyProperties;
37+
import org.springframework.cloud.bootstrap.encrypt.TextEncryptorUtils;
38+
import org.springframework.cloud.context.encrypt.EncryptorFactory;
3139
import org.springframework.mock.env.MockEnvironment;
40+
import org.springframework.security.crypto.encrypt.TextEncryptor;
3241

3342
import static org.assertj.core.api.Assertions.assertThat;
3443
import static org.mockito.Mockito.eq;
@@ -205,6 +214,65 @@ void multipleImportEntriesDoesNotShareSameURIs() {
205214
assertThat(resource2.getProperties().getUri()).isEqualTo(new String[] { "http://urlNo2" });
206215
}
207216

217+
@Test
218+
void setFailsafeDelegateKeysNotConfigured() {
219+
ConfigurableBootstrapContext bootstrapContext = mock(ConfigurableBootstrapContext.class);
220+
when(bootstrapContext.isRegistered(eq(ConfigClientProperties.class))).thenReturn(true);
221+
KeyProperties keyProperties = new KeyProperties();
222+
ConfigClientProperties configClientProperties = new ConfigClientProperties();
223+
configClientProperties.setUri(new String[] { "http://myuri" });
224+
when(bootstrapContext.isRegistered(TextEncryptor.class)).thenReturn(true);
225+
when(bootstrapContext.get(TextEncryptor.class)).thenReturn(new TextEncryptorUtils.FailsafeTextEncryptor());
226+
when(bootstrapContext.get(eq(ConfigClientProperties.class))).thenReturn(configClientProperties);
227+
when(context.getBootstrapContext()).thenReturn(bootstrapContext);
228+
this.resolver.resolve(context, ConfigDataLocation.of("configserver:http://urlNo1"));
229+
TextEncryptor textEncryptor = bootstrapContext.get(TextEncryptor.class);
230+
assertThat(textEncryptor).isInstanceOf(TextEncryptorUtils.FailsafeTextEncryptor.class);
231+
assertThat(((TextEncryptorUtils.FailsafeTextEncryptor) textEncryptor).getDelegate()).isNull();
232+
}
233+
234+
@Test
235+
void setFailsafeDelegateKeysConfigured() {
236+
ConfigurableBootstrapContext bootstrapContext = mock(ConfigurableBootstrapContext.class);
237+
when(bootstrapContext.isRegistered(eq(ConfigClientProperties.class))).thenReturn(true);
238+
environment.setProperty("encrypt.key", "mykey");
239+
240+
// The value is "password" encrypted with the key "mykey"
241+
environment.setProperty("spring.cloud.config.password",
242+
"{cipher}6defc102cd76752fcf4c78231ed82ead85133a09741d9a1442595b4800e2b3d1");
243+
ConfigClientProperties configClientProperties = new ConfigClientProperties();
244+
configClientProperties.setUri(new String[] { "http://myuri" });
245+
when(bootstrapContext.isRegistered(TextEncryptor.class)).thenReturn(true);
246+
when(bootstrapContext.get(TextEncryptor.class)).thenReturn(new TextEncryptorUtils.FailsafeTextEncryptor());
247+
when(bootstrapContext.get(eq(ConfigClientProperties.class))).thenReturn(configClientProperties);
248+
when(context.getBootstrapContext()).thenReturn(bootstrapContext);
249+
KeyProperties keyProperties = new KeyProperties();
250+
keyProperties.setKey("mykey");
251+
252+
// Use this TextEncryptor in the BindHandler we return so it will decrypt the
253+
// password when we bind ConfigClientProperties
254+
TextEncryptor bindHandlerTextEncryptor = new EncryptorFactory(keyProperties.getSalt())
255+
.create(keyProperties.getKey());
256+
when(context.getBootstrapContext().getOrElse(eq(BindHandler.class), eq(null)))
257+
.thenReturn(new TextEncryptorBindHandler(bindHandlerTextEncryptor, keyProperties));
258+
259+
// Call resolve so we can test that the delegate is added to the
260+
// FailsafeTextEncryptor
261+
this.resolver.resolve(context, ConfigDataLocation.of("configserver:http://urlNo1"));
262+
TextEncryptor textEncryptor = bootstrapContext.get(TextEncryptor.class);
263+
264+
// Capture the ConfigClientProperties we create and register in
265+
// ConfigServerConfigDataLocationResolver.resolveProfileSpecific
266+
// it should have the decrypted passord in it
267+
ArgumentCaptor<BootstrapRegistry.InstanceSupplier<ConfigClientProperties>> captor = ArgumentCaptor
268+
.forClass(BootstrapRegistry.InstanceSupplier.class);
269+
verify(bootstrapContext).register(eq(ConfigClientProperties.class), captor.capture());
270+
assertThat(captor.getValue().get(bootstrapContext).getPassword()).isEqualTo("password");
271+
assertThat(textEncryptor).isInstanceOf(TextEncryptorUtils.FailsafeTextEncryptor.class);
272+
assertThat(((TextEncryptorUtils.FailsafeTextEncryptor) textEncryptor).getDelegate())
273+
.isInstanceOf(TextEncryptor.class);
274+
}
275+
208276
private ConfigServerConfigDataResource testUri(String propertyUri, String locationUri) {
209277
this.environment.setProperty(ConfigClientProperties.PREFIX + ".uri", propertyUri);
210278
when(context.getBootstrapContext()).thenReturn(mock(ConfigurableBootstrapContext.class));

0 commit comments

Comments
 (0)
Please sign in to comment.