Add pluggable abstraction for applying custom sanitization rules

Closes gh-27840
pull/27856/head
Madhura Bhave 3 years ago
parent 211532f08d
commit 253f98c3e7

@ -16,9 +16,11 @@
package org.springframework.boot.actuate.autoconfigure.context.properties; package org.springframework.boot.actuate.autoconfigure.context.properties;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnAvailableEndpoint; import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnAvailableEndpoint;
import org.springframework.boot.actuate.context.properties.ConfigurationPropertiesReportEndpoint; import org.springframework.boot.actuate.context.properties.ConfigurationPropertiesReportEndpoint;
import org.springframework.boot.actuate.context.properties.ConfigurationPropertiesReportEndpointWebExtension; import org.springframework.boot.actuate.context.properties.ConfigurationPropertiesReportEndpointWebExtension;
import org.springframework.boot.actuate.endpoint.SanitizingFunction;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
@ -43,8 +45,9 @@ public class ConfigurationPropertiesReportEndpointAutoConfiguration {
@Bean @Bean
@ConditionalOnMissingBean @ConditionalOnMissingBean
public ConfigurationPropertiesReportEndpoint configurationPropertiesReportEndpoint( public ConfigurationPropertiesReportEndpoint configurationPropertiesReportEndpoint(
ConfigurationPropertiesReportEndpointProperties properties) { ConfigurationPropertiesReportEndpointProperties properties,
ConfigurationPropertiesReportEndpoint endpoint = new ConfigurationPropertiesReportEndpoint(); ObjectProvider<SanitizingFunction> sanitizingFunctions) {
ConfigurationPropertiesReportEndpoint endpoint = new ConfigurationPropertiesReportEndpoint(sanitizingFunctions);
String[] keysToSanitize = properties.getKeysToSanitize(); String[] keysToSanitize = properties.getKeysToSanitize();
if (keysToSanitize != null) { if (keysToSanitize != null) {
endpoint.setKeysToSanitize(keysToSanitize); endpoint.setKeysToSanitize(keysToSanitize);

@ -16,7 +16,9 @@
package org.springframework.boot.actuate.autoconfigure.env; package org.springframework.boot.actuate.autoconfigure.env;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnAvailableEndpoint; import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnAvailableEndpoint;
import org.springframework.boot.actuate.endpoint.SanitizingFunction;
import org.springframework.boot.actuate.env.EnvironmentEndpoint; import org.springframework.boot.actuate.env.EnvironmentEndpoint;
import org.springframework.boot.actuate.env.EnvironmentEndpointWebExtension; import org.springframework.boot.actuate.env.EnvironmentEndpointWebExtension;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
@ -41,8 +43,9 @@ public class EnvironmentEndpointAutoConfiguration {
@Bean @Bean
@ConditionalOnMissingBean @ConditionalOnMissingBean
public EnvironmentEndpoint environmentEndpoint(Environment environment, EnvironmentEndpointProperties properties) { public EnvironmentEndpoint environmentEndpoint(Environment environment, EnvironmentEndpointProperties properties,
EnvironmentEndpoint endpoint = new EnvironmentEndpoint(environment); ObjectProvider<SanitizingFunction> sanitizingFunctions) {
EnvironmentEndpoint endpoint = new EnvironmentEndpoint(environment, sanitizingFunctions);
String[] keysToSanitize = properties.getKeysToSanitize(); String[] keysToSanitize = properties.getKeysToSanitize();
if (keysToSanitize != null) { if (keysToSanitize != null) {
endpoint.setKeysToSanitize(keysToSanitize); endpoint.setKeysToSanitize(keysToSanitize);

@ -22,6 +22,7 @@ import org.junit.jupiter.api.Test;
import org.springframework.boot.actuate.context.properties.ConfigurationPropertiesReportEndpoint; import org.springframework.boot.actuate.context.properties.ConfigurationPropertiesReportEndpoint;
import org.springframework.boot.actuate.context.properties.ConfigurationPropertiesReportEndpoint.ApplicationConfigurationProperties; import org.springframework.boot.actuate.context.properties.ConfigurationPropertiesReportEndpoint.ApplicationConfigurationProperties;
import org.springframework.boot.actuate.endpoint.SanitizingFunction;
import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties;
@ -72,6 +73,14 @@ class ConfigurationPropertiesReportEndpointAutoConfigurationTests {
.run(validateTestProperties("******", "******")); .run(validateTestProperties("******", "******"));
} }
@Test
void customSanitizingFunctionShouldBeApplied() {
this.contextRunner.withUserConfiguration(Config.class, SanitizingFunctionConfiguration.class)
.withPropertyValues("management.endpoints.web.exposure.include=configprops",
"test.my-test-property=abc")
.run(validateTestProperties("******", "$$$"));
}
@Test @Test
void runWhenNotExposedShouldNotHaveEndpointBean() { void runWhenNotExposedShouldNotHaveEndpointBean() {
this.contextRunner this.contextRunner
@ -129,4 +138,19 @@ class ConfigurationPropertiesReportEndpointAutoConfigurationTests {
} }
@Configuration(proxyBeanMethods = false)
static class SanitizingFunctionConfiguration {
@Bean
SanitizingFunction testSanitizingFunction() {
return (data) -> {
if (data.getKey().contains("my")) {
return data.withValue("$$$");
}
return data;
};
}
}
} }

@ -20,6 +20,7 @@ import java.util.Map;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.boot.actuate.endpoint.SanitizingFunction;
import org.springframework.boot.actuate.env.EnvironmentEndpoint; import org.springframework.boot.actuate.env.EnvironmentEndpoint;
import org.springframework.boot.actuate.env.EnvironmentEndpoint.EnvironmentDescriptor; import org.springframework.boot.actuate.env.EnvironmentEndpoint.EnvironmentDescriptor;
import org.springframework.boot.actuate.env.EnvironmentEndpoint.PropertySourceDescriptor; import org.springframework.boot.actuate.env.EnvironmentEndpoint.PropertySourceDescriptor;
@ -28,6 +29,8 @@ import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.assertj.AssertableApplicationContext; import org.springframework.boot.test.context.assertj.AssertableApplicationContext;
import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.boot.test.context.runner.ContextConsumer; import org.springframework.boot.test.context.runner.ContextConsumer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
@ -67,6 +70,21 @@ class EnvironmentEndpointAutoConfigurationTests {
.run(validateSystemProperties("******", "123456")); .run(validateSystemProperties("******", "123456"));
} }
@Test
void sanitizingFunctionsCanBeConfiguredViaTheEnvironment() {
this.contextRunner.withUserConfiguration(SanitizingFunctionConfiguration.class)
.withPropertyValues("management.endpoints.web.exposure.include=env")
.withSystemProperties("custom=123456", "password=123456").run((context) -> {
assertThat(context).hasSingleBean(EnvironmentEndpoint.class);
EnvironmentEndpoint endpoint = context.getBean(EnvironmentEndpoint.class);
EnvironmentDescriptor env = endpoint.environment(null);
Map<String, PropertyValueDescriptor> systemProperties = getSource("systemProperties", env)
.getProperties();
assertThat(systemProperties.get("custom").getValue()).isEqualTo("$$$");
assertThat(systemProperties.get("password").getValue()).isEqualTo("******");
});
}
@Test @Test
void additionalKeysToSanitizeCanBeConfiguredViaTheEnvironment() { void additionalKeysToSanitizeCanBeConfiguredViaTheEnvironment() {
this.contextRunner.withPropertyValues("management.endpoints.web.exposure.include=env") this.contextRunner.withPropertyValues("management.endpoints.web.exposure.include=env")
@ -91,4 +109,14 @@ class EnvironmentEndpointAutoConfigurationTests {
.get(); .get();
} }
@Configuration(proxyBeanMethods = false)
static class SanitizingFunctionConfiguration {
@Bean
SanitizingFunction testSanitizingFunction() {
return (data) -> data.withValue("$$$");
}
}
} }

@ -53,7 +53,9 @@ import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeanUtils; import org.springframework.beans.BeanUtils;
import org.springframework.beans.BeansException; import org.springframework.beans.BeansException;
import org.springframework.boot.actuate.endpoint.SanitizableData;
import org.springframework.boot.actuate.endpoint.Sanitizer; import org.springframework.boot.actuate.endpoint.Sanitizer;
import org.springframework.boot.actuate.endpoint.SanitizingFunction;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint; import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.boot.actuate.endpoint.annotation.Selector; import org.springframework.boot.actuate.endpoint.annotation.Selector;
@ -64,6 +66,7 @@ import org.springframework.boot.context.properties.ConstructorBinding;
import org.springframework.boot.context.properties.bind.Name; import org.springframework.boot.context.properties.bind.Name;
import org.springframework.boot.context.properties.source.ConfigurationProperty; import org.springframework.boot.context.properties.source.ConfigurationProperty;
import org.springframework.boot.context.properties.source.ConfigurationPropertyName; import org.springframework.boot.context.properties.source.ConfigurationPropertyName;
import org.springframework.boot.context.properties.source.ConfigurationPropertySource;
import org.springframework.boot.origin.Origin; import org.springframework.boot.origin.Origin;
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware; import org.springframework.context.ApplicationContextAware;
@ -73,6 +76,7 @@ import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.core.annotation.MergedAnnotation; import org.springframework.core.annotation.MergedAnnotation;
import org.springframework.core.annotation.MergedAnnotations; import org.springframework.core.annotation.MergedAnnotations;
import org.springframework.core.annotation.MergedAnnotations.SearchStrategy; import org.springframework.core.annotation.MergedAnnotations.SearchStrategy;
import org.springframework.core.env.PropertySource;
import org.springframework.util.ClassUtils; import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
@ -100,12 +104,20 @@ public class ConfigurationPropertiesReportEndpoint implements ApplicationContext
private static final String CONFIGURATION_PROPERTIES_FILTER_ID = "configurationPropertiesFilter"; private static final String CONFIGURATION_PROPERTIES_FILTER_ID = "configurationPropertiesFilter";
private final Sanitizer sanitizer = new Sanitizer(); private final Sanitizer sanitizer;
private ApplicationContext context; private ApplicationContext context;
private ObjectMapper objectMapper; private ObjectMapper objectMapper;
public ConfigurationPropertiesReportEndpoint() {
this(Collections.emptyList());
}
public ConfigurationPropertiesReportEndpoint(Iterable<SanitizingFunction> sanitizingFunctions) {
this.sanitizer = new Sanitizer(sanitizingFunctions);
}
@Override @Override
public void setApplicationContext(ApplicationContext context) throws BeansException { public void setApplicationContext(ApplicationContext context) throws BeansException {
this.context = context; this.context = context;
@ -236,26 +248,63 @@ public class ConfigurationPropertiesReportEndpoint implements ApplicationContext
map.put(key, sanitize(qualifiedKey, (List<Object>) value)); map.put(key, sanitize(qualifiedKey, (List<Object>) value));
} }
else { else {
value = this.sanitizer.sanitize(key, value); map.put(key, sanitizeWithPropertySourceIfPresent(qualifiedKey, value));
value = this.sanitizer.sanitize(qualifiedKey, value);
map.put(key, value);
} }
}); });
return map; return map;
} }
private Object sanitizeWithPropertySourceIfPresent(String qualifiedKey, Object value) {
ConfigurationPropertyName currentName = getCurrentName(qualifiedKey);
ConfigurationProperty candidate = getCandidate(currentName);
PropertySource<?> propertySource = getPropertySource(candidate);
if (propertySource != null) {
SanitizableData data = new SanitizableData(propertySource, qualifiedKey, value);
return this.sanitizer.sanitize(data);
}
SanitizableData data = new SanitizableData(null, qualifiedKey, value);
return this.sanitizer.sanitize(data);
}
private PropertySource<?> getPropertySource(ConfigurationProperty configurationProperty) {
if (configurationProperty == null) {
return null;
}
ConfigurationPropertySource source = configurationProperty.getSource();
Object underlyingSource = (source != null) ? source.getUnderlyingSource() : null;
return (underlyingSource instanceof PropertySource<?>) ? (PropertySource<?>) underlyingSource : null;
}
private ConfigurationPropertyName getCurrentName(String qualifiedKey) {
return ConfigurationPropertyName.adapt(qualifiedKey, '.');
}
private ConfigurationProperty getCandidate(ConfigurationPropertyName currentName) {
BoundConfigurationProperties bound = BoundConfigurationProperties.get(this.context);
if (bound == null) {
return null;
}
ConfigurationProperty candidate = bound.get(currentName);
if (candidate == null && currentName.isLastElementIndexed()) {
candidate = bound.get(currentName.chop(currentName.getNumberOfElements() - 1));
}
return candidate;
}
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
private List<Object> sanitize(String prefix, List<Object> list) { private List<Object> sanitize(String prefix, List<Object> list) {
List<Object> sanitized = new ArrayList<>(); List<Object> sanitized = new ArrayList<>();
int index = 0;
for (Object item : list) { for (Object item : list) {
String name = prefix + "[" + index++ + "]";
if (item instanceof Map) { if (item instanceof Map) {
sanitized.add(sanitize(prefix, (Map<String, Object>) item)); sanitized.add(sanitize(name, (Map<String, Object>) item));
} }
else if (item instanceof List) { else if (item instanceof List) {
sanitized.add(sanitize(prefix, (List<Object>) item)); sanitized.add(sanitize(name, (List<Object>) item));
} }
else { else {
sanitized.add(this.sanitizer.sanitize(prefix, item)); sanitized.add(sanitizeWithPropertySourceIfPresent(name, item));
} }
} }
return sanitized; return sanitized;
@ -299,24 +348,22 @@ public class ConfigurationPropertiesReportEndpoint implements ApplicationContext
} }
private Map<String, Object> applyInput(String qualifiedKey) { private Map<String, Object> applyInput(String qualifiedKey) {
BoundConfigurationProperties bound = BoundConfigurationProperties.get(this.context); ConfigurationPropertyName currentName = getCurrentName(qualifiedKey);
if (bound == null) { ConfigurationProperty candidate = getCandidate(currentName);
return Collections.emptyMap(); PropertySource<?> propertySource = getPropertySource(candidate);
} if (propertySource != null) {
ConfigurationPropertyName currentName = ConfigurationPropertyName.adapt(qualifiedKey, '.'); Object value = stringifyIfNecessary(candidate.getValue());
ConfigurationProperty candidate = bound.get(currentName); SanitizableData data = new SanitizableData(propertySource, currentName.toString(), value);
if (candidate == null && currentName.isLastElementIndexed()) { return getInput(candidate, this.sanitizer.sanitize(data));
candidate = bound.get(currentName.chop(currentName.getNumberOfElements() - 1)); }
} return Collections.emptyMap();
return (candidate != null) ? getInput(currentName.toString(), candidate) : Collections.emptyMap();
} }
private Map<String, Object> getInput(String property, ConfigurationProperty candidate) { private Map<String, Object> getInput(ConfigurationProperty candidate, Object sanitizedValue) {
Map<String, Object> input = new LinkedHashMap<>(); Map<String, Object> input = new LinkedHashMap<>();
Object value = stringifyIfNecessary(candidate.getValue());
Origin origin = Origin.from(candidate); Origin origin = Origin.from(candidate);
List<Origin> originParents = Origin.parentsFrom(candidate); List<Origin> originParents = Origin.parentsFrom(candidate);
input.put("value", this.sanitizer.sanitize(property, value)); input.put("value", sanitizedValue);
input.put("origin", (origin != null) ? origin.toString() : "none"); input.put("origin", (origin != null) ? origin.toString() : "none");
if (!originParents.isEmpty()) { if (!originParents.isEmpty()) {
input.put("originParents", originParents.stream().map(Object::toString).toArray(String[]::new)); input.put("originParents", originParents.stream().map(Object::toString).toArray(String[]::new));

@ -0,0 +1,86 @@
/*
* Copyright 2012-2021 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
*
* https://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.springframework.core.env.PropertySource;
/**
* Value object that represents the data that can be used by a {@link SanitizingFunction}.
*
* @author Madhura Bhave
* @since 2.6.0
**/
public final class SanitizableData {
/**
* Represents a sanitized value.
*/
public static final String SANITIZED_VALUE = "******";
private final PropertySource<?> propertySource;
private final String key;
private final Object value;
/**
* Create a new {@link SanitizableData} instance.
* @param propertySource the property source that provided the data or {@code null}.
* @param key the data key
* @param value the data value
*/
public SanitizableData(PropertySource<?> propertySource, String key, Object value) {
this.propertySource = propertySource;
this.key = key;
this.value = value;
}
/**
* Return the property source that provided the data or {@code null} If the data was
* not from a {@link PropertySource}.
* @return the property source that provided the data
*/
public PropertySource<?> getPropertySource() {
return this.propertySource;
}
/**
* Return the key of the data.
* @return the data key
*/
public String getKey() {
return this.key;
}
/**
* Return the value of the data.
* @return the data value
*/
public Object getValue() {
return this.value;
}
/**
* Return a new {@link SanitizableData} instance with a different value.
* @param value the new value (often {@link #SANITIZED_VALUE}
* @return a new sanitizable data instance
*/
public SanitizableData withValue(Object value) {
return new SanitizableData(this.propertySource, this.key, value);
}
}

@ -16,8 +16,11 @@
package org.springframework.boot.actuate.endpoint; package org.springframework.boot.actuate.endpoint;
import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@ -38,6 +41,7 @@ import org.springframework.util.StringUtils;
* @author HaiTao Zhang * @author HaiTao Zhang
* @author Chris Bono * @author Chris Bono
* @author David Good * @author David Good
* @author Madhura Bhave
* @since 2.0.0 * @since 2.0.0
*/ */
public class Sanitizer { public class Sanitizer {
@ -55,6 +59,8 @@ public class Sanitizer {
private Pattern[] keysToSanitize; private Pattern[] keysToSanitize;
private final List<SanitizingFunction> sanitizingFunctions = new ArrayList<>();
static { static {
DEFAULT_KEYS_TO_SANITIZE.addAll(URI_USERINFO_KEYS); DEFAULT_KEYS_TO_SANITIZE.addAll(URI_USERINFO_KEYS);
} }
@ -64,9 +70,26 @@ public class Sanitizer {
} }
public Sanitizer(String... keysToSanitize) { public Sanitizer(String... keysToSanitize) {
this(Collections.emptyList(), keysToSanitize);
}
public Sanitizer(Iterable<SanitizingFunction> sanitizingFunctions) {
this(sanitizingFunctions, DEFAULT_KEYS_TO_SANITIZE.toArray(new String[0]));
}
public Sanitizer(Iterable<SanitizingFunction> sanitizingFunctions, String... keysToSanitize) {
sanitizingFunctions.forEach(this.sanitizingFunctions::add);
this.sanitizingFunctions.add(getDefaultSanitizingFunction());
setKeysToSanitize(keysToSanitize); setKeysToSanitize(keysToSanitize);
} }
private SanitizingFunction getDefaultSanitizingFunction() {
return (data) -> {
Object sanitizedValue = sanitize(data.getKey(), data.getValue());
return data.withValue(sanitizedValue);
};
}
/** /**
* Set the keys that should be sanitized, overwriting any existing configuration. Keys * Set the keys that should be sanitized, overwriting any existing configuration. Keys
* can be simple strings that the property ends with or regular expressions. * can be simple strings that the property ends with or regular expressions.
@ -126,12 +149,29 @@ public class Sanitizer {
if (keyIsUriWithUserInfo(pattern)) { if (keyIsUriWithUserInfo(pattern)) {
return sanitizeUris(value.toString()); return sanitizeUris(value.toString());
} }
return "******"; return SanitizableData.SANITIZED_VALUE;
} }
} }
return value; return value;
} }
/**
* Sanitize the value from the given {@link SanitizableData} using the available
* {@link SanitizingFunction}s.
* @param data the sanitizable data
* @return the potentially updated data
* @since 2.6.0
*/
public Object sanitize(SanitizableData data) {
if (data.getValue() == null) {
return null;
}
for (SanitizingFunction sanitizingFunction : this.sanitizingFunctions) {
data = sanitizingFunction.apply(data);
}
return data.getValue();
}
private boolean keyIsUriWithUserInfo(Pattern pattern) { private boolean keyIsUriWithUserInfo(Pattern pattern) {
for (String uriKey : URI_USERINFO_KEYS) { for (String uriKey : URI_USERINFO_KEYS) {
if (pattern.matcher(uriKey).matches()) { if (pattern.matcher(uriKey).matches()) {
@ -149,7 +189,7 @@ public class Sanitizer {
Matcher matcher = URI_USERINFO_PATTERN.matcher(value); Matcher matcher = URI_USERINFO_PATTERN.matcher(value);
String password = matcher.matches() ? matcher.group(1) : null; String password = matcher.matches() ? matcher.group(1) : null;
if (password != null) { if (password != null) {
return StringUtils.replace(value, ":" + password + "@", ":******@"); return StringUtils.replace(value, ":" + password + "@", ":" + SanitizableData.SANITIZED_VALUE + "@");
} }
return value; return value;
} }

@ -0,0 +1,36 @@
/*
* Copyright 2012-2021 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
*
* https://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;
/**
* Function that takes a {@link SanitizableData} and applies sanitization to the value, if
* necessary. Can be used by a {@link Sanitizer} to determine the sanitized value.
*
* @author Madhura Bhave
* @since 2.6.0
*/
@FunctionalInterface
public interface SanitizingFunction {
/**
* Apply the sanitiing function to the given data.
* @param data the data to sanitize
* @return the sanitized data or the original instance is no sanitization is applied
*/
SanitizableData apply(SanitizableData data);
}

@ -18,6 +18,7 @@ package org.springframework.boot.actuate.env;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -27,7 +28,9 @@ import java.util.stream.Stream;
import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude;
import org.springframework.boot.actuate.endpoint.SanitizableData;
import org.springframework.boot.actuate.endpoint.Sanitizer; import org.springframework.boot.actuate.endpoint.Sanitizer;
import org.springframework.boot.actuate.endpoint.SanitizingFunction;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint; import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.boot.actuate.endpoint.annotation.Selector; import org.springframework.boot.actuate.endpoint.annotation.Selector;
@ -64,12 +67,17 @@ import org.springframework.util.SystemPropertyUtils;
@Endpoint(id = "env") @Endpoint(id = "env")
public class EnvironmentEndpoint { public class EnvironmentEndpoint {
private final Sanitizer sanitizer = new Sanitizer(); private final Sanitizer sanitizer;
private final Environment environment; private final Environment environment;
public EnvironmentEndpoint(Environment environment) { public EnvironmentEndpoint(Environment environment) {
this(environment, Collections.emptyList());
}
public EnvironmentEndpoint(Environment environment, Iterable<SanitizingFunction> sanitizingFunctions) {
this.environment = environment; this.environment = environment;
this.sanitizer = new Sanitizer(sanitizingFunctions);
} }
public void setKeysToSanitize(String... keysToSanitize) { public void setKeysToSanitize(String... keysToSanitize) {
@ -149,7 +157,8 @@ public class EnvironmentEndpoint {
PlaceholdersResolver resolver) { PlaceholdersResolver resolver) {
Object resolved = resolver.resolvePlaceholders(source.getProperty(name)); Object resolved = resolver.resolvePlaceholders(source.getProperty(name));
Origin origin = ((source instanceof OriginLookup) ? ((OriginLookup<Object>) source).getOrigin(name) : null); Origin origin = ((source instanceof OriginLookup) ? ((OriginLookup<Object>) source).getOrigin(name) : null);
return new PropertyValueDescriptor(stringifyIfNecessary(sanitize(name, resolved)), origin); Object sanitizedValue = sanitize(source, name, resolved);
return new PropertyValueDescriptor(stringifyIfNecessary(sanitizedValue), origin);
} }
private PlaceholdersResolver getResolver() { private PlaceholdersResolver getResolver() {
@ -184,8 +193,21 @@ public class EnvironmentEndpoint {
} }
} }
public Object sanitize(String name, Object object) { /**
return this.sanitizer.sanitize(name, object); * Apply sanitiation to the given name and value.
* @param key the name to sanitize
* @param value the value to sanitize
* @return the sanitized value
* @deprecated since 2.6.0 for removal in 2.8.0 as sanitization should be internal to
* the class
*/
@Deprecated
public Object sanitize(String key, Object value) {
return this.sanitizer.sanitize(key, value);
}
private Object sanitize(PropertySource<?> source, String name, Object value) {
return this.sanitizer.sanitize(new SanitizableData(source, name, value));
} }
protected Object stringifyIfNecessary(Object value) { protected Object stringifyIfNecessary(Object value) {
@ -207,19 +229,28 @@ public class EnvironmentEndpoint {
private final Sanitizer sanitizer; private final Sanitizer sanitizer;
private final Iterable<PropertySource<?>> sources;
PropertySourcesPlaceholdersSanitizingResolver(Iterable<PropertySource<?>> sources, Sanitizer sanitizer) { PropertySourcesPlaceholdersSanitizingResolver(Iterable<PropertySource<?>> sources, Sanitizer sanitizer) {
super(sources, new PropertyPlaceholderHelper(SystemPropertyUtils.PLACEHOLDER_PREFIX, super(sources, new PropertyPlaceholderHelper(SystemPropertyUtils.PLACEHOLDER_PREFIX,
SystemPropertyUtils.PLACEHOLDER_SUFFIX, SystemPropertyUtils.VALUE_SEPARATOR, true)); SystemPropertyUtils.PLACEHOLDER_SUFFIX, SystemPropertyUtils.VALUE_SEPARATOR, true));
this.sources = sources;
this.sanitizer = sanitizer; this.sanitizer = sanitizer;
} }
@Override @Override
protected String resolvePlaceholder(String placeholder) { protected String resolvePlaceholder(String placeholder) {
String value = super.resolvePlaceholder(placeholder); if (this.sources != null) {
if (value == null) { for (PropertySource<?> source : this.sources) {
return null; Object value = source.getProperty(placeholder);
if (value != null) {
SanitizableData data = new SanitizableData(source, placeholder, value);
Object sanitized = this.sanitizer.sanitize(data);
return (sanitized != null) ? String.valueOf(sanitized) : null;
}
}
} }
return (String) this.sanitizer.sanitize(placeholder, value); return null;
} }
} }

@ -30,6 +30,7 @@ import org.junit.jupiter.api.Test;
import org.springframework.boot.actuate.context.properties.ConfigurationPropertiesReportEndpoint.ConfigurationPropertiesBeanDescriptor; import org.springframework.boot.actuate.context.properties.ConfigurationPropertiesReportEndpoint.ConfigurationPropertiesBeanDescriptor;
import org.springframework.boot.actuate.context.properties.ConfigurationPropertiesReportEndpoint.ContextConfigurationProperties; import org.springframework.boot.actuate.context.properties.ConfigurationPropertiesReportEndpoint.ContextConfigurationProperties;
import org.springframework.boot.actuate.endpoint.SanitizingFunction;
import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.ConstructorBinding; import org.springframework.boot.context.properties.ConstructorBinding;
import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties;
@ -57,6 +58,7 @@ import static org.assertj.core.api.Assertions.entry;
* @author Stephane Nicoll * @author Stephane Nicoll
* @author HaiTao Zhang * @author HaiTao Zhang
* @author Chris Bono * @author Chris Bono
* @author Madhura Bhave
*/ */
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
class ConfigurationPropertiesReportEndpointTests { class ConfigurationPropertiesReportEndpointTests {
@ -286,6 +288,50 @@ class ConfigurationPropertiesReportEndpointTests {
})); }));
} }
@Test
void sanitizeWithCustomSanitizingFunction() {
new ApplicationContextRunner().withUserConfiguration(CustomSanitizingEndpointConfig.class,
SanitizingFunctionConfiguration.class, TestPropertiesConfiguration.class)
.run(assertProperties("test", (properties) -> {
assertThat(properties.get("dbPassword")).isEqualTo("******");
assertThat(properties.get("myTestProperty")).isEqualTo("$$$");
}));
}
@Test
void sanitizeWithCustomPropertySourceBasedSanitizingFunction() {
new ApplicationContextRunner()
.withUserConfiguration(CustomSanitizingEndpointConfig.class,
PropertySourceBasedSanitizingFunctionConfiguration.class, TestPropertiesConfiguration.class)
.withPropertyValues("test.my-test-property=abcde").run(assertProperties("test", (properties) -> {
assertThat(properties.get("dbPassword")).isEqualTo("******");
assertThat(properties.get("myTestProperty")).isEqualTo("$$$");
}));
}
@Test
void sanitizeListsWithCustomSanitizingFunction() {
new ApplicationContextRunner()
.withUserConfiguration(CustomSanitizingEndpointConfig.class, SanitizingFunctionConfiguration.class,
SensiblePropertiesConfiguration.class)
.withPropertyValues("sensible.listItems[0].custom=my-value")
.run(assertProperties("sensible", (properties) -> {
assertThat(properties.get("listItems")).isInstanceOf(List.class);
List<Object> list = (List<Object>) properties.get("listItems");
assertThat(list).hasSize(1);
Map<String, Object> item = (Map<String, Object>) list.get(0);
assertThat(item.get("custom")).isEqualTo("$$$");
}, (inputs) -> {
List<Object> list = (List<Object>) inputs.get("listItems");
assertThat(list).hasSize(1);
Map<String, Object> item = (Map<String, Object>) list.get(0);
Map<String, Object> somePassword = (Map<String, Object>) item.get("custom");
assertThat(somePassword.get("value")).isEqualTo("$$$");
assertThat(somePassword.get("origin"))
.isEqualTo("\"sensible.listItems[0].custom\" from property source \"test\"");
}));
}
@Test @Test
void originParents() { void originParents() {
this.contextRunner.withUserConfiguration(SensiblePropertiesConfiguration.class) this.contextRunner.withUserConfiguration(SensiblePropertiesConfiguration.class)
@ -778,6 +824,8 @@ class ConfigurationPropertiesReportEndpointTests {
private String somePassword = "secret"; private String somePassword = "secret";
private String custom;
public String getSomePassword() { public String getSomePassword() {
return this.somePassword; return this.somePassword;
} }
@ -786,6 +834,60 @@ class ConfigurationPropertiesReportEndpointTests {
this.somePassword = somePassword; this.somePassword = somePassword;
} }
public String getCustom() {
return this.custom;
}
public void setCustom(String custom) {
this.custom = custom;
}
}
}
@Configuration(proxyBeanMethods = false)
static class CustomSanitizingEndpointConfig {
@Bean
ConfigurationPropertiesReportEndpoint endpoint(Environment environment, SanitizingFunction sanitizingFunction) {
ConfigurationPropertiesReportEndpoint endpoint = new ConfigurationPropertiesReportEndpoint(
Collections.singletonList(sanitizingFunction));
String[] keys = environment.getProperty("test.keys-to-sanitize", String[].class);
if (keys != null) {
endpoint.setKeysToSanitize(keys);
}
return endpoint;
}
}
@Configuration(proxyBeanMethods = false)
static class SanitizingFunctionConfiguration {
@Bean
SanitizingFunction testSanitizingFunction() {
return (data) -> {
if (data.getKey().contains("custom") || data.getKey().contains("test")) {
return data.withValue("$$$");
}
return data;
};
}
}
@Configuration(proxyBeanMethods = false)
static class PropertySourceBasedSanitizingFunctionConfiguration {
@Bean
SanitizingFunction testSanitizingFunction() {
return (data) -> {
if (data.getPropertySource() != null && data.getPropertySource().getName().startsWith("test")) {
return data.withValue("$$$");
}
return data;
};
} }
} }

@ -16,6 +16,7 @@
package org.springframework.boot.actuate.endpoint; package org.springframework.boot.actuate.endpoint;
import java.util.Collections;
import java.util.stream.Stream; import java.util.stream.Stream;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@ -31,6 +32,7 @@ import static org.assertj.core.api.Assertions.assertThat;
* @author Stephane Nicoll * @author Stephane Nicoll
* @author Chris Bono * @author Chris Bono
* @author David Good * @author David Good
* @author Madhura Bhave
*/ */
class SanitizerTests { class SanitizerTests {
@ -65,6 +67,22 @@ class SanitizerTests {
assertThat(sanitizer.sanitize("private", "secret")).isEqualTo("secret"); assertThat(sanitizer.sanitize("private", "secret")).isEqualTo("secret");
} }
@Test
void whenCustomSanitizingFunctionPresentValueShouldBeSanitized() {
Sanitizer sanitizer = new Sanitizer(Collections.singletonList((data) -> {
if (data.getKey().equals("custom")) {
return data.withValue("$$$$$$");
}
return data;
}));
SanitizableData secret = new SanitizableData(null, "secret", "xyz");
assertThat(sanitizer.sanitize(secret)).isEqualTo("******");
SanitizableData custom = new SanitizableData(null, "custom", "abcde");
assertThat(sanitizer.sanitize(custom)).isEqualTo("$$$$$$");
SanitizableData hello = new SanitizableData(null, "hello", "abc");
assertThat(sanitizer.sanitize(hello)).isEqualTo("abc");
}
@ParameterizedTest(name = "key = {0}") @ParameterizedTest(name = "key = {0}")
@MethodSource("matchingUriUserInfoKeys") @MethodSource("matchingUriUserInfoKeys")
void uriWithSingleValueWithPasswordShouldBeSanitized(String key) { void uriWithSingleValueWithPasswordShouldBeSanitized(String key) {

@ -164,6 +164,28 @@ class EnvironmentEndpointTests {
}); });
} }
@Test
void keysMatchingCustomSanitizingFunctionHaveTheirValuesSanitized() {
ConfigurableEnvironment environment = new StandardEnvironment();
TestPropertyValues.of("other.service=abcde").applyTo(environment);
TestPropertyValues.of("system.service=123456").applyToSystemProperties(() -> {
EnvironmentDescriptor descriptor = new EnvironmentEndpoint(environment,
Collections.singletonList((data) -> {
String name = data.getPropertySource().getName();
if (name.equals(StandardEnvironment.SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME)) {
return data.withValue("******");
}
return data;
})).environment(null);
assertThat(propertySources(descriptor).get("test").getProperties().get("other.service").getValue())
.isEqualTo("abcde");
Map<String, PropertyValueDescriptor> systemProperties = propertySources(descriptor).get("systemProperties")
.getProperties();
assertThat(systemProperties.get("system.service").getValue()).isEqualTo("******");
return null;
});
}
@Test @Test
void propertyWithPlaceholderResolved() { void propertyWithPlaceholderResolved() {
ConfigurableEnvironment environment = emptyEnvironment(); ConfigurableEnvironment environment = emptyEnvironment();
@ -199,6 +221,17 @@ class EnvironmentEndpointTests {
.isEqualTo("http://${bar.password}://hello"); .isEqualTo("http://${bar.password}://hello");
} }
@Test
void propertyWithSensitivePlaceholderWithCustomFunctionResolved() {
ConfigurableEnvironment environment = emptyEnvironment();
TestPropertyValues.of("my.foo: http://${bar.password}://hello", "bar.password: hello").applyTo(environment);
EnvironmentDescriptor descriptor = new EnvironmentEndpoint(environment,
Collections.singletonList((data) -> data.withValue(data.getPropertySource().getName() + "******")))
.environment(null);
assertThat(propertySources(descriptor).get("test").getProperties().get("my.foo").getValue())
.isEqualTo("test******");
}
@Test @Test
void propertyWithComplexTypeShouldNotFail() { void propertyWithComplexTypeShouldNotFail() {
ConfigurableEnvironment environment = emptyEnvironment(); ConfigurableEnvironment environment = emptyEnvironment();

@ -38,20 +38,46 @@ public final class ConfigurationProperty implements OriginProvider, Comparable<C
private final Object value; private final Object value;
private final ConfigurationPropertySource source;
private final Origin origin; private final Origin origin;
public ConfigurationProperty(ConfigurationPropertyName name, Object value, Origin origin) { public ConfigurationProperty(ConfigurationPropertyName name, Object value, Origin origin) {
this(null, name, value, origin);
}
private ConfigurationProperty(ConfigurationPropertySource source, ConfigurationPropertyName name, Object value,
Origin origin) {
Assert.notNull(name, "Name must not be null"); Assert.notNull(name, "Name must not be null");
Assert.notNull(value, "Value must not be null"); Assert.notNull(value, "Value must not be null");
this.source = source;
this.name = name; this.name = name;
this.value = value; this.value = value;
this.origin = origin; this.origin = origin;
} }
/**
* Return the {@link ConfigurationPropertySource} that provided the property or
* {@code null} if the source is unknown.
* @return the configuration property source
* @since 2.6.0
*/
public ConfigurationPropertySource getSource() {
return this.source;
}
/**
* Return the name of the configuration property.
* @return the configuration property name
*/
public ConfigurationPropertyName getName() { public ConfigurationPropertyName getName() {
return this.name; return this.name;
} }
/**
* Return the value of the configuration property.
* @return the configuration property value
*/
public Object getValue() { public Object getValue() {
return this.value; return this.value;
} }
@ -101,11 +127,12 @@ public final class ConfigurationProperty implements OriginProvider, Comparable<C
return new ConfigurationProperty(name, value.getValue(), value.getOrigin()); return new ConfigurationProperty(name, value.getValue(), value.getOrigin());
} }
static ConfigurationProperty of(ConfigurationPropertyName name, Object value, Origin origin) { static ConfigurationProperty of(ConfigurationPropertySource source, ConfigurationPropertyName name, Object value,
Origin origin) {
if (value == null) { if (value == null) {
return null; return null;
} }
return new ConfigurationProperty(name, value, origin); return new ConfigurationProperty(source, name, value, origin);
} }
} }

@ -46,7 +46,8 @@ class PrefixedConfigurationPropertySource implements ConfigurationPropertySource
if (configurationProperty == null) { if (configurationProperty == null) {
return null; return null;
} }
return ConfigurationProperty.of(name, configurationProperty.getValue(), configurationProperty.getOrigin()); return ConfigurationProperty.of(configurationProperty.getSource(), name, configurationProperty.getValue(),
configurationProperty.getOrigin());
} }
private ConfigurationPropertyName getPrefixedName(ConfigurationPropertyName name) { private ConfigurationPropertyName getPrefixedName(ConfigurationPropertyName name) {

@ -83,8 +83,8 @@ class SpringConfigurationPropertySource implements ConfigurationPropertySource {
for (String candidate : mapper.map(name)) { for (String candidate : mapper.map(name)) {
Object value = getPropertySource().getProperty(candidate); Object value = getPropertySource().getProperty(candidate);
if (value != null) { if (value != null) {
Origin origin = PropertySourceOrigin.get(getPropertySource(), candidate); Origin origin = PropertySourceOrigin.get(this.propertySource, candidate);
return ConfigurationProperty.of(name, value, origin); return ConfigurationProperty.of(this, name, value, origin);
} }
} }
} }

@ -105,7 +105,7 @@ class SpringIterableConfigurationPropertySource extends SpringConfigurationPrope
Object value = getPropertySource().getProperty(candidate); Object value = getPropertySource().getProperty(candidate);
if (value != null) { if (value != null) {
Origin origin = PropertySourceOrigin.get(getPropertySource(), candidate); Origin origin = PropertySourceOrigin.get(getPropertySource(), candidate);
return ConfigurationProperty.of(name, value, origin); return ConfigurationProperty.of(this, name, value, origin);
} }
} }
return null; return null;

@ -20,6 +20,7 @@ import org.junit.jupiter.api.Test;
import org.springframework.boot.origin.Origin; import org.springframework.boot.origin.Origin;
import org.springframework.boot.origin.OriginProvider; import org.springframework.boot.origin.OriginProvider;
import org.springframework.core.env.PropertySource;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
@ -35,6 +36,8 @@ class ConfigurationPropertyTests {
private static final ConfigurationPropertyName NAME = ConfigurationPropertyName.of("foo"); private static final ConfigurationPropertyName NAME = ConfigurationPropertyName.of("foo");
private ConfigurationPropertySource source = ConfigurationPropertySource.from(mock(PropertySource.class));
@Test @Test
void createWhenNameIsNullShouldThrowException() { void createWhenNameIsNullShouldThrowException() {
assertThatIllegalArgumentException().isThrownBy(() -> new ConfigurationProperty(null, "bar", null)) assertThatIllegalArgumentException().isThrownBy(() -> new ConfigurationProperty(null, "bar", null))
@ -49,23 +52,30 @@ class ConfigurationPropertyTests {
@Test @Test
void getNameShouldReturnName() { void getNameShouldReturnName() {
ConfigurationProperty property = ConfigurationProperty.of(NAME, "foo", null); ConfigurationProperty property = ConfigurationProperty.of(this.source, NAME, "foo", null);
assertThat((Object) property.getName()).isEqualTo(NAME); assertThat((Object) property.getName()).isEqualTo(NAME);
} }
@Test @Test
void getValueShouldReturnValue() { void getValueShouldReturnValue() {
ConfigurationProperty property = ConfigurationProperty.of(NAME, "foo", null); ConfigurationProperty property = ConfigurationProperty.of(this.source, NAME, "foo", null);
assertThat(property.getValue()).isEqualTo("foo"); assertThat(property.getValue()).isEqualTo("foo");
} }
@Test @Test
void getPropertyOriginShouldReturnValuePropertyOrigin() { void getPropertyOriginShouldReturnValuePropertyOrigin() {
Origin origin = mock(Origin.class); Origin origin = mock(Origin.class);
OriginProvider property = ConfigurationProperty.of(NAME, "foo", origin); OriginProvider property = ConfigurationProperty.of(this.source, NAME, "foo", origin);
assertThat(property.getOrigin()).isEqualTo(origin); assertThat(property.getOrigin()).isEqualTo(origin);
} }
@Test
void getPropertySourceShouldReturnPropertySource() {
Origin origin = mock(Origin.class);
ConfigurationProperty property = ConfigurationProperty.of(this.source, NAME, "foo", origin);
assertThat(property.getSource()).isEqualTo(this.source);
}
@Test @Test
void equalsAndHashCode() { void equalsAndHashCode() {
ConfigurationProperty property1 = new ConfigurationProperty(ConfigurationPropertyName.of("foo"), "bar", null); ConfigurationProperty property1 = new ConfigurationProperty(ConfigurationPropertyName.of("foo"), "bar", null);
@ -78,7 +88,7 @@ class ConfigurationPropertyTests {
@Test @Test
void toStringShouldReturnValue() { void toStringShouldReturnValue() {
ConfigurationProperty property = ConfigurationProperty.of(NAME, "foo", null); ConfigurationProperty property = ConfigurationProperty.of(this.source, NAME, "foo", null);
assertThat(property.toString()).contains("name").contains("value"); assertThat(property.toString()).contains("name").contains("value");
} }

@ -61,7 +61,7 @@ class SpringConfigurationPropertySourceTests {
} }
@Test @Test
void getValueOrigin() { void getValueOriginAndPropertySource() {
Map<String, Object> source = new LinkedHashMap<>(); Map<String, Object> source = new LinkedHashMap<>();
source.put("key", "value"); source.put("key", "value");
PropertySource<?> propertySource = new MapPropertySource("test", source); PropertySource<?> propertySource = new MapPropertySource("test", source);
@ -69,8 +69,9 @@ class SpringConfigurationPropertySourceTests {
ConfigurationPropertyName name = ConfigurationPropertyName.of("my.key"); ConfigurationPropertyName name = ConfigurationPropertyName.of("my.key");
mapper.addFromConfigurationProperty(name, "key"); mapper.addFromConfigurationProperty(name, "key");
SpringConfigurationPropertySource adapter = new SpringConfigurationPropertySource(propertySource, mapper); SpringConfigurationPropertySource adapter = new SpringConfigurationPropertySource(propertySource, mapper);
assertThat(adapter.getConfigurationProperty(name).getOrigin().toString()) ConfigurationProperty configurationProperty = adapter.getConfigurationProperty(name);
.isEqualTo("\"key\" from property source \"test\""); assertThat(configurationProperty.getOrigin().toString()).isEqualTo("\"key\" from property source \"test\"");
assertThat(configurationProperty.getSource()).isEqualTo(adapter);
} }
@Test @Test

Loading…
Cancel
Save