From a6124e7063d924fe192b435e98e1a4c9874410ec Mon Sep 17 00:00:00 2001 From: Dave Syer Date: Fri, 2 Oct 2015 14:54:50 +0100 Subject: [PATCH] Add another property source for decrypted bootstrap properties The problem is that we do want bootstrap properties to override default properties in most cases, but if they are decrypted those values have to be added with the highest possible precedence. Fixes gh-54 --- .../PropertySourceBootstrapConfiguration.java | 10 ++- ...ironmentDecryptApplicationInitializer.java | 90 ++++++++++++++----- ...ringAutoConfigurationIntegrationTests.java | 8 +- ...gCustomPropertySourceIntegrationTests.java | 65 ++++++++++++++ .../src/test/resources/custom.properties | 1 + 5 files changed, 147 insertions(+), 27 deletions(-) create mode 100644 spring-cloud-context/src/test/java/org/springframework/cloud/bootstrap/BootstrapOrderingCustomPropertySourceIntegrationTests.java create mode 100644 spring-cloud-context/src/test/resources/custom.properties diff --git a/spring-cloud-context/src/main/java/org/springframework/cloud/bootstrap/config/PropertySourceBootstrapConfiguration.java b/spring-cloud-context/src/main/java/org/springframework/cloud/bootstrap/config/PropertySourceBootstrapConfiguration.java index 5423386b..92585442 100644 --- a/spring-cloud-context/src/main/java/org/springframework/cloud/bootstrap/config/PropertySourceBootstrapConfiguration.java +++ b/spring-cloud-context/src/main/java/org/springframework/cloud/bootstrap/config/PropertySourceBootstrapConfiguration.java @@ -38,6 +38,7 @@ import org.springframework.cloud.logging.LoggingRebinder; import org.springframework.context.ApplicationContextInitializer; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Configuration; +import org.springframework.core.Ordered; import org.springframework.core.annotation.AnnotationAwareOrderComparator; import org.springframework.core.env.CompositePropertySource; import org.springframework.core.env.ConfigurableEnvironment; @@ -53,16 +54,23 @@ import org.springframework.util.ResourceUtils; @Configuration @EnableConfigurationProperties(PropertySourceBootstrapProperties.class) public class PropertySourceBootstrapConfiguration - implements ApplicationContextInitializer { + implements ApplicationContextInitializer, Ordered { private static final String BOOTSTRAP_PROPERTY_SOURCE_NAME = BootstrapApplicationListener.BOOTSTRAP_PROPERTY_SOURCE_NAME; private static Log logger = LogFactory .getLog(PropertySourceBootstrapConfiguration.class); + private int order = Ordered.HIGHEST_PRECEDENCE + 10; + @Autowired(required = false) private List propertySourceLocators = new ArrayList<>(); + @Override + public int getOrder() { + return this.order; + } + public void setPropertySourceLocators( Collection propertySourceLocators) { this.propertySourceLocators = new ArrayList<>(propertySourceLocators); diff --git a/spring-cloud-context/src/main/java/org/springframework/cloud/bootstrap/encrypt/EnvironmentDecryptApplicationInitializer.java b/spring-cloud-context/src/main/java/org/springframework/cloud/bootstrap/encrypt/EnvironmentDecryptApplicationInitializer.java index c829247f..fafa9f1d 100644 --- a/spring-cloud-context/src/main/java/org/springframework/cloud/bootstrap/encrypt/EnvironmentDecryptApplicationInitializer.java +++ b/spring-cloud-context/src/main/java/org/springframework/cloud/bootstrap/encrypt/EnvironmentDecryptApplicationInitializer.java @@ -16,7 +16,9 @@ package org.springframework.cloud.bootstrap.encrypt; import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.Map; +import java.util.Set; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -38,7 +40,7 @@ import org.springframework.security.crypto.encrypt.TextEncryptor; /** * Decrypt properties from the environment and insert them with high priority so they * override the encrypted values. - * + * * @author Dave Syer * */ @@ -47,6 +49,8 @@ public class EnvironmentDecryptApplicationInitializer implements public static final String DECRYPTED_PROPERTY_SOURCE_NAME = "decrypted"; + public static final String DECRYPTED_BOOTSTRAP_PROPERTY_SOURCE_NAME = "decryptedBootstrap"; + private int order = Ordered.HIGHEST_PRECEDENCE + 15; private static Log logger = LogFactory @@ -58,7 +62,7 @@ public class EnvironmentDecryptApplicationInitializer implements /** * Strategy to determine how to handle exceptions during decryption. - * + * * @param failOnError the flag value (default true) */ public void setFailOnError(boolean failOnError) { @@ -71,28 +75,54 @@ public class EnvironmentDecryptApplicationInitializer implements @Override public int getOrder() { - return order; + return this.order; } @Override public void initialize(ConfigurableApplicationContext applicationContext) { + ConfigurableEnvironment environment = applicationContext.getEnvironment(); - MapPropertySource decrypted = new MapPropertySource( - DECRYPTED_PROPERTY_SOURCE_NAME, decrypt(environment.getPropertySources())); - if (!decrypted.getSource().isEmpty()) { + MutablePropertySources propertySources = environment.getPropertySources(); + + Set found = new LinkedHashSet<>(); + Map map = decrypt(propertySources); + if (!map.isEmpty()) { // We have some decrypted properties - insert(environment.getPropertySources(), decrypted); + found.addAll(map.keySet()); + insert(applicationContext, + new MapPropertySource(DECRYPTED_PROPERTY_SOURCE_NAME, map)); + } + PropertySource bootstrap = propertySources + .get(BootstrapApplicationListener.BOOTSTRAP_PROPERTY_SOURCE_NAME); + if (bootstrap != null) { + map = decrypt(bootstrap); + if (!map.isEmpty()) { + found.addAll(map.keySet()); + insert(applicationContext, new MapPropertySource( + DECRYPTED_BOOTSTRAP_PROPERTY_SOURCE_NAME, map)); + } + } + if (!found.isEmpty()) { ApplicationContext parent = applicationContext.getParent(); - if (parent != null - && (parent.getEnvironment() instanceof ConfigurableEnvironment)) { - ConfigurableEnvironment mutable = (ConfigurableEnvironment) parent - .getEnvironment(); + if (parent != null) { // The parent is actually the bootstrap context, and it is fully // initialized, so we can fire an EnvironmentChangeEvent there to rebind // @ConfigurationProperties, in case they were encrypted. - insert(mutable.getPropertySources(), decrypted); - parent.publishEvent(new EnvironmentChangeEvent(decrypted.getSource() - .keySet())); + parent.publishEvent(new EnvironmentChangeEvent(found)); + } + + } + } + + private void insert(ApplicationContext applicationContext, + MapPropertySource propertySource) { + ApplicationContext parent = applicationContext; + while (parent != null) { + if (parent.getEnvironment() instanceof ConfigurableEnvironment) { + ConfigurableEnvironment mutable = (ConfigurableEnvironment) parent + .getEnvironment(); + insert(mutable.getPropertySources(), propertySource); + parent = parent.getParent(); } } } @@ -101,9 +131,17 @@ public class EnvironmentDecryptApplicationInitializer implements MapPropertySource propertySource) { if (propertySources .contains(BootstrapApplicationListener.BOOTSTRAP_PROPERTY_SOURCE_NAME)) { - propertySources.addAfter( - BootstrapApplicationListener.BOOTSTRAP_PROPERTY_SOURCE_NAME, - propertySource); + if (DECRYPTED_BOOTSTRAP_PROPERTY_SOURCE_NAME + .equals(propertySource.getName())) { + propertySources.addBefore( + BootstrapApplicationListener.BOOTSTRAP_PROPERTY_SOURCE_NAME, + propertySource); + } + else { + propertySources.addAfter( + BootstrapApplicationListener.BOOTSTRAP_PROPERTY_SOURCE_NAME, + propertySource); + } } else { propertySources.addFirst(propertySource); @@ -118,6 +156,12 @@ public class EnvironmentDecryptApplicationInitializer implements return overrides; } + private Map decrypt(PropertySource source) { + Map overrides = new LinkedHashMap(); + decrypt(source, overrides); + return overrides; + } + private void decrypt(PropertySource source, Map overrides) { if (source instanceof EnumerablePropertySource) { @@ -130,18 +174,20 @@ public class EnvironmentDecryptApplicationInitializer implements if (value.startsWith("{cipher}")) { value = value.substring("{cipher}".length()); try { - value = encryptor.decrypt(value); + value = this.encryptor.decrypt(value); if (logger.isDebugEnabled()) { logger.debug("Decrypted: key=" + key); } - } catch (Exception e) { + } + catch (Exception e) { String message = "Cannot decrypt: key=" + key; - if (failOnError) { + if (this.failOnError) { throw new IllegalStateException(message, e); } if (logger.isDebugEnabled()) { logger.warn(message, e); - } else { + } + else { logger.warn(message); } // Set value to empty to avoid making a password out of the @@ -164,5 +210,5 @@ public class EnvironmentDecryptApplicationInitializer implements } } - + } diff --git a/spring-cloud-context/src/test/java/org/springframework/cloud/bootstrap/BootstrapOrderingAutoConfigurationIntegrationTests.java b/spring-cloud-context/src/test/java/org/springframework/cloud/bootstrap/BootstrapOrderingAutoConfigurationIntegrationTests.java index 74a4a66c..81240eec 100644 --- a/spring-cloud-context/src/test/java/org/springframework/cloud/bootstrap/BootstrapOrderingAutoConfigurationIntegrationTests.java +++ b/spring-cloud-context/src/test/java/org/springframework/cloud/bootstrap/BootstrapOrderingAutoConfigurationIntegrationTests.java @@ -20,23 +20,23 @@ import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; @IntegrationTest("encrypt.key:deadbeef") @ActiveProfiles("encrypt") public class BootstrapOrderingAutoConfigurationIntegrationTests { - + @Autowired private ConfigurableEnvironment environment; @Test public void bootstrapPropertiesExist() { - assertTrue(environment.getPropertySources().contains("bootstrap")); + assertTrue(this.environment.getPropertySources().contains("bootstrap")); } @Test public void normalPropertiesDecrypted() { - assertEquals("foo", environment.resolvePlaceholders("${foo}")); + assertEquals("foo", this.environment.resolvePlaceholders("${foo}")); } @Test public void bootstrapPropertiesDecrypted() { - assertEquals("bar", environment.resolvePlaceholders("${bar}")); + assertEquals("bar", this.environment.resolvePlaceholders("${bar}")); } @EnableAutoConfiguration diff --git a/spring-cloud-context/src/test/java/org/springframework/cloud/bootstrap/BootstrapOrderingCustomPropertySourceIntegrationTests.java b/spring-cloud-context/src/test/java/org/springframework/cloud/bootstrap/BootstrapOrderingCustomPropertySourceIntegrationTests.java new file mode 100644 index 00000000..9bc8e28e --- /dev/null +++ b/spring-cloud-context/src/test/java/org/springframework/cloud/bootstrap/BootstrapOrderingCustomPropertySourceIntegrationTests.java @@ -0,0 +1,65 @@ +package org.springframework.cloud.bootstrap; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.test.IntegrationTest; +import org.springframework.boot.test.SpringApplicationConfiguration; +import org.springframework.cloud.bootstrap.BootstrapOrderingCustomPropertySourceIntegrationTests.Application; +import org.springframework.cloud.bootstrap.config.PropertySourceLocator; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.Environment; +import org.springframework.core.env.MapPropertySource; +import org.springframework.core.env.PropertySource; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +@RunWith(SpringJUnit4ClassRunner.class) +@SpringApplicationConfiguration(classes = Application.class) +@IntegrationTest({"encrypt.key:deadbeef", "spring.cloud.bootstrap.name:custom"}) +@ActiveProfiles("encrypt") +public class BootstrapOrderingCustomPropertySourceIntegrationTests { + + @Autowired + private ConfigurableEnvironment environment; + + @Test + public void bootstrapPropertiesExist() { + assertTrue(this.environment.getPropertySources().contains("bootstrap")); + } + + @Test + public void customPropertiesDecrypted() { + assertEquals("bar", this.environment.resolvePlaceholders("${custom.foo}")); + } + + @EnableAutoConfiguration + @Configuration + protected static class Application { + + } + + @Configuration + // This is added to bootstrap context as a source in bootstrap.properties + protected static class PropertySourceConfiguration implements PropertySourceLocator { + + public static Map MAP = new HashMap( + Collections. singletonMap("custom.foo", "{cipher}6154ca04d4bb6144d672c4e3d750b5147116dd381946d51fa44f8bc25dc256f4")); + + @Override + public PropertySource locate(Environment environment) { + return new MapPropertySource("testBootstrap", MAP); + } + + } + +} diff --git a/spring-cloud-context/src/test/resources/custom.properties b/spring-cloud-context/src/test/resources/custom.properties new file mode 100644 index 00000000..1ee3320d --- /dev/null +++ b/spring-cloud-context/src/test/resources/custom.properties @@ -0,0 +1 @@ +spring.main.sources: org.springframework.cloud.bootstrap.BootstrapOrderingCustomPropertySourceIntegrationTests.PropertySourceConfiguration