From 3eee1f1ad01a1a02042f5604d193237356c01a8e Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Tue, 11 Feb 2025 18:43:38 -0800 Subject: [PATCH] Add convenience methods to `SanitizingFunction` Add a `sanitizeValue()` factory method and `if...` methods that can be used to quickly build a `SanitizingFunction`. Closes gh-39243 --- .../actuate/endpoint/SanitizableData.java | 18 + .../boot/actuate/endpoint/Sanitizer.java | 2 +- .../actuate/endpoint/SanitizingFunction.java | 398 ++++++++++++++++++ .../boot/actuate/endpoint/SanitizerTests.java | 9 + .../endpoint/SanitizingFunctionTests.java | 338 +++++++++++++++ 5 files changed, 764 insertions(+), 1 deletion(-) create mode 100644 spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/SanitizingFunctionTests.java diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/SanitizableData.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/SanitizableData.java index eaaa15c54d..696e3b96e3 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/SanitizableData.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/SanitizableData.java @@ -16,6 +16,8 @@ package org.springframework.boot.actuate.endpoint; +import java.util.Locale; + import org.springframework.core.env.PropertySource; /** @@ -36,6 +38,8 @@ public final class SanitizableData { private final String key; + private String lowerCaseKey; + private final Object value; /** @@ -67,6 +71,20 @@ public final class SanitizableData { return this.key; } + /** + * Return the key as a lowercase value. + * @return the key as a lowercase value + * @since 3.5.0 + */ + public String getLowerCaseKey() { + String result = this.lowerCaseKey; + if (result == null && this.key != null) { + result = this.key.toLowerCase(Locale.getDefault()); + this.lowerCaseKey = result; + } + return result; + } + /** * Return the value of the data. * @return the data value diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/Sanitizer.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/Sanitizer.java index 3ad0497f79..76ddf4a10a 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/Sanitizer.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/Sanitizer.java @@ -72,7 +72,7 @@ public class Sanitizer { return SanitizableData.SANITIZED_VALUE; } for (SanitizingFunction sanitizingFunction : this.sanitizingFunctions) { - data = sanitizingFunction.apply(data); + data = sanitizingFunction.applyUnlessFiltered(data); Object sanitizedValue = data.getValue(); if (!value.equals(sanitizedValue)) { return sanitizedValue; diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/SanitizingFunction.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/SanitizingFunction.java index 3d9ffecefe..bc47f686c8 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/SanitizingFunction.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/SanitizingFunction.java @@ -16,12 +16,27 @@ package org.springframework.boot.actuate.endpoint; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; +import java.util.function.BiPredicate; +import java.util.function.Predicate; +import java.util.regex.Pattern; + +import org.springframework.util.Assert; + /** * 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. + *

+ * This interface also provides convenience methods that can help build a + * {@link SanitizingFunction} instances, for example to return from a {@code @Bean} + * method. See {@link #sanitizeValue()} for an example. * * @author Madhura Bhave + * @author Phillip Webb * @since 2.6.0 + * @see Sanitizer */ @FunctionalInterface public interface SanitizingFunction { @@ -33,4 +48,387 @@ public interface SanitizingFunction { */ SanitizableData apply(SanitizableData data); + /** + * Return an optional filter that determines if the sanitizing function applies. + * @return a predicate used to filter functions or {@code null} if no filter is + * declared + * @since 3.5.0 + * @see #applyUnlessFiltered(SanitizableData) + */ + default Predicate filter() { + return null; + } + + /** + * Apply the sanitizing function as long as the filter passes or there is no filter. + * @param data the data to sanitize + * @return the sanitized data or the original instance is no sanitization is applied + * @since 3.5.0 + */ + default SanitizableData applyUnlessFiltered(SanitizableData data) { + return (filter() == null || filter().test(data)) ? apply(data) : data; + } + + /** + * Return a new function with a filter that also applies if the data is + * likely to contain a sensitive value. This method can help construct a useful + * sanitizing function, but may not catch all sensitive data so care should be taken + * to test the results for your specific environment. + * @return a new sanitizing function with an updated {@link #filter()} + * @since 3.5.0 + * @see #filter() + * @see #sanitizeValue() + */ + default SanitizingFunction ifLikelySenstive() { + return ifLikelyCredential().ifLikelyUri().ifLikelySenstiveEnvironmentVariable().ifVcapServices(); + } + + /** + * Return a new function with a filter that also applies if the data is + * likely to contain a credential. This method can help construct a useful sanitizing + * function, but may not catch all sensitive data so care should be taken to test the + * results for your specific environment. + * @return a new sanitizing function with an updated {@link #filter()} + * @since 3.5.0 + * @see #filter() + * @see #sanitizeValue() + */ + default SanitizingFunction ifLikelyCredential() { + return ifKeyEndsWith("password", "secret", "key", "token").ifKeyContains("credentials"); + } + + /** + * Return a new function with a filter that also applies if the data is + * likely to contain a URI. This method can help construct a useful sanitizing + * function, but may not catch all sensitive data so care should be taken to test the + * results for your specific environment. + * @return a new sanitizing function with an updated {@link #filter()} + * @since 3.5.0 + * @see #filter() + * @see #sanitizeValue() + */ + default SanitizingFunction ifLikelyUri() { + return ifKeyEndsWith("uri", "uris", "url", "urls", "address", "addresses"); + } + + /** + * Return a new function with a filter that also applies if the data is + * likely to sensitive environment variable value. This method can help construct a + * useful sanitizing function, but may not catch all sensitive data so care should be + * taken to test the results for your specific environment. + * @return a new sanitizing function with an updated {@link #filter()} + * @since 3.5.0 + * @see #filter() + * @see #sanitizeValue() + */ + default SanitizingFunction ifLikelySenstiveEnvironmentVariable() { + return ifKeyMatches("sun.java.command", "^spring[._]application[._]json$"); + } + + /** + * Return a new function with a filter that also applies if the data is for + * VCAP services. + * @return a new sanitizing function with an updated {@link #filter()} + * @since 3.5.0 + * @see #filter() + * @see #sanitizeValue() + */ + + default SanitizingFunction ifVcapServices() { + return ifKeyEquals("vcap_services").ifKeyMatches("^vcap\\.services.*$"); + } + + /** + * Return a new function with a filter that also applies if the data key is + * equal to any of the given values (ignoring case). + * @param values the case insensitive values that the key can equal + * @return a new sanitizing function with an updated {@link #filter()} + * @since 3.5.0 + * @see #filter() + * @see #sanitizeValue() + */ + default SanitizingFunction ifKeyEquals(String... values) { + Assert.notNull(values, "'values' must not be null"); + return ifKeyMatchesIgnoringCase(String::equals, values); + } + + /** + * Return a new function with a filter that also applies if the data key ends + * with any of the given values (ignoring case). + * @param suffixes the case insensitive suffixes that they key can end with + * @return a new sanitizing function with an updated {@link #filter()} + * @since 3.5.0 + * @see #filter() + * @see #sanitizeValue() + */ + default SanitizingFunction ifKeyEndsWith(String... suffixes) { + Assert.notNull(suffixes, "'suffixes' must not be null"); + return ifKeyMatchesIgnoringCase(String::endsWith, suffixes); + } + + /** + * Return a new function with a filter that also applies if the data key + * contains any of the given values (ignoring case). + * @param values the case insensitive values that the key can contain + * @return a new sanitizing function with an updated {@link #filter()} + * @since 3.5.0 + * @see #filter() + * @see #sanitizeValue() + */ + default SanitizingFunction ifKeyContains(String... values) { + Assert.notNull(values, "'values' must not be null"); + return ifKeyMatchesIgnoringCase(String::contains, values); + } + + /** + * Return a new function with a filter that also applies if the data key and + * any of the values match the given predicate. The predicate is only called with + * lower case values. + * @param predicate the predicate used to check the key against a value. The key is + * the first argument and the value is the second. Both are converted to lower case + * @param values the case insensitive values that the key can match + * @return a new sanitizing function with an updated {@link #filter()} + * @since 3.5.0 + * @see #filter() + * @see #sanitizeValue() + */ + default SanitizingFunction ifKeyMatchesIgnoringCase(BiPredicate predicate, String... values) { + Assert.notNull(predicate, "'predicate' must not be null"); + Assert.notNull(values, "'values' must not be null"); + return ifMatches(Arrays.stream(values).map((value) -> onKeyIgnoringCase(predicate, value)).toList()); + } + + /** + * Return a new function with a filter that also applies if the data key + * matches any of the given regex patterns (ignoring case). + * @param regexes the case insensitive regexes that the key can match + * @return a new sanitizing function with an updated {@link #filter()} + * @since 3.5.0 + * @see #filter() + * @see #sanitizeValue() + */ + default SanitizingFunction ifKeyMatches(String... regexes) { + Assert.notNull(regexes, "'regexes' must not be null"); + return ifKeyMatches(Arrays.stream(regexes).map(this::caseInsensitivePattern).toArray(Pattern[]::new)); + } + + /** + * Return a new function with a filter that also applies if the data key + * matches any of the given patterns. + * @param patterns the patterns that the key can match + * @return a new sanitizing function with an updated {@link #filter()} + * @since 3.5.0 + * @see #filter() + * @see #sanitizeValue() + */ + default SanitizingFunction ifKeyMatches(Pattern... patterns) { + Assert.notNull(patterns, "'patterns' must not be null"); + return ifKeyMatches(Arrays.stream(patterns).map(Pattern::asMatchPredicate).toList()); + } + + /** + * Return a new function with a filter that also applies if the data key + * matches any of the given predicates. + * @param predicates the predicates that the key can match + * @return a new sanitizing function with an updated {@link #filter()} + * @since 3.5.0 + * @see #filter() + * @see #sanitizeValue() + */ + default SanitizingFunction ifKeyMatches(List> predicates) { + Assert.notNull(predicates, "'predicates' must not be null"); + return ifMatches(predicates.stream().map(this::onKey).toList()); + } + + /** + * Return a new function with a filter that also applies if the data key + * matches any of the given predicate. + * @param predicate the predicate that the key can match + * @return a new sanitizing function with an updated {@link #filter()} + * @since 3.5.0 + * @see #filter() + * @see #sanitizeValue() + */ + default SanitizingFunction ifKeyMatches(Predicate predicate) { + Assert.notNull(predicate, "'predicate' must not be null"); + return ifMatches(onKey(predicate)); + } + + /** + * Return a new function with a filter that also applies if the data string + * value matches any of the given regex patterns (ignoring case). + * @param regexes the case insensitive regexes that the values string can match + * @return a new sanitizing function with an updated {@link #filter()} + * @since 3.5.0 + * @see #filter() + * @see #sanitizeValue() + */ + default SanitizingFunction ifValueStringMatches(String... regexes) { + Assert.notNull(regexes, "'regexes' must not be null"); + return ifValueStringMatches(Arrays.stream(regexes).map(this::caseInsensitivePattern).toArray(Pattern[]::new)); + } + + /** + * Return a new function with a filter that also applies if the data string + * value matches any of the given patterns. + * @param patterns the patterns that the value string can match + * @return a new sanitizing function with an updated {@link #filter()} + * @since 3.5.0 + * @see #filter() + * @see #sanitizeValue() + */ + default SanitizingFunction ifValueStringMatches(Pattern... patterns) { + Assert.notNull(patterns, "'patterns' must not be null"); + return ifValueStringMatches(Arrays.stream(patterns).map(Pattern::asMatchPredicate).toList()); + } + + /** + * Return a new function with a filter that also applies if the data string + * value matches any of the given predicates. + * @param predicates the predicates that the value string can match + * @return a new sanitizing function with an updated {@link #filter()} + * @since 3.5.0 + * @see #filter() + * @see #sanitizeValue() + */ + + default SanitizingFunction ifValueStringMatches(List> predicates) { + Assert.notNull(predicates, "'predicates' must not be null"); + return ifMatches(predicates.stream().map(this::onValueString).toList()); + } + + /** + * Return a new function with a filter that also applies if the data value + * matches any of the given predicates. + * @param predicates the predicates that the value can match + * @return a new sanitizing function with an updated {@link #filter()} + * @since 3.5.0 + * @see #filter() + * @see #sanitizeValue() + */ + default SanitizingFunction ifValueMatches(List> predicates) { + Assert.notNull(predicates, "'predicates' must not be null"); + return ifMatches(predicates.stream().map(this::onValue).toList()); + } + + /** + * Return a new function with a filter that also applies if the data string + * value matches the given predicate. + * @param predicate the predicate that the value string can match + * @return a new sanitizing function with an updated {@link #filter()} + * @since 3.5.0 + * @see #filter() + * @see #sanitizeValue() + */ + + default SanitizingFunction ifValueStringMatches(Predicate predicate) { + Assert.notNull(predicate, "'predicate' must not be null"); + return ifMatches(onValueString(predicate)); + } + + /** + * Return a new function with a filter that also applies if the data value + * matches the given predicate. + * @param predicate the predicate that the value can match + * @return a new sanitizing function with an updated {@link #filter()} + * @since 3.5.0 + * @see #filter() + * @see #sanitizeValue() + */ + default SanitizingFunction ifValueMatches(Predicate predicate) { + Assert.notNull(predicate, "'predicate' must not be null"); + return ifMatches((data) -> predicate.test(data.getValue())); + } + + /** + * Return a new function with a filter that also applies if the data matches + * any of the given predicates. + * @param predicates the predicates that the data can match + * @return a new sanitizing function with an updated {@link #filter()} + * @since 3.5.0 + * @see #filter() + * @see #sanitizeValue() + */ + default SanitizingFunction ifMatches(List> predicates) { + Assert.notNull(predicates, "'predicates' must not be null"); + Predicate combined = null; + for (Predicate predicate : predicates) { + combined = (combined != null) ? combined.or(predicate) : predicate; + } + return ifMatches(combined); + } + + /** + * Return a new function with a filter that also applies if the data matches + * the given predicate. + * @param predicate the predicate that the data can match + * @return a new sanitizing function with an updated {@link #filter()} + * @since 3.5.0 + * @see #filter() + * @see #sanitizeValue() + */ + default SanitizingFunction ifMatches(Predicate predicate) { + Assert.notNull(predicate, "'predicate' must not be null"); + Predicate filter = (filter() != null) ? filter().or(predicate) : predicate; + return new SanitizingFunction() { + + @Override + public Predicate filter() { + return filter; + } + + @Override + public SanitizableData apply(SanitizableData data) { + return SanitizingFunction.this.apply(data); + } + + }; + } + + private Pattern caseInsensitivePattern(String regex) { + Assert.notNull(regex, "'regex' must not be null"); + return Pattern.compile(regex, Pattern.CASE_INSENSITIVE); + } + + private Predicate onKeyIgnoringCase(BiPredicate predicate, String value) { + Assert.notNull(predicate, "'predicate' must not be null"); + Assert.notNull(value, "'value' must not be null"); + String lowerCaseValue = value.toLowerCase(Locale.getDefault()); + return (data) -> nullSafeTest(data.getLowerCaseKey(), + (lowerCaseKey) -> predicate.test(lowerCaseKey, lowerCaseValue)); + } + + private Predicate onKey(Predicate predicate) { + Assert.notNull(predicate, "'predicate' must not be null"); + return (data) -> nullSafeTest(data.getKey(), predicate); + } + + private Predicate onValue(Predicate predicate) { + Assert.notNull(predicate, "'predicate' must not be null"); + return (data) -> nullSafeTest(data.getValue(), predicate); + } + + private Predicate onValueString(Predicate predicate) { + Assert.notNull(predicate, "'predicate' must not be null"); + return (data) -> nullSafeTest((data.getValue() != null) ? data.getValue().toString() : null, predicate); + } + + private boolean nullSafeTest(T value, Predicate predicate) { + return value != null && predicate.test(value); + } + + /** + * Factory method to return a {@link SanitizingFunction} that sanitizes the value. + * This method is often chained with one or more {@code if...} methods. For example: + *
+	 * return SanitizingFunction.sanitizeValue()
+	 * 	.ifKeyContains("password", "secret")
+	 * 	.ifValueStringMatches("^gh._[a-zA-Z0-9]{36}$");
+	 * 
+ * @return a {@link SanitizingFunction} that sanitizes values. + */ + static SanitizingFunction sanitizeValue() { + return SanitizableData::withSanitizedValue; + } + } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/SanitizerTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/SanitizerTests.java index b5c658e344..db4a8317d4 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/SanitizerTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/SanitizerTests.java @@ -93,6 +93,15 @@ class SanitizerTests { assertThat(sanitizer.sanitize(password, true)).isEqualTo("------"); } + @Test + void overridingDefaultSanitizingFunctionWithFiltered() { + Sanitizer sanitizer = new Sanitizer(List.of(SanitizingFunction.sanitizeValue().ifLikelySenstive())); + SanitizableData other = new SanitizableData(null, "other", "123456"); + SanitizableData password = new SanitizableData(null, "password", "123456"); + assertThat(sanitizer.sanitize(other, true)).isEqualTo("123456"); + assertThat(sanitizer.sanitize(password, true)).isEqualTo(SanitizableData.SANITIZED_VALUE); + } + @Test void whenValueSanitizedLaterSanitizingFunctionsShouldBeSkipped() { final String sameKey = "custom"; diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/SanitizingFunctionTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/SanitizingFunctionTests.java new file mode 100644 index 0000000000..ba85c5bafe --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/SanitizingFunctionTests.java @@ -0,0 +1,338 @@ +/* + * Copyright 2012-2025 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 java.util.List; +import java.util.Objects; +import java.util.regex.Pattern; + +import org.assertj.core.api.Condition; +import org.assertj.core.api.ObjectAssert; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link SanitizingFunction}. + * + * @author Phillip Webb + */ +class SanitizingFunctionTests { + + private static final SanitizableData data = data("key"); + + @Test + void applyUnlessFilteredWhenHasNoFilterReturnsFiltered() { + SanitizingFunction function = SanitizingFunction.sanitizeValue(); + assertThat(function.apply(data)).has(sanitizedValue()); + assertThat(function.applyUnlessFiltered(data)).has(sanitizedValue()); + } + + @Test + void applyUnlessFilteredWhenHasFilterTestingTrueReturnsFiltered() { + SanitizingFunction function = SanitizingFunction.sanitizeValue().ifMatches((data) -> true); + assertThat(function.apply(data)).has(sanitizedValue()); + assertThat(function.applyUnlessFiltered(data)).has(sanitizedValue()); + } + + @Test + void applyUnlessFilteredWhenHasFilterTestingFalseReturnsUnfiltered() { + SanitizingFunction function = SanitizingFunction.sanitizeValue().ifMatches((data) -> false); + assertThat(function.apply(data)).has(sanitizedValue()); + assertThat(function.applyUnlessFiltered(data)).has(unsanitizedValue()); + } + + @Test + void ifLikelySenstiveFiltersExpected() { + SanitizingFunction function = SanitizingFunction.sanitizeValue().ifLikelySenstive(); + assertThat(function).satisfies(this::likelyCredentialChecks, this::likelyUriChecks, + this::likelySenstiveEnvironmentVariableChecks, this::vcapServicesChecks); + } + + @Test + void ifLikelyCredentialFiltersExpected() { + SanitizingFunction function = SanitizingFunction.sanitizeValue().ifLikelyCredential(); + assertThat(function).satisfies(this::likelyCredentialChecks); + } + + private void likelyCredentialChecks(SanitizingFunction function) { + assertThatApplyingToKey(function, "password").has(sanitizedValue()); + assertThatApplyingToKey(function, "database.password").has(sanitizedValue()); + assertThatApplyingToKey(function, "PASSWORD").has(sanitizedValue()); + assertThatApplyingToKey(function, "secret").has(sanitizedValue()); + assertThatApplyingToKey(function, "key").has(sanitizedValue()); + assertThatApplyingToKey(function, "token").has(sanitizedValue()); + assertThatApplyingToKey(function, "credentials").has(sanitizedValue()); + assertThatApplyingToKey(function, "thecredentialssecret").has(sanitizedValue()); + assertThatApplyingToKey(function, "some.credentials.here").has(sanitizedValue()); + assertThatApplyingToKey(function, "test").has(unsanitizedValue()); + } + + @Test + void ifLikelyUriFiltersExpected() { + SanitizingFunction function = SanitizingFunction.sanitizeValue().ifLikelyUri(); + assertThat(function).satisfies(this::likelyUriChecks); + } + + private void likelyUriChecks(SanitizingFunction function) { + assertThatApplyingToKey(function, "uri").has(sanitizedValue()); + assertThatApplyingToKey(function, "URI").has(sanitizedValue()); + assertThatApplyingToKey(function, "database.uri").has(sanitizedValue()); + assertThatApplyingToKey(function, "uris").has(sanitizedValue()); + assertThatApplyingToKey(function, "url").has(sanitizedValue()); + assertThatApplyingToKey(function, "urls").has(sanitizedValue()); + assertThatApplyingToKey(function, "address").has(sanitizedValue()); + assertThatApplyingToKey(function, "addresses").has(sanitizedValue()); + assertThatApplyingToKey(function, "test").has(unsanitizedValue()); + } + + @Test + void ifLikelySenstiveEnvironmentVariableFiltersExpected() { + SanitizingFunction function = SanitizingFunction.sanitizeValue().ifLikelySenstiveEnvironmentVariable(); + assertThat(function).satisfies(this::likelySenstiveEnvironmentVariableChecks); + } + + private void likelySenstiveEnvironmentVariableChecks(SanitizingFunction function) { + assertThatApplyingToKey(function, "sun.java.command").has(sanitizedValue()); + assertThatApplyingToKey(function, "spring.application.json").has(sanitizedValue()); + assertThatApplyingToKey(function, "SPRING_APPLICATION_JSON").has(sanitizedValue()); + assertThatApplyingToKey(function, "some.other.json").has(unsanitizedValue()); + } + + @Test + void ifVcapServicesFiltersExpected() { + SanitizingFunction function = SanitizingFunction.sanitizeValue().ifVcapServices(); + assertThat(function).satisfies(this::vcapServicesChecks); + } + + private void vcapServicesChecks(SanitizingFunction function) { + assertThatApplyingToKey(function, "vcap_services").has(sanitizedValue()); + assertThatApplyingToKey(function, "vcap.services").has(sanitizedValue()); + assertThatApplyingToKey(function, "vcap.services.whatever").has(sanitizedValue()); + assertThatApplyingToKey(function, "notvcap.services").has(unsanitizedValue()); + } + + @Test + void ifKeyEqualsFiltersExpected() { + SanitizingFunction function = SanitizingFunction.sanitizeValue().ifKeyEquals("spring", "test"); + assertThatApplyingToKey(function, "spring").has(sanitizedValue()); + assertThatApplyingToKey(function, "SPRING").has(sanitizedValue()); + assertThatApplyingToKey(function, "SpRiNg").has(sanitizedValue()); + assertThatApplyingToKey(function, "test").has(sanitizedValue()); + assertThatApplyingToKey(function, "boot").has(unsanitizedValue()); + assertThatApplyingToKey(function, "xspring").has(unsanitizedValue()); + assertThatApplyingToKey(function, "springx").has(unsanitizedValue()); + assertThatApplyingToKey(function, null).has(unsanitizedValue()); + } + + @Test + void ifKeyEndsWithFiltersExpected() { + SanitizingFunction function = SanitizingFunction.sanitizeValue().ifKeyEndsWith("boot", "test"); + assertThatApplyingToKey(function, "springboot").has(sanitizedValue()); + assertThatApplyingToKey(function, "SPRINGboot").has(sanitizedValue()); + assertThatApplyingToKey(function, "springBOOT").has(sanitizedValue()); + assertThatApplyingToKey(function, "boot").has(sanitizedValue()); + assertThatApplyingToKey(function, "atest").has(sanitizedValue()); + assertThatApplyingToKey(function, "bootx").has(unsanitizedValue()); + assertThatApplyingToKey(function, null).has(unsanitizedValue()); + } + + @Test + void ifKeyContainsFiltersExpected() { + SanitizingFunction function = SanitizingFunction.sanitizeValue().ifKeyContains("oo", "ee"); + assertThatApplyingToKey(function, "oo").has(sanitizedValue()); + assertThatApplyingToKey(function, "OO").has(sanitizedValue()); + assertThatApplyingToKey(function, "bOOt").has(sanitizedValue()); + assertThatApplyingToKey(function, "boot").has(sanitizedValue()); + assertThatApplyingToKey(function, "beet").has(sanitizedValue()); + assertThatApplyingToKey(function, "spring").has(unsanitizedValue()); + assertThatApplyingToKey(function, null).has(unsanitizedValue()); + } + + @Test + void ifKeyMatchesIgnoringCaseFiltersExpected() { + SanitizingFunction function = SanitizingFunction.sanitizeValue() + .ifKeyMatchesIgnoringCase((key, value) -> key.startsWith(value) && key.endsWith(value), "x", "y"); + assertThatApplyingToKey(function, "xtestx").has(sanitizedValue()); + assertThatApplyingToKey(function, "XtestX").has(sanitizedValue()); + assertThatApplyingToKey(function, "YY").has(sanitizedValue()); + assertThatApplyingToKey(function, "xy").has(unsanitizedValue()); + assertThatApplyingToKey(function, null).has(unsanitizedValue()); + } + + @Test + void ifKeyMatchesWithRegexFiltersExpected() { + SanitizingFunction function = SanitizingFunction.sanitizeValue().ifKeyMatches("^sp.*$", "^bo.*$"); + assertThatApplyingToKey(function, "spring").has(sanitizedValue()); + assertThatApplyingToKey(function, "spin").has(sanitizedValue()); + assertThatApplyingToKey(function, "SPRING").has(sanitizedValue()); + assertThatApplyingToKey(function, "BOOT").has(sanitizedValue()); + assertThatApplyingToKey(function, "xspring").has(unsanitizedValue()); + assertThatApplyingToKey(function, null).has(unsanitizedValue()); + } + + @Test + void ifKeyMatchesWithPatternFiltersExpected() { + SanitizingFunction function = SanitizingFunction.sanitizeValue().ifKeyMatches(Pattern.compile("^sp.*$")); + assertThatApplyingToKey(function, "spring").has(sanitizedValue()); + assertThatApplyingToKey(function, "spin").has(sanitizedValue()); + assertThatApplyingToKey(function, "SPRING").has(unsanitizedValue()); + assertThatApplyingToKey(function, "xspring").has(unsanitizedValue()); + assertThatApplyingToKey(function, null).has(unsanitizedValue()); + } + + @Test + void ifKeyMatchesWithPredicatesFiltersExpected() { + SanitizingFunction function = SanitizingFunction.sanitizeValue() + .ifKeyMatches(List.of((key) -> key.startsWith("sp"), (key) -> key.startsWith("BO"))); + assertThatApplyingToKey(function, "spring").has(sanitizedValue()); + assertThatApplyingToKey(function, "spin").has(sanitizedValue()); + assertThatApplyingToKey(function, "BO").has(sanitizedValue()); + assertThatApplyingToKey(function, "SPRING").has(unsanitizedValue()); + assertThatApplyingToKey(function, "boot").has(unsanitizedValue()); + assertThatApplyingToKey(function, null).has(unsanitizedValue()); + } + + @Test + void ifKeyMatchesWithPredicateFiltersExpected() { + SanitizingFunction function = SanitizingFunction.sanitizeValue().ifKeyMatches((key) -> key.startsWith("sp")); + assertThatApplyingToKey(function, "spring").has(sanitizedValue()); + assertThatApplyingToKey(function, "spin").has(sanitizedValue()); + assertThatApplyingToKey(function, "boot").has(unsanitizedValue()); + assertThatApplyingToKey(function, null).has(unsanitizedValue()); + } + + @Test + void ifValueStringMatchesWithRegexesFiltersExpected() { + SanitizingFunction function = SanitizingFunction.sanitizeValue().ifValueStringMatches("^sp.*$", "^bo.*$"); + assertThatApplyingToValue(function, "spring").has(sanitizedValue()); + assertThatApplyingToValue(function, "SPRING").has(sanitizedValue()); + assertThatApplyingToValue(function, "boot").has(sanitizedValue()); + assertThatApplyingToValue(function, "other").has(unsanitizedValue()); + assertThatApplyingToKey(function, null).has(unsanitizedValue()); + } + + @Test + void ifValueStringMatchesWithPatternsFiltersExpected() { + SanitizingFunction function = SanitizingFunction.sanitizeValue() + .ifValueStringMatches(Pattern.compile("^sp.*$")); + assertThatApplyingToValue(function, "spring").has(sanitizedValue()); + assertThatApplyingToValue(function, "spin").has(sanitizedValue()); + assertThatApplyingToValue(function, "SPRING").has(unsanitizedValue()); + assertThatApplyingToValue(function, "xspring").has(unsanitizedValue()); + assertThatApplyingToValue(function, null).has(unsanitizedValue()); + } + + @Test + void ifValueStringStringMatchesWithPredicatesFiltersExpected() { + SanitizingFunction function = SanitizingFunction.sanitizeValue() + .ifValueStringMatches(List.of((value) -> value.startsWith("sp"), (value) -> value.startsWith("BO"))); + assertThatApplyingToValue(function, "spring").has(sanitizedValue()); + assertThatApplyingToValue(function, "spin").has(sanitizedValue()); + assertThatApplyingToValue(function, "BO").has(sanitizedValue()); + assertThatApplyingToValue(function, "SPRING").has(unsanitizedValue()); + assertThatApplyingToValue(function, "boot").has(unsanitizedValue()); + assertThatApplyingToValue(function, null).has(unsanitizedValue()); + } + + @Test + void ifValueStringMatchesWithPredicateFiltersExpected() { + SanitizingFunction function = SanitizingFunction.sanitizeValue() + .ifValueStringMatches((value) -> value.startsWith("sp")); + assertThatApplyingToValue(function, "spring").has(sanitizedValue()); + assertThatApplyingToValue(function, "spin").has(sanitizedValue()); + assertThatApplyingToValue(function, "boot").has(unsanitizedValue()); + assertThatApplyingToValue(function, null).has(unsanitizedValue()); + } + + @Test + void ifValueMatchesWithPredicatesFiltersExpected() { + SanitizingFunction function = SanitizingFunction.sanitizeValue() + .ifValueMatches(List.of((value) -> value instanceof String string && string.startsWith("sp"), + (value) -> value instanceof String string && string.startsWith("BO"))); + assertThatApplyingToValue(function, "spring").has(sanitizedValue()); + assertThatApplyingToValue(function, "spin").has(sanitizedValue()); + assertThatApplyingToValue(function, "BO").has(sanitizedValue()); + assertThatApplyingToValue(function, "SPRING").has(unsanitizedValue()); + assertThatApplyingToValue(function, "boot").has(unsanitizedValue()); + assertThatApplyingToValue(function, 123).has(unsanitizedValue()); + assertThatApplyingToValue(function, null).has(unsanitizedValue()); + } + + @Test + void ifValueMatchesWithPredicateFiltersExpected() { + SanitizingFunction function = SanitizingFunction.sanitizeValue() + .ifValueMatches((value) -> value instanceof String string && string.startsWith("sp")); + assertThatApplyingToValue(function, "spring").has(sanitizedValue()); + assertThatApplyingToValue(function, "spin").has(sanitizedValue()); + assertThatApplyingToValue(function, "boot").has(unsanitizedValue()); + assertThatApplyingToValue(function, 123).has(unsanitizedValue()); + assertThatApplyingToKey(function, null).has(unsanitizedValue()); + } + + @Test + void ifMatchesPredicatesFiltersExpected() { + SanitizingFunction function = SanitizingFunction.sanitizeValue() + .ifMatches(List.of((data) -> data.getKey().startsWith("sp") && "boot".equals(data.getValue()), + (data) -> data.getKey().startsWith("sp") && "framework".equals(data.getValue()))); + assertThatApplying(function, data("spring", "boot")).is(sanitizedValue()); + assertThatApplying(function, data("spring", "framework")).is(sanitizedValue()); + assertThatApplying(function, data("spring", "data")).is(unsanitizedValue()); + assertThatApplying(function, data("spring", null)).is(unsanitizedValue()); + } + + @Test + void ifMatchesPredicateFiltersExpected() { + SanitizingFunction function = SanitizingFunction.sanitizeValue() + .ifMatches((data) -> data.getKey().startsWith("sp") && "boot".equals(data.getValue())); + assertThatApplying(function, data("spring", "boot")).is(sanitizedValue()); + assertThatApplying(function, data("spring", "framework")).is(unsanitizedValue()); + assertThatApplying(function, data("spring", "data")).is(unsanitizedValue()); + assertThatApplying(function, data("spring", null)).is(unsanitizedValue()); + } + + private ObjectAssert assertThatApplyingToKey(SanitizingFunction function, String key) { + return assertThatApplying(function, data(key)); + } + + private ObjectAssert assertThatApplyingToValue(SanitizingFunction function, Object value) { + return assertThatApplying(function, data("key", value)); + } + + private ObjectAssert assertThatApplying(SanitizingFunction function, SanitizableData data) { + return assertThat(function.applyUnlessFiltered(data)).as("%s:%s", data.getKey(), data.getValue()); + } + + private Condition sanitizedValue() { + return new Condition<>((data) -> Objects.equals(data.getValue(), SanitizableData.SANITIZED_VALUE), + "sanitized value"); + } + + private Condition unsanitizedValue() { + return new Condition<>((data) -> !Objects.equals(data.getValue(), SanitizableData.SANITIZED_VALUE), + "unsanitized value"); + } + + private static SanitizableData data(String key) { + return data(key, "value"); + } + + private static SanitizableData data(String key, Object value) { + return new SanitizableData(null, key, value); + } + +}