From 642224feff5f08e830c469ada96e88523239902a Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Tue, 29 Jul 2014 15:13:45 -0700 Subject: [PATCH] Support regex in keys-to-sanitize Update EnvironmentEndpoint and ConfigurationPropertiesReportEndpoint to allow regex patterns in `keys-to-sanitize`. Fixes gh-1245 --- ...ConfigurationPropertiesReportEndpoint.java | 17 +--- .../actuate/endpoint/EnvironmentEndpoint.java | 13 +-- .../boot/actuate/endpoint/Sanitizer.java | 84 ++++++++++++++++++ ...gurationPropertiesReportEndpointTests.java | 67 +++++++++++++++ .../endpoint/EnvironmentEndpointTests.java | 85 +++++++++++++++++++ .../boot/actuate/endpoint/SanitizerTests.java | 49 +++++++++++ .../appendix-application-properties.adoc | 3 +- 7 files changed, 293 insertions(+), 25 deletions(-) create mode 100644 spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/Sanitizer.java create mode 100644 spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/SanitizerTests.java diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/ConfigurationPropertiesReportEndpoint.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/ConfigurationPropertiesReportEndpoint.java index 628acdacd4..62e5369845 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/ConfigurationPropertiesReportEndpoint.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/ConfigurationPropertiesReportEndpoint.java @@ -29,7 +29,6 @@ import org.springframework.context.ApplicationContextAware; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.support.DefaultConversionService; -import org.springframework.util.Assert; import org.springframework.util.StringUtils; import com.fasterxml.jackson.databind.BeanDescription; @@ -66,7 +65,7 @@ public class ConfigurationPropertiesReportEndpoint extends private static final String CGLIB_FILTER_ID = "cglibFilter"; - private String[] keysToSanitize = new String[] { "password", "secret", "key" }; + private final Sanitizer sanitizer = new Sanitizer(); private ApplicationContext context; @@ -87,8 +86,7 @@ public class ConfigurationPropertiesReportEndpoint extends } public void setKeysToSanitize(String... keysToSanitize) { - Assert.notNull(keysToSanitize, "KeysToSanitize must not be null"); - this.keysToSanitize = keysToSanitize; + this.sanitizer.setKeysToSanitize(keysToSanitize); } @Override @@ -192,21 +190,12 @@ public class ConfigurationPropertiesReportEndpoint extends map.put(entry.getKey(), sanitize((Map) entry.getValue())); } else { - map.put(entry.getKey(), sanitize(entry.getKey(), entry.getValue())); + map.put(entry.getKey(), this.sanitizer.sanitize(entry.getKey(), entry.getValue())); } } return map; } - private Object sanitize(String name, Object object) { - for (String keyToSanitize : this.keysToSanitize) { - if (name.toLowerCase().endsWith(keyToSanitize)) { - return (object == null ? null : "******"); - } - } - return object; - } - /** * Extension to {@link JacksonAnnotationIntrospector} to suppress CGLIB generated bean * properties. diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/EnvironmentEndpoint.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/EnvironmentEndpoint.java index c44cc4619c..e508cc3fe8 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/EnvironmentEndpoint.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/EnvironmentEndpoint.java @@ -32,7 +32,6 @@ import org.springframework.core.env.Environment; import org.springframework.core.env.MutablePropertySources; import org.springframework.core.env.PropertySource; import org.springframework.core.env.StandardEnvironment; -import org.springframework.util.Assert; import org.springframework.util.ReflectionUtils; /** @@ -48,7 +47,7 @@ public class EnvironmentEndpoint extends AbstractEndpoint> i private Environment environment; - private String[] keysToSanitize = new String[] { "password", "secret", "key" }; + private final Sanitizer sanitizer = new Sanitizer(); /** * Create a new {@link EnvironmentEndpoint} instance. @@ -58,8 +57,7 @@ public class EnvironmentEndpoint extends AbstractEndpoint> i } public void setKeysToSanitize(String... keysToSanitize) { - Assert.notNull(keysToSanitize, "KeysToSanitize must not be null"); - this.keysToSanitize = keysToSanitize; + this.sanitizer.setKeysToSanitize(keysToSanitize); } @Override @@ -124,12 +122,7 @@ public class EnvironmentEndpoint extends AbstractEndpoint> i } public Object sanitize(String name, Object object) { - for (String keyToSanitize : this.keysToSanitize) { - if (name.toLowerCase().endsWith(keyToSanitize)) { - return (object == null ? null : "******"); - } - } - return object; + return this.sanitizer.sanitize(name, object); } @Override diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/Sanitizer.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/Sanitizer.java new file mode 100644 index 0000000000..a0288d331d --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/Sanitizer.java @@ -0,0 +1,84 @@ +/* + * Copyright 2012-2014 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.endpoint; + +import java.util.regex.Pattern; + +import org.springframework.util.Assert; + +/** + * Internal strategy used to sanitize potentially sensitive keys. + * + * @author Christian Dupuis + * @author Toshiaki Maki + * @author Phillip Webb + */ +class Sanitizer { + + private static final String[] REGEX_PARTS = { "*", "$", "^", "+" }; + + private Pattern[] keysToSanitize; + + public Sanitizer() { + setKeysToSanitize(new String[] { "password", "secret", "key" }); + } + + /** + * Set the keys that should be sanitize. Keys can be simple strings that the property + * ends with or regex expressions. + * @param keysToSanitize the keys to sanitize + */ + public void setKeysToSanitize(String... keysToSanitize) { + Assert.notNull(keysToSanitize, "KeysToSanitize must not be null"); + this.keysToSanitize = new Pattern[keysToSanitize.length]; + for (int i = 0; i < keysToSanitize.length; i++) { + this.keysToSanitize[i] = getPattern(keysToSanitize[i]); + } + } + + private Pattern getPattern(String value) { + if (isRegex(value)) { + return Pattern.compile(value, Pattern.CASE_INSENSITIVE); + } + return Pattern.compile(".*" + value + "$", Pattern.CASE_INSENSITIVE); + } + + private boolean isRegex(String value) { + for (String part : REGEX_PARTS) { + if (value.contains(part)) { + return true; + } + } + return false; + } + + /** + * Sanitize the given value if necessary. + * @param key the key to sanitize + * @param value the value + * @return the potentially sanitized value + */ + public Object sanitize(String key, Object value) { + for (Pattern pattern : this.keysToSanitize) { + if (pattern.matcher(key).matches()) { + return (value == null ? null : "******"); + } + } + return value; + } + +} diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/ConfigurationPropertiesReportEndpointTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/ConfigurationPropertiesReportEndpointTests.java index 108057c633..d913a2adcb 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/ConfigurationPropertiesReportEndpointTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/ConfigurationPropertiesReportEndpointTests.java @@ -21,6 +21,8 @@ import java.util.Map; import org.junit.Test; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.test.EnvironmentTestUtils; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -79,6 +81,71 @@ public class ConfigurationPropertiesReportEndpointTests extends assertEquals("******", nestedProperties.get("myTestProperty")); } + @SuppressWarnings("unchecked") + @Test + public void testKeySanitizationWithCustomPattern() throws Exception { + ConfigurationPropertiesReportEndpoint report = getEndpointBean(); + report.setKeysToSanitize(".*pass.*"); + Map properties = report.invoke(); + Map nestedProperties = (Map) ((Map) properties + .get("testProperties")).get("properties"); + assertNotNull(nestedProperties); + assertEquals("******", nestedProperties.get("dbPassword")); + assertEquals("654321", nestedProperties.get("myTestProperty")); + } + + @SuppressWarnings("unchecked") + @Test + public void testKeySanitizationWithCustomKeysByEnvironment() throws Exception { + this.context = new AnnotationConfigApplicationContext(); + EnvironmentTestUtils.addEnvironment(this.context, + "endpoints.configprops.keys-to-sanitize:property"); + this.context.register(Config.class); + this.context.refresh(); + ConfigurationPropertiesReportEndpoint report = getEndpointBean(); + Map properties = report.invoke(); + Map nestedProperties = (Map) ((Map) properties + .get("testProperties")).get("properties"); + assertNotNull(nestedProperties); + assertEquals("123456", nestedProperties.get("dbPassword")); + assertEquals("******", nestedProperties.get("myTestProperty")); + } + + @SuppressWarnings("unchecked") + @Test + public void testKeySanitizationWithCustomPatternByEnvironment() throws Exception { + this.context = new AnnotationConfigApplicationContext(); + EnvironmentTestUtils.addEnvironment(this.context, + "endpoints.configprops.keys-to-sanitize: .*pass.*"); + this.context.register(Config.class); + this.context.refresh(); + ConfigurationPropertiesReportEndpoint report = getEndpointBean(); + Map properties = report.invoke(); + Map nestedProperties = (Map) ((Map) properties + .get("testProperties")).get("properties"); + assertNotNull(nestedProperties); + assertEquals("******", nestedProperties.get("dbPassword")); + assertEquals("654321", nestedProperties.get("myTestProperty")); + } + + @SuppressWarnings("unchecked") + @Test + public void testKeySanitizationWithCustomPatternAndKeyByEnvironment() + throws Exception { + this.context = new AnnotationConfigApplicationContext(); + EnvironmentTestUtils.addEnvironment(this.context, + "endpoints.configprops.keys-to-sanitize: .*pass.*, property"); + this.context.register(Config.class); + this.context.refresh(); + ConfigurationPropertiesReportEndpoint report = getEndpointBean(); + Map properties = report.invoke(); + Map nestedProperties = (Map) ((Map) properties + .get("testProperties")).get("properties"); + assertNotNull(nestedProperties); + assertEquals("******", nestedProperties.get("dbPassword")); + assertEquals("******", nestedProperties.get("myTestProperty")); + } + @Configuration @EnableConfigurationProperties public static class Parent { diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/EnvironmentEndpointTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/EnvironmentEndpointTests.java index 2761994fe4..cf5d31c00e 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/EnvironmentEndpointTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/EnvironmentEndpointTests.java @@ -21,6 +21,8 @@ import java.util.Map; import org.junit.Test; import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.test.EnvironmentTestUtils; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.env.CompositePropertySource; @@ -74,6 +76,89 @@ public class EnvironmentEndpointTests extends AbstractEndpointTests) env.get("systemProperties")).get("apiKey")); } + @SuppressWarnings("unchecked") + @Test + public void testKeySanitizationWithCustomKeys() throws Exception { + System.setProperty("dbPassword", "123456"); + System.setProperty("apiKey", "123456"); + EnvironmentEndpoint report = getEndpointBean(); + report.setKeysToSanitize("key"); + Map env = report.invoke(); + assertEquals("123456", + ((Map) env.get("systemProperties")).get("dbPassword")); + assertEquals("******", + ((Map) env.get("systemProperties")).get("apiKey")); + } + + @SuppressWarnings("unchecked") + @Test + public void testKeySanitizationWithCustomPattern() throws Exception { + System.setProperty("dbPassword", "123456"); + System.setProperty("apiKey", "123456"); + EnvironmentEndpoint report = getEndpointBean(); + report.setKeysToSanitize(".*pass.*"); + Map env = report.invoke(); + assertEquals("******", + ((Map) env.get("systemProperties")).get("dbPassword")); + assertEquals("123456", + ((Map) env.get("systemProperties")).get("apiKey")); + } + + @SuppressWarnings("unchecked") + @Test + public void testKeySanitizationWithCustomKeysByEnvironment() throws Exception { + this.context = new AnnotationConfigApplicationContext(); + EnvironmentTestUtils.addEnvironment(this.context, + "endpoints.env.keys-to-sanitize: key"); + this.context.register(Config.class); + this.context.refresh(); + System.setProperty("dbPassword", "123456"); + System.setProperty("apiKey", "123456"); + EnvironmentEndpoint report = getEndpointBean(); + Map env = report.invoke(); + assertEquals("123456", + ((Map) env.get("systemProperties")).get("dbPassword")); + assertEquals("******", + ((Map) env.get("systemProperties")).get("apiKey")); + } + + @SuppressWarnings("unchecked") + @Test + public void testKeySanitizationWithCustomPatternByEnvironment() throws Exception { + this.context = new AnnotationConfigApplicationContext(); + EnvironmentTestUtils.addEnvironment(this.context, + "endpoints.env.keys-to-sanitize: .*pass.*"); + this.context.register(Config.class); + this.context.refresh(); + System.setProperty("dbPassword", "123456"); + System.setProperty("apiKey", "123456"); + EnvironmentEndpoint report = getEndpointBean(); + Map env = report.invoke(); + assertEquals("******", + ((Map) env.get("systemProperties")).get("dbPassword")); + assertEquals("123456", + ((Map) env.get("systemProperties")).get("apiKey")); + } + + @SuppressWarnings("unchecked") + @Test + public void testKeySanitizationWithCustomPatternAndKeyByEnvironment() + throws Exception { + this.context = new AnnotationConfigApplicationContext(); + EnvironmentTestUtils.addEnvironment(this.context, + "endpoints.env.keys-to-sanitize: .*pass.*, key"); + this.context.register(Config.class); + this.context.refresh(); + System.setProperty("dbPassword", "123456"); + System.setProperty("apiKey", "123456"); + EnvironmentEndpoint report = getEndpointBean(); + Map env = report.invoke(); + assertEquals("******", + ((Map) env.get("systemProperties")).get("dbPassword")); + assertEquals("******", + ((Map) env.get("systemProperties")).get("apiKey")); + } + @Configuration @EnableConfigurationProperties public static class Config { diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/SanitizerTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/SanitizerTests.java new file mode 100644 index 0000000000..ffca349b5e --- /dev/null +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/SanitizerTests.java @@ -0,0 +1,49 @@ +/* + * Copyright 2012-2014 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.endpoint; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +/** + * Tests for {@link Sanitizer}. + * + * @author Phillip Webb + */ +public class SanitizerTests { + + private Sanitizer sanitizer = new Sanitizer(); + + @Test + public void defaults() throws Exception { + assertEquals(this.sanitizer.sanitize("password", "secret"), "******"); + assertEquals(this.sanitizer.sanitize("my-password", "secret"), "******"); + assertEquals(this.sanitizer.sanitize("my-OTHER.paSSword", "secret"), "******"); + assertEquals(this.sanitizer.sanitize("somesecret", "secret"), "******"); + assertEquals(this.sanitizer.sanitize("somekey", "secret"), "******"); + assertEquals(this.sanitizer.sanitize("find", "secret"), "secret"); + } + + @Test + public void regex() throws Exception { + this.sanitizer.setKeysToSanitize(".*lock.*"); + assertEquals(this.sanitizer.sanitize("verylOCkish", "secret"), "******"); + assertEquals(this.sanitizer.sanitize("veryokish", "secret"), "secret"); + } + +} diff --git a/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc b/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc index 2c4b27c076..19d97d36db 100644 --- a/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc +++ b/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc @@ -337,13 +337,14 @@ content into your application; rather pick only the properties that you need. endpoints.configprops.id=configprops endpoints.configprops.sensitive=true endpoints.configprops.enabled=true - endpoints.configprops.keys-to-sanitize=password,secret + endpoints.configprops.keys-to-sanitize=password,secret,key # suffix or regex endpoints.dump.id=dump endpoints.dump.sensitive=true endpoints.dump.enabled=true endpoints.env.id=env endpoints.env.sensitive=true endpoints.env.enabled=true + endpoints.env.keys-to-sanitize=password,secret,key # suffix or regex endpoints.health.id=health endpoints.health.sensitive=false endpoints.health.enabled=true