Skip to content

Commit

Permalink
Set the FailesafeTextEncryptor delegate if we can create a valid KeyP…
Browse files Browse the repository at this point in the history
…roperties (spring-cloud#2399)

Fixes spring-cloud#2330

Co-authored-by: Ryan Baxter <[email protected]>
  • Loading branch information
Ryan Baxter and ryanjbaxter authored Apr 5, 2024
1 parent 8e7595b commit c372fcf
Show file tree
Hide file tree
Showing 2 changed files with 120 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,16 @@
import org.springframework.boot.context.properties.bind.Bindable;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.boot.logging.DeferredLogFactory;
import org.springframework.cloud.bootstrap.encrypt.KeyProperties;
import org.springframework.cloud.bootstrap.encrypt.RsaProperties;
import org.springframework.cloud.bootstrap.encrypt.TextEncryptorUtils;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.context.encrypt.EncryptorFactory;
import org.springframework.core.Ordered;
import org.springframework.core.log.LogMessage;
import org.springframework.retry.support.RetryTemplate;
import org.springframework.security.crypto.encrypt.TextEncryptor;
import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.client.RestTemplate;
Expand All @@ -54,6 +60,8 @@ public class ConfigServerConfigDataLocationResolver
* Prefix for Config Server imports.
*/
public static final String PREFIX = "configserver:";
static final boolean RSA_IS_PRESENT = ClassUtils
.isPresent("org.springframework.security.rsa.crypto.RsaSecretEncryptor", null);

private final Log log;

Expand All @@ -66,6 +74,41 @@ public int getOrder() {
return -1;
}

/*
* Depending on whether encrypt.key is set as an environment or system property we may
* have created a TextEncryptor implementation or just created a
* FailsafeTextEncryptor. This is because when TextEncryptorConfigBootstrapper runs we
* have not yet loaded any configuration files (application.yaml | properties etc).
* However, at this point when the ConfigServerConfigDataLocationResolver is resolving
* configuration we would have resolved the configuration files on the classpath at
* least so we can potentially create a properly configured TextEncryptor. So if the
* FailsafeTextEncryptor is in the context and we can create a TextEncryptor then we
* set the delegate in the FailsafeTextEncryptor so that we can decrypt any encrypted
* properties at this point.
*/
protected void setTextEncryptorDelegate(ConfigDataLocationResolverContext context) {
if (context.getBootstrapContext().isRegistered(TextEncryptor.class)) {
Binder binder = context.getBinder();
KeyProperties keyProperties = binder.bindOrCreate(KeyProperties.PREFIX, Bindable.of(KeyProperties.class));
boolean textEncryptorRegistered = context.getBootstrapContext().isRegistered(TextEncryptor.class);
if (TextEncryptorUtils.keysConfigured(keyProperties) && textEncryptorRegistered) {
TextEncryptor textEncryptor = context.getBootstrapContext().get(TextEncryptor.class);
if (textEncryptor instanceof TextEncryptorUtils.FailsafeTextEncryptor failsafeTextEncryptor) {
TextEncryptor delegate;
if (RSA_IS_PRESENT) {
RsaProperties rsaProperties = binder.bindOrCreate(RsaProperties.PREFIX,
Bindable.of(RsaProperties.class));
delegate = TextEncryptorUtils.createTextEncryptor(keyProperties, rsaProperties);
}
else {
delegate = new EncryptorFactory(keyProperties.getSalt()).create(keyProperties.getKey());
}
failsafeTextEncryptor.setDelegate(delegate);
}
}
}
}

protected PropertyHolder loadProperties(ConfigDataLocationResolverContext context, String uris) {
Binder binder = context.getBinder();
BindHandler bindHandler = getBindHandler(context);
Expand Down Expand Up @@ -182,6 +225,7 @@ public List<ConfigServerConfigDataResource> resolve(ConfigDataLocationResolverCo
public List<ConfigServerConfigDataResource> resolveProfileSpecific(
ConfigDataLocationResolverContext resolverContext, ConfigDataLocation location, Profiles profiles)
throws ConfigDataLocationNotFoundException {
setTextEncryptorDelegate(resolverContext);
String uris = location.getNonPrefixedValue(getPrefix());
PropertyHolder propertyHolder = loadProperties(resolverContext, uris);
ConfigClientProperties properties = propertyHolder.properties;
Expand Down Expand Up @@ -269,14 +313,6 @@ public List<ServiceInstance> getConfigServerInstances(String serviceId) {
return locations;
}

private class PropertyHolder {

ConfigClientProperties properties;

RetryProperties retryProperties;

}

public static class PropertyResolver {

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

}

private class PropertyHolder {

ConfigClientProperties properties;

RetryProperties retryProperties;

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,23 @@

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;

import org.springframework.beans.factory.support.InstanceSupplier;
import org.springframework.boot.BootstrapRegistry;
import org.springframework.boot.ConfigurableBootstrapContext;
import org.springframework.boot.context.config.ConfigDataLocation;
import org.springframework.boot.context.config.ConfigDataLocationResolverContext;
import org.springframework.boot.context.config.Profiles;
import org.springframework.boot.context.properties.bind.BindHandler;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.boot.logging.DeferredLog;
import org.springframework.cloud.bootstrap.TextEncryptorBindHandler;
import org.springframework.cloud.bootstrap.encrypt.KeyProperties;
import org.springframework.cloud.bootstrap.encrypt.TextEncryptorUtils;
import org.springframework.cloud.context.encrypt.EncryptorFactory;
import org.springframework.mock.env.MockEnvironment;
import org.springframework.security.crypto.encrypt.TextEncryptor;

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

@Test
void setFailsafeDelegateKeysNotConfigured() {
ConfigurableBootstrapContext bootstrapContext = mock(ConfigurableBootstrapContext.class);
when(bootstrapContext.isRegistered(eq(ConfigClientProperties.class))).thenReturn(true);
KeyProperties keyProperties = new KeyProperties();
ConfigClientProperties configClientProperties = new ConfigClientProperties();
configClientProperties.setUri(new String[] { "http://myuri" });
when(bootstrapContext.isRegistered(TextEncryptor.class)).thenReturn(true);
when(bootstrapContext.get(TextEncryptor.class)).thenReturn(new TextEncryptorUtils.FailsafeTextEncryptor());
when(bootstrapContext.get(eq(ConfigClientProperties.class))).thenReturn(configClientProperties);
when(context.getBootstrapContext()).thenReturn(bootstrapContext);
this.resolver.resolve(context, ConfigDataLocation.of("configserver:http://urlNo1"));
TextEncryptor textEncryptor = bootstrapContext.get(TextEncryptor.class);
assertThat(textEncryptor).isInstanceOf(TextEncryptorUtils.FailsafeTextEncryptor.class);
assertThat(((TextEncryptorUtils.FailsafeTextEncryptor) textEncryptor).getDelegate()).isNull();
}

@Test
void setFailsafeDelegateKeysConfigured() {
ConfigurableBootstrapContext bootstrapContext = mock(ConfigurableBootstrapContext.class);
when(bootstrapContext.isRegistered(eq(ConfigClientProperties.class))).thenReturn(true);
environment.setProperty("encrypt.key", "mykey");

// The value is "password" encrypted with the key "mykey"
environment.setProperty("spring.cloud.config.password",
"{cipher}6defc102cd76752fcf4c78231ed82ead85133a09741d9a1442595b4800e2b3d1");
ConfigClientProperties configClientProperties = new ConfigClientProperties();
configClientProperties.setUri(new String[] { "http://myuri" });
when(bootstrapContext.isRegistered(TextEncryptor.class)).thenReturn(true);
when(bootstrapContext.get(TextEncryptor.class)).thenReturn(new TextEncryptorUtils.FailsafeTextEncryptor());
when(bootstrapContext.get(eq(ConfigClientProperties.class))).thenReturn(configClientProperties);
when(context.getBootstrapContext()).thenReturn(bootstrapContext);
KeyProperties keyProperties = new KeyProperties();
keyProperties.setKey("mykey");

// Use this TextEncryptor in the BindHandler we return so it will decrypt the
// password when we bind ConfigClientProperties
TextEncryptor bindHandlerTextEncryptor = new EncryptorFactory(keyProperties.getSalt())
.create(keyProperties.getKey());
when(context.getBootstrapContext().getOrElse(eq(BindHandler.class), eq(null)))
.thenReturn(new TextEncryptorBindHandler(bindHandlerTextEncryptor, keyProperties));

// Call resolve so we can test that the delegate is added to the
// FailsafeTextEncryptor
this.resolver.resolve(context, ConfigDataLocation.of("configserver:http://urlNo1"));
TextEncryptor textEncryptor = bootstrapContext.get(TextEncryptor.class);

// Capture the ConfigClientProperties we create and register in
// ConfigServerConfigDataLocationResolver.resolveProfileSpecific
// it should have the decrypted passord in it
ArgumentCaptor<BootstrapRegistry.InstanceSupplier<ConfigClientProperties>> captor = ArgumentCaptor
.forClass(BootstrapRegistry.InstanceSupplier.class);
verify(bootstrapContext).register(eq(ConfigClientProperties.class), captor.capture());
assertThat(captor.getValue().get(bootstrapContext).getPassword()).isEqualTo("password");
assertThat(textEncryptor).isInstanceOf(TextEncryptorUtils.FailsafeTextEncryptor.class);
assertThat(((TextEncryptorUtils.FailsafeTextEncryptor) textEncryptor).getDelegate())
.isInstanceOf(TextEncryptor.class);
}

private ConfigServerConfigDataResource testUri(String propertyUri, String locationUri) {
this.environment.setProperty(ConfigClientProperties.PREFIX + ".uri", propertyUri);
when(context.getBootstrapContext()).thenReturn(mock(ConfigurableBootstrapContext.class));
Expand Down

0 comments on commit c372fcf

Please sign in to comment.