Support role-based sanitization for actuator endpoints

Closes gh-32156
This commit is contained in:
Madhura Bhave 2022-07-21 12:59:44 -07:00
parent ada2450483
commit 47effdcade
43 changed files with 1213 additions and 968 deletions

View File

@ -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<SanitizingFunction> 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());
}
}

View File

@ -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<String> 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<String> getRoles() {
return this.roles;
}
}

View File

@ -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<SanitizingFunction> 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());
}
}

View File

@ -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<String> 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<String> getRoles() {
return this.roles;
}
}

View File

@ -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

View File

@ -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;

View File

@ -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;

View File

@ -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
}
}

View File

@ -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<SanitizingFunction> 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());
}
}

View File

@ -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<String> roles = new HashSet<>();
public Show getShowValues() {
return this.showValues;
}
public void setShowValues(Show showValues) {
this.showValues = showValues;
}
public Set<String> getRoles() {
return this.roles;
}
}

View File

@ -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",

View File

@ -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<String> roles = (Set<String>) 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$$$"));

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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());
}
}

View File

@ -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<String> roles = (Set<String>) 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

View File

@ -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;

View File

@ -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<String> roles = (Set<String>) 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());
}
}

View File

@ -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.
*
* <p>
* 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<SanitizingFunction> sanitizingFunctions) {
public ConfigurationPropertiesReportEndpoint(Iterable<SanitizingFunction> 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<ConfigurationPropertiesBean> beanFilterPredicate) {
ApplicationConfigurationProperties getConfigurationProperties(String prefix, boolean showUnsanitized) {
return getConfigurationProperties(this.context, (bean) -> bean.getAnnotation().prefix().startsWith(prefix),
showUnsanitized);
}
private ApplicationConfigurationProperties getConfigurationProperties(ApplicationContext context,
Predicate<ConfigurationPropertiesBean> beanFilterPredicate, boolean showUnsanitized) {
ObjectMapper mapper = getObjectMapper();
Map<String, ContextConfigurationProperties> 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<ConfigurationPropertiesBean> beanFilterPredicate) {
Predicate<ConfigurationPropertiesBean> beanFilterPredicate, boolean showUnsanitized) {
Map<String, ConfigurationPropertiesBean> beans = ConfigurationPropertiesBean.getAll(context);
Map<String, ConfigurationPropertiesBeanDescriptor> 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<String, Object> serialized = safeSerialize(mapper, bean.getInstance(), prefix);
Map<String, Object> properties = sanitize(prefix, serialized);
Map<String, Object> inputs = getInputs(prefix, serialized);
Map<String, Object> properties = sanitize(prefix, serialized, showUnsanitized);
Map<String, Object> 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<String, Object> sanitize(String prefix, Map<String, Object> map) {
private Map<String, Object> sanitize(String prefix, Map<String, Object> map, boolean showUnsanitized) {
map.forEach((key, value) -> {
String qualifiedKey = getQualifiedKey(prefix, key);
if (value instanceof Map) {
map.put(key, sanitize(qualifiedKey, (Map<String, Object>) value));
map.put(key, sanitize(qualifiedKey, (Map<String, Object>) value, showUnsanitized));
}
else if (value instanceof List) {
map.put(key, sanitize(qualifiedKey, (List<Object>) value));
map.put(key, sanitize(qualifiedKey, (List<Object>) 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<Object> sanitize(String prefix, List<Object> list) {
private List<Object> sanitize(String prefix, List<Object> list, boolean showUnsanitized) {
List<Object> sanitized = new ArrayList<>();
int index = 0;
for (Object item : list) {
String name = prefix + "[" + index++ + "]";
if (item instanceof Map) {
sanitized.add(sanitize(name, (Map<String, Object>) item));
sanitized.add(sanitize(name, (Map<String, Object>) item, showUnsanitized));
}
else if (item instanceof List) {
sanitized.add(sanitize(name, (List<Object>) item));
sanitized.add(sanitize(name, (List<Object>) item, showUnsanitized));
}
else {
sanitized.add(sanitizeWithPropertySourceIfPresent(name, item));
sanitized.add(sanitizeWithPropertySourceIfPresent(name, item, showUnsanitized));
}
}
return sanitized;
}
@SuppressWarnings("unchecked")
private Map<String, Object> getInputs(String prefix, Map<String, Object> map) {
private Map<String, Object> getInputs(String prefix, Map<String, Object> map, boolean showUnsanitized) {
Map<String, Object> augmented = new LinkedHashMap<>(map);
map.forEach((key, value) -> {
String qualifiedKey = getQualifiedKey(prefix, key);
if (value instanceof Map) {
augmented.put(key, getInputs(qualifiedKey, (Map<String, Object>) value));
augmented.put(key, getInputs(qualifiedKey, (Map<String, Object>) value, showUnsanitized));
}
else if (value instanceof List) {
augmented.put(key, getInputs(qualifiedKey, (List<Object>) value));
augmented.put(key, getInputs(qualifiedKey, (List<Object>) value, showUnsanitized));
}
else {
augmented.put(key, applyInput(qualifiedKey));
augmented.put(key, applyInput(qualifiedKey, showUnsanitized));
}
});
return augmented;
}
@SuppressWarnings("unchecked")
private List<Object> getInputs(String prefix, List<Object> list) {
private List<Object> getInputs(String prefix, List<Object> list, boolean showUnsanitized) {
List<Object> augmented = new ArrayList<>();
int index = 0;
for (Object item : list) {
String name = prefix + "[" + index++ + "]";
if (item instanceof Map) {
augmented.add(getInputs(name, (Map<String, Object>) item));
augmented.add(getInputs(name, (Map<String, Object>) item, showUnsanitized));
}
else if (item instanceof List) {
augmented.add(getInputs(name, (List<Object>) item));
augmented.add(getInputs(name, (List<Object>) item, showUnsanitized));
}
else {
augmented.add(applyInput(name));
augmented.add(applyInput(name, showUnsanitized));
}
}
return augmented;
}
private Map<String, Object> applyInput(String qualifiedKey) {
private Map<String, Object> 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<String, ContextConfigurationProperties> contexts;
private ApplicationConfigurationProperties(Map<String, ContextConfigurationProperties> contexts) {
ApplicationConfigurationProperties(Map<String, ContextConfigurationProperties> contexts) {
this.contexts = contexts;
}

View File

@ -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<String> roles;
public ConfigurationPropertiesReportEndpointWebExtension(ConfigurationPropertiesReportEndpoint delegate,
Show showValues, Set<String> 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<ApplicationConfigurationProperties> 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)

View File

@ -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<String> 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<String> 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<SanitizingFunction> 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<SanitizingFunction> 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<SanitizingFunction> 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;
}
}

View File

@ -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<String> roles) {
return switch (this) {
case NEVER -> false;
case ALWAYS -> true;
case WHEN_AUTHORIZED -> isAuthorized(securityContext, roles);
};
}
private boolean isAuthorized(SecurityContext securityContext, Collection<String> 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);
}
}

View File

@ -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<SanitizingFunction> sanitizingFunctions) {
public EnvironmentEndpoint(Environment environment, Iterable<SanitizingFunction> 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<String> propertyNamePredicate) {
PlaceholdersResolver resolver = getResolver();
private EnvironmentDescriptor getEnvironmentDescriptor(Predicate<String> propertyNamePredicate,
boolean showUnsanitized) {
List<PropertySourceDescriptor> 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<String, PropertyValueDescriptor> 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<String, PropertyValueDescriptor> 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<String, PropertyValueDescriptor> getPropertySourceDescriptors(String propertyName) {
private Map<String, PropertyValueDescriptor> getPropertySourceDescriptors(String propertyName,
boolean showUnsanitized) {
Map<String, PropertyValueDescriptor> 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<String> namePredicate) {
Predicate<String> namePredicate, boolean showUnsanitized) {
Map<String, PropertyValueDescriptor> 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<Object>) 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<String, PropertySource<?>> getPropertySourcesAsMap() {
Map<String, PropertySource<?>> 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<PropertySource<?>> sources;
PropertySourcesPlaceholdersSanitizingResolver(Iterable<PropertySource<?>> 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<PropertySourceEntryDescriptor> propertySources;
private EnvironmentEntryDescriptor(PropertySummaryDescriptor property, List<String> activeProfiles,
EnvironmentEntryDescriptor(PropertySummaryDescriptor property, List<String> activeProfiles,
List<PropertySourceEntryDescriptor> propertySources) {
this.property = property;
this.activeProfiles = activeProfiles;

View File

@ -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<String> roles;
public EnvironmentEndpointWebExtension(EnvironmentEndpoint delegate, Show showValues, Set<String> roles) {
this.delegate = delegate;
this.showValues = showValues;
this.roles = roles;
}
@ReadOperation
public WebEndpointResponse<EnvironmentEntryDescriptor> 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<EnvironmentEntryDescriptor> 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);
}

View File

@ -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<SanitizingFunction> 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<? extends Trigger> 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<String, Object> quartzTrigger(String groupName, String triggerName) throws SchedulerException {
Map<String, Object> 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<String, Object> sanitizeJobDataMap(JobDataMap dataMap) {
private Map<String, Object> sanitizeJobDataMap(JobDataMap dataMap, boolean showUnsanitized) {
if (dataMap != null) {
Map<String, Object> 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;

View File

@ -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<String> roles;
public QuartzEndpointWebExtension(QuartzEndpoint delegate, Show showValues, Set<String> roles) {
this.delegate = delegate;
this.showValues = showValues;
this.roles = roles;
}
@ReadOperation
@ -62,10 +72,11 @@ public class QuartzEndpointWebExtension {
}
@ReadOperation
public WebEndpointResponse<Object> 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<Object> 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 <T> WebEndpointResponse<T> handle(String jobsOrTriggers, ResponseSupplier<T> jobAction,

View File

@ -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<String> 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() {

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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<String, Object> secrets = (Map<String, Object>) properties.get("secrets");
Map<String, Object> hidden = (Map<String, Object>) 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<String, Object> sensitiveUri = (Map<String, Object>) 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<String, Object> noPasswordUri = (Map<String, Object>) 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<String> 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;
}

View File

@ -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);
}
}

View File

@ -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

View File

@ -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<String> 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("------");
}
}

View File

@ -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();
}
}

View File

@ -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<String, PropertySourceDescriptor> 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<String, PropertyValueDescriptor> 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<String, PropertyValueDescriptor> 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<String, PropertySourceDescriptor> 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<String, PropertyValueDescriptor> 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<String, PropertyValueDescriptor> 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<String, PropertyValueDescriptor> 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<String, PropertyValueDescriptor> 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<String, PropertyValueDescriptor> 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<String, PropertyValueDescriptor> 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<String, PropertySourceEntryDescriptor> 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<String, PropertySourceEntryDescriptor> 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<String, PropertySourceDescriptor> 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);
}
}

View File

@ -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);
}
}

View File

@ -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<String, Object> 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<String, Object> 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());
}
}

View File

@ -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<String, Object> triggerDetails = this.endpoint.quartzTrigger("samples", "3am-every-day");
Map<String, Object> 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<String, Object> triggerDetails = this.endpoint.quartzTrigger("samples", "every-hour");
Map<String, Object> 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<String, Object> triggerDetails = this.endpoint.quartzTrigger("samples", "every-hour-mon-wed");
Map<String, Object> 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<String, Object> triggerDetails = this.endpoint.quartzTrigger("samples", "once-a-week");
Map<String, Object> 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<String, Object> triggerDetails = this.endpoint.quartzTrigger("samples", "custom");
Map<String, Object> 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<String, Object> triggerDetails = this.endpoint.quartzTrigger("samples", "3am-every-day");
Map<String, Object> 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<String, Object> 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<String, Object> triggerDetails = this.endpoint.quartzTrigger("samples", "trigger");
Map<String, Object> 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<String, Object> 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 {

View File

@ -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();

View File

@ -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 {

View File

@ -36,22 +36,31 @@ See also the section on "`<<web#web.servlet.spring-mvc.error-handling, Error Han
[[howto.actuator.sanitize-sensitive-values]]
=== Sanitize Sensitive Values
Information returned by the `env` and `configprops` endpoints can be somewhat sensitive so keys matching certain patterns are sanitized by default (that is their values are replaced by `+******+`).
Spring Boot uses sensible defaults for such keys: any key ending with the word "password", "secret", "key", "token", "vcap_services", "sun.java.command" is entirely sanitized.
Additionally, any key that holds the word `credentials` (configured as a regular expression, that is `+.*credentials.*+`) as part of the key is also entirely sanitized.
Information returned by the `/env`, `/configprops` and `/quartz` endpoints can be somewhat sensitive.
All values are sanitized by default (that is replaced by `+******+`).
Viewing original values in the unsanitized form can be configured per endpoint using the `showValues` property for that endpoint.
This property can be configured to have the following values:
Furthermore, Spring Boot sanitizes the sensitive portion of URI-like values for keys with one of the following endings:
- `ALWAYS` - all values are shown in their unsanitized form to all users
- `NEVER - all values are always sanitized (that is replaced by `+******+`)
- `WHEN_AUTHORIZED` - all values are shown in their unsanitized form to authorized users
- `address`
- `addresses`
- `uri`
- `uris`
- `url`
- `urls`
For HTTP endpoints, a user is considered to be authorized if they have authenticated and have the roles configured by the endpoint's roles property.
By default, any authenticated user is authorized.
For JMX endpoints, all users are always authorized.
The sensitive portion of the URI is identified using the format `<scheme>://<username>:<password>@<host>:<port>/`.
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 `<<howto#howto.actuator.sanitize-sensitive-values.customizing-sanitization, SanitizingFunction>>` 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.