diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/context/properties/ConfigurationPropertiesReportEndpointAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/context/properties/ConfigurationPropertiesReportEndpointAutoConfiguration.java index 8c029bfb6b7..29c9ce665b7 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/context/properties/ConfigurationPropertiesReportEndpointAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/context/properties/ConfigurationPropertiesReportEndpointAutoConfiguration.java @@ -16,8 +16,6 @@ package org.springframework.boot.actuate.autoconfigure.context.properties; -import java.util.stream.Collectors; - import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnAvailableEndpoint; import org.springframework.boot.actuate.autoconfigure.endpoint.expose.EndpointExposure; @@ -50,17 +48,8 @@ public class ConfigurationPropertiesReportEndpointAutoConfiguration { public ConfigurationPropertiesReportEndpoint configurationPropertiesReportEndpoint( ConfigurationPropertiesReportEndpointProperties properties, ObjectProvider sanitizingFunctions) { - ConfigurationPropertiesReportEndpoint endpoint = new ConfigurationPropertiesReportEndpoint( - sanitizingFunctions.orderedStream().collect(Collectors.toList())); - String[] keysToSanitize = properties.getKeysToSanitize(); - if (keysToSanitize != null) { - endpoint.setKeysToSanitize(keysToSanitize); - } - String[] additionalKeysToSanitize = properties.getAdditionalKeysToSanitize(); - if (additionalKeysToSanitize != null) { - endpoint.keysToSanitize(additionalKeysToSanitize); - } - return endpoint; + return new ConfigurationPropertiesReportEndpoint(sanitizingFunctions.orderedStream().toList(), + properties.getShowValues()); } @Bean @@ -68,8 +57,10 @@ public class ConfigurationPropertiesReportEndpointAutoConfiguration { @ConditionalOnBean(ConfigurationPropertiesReportEndpoint.class) @ConditionalOnAvailableEndpoint(exposure = { EndpointExposure.WEB, EndpointExposure.CLOUD_FOUNDRY }) public ConfigurationPropertiesReportEndpointWebExtension configurationPropertiesReportEndpointWebExtension( - ConfigurationPropertiesReportEndpoint configurationPropertiesReportEndpoint) { - return new ConfigurationPropertiesReportEndpointWebExtension(configurationPropertiesReportEndpoint); + ConfigurationPropertiesReportEndpoint configurationPropertiesReportEndpoint, + ConfigurationPropertiesReportEndpointProperties properties) { + return new ConfigurationPropertiesReportEndpointWebExtension(configurationPropertiesReportEndpoint, + properties.getShowValues(), properties.getRoles()); } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/context/properties/ConfigurationPropertiesReportEndpointProperties.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/context/properties/ConfigurationPropertiesReportEndpointProperties.java index d4a2b138358..1efbb193ac1 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/context/properties/ConfigurationPropertiesReportEndpointProperties.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/context/properties/ConfigurationPropertiesReportEndpointProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,44 +16,44 @@ package org.springframework.boot.actuate.autoconfigure.context.properties; +import java.util.HashSet; +import java.util.Set; + import org.springframework.boot.actuate.context.properties.ConfigurationPropertiesReportEndpoint; +import org.springframework.boot.actuate.endpoint.Show; import org.springframework.boot.context.properties.ConfigurationProperties; /** * Configuration properties for {@link ConfigurationPropertiesReportEndpoint}. * * @author Stephane Nicoll + * @author Madhura Bhave * @since 2.0.0 */ @ConfigurationProperties("management.endpoint.configprops") public class ConfigurationPropertiesReportEndpointProperties { /** - * Keys that should be sanitized. Keys can be simple strings that the property ends - * with or regular expressions. + * When to show unsanitized values. */ - private String[] keysToSanitize; + private Show showValues = Show.NEVER; /** - * Keys that should be sanitized in addition to those already configured. Keys can be - * simple strings that the property ends with or regular expressions. + * Roles used to determine whether a user is authorized to be shown unsanitized + * values. When empty, all authenticated users are authorized. */ - private String[] additionalKeysToSanitize; + private Set roles = new HashSet<>(); - public String[] getKeysToSanitize() { - return this.keysToSanitize; + public Show getShowValues() { + return this.showValues; } - public void setKeysToSanitize(String[] keysToSanitize) { - this.keysToSanitize = keysToSanitize; + public void setShowValues(Show showValues) { + this.showValues = showValues; } - public String[] getAdditionalKeysToSanitize() { - return this.additionalKeysToSanitize; - } - - public void setAdditionalKeysToSanitize(String[] additionalKeysToSanitize) { - this.additionalKeysToSanitize = additionalKeysToSanitize; + public Set getRoles() { + return this.roles; } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/env/EnvironmentEndpointAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/env/EnvironmentEndpointAutoConfiguration.java index e6af2bc2f6a..42e8308833b 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/env/EnvironmentEndpointAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/env/EnvironmentEndpointAutoConfiguration.java @@ -16,8 +16,6 @@ package org.springframework.boot.actuate.autoconfigure.env; -import java.util.stream.Collectors; - import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnAvailableEndpoint; import org.springframework.boot.actuate.autoconfigure.endpoint.expose.EndpointExposure; @@ -48,25 +46,18 @@ public class EnvironmentEndpointAutoConfiguration { @ConditionalOnMissingBean public EnvironmentEndpoint environmentEndpoint(Environment environment, EnvironmentEndpointProperties properties, ObjectProvider sanitizingFunctions) { - EnvironmentEndpoint endpoint = new EnvironmentEndpoint(environment, - sanitizingFunctions.orderedStream().collect(Collectors.toList())); - String[] keysToSanitize = properties.getKeysToSanitize(); - if (keysToSanitize != null) { - endpoint.setKeysToSanitize(keysToSanitize); - } - String[] additionalKeysToSanitize = properties.getAdditionalKeysToSanitize(); - if (additionalKeysToSanitize != null) { - endpoint.keysToSanitize(additionalKeysToSanitize); - } - return endpoint; + return new EnvironmentEndpoint(environment, sanitizingFunctions.orderedStream().toList(), + properties.getShowValues()); } @Bean @ConditionalOnMissingBean @ConditionalOnBean(EnvironmentEndpoint.class) @ConditionalOnAvailableEndpoint(exposure = { EndpointExposure.WEB, EndpointExposure.CLOUD_FOUNDRY }) - public EnvironmentEndpointWebExtension environmentEndpointWebExtension(EnvironmentEndpoint environmentEndpoint) { - return new EnvironmentEndpointWebExtension(environmentEndpoint); + public EnvironmentEndpointWebExtension environmentEndpointWebExtension(EnvironmentEndpoint environmentEndpoint, + EnvironmentEndpointProperties properties) { + return new EnvironmentEndpointWebExtension(environmentEndpoint, properties.getShowValues(), + properties.getRoles()); } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/env/EnvironmentEndpointProperties.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/env/EnvironmentEndpointProperties.java index cd1bfddf133..f2d40740448 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/env/EnvironmentEndpointProperties.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/env/EnvironmentEndpointProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,10 @@ package org.springframework.boot.actuate.autoconfigure.env; +import java.util.HashSet; +import java.util.Set; + +import org.springframework.boot.actuate.endpoint.Show; import org.springframework.boot.actuate.env.EnvironmentEndpoint; import org.springframework.boot.context.properties.ConfigurationProperties; @@ -29,31 +33,26 @@ import org.springframework.boot.context.properties.ConfigurationProperties; public class EnvironmentEndpointProperties { /** - * Keys that should be sanitized. Keys can be simple strings that the property ends - * with or regular expressions. + * When to show unsanitized values. */ - private String[] keysToSanitize; + private Show showValues = Show.NEVER; /** - * Keys that should be sanitized in addition to those already configured. Keys can be - * simple strings that the property ends with or regular expressions. + * Roles used to determine whether a user is authorized to be shown unsanitized + * values. When empty, all authenticated users are authorized. */ - private String[] additionalKeysToSanitize; + private Set roles = new HashSet<>(); - public String[] getKeysToSanitize() { - return this.keysToSanitize; + public Show getShowValues() { + return this.showValues; } - public void setKeysToSanitize(String[] keysToSanitize) { - this.keysToSanitize = keysToSanitize; + public void setShowValues(Show showValues) { + this.showValues = showValues; } - public String[] getAdditionalKeysToSanitize() { - return this.additionalKeysToSanitize; - } - - public void setAdditionalKeysToSanitize(String[] additionalKeysToSanitize) { - this.additionalKeysToSanitize = additionalKeysToSanitize; + public Set getRoles() { + return this.roles; } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/AutoConfiguredHealthEndpointGroup.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/AutoConfiguredHealthEndpointGroup.java index 1e4523a0353..949897e8424 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/AutoConfiguredHealthEndpointGroup.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/AutoConfiguredHealthEndpointGroup.java @@ -16,20 +16,15 @@ package org.springframework.boot.actuate.autoconfigure.health; -import java.security.Principal; import java.util.Collection; import java.util.function.Predicate; -import org.springframework.boot.actuate.autoconfigure.health.HealthProperties.Show; import org.springframework.boot.actuate.endpoint.SecurityContext; +import org.springframework.boot.actuate.endpoint.Show; import org.springframework.boot.actuate.health.AdditionalHealthEndpointPath; import org.springframework.boot.actuate.health.HealthEndpointGroup; import org.springframework.boot.actuate.health.HttpCodeStatusMapper; import org.springframework.boot.actuate.health.StatusAggregator; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.util.ClassUtils; -import org.springframework.util.CollectionUtils; /** * Auto-configured {@link HealthEndpointGroup} backed by {@link HealthProperties}. @@ -83,54 +78,13 @@ class AutoConfiguredHealthEndpointGroup implements HealthEndpointGroup { @Override public boolean showComponents(SecurityContext securityContext) { - if (this.showComponents == null) { - return showDetails(securityContext); - } - return getShowResult(securityContext, this.showComponents); + Show show = (this.showComponents != null) ? this.showComponents : this.showDetails; + return show.isShown(securityContext, this.roles); } @Override public boolean showDetails(SecurityContext securityContext) { - return getShowResult(securityContext, this.showDetails); - } - - private boolean getShowResult(SecurityContext securityContext, Show show) { - return switch (show) { - case NEVER -> false; - case ALWAYS -> true; - case WHEN_AUTHORIZED -> isAuthorized(securityContext); - }; - } - - private boolean isAuthorized(SecurityContext securityContext) { - Principal principal = securityContext.getPrincipal(); - if (principal == null) { - return false; - } - if (CollectionUtils.isEmpty(this.roles)) { - return true; - } - boolean checkAuthorities = isSpringSecurityAuthentication(principal); - for (String role : this.roles) { - if (securityContext.isUserInRole(role)) { - return true; - } - if (checkAuthorities) { - Authentication authentication = (Authentication) principal; - for (GrantedAuthority authority : authentication.getAuthorities()) { - String name = authority.getAuthority(); - if (role.equals(name)) { - return true; - } - } - } - } - return false; - } - - private boolean isSpringSecurityAuthentication(Principal principal) { - return ClassUtils.isPresent("org.springframework.security.core.Authentication", null) - && (principal instanceof Authentication); + return this.showDetails.isShown(securityContext, this.roles); } @Override diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/AutoConfiguredHealthEndpointGroups.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/AutoConfiguredHealthEndpointGroups.java index 55be3d1a225..ff47f03252e 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/AutoConfiguredHealthEndpointGroups.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/AutoConfiguredHealthEndpointGroups.java @@ -31,8 +31,8 @@ import org.springframework.beans.factory.ListableBeanFactory; import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.beans.factory.annotation.BeanFactoryAnnotationUtils; import org.springframework.boot.actuate.autoconfigure.health.HealthEndpointProperties.Group; -import org.springframework.boot.actuate.autoconfigure.health.HealthProperties.Show; import org.springframework.boot.actuate.autoconfigure.health.HealthProperties.Status; +import org.springframework.boot.actuate.endpoint.Show; import org.springframework.boot.actuate.health.AdditionalHealthEndpointPath; import org.springframework.boot.actuate.health.HealthEndpointGroup; import org.springframework.boot.actuate.health.HealthEndpointGroups; diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointProperties.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointProperties.java index c6c0a53e5fe..cf53bb6c403 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointProperties.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointProperties.java @@ -21,6 +21,7 @@ import java.util.LinkedHashMap; import java.util.Map; import java.util.Set; +import org.springframework.boot.actuate.endpoint.Show; import org.springframework.boot.actuate.health.HealthEndpoint; import org.springframework.boot.context.properties.ConfigurationProperties; diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthProperties.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthProperties.java index c91ca70b101..95ef11c5de1 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthProperties.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthProperties.java @@ -23,7 +23,7 @@ import java.util.List; import java.util.Map; import java.util.Set; -import org.springframework.boot.actuate.health.HealthEndpoint; +import org.springframework.boot.actuate.endpoint.Show; import org.springframework.boot.context.properties.NestedConfigurationProperty; /** @@ -103,27 +103,4 @@ public abstract class HealthProperties { } - /** - * Options for showing items in responses from the {@link HealthEndpoint} web - * extensions. - */ - public enum Show { - - /** - * Never show the item in the response. - */ - NEVER, - - /** - * Show the item in the response when accessed by an authorized user. - */ - WHEN_AUTHORIZED, - - /** - * Always show the item in the response. - */ - ALWAYS - - } - } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/quartz/QuartzEndpointAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/quartz/QuartzEndpointAutoConfiguration.java index e7071aeef91..a97d2bcdd4a 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/quartz/QuartzEndpointAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/quartz/QuartzEndpointAutoConfiguration.java @@ -18,8 +18,10 @@ package org.springframework.boot.actuate.autoconfigure.quartz; import org.quartz.Scheduler; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnAvailableEndpoint; import org.springframework.boot.actuate.autoconfigure.endpoint.expose.EndpointExposure; +import org.springframework.boot.actuate.endpoint.SanitizingFunction; import org.springframework.boot.actuate.quartz.QuartzEndpoint; import org.springframework.boot.actuate.quartz.QuartzEndpointWebExtension; import org.springframework.boot.autoconfigure.AutoConfiguration; @@ -28,6 +30,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.quartz.QuartzAutoConfiguration; +import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; /** @@ -40,21 +43,23 @@ import org.springframework.context.annotation.Bean; @AutoConfiguration(after = QuartzAutoConfiguration.class) @ConditionalOnClass(Scheduler.class) @ConditionalOnAvailableEndpoint(endpoint = QuartzEndpoint.class) +@EnableConfigurationProperties(QuartzEndpointProperties.class) public class QuartzEndpointAutoConfiguration { @Bean @ConditionalOnBean(Scheduler.class) @ConditionalOnMissingBean - public QuartzEndpoint quartzEndpoint(Scheduler scheduler) { - return new QuartzEndpoint(scheduler); + public QuartzEndpoint quartzEndpoint(Scheduler scheduler, ObjectProvider sanitizingFunctions) { + return new QuartzEndpoint(scheduler, sanitizingFunctions.orderedStream().toList()); } @Bean @ConditionalOnBean(QuartzEndpoint.class) @ConditionalOnMissingBean @ConditionalOnAvailableEndpoint(exposure = { EndpointExposure.WEB, EndpointExposure.CLOUD_FOUNDRY }) - public QuartzEndpointWebExtension quartzEndpointWebExtension(QuartzEndpoint endpoint) { - return new QuartzEndpointWebExtension(endpoint); + public QuartzEndpointWebExtension quartzEndpointWebExtension(QuartzEndpoint endpoint, + QuartzEndpointProperties properties) { + return new QuartzEndpointWebExtension(endpoint, properties.getShowValues(), properties.getRoles()); } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/quartz/QuartzEndpointProperties.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/quartz/QuartzEndpointProperties.java new file mode 100644 index 00000000000..a55b49cbeaf --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/quartz/QuartzEndpointProperties.java @@ -0,0 +1,58 @@ +/* + * Copyright 2012-2022 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.autoconfigure.quartz; + +import java.util.HashSet; +import java.util.Set; + +import org.springframework.boot.actuate.endpoint.Show; +import org.springframework.boot.actuate.quartz.QuartzEndpoint; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * Configuration properties for {@link QuartzEndpoint}. + * + * @author Madhura Bhave + * @since 3.0.0 + */ +@ConfigurationProperties("management.endpoint.quartz") +public class QuartzEndpointProperties { + + /** + * When to show unsanitized job or trigger values. + */ + private Show showValues = Show.NEVER; + + /** + * Roles used to determine whether a user is authorized to be shown unsanitized job or + * trigger values. When empty, all authenticated users are authorized. + */ + private Set roles = new HashSet<>(); + + public Show getShowValues() { + return this.showValues; + } + + public void setShowValues(Show showValues) { + this.showValues = showValues; + } + + public Set getRoles() { + return this.roles; + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json index 8cbce27c119..dfe63ad4b0b 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -30,30 +30,6 @@ "description": "Whether to enable default metrics exporters.", "defaultValue": true }, - { - "name": "management.endpoint.configprops.keys-to-sanitize", - "defaultValue": [ - "password", - "secret", - "key", - "token", - ".*credentials.*", - "vcap_services", - "sun.java.command" - ] - }, - { - "name": "management.endpoint.env.keys-to-sanitize", - "defaultValue": [ - "password", - "secret", - "key", - "token", - ".*credentials.*", - "vcap_services", - "sun.java.command" - ] - }, { "name": "management.endpoint.health.probes.add-additional-paths", "type": "java.lang.Boolean", diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/context/properties/ConfigurationPropertiesReportEndpointAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/context/properties/ConfigurationPropertiesReportEndpointAutoConfigurationTests.java index 035abffdea2..8fbe0481011 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/context/properties/ConfigurationPropertiesReportEndpointAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/context/properties/ConfigurationPropertiesReportEndpointAutoConfigurationTests.java @@ -17,6 +17,7 @@ package org.springframework.boot.actuate.autoconfigure.context.properties; import java.util.Map; +import java.util.Set; import org.junit.jupiter.api.Test; @@ -24,6 +25,7 @@ import org.springframework.boot.actuate.context.properties.ConfigurationProperti import org.springframework.boot.actuate.context.properties.ConfigurationPropertiesReportEndpoint.ApplicationConfigurationProperties; import org.springframework.boot.actuate.context.properties.ConfigurationPropertiesReportEndpointWebExtension; import org.springframework.boot.actuate.endpoint.SanitizingFunction; +import org.springframework.boot.actuate.endpoint.Show; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; @@ -33,6 +35,7 @@ import org.springframework.boot.test.context.runner.ContextConsumer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.annotation.Order; +import org.springframework.test.util.ReflectionTestUtils; import static org.assertj.core.api.Assertions.assertThat; @@ -50,7 +53,7 @@ class ConfigurationPropertiesReportEndpointAutoConfigurationTests { void runShouldHaveEndpointBean() { this.contextRunner.withUserConfiguration(Config.class) .withPropertyValues("management.endpoints.web.exposure.include=configprops") - .run(validateTestProperties("******", "654321")); + .run(validateTestProperties("******", "******")); } @Test @@ -60,24 +63,42 @@ class ConfigurationPropertiesReportEndpointAutoConfigurationTests { } @Test - void keysToSanitizeCanBeConfiguredViaTheEnvironment() { + @SuppressWarnings("unchecked") + void rolesCanBeConfiguredViaTheEnvironment() { this.contextRunner.withUserConfiguration(Config.class) - .withPropertyValues("management.endpoint.configprops.keys-to-sanitize: .*pass.*, property") - .withPropertyValues("management.endpoints.web.exposure.include=configprops") - .run(validateTestProperties("******", "******")); + .withPropertyValues("management.endpoint.configprops.roles: test") + .withPropertyValues("management.endpoints.web.exposure.include=configprops").run((context) -> { + assertThat(context).hasSingleBean(ConfigurationPropertiesReportEndpointWebExtension.class); + ConfigurationPropertiesReportEndpointWebExtension endpoint = context + .getBean(ConfigurationPropertiesReportEndpointWebExtension.class); + Set roles = (Set) ReflectionTestUtils.getField(endpoint, "roles"); + assertThat(roles.contains("test")).isTrue(); + }); } @Test - void additionalKeysToSanitizeCanBeConfiguredViaTheEnvironment() { + @SuppressWarnings("unchecked") + void showValuesCanBeConfiguredViaTheEnvironment() { this.contextRunner.withUserConfiguration(Config.class) - .withPropertyValues("management.endpoint.configprops.additional-keys-to-sanitize: property") - .withPropertyValues("management.endpoints.web.exposure.include=configprops") - .run(validateTestProperties("******", "******")); + .withPropertyValues("management.endpoint.configprops.show-values: WHEN_AUTHORIZED") + .withPropertyValues("management.endpoints.web.exposure.include=configprops").run((context) -> { + assertThat(context).hasSingleBean(ConfigurationPropertiesReportEndpoint.class); + assertThat(context).hasSingleBean(ConfigurationPropertiesReportEndpointWebExtension.class); + ConfigurationPropertiesReportEndpointWebExtension webExtension = context + .getBean(ConfigurationPropertiesReportEndpointWebExtension.class); + ConfigurationPropertiesReportEndpoint endpoint = context + .getBean(ConfigurationPropertiesReportEndpoint.class); + Show showValuesWebExtension = (Show) ReflectionTestUtils.getField(webExtension, "showValues"); + assertThat(showValuesWebExtension).isEqualTo(Show.WHEN_AUTHORIZED); + Show showValues = (Show) ReflectionTestUtils.getField(endpoint, "showValues"); + assertThat(showValues).isEqualTo(Show.WHEN_AUTHORIZED); + }); } @Test void customSanitizingFunctionsAreAppliedInOrder() { - this.contextRunner.withUserConfiguration(Config.class, SanitizingFunctionConfiguration.class) + this.contextRunner.withPropertyValues("management.endpoint.configprops.show-values: ALWAYS") + .withUserConfiguration(Config.class, SanitizingFunctionConfiguration.class) .withPropertyValues("management.endpoints.web.exposure.include=configprops", "test.my-test-property=abc") .run(validateTestProperties("$$$111$$$", "$$$222$$$")); diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/ConfigurationPropertiesReportEndpointDocumentationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/ConfigurationPropertiesReportEndpointDocumentationTests.java index e1e5be8b105..598476ad243 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/ConfigurationPropertiesReportEndpointDocumentationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/ConfigurationPropertiesReportEndpointDocumentationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,9 +16,12 @@ package org.springframework.boot.actuate.autoconfigure.endpoint.web.documentation; +import java.util.Collections; + import org.junit.jupiter.api.Test; import org.springframework.boot.actuate.context.properties.ConfigurationPropertiesReportEndpoint; +import org.springframework.boot.actuate.endpoint.Show; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; @@ -80,7 +83,7 @@ class ConfigurationPropertiesReportEndpointDocumentationTests extends MockMvcEnd @Bean ConfigurationPropertiesReportEndpoint endpoint() { - return new ConfigurationPropertiesReportEndpoint(); + return new ConfigurationPropertiesReportEndpoint(Collections.emptyList(), Show.ALWAYS); } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/EnvironmentEndpointDocumentationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/EnvironmentEndpointDocumentationTests.java index e3e6cffffea..7de7b7e3805 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/EnvironmentEndpointDocumentationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/EnvironmentEndpointDocumentationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,7 @@ package org.springframework.boot.actuate.autoconfigure.endpoint.web.documentation; import java.io.IOException; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; @@ -27,6 +28,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import org.junit.jupiter.api.Test; +import org.springframework.boot.actuate.endpoint.Show; import org.springframework.boot.actuate.env.EnvironmentEndpoint; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -148,7 +150,7 @@ class EnvironmentEndpointDocumentationTests extends MockMvcEndpointDocumentation && !"Inlined Test Properties".equals(propertySource.getName()); } - }); + }, Collections.emptyList(), Show.ALWAYS); } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/QuartzEndpointDocumentationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/QuartzEndpointDocumentationTests.java index 87cb2b9f1b0..7fe90ed4346 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/QuartzEndpointDocumentationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/QuartzEndpointDocumentationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2022 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. @@ -51,6 +51,7 @@ import org.quartz.TriggerKey; import org.quartz.impl.matchers.GroupMatcher; import org.quartz.spi.OperableTrigger; +import org.springframework.boot.actuate.endpoint.Show; import org.springframework.boot.actuate.quartz.QuartzEndpoint; import org.springframework.boot.actuate.quartz.QuartzEndpointWebExtension; import org.springframework.boot.test.mock.mockito.MockBean; @@ -456,12 +457,12 @@ class QuartzEndpointDocumentationTests extends MockMvcEndpointDocumentationTests @Bean QuartzEndpoint endpoint(Scheduler scheduler) { - return new QuartzEndpoint(scheduler); + return new QuartzEndpoint(scheduler, Collections.emptyList()); } @Bean QuartzEndpointWebExtension endpointWebExtension(QuartzEndpoint endpoint) { - return new QuartzEndpointWebExtension(endpoint); + return new QuartzEndpointWebExtension(endpoint, Show.ALWAYS, Collections.emptySet()); } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/env/EnvironmentEndpointAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/env/EnvironmentEndpointAutoConfigurationTests.java index 1f2b93d0134..9bf3308537b 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/env/EnvironmentEndpointAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/env/EnvironmentEndpointAutoConfigurationTests.java @@ -17,10 +17,12 @@ package org.springframework.boot.actuate.autoconfigure.env; import java.util.Map; +import java.util.Set; import org.junit.jupiter.api.Test; import org.springframework.boot.actuate.endpoint.SanitizingFunction; +import org.springframework.boot.actuate.endpoint.Show; import org.springframework.boot.actuate.env.EnvironmentEndpoint; import org.springframework.boot.actuate.env.EnvironmentEndpoint.EnvironmentDescriptor; import org.springframework.boot.actuate.env.EnvironmentEndpoint.PropertySourceDescriptor; @@ -33,6 +35,7 @@ import org.springframework.boot.test.context.runner.ContextConsumer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.annotation.Order; +import org.springframework.test.util.ReflectionTestUtils; import static org.assertj.core.api.Assertions.assertThat; @@ -64,17 +67,10 @@ class EnvironmentEndpointAutoConfigurationTests { this.contextRunner.run((context) -> assertThat(context).doesNotHaveBean(EnvironmentEndpoint.class)); } - @Test - void keysToSanitizeCanBeConfiguredViaTheEnvironment() { - this.contextRunner.withPropertyValues("management.endpoints.web.exposure.include=env") - .withSystemProperties("dbPassword=123456", "apiKey=123456") - .withPropertyValues("management.endpoint.env.keys-to-sanitize=.*pass.*") - .run(validateSystemProperties("******", "123456")); - } - @Test void customSanitizingFunctionsAreAppliedInOrder() { this.contextRunner.withUserConfiguration(SanitizingFunctionConfiguration.class) + .withPropertyValues("management.endpoint.env.show-values: WHEN_AUTHORIZED") .withPropertyValues("management.endpoints.web.exposure.include=env") .withSystemProperties("custom=123456", "password=123456").run((context) -> { assertThat(context).hasSingleBean(EnvironmentEndpoint.class); @@ -88,11 +84,34 @@ class EnvironmentEndpointAutoConfigurationTests { } @Test - void additionalKeysToSanitizeCanBeConfiguredViaTheEnvironment() { - this.contextRunner.withPropertyValues("management.endpoints.web.exposure.include=env") - .withSystemProperties("dbPassword=123456", "apiKey=123456") - .withPropertyValues("management.endpoint.env.additional-keys-to-sanitize=key") - .run(validateSystemProperties("******", "******")); + @SuppressWarnings("unchecked") + void rolesCanBeConfiguredViaTheEnvironment() { + this.contextRunner.withPropertyValues("management.endpoint.env.roles: test") + .withPropertyValues("management.endpoints.web.exposure.include=env") + .withSystemProperties("dbPassword=123456", "apiKey=123456").run((context) -> { + assertThat(context).hasSingleBean(EnvironmentEndpointWebExtension.class); + EnvironmentEndpointWebExtension endpoint = context.getBean(EnvironmentEndpointWebExtension.class); + Set roles = (Set) ReflectionTestUtils.getField(endpoint, "roles"); + assertThat(roles.contains("test")).isTrue(); + }); + } + + @Test + @SuppressWarnings("unchecked") + void showValuesCanBeConfiguredViaTheEnvironment() { + this.contextRunner.withPropertyValues("management.endpoint.env.show-values: WHEN_AUTHORIZED") + .withPropertyValues("management.endpoints.web.exposure.include=env") + .withSystemProperties("dbPassword=123456", "apiKey=123456").run((context) -> { + assertThat(context).hasSingleBean(EnvironmentEndpoint.class); + assertThat(context).hasSingleBean(EnvironmentEndpointWebExtension.class); + EnvironmentEndpointWebExtension webExtension = context + .getBean(EnvironmentEndpointWebExtension.class); + EnvironmentEndpoint endpoint = context.getBean(EnvironmentEndpoint.class); + Show showValuesWebExtension = (Show) ReflectionTestUtils.getField(webExtension, "showValues"); + assertThat(showValuesWebExtension).isEqualTo(Show.WHEN_AUTHORIZED); + Show showValues = (Show) ReflectionTestUtils.getField(endpoint, "showValues"); + assertThat(showValues).isEqualTo(Show.WHEN_AUTHORIZED); + }); } @Test diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/AutoConfiguredHealthEndpointGroupTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/AutoConfiguredHealthEndpointGroupTests.java index 7c30ed2ed10..641a23fc4c2 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/AutoConfiguredHealthEndpointGroupTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/AutoConfiguredHealthEndpointGroupTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2022 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. @@ -25,8 +25,8 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.boot.actuate.autoconfigure.health.HealthProperties.Show; import org.springframework.boot.actuate.endpoint.SecurityContext; +import org.springframework.boot.actuate.endpoint.Show; import org.springframework.boot.actuate.health.HttpCodeStatusMapper; import org.springframework.boot.actuate.health.StatusAggregator; import org.springframework.security.core.Authentication; diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/quartz/QuartzEndpointAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/quartz/QuartzEndpointAutoConfigurationTests.java index 4b6cb39180b..c535fd95eba 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/quartz/QuartzEndpointAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/quartz/QuartzEndpointAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,15 +16,20 @@ package org.springframework.boot.actuate.autoconfigure.quartz; +import java.util.Collections; +import java.util.Set; + import org.junit.jupiter.api.Test; import org.quartz.Scheduler; +import org.springframework.boot.actuate.endpoint.Show; import org.springframework.boot.actuate.quartz.QuartzEndpoint; import org.springframework.boot.actuate.quartz.QuartzEndpointWebExtension; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.test.util.ReflectionTestUtils; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; @@ -81,6 +86,34 @@ class QuartzEndpointAutoConfigurationTests { .doesNotHaveBean(QuartzEndpointWebExtension.class)); } + @Test + @SuppressWarnings("unchecked") + void rolesCanBeConfiguredViaTheEnvironment() { + this.contextRunner.withBean(Scheduler.class, () -> mock(Scheduler.class)) + .withPropertyValues("management.endpoint.quartz.roles: test") + .withPropertyValues("management.endpoints.web.exposure.include=quartz") + .withSystemProperties("dbPassword=123456", "apiKey=123456").run((context) -> { + assertThat(context).hasSingleBean(QuartzEndpointWebExtension.class); + QuartzEndpointWebExtension endpoint = context.getBean(QuartzEndpointWebExtension.class); + Set roles = (Set) ReflectionTestUtils.getField(endpoint, "roles"); + assertThat(roles.contains("test")).isTrue(); + }); + } + + @Test + @SuppressWarnings("unchecked") + void showValuesCanBeConfiguredViaTheEnvironment() { + this.contextRunner.withBean(Scheduler.class, () -> mock(Scheduler.class)) + .withPropertyValues("management.endpoint.quartz.show-values: WHEN_AUTHORIZED") + .withPropertyValues("management.endpoints.web.exposure.include=quartz") + .withSystemProperties("dbPassword=123456", "apiKey=123456").run((context) -> { + assertThat(context).hasSingleBean(QuartzEndpointWebExtension.class); + QuartzEndpointWebExtension webExtension = context.getBean(QuartzEndpointWebExtension.class); + Show showValuesWebExtension = (Show) ReflectionTestUtils.getField(webExtension, "showValues"); + assertThat(showValuesWebExtension).isEqualTo(Show.WHEN_AUTHORIZED); + }); + } + @Configuration(proxyBeanMethods = false) static class CustomEndpointConfiguration { @@ -94,7 +127,7 @@ class QuartzEndpointAutoConfigurationTests { private static final class CustomEndpoint extends QuartzEndpoint { private CustomEndpoint() { - super(mock(Scheduler.class)); + super(mock(Scheduler.class), Collections.emptyList()); } } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpoint.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpoint.java index 045ff830031..4756391471a 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpoint.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpoint.java @@ -55,6 +55,7 @@ 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.SanitizingFunction; +import org.springframework.boot.actuate.endpoint.Show; import org.springframework.boot.actuate.endpoint.annotation.Endpoint; import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; import org.springframework.boot.actuate.endpoint.annotation.Selector; @@ -83,11 +84,11 @@ import org.springframework.util.StringUtils; * {@link ConfigurationProperties @ConfigurationProperties} annotated beans. * *

- * To protect sensitive information from being exposed, certain property values are masked - * if their names end with a set of configurable values (default "password" and "secret"). - * Configure property names by using - * {@code management.endpoint.configprops.keys-to-sanitize} in your Spring Boot - * application configuration. + * To protect sensitive information from being exposed, all property values are masked by + * default. To configure when property values should be shown, use + * {@code management.endpoint.configprops.show-values} and + * {@code management.endpoint.configprops.roles} in your Spring Boot application + * configuration. * * @author Christian Dupuis * @author Dave Syer @@ -104,16 +105,15 @@ public class ConfigurationPropertiesReportEndpoint implements ApplicationContext private final Sanitizer sanitizer; + private final Show showValues; + private ApplicationContext context; private ObjectMapper objectMapper; - public ConfigurationPropertiesReportEndpoint() { - this(Collections.emptyList()); - } - - public ConfigurationPropertiesReportEndpoint(Iterable sanitizingFunctions) { + public ConfigurationPropertiesReportEndpoint(Iterable sanitizingFunctions, Show showValues) { this.sanitizer = new Sanitizer(sanitizingFunctions); + this.showValues = showValues; } @Override @@ -121,31 +121,35 @@ public class ConfigurationPropertiesReportEndpoint implements ApplicationContext this.context = context; } - public void setKeysToSanitize(String... keysToSanitize) { - this.sanitizer.setKeysToSanitize(keysToSanitize); - } - - public void keysToSanitize(String... keysToSanitize) { - this.sanitizer.keysToSanitize(keysToSanitize); - } - @ReadOperation public ApplicationConfigurationProperties configurationProperties() { - return extract(this.context, (bean) -> true); + boolean showUnsanitized = this.showValues.isShown(true); + return getConfigurationProperties(showUnsanitized); + } + + ApplicationConfigurationProperties getConfigurationProperties(boolean showUnsanitized) { + return getConfigurationProperties(this.context, (bean) -> true, showUnsanitized); } @ReadOperation public ApplicationConfigurationProperties configurationPropertiesWithPrefix(@Selector String prefix) { - return extract(this.context, (bean) -> bean.getAnnotation().prefix().startsWith(prefix)); + boolean showUnsanitized = this.showValues.isShown(true); + return getConfigurationProperties(prefix, showUnsanitized); } - private ApplicationConfigurationProperties extract(ApplicationContext context, - Predicate beanFilterPredicate) { + ApplicationConfigurationProperties getConfigurationProperties(String prefix, boolean showUnsanitized) { + return getConfigurationProperties(this.context, (bean) -> bean.getAnnotation().prefix().startsWith(prefix), + showUnsanitized); + } + + private ApplicationConfigurationProperties getConfigurationProperties(ApplicationContext context, + Predicate beanFilterPredicate, boolean showUnsanitized) { ObjectMapper mapper = getObjectMapper(); Map contexts = new HashMap<>(); ApplicationContext target = context; + while (target != null) { - contexts.put(target.getId(), describeBeans(mapper, target, beanFilterPredicate)); + contexts.put(target.getId(), describeBeans(mapper, target, beanFilterPredicate, showUnsanitized)); target = target.getParent(); } return new ApplicationConfigurationProperties(contexts); @@ -196,20 +200,21 @@ public class ConfigurationPropertiesReportEndpoint implements ApplicationContext } private ContextConfigurationProperties describeBeans(ObjectMapper mapper, ApplicationContext context, - Predicate beanFilterPredicate) { + Predicate beanFilterPredicate, boolean showUnsanitized) { Map beans = ConfigurationPropertiesBean.getAll(context); Map descriptors = beans.values().stream() - .filter(beanFilterPredicate) - .collect(Collectors.toMap(ConfigurationPropertiesBean::getName, (bean) -> describeBean(mapper, bean))); + .filter(beanFilterPredicate).collect(Collectors.toMap(ConfigurationPropertiesBean::getName, + (bean) -> describeBean(mapper, bean, showUnsanitized))); return new ContextConfigurationProperties(descriptors, (context.getParent() != null) ? context.getParent().getId() : null); } - private ConfigurationPropertiesBeanDescriptor describeBean(ObjectMapper mapper, ConfigurationPropertiesBean bean) { + private ConfigurationPropertiesBeanDescriptor describeBean(ObjectMapper mapper, ConfigurationPropertiesBean bean, + boolean showUnsanitized) { String prefix = bean.getAnnotation().prefix(); Map serialized = safeSerialize(mapper, bean.getInstance(), prefix); - Map properties = sanitize(prefix, serialized); - Map inputs = getInputs(prefix, serialized); + Map properties = sanitize(prefix, serialized, showUnsanitized); + Map inputs = getInputs(prefix, serialized, showUnsanitized); return new ConfigurationPropertiesBeanDescriptor(prefix, properties, inputs); } @@ -236,35 +241,36 @@ public class ConfigurationPropertiesReportEndpoint implements ApplicationContext * information. * @param prefix the property prefix * @param map the source map + * @param showUnsanitized whether to show the unsanitized values * @return the sanitized map */ @SuppressWarnings("unchecked") - private Map sanitize(String prefix, Map map) { + private Map sanitize(String prefix, Map map, boolean showUnsanitized) { map.forEach((key, value) -> { String qualifiedKey = getQualifiedKey(prefix, key); if (value instanceof Map) { - map.put(key, sanitize(qualifiedKey, (Map) value)); + map.put(key, sanitize(qualifiedKey, (Map) value, showUnsanitized)); } else if (value instanceof List) { - map.put(key, sanitize(qualifiedKey, (List) value)); + map.put(key, sanitize(qualifiedKey, (List) value, showUnsanitized)); } else { - map.put(key, sanitizeWithPropertySourceIfPresent(qualifiedKey, value)); + map.put(key, sanitizeWithPropertySourceIfPresent(qualifiedKey, value, showUnsanitized)); } }); return map; } - private Object sanitizeWithPropertySourceIfPresent(String qualifiedKey, Object value) { + private Object sanitizeWithPropertySourceIfPresent(String qualifiedKey, Object value, boolean showUnsanitized) { 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); + return this.sanitizer.sanitize(data, showUnsanitized); } SanitizableData data = new SanitizableData(null, qualifiedKey, value); - return this.sanitizer.sanitize(data); + return this.sanitizer.sanitize(data, showUnsanitized); } private PropertySource getPropertySource(ConfigurationProperty configurationProperty) { @@ -293,69 +299,69 @@ public class ConfigurationPropertiesReportEndpoint implements ApplicationContext } @SuppressWarnings("unchecked") - private List sanitize(String prefix, List list) { + private List sanitize(String prefix, List list, boolean showUnsanitized) { List sanitized = new ArrayList<>(); int index = 0; for (Object item : list) { String name = prefix + "[" + index++ + "]"; if (item instanceof Map) { - sanitized.add(sanitize(name, (Map) item)); + sanitized.add(sanitize(name, (Map) item, showUnsanitized)); } else if (item instanceof List) { - sanitized.add(sanitize(name, (List) item)); + sanitized.add(sanitize(name, (List) item, showUnsanitized)); } else { - sanitized.add(sanitizeWithPropertySourceIfPresent(name, item)); + sanitized.add(sanitizeWithPropertySourceIfPresent(name, item, showUnsanitized)); } } return sanitized; } @SuppressWarnings("unchecked") - private Map getInputs(String prefix, Map map) { + private Map getInputs(String prefix, Map map, boolean showUnsanitized) { Map augmented = new LinkedHashMap<>(map); map.forEach((key, value) -> { String qualifiedKey = getQualifiedKey(prefix, key); if (value instanceof Map) { - augmented.put(key, getInputs(qualifiedKey, (Map) value)); + augmented.put(key, getInputs(qualifiedKey, (Map) value, showUnsanitized)); } else if (value instanceof List) { - augmented.put(key, getInputs(qualifiedKey, (List) value)); + augmented.put(key, getInputs(qualifiedKey, (List) value, showUnsanitized)); } else { - augmented.put(key, applyInput(qualifiedKey)); + augmented.put(key, applyInput(qualifiedKey, showUnsanitized)); } }); return augmented; } @SuppressWarnings("unchecked") - private List getInputs(String prefix, List list) { + private List getInputs(String prefix, List list, boolean showUnsanitized) { List augmented = new ArrayList<>(); int index = 0; for (Object item : list) { String name = prefix + "[" + index++ + "]"; if (item instanceof Map) { - augmented.add(getInputs(name, (Map) item)); + augmented.add(getInputs(name, (Map) item, showUnsanitized)); } else if (item instanceof List) { - augmented.add(getInputs(name, (List) item)); + augmented.add(getInputs(name, (List) item, showUnsanitized)); } else { - augmented.add(applyInput(name)); + augmented.add(applyInput(name, showUnsanitized)); } } return augmented; } - private Map applyInput(String qualifiedKey) { + private Map applyInput(String qualifiedKey, boolean showUnsanitized) { ConfigurationPropertyName currentName = getCurrentName(qualifiedKey); ConfigurationProperty candidate = getCandidate(currentName); PropertySource propertySource = getPropertySource(candidate); if (propertySource != null) { Object value = stringifyIfNecessary(candidate.getValue()); SanitizableData data = new SanitizableData(propertySource, currentName.toString(), value); - return getInput(candidate, this.sanitizer.sanitize(data)); + return getInput(candidate, this.sanitizer.sanitize(data, showUnsanitized)); } return Collections.emptyMap(); } @@ -550,7 +556,7 @@ public class ConfigurationPropertiesReportEndpoint implements ApplicationContext private final Map contexts; - private ApplicationConfigurationProperties(Map contexts) { + ApplicationConfigurationProperties(Map contexts) { this.contexts = contexts; } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpointWebExtension.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpointWebExtension.java index 8f3b5db2614..bb1c2c151bd 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpointWebExtension.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpointWebExtension.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,11 @@ package org.springframework.boot.actuate.context.properties; +import java.util.Set; + import org.springframework.boot.actuate.context.properties.ConfigurationPropertiesReportEndpoint.ApplicationConfigurationProperties; +import org.springframework.boot.actuate.endpoint.SecurityContext; +import org.springframework.boot.actuate.endpoint.Show; import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; import org.springframework.boot.actuate.endpoint.annotation.Selector; import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse; @@ -34,15 +38,29 @@ public class ConfigurationPropertiesReportEndpointWebExtension { private final ConfigurationPropertiesReportEndpoint delegate; - public ConfigurationPropertiesReportEndpointWebExtension(ConfigurationPropertiesReportEndpoint delegate) { + private final Show showValues; + + private final Set roles; + + public ConfigurationPropertiesReportEndpointWebExtension(ConfigurationPropertiesReportEndpoint delegate, + Show showValues, Set roles) { this.delegate = delegate; + this.showValues = showValues; + this.roles = roles; + } + + @ReadOperation + public ApplicationConfigurationProperties configurationProperties(SecurityContext securityContext) { + boolean showUnsanitized = this.showValues.isShown(securityContext, this.roles); + return this.delegate.getConfigurationProperties(showUnsanitized); } @ReadOperation public WebEndpointResponse configurationPropertiesWithPrefix( - @Selector String prefix) { - ApplicationConfigurationProperties configurationProperties = this.delegate - .configurationPropertiesWithPrefix(prefix); + SecurityContext securityContext, @Selector String prefix) { + boolean showUnsanitized = this.showValues.isShown(securityContext, this.roles); + ApplicationConfigurationProperties configurationProperties = this.delegate.getConfigurationProperties(prefix, + showUnsanitized); boolean foundMatchingBeans = configurationProperties.getContexts().values().stream() .anyMatch((context) -> !context.getBeans().isEmpty()); return (foundMatchingBeans) ? new WebEndpointResponse<>(configurationProperties, WebEndpointResponse.STATUS_OK) 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 02e3ac96bcf..9514ed6379e 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 @@ -17,17 +17,8 @@ package org.springframework.boot.actuate.endpoint; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; -import java.util.LinkedHashSet; import java.util.List; -import java.util.Set; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import java.util.stream.Collectors; - -import org.springframework.util.Assert; -import org.springframework.util.StringUtils; /** * Strategy that should be used by endpoint implementations to sanitize potentially @@ -46,148 +37,40 @@ import org.springframework.util.StringUtils; */ public class Sanitizer { - private static final String[] REGEX_PARTS = { "*", "$", "^", "+" }; - - private static final Set DEFAULT_KEYS_TO_SANITIZE = new LinkedHashSet<>( - Arrays.asList("password", "secret", "key", "token", ".*credentials.*", "vcap_services", - "^vcap\\.services.*$", "sun.java.command", "^spring[._]application[._]json$")); - - private static final Set URI_USERINFO_KEYS = new LinkedHashSet<>( - Arrays.asList("uri", "uris", "url", "urls", "address", "addresses")); - - private static final Pattern URI_USERINFO_PATTERN = Pattern - .compile("^\\[?[A-Za-z][A-Za-z0-9\\+\\.\\-]+://.+:(.*)@.+$"); - - private Pattern[] keysToSanitize; - private final List sanitizingFunctions = new ArrayList<>(); - static { - DEFAULT_KEYS_TO_SANITIZE.addAll(URI_USERINFO_KEYS); - } - /** - * Create a new {@link Sanitizer} instance with a default set of keys to sanitize. + * Create a new {@link Sanitizer} instance. */ public Sanitizer() { - this(DEFAULT_KEYS_TO_SANITIZE.toArray(new String[0])); + this(Collections.emptyList()); } /** - * Create a new {@link Sanitizer} instance with specific keys to sanitize. - * @param keysToSanitize the keys to sanitize - */ - public Sanitizer(String... keysToSanitize) { - this(Collections.emptyList(), keysToSanitize); - } - - /** - * Create a new {@link Sanitizer} instance with a default set of keys to sanitize and - * additional sanitizing functions. + * Create a new {@link Sanitizer} instance with sanitizing functions. * @param sanitizingFunctions the sanitizing functions to apply * @since 2.6.0 */ public Sanitizer(Iterable sanitizingFunctions) { - this(sanitizingFunctions, DEFAULT_KEYS_TO_SANITIZE.toArray(new String[0])); - } - - /** - * Create a new {@link Sanitizer} instance with specific keys to sanitize and - * additional sanitizing functions. - * @param sanitizingFunctions the sanitizing functions to apply - * @param keysToSanitize the keys to sanitize - * @since 2.6.0 - */ - public Sanitizer(Iterable sanitizingFunctions, String... keysToSanitize) { sanitizingFunctions.forEach(this.sanitizingFunctions::add); - this.sanitizingFunctions.add(getDefaultSanitizingFunction()); - 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 - * can be simple strings that the property ends with or regular expressions. - * @param keysToSanitize the keys to sanitize - */ - public void setKeysToSanitize(String... keysToSanitize) { - Assert.notNull(keysToSanitize, "KeysToSanitize must not be null"); - this.keysToSanitize = new Pattern[keysToSanitize.length]; - for (int i = 0; i < keysToSanitize.length; i++) { - this.keysToSanitize[i] = getPattern(keysToSanitize[i]); - } - } - - /** - * Adds keys that should be sanitized. Keys can be simple strings that the property - * ends with or regular expressions. - * @param keysToSanitize the keys to sanitize - * @since 2.5.0 - */ - public void keysToSanitize(String... keysToSanitize) { - Assert.notNull(keysToSanitize, "KeysToSanitize must not be null"); - int existingKeys = this.keysToSanitize.length; - this.keysToSanitize = Arrays.copyOf(this.keysToSanitize, this.keysToSanitize.length + keysToSanitize.length); - for (int i = 0; i < keysToSanitize.length; i++) { - this.keysToSanitize[i + existingKeys] = getPattern(keysToSanitize[i]); - } - } - - private Pattern getPattern(String value) { - if (isRegex(value)) { - return Pattern.compile(value, Pattern.CASE_INSENSITIVE); - } - return Pattern.compile(".*" + value + "$", Pattern.CASE_INSENSITIVE); - } - - private boolean isRegex(String value) { - for (String part : REGEX_PARTS) { - if (value.contains(part)) { - return true; - } - } - return false; - } - - /** - * Sanitize the given value if necessary. - * @param key the key to sanitize - * @param value the value - * @return the potentially sanitized value - */ - public Object sanitize(String key, Object value) { - if (value == null) { - return null; - } - for (Pattern pattern : this.keysToSanitize) { - if (pattern.matcher(key).matches()) { - if (keyIsUriWithUserInfo(pattern)) { - return sanitizeUris(value.toString()); - } - return SanitizableData.SANITIZED_VALUE; - } - } - return value; } /** * Sanitize the value from the given {@link SanitizableData} using the available * {@link SanitizingFunction}s. * @param data the sanitizable data + * @param showUnsanitized whether to show the unsanitized values or not * @return the potentially updated data - * @since 2.6.0 + * @since 3.0.0 */ - public Object sanitize(SanitizableData data) { + public Object sanitize(SanitizableData data, boolean showUnsanitized) { Object value = data.getValue(); if (value == null) { return null; } + if (!showUnsanitized) { + return SanitizableData.SANITIZED_VALUE; + } for (SanitizingFunction sanitizingFunction : this.sanitizingFunctions) { data = sanitizingFunction.apply(data); Object sanitizedValue = data.getValue(); @@ -198,26 +81,4 @@ public class Sanitizer { return value; } - private boolean keyIsUriWithUserInfo(Pattern pattern) { - for (String uriKey : URI_USERINFO_KEYS) { - if (pattern.matcher(uriKey).matches()) { - return true; - } - } - return false; - } - - private Object sanitizeUris(String value) { - return Arrays.stream(value.split(",")).map(this::sanitizeUri).collect(Collectors.joining(",")); - } - - private String sanitizeUri(String value) { - Matcher matcher = URI_USERINFO_PATTERN.matcher(value); - String password = matcher.matches() ? matcher.group(1) : null; - if (password != null) { - return StringUtils.replace(value, ":" + password + "@", ":" + SanitizableData.SANITIZED_VALUE + "@"); - } - return value; - } - } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/Show.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/Show.java new file mode 100644 index 00000000000..8af2fb35b9b --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/Show.java @@ -0,0 +1,108 @@ +/* + * Copyright 2012-2022 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.security.Principal; +import java.util.Collection; + +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.util.ClassUtils; +import org.springframework.util.CollectionUtils; + +/** + * Options for showing data in endpoint responses. + * + * @author Madhura Bhave + * @since 3.0.0 + */ +public enum Show { + + /** + * Never show the item in the response. + */ + NEVER, + + /** + * Show the item in the response when accessed by an authorized user. + */ + WHEN_AUTHORIZED, + + /** + * Always show the item in the response. + */ + ALWAYS; + + /** + * Return if data should be shown when no {@link SecurityContext} is available. + * @param unauthorizedResult the result to used for an unauthorized user + * @return if data should be shown + */ + public boolean isShown(boolean unauthorizedResult) { + return switch (this) { + case NEVER -> false; + case ALWAYS -> true; + case WHEN_AUTHORIZED -> unauthorizedResult; + }; + } + + /** + * Return if data should be shown. + * @param securityContext the security context + * @param roles the required roles + * @return if data should be shown + */ + public boolean isShown(SecurityContext securityContext, Collection roles) { + return switch (this) { + case NEVER -> false; + case ALWAYS -> true; + case WHEN_AUTHORIZED -> isAuthorized(securityContext, roles); + }; + } + + private boolean isAuthorized(SecurityContext securityContext, Collection roles) { + Principal principal = securityContext.getPrincipal(); + if (principal == null) { + return false; + } + if (CollectionUtils.isEmpty(roles)) { + return true; + } + boolean checkAuthorities = isSpringSecurityAuthentication(principal); + for (String role : roles) { + if (securityContext.isUserInRole(role)) { + return true; + } + if (checkAuthorities) { + Authentication authentication = (Authentication) principal; + for (GrantedAuthority authority : authentication.getAuthorities()) { + String name = authority.getAuthority(); + if (role.equals(name)) { + return true; + } + } + } + } + return false; + } + + private boolean isSpringSecurityAuthentication(Principal principal) { + return ClassUtils.isPresent("org.springframework.security.core.Authentication", null) + && (principal instanceof Authentication); + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/env/EnvironmentEndpoint.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/env/EnvironmentEndpoint.java index 28fa44c22bb..86de6242f23 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/env/EnvironmentEndpoint.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/env/EnvironmentEndpoint.java @@ -18,7 +18,6 @@ package org.springframework.boot.actuate.env; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -31,6 +30,7 @@ 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.SanitizingFunction; +import org.springframework.boot.actuate.endpoint.Show; import org.springframework.boot.actuate.endpoint.annotation.Endpoint; import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; import org.springframework.boot.actuate.endpoint.annotation.Selector; @@ -48,9 +48,7 @@ import org.springframework.core.env.PropertySource; import org.springframework.core.env.StandardEnvironment; import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; -import org.springframework.util.PropertyPlaceholderHelper; import org.springframework.util.StringUtils; -import org.springframework.util.SystemPropertyUtils; /** * {@link Endpoint @Endpoint} to expose {@link ConfigurableEnvironment environment} @@ -71,50 +69,48 @@ public class EnvironmentEndpoint { private final Environment environment; - public EnvironmentEndpoint(Environment environment) { - this(environment, Collections.emptyList()); - } + private final Show showValues; - public EnvironmentEndpoint(Environment environment, Iterable sanitizingFunctions) { + public EnvironmentEndpoint(Environment environment, Iterable sanitizingFunctions, + Show showValues) { this.environment = environment; this.sanitizer = new Sanitizer(sanitizingFunctions); - } - - public void setKeysToSanitize(String... keysToSanitize) { - this.sanitizer.setKeysToSanitize(keysToSanitize); - } - - public void keysToSanitize(String... keysToSanitize) { - this.sanitizer.keysToSanitize(keysToSanitize); + this.showValues = showValues; } @ReadOperation public EnvironmentDescriptor environment(@Nullable String pattern) { + boolean showUnsanitized = this.showValues.isShown(true); + return getEnvironmentDescriptor(pattern, showUnsanitized); + } + + EnvironmentDescriptor getEnvironmentDescriptor(String pattern, boolean showUnsanitized) { if (StringUtils.hasText(pattern)) { - return getEnvironmentDescriptor(Pattern.compile(pattern).asPredicate()); + return getEnvironmentDescriptor(Pattern.compile(pattern).asPredicate(), showUnsanitized); } - return getEnvironmentDescriptor((name) -> true); + return getEnvironmentDescriptor((name) -> true, showUnsanitized); } - @ReadOperation - public EnvironmentEntryDescriptor environmentEntry(@Selector String toMatch) { - return getEnvironmentEntryDescriptor(toMatch); - } - - private EnvironmentDescriptor getEnvironmentDescriptor(Predicate propertyNamePredicate) { - PlaceholdersResolver resolver = getResolver(); + private EnvironmentDescriptor getEnvironmentDescriptor(Predicate propertyNamePredicate, + boolean showUnsanitized) { List propertySources = new ArrayList<>(); getPropertySourcesAsMap().forEach((sourceName, source) -> { if (source instanceof EnumerablePropertySource) { - propertySources.add(describeSource(sourceName, (EnumerablePropertySource) source, resolver, - propertyNamePredicate)); + propertySources.add(describeSource(sourceName, (EnumerablePropertySource) source, + propertyNamePredicate, showUnsanitized)); } }); return new EnvironmentDescriptor(Arrays.asList(this.environment.getActiveProfiles()), propertySources); } - private EnvironmentEntryDescriptor getEnvironmentEntryDescriptor(String propertyName) { - Map descriptors = getPropertySourceDescriptors(propertyName); + @ReadOperation + public EnvironmentEntryDescriptor environmentEntry(@Selector String toMatch) { + boolean showUnsanitized = this.showValues.isShown(true); + return getEnvironmentEntryDescriptor(toMatch, showUnsanitized); + } + + EnvironmentEntryDescriptor getEnvironmentEntryDescriptor(String propertyName, boolean showUnsanitized) { + Map descriptors = getPropertySourceDescriptors(propertyName, showUnsanitized); PropertySummaryDescriptor summary = getPropertySummaryDescriptor(descriptors); return new EnvironmentEntryDescriptor(summary, Arrays.asList(this.environment.getActiveProfiles()), toPropertySourceDescriptors(descriptors)); @@ -136,35 +132,31 @@ public class EnvironmentEndpoint { return null; } - private Map getPropertySourceDescriptors(String propertyName) { + private Map getPropertySourceDescriptors(String propertyName, + boolean showUnsanitized) { Map propertySources = new LinkedHashMap<>(); - PlaceholdersResolver resolver = getResolver(); getPropertySourcesAsMap().forEach((sourceName, source) -> propertySources.put(sourceName, - source.containsProperty(propertyName) ? describeValueOf(propertyName, source, resolver) : null)); + source.containsProperty(propertyName) ? describeValueOf(propertyName, source, showUnsanitized) : null)); return propertySources; } private PropertySourceDescriptor describeSource(String sourceName, EnumerablePropertySource source, - PlaceholdersResolver resolver, Predicate namePredicate) { + Predicate namePredicate, boolean showUnsanitized) { Map properties = new LinkedHashMap<>(); Stream.of(source.getPropertyNames()).filter(namePredicate) - .forEach((name) -> properties.put(name, describeValueOf(name, source, resolver))); + .forEach((name) -> properties.put(name, describeValueOf(name, source, showUnsanitized))); return new PropertySourceDescriptor(sourceName, properties); } @SuppressWarnings("unchecked") - private PropertyValueDescriptor describeValueOf(String name, PropertySource source, - PlaceholdersResolver resolver) { + private PropertyValueDescriptor describeValueOf(String name, PropertySource source, boolean showUnsanitized) { + PlaceholdersResolver resolver = new PropertySourcesPlaceholdersResolver(getPropertySources()); Object resolved = resolver.resolvePlaceholders(source.getProperty(name)); Origin origin = ((source instanceof OriginLookup) ? ((OriginLookup) source).getOrigin(name) : null); - Object sanitizedValue = sanitize(source, name, resolved); + Object sanitizedValue = sanitize(source, name, resolved, showUnsanitized); return new PropertyValueDescriptor(stringifyIfNecessary(sanitizedValue), origin); } - private PlaceholdersResolver getResolver() { - return new PropertySourcesPlaceholdersSanitizingResolver(getPropertySources(), this.sanitizer); - } - private Map> getPropertySourcesAsMap() { Map> map = new LinkedHashMap<>(); for (PropertySource source : getPropertySources()) { @@ -193,8 +185,8 @@ public class EnvironmentEndpoint { } } - private Object sanitize(PropertySource source, String name, Object value) { - return this.sanitizer.sanitize(new SanitizableData(source, name, value)); + private Object sanitize(PropertySource source, String name, Object value, boolean showUnsanitized) { + return this.sanitizer.sanitize(new SanitizableData(source, name, value), showUnsanitized); } protected Object stringifyIfNecessary(Object value) { @@ -208,40 +200,6 @@ public class EnvironmentEndpoint { return "Complex property type " + value.getClass().getName(); } - /** - * {@link PropertySourcesPlaceholdersResolver} that sanitizes sensitive placeholders - * if present. - */ - private static class PropertySourcesPlaceholdersSanitizingResolver extends PropertySourcesPlaceholdersResolver { - - private final Sanitizer sanitizer; - - private final Iterable> sources; - - PropertySourcesPlaceholdersSanitizingResolver(Iterable> sources, Sanitizer sanitizer) { - super(sources, new PropertyPlaceholderHelper(SystemPropertyUtils.PLACEHOLDER_PREFIX, - SystemPropertyUtils.PLACEHOLDER_SUFFIX, SystemPropertyUtils.VALUE_SEPARATOR, true)); - this.sources = sources; - this.sanitizer = sanitizer; - } - - @Override - protected String resolvePlaceholder(String placeholder) { - if (this.sources != null) { - for (PropertySource source : this.sources) { - 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 null; - } - - } - /** * A description of an {@link Environment}. */ @@ -278,7 +236,7 @@ public class EnvironmentEndpoint { private final List propertySources; - private EnvironmentEntryDescriptor(PropertySummaryDescriptor property, List activeProfiles, + EnvironmentEntryDescriptor(PropertySummaryDescriptor property, List activeProfiles, List propertySources) { this.property = property; this.activeProfiles = activeProfiles; diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/env/EnvironmentEndpointWebExtension.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/env/EnvironmentEndpointWebExtension.java index 833b57a2848..417c41bc7a8 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/env/EnvironmentEndpointWebExtension.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/env/EnvironmentEndpointWebExtension.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,11 +16,17 @@ package org.springframework.boot.actuate.env; +import java.util.Set; + +import org.springframework.boot.actuate.endpoint.SecurityContext; +import org.springframework.boot.actuate.endpoint.Show; import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; import org.springframework.boot.actuate.endpoint.annotation.Selector; import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse; import org.springframework.boot.actuate.endpoint.web.annotation.EndpointWebExtension; +import org.springframework.boot.actuate.env.EnvironmentEndpoint.EnvironmentDescriptor; import org.springframework.boot.actuate.env.EnvironmentEndpoint.EnvironmentEntryDescriptor; +import org.springframework.lang.Nullable; /** * {@link EndpointWebExtension @EndpointWebExtension} for the {@link EnvironmentEndpoint}. @@ -34,13 +40,27 @@ public class EnvironmentEndpointWebExtension { private final EnvironmentEndpoint delegate; - public EnvironmentEndpointWebExtension(EnvironmentEndpoint delegate) { + private final Show showValues; + + private final Set roles; + + public EnvironmentEndpointWebExtension(EnvironmentEndpoint delegate, Show showValues, Set roles) { this.delegate = delegate; + this.showValues = showValues; + this.roles = roles; } @ReadOperation - public WebEndpointResponse environmentEntry(@Selector String toMatch) { - EnvironmentEntryDescriptor descriptor = this.delegate.environmentEntry(toMatch); + public EnvironmentDescriptor environment(SecurityContext securityContext, @Nullable String pattern) { + boolean showUnsanitized = this.showValues.isShown(securityContext, this.roles); + return this.delegate.getEnvironmentDescriptor(pattern, showUnsanitized); + } + + @ReadOperation + public WebEndpointResponse environmentEntry(SecurityContext securityContext, + @Selector String toMatch) { + boolean showUnsanitized = this.showValues.isShown(securityContext, this.roles); + EnvironmentEntryDescriptor descriptor = this.delegate.getEnvironmentEntryDescriptor(toMatch, showUnsanitized); return (descriptor.getProperty() != null) ? new WebEndpointResponse<>(descriptor, WebEndpointResponse.STATUS_OK) : new WebEndpointResponse<>(WebEndpointResponse.STATUS_NOT_FOUND); } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/quartz/QuartzEndpoint.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/quartz/QuartzEndpoint.java index 7f1a12d72c8..f802bcaeafd 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/quartz/QuartzEndpoint.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/quartz/QuartzEndpoint.java @@ -48,7 +48,9 @@ import org.quartz.Trigger.TriggerState; import org.quartz.TriggerKey; import org.quartz.impl.matchers.GroupMatcher; +import org.springframework.boot.actuate.endpoint.SanitizableData; 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.ReadOperation; import org.springframework.util.Assert; @@ -71,25 +73,10 @@ public class QuartzEndpoint { private final Sanitizer sanitizer; - /** - * Create an instance for the specified {@link Scheduler} using a default - * {@link Sanitizer}. - * @param scheduler the scheduler to use to retrieve jobs and triggers details - */ - public QuartzEndpoint(Scheduler scheduler) { - this(scheduler, new Sanitizer()); - } - - /** - * Create an instance for the specified {@link Scheduler} and {@link Sanitizer}. - * @param scheduler the scheduler to use to retrieve jobs and triggers details - * @param sanitizer the sanitizer to use to sanitize data maps - */ - public QuartzEndpoint(Scheduler scheduler, Sanitizer sanitizer) { + public QuartzEndpoint(Scheduler scheduler, Iterable sanitizingFunctions) { Assert.notNull(scheduler, "Scheduler must not be null"); - Assert.notNull(sanitizer, "Sanitizer must not be null"); this.scheduler = scheduler; - this.sanitizer = sanitizer; + this.sanitizer = new Sanitizer(sanitizingFunctions); } /** @@ -201,17 +188,19 @@ public class QuartzEndpoint { * group name and job name. * @param groupName the name of the group * @param jobName the name of the job + * @param showUnsanitized whether to sanitize values in data map * @return the details of the job or {@code null} if such job does not exist * @throws SchedulerException if retrieving the information from the scheduler failed */ - public QuartzJobDetails quartzJob(String groupName, String jobName) throws SchedulerException { + public QuartzJobDetails quartzJob(String groupName, String jobName, boolean showUnsanitized) + throws SchedulerException { JobKey jobKey = JobKey.jobKey(jobName, groupName); JobDetail jobDetail = this.scheduler.getJobDetail(jobKey); if (jobDetail != null) { List triggers = this.scheduler.getTriggersOfJob(jobKey); return new QuartzJobDetails(jobDetail.getKey().getGroup(), jobDetail.getKey().getName(), jobDetail.getDescription(), jobDetail.getJobClass().getName(), jobDetail.isDurable(), - jobDetail.requestsRecovery(), sanitizeJobDataMap(jobDetail.getJobDataMap()), + jobDetail.requestsRecovery(), sanitizeJobDataMap(jobDetail.getJobDataMap(), showUnsanitized), extractTriggersSummary(triggers)); } return null; @@ -236,14 +225,18 @@ public class QuartzEndpoint { * name. * @param groupName the name of the group * @param triggerName the name of the trigger + * @param showUnsanitized whether to sanitize values in data map * @return the details of the trigger or {@code null} if such trigger does not exist * @throws SchedulerException if retrieving the information from the scheduler failed */ - public Map quartzTrigger(String groupName, String triggerName) throws SchedulerException { + Map quartzTrigger(String groupName, String triggerName, boolean showUnsanitized) + throws SchedulerException { TriggerKey triggerKey = TriggerKey.triggerKey(triggerName, groupName); Trigger trigger = this.scheduler.getTrigger(triggerKey); - return (trigger != null) ? TriggerDescription.of(trigger).buildDetails( - this.scheduler.getTriggerState(triggerKey), sanitizeJobDataMap(trigger.getJobDataMap())) : null; + return (trigger != null) + ? TriggerDescription.of(trigger).buildDetails(this.scheduler.getTriggerState(triggerKey), + sanitizeJobDataMap(trigger.getJobDataMap(), showUnsanitized)) + : null; } private static Duration getIntervalDuration(long amount, IntervalUnit unit) { @@ -255,15 +248,20 @@ public class QuartzEndpoint { : null; } - private Map sanitizeJobDataMap(JobDataMap dataMap) { + private Map sanitizeJobDataMap(JobDataMap dataMap, boolean showUnsanitized) { if (dataMap != null) { Map map = new LinkedHashMap<>(dataMap.getWrappedMap()); - map.replaceAll(this.sanitizer::sanitize); + map.replaceAll((key, value) -> getSanitizedValue(showUnsanitized, key, value)); return map; } return null; } + private Object getSanitizedValue(boolean showUnsanitized, String key, Object value) { + SanitizableData data = new SanitizableData(null, key, value); + return this.sanitizer.sanitize(data, showUnsanitized); + } + private static TemporalUnit temporalUnit(IntervalUnit unit) { return switch (unit) { case DAY -> ChronoUnit.DAYS; diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/quartz/QuartzEndpointWebExtension.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/quartz/QuartzEndpointWebExtension.java index e3468512e29..ab0aca4856d 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/quartz/QuartzEndpointWebExtension.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/quartz/QuartzEndpointWebExtension.java @@ -16,10 +16,14 @@ package org.springframework.boot.actuate.quartz; +import java.util.Set; + import org.quartz.SchedulerException; import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.RuntimeHintsRegistrar; +import org.springframework.boot.actuate.endpoint.SecurityContext; +import org.springframework.boot.actuate.endpoint.Show; import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; import org.springframework.boot.actuate.endpoint.annotation.Selector; import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse; @@ -44,8 +48,14 @@ public class QuartzEndpointWebExtension { private final QuartzEndpoint delegate; - public QuartzEndpointWebExtension(QuartzEndpoint delegate) { + private final Show showValues; + + private final Set roles; + + public QuartzEndpointWebExtension(QuartzEndpoint delegate, Show showValues, Set roles) { this.delegate = delegate; + this.showValues = showValues; + this.roles = roles; } @ReadOperation @@ -62,10 +72,11 @@ public class QuartzEndpointWebExtension { } @ReadOperation - public WebEndpointResponse quartzJobOrTrigger(@Selector String jobsOrTriggers, @Selector String group, - @Selector String name) throws SchedulerException { - return handle(jobsOrTriggers, () -> this.delegate.quartzJob(group, name), - () -> this.delegate.quartzTrigger(group, name)); + public WebEndpointResponse quartzJobOrTrigger(SecurityContext securityContext, + @Selector String jobsOrTriggers, @Selector String group, @Selector String name) throws SchedulerException { + boolean showUnsanitized = this.showValues.isShown(securityContext, this.roles); + return handle(jobsOrTriggers, () -> this.delegate.quartzJob(group, name, showUnsanitized), + () -> this.delegate.quartzTrigger(group, name, showUnsanitized)); } private WebEndpointResponse handle(String jobsOrTriggers, ResponseSupplier jobAction, diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpointFilteringTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpointFilteringTests.java index ec2f5c7a0b2..e3a54d01c2b 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpointFilteringTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpointFilteringTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,15 +16,21 @@ package org.springframework.boot.actuate.context.properties; +import java.util.Collections; +import java.util.Optional; + import org.junit.jupiter.api.Test; import org.springframework.boot.actuate.context.properties.ConfigurationPropertiesReportEndpoint.ApplicationConfigurationProperties; +import org.springframework.boot.actuate.context.properties.ConfigurationPropertiesReportEndpoint.ConfigurationPropertiesBeanDescriptor; import org.springframework.boot.actuate.context.properties.ConfigurationPropertiesReportEndpoint.ContextConfigurationProperties; +import org.springframework.boot.actuate.endpoint.Show; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; import static org.assertj.core.api.Assertions.assertThat; @@ -39,16 +45,7 @@ class ConfigurationPropertiesReportEndpointFilteringTests { void filterByPrefixSingleMatch() { ApplicationContextRunner contextRunner = new ApplicationContextRunner().withUserConfiguration(Config.class) .withPropertyValues("foo.primary.name:foo1", "foo.secondary.name:foo2", "only.bar.name:solo1"); - contextRunner.run((context) -> { - ConfigurationPropertiesReportEndpoint endpoint = context - .getBean(ConfigurationPropertiesReportEndpoint.class); - ApplicationConfigurationProperties applicationProperties = endpoint - .configurationPropertiesWithPrefix("only.bar"); - assertThat(applicationProperties.getContexts()).containsOnlyKeys(context.getId()); - ContextConfigurationProperties contextProperties = applicationProperties.getContexts().get(context.getId()); - assertThat(contextProperties.getBeans().values()).singleElement().hasFieldOrPropertyWithValue("prefix", - "only.bar"); - }); + assertProperties(contextRunner, "solo1"); } @Test @@ -81,15 +78,84 @@ class ConfigurationPropertiesReportEndpointFilteringTests { }); } + @Test + void noSanitizationWhenShowAlways() { + ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withUserConfiguration(ConfigWithAlways.class) + .withPropertyValues("foo.primary.name:foo1", "foo.secondary.name:foo2", "only.bar.name:solo1"); + assertProperties(contextRunner, "solo1"); + } + + @Test + void sanitizationWhenShowNever() { + ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withUserConfiguration(ConfigWithNever.class) + .withPropertyValues("foo.primary.name:foo1", "foo.secondary.name:foo2", "only.bar.name:solo1"); + assertProperties(contextRunner, "******"); + } + + private void assertProperties(ApplicationContextRunner contextRunner, String value) { + contextRunner.run((context) -> { + ConfigurationPropertiesReportEndpoint endpoint = context + .getBean(ConfigurationPropertiesReportEndpoint.class); + ApplicationConfigurationProperties applicationProperties = endpoint + .configurationPropertiesWithPrefix("only.bar"); + assertThat(applicationProperties.getContexts()).containsOnlyKeys(context.getId()); + ContextConfigurationProperties contextProperties = applicationProperties.getContexts().get(context.getId()); + Optional key = contextProperties.getBeans().keySet().stream() + .filter((id) -> findIdFromPrefix("only.bar", id)).findAny(); + ConfigurationPropertiesBeanDescriptor descriptor = contextProperties.getBeans().get(key.get()); + assertThat(descriptor.getPrefix()).isEqualTo("only.bar"); + assertThat(descriptor.getProperties().get("name")).isEqualTo(value); + }); + } + + private boolean findIdFromPrefix(String prefix, String id) { + int separator = id.indexOf("-"); + String candidate = (separator != -1) ? id.substring(0, separator) : id; + return prefix.equals(candidate); + } + @Configuration(proxyBeanMethods = false) + @Import(BaseConfiguration.class) @EnableConfigurationProperties(Bar.class) static class Config { @Bean ConfigurationPropertiesReportEndpoint endpoint() { - return new ConfigurationPropertiesReportEndpoint(); + return new ConfigurationPropertiesReportEndpoint(Collections.emptyList(), Show.WHEN_AUTHORIZED); } + } + + @Configuration(proxyBeanMethods = false) + @Import(BaseConfiguration.class) + @EnableConfigurationProperties(Bar.class) + static class ConfigWithNever { + + @Bean + ConfigurationPropertiesReportEndpoint endpoint() { + return new ConfigurationPropertiesReportEndpoint(Collections.emptyList(), Show.NEVER); + } + + } + + @Configuration(proxyBeanMethods = false) + @Import(BaseConfiguration.class) + @EnableConfigurationProperties(Bar.class) + static class ConfigWithAlways { + + @Bean + ConfigurationPropertiesReportEndpoint endpoint() { + return new ConfigurationPropertiesReportEndpoint(Collections.emptyList(), Show.ALWAYS); + } + + } + + @Configuration(proxyBeanMethods = false) + @EnableConfigurationProperties(Bar.class) + static class BaseConfiguration { + @Bean @ConfigurationProperties(prefix = "foo.primary") Foo primaryFoo() { diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpointMethodAnnotationsTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpointMethodAnnotationsTests.java index dddd6007f1a..6732426dcc3 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpointMethodAnnotationsTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpointMethodAnnotationsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,11 +16,14 @@ package org.springframework.boot.actuate.context.properties; +import java.util.Collections; + import org.junit.jupiter.api.Test; import org.springframework.boot.actuate.context.properties.ConfigurationPropertiesReportEndpoint.ApplicationConfigurationProperties; import org.springframework.boot.actuate.context.properties.ConfigurationPropertiesReportEndpoint.ConfigurationPropertiesBeanDescriptor; import org.springframework.boot.actuate.context.properties.ConfigurationPropertiesReportEndpoint.ContextConfigurationProperties; +import org.springframework.boot.actuate.endpoint.Show; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.test.context.runner.ApplicationContextRunner; @@ -79,7 +82,7 @@ class ConfigurationPropertiesReportEndpointMethodAnnotationsTests { @Bean ConfigurationPropertiesReportEndpoint endpoint() { - return new ConfigurationPropertiesReportEndpoint(); + return new ConfigurationPropertiesReportEndpoint(Collections.emptyList(), Show.ALWAYS); } @Bean @@ -102,7 +105,7 @@ class ConfigurationPropertiesReportEndpointMethodAnnotationsTests { @Bean ConfigurationPropertiesReportEndpoint endpoint() { - return new ConfigurationPropertiesReportEndpoint(); + return new ConfigurationPropertiesReportEndpoint(Collections.emptyList(), Show.ALWAYS); } @Bean diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpointParentTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpointParentTests.java index 52e9cae993d..0808854014e 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpointParentTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpointParentTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,9 +16,12 @@ package org.springframework.boot.actuate.context.properties; +import java.util.Collections; + import org.junit.jupiter.api.Test; import org.springframework.boot.actuate.context.properties.ConfigurationPropertiesReportEndpoint.ApplicationConfigurationProperties; +import org.springframework.boot.actuate.endpoint.Show; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.test.context.runner.ApplicationContextRunner; @@ -86,7 +89,7 @@ class ConfigurationPropertiesReportEndpointParentTests { @Bean ConfigurationPropertiesReportEndpoint endpoint() { - return new ConfigurationPropertiesReportEndpoint(); + return new ConfigurationPropertiesReportEndpoint(Collections.emptyList(), Show.ALWAYS); } @Bean @@ -102,7 +105,7 @@ class ConfigurationPropertiesReportEndpointParentTests { @Bean ConfigurationPropertiesReportEndpoint endpoint() { - return new ConfigurationPropertiesReportEndpoint(); + return new ConfigurationPropertiesReportEndpoint(Collections.emptyList(), Show.ALWAYS); } @Bean diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpointProxyTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpointProxyTests.java index 61c7a827097..86ef7411220 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpointProxyTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpointProxyTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ package org.springframework.boot.actuate.context.properties; +import java.util.Collections; import java.util.Map; import javax.sql.DataSource; @@ -24,6 +25,7 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.actuate.context.properties.ConfigurationPropertiesReportEndpoint.ApplicationConfigurationProperties; import org.springframework.boot.actuate.context.properties.ConfigurationPropertiesReportEndpoint.ConfigurationPropertiesBeanDescriptor; +import org.springframework.boot.actuate.endpoint.Show; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.test.context.runner.ApplicationContextRunner; @@ -85,7 +87,7 @@ class ConfigurationPropertiesReportEndpointProxyTests { @Bean ConfigurationPropertiesReportEndpoint endpoint() { - return new ConfigurationPropertiesReportEndpoint(); + return new ConfigurationPropertiesReportEndpoint(Collections.emptyList(), Show.ALWAYS); } @Bean diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpointSerializationTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpointSerializationTests.java index 798ff711899..d4e02517a83 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpointSerializationTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpointSerializationTests.java @@ -31,6 +31,7 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.actuate.context.properties.ConfigurationPropertiesReportEndpoint.ApplicationConfigurationProperties; import org.springframework.boot.actuate.context.properties.ConfigurationPropertiesReportEndpoint.ConfigurationPropertiesBeanDescriptor; +import org.springframework.boot.actuate.endpoint.Show; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.ConfigurationPropertiesBinding; import org.springframework.boot.context.properties.EnableConfigurationProperties; @@ -290,7 +291,7 @@ class ConfigurationPropertiesReportEndpointSerializationTests { @Bean ConfigurationPropertiesReportEndpoint endpoint() { - return new ConfigurationPropertiesReportEndpoint(); + return new ConfigurationPropertiesReportEndpoint(Collections.emptyList(), Show.ALWAYS); } } @@ -553,7 +554,7 @@ class ConfigurationPropertiesReportEndpointSerializationTests { @Bean ConfigurationPropertiesReportEndpoint endpoint() { - return new ConfigurationPropertiesReportEndpoint(); + return new ConfigurationPropertiesReportEndpoint(Collections.emptyList(), Show.ALWAYS); } @Bean diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpointTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpointTests.java index 79756319094..8309dd21598 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpointTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpointTests.java @@ -32,6 +32,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.actuate.context.properties.ConfigurationPropertiesReportEndpoint.ConfigurationPropertiesBeanDescriptor; import org.springframework.boot.actuate.context.properties.ConfigurationPropertiesReportEndpoint.ContextConfigurationProperties; import org.springframework.boot.actuate.endpoint.SanitizingFunction; +import org.springframework.boot.actuate.endpoint.Show; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.ConstructorBinding; import org.springframework.boot.context.properties.EnableConfigurationProperties; @@ -45,7 +46,6 @@ import org.springframework.boot.test.context.runner.ContextConsumer; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.core.env.Environment; import org.springframework.mock.env.MockPropertySource; import static org.assertj.core.api.Assertions.assertThat; @@ -175,82 +175,10 @@ class ConfigurationPropertiesReportEndpointTests { (properties) -> assertThat(properties.get("mixedBoolean")).isEqualTo(true))); } - @Test - void sanitizeWithDefaultSettings() { - this.contextRunner.withUserConfiguration(TestPropertiesConfiguration.class) - .run(assertProperties("test", (properties) -> { - assertThat(properties.get("dbPassword")).isEqualTo("******"); - assertThat(properties.get("myTestProperty")).isEqualTo("654321"); - })); - } - - @Test - void sanitizeWithCustomKey() { - this.contextRunner.withUserConfiguration(TestPropertiesConfiguration.class) - .withPropertyValues("test.keys-to-sanitize=property").run(assertProperties("test", (properties) -> { - assertThat(properties.get("dbPassword")).isEqualTo("123456"); - assertThat(properties.get("myTestProperty")).isEqualTo("******"); - })); - } - - @Test - void sanitizeWithCustomKeyPattern() { - this.contextRunner.withUserConfiguration(TestPropertiesConfiguration.class) - .withPropertyValues("test.keys-to-sanitize=.*pass.*").run(assertProperties("test", (properties) -> { - assertThat(properties.get("dbPassword")).isEqualTo("******"); - assertThat(properties.get("myTestProperty")).isEqualTo("654321"); - })); - } - - @Test - void sanitizeWithCustomPatternUsingCompositeKeys() { - this.contextRunner.withUserConfiguration(Gh4415PropertiesConfiguration.class) - .withPropertyValues("test.keys-to-sanitize=.*\\.secrets\\..*,.*\\.hidden\\..*") - .run(assertProperties("gh4415", (properties) -> { - Map secrets = (Map) properties.get("secrets"); - Map hidden = (Map) properties.get("hidden"); - assertThat(secrets.get("mine")).isEqualTo("******"); - assertThat(secrets.get("yours")).isEqualTo("******"); - assertThat(hidden.get("mine")).isEqualTo("******"); - })); - } - - @Test - void sanitizeUriWithSensitiveInfo() { - this.contextRunner.withUserConfiguration(SensiblePropertiesConfiguration.class) - .withPropertyValues("sensible.sensitiveUri=http://user:password@localhost:8080") - .run(assertProperties("sensible", (properties) -> assertThat(properties.get("sensitiveUri")) - .isEqualTo("http://user:******@localhost:8080"), (inputs) -> { - Map sensitiveUri = (Map) inputs.get("sensitiveUri"); - assertThat(sensitiveUri.get("value")).isEqualTo("http://user:******@localhost:8080"); - assertThat(sensitiveUri.get("origin")) - .isEqualTo("\"sensible.sensitiveUri\" from property source \"test\""); - })); - } - - @Test - void sanitizeUriWithNoPassword() { - this.contextRunner.withUserConfiguration(SensiblePropertiesConfiguration.class) - .withPropertyValues("sensible.noPasswordUri=http://user:@localhost:8080") - .run(assertProperties("sensible", (properties) -> assertThat(properties.get("noPasswordUri")) - .isEqualTo("http://user:******@localhost:8080"), (inputs) -> { - Map noPasswordUri = (Map) inputs.get("noPasswordUri"); - assertThat(noPasswordUri.get("value")).isEqualTo("http://user:******@localhost:8080"); - assertThat(noPasswordUri.get("origin")) - .isEqualTo("\"sensible.noPasswordUri\" from property source \"test\""); - })); - } - - @Test - void sanitizeAddressesFieldContainingMultipleRawSensitiveUris() { - this.contextRunner.withUserConfiguration(SensiblePropertiesConfiguration.class) - .run(assertProperties("sensible", (properties) -> assertThat(properties.get("rawSensitiveAddresses")) - .isEqualTo("http://user:******@localhost:8080,http://user2:******@localhost:8082"))); - } - @Test void sanitizeLists() { - this.contextRunner.withUserConfiguration(SensiblePropertiesConfiguration.class) + new ApplicationContextRunner() + .withUserConfiguration(EndpointConfigWithShowNever.class, SensiblePropertiesConfiguration.class) .withPropertyValues("sensible.listItems[0].some-password=password") .run(assertProperties("sensible", (properties) -> { assertThat(properties.get("listItems")).isInstanceOf(List.class); @@ -271,7 +199,8 @@ class ConfigurationPropertiesReportEndpointTests { @Test void listsOfListsAreSanitized() { - this.contextRunner.withUserConfiguration(SensiblePropertiesConfiguration.class) + new ApplicationContextRunner() + .withUserConfiguration(EndpointConfigWithShowNever.class, SensiblePropertiesConfiguration.class) .withPropertyValues("sensible.listOfListItems[0][0].some-password=password") .run(assertProperties("sensible", (properties) -> { assertThat(properties.get("listOfListItems")).isInstanceOf(List.class); @@ -311,7 +240,7 @@ class ConfigurationPropertiesReportEndpointTests { .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("dbPassword")).isEqualTo("123456"); assertThat(properties.get("myTestProperty")).isEqualTo("$$$"); })); } @@ -339,6 +268,26 @@ class ConfigurationPropertiesReportEndpointTests { })); } + @Test + void noSanitizationWhenShowAlways() { + new ApplicationContextRunner() + .withUserConfiguration(EndpointConfigWithShowAlways.class, TestPropertiesConfiguration.class) + .run(assertProperties("test", (properties) -> { + assertThat(properties.get("dbPassword")).isEqualTo("123456"); + assertThat(properties.get("myTestProperty")).isEqualTo("654321"); + })); + } + + @Test + void sanitizationWhenShowNever() { + new ApplicationContextRunner() + .withUserConfiguration(EndpointConfigWithShowNever.class, TestPropertiesConfiguration.class) + .run(assertProperties("test", (properties) -> { + assertThat(properties.get("dbPassword")).isEqualTo("******"); + assertThat(properties.get("myTestProperty")).isEqualTo("******"); + })); + } + @Test void originParents() { this.contextRunner.withUserConfiguration(SensiblePropertiesConfiguration.class) @@ -367,8 +316,9 @@ class ConfigurationPropertiesReportEndpointTests { return (context) -> { ConfigurationPropertiesReportEndpoint endpoint = context .getBean(ConfigurationPropertiesReportEndpoint.class); - ContextConfigurationProperties allProperties = endpoint.configurationProperties().getContexts() - .get(context.getId()); + ConfigurationPropertiesReportEndpoint.ApplicationConfigurationProperties configurationProperties = endpoint + .configurationProperties(); + ContextConfigurationProperties allProperties = configurationProperties.getContexts().get(context.getId()); Optional key = allProperties.getBeans().keySet().stream() .filter((id) -> findIdFromPrefix(prefix, id)).findAny(); assertThat(key).describedAs("No configuration properties with prefix '%s' found", prefix).isPresent(); @@ -421,12 +371,33 @@ class ConfigurationPropertiesReportEndpointTests { static class EndpointConfig { @Bean - ConfigurationPropertiesReportEndpoint endpoint(Environment environment) { - ConfigurationPropertiesReportEndpoint endpoint = new ConfigurationPropertiesReportEndpoint(); - String[] keys = environment.getProperty("test.keys-to-sanitize", String[].class); - if (keys != null) { - endpoint.setKeysToSanitize(keys); - } + ConfigurationPropertiesReportEndpoint endpoint() { + ConfigurationPropertiesReportEndpoint endpoint = new ConfigurationPropertiesReportEndpoint( + Collections.emptyList(), Show.WHEN_AUTHORIZED); + return endpoint; + } + + } + + @Configuration(proxyBeanMethods = false) + static class EndpointConfigWithShowAlways { + + @Bean + ConfigurationPropertiesReportEndpoint endpoint() { + ConfigurationPropertiesReportEndpoint endpoint = new ConfigurationPropertiesReportEndpoint( + Collections.emptyList(), Show.ALWAYS); + return endpoint; + } + + } + + @Configuration(proxyBeanMethods = false) + static class EndpointConfigWithShowNever { + + @Bean + ConfigurationPropertiesReportEndpoint endpoint() { + ConfigurationPropertiesReportEndpoint endpoint = new ConfigurationPropertiesReportEndpoint( + Collections.emptyList(), Show.NEVER); return endpoint; } @@ -891,13 +862,9 @@ class ConfigurationPropertiesReportEndpointTests { static class CustomSanitizingEndpointConfig { @Bean - ConfigurationPropertiesReportEndpoint endpoint(Environment environment, SanitizingFunction sanitizingFunction) { + ConfigurationPropertiesReportEndpoint endpoint(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); - } + Collections.singletonList(sanitizingFunction), Show.ALWAYS); return endpoint; } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpointWebExtensionTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpointWebExtensionTests.java new file mode 100644 index 00000000000..c2cea580c22 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpointWebExtensionTests.java @@ -0,0 +1,96 @@ +/* + * Copyright 2012-2022 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.context.properties; + +import java.security.Principal; +import java.util.Collections; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.actuate.context.properties.ConfigurationPropertiesReportEndpoint.ApplicationConfigurationProperties; +import org.springframework.boot.actuate.endpoint.SecurityContext; +import org.springframework.boot.actuate.endpoint.Show; + +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link ConfigurationPropertiesReportEndpointWebExtension}. + * + * @author Madhura Bhave + */ +class ConfigurationPropertiesReportEndpointWebExtensionTests { + + private ConfigurationPropertiesReportEndpointWebExtension webExtension; + + private ConfigurationPropertiesReportEndpoint delegate; + + @BeforeEach + void setup() { + this.delegate = mock(ConfigurationPropertiesReportEndpoint.class); + } + + @Test + void whenShowValuesIsNever() { + this.webExtension = new ConfigurationPropertiesReportEndpointWebExtension(this.delegate, Show.NEVER, + Collections.emptySet()); + this.webExtension.configurationProperties(null); + then(this.delegate).should().getConfigurationProperties(false); + verifyPrefixed(null, false); + } + + @Test + void whenShowValuesIsAlways() { + this.webExtension = new ConfigurationPropertiesReportEndpointWebExtension(this.delegate, Show.ALWAYS, + Collections.emptySet()); + this.webExtension.configurationProperties(null); + then(this.delegate).should().getConfigurationProperties(true); + verifyPrefixed(null, true); + } + + @Test + void whenShowValuesIsWhenAuthorizedAndSecurityContextIsAuthorized() { + SecurityContext securityContext = mock(SecurityContext.class); + given(securityContext.getPrincipal()).willReturn(mock(Principal.class)); + this.webExtension = new ConfigurationPropertiesReportEndpointWebExtension(this.delegate, Show.WHEN_AUTHORIZED, + Collections.emptySet()); + this.webExtension.configurationProperties(securityContext); + then(this.delegate).should().getConfigurationProperties(true); + verifyPrefixed(securityContext, true); + + } + + @Test + void whenShowValuesIsWhenAuthorizedAndSecurityContextIsNotAuthorized() { + SecurityContext securityContext = mock(SecurityContext.class); + this.webExtension = new ConfigurationPropertiesReportEndpointWebExtension(this.delegate, Show.WHEN_AUTHORIZED, + Collections.emptySet()); + this.webExtension.configurationProperties(securityContext); + then(this.delegate).should().getConfigurationProperties(false); + verifyPrefixed(securityContext, false); + } + + private void verifyPrefixed(SecurityContext securityContext, boolean showUnsanitized) { + given(this.delegate.getConfigurationProperties("test", showUnsanitized)) + .willReturn(new ApplicationConfigurationProperties(Collections.emptyMap())); + this.webExtension.configurationPropertiesWithPrefix(securityContext, "test"); + then(this.delegate).should().getConfigurationProperties("test", showUnsanitized); + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpointWebIntegrationTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpointWebIntegrationTests.java index 80b6b16da45..0fe5bc9fb8a 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpointWebIntegrationTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpointWebIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,8 +16,11 @@ package org.springframework.boot.actuate.context.properties; +import java.util.Collections; + import org.junit.jupiter.api.BeforeEach; +import org.springframework.boot.actuate.endpoint.Show; import org.springframework.boot.actuate.endpoint.web.test.WebEndpointTest; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; @@ -77,13 +80,13 @@ class ConfigurationPropertiesReportEndpointWebIntegrationTests { @Bean ConfigurationPropertiesReportEndpoint endpoint() { - return new ConfigurationPropertiesReportEndpoint(); + return new ConfigurationPropertiesReportEndpoint(Collections.emptyList(), null); } @Bean ConfigurationPropertiesReportEndpointWebExtension endpointWebExtension( ConfigurationPropertiesReportEndpoint endpoint) { - return new ConfigurationPropertiesReportEndpointWebExtension(endpoint); + return new ConfigurationPropertiesReportEndpointWebExtension(endpoint, Show.ALWAYS, Collections.emptySet()); } @Bean 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 fdcc72fe82d..4cf48560476 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 @@ -19,11 +19,8 @@ package org.springframework.boot.actuate.endpoint; import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.stream.Stream; import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.MethodSource; import static org.assertj.core.api.Assertions.assertThat; @@ -39,42 +36,21 @@ import static org.assertj.core.api.Assertions.assertThat; class SanitizerTests { @Test - void defaultNonUriKeys() { + void whenNoSanitizationFunctionAndShowUnsanitizedIsFalse() { Sanitizer sanitizer = new Sanitizer(); - assertThat(sanitizer.sanitize("password", "secret")).isEqualTo("******"); - assertThat(sanitizer.sanitize("my-password", "secret")).isEqualTo("******"); - assertThat(sanitizer.sanitize("my-OTHER.paSSword", "secret")).isEqualTo("******"); - assertThat(sanitizer.sanitize("somesecret", "secret")).isEqualTo("******"); - assertThat(sanitizer.sanitize("somekey", "secret")).isEqualTo("******"); - assertThat(sanitizer.sanitize("token", "secret")).isEqualTo("******"); - assertThat(sanitizer.sanitize("sometoken", "secret")).isEqualTo("******"); - assertThat(sanitizer.sanitize("find", "secret")).isEqualTo("secret"); - assertThat(sanitizer.sanitize("sun.java.command", "--spring.data.redis.password=pa55w0rd")).isEqualTo("******"); - assertThat(sanitizer.sanitize("SPRING_APPLICATION_JSON", "{password:123}")).isEqualTo("******"); - assertThat(sanitizer.sanitize("spring.application.json", "{password:123}")).isEqualTo("******"); - assertThat(sanitizer.sanitize("VCAP_SERVICES", "{json}")).isEqualTo("******"); - assertThat(sanitizer.sanitize("vcap.services.db.codeword", "secret")).isEqualTo("******"); + assertThat(sanitizer.sanitize(new SanitizableData(null, "password", "secret"), false)).isEqualTo("******"); + assertThat(sanitizer.sanitize(new SanitizableData(null, "other", "something"), false)).isEqualTo("******"); } @Test - void whenAdditionalKeysAreAddedValuesOfBothThemAndTheDefaultKeysAreSanitized() { + void whenNoSanitizationFunctionAndShowUnsanitizedIsTrue() { Sanitizer sanitizer = new Sanitizer(); - sanitizer.keysToSanitize("find", "confidential"); - assertThat(sanitizer.sanitize("password", "secret")).isEqualTo("******"); - assertThat(sanitizer.sanitize("my-password", "secret")).isEqualTo("******"); - assertThat(sanitizer.sanitize("my-OTHER.paSSword", "secret")).isEqualTo("******"); - assertThat(sanitizer.sanitize("somesecret", "secret")).isEqualTo("******"); - assertThat(sanitizer.sanitize("somekey", "secret")).isEqualTo("******"); - assertThat(sanitizer.sanitize("token", "secret")).isEqualTo("******"); - assertThat(sanitizer.sanitize("sometoken", "secret")).isEqualTo("******"); - assertThat(sanitizer.sanitize("find", "secret")).isEqualTo("******"); - assertThat(sanitizer.sanitize("sun.java.command", "--spring.data.redis.password=pa55w0rd")).isEqualTo("******"); - assertThat(sanitizer.sanitize("confidential", "secret")).isEqualTo("******"); - assertThat(sanitizer.sanitize("private", "secret")).isEqualTo("secret"); + assertThat(sanitizer.sanitize(new SanitizableData(null, "password", "secret"), true)).isEqualTo("secret"); + assertThat(sanitizer.sanitize(new SanitizableData(null, "other", "something"), true)).isEqualTo("something"); } @Test - void whenCustomSanitizingFunctionPresentValueShouldBeSanitized() { + void whenCustomSanitizationFunctionAndShowUnsanitizedIsFalse() { Sanitizer sanitizer = new Sanitizer(Collections.singletonList((data) -> { if (data.getKey().equals("custom")) { return data.withValue("$$$$$$"); @@ -82,11 +58,27 @@ class SanitizerTests { return data; })); SanitizableData secret = new SanitizableData(null, "secret", "xyz"); - assertThat(sanitizer.sanitize(secret)).isEqualTo("******"); + assertThat(sanitizer.sanitize(secret, false)).isEqualTo("******"); SanitizableData custom = new SanitizableData(null, "custom", "abcde"); - assertThat(sanitizer.sanitize(custom)).isEqualTo("$$$$$$"); + assertThat(sanitizer.sanitize(custom, false)).isEqualTo("******"); SanitizableData hello = new SanitizableData(null, "hello", "abc"); - assertThat(sanitizer.sanitize(hello)).isEqualTo("abc"); + assertThat(sanitizer.sanitize(hello, false)).isEqualTo("******"); + } + + @Test + void whenCustomSanitizationFunctionAndShowUnsanitizedIsTrue() { + 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, true)).isEqualTo("xyz"); + SanitizableData custom = new SanitizableData(null, "custom", "abcde"); + assertThat(sanitizer.sanitize(custom, true)).isEqualTo("$$$$$$"); + SanitizableData hello = new SanitizableData(null, "hello", "abc"); + assertThat(sanitizer.sanitize(hello, true)).isEqualTo("abc"); } @Test @@ -98,7 +90,7 @@ class SanitizerTests { return data; })); SanitizableData password = new SanitizableData(null, "password", "123456"); - assertThat(sanitizer.sanitize(password)).isEqualTo("------"); + assertThat(sanitizer.sanitize(password, true)).isEqualTo("------"); } @Test @@ -119,101 +111,7 @@ class SanitizerTests { }); Sanitizer sanitizer = new Sanitizer(sanitizingFunctions); SanitizableData custom = new SanitizableData(null, sameKey, "123456"); - assertThat(sanitizer.sanitize(custom)).isEqualTo("------"); - } - - @ParameterizedTest(name = "key = {0}") - @MethodSource("matchingUriUserInfoKeys") - void uriWithSingleValueWithPasswordShouldBeSanitized(String key) { - Sanitizer sanitizer = new Sanitizer(); - assertThat(sanitizer.sanitize(key, "http://user:password@localhost:8080")) - .isEqualTo("http://user:******@localhost:8080"); - } - - @ParameterizedTest(name = "key = {0}") - @MethodSource("matchingUriUserInfoKeys") - void uriWithNonAlphaSchemeCharactersAndSingleValueWithPasswordShouldBeSanitized(String key) { - Sanitizer sanitizer = new Sanitizer(); - assertThat(sanitizer.sanitize(key, "s-ch3m.+-e://user:password@localhost:8080")) - .isEqualTo("s-ch3m.+-e://user:******@localhost:8080"); - } - - @ParameterizedTest(name = "key = {0}") - @MethodSource("matchingUriUserInfoKeys") - void uriWithSingleValueWithNoPasswordShouldNotBeSanitized(String key) { - Sanitizer sanitizer = new Sanitizer(); - assertThat(sanitizer.sanitize(key, "http://localhost:8080")).isEqualTo("http://localhost:8080"); - assertThat(sanitizer.sanitize(key, "http://user@localhost:8080")).isEqualTo("http://user@localhost:8080"); - } - - @ParameterizedTest(name = "key = {0}") - @MethodSource("matchingUriUserInfoKeys") - void uriWithSingleValueWithPasswordMatchingOtherPartsOfStringShouldBeSanitized(String key) { - Sanitizer sanitizer = new Sanitizer(); - assertThat(sanitizer.sanitize(key, "http://user://@localhost:8080")) - .isEqualTo("http://user:******@localhost:8080"); - } - - @ParameterizedTest(name = "key = {0}") - @MethodSource("matchingUriUserInfoKeys") - void uriWithMultipleValuesEachWithPasswordShouldHaveAllSanitized(String key) { - Sanitizer sanitizer = new Sanitizer(); - assertThat( - sanitizer.sanitize(key, "http://user1:password1@localhost:8080,http://user2:password2@localhost:8082")) - .isEqualTo("http://user1:******@localhost:8080,http://user2:******@localhost:8082"); - } - - @ParameterizedTest(name = "key = {0}") - @MethodSource("matchingUriUserInfoKeys") - void uriWithMultipleValuesNoneWithPasswordShouldHaveNoneSanitized(String key) { - Sanitizer sanitizer = new Sanitizer(); - assertThat(sanitizer.sanitize(key, "http://user@localhost:8080,http://localhost:8082")) - .isEqualTo("http://user@localhost:8080,http://localhost:8082"); - } - - @ParameterizedTest(name = "key = {0}") - @MethodSource("matchingUriUserInfoKeys") - void uriWithMultipleValuesSomeWithPasswordShouldHaveThoseSanitized(String key) { - Sanitizer sanitizer = new Sanitizer(); - assertThat(sanitizer.sanitize(key, - "http://user1:password1@localhost:8080,http://user2@localhost:8082,http://localhost:8083")).isEqualTo( - "http://user1:******@localhost:8080,http://user2@localhost:8082,http://localhost:8083"); - } - - @ParameterizedTest(name = "key = {0}") - @MethodSource("matchingUriUserInfoKeys") - void uriWithMultipleValuesWithPasswordMatchingOtherPartsOfStringShouldBeSanitized(String key) { - Sanitizer sanitizer = new Sanitizer(); - assertThat(sanitizer.sanitize(key, "http://user1://@localhost:8080,http://user2://@localhost:8082")) - .isEqualTo("http://user1:******@localhost:8080,http://user2:******@localhost:8082"); - } - - @ParameterizedTest(name = "key = {0}") - @MethodSource("matchingUriUserInfoKeys") - void uriKeyWithUserProvidedListLiteralShouldBeSanitized(String key) { - Sanitizer sanitizer = new Sanitizer(); - assertThat(sanitizer.sanitize(key, "[amqp://username:password@host/]")) - .isEqualTo("[amqp://username:******@host/]"); - assertThat(sanitizer.sanitize(key, - "[http://user1:password1@localhost:8080,http://user2@localhost:8082,http://localhost:8083]")).isEqualTo( - "[http://user1:******@localhost:8080,http://user2@localhost:8082,http://localhost:8083]"); - assertThat(sanitizer.sanitize(key, - "[http://user1:password1@localhost:8080,http://user2:password2@localhost:8082]")) - .isEqualTo("[http://user1:******@localhost:8080,http://user2:******@localhost:8082]"); - assertThat(sanitizer.sanitize(key, "[http://user1@localhost:8080,http://user2@localhost:8082]")) - .isEqualTo("[http://user1@localhost:8080,http://user2@localhost:8082]"); - } - - private static Stream matchingUriUserInfoKeys() { - return Stream.of("uri", "my.uri", "myuri", "uris", "my.uris", "myuris", "url", "my.url", "myurl", "urls", - "my.urls", "myurls", "address", "my.address", "myaddress", "addresses", "my.addresses", "myaddresses"); - } - - @Test - void regex() { - Sanitizer sanitizer = new Sanitizer(".*lock.*"); - assertThat(sanitizer.sanitize("verylOCkish", "secret")).isEqualTo("******"); - assertThat(sanitizer.sanitize("veryokish", "secret")).isEqualTo("secret"); + assertThat(sanitizer.sanitize(custom, true)).isEqualTo("------"); } } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/ShowTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/ShowTests.java new file mode 100644 index 00000000000..9fcb761da46 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/ShowTests.java @@ -0,0 +1,108 @@ +/* + * Copyright 2012-2022 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.security.Principal; +import java.util.Collections; + +import org.junit.jupiter.api.Test; + +import org.springframework.security.core.Authentication; +import org.springframework.security.core.authority.SimpleGrantedAuthority; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link Show}. + * + * @author Madhura Bhave + */ +class ShowTests { + + @Test + void isShownWhenNever() { + assertThat(Show.NEVER.isShown(null, Collections.emptySet())).isFalse(); + assertThat(Show.NEVER.isShown(true)).isFalse(); + assertThat(Show.NEVER.isShown(false)).isFalse(); + } + + @Test + void isShownWhenAlways() { + assertThat(Show.ALWAYS.isShown(null, Collections.emptySet())).isTrue(); + assertThat(Show.ALWAYS.isShown(true)).isTrue(); + assertThat(Show.ALWAYS.isShown(true)).isTrue(); + } + + @Test + void isShownWithUnauthorizedResult() { + assertThat(Show.WHEN_AUTHORIZED.isShown(true)).isTrue(); + assertThat(Show.WHEN_AUTHORIZED.isShown(false)).isFalse(); + } + + @Test + void isShownWhenUserNotInRole() { + SecurityContext securityContext = mock(SecurityContext.class); + given(securityContext.getPrincipal()).willReturn(mock(Principal.class)); + given(securityContext.isUserInRole("admin")).willReturn(false); + assertThat(Show.WHEN_AUTHORIZED.isShown(securityContext, Collections.singleton("admin"))).isFalse(); + } + + @Test + void isShownWhenUserInRole() { + SecurityContext securityContext = mock(SecurityContext.class); + given(securityContext.getPrincipal()).willReturn(mock(Principal.class)); + given(securityContext.isUserInRole("admin")).willReturn(true); + assertThat(Show.WHEN_AUTHORIZED.isShown(securityContext, Collections.singleton("admin"))).isTrue(); + } + + @Test + void isShownWhenPrincipalNull() { + SecurityContext securityContext = mock(SecurityContext.class); + given(securityContext.isUserInRole("admin")).willReturn(true); + assertThat(Show.WHEN_AUTHORIZED.isShown(securityContext, Collections.singleton("admin"))).isFalse(); + } + + @Test + void isShownWhenRolesEmpty() { + SecurityContext securityContext = mock(SecurityContext.class); + given(securityContext.getPrincipal()).willReturn(mock(Principal.class)); + assertThat(Show.WHEN_AUTHORIZED.isShown(securityContext, Collections.emptySet())).isTrue(); + } + + @Test + void isShownWhenSpringSecurityAuthenticationAndUnauthorized() { + SecurityContext securityContext = mock(SecurityContext.class); + Authentication authentication = mock(Authentication.class); + given(securityContext.getPrincipal()).willReturn(authentication); + given(authentication.getAuthorities()) + .willAnswer((invocation) -> Collections.singleton(new SimpleGrantedAuthority("other"))); + assertThat(Show.WHEN_AUTHORIZED.isShown(securityContext, Collections.singleton("admin"))).isFalse(); + } + + @Test + void isShownWhenSpringSecurityAuthenticationAndAuthorized() { + SecurityContext securityContext = mock(SecurityContext.class); + Authentication authentication = mock(Authentication.class); + given(securityContext.getPrincipal()).willReturn(authentication); + given(authentication.getAuthorities()) + .willAnswer((invocation) -> Collections.singleton(new SimpleGrantedAuthority("admin"))); + assertThat(Show.WHEN_AUTHORIZED.isShown(securityContext, Collections.singleton("admin"))).isTrue(); + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/env/EnvironmentEndpointTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/env/EnvironmentEndpointTests.java index 714f81a25de..dd9d2e1f1e3 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/env/EnvironmentEndpointTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/env/EnvironmentEndpointTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2022 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. @@ -27,6 +27,7 @@ import java.util.Map; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; +import org.springframework.boot.actuate.endpoint.Show; import org.springframework.boot.actuate.env.EnvironmentEndpoint.EnvironmentDescriptor; import org.springframework.boot.actuate.env.EnvironmentEndpoint.EnvironmentEntryDescriptor; import org.springframework.boot.actuate.env.EnvironmentEndpoint.PropertySourceDescriptor; @@ -73,7 +74,8 @@ class EnvironmentEndpointTests { ConfigurableEnvironment environment = emptyEnvironment(); environment.getPropertySources().addLast(singleKeyPropertySource("one", "my.key", "first")); environment.getPropertySources().addLast(singleKeyPropertySource("two", "my.key", "second")); - EnvironmentDescriptor descriptor = new EnvironmentEndpoint(environment).environment(null); + EnvironmentDescriptor descriptor = new EnvironmentEndpoint(environment, Collections.emptyList(), Show.ALWAYS) + .environment(null); assertThat(descriptor.getActiveProfiles()).isEmpty(); Map sources = propertySources(descriptor); assertThat(sources.keySet()).containsExactly("one", "two"); @@ -81,6 +83,38 @@ class EnvironmentEndpointTests { assertThat(sources.get("two").getProperties()).containsOnlyKeys("my.key"); } + @Test + void responseWhenShowNever() { + ConfigurableEnvironment environment = new StandardEnvironment(); + TestPropertyValues.of("other.service=abcde").applyTo(environment); + TestPropertyValues.of("system.service=123456").applyToSystemProperties(() -> { + EnvironmentDescriptor descriptor = new EnvironmentEndpoint(environment, Collections.emptyList(), Show.NEVER) + .environment(null); + assertThat(propertySources(descriptor).get("test").getProperties().get("other.service").getValue()) + .isEqualTo("******"); + Map systemProperties = propertySources(descriptor).get("systemProperties") + .getProperties(); + assertThat(systemProperties.get("system.service").getValue()).isEqualTo("******"); + return null; + }); + } + + @Test + void responseWhenShowWhenAuthorized() { + ConfigurableEnvironment environment = new StandardEnvironment(); + TestPropertyValues.of("other.service=abcde").applyTo(environment); + TestPropertyValues.of("system.service=123456").applyToSystemProperties(() -> { + EnvironmentDescriptor descriptor = new EnvironmentEndpoint(environment, Collections.emptyList(), + Show.WHEN_AUTHORIZED).environment(null); + assertThat(propertySources(descriptor).get("test").getProperties().get("other.service").getValue()) + .isEqualTo("abcde"); + Map systemProperties = propertySources(descriptor).get("systemProperties") + .getProperties(); + assertThat(systemProperties.get("system.service").getValue()).isEqualTo("123456"); + return null; + }); + } + @Test void compositeSourceIsHandledCorrectly() { ConfigurableEnvironment environment = emptyEnvironment(); @@ -88,82 +122,14 @@ class EnvironmentEndpointTests { source.addPropertySource(new MapPropertySource("one", Collections.singletonMap("foo", "bar"))); source.addPropertySource(new MapPropertySource("two", Collections.singletonMap("foo", "spam"))); environment.getPropertySources().addFirst(source); - EnvironmentDescriptor descriptor = new EnvironmentEndpoint(environment).environment(null); + EnvironmentDescriptor descriptor = new EnvironmentEndpoint(environment, Collections.emptyList(), Show.ALWAYS) + .environment(null); Map sources = propertySources(descriptor); assertThat(sources.keySet()).containsExactly("composite:one", "composite:two"); assertThat(sources.get("composite:one").getProperties().get("foo").getValue()).isEqualTo("bar"); assertThat(sources.get("composite:two").getProperties().get("foo").getValue()).isEqualTo("spam"); } - @Test - void sensitiveKeysHaveTheirValuesSanitized() { - TestPropertyValues.of("dbPassword=123456", "apiKey=123456", "mySecret=123456", "myCredentials=123456", - "VCAP_SERVICES=123456").applyToSystemProperties(() -> { - EnvironmentDescriptor descriptor = new EnvironmentEndpoint(new StandardEnvironment()) - .environment(null); - Map systemProperties = propertySources(descriptor) - .get("systemProperties").getProperties(); - assertThat(systemProperties.get("dbPassword").getValue()).isEqualTo("******"); - assertThat(systemProperties.get("apiKey").getValue()).isEqualTo("******"); - assertThat(systemProperties.get("mySecret").getValue()).isEqualTo("******"); - assertThat(systemProperties.get("myCredentials").getValue()).isEqualTo("******"); - assertThat(systemProperties.get("VCAP_SERVICES").getValue()).isEqualTo("******"); - PropertyValueDescriptor command = systemProperties.get("sun.java.command"); - if (command != null) { - assertThat(command.getValue()).isEqualTo("******"); - } - return null; - }); - } - - @Test - void sensitiveKeysMatchingCredentialsPatternHaveTheirValuesSanitized() { - TestPropertyValues - .of("my.services.amqp-free.credentials.uri=123456", "credentials.http_api_uri=123456", - "my.services.cleardb-free.credentials=123456", "foo.mycredentials.uri=123456") - .applyToSystemProperties(() -> { - EnvironmentDescriptor descriptor = new EnvironmentEndpoint(new StandardEnvironment()) - .environment(null); - Map systemProperties = propertySources(descriptor) - .get("systemProperties").getProperties(); - assertThat(systemProperties.get("my.services.amqp-free.credentials.uri").getValue()) - .isEqualTo("******"); - assertThat(systemProperties.get("credentials.http_api_uri").getValue()).isEqualTo("******"); - assertThat(systemProperties.get("my.services.cleardb-free.credentials").getValue()) - .isEqualTo("******"); - assertThat(systemProperties.get("foo.mycredentials.uri").getValue()).isEqualTo("******"); - return null; - }); - } - - @Test - void sensitiveKeysMatchingCustomNameHaveTheirValuesSanitized() { - TestPropertyValues.of("dbPassword=123456", "apiKey=123456").applyToSystemProperties(() -> { - EnvironmentEndpoint endpoint = new EnvironmentEndpoint(new StandardEnvironment()); - endpoint.setKeysToSanitize("key"); - EnvironmentDescriptor descriptor = endpoint.environment(null); - Map systemProperties = propertySources(descriptor).get("systemProperties") - .getProperties(); - assertThat(systemProperties.get("dbPassword").getValue()).isEqualTo("123456"); - assertThat(systemProperties.get("apiKey").getValue()).isEqualTo("******"); - return null; - }); - } - - @Test - void sensitiveKeysMatchingCustomPatternHaveTheirValuesSanitized() { - TestPropertyValues.of("dbPassword=123456", "apiKey=123456").applyToSystemProperties(() -> { - EnvironmentEndpoint endpoint = new EnvironmentEndpoint(new StandardEnvironment()); - endpoint.setKeysToSanitize(".*pass.*"); - EnvironmentDescriptor descriptor = endpoint.environment(null); - Map systemProperties = propertySources(descriptor).get("systemProperties") - .getProperties(); - assertThat(systemProperties.get("dbPassword").getValue()).isEqualTo("******"); - assertThat(systemProperties.get("apiKey").getValue()).isEqualTo("123456"); - return null; - }); - } - @Test void keysMatchingCustomSanitizingFunctionHaveTheirValuesSanitized() { ConfigurableEnvironment environment = new StandardEnvironment(); @@ -176,7 +142,7 @@ class EnvironmentEndpointTests { return data.withValue("******"); } return data; - })).environment(null); + }), Show.ALWAYS).environment(null); assertThat(propertySources(descriptor).get("test").getProperties().get("other.service").getValue()) .isEqualTo("abcde"); Map systemProperties = propertySources(descriptor).get("systemProperties") @@ -190,7 +156,8 @@ class EnvironmentEndpointTests { void propertyWithPlaceholderResolved() { ConfigurableEnvironment environment = emptyEnvironment(); TestPropertyValues.of("my.foo: ${bar.blah}", "bar.blah: hello").applyTo(environment); - EnvironmentDescriptor descriptor = new EnvironmentEndpoint(environment).environment(null); + EnvironmentDescriptor descriptor = new EnvironmentEndpoint(environment, Collections.emptyList(), Show.ALWAYS) + .environment(null); assertThat(propertySources(descriptor).get("test").getProperties().get("my.foo").getValue()).isEqualTo("hello"); } @@ -198,46 +165,19 @@ class EnvironmentEndpointTests { void propertyWithPlaceholderNotResolved() { ConfigurableEnvironment environment = emptyEnvironment(); TestPropertyValues.of("my.foo: ${bar.blah}").applyTo(environment); - EnvironmentDescriptor descriptor = new EnvironmentEndpoint(environment).environment(null); + EnvironmentDescriptor descriptor = new EnvironmentEndpoint(environment, Collections.emptyList(), Show.ALWAYS) + .environment(null); assertThat(propertySources(descriptor).get("test").getProperties().get("my.foo").getValue()) .isEqualTo("${bar.blah}"); } - @Test - void propertyWithSensitivePlaceholderResolved() { - ConfigurableEnvironment environment = emptyEnvironment(); - TestPropertyValues.of("my.foo: http://${bar.password}://hello", "bar.password: hello").applyTo(environment); - EnvironmentDescriptor descriptor = new EnvironmentEndpoint(environment).environment(null); - assertThat(propertySources(descriptor).get("test").getProperties().get("my.foo").getValue()) - .isEqualTo("http://******://hello"); - } - - @Test - void propertyWithSensitivePlaceholderNotResolved() { - ConfigurableEnvironment environment = emptyEnvironment(); - TestPropertyValues.of("my.foo: http://${bar.password}://hello").applyTo(environment); - EnvironmentDescriptor descriptor = new EnvironmentEndpoint(environment).environment(null); - assertThat(propertySources(descriptor).get("test").getProperties().get("my.foo").getValue()) - .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 void propertyWithComplexTypeShouldNotFail() { ConfigurableEnvironment environment = emptyEnvironment(); environment.getPropertySources() .addFirst(singleKeyPropertySource("test", "foo", Collections.singletonMap("bar", "baz"))); - EnvironmentDescriptor descriptor = new EnvironmentEndpoint(environment).environment(null); + EnvironmentDescriptor descriptor = new EnvironmentEndpoint(environment, Collections.emptyList(), Show.ALWAYS) + .environment(null); String value = (String) propertySources(descriptor).get("test").getProperties().get("foo").getValue(); assertThat(value).isEqualTo("Complex property type java.util.Collections$SingletonMap"); } @@ -251,7 +191,8 @@ class EnvironmentEndpointTests { map.put("boolean", true); map.put("biginteger", BigInteger.valueOf(200)); environment.getPropertySources().addFirst(new MapPropertySource("test", map)); - EnvironmentDescriptor descriptor = new EnvironmentEndpoint(environment).environment(null); + EnvironmentDescriptor descriptor = new EnvironmentEndpoint(environment, Collections.emptyList(), Show.ALWAYS) + .environment(null); Map properties = propertySources(descriptor).get("test").getProperties(); assertThat(properties.get("char").getValue()).isEqualTo('a'); assertThat(properties.get("integer").getValue()).isEqualTo(100); @@ -263,26 +204,42 @@ class EnvironmentEndpointTests { void propertyWithCharSequenceTypeIsConvertedToString() { ConfigurableEnvironment environment = emptyEnvironment(); environment.getPropertySources().addFirst(singleKeyPropertySource("test", "foo", new CharSequenceProperty())); - EnvironmentDescriptor descriptor = new EnvironmentEndpoint(environment).environment(null); + EnvironmentDescriptor descriptor = new EnvironmentEndpoint(environment, Collections.emptyList(), Show.ALWAYS) + .environment(null); String value = (String) propertySources(descriptor).get("test").getProperties().get("foo").getValue(); assertThat(value).isEqualTo("test value"); } @Test void propertyEntry() { + testPropertyEntry(Show.ALWAYS, "bar", "another"); + } + + @Test + void propertyEntryWhenShowNever() { + testPropertyEntry(Show.NEVER, "******", "******"); + } + + @Test + void propertyEntryWhenShowWhenAuthorized() { + testPropertyEntry(Show.ALWAYS, "bar", "another"); + } + + private void testPropertyEntry(Show always, String bar, String another) { TestPropertyValues.of("my.foo=another").applyToSystemProperties(() -> { StandardEnvironment environment = new StandardEnvironment(); TestPropertyValues.of("my.foo=bar", "my.foo2=bar2").applyTo(environment, TestPropertyValues.Type.MAP, "test"); - EnvironmentEntryDescriptor descriptor = new EnvironmentEndpoint(environment).environmentEntry("my.foo"); + EnvironmentEntryDescriptor descriptor = new EnvironmentEndpoint(environment, Collections.emptyList(), + always).environmentEntry("my.foo"); assertThat(descriptor).isNotNull(); assertThat(descriptor.getProperty()).isNotNull(); assertThat(descriptor.getProperty().getSource()).isEqualTo("test"); - assertThat(descriptor.getProperty().getValue()).isEqualTo("bar"); + assertThat(descriptor.getProperty().getValue()).isEqualTo(bar); Map sources = propertySources(descriptor); assertThat(sources.keySet()).containsExactly("test", "systemProperties", "systemEnvironment"); - assertPropertySourceEntryDescriptor(sources.get("test"), "bar", null); - assertPropertySourceEntryDescriptor(sources.get("systemProperties"), "another", null); + assertPropertySourceEntryDescriptor(sources.get("test"), bar, null); + assertPropertySourceEntryDescriptor(sources.get("systemProperties"), another, null); assertPropertySourceEntryDescriptor(sources.get("systemEnvironment"), null, null); return null; }); @@ -294,7 +251,8 @@ class EnvironmentEndpointTests { OriginParentMockPropertySource propertySource = new OriginParentMockPropertySource(); propertySource.setProperty("name", "test"); environment.getPropertySources().addFirst(propertySource); - EnvironmentEntryDescriptor descriptor = new EnvironmentEndpoint(environment).environmentEntry("name"); + EnvironmentEntryDescriptor descriptor = new EnvironmentEndpoint(environment, Collections.emptyList(), + Show.ALWAYS).environmentEntry("name"); PropertySourceEntryDescriptor entryDescriptor = propertySources(descriptor).get("mockProperties"); assertThat(entryDescriptor.getProperty().getOrigin()).isEqualTo("name"); assertThat(entryDescriptor.getProperty().getOriginParents()).containsExactly("spring", "boot"); @@ -304,7 +262,8 @@ class EnvironmentEndpointTests { void propertyEntryNotFound() { ConfigurableEnvironment environment = emptyEnvironment(); environment.getPropertySources().addFirst(singleKeyPropertySource("test", "foo", "bar")); - EnvironmentEntryDescriptor descriptor = new EnvironmentEndpoint(environment).environmentEntry("does.not.exist"); + EnvironmentEntryDescriptor descriptor = new EnvironmentEndpoint(environment, Collections.emptyList(), + Show.ALWAYS).environmentEntry("does.not.exist"); assertThat(descriptor).isNotNull(); assertThat(descriptor.getProperty()).isNull(); Map sources = propertySources(descriptor); @@ -317,33 +276,14 @@ class EnvironmentEndpointTests { ConfigurableEnvironment environment = emptyEnvironment(); environment.getPropertySources().addFirst(singleKeyPropertySource("one", "a", "alpha")); environment.getPropertySources().addFirst(singleKeyPropertySource("two", "a", "apple")); - EnvironmentDescriptor descriptor = new EnvironmentEndpoint(environment).environment(null); + EnvironmentDescriptor descriptor = new EnvironmentEndpoint(environment, Collections.emptyList(), Show.ALWAYS) + .environment(null); Map sources = propertySources(descriptor); assertThat(sources.keySet()).containsExactly("two", "one"); assertThat(sources.get("one").getProperties().get("a").getValue()).isEqualTo("alpha"); assertThat(sources.get("two").getProperties().get("a").getValue()).isEqualTo("apple"); } - @Test - void uriPropertyWithSensitiveInfo() { - ConfigurableEnvironment environment = new StandardEnvironment(); - TestPropertyValues.of("sensitive.uri=http://user:password@localhost:8080").applyTo(environment); - EnvironmentEntryDescriptor descriptor = new EnvironmentEndpoint(environment).environmentEntry("sensitive.uri"); - assertThat(descriptor.getProperty().getValue()).isEqualTo("http://user:******@localhost:8080"); - } - - @Test - void addressesPropertyWithMultipleEntriesEachWithSensitiveInfo() { - ConfigurableEnvironment environment = new StandardEnvironment(); - TestPropertyValues - .of("sensitive.addresses=http://user:password@localhost:8080,http://user2:password2@localhost:8082") - .applyTo(environment); - EnvironmentEntryDescriptor descriptor = new EnvironmentEndpoint(environment) - .environmentEntry("sensitive.addresses"); - assertThat(descriptor.getProperty().getValue()) - .isEqualTo("http://user:******@localhost:8080,http://user2:******@localhost:8082"); - } - private static ConfigurableEnvironment emptyEnvironment() { StandardEnvironment environment = new StandardEnvironment(); environment.getPropertySources().remove(StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME); @@ -418,7 +358,7 @@ class EnvironmentEndpointTests { @Bean EnvironmentEndpoint environmentEndpoint(Environment environment) { - return new EnvironmentEndpoint(environment); + return new EnvironmentEndpoint(environment, Collections.emptyList(), Show.ALWAYS); } } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/env/EnvironmentEndpointWebExtensionTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/env/EnvironmentEndpointWebExtensionTests.java new file mode 100644 index 00000000000..c8d01314552 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/env/EnvironmentEndpointWebExtensionTests.java @@ -0,0 +1,94 @@ +/* + * Copyright 2012-2022 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.env; + +import java.security.Principal; +import java.util.Collections; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.actuate.endpoint.SecurityContext; +import org.springframework.boot.actuate.endpoint.Show; +import org.springframework.boot.actuate.env.EnvironmentEndpoint.EnvironmentEntryDescriptor; + +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link EnvironmentEndpointWebExtension}. + * + * @author Madhura Bhave + */ +class EnvironmentEndpointWebExtensionTests { + + private EnvironmentEndpointWebExtension webExtension; + + private EnvironmentEndpoint delegate; + + @BeforeEach + void setup() { + this.delegate = mock(EnvironmentEndpoint.class); + } + + @Test + void whenShowValuesIsNever() { + this.webExtension = new EnvironmentEndpointWebExtension(this.delegate, Show.NEVER, Collections.emptySet()); + this.webExtension.environment(null, null); + then(this.delegate).should().getEnvironmentDescriptor(null, false); + verifyPrefixed(null, false); + } + + @Test + void whenShowValuesIsAlways() { + this.webExtension = new EnvironmentEndpointWebExtension(this.delegate, Show.ALWAYS, Collections.emptySet()); + this.webExtension.environment(null, null); + then(this.delegate).should().getEnvironmentDescriptor(null, true); + verifyPrefixed(null, true); + } + + @Test + void whenShowValuesIsWhenAuthorizedAndSecurityContextIsAuthorized() { + SecurityContext securityContext = mock(SecurityContext.class); + given(securityContext.getPrincipal()).willReturn(mock(Principal.class)); + this.webExtension = new EnvironmentEndpointWebExtension(this.delegate, Show.WHEN_AUTHORIZED, + Collections.emptySet()); + this.webExtension.environment(securityContext, null); + then(this.delegate).should().getEnvironmentDescriptor(null, true); + verifyPrefixed(securityContext, true); + + } + + @Test + void whenShowValuesIsWhenAuthorizedAndSecurityContextIsNotAuthorized() { + SecurityContext securityContext = mock(SecurityContext.class); + this.webExtension = new EnvironmentEndpointWebExtension(this.delegate, Show.WHEN_AUTHORIZED, + Collections.emptySet()); + this.webExtension.environment(securityContext, null); + then(this.delegate).should().getEnvironmentDescriptor(null, false); + verifyPrefixed(securityContext, false); + } + + private void verifyPrefixed(SecurityContext securityContext, boolean showUnsanitized) { + given(this.delegate.getEnvironmentEntryDescriptor("test", showUnsanitized)) + .willReturn(new EnvironmentEntryDescriptor(null, Collections.emptyList(), Collections.emptyList())); + this.webExtension.environmentEntry(securityContext, "test"); + then(this.delegate).should().getEnvironmentEntryDescriptor("test", showUnsanitized); + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/env/EnvironmentEndpointWebIntegrationTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/env/EnvironmentEndpointWebIntegrationTests.java index f46d8d5569c..49b3e9f347f 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/env/EnvironmentEndpointWebIntegrationTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/env/EnvironmentEndpointWebIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,11 +16,13 @@ package org.springframework.boot.actuate.env; +import java.util.Collections; import java.util.HashMap; import java.util.Map; import org.junit.jupiter.api.BeforeEach; +import org.springframework.boot.actuate.endpoint.Show; import org.springframework.boot.actuate.endpoint.web.test.WebEndpointTest; import org.springframework.boot.test.util.TestPropertyValues; import org.springframework.context.ConfigurableApplicationContext; @@ -76,17 +78,6 @@ class EnvironmentEndpointWebIntegrationTests { .isEqualTo("${my.bar}"); } - @WebEndpointTest - void nestedPathWithSensitivePlaceholderShouldSanitize() { - Map map = new HashMap<>(); - map.put("my.foo", "${my.password}"); - map.put("my.password", "hello"); - this.context.getEnvironment().getPropertySources().addFirst(new MapPropertySource("placeholder", map)); - this.client.get().uri("/actuator/env/my.foo").exchange().expectStatus().isOk().expectBody() - .jsonPath("property.value").isEqualTo("******").jsonPath(forPropertyEntry("placeholder")) - .isEqualTo("******"); - } - @WebEndpointTest void nestedPathForUnknownKeyShouldReturn404() { this.client.get().uri("/actuator/env/this.does.not.exist").exchange().expectStatus().isNotFound(); @@ -103,16 +94,6 @@ class EnvironmentEndpointWebIntegrationTests { .isEqualTo("${my.bar}"); } - @WebEndpointTest - void nestedPathMatchedByRegexWithSensitivePlaceholderShouldSanitize() { - Map map = new HashMap<>(); - map.put("my.foo", "${my.password}"); - map.put("my.password", "hello"); - this.context.getEnvironment().getPropertySources().addFirst(new MapPropertySource("placeholder", map)); - this.client.get().uri("/actuator/env?pattern=my.*").exchange().expectStatus().isOk().expectBody() - .jsonPath(forProperty("placeholder", "my.foo")).isEqualTo("******"); - } - private String forProperty(String source, String name) { return "propertySources[?(@.name=='" + source + "')].properties.['" + name + "'].value"; } @@ -126,12 +107,12 @@ class EnvironmentEndpointWebIntegrationTests { @Bean EnvironmentEndpoint endpoint(Environment environment) { - return new EnvironmentEndpoint(environment); + return new EnvironmentEndpoint(environment, Collections.emptyList(), Show.ALWAYS); } @Bean EnvironmentEndpointWebExtension environmentEndpointWebExtension(EnvironmentEndpoint endpoint) { - return new EnvironmentEndpointWebExtension(endpoint); + return new EnvironmentEndpointWebExtension(endpoint, Show.ALWAYS, Collections.emptySet()); } } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/quartz/QuartzEndpointTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/quartz/QuartzEndpointTests.java index a7a8e3388c5..f76599a2edb 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/quartz/QuartzEndpointTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/quartz/QuartzEndpointTests.java @@ -62,7 +62,6 @@ import org.quartz.TriggerKey; import org.quartz.impl.matchers.GroupMatcher; import org.quartz.spi.OperableTrigger; -import org.springframework.boot.actuate.endpoint.Sanitizer; import org.springframework.boot.actuate.quartz.QuartzEndpoint.QuartzJobDetails; import org.springframework.boot.actuate.quartz.QuartzEndpoint.QuartzJobGroupSummary; import org.springframework.boot.actuate.quartz.QuartzEndpoint.QuartzJobSummary; @@ -107,7 +106,7 @@ class QuartzEndpointTests { QuartzEndpointTests() { this.scheduler = mock(Scheduler.class); - this.endpoint = new QuartzEndpoint(this.scheduler); + this.endpoint = new QuartzEndpoint(this.scheduler, Collections.emptyList()); } @Test @@ -417,7 +416,7 @@ class QuartzEndpointTests { mockTriggers(trigger); given(this.scheduler.getTriggerState(TriggerKey.triggerKey("3am-every-day", "samples"))) .willReturn(TriggerState.NORMAL); - Map triggerDetails = this.endpoint.quartzTrigger("samples", "3am-every-day"); + Map triggerDetails = this.endpoint.quartzTrigger("samples", "3am-every-day", true); assertThat(triggerDetails).contains(entry("group", "samples"), entry("name", "3am-every-day"), entry("description", "Sample description"), entry("type", "cron"), entry("state", TriggerState.NORMAL), entry("priority", 3)); @@ -443,7 +442,7 @@ class QuartzEndpointTests { mockTriggers(trigger); given(this.scheduler.getTriggerState(TriggerKey.triggerKey("every-hour", "samples"))) .willReturn(TriggerState.COMPLETE); - Map triggerDetails = this.endpoint.quartzTrigger("samples", "every-hour"); + Map triggerDetails = this.endpoint.quartzTrigger("samples", "every-hour", true); assertThat(triggerDetails).contains(entry("group", "samples"), entry("name", "every-hour"), entry("description", "Every hour"), entry("type", "simple"), entry("state", TriggerState.COMPLETE), entry("priority", 20)); @@ -470,7 +469,7 @@ class QuartzEndpointTests { mockTriggers(trigger); given(this.scheduler.getTriggerState(TriggerKey.triggerKey("every-hour-mon-wed", "samples"))) .willReturn(TriggerState.NORMAL); - Map triggerDetails = this.endpoint.quartzTrigger("samples", "every-hour-mon-wed"); + Map triggerDetails = this.endpoint.quartzTrigger("samples", "every-hour-mon-wed", true); assertThat(triggerDetails).contains(entry("group", "samples"), entry("name", "every-hour-mon-wed"), entry("description", "Every working hour Mon Wed"), entry("type", "dailyTimeInterval"), entry("state", TriggerState.NORMAL), entry("priority", 4)); @@ -499,7 +498,7 @@ class QuartzEndpointTests { mockTriggers(trigger); given(this.scheduler.getTriggerState(TriggerKey.triggerKey("once-a-week", "samples"))) .willReturn(TriggerState.BLOCKED); - Map triggerDetails = this.endpoint.quartzTrigger("samples", "once-a-week"); + Map triggerDetails = this.endpoint.quartzTrigger("samples", "once-a-week", true); assertThat(triggerDetails).contains(entry("group", "samples"), entry("name", "once-a-week"), entry("description", "Once a week"), entry("type", "calendarInterval"), entry("state", TriggerState.BLOCKED), entry("priority", 8)); @@ -524,7 +523,7 @@ class QuartzEndpointTests { mockTriggers(trigger); given(this.scheduler.getTriggerState(TriggerKey.triggerKey("custom", "samples"))) .willReturn(TriggerState.ERROR); - Map triggerDetails = this.endpoint.quartzTrigger("samples", "custom"); + Map triggerDetails = this.endpoint.quartzTrigger("samples", "custom", true); assertThat(triggerDetails).contains(entry("group", "samples"), entry("name", "custom"), entry("type", "custom"), entry("state", TriggerState.ERROR), entry("priority", 9)); assertThat(triggerDetails).contains(entry("previousFireTime", previousFireTime), @@ -542,9 +541,22 @@ class QuartzEndpointTests { mockTriggers(trigger); given(this.scheduler.getTriggerState(TriggerKey.triggerKey("3am-every-day", "samples"))) .willReturn(TriggerState.NORMAL); - Map triggerDetails = this.endpoint.quartzTrigger("samples", "3am-every-day"); + Map triggerDetails = this.endpoint.quartzTrigger("samples", "3am-every-day", true); assertThat(triggerDetails).extractingByKey("data", nestedMap()).containsOnly(entry("user", "user"), - entry("password", "******"), entry("url", "https://user:******@example.com")); + entry("password", "secret"), entry("url", "https://user:secret@example.com")); + } + + @Test + void quartzTriggerWithDataMapAndShowUnsanitizedFalse() throws SchedulerException { + CronTrigger trigger = TriggerBuilder.newTrigger().withIdentity("3am-every-day", "samples") + .withSchedule(CronScheduleBuilder.dailyAtHourAndMinute(3, 0)).usingJobData("user", "user") + .usingJobData("password", "secret").usingJobData("url", "https://user:secret@example.com").build(); + mockTriggers(trigger); + given(this.scheduler.getTriggerState(TriggerKey.triggerKey("3am-every-day", "samples"))) + .willReturn(TriggerState.NORMAL); + Map triggerDetails = this.endpoint.quartzTrigger("samples", "3am-every-day", false); + assertThat(triggerDetails).extractingByKey("data", nestedMap()).containsOnly(entry("user", "******"), + entry("password", "******"), entry("url", "******")); } @ParameterizedTest(name = "unit {1}") @@ -554,7 +566,7 @@ class QuartzEndpointTests { .withSchedule(CalendarIntervalScheduleBuilder.calendarIntervalSchedule().withInterval(amount, unit)) .build(); mockTriggers(trigger); - Map triggerDetails = this.endpoint.quartzTrigger("samples", "trigger"); + Map triggerDetails = this.endpoint.quartzTrigger("samples", "trigger", true); assertThat(triggerDetails).extractingByKey("calendarInterval", nestedMap()) .contains(entry("interval", expectedDuration.toMillis())); } @@ -575,7 +587,7 @@ class QuartzEndpointTests { JobDetail job = JobBuilder.newJob(Job.class).withIdentity("hello", "samples").withDescription("A sample job") .storeDurably().requestRecovery(false).build(); mockJobs(job); - QuartzJobDetails jobDetails = this.endpoint.quartzJob("samples", "hello"); + QuartzJobDetails jobDetails = this.endpoint.quartzJob("samples", "hello", true); assertThat(jobDetails.getGroup()).isEqualTo("samples"); assertThat(jobDetails.getName()).isEqualTo("hello"); assertThat(jobDetails.getDescription()).isEqualTo("A sample job"); @@ -600,7 +612,7 @@ class QuartzEndpointTests { mockTriggers(trigger); given(this.scheduler.getTriggersOfJob(JobKey.jobKey("hello", "samples"))) .willAnswer((invocation) -> Collections.singletonList(trigger)); - QuartzJobDetails jobDetails = this.endpoint.quartzJob("samples", "hello"); + QuartzJobDetails jobDetails = this.endpoint.quartzJob("samples", "hello", true); assertThat(jobDetails.getTriggers()).hasSize(1); Map triggerDetails = jobDetails.getTriggers().get(0); assertThat(triggerDetails).containsOnly(entry("group", "samples"), entry("name", "3am-every-day"), @@ -622,7 +634,7 @@ class QuartzEndpointTests { mockTriggers(triggerOne, triggerTwo); given(this.scheduler.getTriggersOfJob(JobKey.jobKey("hello", "samples"))) .willAnswer((invocation) -> Arrays.asList(triggerOne, triggerTwo)); - QuartzJobDetails jobDetails = this.endpoint.quartzJob("samples", "hello"); + QuartzJobDetails jobDetails = this.endpoint.quartzJob("samples", "hello", true); assertThat(jobDetails.getTriggers()).hasSize(2); assertThat(jobDetails.getTriggers().get(0)).containsEntry("name", "two"); assertThat(jobDetails.getTriggers().get(1)).containsEntry("name", "one"); @@ -642,35 +654,30 @@ class QuartzEndpointTests { mockTriggers(triggerOne, triggerTwo); given(this.scheduler.getTriggersOfJob(JobKey.jobKey("hello", "samples"))) .willAnswer((invocation) -> Arrays.asList(triggerOne, triggerTwo)); - QuartzJobDetails jobDetails = this.endpoint.quartzJob("samples", "hello"); + QuartzJobDetails jobDetails = this.endpoint.quartzJob("samples", "hello", true); assertThat(jobDetails.getTriggers()).hasSize(2); assertThat(jobDetails.getTriggers().get(0)).containsEntry("name", "two"); assertThat(jobDetails.getTriggers().get(1)).containsEntry("name", "one"); } @Test - void quartzJobWithSensitiveDataMap() throws SchedulerException { + void quartzJobWithDataMap() throws SchedulerException { JobDetail job = JobBuilder.newJob(Job.class).withIdentity("hello", "samples").usingJobData("user", "user") .usingJobData("password", "secret").usingJobData("url", "https://user:secret@example.com").build(); mockJobs(job); - QuartzJobDetails jobDetails = this.endpoint.quartzJob("samples", "hello"); - assertThat(jobDetails.getData()).containsOnly(entry("user", "user"), entry("password", "******"), - entry("url", "https://user:******@example.com")); + QuartzJobDetails jobDetails = this.endpoint.quartzJob("samples", "hello", true); + assertThat(jobDetails.getData()).containsOnly(entry("user", "user"), entry("password", "secret"), + entry("url", "https://user:secret@example.com")); } @Test - void quartzJobWithSensitiveDataMapAndCustomSanitizer() throws SchedulerException { - JobDetail job = JobBuilder.newJob(Job.class).withIdentity("hello", "samples").usingJobData("test", "value") - .usingJobData("secret", "value").build(); + void quartzJobWithDataMapAndShowUnsanitizedFalse() throws SchedulerException { + JobDetail job = JobBuilder.newJob(Job.class).withIdentity("hello", "samples").usingJobData("user", "user") + .usingJobData("password", "secret").usingJobData("url", "https://user:secret@example.com").build(); mockJobs(job); - Sanitizer sanitizer = mock(Sanitizer.class); - given(sanitizer.sanitize("test", "value")).willReturn("value"); - given(sanitizer.sanitize("secret", "value")).willReturn("----"); - QuartzJobDetails jobDetails = new QuartzEndpoint(this.scheduler, sanitizer).quartzJob("samples", "hello"); - assertThat(jobDetails.getData()).containsOnly(entry("test", "value"), entry("secret", "----")); - then(sanitizer).should().sanitize("test", "value"); - then(sanitizer).should().sanitize("secret", "value"); - then(sanitizer).shouldHaveNoMoreInteractions(); + QuartzJobDetails jobDetails = this.endpoint.quartzJob("samples", "hello", false); + assertThat(jobDetails.getData()).containsOnly(entry("user", "******"), entry("password", "******"), + entry("url", "******")); } private void mockJobs(JobDetail... jobs) throws SchedulerException { diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/quartz/QuartzEndpointWebExtensionTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/quartz/QuartzEndpointWebExtensionTests.java index 22ba2721bc7..85f35a428ad 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/quartz/QuartzEndpointWebExtensionTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/quartz/QuartzEndpointWebExtensionTests.java @@ -16,13 +16,18 @@ package org.springframework.boot.actuate.quartz; +import java.security.Principal; +import java.util.Collections; import java.util.Set; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.aot.hint.MemberCategory; import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.predicate.RuntimeHintsPredicates; +import org.springframework.boot.actuate.endpoint.SecurityContext; +import org.springframework.boot.actuate.endpoint.Show; import org.springframework.boot.actuate.quartz.QuartzEndpoint.QuartzGroups; import org.springframework.boot.actuate.quartz.QuartzEndpoint.QuartzJobDetails; import org.springframework.boot.actuate.quartz.QuartzEndpoint.QuartzJobGroupSummary; @@ -30,14 +35,67 @@ import org.springframework.boot.actuate.quartz.QuartzEndpoint.QuartzTriggerGroup import org.springframework.boot.actuate.quartz.QuartzEndpointWebExtension.QuartzEndpointWebExtensionRuntimeHints; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.mock; /** * Tests for {@link QuartzEndpointWebExtension}. * * @author Moritz Halbritter + * @author Madhura Bhave */ class QuartzEndpointWebExtensionTests { + private QuartzEndpointWebExtension webExtension; + + private QuartzEndpoint delegate; + + @BeforeEach + void setup() { + this.delegate = mock(QuartzEndpoint.class); + } + + @Test + void whenShowValuesIsNever() throws Exception { + this.webExtension = new QuartzEndpointWebExtension(this.delegate, Show.NEVER, Collections.emptySet()); + this.webExtension.quartzJobOrTrigger(null, "jobs", "a", "b"); + this.webExtension.quartzJobOrTrigger(null, "triggers", "a", "b"); + then(this.delegate).should().quartzJob("a", "b", false); + then(this.delegate).should().quartzTrigger("a", "b", false); + } + + @Test + void whenShowValuesIsAlways() throws Exception { + this.webExtension = new QuartzEndpointWebExtension(this.delegate, Show.ALWAYS, Collections.emptySet()); + this.webExtension.quartzJobOrTrigger(null, "a", "b", "c"); + this.webExtension.quartzJobOrTrigger(null, "jobs", "a", "b"); + this.webExtension.quartzJobOrTrigger(null, "triggers", "a", "b"); + then(this.delegate).should().quartzJob("a", "b", true); + then(this.delegate).should().quartzTrigger("a", "b", true); + } + + @Test + void whenShowValuesIsWhenAuthorizedAndSecurityContextIsAuthorized() throws Exception { + SecurityContext securityContext = mock(SecurityContext.class); + given(securityContext.getPrincipal()).willReturn(mock(Principal.class)); + this.webExtension = new QuartzEndpointWebExtension(this.delegate, Show.WHEN_AUTHORIZED, Collections.emptySet()); + this.webExtension.quartzJobOrTrigger(securityContext, "jobs", "a", "b"); + this.webExtension.quartzJobOrTrigger(securityContext, "triggers", "a", "b"); + then(this.delegate).should().quartzJob("a", "b", true); + then(this.delegate).should().quartzTrigger("a", "b", true); + } + + @Test + void whenShowValuesIsWhenAuthorizedAndSecurityContextIsNotAuthorized() throws Exception { + SecurityContext securityContext = mock(SecurityContext.class); + this.webExtension = new QuartzEndpointWebExtension(this.delegate, Show.WHEN_AUTHORIZED, Collections.emptySet()); + this.webExtension.quartzJobOrTrigger(securityContext, "jobs", "a", "b"); + this.webExtension.quartzJobOrTrigger(securityContext, "triggers", "a", "b"); + then(this.delegate).should().quartzJob("a", "b", false); + then(this.delegate).should().quartzTrigger("a", "b", false); + } + @Test void shouldRegisterHints() { RuntimeHints runtimeHints = new RuntimeHints(); diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/quartz/QuartzEndpointWebIntegrationTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/quartz/QuartzEndpointWebIntegrationTests.java index cd58eddaa34..3ab376f6061 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/quartz/QuartzEndpointWebIntegrationTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/quartz/QuartzEndpointWebIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2022 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. @@ -42,6 +42,7 @@ import org.quartz.TriggerBuilder; import org.quartz.TriggerKey; import org.quartz.impl.matchers.GroupMatcher; +import org.springframework.boot.actuate.endpoint.Show; import org.springframework.boot.actuate.endpoint.web.test.WebEndpointTest; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -175,12 +176,12 @@ class QuartzEndpointWebIntegrationTests { @Bean QuartzEndpoint endpoint(Scheduler scheduler) { - return new QuartzEndpoint(scheduler); + return new QuartzEndpoint(scheduler, Collections.emptyList()); } @Bean QuartzEndpointWebExtension quartzEndpointWebExtension(QuartzEndpoint endpoint) { - return new QuartzEndpointWebExtension(endpoint); + return new QuartzEndpointWebExtension(endpoint, Show.ALWAYS, Collections.emptySet()); } private void mockJobs(Scheduler scheduler, JobDetail... jobs) throws SchedulerException { diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/actuator.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/actuator.adoc index 24bf8ef90c3..2a7dd0e81eb 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/actuator.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/actuator.adoc @@ -36,22 +36,31 @@ See also the section on "`<://:@:/`. -For example, for the property `myclient.uri=http://user1:password1@localhost:8081`, the resulting sanitized value is -`++http://user1:******@localhost:8081++`. +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + management: + endpoint: + env: + show-values: WHEN_AUTHORIZED + roles: "admin" +---- + +The configuration above enables the ability for all users with the `admin` role to view all values in their original form from the `/env` endpoint. + +NOTE: When `show-values` is set to `ALWAYS` or `WHEN_AUTHORIZED` any sanitization applied by a `<>` will still be applied. @@ -59,9 +68,6 @@ For example, for the property `myclient.uri=http://user1:password1@localhost:808 ==== Customizing Sanitization Sanitization can be customized in two different ways. -The default patterns used by the `env` and `configprops` endpoints can be replaced using configprop:management.endpoint.env.keys-to-sanitize[] and configprop:management.endpoint.configprops.keys-to-sanitize[] respectively. -Alternatively, additional patterns can be configured using configprop:management.endpoint.env.additional-keys-to-sanitize[] and configprop:management.endpoint.configprops.additional-keys-to-sanitize[]. - To take more control over the sanitization, define a `SanitizingFunction` bean. The `SanitizableData` with which the function is called provides access to the key and value as well as the `PropertySource` from which they came. This allows you to, for example, sanitize every value that comes from a particular property source.