Set the FailesafeTextEncryptor delegate if we can create a valid KeyProperties (#2399)
Fixes #2330 Co-authored-by: Ryan Baxter <524254+ryanjbaxter@users.noreply.github.com>
This commit is contained in:
@@ -37,10 +37,16 @@ import org.springframework.boot.context.properties.bind.BindHandler;
|
|||||||
import org.springframework.boot.context.properties.bind.Bindable;
|
import org.springframework.boot.context.properties.bind.Bindable;
|
||||||
import org.springframework.boot.context.properties.bind.Binder;
|
import org.springframework.boot.context.properties.bind.Binder;
|
||||||
import org.springframework.boot.logging.DeferredLogFactory;
|
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.client.ServiceInstance;
|
||||||
|
import org.springframework.cloud.context.encrypt.EncryptorFactory;
|
||||||
import org.springframework.core.Ordered;
|
import org.springframework.core.Ordered;
|
||||||
import org.springframework.core.log.LogMessage;
|
import org.springframework.core.log.LogMessage;
|
||||||
import org.springframework.retry.support.RetryTemplate;
|
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.ObjectUtils;
|
||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
import org.springframework.web.client.RestTemplate;
|
import org.springframework.web.client.RestTemplate;
|
||||||
@@ -54,6 +60,8 @@ public class ConfigServerConfigDataLocationResolver
|
|||||||
* Prefix for Config Server imports.
|
* Prefix for Config Server imports.
|
||||||
*/
|
*/
|
||||||
public static final String PREFIX = "configserver:";
|
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;
|
private final Log log;
|
||||||
|
|
||||||
@@ -66,6 +74,41 @@ public class ConfigServerConfigDataLocationResolver
|
|||||||
return -1;
|
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) {
|
protected PropertyHolder loadProperties(ConfigDataLocationResolverContext context, String uris) {
|
||||||
Binder binder = context.getBinder();
|
Binder binder = context.getBinder();
|
||||||
BindHandler bindHandler = getBindHandler(context);
|
BindHandler bindHandler = getBindHandler(context);
|
||||||
@@ -182,6 +225,7 @@ public class ConfigServerConfigDataLocationResolver
|
|||||||
public List<ConfigServerConfigDataResource> resolveProfileSpecific(
|
public List<ConfigServerConfigDataResource> resolveProfileSpecific(
|
||||||
ConfigDataLocationResolverContext resolverContext, ConfigDataLocation location, Profiles profiles)
|
ConfigDataLocationResolverContext resolverContext, ConfigDataLocation location, Profiles profiles)
|
||||||
throws ConfigDataLocationNotFoundException {
|
throws ConfigDataLocationNotFoundException {
|
||||||
|
setTextEncryptorDelegate(resolverContext);
|
||||||
String uris = location.getNonPrefixedValue(getPrefix());
|
String uris = location.getNonPrefixedValue(getPrefix());
|
||||||
PropertyHolder propertyHolder = loadProperties(resolverContext, uris);
|
PropertyHolder propertyHolder = loadProperties(resolverContext, uris);
|
||||||
ConfigClientProperties properties = propertyHolder.properties;
|
ConfigClientProperties properties = propertyHolder.properties;
|
||||||
@@ -269,14 +313,6 @@ public class ConfigServerConfigDataLocationResolver
|
|||||||
return locations;
|
return locations;
|
||||||
}
|
}
|
||||||
|
|
||||||
private class PropertyHolder {
|
|
||||||
|
|
||||||
ConfigClientProperties properties;
|
|
||||||
|
|
||||||
RetryProperties retryProperties;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class PropertyResolver {
|
public static class PropertyResolver {
|
||||||
|
|
||||||
private final Binder binder;
|
private final Binder binder;
|
||||||
@@ -302,4 +338,12 @@ public class ConfigServerConfigDataLocationResolver
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class PropertyHolder {
|
||||||
|
|
||||||
|
ConfigClientProperties properties;
|
||||||
|
|
||||||
|
RetryProperties retryProperties;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,14 +21,23 @@ import java.util.List;
|
|||||||
|
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
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.ConfigurableBootstrapContext;
|
||||||
import org.springframework.boot.context.config.ConfigDataLocation;
|
import org.springframework.boot.context.config.ConfigDataLocation;
|
||||||
import org.springframework.boot.context.config.ConfigDataLocationResolverContext;
|
import org.springframework.boot.context.config.ConfigDataLocationResolverContext;
|
||||||
import org.springframework.boot.context.config.Profiles;
|
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.context.properties.bind.Binder;
|
||||||
import org.springframework.boot.logging.DeferredLog;
|
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.mock.env.MockEnvironment;
|
||||||
|
import org.springframework.security.crypto.encrypt.TextEncryptor;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import static org.mockito.Mockito.eq;
|
import static org.mockito.Mockito.eq;
|
||||||
@@ -205,6 +214,65 @@ public class ConfigServerConfigDataLocationResolverTests {
|
|||||||
assertThat(resource2.getProperties().getUri()).isEqualTo(new String[] { "http://urlNo2" });
|
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) {
|
private ConfigServerConfigDataResource testUri(String propertyUri, String locationUri) {
|
||||||
this.environment.setProperty(ConfigClientProperties.PREFIX + ".uri", propertyUri);
|
this.environment.setProperty(ConfigClientProperties.PREFIX + ".uri", propertyUri);
|
||||||
when(context.getBootstrapContext()).thenReturn(mock(ConfigurableBootstrapContext.class));
|
when(context.getBootstrapContext()).thenReturn(mock(ConfigurableBootstrapContext.class));
|
||||||
|
|||||||
Reference in New Issue
Block a user