Skip to content

Commit 253f98c

Browse files
committed
Add pluggable abstraction for applying custom sanitization rules
Closes gh-27840
1 parent 211532f commit 253f98c

File tree

18 files changed

+537
-47
lines changed

18 files changed

+537
-47
lines changed

spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/context/properties/ConfigurationPropertiesReportEndpointAutoConfiguration.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,11 @@
1616

1717
package org.springframework.boot.actuate.autoconfigure.context.properties;
1818

19+
import org.springframework.beans.factory.ObjectProvider;
1920
import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnAvailableEndpoint;
2021
import org.springframework.boot.actuate.context.properties.ConfigurationPropertiesReportEndpoint;
2122
import org.springframework.boot.actuate.context.properties.ConfigurationPropertiesReportEndpointWebExtension;
23+
import org.springframework.boot.actuate.endpoint.SanitizingFunction;
2224
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
2325
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
2426
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
@@ -43,8 +45,9 @@ public class ConfigurationPropertiesReportEndpointAutoConfiguration {
4345
@Bean
4446
@ConditionalOnMissingBean
4547
public ConfigurationPropertiesReportEndpoint configurationPropertiesReportEndpoint(
46-
ConfigurationPropertiesReportEndpointProperties properties) {
47-
ConfigurationPropertiesReportEndpoint endpoint = new ConfigurationPropertiesReportEndpoint();
48+
ConfigurationPropertiesReportEndpointProperties properties,
49+
ObjectProvider<SanitizingFunction> sanitizingFunctions) {
50+
ConfigurationPropertiesReportEndpoint endpoint = new ConfigurationPropertiesReportEndpoint(sanitizingFunctions);
4851
String[] keysToSanitize = properties.getKeysToSanitize();
4952
if (keysToSanitize != null) {
5053
endpoint.setKeysToSanitize(keysToSanitize);

spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/env/EnvironmentEndpointAutoConfiguration.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@
1616

1717
package org.springframework.boot.actuate.autoconfigure.env;
1818

19+
import org.springframework.beans.factory.ObjectProvider;
1920
import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnAvailableEndpoint;
21+
import org.springframework.boot.actuate.endpoint.SanitizingFunction;
2022
import org.springframework.boot.actuate.env.EnvironmentEndpoint;
2123
import org.springframework.boot.actuate.env.EnvironmentEndpointWebExtension;
2224
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
@@ -41,8 +43,9 @@ public class EnvironmentEndpointAutoConfiguration {
4143

4244
@Bean
4345
@ConditionalOnMissingBean
44-
public EnvironmentEndpoint environmentEndpoint(Environment environment, EnvironmentEndpointProperties properties) {
45-
EnvironmentEndpoint endpoint = new EnvironmentEndpoint(environment);
46+
public EnvironmentEndpoint environmentEndpoint(Environment environment, EnvironmentEndpointProperties properties,
47+
ObjectProvider<SanitizingFunction> sanitizingFunctions) {
48+
EnvironmentEndpoint endpoint = new EnvironmentEndpoint(environment, sanitizingFunctions);
4649
String[] keysToSanitize = properties.getKeysToSanitize();
4750
if (keysToSanitize != null) {
4851
endpoint.setKeysToSanitize(keysToSanitize);

spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/context/properties/ConfigurationPropertiesReportEndpointAutoConfigurationTests.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222

2323
import org.springframework.boot.actuate.context.properties.ConfigurationPropertiesReportEndpoint;
2424
import org.springframework.boot.actuate.context.properties.ConfigurationPropertiesReportEndpoint.ApplicationConfigurationProperties;
25+
import org.springframework.boot.actuate.endpoint.SanitizingFunction;
2526
import org.springframework.boot.autoconfigure.AutoConfigurations;
2627
import org.springframework.boot.context.properties.ConfigurationProperties;
2728
import org.springframework.boot.context.properties.EnableConfigurationProperties;
@@ -72,6 +73,14 @@ void additionalKeysToSanitizeCanBeConfiguredViaTheEnvironment() {
7273
.run(validateTestProperties("******", "******"));
7374
}
7475

76+
@Test
77+
void customSanitizingFunctionShouldBeApplied() {
78+
this.contextRunner.withUserConfiguration(Config.class, SanitizingFunctionConfiguration.class)
79+
.withPropertyValues("management.endpoints.web.exposure.include=configprops",
80+
"test.my-test-property=abc")
81+
.run(validateTestProperties("******", "$$$"));
82+
}
83+
7584
@Test
7685
void runWhenNotExposedShouldNotHaveEndpointBean() {
7786
this.contextRunner
@@ -129,4 +138,19 @@ public void setMyTestProperty(String myTestProperty) {
129138

130139
}
131140

141+
@Configuration(proxyBeanMethods = false)
142+
static class SanitizingFunctionConfiguration {
143+
144+
@Bean
145+
SanitizingFunction testSanitizingFunction() {
146+
return (data) -> {
147+
if (data.getKey().contains("my")) {
148+
return data.withValue("$$$");
149+
}
150+
return data;
151+
};
152+
}
153+
154+
}
155+
132156
}

spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/env/EnvironmentEndpointAutoConfigurationTests.java

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
import org.junit.jupiter.api.Test;
2222

23+
import org.springframework.boot.actuate.endpoint.SanitizingFunction;
2324
import org.springframework.boot.actuate.env.EnvironmentEndpoint;
2425
import org.springframework.boot.actuate.env.EnvironmentEndpoint.EnvironmentDescriptor;
2526
import org.springframework.boot.actuate.env.EnvironmentEndpoint.PropertySourceDescriptor;
@@ -28,6 +29,8 @@
2829
import org.springframework.boot.test.context.assertj.AssertableApplicationContext;
2930
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
3031
import org.springframework.boot.test.context.runner.ContextConsumer;
32+
import org.springframework.context.annotation.Bean;
33+
import org.springframework.context.annotation.Configuration;
3134

3235
import static org.assertj.core.api.Assertions.assertThat;
3336

@@ -67,6 +70,21 @@ void keysToSanitizeCanBeConfiguredViaTheEnvironment() {
6770
.run(validateSystemProperties("******", "123456"));
6871
}
6972

73+
@Test
74+
void sanitizingFunctionsCanBeConfiguredViaTheEnvironment() {
75+
this.contextRunner.withUserConfiguration(SanitizingFunctionConfiguration.class)
76+
.withPropertyValues("management.endpoints.web.exposure.include=env")
77+
.withSystemProperties("custom=123456", "password=123456").run((context) -> {
78+
assertThat(context).hasSingleBean(EnvironmentEndpoint.class);
79+
EnvironmentEndpoint endpoint = context.getBean(EnvironmentEndpoint.class);
80+
EnvironmentDescriptor env = endpoint.environment(null);
81+
Map<String, PropertyValueDescriptor> systemProperties = getSource("systemProperties", env)
82+
.getProperties();
83+
assertThat(systemProperties.get("custom").getValue()).isEqualTo("$$$");
84+
assertThat(systemProperties.get("password").getValue()).isEqualTo("******");
85+
});
86+
}
87+
7088
@Test
7189
void additionalKeysToSanitizeCanBeConfiguredViaTheEnvironment() {
7290
this.contextRunner.withPropertyValues("management.endpoints.web.exposure.include=env")
@@ -91,4 +109,14 @@ private PropertySourceDescriptor getSource(String name, EnvironmentDescriptor de
91109
.get();
92110
}
93111

112+
@Configuration(proxyBeanMethods = false)
113+
static class SanitizingFunctionConfiguration {
114+
115+
@Bean
116+
SanitizingFunction testSanitizingFunction() {
117+
return (data) -> data.withValue("$$$");
118+
}
119+
120+
}
121+
94122
}

spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpoint.java

Lines changed: 67 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,9 @@
5353

5454
import org.springframework.beans.BeanUtils;
5555
import org.springframework.beans.BeansException;
56+
import org.springframework.boot.actuate.endpoint.SanitizableData;
5657
import org.springframework.boot.actuate.endpoint.Sanitizer;
58+
import org.springframework.boot.actuate.endpoint.SanitizingFunction;
5759
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
5860
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
5961
import org.springframework.boot.actuate.endpoint.annotation.Selector;
@@ -64,6 +66,7 @@
6466
import org.springframework.boot.context.properties.bind.Name;
6567
import org.springframework.boot.context.properties.source.ConfigurationProperty;
6668
import org.springframework.boot.context.properties.source.ConfigurationPropertyName;
69+
import org.springframework.boot.context.properties.source.ConfigurationPropertySource;
6770
import org.springframework.boot.origin.Origin;
6871
import org.springframework.context.ApplicationContext;
6972
import org.springframework.context.ApplicationContextAware;
@@ -73,6 +76,7 @@
7376
import org.springframework.core.annotation.MergedAnnotation;
7477
import org.springframework.core.annotation.MergedAnnotations;
7578
import org.springframework.core.annotation.MergedAnnotations.SearchStrategy;
79+
import org.springframework.core.env.PropertySource;
7680
import org.springframework.util.ClassUtils;
7781
import org.springframework.util.StringUtils;
7882

@@ -100,12 +104,20 @@ public class ConfigurationPropertiesReportEndpoint implements ApplicationContext
100104

101105
private static final String CONFIGURATION_PROPERTIES_FILTER_ID = "configurationPropertiesFilter";
102106

103-
private final Sanitizer sanitizer = new Sanitizer();
107+
private final Sanitizer sanitizer;
104108

105109
private ApplicationContext context;
106110

107111
private ObjectMapper objectMapper;
108112

113+
public ConfigurationPropertiesReportEndpoint() {
114+
this(Collections.emptyList());
115+
}
116+
117+
public ConfigurationPropertiesReportEndpoint(Iterable<SanitizingFunction> sanitizingFunctions) {
118+
this.sanitizer = new Sanitizer(sanitizingFunctions);
119+
}
120+
109121
@Override
110122
public void setApplicationContext(ApplicationContext context) throws BeansException {
111123
this.context = context;
@@ -236,26 +248,63 @@ else if (value instanceof List) {
236248
map.put(key, sanitize(qualifiedKey, (List<Object>) value));
237249
}
238250
else {
239-
value = this.sanitizer.sanitize(key, value);
240-
value = this.sanitizer.sanitize(qualifiedKey, value);
241-
map.put(key, value);
251+
map.put(key, sanitizeWithPropertySourceIfPresent(qualifiedKey, value));
242252
}
243253
});
244254
return map;
245255
}
246256

257+
private Object sanitizeWithPropertySourceIfPresent(String qualifiedKey, Object value) {
258+
ConfigurationPropertyName currentName = getCurrentName(qualifiedKey);
259+
ConfigurationProperty candidate = getCandidate(currentName);
260+
PropertySource<?> propertySource = getPropertySource(candidate);
261+
if (propertySource != null) {
262+
SanitizableData data = new SanitizableData(propertySource, qualifiedKey, value);
263+
return this.sanitizer.sanitize(data);
264+
}
265+
SanitizableData data = new SanitizableData(null, qualifiedKey, value);
266+
return this.sanitizer.sanitize(data);
267+
}
268+
269+
private PropertySource<?> getPropertySource(ConfigurationProperty configurationProperty) {
270+
if (configurationProperty == null) {
271+
return null;
272+
}
273+
ConfigurationPropertySource source = configurationProperty.getSource();
274+
Object underlyingSource = (source != null) ? source.getUnderlyingSource() : null;
275+
return (underlyingSource instanceof PropertySource<?>) ? (PropertySource<?>) underlyingSource : null;
276+
}
277+
278+
private ConfigurationPropertyName getCurrentName(String qualifiedKey) {
279+
return ConfigurationPropertyName.adapt(qualifiedKey, '.');
280+
}
281+
282+
private ConfigurationProperty getCandidate(ConfigurationPropertyName currentName) {
283+
BoundConfigurationProperties bound = BoundConfigurationProperties.get(this.context);
284+
if (bound == null) {
285+
return null;
286+
}
287+
ConfigurationProperty candidate = bound.get(currentName);
288+
if (candidate == null && currentName.isLastElementIndexed()) {
289+
candidate = bound.get(currentName.chop(currentName.getNumberOfElements() - 1));
290+
}
291+
return candidate;
292+
}
293+
247294
@SuppressWarnings("unchecked")
248295
private List<Object> sanitize(String prefix, List<Object> list) {
249296
List<Object> sanitized = new ArrayList<>();
297+
int index = 0;
250298
for (Object item : list) {
299+
String name = prefix + "[" + index++ + "]";
251300
if (item instanceof Map) {
252-
sanitized.add(sanitize(prefix, (Map<String, Object>) item));
301+
sanitized.add(sanitize(name, (Map<String, Object>) item));
253302
}
254303
else if (item instanceof List) {
255-
sanitized.add(sanitize(prefix, (List<Object>) item));
304+
sanitized.add(sanitize(name, (List<Object>) item));
256305
}
257306
else {
258-
sanitized.add(this.sanitizer.sanitize(prefix, item));
307+
sanitized.add(sanitizeWithPropertySourceIfPresent(name, item));
259308
}
260309
}
261310
return sanitized;
@@ -299,24 +348,22 @@ else if (item instanceof List) {
299348
}
300349

301350
private Map<String, Object> applyInput(String qualifiedKey) {
302-
BoundConfigurationProperties bound = BoundConfigurationProperties.get(this.context);
303-
if (bound == null) {
304-
return Collections.emptyMap();
305-
}
306-
ConfigurationPropertyName currentName = ConfigurationPropertyName.adapt(qualifiedKey, '.');
307-
ConfigurationProperty candidate = bound.get(currentName);
308-
if (candidate == null && currentName.isLastElementIndexed()) {
309-
candidate = bound.get(currentName.chop(currentName.getNumberOfElements() - 1));
310-
}
311-
return (candidate != null) ? getInput(currentName.toString(), candidate) : Collections.emptyMap();
351+
ConfigurationPropertyName currentName = getCurrentName(qualifiedKey);
352+
ConfigurationProperty candidate = getCandidate(currentName);
353+
PropertySource<?> propertySource = getPropertySource(candidate);
354+
if (propertySource != null) {
355+
Object value = stringifyIfNecessary(candidate.getValue());
356+
SanitizableData data = new SanitizableData(propertySource, currentName.toString(), value);
357+
return getInput(candidate, this.sanitizer.sanitize(data));
358+
}
359+
return Collections.emptyMap();
312360
}
313361

314-
private Map<String, Object> getInput(String property, ConfigurationProperty candidate) {
362+
private Map<String, Object> getInput(ConfigurationProperty candidate, Object sanitizedValue) {
315363
Map<String, Object> input = new LinkedHashMap<>();
316-
Object value = stringifyIfNecessary(candidate.getValue());
317364
Origin origin = Origin.from(candidate);
318365
List<Origin> originParents = Origin.parentsFrom(candidate);
319-
input.put("value", this.sanitizer.sanitize(property, value));
366+
input.put("value", sanitizedValue);
320367
input.put("origin", (origin != null) ? origin.toString() : "none");
321368
if (!originParents.isEmpty()) {
322369
input.put("originParents", originParents.stream().map(Object::toString).toArray(String[]::new));
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/*
2+
* Copyright 2012-2021 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.actuate.endpoint;
18+
19+
import org.springframework.core.env.PropertySource;
20+
21+
/**
22+
* Value object that represents the data that can be used by a {@link SanitizingFunction}.
23+
*
24+
* @author Madhura Bhave
25+
* @since 2.6.0
26+
**/
27+
public final class SanitizableData {
28+
29+
/**
30+
* Represents a sanitized value.
31+
*/
32+
public static final String SANITIZED_VALUE = "******";
33+
34+
private final PropertySource<?> propertySource;
35+
36+
private final String key;
37+
38+
private final Object value;
39+
40+
/**
41+
* Create a new {@link SanitizableData} instance.
42+
* @param propertySource the property source that provided the data or {@code null}.
43+
* @param key the data key
44+
* @param value the data value
45+
*/
46+
public SanitizableData(PropertySource<?> propertySource, String key, Object value) {
47+
this.propertySource = propertySource;
48+
this.key = key;
49+
this.value = value;
50+
}
51+
52+
/**
53+
* Return the property source that provided the data or {@code null} If the data was
54+
* not from a {@link PropertySource}.
55+
* @return the property source that provided the data
56+
*/
57+
public PropertySource<?> getPropertySource() {
58+
return this.propertySource;
59+
}
60+
61+
/**
62+
* Return the key of the data.
63+
* @return the data key
64+
*/
65+
public String getKey() {
66+
return this.key;
67+
}
68+
69+
/**
70+
* Return the value of the data.
71+
* @return the data value
72+
*/
73+
public Object getValue() {
74+
return this.value;
75+
}
76+
77+
/**
78+
* Return a new {@link SanitizableData} instance with a different value.
79+
* @param value the new value (often {@link #SANITIZED_VALUE}
80+
* @return a new sanitizable data instance
81+
*/
82+
public SanitizableData withValue(Object value) {
83+
return new SanitizableData(this.propertySource, this.key, value);
84+
}
85+
86+
}

0 commit comments

Comments
 (0)