diff --git a/docs/pom.xml b/docs/pom.xml index de6a1861..c4ce40d6 100644 --- a/docs/pom.xml +++ b/docs/pom.xml @@ -8,7 +8,7 @@ org.springframework.cloud spring-cloud-commons-parent - 2.1.2.BUILD-SNAPSHOT + 2.1.3.BUILD-SNAPSHOT pom Spring Cloud Commons Docs @@ -16,7 +16,7 @@ spring-cloud-commons ${basedir}/.. - 1.2.x,1.3.x,2.0.x,2.1.x + 1.3.x,2.0.x,2.1.x diff --git a/docs/src/main/asciidoc/index.adoc b/docs/src/main/asciidoc/index.adoc new file mode 120000 index 00000000..583ddeb0 --- /dev/null +++ b/docs/src/main/asciidoc/index.adoc @@ -0,0 +1 @@ +spring-cloud-commons.adoc \ No newline at end of file diff --git a/pom.xml b/pom.xml index a2664378..2fe15e00 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ 4.0.0 org.springframework.cloud spring-cloud-commons-parent - 2.1.2.BUILD-SNAPSHOT + 2.1.3.BUILD-SNAPSHOT pom Spring Cloud Commons Parent Spring Cloud Commons Parent @@ -13,7 +13,7 @@ org.springframework.cloud spring-cloud-build - 2.1.4.BUILD-SNAPSHOT + 2.1.7.BUILD-SNAPSHOT diff --git a/spring-cloud-commons-dependencies/pom.xml b/spring-cloud-commons-dependencies/pom.xml index af349d61..e1abc087 100644 --- a/spring-cloud-commons-dependencies/pom.xml +++ b/spring-cloud-commons-dependencies/pom.xml @@ -6,11 +6,11 @@ spring-cloud-dependencies-parent org.springframework.cloud - 2.1.4.BUILD-SNAPSHOT + 2.1.7.BUILD-SNAPSHOT spring-cloud-commons-dependencies - 2.1.2.BUILD-SNAPSHOT + 2.1.3.BUILD-SNAPSHOT pom spring-cloud-commons-dependencies Spring Cloud Commons Dependencies diff --git a/spring-cloud-commons/pom.xml b/spring-cloud-commons/pom.xml index 408520c8..368deda5 100644 --- a/spring-cloud-commons/pom.xml +++ b/spring-cloud-commons/pom.xml @@ -7,7 +7,7 @@ org.springframework.cloud spring-cloud-commons-parent - 2.1.2.BUILD-SNAPSHOT + 2.1.3.BUILD-SNAPSHOT .. spring-cloud-commons diff --git a/spring-cloud-context-integration-tests/pom.xml b/spring-cloud-context-integration-tests/pom.xml index de29c8ed..c7b62813 100644 --- a/spring-cloud-context-integration-tests/pom.xml +++ b/spring-cloud-context-integration-tests/pom.xml @@ -7,7 +7,7 @@ org.springframework.cloud spring-cloud-commons-parent - 2.1.2.BUILD-SNAPSHOT + 2.1.3.BUILD-SNAPSHOT .. spring-cloud-context-integration-tests diff --git a/spring-cloud-context/pom.xml b/spring-cloud-context/pom.xml index 063f5b88..4ed22eb7 100644 --- a/spring-cloud-context/pom.xml +++ b/spring-cloud-context/pom.xml @@ -7,7 +7,7 @@ org.springframework.cloud spring-cloud-commons-parent - 2.1.2.BUILD-SNAPSHOT + 2.1.3.BUILD-SNAPSHOT .. spring-cloud-context 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 4842b504..3ced0466 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 @@ -1,5 +1,5 @@ /* - * Copyright 2013-2017 the original author or authors. + * Copyright 2013-2019 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,7 @@ package org.springframework.cloud.bootstrap.encrypt; import java.util.ArrayList; +import java.util.Collections; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; @@ -47,7 +48,7 @@ import org.springframework.security.crypto.encrypt.TextEncryptor; * override the encrypted values. * * @author Dave Syer - * + * @author Tim Ysewyn */ public class EnvironmentDecryptApplicationInitializer implements ApplicationContextInitializer, Ordered { @@ -62,6 +63,11 @@ public class EnvironmentDecryptApplicationInitializer implements */ public static final String DECRYPTED_BOOTSTRAP_PROPERTY_SOURCE_NAME = "decryptedBootstrap"; + /** + * Prefix indicating an encrypted value. + */ + public static final String ENCRYPTED_PROPERTY_PREFIX = "{cipher}"; + private static final Pattern COLLECTION_PROPERTY = Pattern .compile("(\\S+)?\\[(\\d+)\\](\\.\\S+)?"); @@ -97,11 +103,24 @@ public class EnvironmentDecryptApplicationInitializer implements @Override public void initialize(ConfigurableApplicationContext applicationContext) { - ConfigurableEnvironment environment = applicationContext.getEnvironment(); MutablePropertySources propertySources = environment.getPropertySources(); Set found = new LinkedHashSet<>(); + if (!propertySources.contains(DECRYPTED_BOOTSTRAP_PROPERTY_SOURCE_NAME)) { + // No reason to decrypt bootstrap twice + PropertySource bootstrap = propertySources + .get(BootstrapApplicationListener.BOOTSTRAP_PROPERTY_SOURCE_NAME); + if (bootstrap != null) { + Map map = decrypt(bootstrap); + if (!map.isEmpty()) { + found.addAll(map.keySet()); + insert(applicationContext, new SystemEnvironmentPropertySource( + DECRYPTED_BOOTSTRAP_PROPERTY_SOURCE_NAME, map)); + } + } + } + removeDecryptedProperties(applicationContext); Map map = decrypt(propertySources); if (!map.isEmpty()) { // We have some decrypted properties @@ -109,16 +128,6 @@ public class EnvironmentDecryptApplicationInitializer implements insert(applicationContext, new SystemEnvironmentPropertySource( 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 SystemEnvironmentPropertySource( - DECRYPTED_BOOTSTRAP_PROPERTY_SOURCE_NAME, map)); - } - } if (!found.isEmpty()) { ApplicationContext parent = applicationContext.getParent(); if (parent != null) { @@ -165,31 +174,56 @@ public class EnvironmentDecryptApplicationInitializer implements } } + private void removeDecryptedProperties(ApplicationContext applicationContext) { + ApplicationContext parent = applicationContext; + while (parent != null) { + if (parent.getEnvironment() instanceof ConfigurableEnvironment) { + ((ConfigurableEnvironment) parent.getEnvironment()).getPropertySources() + .remove(DECRYPTED_PROPERTY_SOURCE_NAME); + } + parent = parent.getParent(); + } + } + public Map decrypt(PropertySources propertySources) { - Map overrides = new LinkedHashMap<>(); + Map properties = merge(propertySources); + decrypt(properties); + return properties; + } + + private Map decrypt(PropertySource source) { + Map properties = merge(source); + decrypt(properties); + return properties; + } + + private Map merge(PropertySources propertySources) { + Map properties = new LinkedHashMap<>(); List> sources = new ArrayList<>(); for (PropertySource source : propertySources) { sources.add(0, source); } for (PropertySource source : sources) { - decrypt(source, overrides); + merge(source, properties); } - return overrides; + return properties; } - private Map decrypt(PropertySource source) { - Map overrides = new LinkedHashMap<>(); - decrypt(source, overrides); - return overrides; + private Map merge(PropertySource source) { + Map properties = new LinkedHashMap<>(); + merge(source, properties); + return properties; } - private void decrypt(PropertySource source, Map overrides) { - + private void merge(PropertySource source, Map properties) { if (source instanceof CompositePropertySource) { - for (PropertySource nested : ((CompositePropertySource) source) - .getPropertySources()) { - decrypt(nested, overrides); + List> sources = new ArrayList<>( + ((CompositePropertySource) source).getPropertySources()); + Collections.reverse(sources); + + for (PropertySource nested : sources) { + merge(nested, properties); } } @@ -202,47 +236,63 @@ public class EnvironmentDecryptApplicationInitializer implements Object property = source.getProperty(key); if (property != null) { String value = property.toString(); - if (value.startsWith("{cipher}")) { - value = value.substring("{cipher}".length()); - try { - value = this.encryptor.decrypt(value); - if (logger.isDebugEnabled()) { - logger.debug("Decrypted: key=" + key); - } - } - catch (Exception e) { - String message = "Cannot decrypt: key=" + key; - if (logger.isDebugEnabled()) { - logger.warn(message, e); - } - else { - logger.warn(message); - } - if (this.failOnError) { - throw new IllegalStateException(message, e); - } - // Set value to empty to avoid making a password out of the - // cipher text - value = ""; - } - overrides.put(key, value); + if (value.startsWith(ENCRYPTED_PROPERTY_PREFIX)) { + properties.put(key, value); if (COLLECTION_PROPERTY.matcher(key).matches()) { sourceHasDecryptedCollection = true; } } else if (COLLECTION_PROPERTY.matcher(key).matches()) { - // put non-ecrypted properties so merging of index properties + // put non-encrypted properties so merging of index properties // happens correctly otherCollectionProperties.put(key, value); } + else { + // override previously encrypted with non-encrypted property + properties.remove(key); + } } } // copy all indexed properties even if not encrypted if (sourceHasDecryptedCollection && !otherCollectionProperties.isEmpty()) { - overrides.putAll(otherCollectionProperties); + properties.putAll(otherCollectionProperties); } } } + private void decrypt(Map properties) { + properties.replaceAll((key, value) -> { + String valueString = value.toString(); + if (!valueString.startsWith(ENCRYPTED_PROPERTY_PREFIX)) { + return value; + } + return decrypt(key, valueString); + }); + } + + private String decrypt(String key, String original) { + String value = original.substring(ENCRYPTED_PROPERTY_PREFIX.length()); + try { + value = this.encryptor.decrypt(value); + if (logger.isDebugEnabled()) { + logger.debug("Decrypted: key=" + key); + } + return value; + } + catch (Exception e) { + String message = "Cannot decrypt: key=" + key; + if (logger.isDebugEnabled()) { + logger.warn(message, e); + } + else { + logger.warn(message); + } + if (this.failOnError) { + throw new IllegalStateException(message, e); + } + return ""; + } + } + } diff --git a/spring-cloud-context/src/main/java/org/springframework/cloud/logging/LoggingRebinder.java b/spring-cloud-context/src/main/java/org/springframework/cloud/logging/LoggingRebinder.java index a1f8a77d..132f1afa 100644 --- a/spring-cloud-context/src/main/java/org/springframework/cloud/logging/LoggingRebinder.java +++ b/spring-cloud-context/src/main/java/org/springframework/cloud/logging/LoggingRebinder.java @@ -17,6 +17,7 @@ package org.springframework.cloud.logging; import java.util.Collections; +import java.util.Locale; import java.util.Map; import java.util.Map.Entry; @@ -37,6 +38,7 @@ import org.springframework.core.env.Environment; * changed. * * @author Dave Syer + * @author Olga Maciaszek-Sharma * */ public class LoggingRebinder @@ -79,11 +81,19 @@ public class LoggingRebinder name = null; } level = environment.resolvePlaceholders(level); - system.setLogLevel(name, LogLevel.valueOf(level.toUpperCase())); + system.setLogLevel(name, resolveLogLevel(level)); } catch (RuntimeException ex) { this.logger.error("Cannot set level: " + level + " for '" + name + "'"); } } + private LogLevel resolveLogLevel(String level) { + String trimmedLevel = level.trim(); + if ("false".equalsIgnoreCase(trimmedLevel)) { + return LogLevel.OFF; + } + return LogLevel.valueOf(trimmedLevel.toUpperCase(Locale.ENGLISH)); + } + } diff --git a/spring-cloud-context/src/test/java/org/springframework/cloud/bootstrap/encrypt/EnvironmentDecryptApplicationInitializerTests.java b/spring-cloud-context/src/test/java/org/springframework/cloud/bootstrap/encrypt/EnvironmentDecryptApplicationInitializerTests.java index 3fc28c5f..5638e769 100644 --- a/spring-cloud-context/src/test/java/org/springframework/cloud/bootstrap/encrypt/EnvironmentDecryptApplicationInitializerTests.java +++ b/spring-cloud-context/src/test/java/org/springframework/cloud/bootstrap/encrypt/EnvironmentDecryptApplicationInitializerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2013-2019 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,9 @@ package org.springframework.cloud.bootstrap.encrypt; +import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; import java.util.Map; import org.junit.Rule; @@ -37,14 +39,19 @@ import org.springframework.security.crypto.encrypt.Encryptors; import org.springframework.security.crypto.encrypt.TextEncryptor; import static org.assertj.core.api.BDDAssertions.then; -import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; +import static org.springframework.cloud.bootstrap.BootstrapApplicationListener.BOOTSTRAP_PROPERTY_SOURCE_NAME; +import static org.springframework.cloud.bootstrap.encrypt.EnvironmentDecryptApplicationInitializer.DECRYPTED_BOOTSTRAP_PROPERTY_SOURCE_NAME; import static org.springframework.cloud.bootstrap.encrypt.EnvironmentDecryptApplicationInitializer.DECRYPTED_PROPERTY_SOURCE_NAME; /** * @author Dave Syer * @author Biju Kunjummen + * @author Tim Ysewyn */ public class EnvironmentDecryptApplicationInitializerTests { @@ -75,9 +82,8 @@ public class EnvironmentDecryptApplicationInitializerTests { public void propertySourcesOrderedCorrectly() { ConfigurableApplicationContext context = new AnnotationConfigApplicationContext(); TestPropertyValues.of("foo: {cipher}bar").applyTo(context); - context.getEnvironment().getPropertySources() - .addFirst(new MapPropertySource("test_override", - Collections.singletonMap("foo", "{cipher}spam"))); + context.getEnvironment().getPropertySources().addFirst(new MapPropertySource( + "test_override", Collections.singletonMap("foo", "{cipher}spam"))); this.listener.initialize(context); then(context.getEnvironment().getProperty("foo")).isEqualTo("spam"); } @@ -170,23 +176,100 @@ public class EnvironmentDecryptApplicationInitializerTests { @Test public void testDecryptCompositePropertySource() { - String expected = "always"; - TextEncryptor textEncryptor = mock(TextEncryptor.class); - when(textEncryptor.decrypt(anyString())).thenReturn(expected); - ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(); EnvironmentDecryptApplicationInitializer initializer = new EnvironmentDecryptApplicationInitializer( - textEncryptor); + Encryptors.noOpText()); + + MapPropertySource devProfile = new MapPropertySource("dev-profile", + Collections.singletonMap("key", "{cipher}value1")); + + MapPropertySource defaultProfile = new MapPropertySource("default-profile", + Collections.singletonMap("key", "{cipher}value2")); - MapPropertySource source = new MapPropertySource("nobody", - Collections.singletonMap("key", "{cipher}value")); CompositePropertySource cps = mock(CompositePropertySource.class); - when(cps.getPropertyNames()).thenReturn(source.getPropertyNames()); - when(cps.getPropertySources()).thenReturn(Collections.singleton(source)); + when(cps.getPropertyNames()).thenReturn(devProfile.getPropertyNames()); + when(cps.getPropertySources()) + .thenReturn(Arrays.asList(devProfile, defaultProfile)); ctx.getEnvironment().getPropertySources().addLast(cps); initializer.initialize(ctx); - then(ctx.getEnvironment().getProperty("key")).isEqualTo(expected); + then(ctx.getEnvironment().getProperty("key")).isEqualTo("value1"); + } + + @Test + public void propertySourcesOrderedCorrectlyWithUnencryptedOverrides() { + ConfigurableApplicationContext context = new AnnotationConfigApplicationContext(); + TestPropertyValues.of("foo: {cipher}bar").applyTo(context); + context.getEnvironment().getPropertySources().addFirst(new MapPropertySource( + "test_override", Collections.singletonMap("foo", "spam"))); + this.listener.initialize(context); + then(context.getEnvironment().getProperty("foo")).isEqualTo("spam"); + } + + @Test + public void doNotDecryptBootstrapTwice() { + TextEncryptor encryptor = mock(TextEncryptor.class); + when(encryptor.decrypt("bar")).thenReturn("bar"); + when(encryptor.decrypt("bar2")).thenReturn("bar2"); + when(encryptor.decrypt("bar3")).thenReturn("bar3"); + when(encryptor.decrypt("baz")).thenReturn("baz"); + + EnvironmentDecryptApplicationInitializer initializer = new EnvironmentDecryptApplicationInitializer( + encryptor); + + ConfigurableApplicationContext context = new AnnotationConfigApplicationContext(); + CompositePropertySource bootstrap = new CompositePropertySource( + BOOTSTRAP_PROPERTY_SOURCE_NAME); + bootstrap.addPropertySource(new MapPropertySource("configService", + Collections.singletonMap("foo", "{cipher}bar"))); + context.getEnvironment().getPropertySources().addFirst(bootstrap); + + Map props = new HashMap<>(); + props.put("foo2", "{cipher}bar2"); + props.put("bar", "{cipher}baz"); + context.getEnvironment().getPropertySources().addAfter( + BOOTSTRAP_PROPERTY_SOURCE_NAME, new MapPropertySource("remote", props)); + + initializer.initialize(context); + + // Simulate retrieval of new properties via Spring Cloud Config + props.put("foo2", "{cipher}bar3"); + context.getEnvironment().getPropertySources().replace("remote", + new MapPropertySource("remote", props)); + + initializer.initialize(context); + + verify(encryptor).decrypt("bar"); + verify(encryptor).decrypt("bar2"); + verify(encryptor).decrypt("bar3"); + verify(encryptor, times(2)).decrypt("baz"); + + // Check if all encrypted properties are still decrypted + PropertySource decryptedBootstrap = context.getEnvironment() + .getPropertySources().get(DECRYPTED_BOOTSTRAP_PROPERTY_SOURCE_NAME); + then(decryptedBootstrap.getProperty("foo")).isEqualTo("bar"); + + PropertySource decrypted = context.getEnvironment().getPropertySources() + .get(DECRYPTED_PROPERTY_SOURCE_NAME); + then(decrypted.getProperty("foo2")).isEqualTo("bar3"); + then(decrypted.getProperty("bar")).isEqualTo("baz"); + } + + @Test + public void testOnlyDecryptIfNotOverridden() { + ConfigurableApplicationContext context = new AnnotationConfigApplicationContext(); + TextEncryptor encryptor = mock(TextEncryptor.class); + when(encryptor.decrypt("bar2")).thenReturn("bar2"); + EnvironmentDecryptApplicationInitializer initializer = new EnvironmentDecryptApplicationInitializer( + encryptor); + TestPropertyValues.of("foo: {cipher}bar", "foo2: {cipher}bar2").applyTo(context); + context.getEnvironment().getPropertySources().addFirst(new MapPropertySource( + "test_override", Collections.singletonMap("foo", "spam"))); + initializer.initialize(context); + then(context.getEnvironment().getProperty("foo")).isEqualTo("spam"); + then(context.getEnvironment().getProperty("foo2")).isEqualTo("bar2"); + verify(encryptor).decrypt("bar2"); + verifyNoMoreInteractions(encryptor); } } diff --git a/spring-cloud-context/src/test/java/org/springframework/cloud/logging/LoggingRebinderTests.java b/spring-cloud-context/src/test/java/org/springframework/cloud/logging/LoggingRebinderTests.java index 9a626783..4b97c0bc 100644 --- a/spring-cloud-context/src/test/java/org/springframework/cloud/logging/LoggingRebinderTests.java +++ b/spring-cloud-context/src/test/java/org/springframework/cloud/logging/LoggingRebinderTests.java @@ -18,6 +18,7 @@ package org.springframework.cloud.logging; import java.util.Collections; +import ch.qos.logback.classic.Level; import org.junit.After; import org.junit.Test; import org.slf4j.Logger; @@ -33,6 +34,7 @@ import static org.assertj.core.api.BDDAssertions.then; /** * @author Dave Syer + * @author Olga Maciaszek-Sharma * */ public class LoggingRebinderTests { @@ -71,4 +73,17 @@ public class LoggingRebinderTests { then(this.logger.isTraceEnabled()).isTrue(); } + @Test + public void logLevelFalseResolvedToOff() { + ch.qos.logback.classic.Logger logger = (ch.qos.logback.classic.Logger) LoggerFactory + .getLogger("org.springframework.cloud"); + StandardEnvironment environment = new StandardEnvironment(); + TestPropertyValues.of("logging.level.org.springframework.cloud=false") + .applyTo(environment); + rebinder.setEnvironment(environment); + rebinder.onApplicationEvent(new EnvironmentChangeEvent(environment, + Collections.singleton("logging.level.org.springframework.cloud"))); + then(Level.OFF).isEqualTo((logger.getLevel())); + } + } diff --git a/spring-cloud-loadbalancer/pom.xml b/spring-cloud-loadbalancer/pom.xml index 852a296d..cebcfe7a 100644 --- a/spring-cloud-loadbalancer/pom.xml +++ b/spring-cloud-loadbalancer/pom.xml @@ -7,7 +7,7 @@ org.springframework.cloud spring-cloud-commons-parent - 2.1.2.BUILD-SNAPSHOT + 2.1.3.BUILD-SNAPSHOT .. spring-cloud-loadbalancer diff --git a/spring-cloud-starter/pom.xml b/spring-cloud-starter/pom.xml index 64c17ed9..83a5cf87 100644 --- a/spring-cloud-starter/pom.xml +++ b/spring-cloud-starter/pom.xml @@ -6,7 +6,7 @@ org.springframework.cloud spring-cloud-commons-parent - 2.1.2.BUILD-SNAPSHOT + 2.1.3.BUILD-SNAPSHOT spring-cloud-starter spring-cloud-starter diff --git a/spring-cloud-test-support/pom.xml b/spring-cloud-test-support/pom.xml index 7a2036f3..f86e440b 100644 --- a/spring-cloud-test-support/pom.xml +++ b/spring-cloud-test-support/pom.xml @@ -7,7 +7,7 @@ org.springframework.cloud spring-cloud-commons-parent - 2.1.2.BUILD-SNAPSHOT + 2.1.3.BUILD-SNAPSHOT .. spring-cloud-test-support