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; package org.springframework.boot.actuate.autoconfigure.context.properties;
import java.util.stream.Collectors;
import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnAvailableEndpoint; import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnAvailableEndpoint;
import org.springframework.boot.actuate.autoconfigure.endpoint.expose.EndpointExposure; import org.springframework.boot.actuate.autoconfigure.endpoint.expose.EndpointExposure;
@@ -50,17 +48,8 @@ public class ConfigurationPropertiesReportEndpointAutoConfiguration {
public ConfigurationPropertiesReportEndpoint configurationPropertiesReportEndpoint( public ConfigurationPropertiesReportEndpoint configurationPropertiesReportEndpoint(
ConfigurationPropertiesReportEndpointProperties properties, ConfigurationPropertiesReportEndpointProperties properties,
ObjectProvider<SanitizingFunction> sanitizingFunctions) { ObjectProvider<SanitizingFunction> sanitizingFunctions) {
ConfigurationPropertiesReportEndpoint endpoint = new ConfigurationPropertiesReportEndpoint( return new ConfigurationPropertiesReportEndpoint(sanitizingFunctions.orderedStream().toList(),
sanitizingFunctions.orderedStream().collect(Collectors.toList())); properties.getShowValues());
String[] keysToSanitize = properties.getKeysToSanitize();
if (keysToSanitize != null) {
endpoint.setKeysToSanitize(keysToSanitize);
}
String[] additionalKeysToSanitize = properties.getAdditionalKeysToSanitize();
if (additionalKeysToSanitize != null) {
endpoint.keysToSanitize(additionalKeysToSanitize);
}
return endpoint;
} }
@Bean @Bean
@@ -68,8 +57,10 @@ public class ConfigurationPropertiesReportEndpointAutoConfiguration {
@ConditionalOnBean(ConfigurationPropertiesReportEndpoint.class) @ConditionalOnBean(ConfigurationPropertiesReportEndpoint.class)
@ConditionalOnAvailableEndpoint(exposure = { EndpointExposure.WEB, EndpointExposure.CLOUD_FOUNDRY }) @ConditionalOnAvailableEndpoint(exposure = { EndpointExposure.WEB, EndpointExposure.CLOUD_FOUNDRY })
public ConfigurationPropertiesReportEndpointWebExtension configurationPropertiesReportEndpointWebExtension( public ConfigurationPropertiesReportEndpointWebExtension configurationPropertiesReportEndpointWebExtension(
ConfigurationPropertiesReportEndpoint configurationPropertiesReportEndpoint) { ConfigurationPropertiesReportEndpoint configurationPropertiesReportEndpoint,
return new ConfigurationPropertiesReportEndpointWebExtension(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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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; 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.context.properties.ConfigurationPropertiesReportEndpoint;
import org.springframework.boot.actuate.endpoint.Show;
import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.ConfigurationProperties;
/** /**
* Configuration properties for {@link ConfigurationPropertiesReportEndpoint}. * Configuration properties for {@link ConfigurationPropertiesReportEndpoint}.
* *
* @author Stephane Nicoll * @author Stephane Nicoll
* @author Madhura Bhave
* @since 2.0.0 * @since 2.0.0
*/ */
@ConfigurationProperties("management.endpoint.configprops") @ConfigurationProperties("management.endpoint.configprops")
public class ConfigurationPropertiesReportEndpointProperties { public class ConfigurationPropertiesReportEndpointProperties {
/** /**
* Keys that should be sanitized. Keys can be simple strings that the property ends * When to show unsanitized values.
* with or regular expressions.
*/ */
private String[] keysToSanitize; private Show showValues = Show.NEVER;
/** /**
* Keys that should be sanitized in addition to those already configured. Keys can be * Roles used to determine whether a user is authorized to be shown unsanitized
* simple strings that the property ends with or regular expressions. * values. When empty, all authenticated users are authorized.
*/ */
private String[] additionalKeysToSanitize; private Set<String> roles = new HashSet<>();
public String[] getKeysToSanitize() { public Show getShowValues() {
return this.keysToSanitize; return this.showValues;
} }
public void setKeysToSanitize(String[] keysToSanitize) { public void setShowValues(Show showValues) {
this.keysToSanitize = keysToSanitize; this.showValues = showValues;
} }
public String[] getAdditionalKeysToSanitize() { public Set<String> getRoles() {
return this.additionalKeysToSanitize; return this.roles;
}
public void setAdditionalKeysToSanitize(String[] additionalKeysToSanitize) {
this.additionalKeysToSanitize = additionalKeysToSanitize;
} }
} }

View File

@@ -16,8 +16,6 @@
package org.springframework.boot.actuate.autoconfigure.env; package org.springframework.boot.actuate.autoconfigure.env;
import java.util.stream.Collectors;
import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnAvailableEndpoint; import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnAvailableEndpoint;
import org.springframework.boot.actuate.autoconfigure.endpoint.expose.EndpointExposure; import org.springframework.boot.actuate.autoconfigure.endpoint.expose.EndpointExposure;
@@ -48,25 +46,18 @@ public class EnvironmentEndpointAutoConfiguration {
@ConditionalOnMissingBean @ConditionalOnMissingBean
public EnvironmentEndpoint environmentEndpoint(Environment environment, EnvironmentEndpointProperties properties, public EnvironmentEndpoint environmentEndpoint(Environment environment, EnvironmentEndpointProperties properties,
ObjectProvider<SanitizingFunction> sanitizingFunctions) { ObjectProvider<SanitizingFunction> sanitizingFunctions) {
EnvironmentEndpoint endpoint = new EnvironmentEndpoint(environment, return new EnvironmentEndpoint(environment, sanitizingFunctions.orderedStream().toList(),
sanitizingFunctions.orderedStream().collect(Collectors.toList())); properties.getShowValues());
String[] keysToSanitize = properties.getKeysToSanitize();
if (keysToSanitize != null) {
endpoint.setKeysToSanitize(keysToSanitize);
}
String[] additionalKeysToSanitize = properties.getAdditionalKeysToSanitize();
if (additionalKeysToSanitize != null) {
endpoint.keysToSanitize(additionalKeysToSanitize);
}
return endpoint;
} }
@Bean @Bean
@ConditionalOnMissingBean @ConditionalOnMissingBean
@ConditionalOnBean(EnvironmentEndpoint.class) @ConditionalOnBean(EnvironmentEndpoint.class)
@ConditionalOnAvailableEndpoint(exposure = { EndpointExposure.WEB, EndpointExposure.CLOUD_FOUNDRY }) @ConditionalOnAvailableEndpoint(exposure = { EndpointExposure.WEB, EndpointExposure.CLOUD_FOUNDRY })
public EnvironmentEndpointWebExtension environmentEndpointWebExtension(EnvironmentEndpoint environmentEndpoint) { public EnvironmentEndpointWebExtension environmentEndpointWebExtension(EnvironmentEndpoint environmentEndpoint,
return new EnvironmentEndpointWebExtension(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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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; 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.actuate.env.EnvironmentEndpoint;
import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.ConfigurationProperties;
@@ -29,31 +33,26 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
public class EnvironmentEndpointProperties { public class EnvironmentEndpointProperties {
/** /**
* Keys that should be sanitized. Keys can be simple strings that the property ends * When to show unsanitized values.
* with or regular expressions.
*/ */
private String[] keysToSanitize; private Show showValues = Show.NEVER;
/** /**
* Keys that should be sanitized in addition to those already configured. Keys can be * Roles used to determine whether a user is authorized to be shown unsanitized
* simple strings that the property ends with or regular expressions. * values. When empty, all authenticated users are authorized.
*/ */
private String[] additionalKeysToSanitize; private Set<String> roles = new HashSet<>();
public String[] getKeysToSanitize() { public Show getShowValues() {
return this.keysToSanitize; return this.showValues;
} }
public void setKeysToSanitize(String[] keysToSanitize) { public void setShowValues(Show showValues) {
this.keysToSanitize = keysToSanitize; this.showValues = showValues;
} }
public String[] getAdditionalKeysToSanitize() { public Set<String> getRoles() {
return this.additionalKeysToSanitize; return this.roles;
}
public void setAdditionalKeysToSanitize(String[] additionalKeysToSanitize) {
this.additionalKeysToSanitize = additionalKeysToSanitize;
} }
} }

View File

@@ -16,20 +16,15 @@
package org.springframework.boot.actuate.autoconfigure.health; package org.springframework.boot.actuate.autoconfigure.health;
import java.security.Principal;
import java.util.Collection; import java.util.Collection;
import java.util.function.Predicate; 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.SecurityContext;
import org.springframework.boot.actuate.endpoint.Show;
import org.springframework.boot.actuate.health.AdditionalHealthEndpointPath; import org.springframework.boot.actuate.health.AdditionalHealthEndpointPath;
import org.springframework.boot.actuate.health.HealthEndpointGroup; import org.springframework.boot.actuate.health.HealthEndpointGroup;
import org.springframework.boot.actuate.health.HttpCodeStatusMapper; import org.springframework.boot.actuate.health.HttpCodeStatusMapper;
import org.springframework.boot.actuate.health.StatusAggregator; 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}. * Auto-configured {@link HealthEndpointGroup} backed by {@link HealthProperties}.
@@ -83,54 +78,13 @@ class AutoConfiguredHealthEndpointGroup implements HealthEndpointGroup {
@Override @Override
public boolean showComponents(SecurityContext securityContext) { public boolean showComponents(SecurityContext securityContext) {
if (this.showComponents == null) { Show show = (this.showComponents != null) ? this.showComponents : this.showDetails;
return showDetails(securityContext); return show.isShown(securityContext, this.roles);
}
return getShowResult(securityContext, this.showComponents);
} }
@Override @Override
public boolean showDetails(SecurityContext securityContext) { public boolean showDetails(SecurityContext securityContext) {
return getShowResult(securityContext, this.showDetails); return this.showDetails.isShown(securityContext, this.roles);
}
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);
} }
@Override @Override

View File

@@ -31,8 +31,8 @@ import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.annotation.BeanFactoryAnnotationUtils; import org.springframework.beans.factory.annotation.BeanFactoryAnnotationUtils;
import org.springframework.boot.actuate.autoconfigure.health.HealthEndpointProperties.Group; 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.autoconfigure.health.HealthProperties.Status;
import org.springframework.boot.actuate.endpoint.Show;
import org.springframework.boot.actuate.health.AdditionalHealthEndpointPath; import org.springframework.boot.actuate.health.AdditionalHealthEndpointPath;
import org.springframework.boot.actuate.health.HealthEndpointGroup; import org.springframework.boot.actuate.health.HealthEndpointGroup;
import org.springframework.boot.actuate.health.HealthEndpointGroups; import org.springframework.boot.actuate.health.HealthEndpointGroups;

View File

@@ -21,6 +21,7 @@ import java.util.LinkedHashMap;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import org.springframework.boot.actuate.endpoint.Show;
import org.springframework.boot.actuate.health.HealthEndpoint; import org.springframework.boot.actuate.health.HealthEndpoint;
import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.ConfigurationProperties;

View File

@@ -23,7 +23,7 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; 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; 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.quartz.Scheduler;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnAvailableEndpoint; import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnAvailableEndpoint;
import org.springframework.boot.actuate.autoconfigure.endpoint.expose.EndpointExposure; 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.QuartzEndpoint;
import org.springframework.boot.actuate.quartz.QuartzEndpointWebExtension; import org.springframework.boot.actuate.quartz.QuartzEndpointWebExtension;
import org.springframework.boot.autoconfigure.AutoConfiguration; 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.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.quartz.QuartzAutoConfiguration; import org.springframework.boot.autoconfigure.quartz.QuartzAutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
/** /**
@@ -40,21 +43,23 @@ import org.springframework.context.annotation.Bean;
@AutoConfiguration(after = QuartzAutoConfiguration.class) @AutoConfiguration(after = QuartzAutoConfiguration.class)
@ConditionalOnClass(Scheduler.class) @ConditionalOnClass(Scheduler.class)
@ConditionalOnAvailableEndpoint(endpoint = QuartzEndpoint.class) @ConditionalOnAvailableEndpoint(endpoint = QuartzEndpoint.class)
@EnableConfigurationProperties(QuartzEndpointProperties.class)
public class QuartzEndpointAutoConfiguration { public class QuartzEndpointAutoConfiguration {
@Bean @Bean
@ConditionalOnBean(Scheduler.class) @ConditionalOnBean(Scheduler.class)
@ConditionalOnMissingBean @ConditionalOnMissingBean
public QuartzEndpoint quartzEndpoint(Scheduler scheduler) { public QuartzEndpoint quartzEndpoint(Scheduler scheduler, ObjectProvider<SanitizingFunction> sanitizingFunctions) {
return new QuartzEndpoint(scheduler); return new QuartzEndpoint(scheduler, sanitizingFunctions.orderedStream().toList());
} }
@Bean @Bean
@ConditionalOnBean(QuartzEndpoint.class) @ConditionalOnBean(QuartzEndpoint.class)
@ConditionalOnMissingBean @ConditionalOnMissingBean
@ConditionalOnAvailableEndpoint(exposure = { EndpointExposure.WEB, EndpointExposure.CLOUD_FOUNDRY }) @ConditionalOnAvailableEndpoint(exposure = { EndpointExposure.WEB, EndpointExposure.CLOUD_FOUNDRY })
public QuartzEndpointWebExtension quartzEndpointWebExtension(QuartzEndpoint endpoint) { public QuartzEndpointWebExtension quartzEndpointWebExtension(QuartzEndpoint endpoint,
return new QuartzEndpointWebExtension(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.", "description": "Whether to enable default metrics exporters.",
"defaultValue": true "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", "name": "management.endpoint.health.probes.add-additional-paths",
"type": "java.lang.Boolean", "type": "java.lang.Boolean",

View File

@@ -17,6 +17,7 @@
package org.springframework.boot.actuate.autoconfigure.context.properties; package org.springframework.boot.actuate.autoconfigure.context.properties;
import java.util.Map; import java.util.Map;
import java.util.Set;
import org.junit.jupiter.api.Test; 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.ConfigurationPropertiesReportEndpoint.ApplicationConfigurationProperties;
import org.springframework.boot.actuate.context.properties.ConfigurationPropertiesReportEndpointWebExtension; import org.springframework.boot.actuate.context.properties.ConfigurationPropertiesReportEndpointWebExtension;
import org.springframework.boot.actuate.endpoint.SanitizingFunction; import org.springframework.boot.actuate.endpoint.SanitizingFunction;
import org.springframework.boot.actuate.endpoint.Show;
import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties;
@@ -33,6 +35,7 @@ import org.springframework.boot.test.context.runner.ContextConsumer;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order; import org.springframework.core.annotation.Order;
import org.springframework.test.util.ReflectionTestUtils;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
@@ -50,7 +53,7 @@ class ConfigurationPropertiesReportEndpointAutoConfigurationTests {
void runShouldHaveEndpointBean() { void runShouldHaveEndpointBean() {
this.contextRunner.withUserConfiguration(Config.class) this.contextRunner.withUserConfiguration(Config.class)
.withPropertyValues("management.endpoints.web.exposure.include=configprops") .withPropertyValues("management.endpoints.web.exposure.include=configprops")
.run(validateTestProperties("******", "654321")); .run(validateTestProperties("******", "******"));
} }
@Test @Test
@@ -60,24 +63,42 @@ class ConfigurationPropertiesReportEndpointAutoConfigurationTests {
} }
@Test @Test
void keysToSanitizeCanBeConfiguredViaTheEnvironment() { @SuppressWarnings("unchecked")
void rolesCanBeConfiguredViaTheEnvironment() {
this.contextRunner.withUserConfiguration(Config.class) this.contextRunner.withUserConfiguration(Config.class)
.withPropertyValues("management.endpoint.configprops.keys-to-sanitize: .*pass.*, property") .withPropertyValues("management.endpoint.configprops.roles: test")
.withPropertyValues("management.endpoints.web.exposure.include=configprops") .withPropertyValues("management.endpoints.web.exposure.include=configprops").run((context) -> {
.run(validateTestProperties("******", "******")); 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 @Test
void additionalKeysToSanitizeCanBeConfiguredViaTheEnvironment() { @SuppressWarnings("unchecked")
void showValuesCanBeConfiguredViaTheEnvironment() {
this.contextRunner.withUserConfiguration(Config.class) this.contextRunner.withUserConfiguration(Config.class)
.withPropertyValues("management.endpoint.configprops.additional-keys-to-sanitize: property") .withPropertyValues("management.endpoint.configprops.show-values: WHEN_AUTHORIZED")
.withPropertyValues("management.endpoints.web.exposure.include=configprops") .withPropertyValues("management.endpoints.web.exposure.include=configprops").run((context) -> {
.run(validateTestProperties("******", "******")); 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 @Test
void customSanitizingFunctionsAreAppliedInOrder() { 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", .withPropertyValues("management.endpoints.web.exposure.include=configprops",
"test.my-test-property=abc") "test.my-test-property=abc")
.run(validateTestProperties("$$$111$$$", "$$$222$$$")); .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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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; package org.springframework.boot.actuate.autoconfigure.endpoint.web.documentation;
import java.util.Collections;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.boot.actuate.context.properties.ConfigurationPropertiesReportEndpoint; import org.springframework.boot.actuate.context.properties.ConfigurationPropertiesReportEndpoint;
import org.springframework.boot.actuate.endpoint.Show;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import; import org.springframework.context.annotation.Import;
@@ -80,7 +83,7 @@ class ConfigurationPropertiesReportEndpointDocumentationTests extends MockMvcEnd
@Bean @Bean
ConfigurationPropertiesReportEndpoint endpoint() { 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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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; package org.springframework.boot.actuate.autoconfigure.endpoint.web.documentation;
import java.io.IOException; import java.io.IOException;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
@@ -27,6 +28,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.SerializationFeature;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.boot.actuate.endpoint.Show;
import org.springframework.boot.actuate.env.EnvironmentEndpoint; import org.springframework.boot.actuate.env.EnvironmentEndpoint;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
@@ -148,7 +150,7 @@ class EnvironmentEndpointDocumentationTests extends MockMvcEndpointDocumentation
&& !"Inlined Test Properties".equals(propertySource.getName()); && !"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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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.impl.matchers.GroupMatcher;
import org.quartz.spi.OperableTrigger; 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.QuartzEndpoint;
import org.springframework.boot.actuate.quartz.QuartzEndpointWebExtension; import org.springframework.boot.actuate.quartz.QuartzEndpointWebExtension;
import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.boot.test.mock.mockito.MockBean;
@@ -456,12 +457,12 @@ class QuartzEndpointDocumentationTests extends MockMvcEndpointDocumentationTests
@Bean @Bean
QuartzEndpoint endpoint(Scheduler scheduler) { QuartzEndpoint endpoint(Scheduler scheduler) {
return new QuartzEndpoint(scheduler); return new QuartzEndpoint(scheduler, Collections.emptyList());
} }
@Bean @Bean
QuartzEndpointWebExtension endpointWebExtension(QuartzEndpoint endpoint) { 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; package org.springframework.boot.actuate.autoconfigure.env;
import java.util.Map; import java.util.Map;
import java.util.Set;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.boot.actuate.endpoint.SanitizingFunction; 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;
import org.springframework.boot.actuate.env.EnvironmentEndpoint.EnvironmentDescriptor; import org.springframework.boot.actuate.env.EnvironmentEndpoint.EnvironmentDescriptor;
import org.springframework.boot.actuate.env.EnvironmentEndpoint.PropertySourceDescriptor; import org.springframework.boot.actuate.env.EnvironmentEndpoint.PropertySourceDescriptor;
@@ -33,6 +35,7 @@ import org.springframework.boot.test.context.runner.ContextConsumer;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order; import org.springframework.core.annotation.Order;
import org.springframework.test.util.ReflectionTestUtils;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
@@ -64,17 +67,10 @@ class EnvironmentEndpointAutoConfigurationTests {
this.contextRunner.run((context) -> assertThat(context).doesNotHaveBean(EnvironmentEndpoint.class)); 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 @Test
void customSanitizingFunctionsAreAppliedInOrder() { void customSanitizingFunctionsAreAppliedInOrder() {
this.contextRunner.withUserConfiguration(SanitizingFunctionConfiguration.class) this.contextRunner.withUserConfiguration(SanitizingFunctionConfiguration.class)
.withPropertyValues("management.endpoint.env.show-values: WHEN_AUTHORIZED")
.withPropertyValues("management.endpoints.web.exposure.include=env") .withPropertyValues("management.endpoints.web.exposure.include=env")
.withSystemProperties("custom=123456", "password=123456").run((context) -> { .withSystemProperties("custom=123456", "password=123456").run((context) -> {
assertThat(context).hasSingleBean(EnvironmentEndpoint.class); assertThat(context).hasSingleBean(EnvironmentEndpoint.class);
@@ -88,11 +84,34 @@ class EnvironmentEndpointAutoConfigurationTests {
} }
@Test @Test
void additionalKeysToSanitizeCanBeConfiguredViaTheEnvironment() { @SuppressWarnings("unchecked")
this.contextRunner.withPropertyValues("management.endpoints.web.exposure.include=env") void rolesCanBeConfiguredViaTheEnvironment() {
.withSystemProperties("dbPassword=123456", "apiKey=123456") this.contextRunner.withPropertyValues("management.endpoint.env.roles: test")
.withPropertyValues("management.endpoint.env.additional-keys-to-sanitize=key") .withPropertyValues("management.endpoints.web.exposure.include=env")
.run(validateSystemProperties("******", "******")); .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 @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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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.Mock;
import org.mockito.junit.jupiter.MockitoExtension; 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.SecurityContext;
import org.springframework.boot.actuate.endpoint.Show;
import org.springframework.boot.actuate.health.HttpCodeStatusMapper; import org.springframework.boot.actuate.health.HttpCodeStatusMapper;
import org.springframework.boot.actuate.health.StatusAggregator; import org.springframework.boot.actuate.health.StatusAggregator;
import org.springframework.security.core.Authentication; 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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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; package org.springframework.boot.actuate.autoconfigure.quartz;
import java.util.Collections;
import java.util.Set;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.quartz.Scheduler; import org.quartz.Scheduler;
import org.springframework.boot.actuate.endpoint.Show;
import org.springframework.boot.actuate.quartz.QuartzEndpoint; import org.springframework.boot.actuate.quartz.QuartzEndpoint;
import org.springframework.boot.actuate.quartz.QuartzEndpointWebExtension; import org.springframework.boot.actuate.quartz.QuartzEndpointWebExtension;
import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.test.util.ReflectionTestUtils;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
@@ -81,6 +86,34 @@ class QuartzEndpointAutoConfigurationTests {
.doesNotHaveBean(QuartzEndpointWebExtension.class)); .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) @Configuration(proxyBeanMethods = false)
static class CustomEndpointConfiguration { static class CustomEndpointConfiguration {
@@ -94,7 +127,7 @@ class QuartzEndpointAutoConfigurationTests {
private static final class CustomEndpoint extends QuartzEndpoint { private static final class CustomEndpoint extends QuartzEndpoint {
private CustomEndpoint() { 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.SanitizableData;
import org.springframework.boot.actuate.endpoint.Sanitizer; import org.springframework.boot.actuate.endpoint.Sanitizer;
import org.springframework.boot.actuate.endpoint.SanitizingFunction; import org.springframework.boot.actuate.endpoint.SanitizingFunction;
import org.springframework.boot.actuate.endpoint.Show;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint; import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.boot.actuate.endpoint.annotation.Selector; import org.springframework.boot.actuate.endpoint.annotation.Selector;
@@ -83,11 +84,11 @@ import org.springframework.util.StringUtils;
* {@link ConfigurationProperties @ConfigurationProperties} annotated beans. * {@link ConfigurationProperties @ConfigurationProperties} annotated beans.
* *
* <p> * <p>
* To protect sensitive information from being exposed, certain property values are masked * To protect sensitive information from being exposed, all property values are masked by
* if their names end with a set of configurable values (default "password" and "secret"). * default. To configure when property values should be shown, use
* Configure property names by using * {@code management.endpoint.configprops.show-values} and
* {@code management.endpoint.configprops.keys-to-sanitize} in your Spring Boot * {@code management.endpoint.configprops.roles} in your Spring Boot application
* application configuration. * configuration.
* *
* @author Christian Dupuis * @author Christian Dupuis
* @author Dave Syer * @author Dave Syer
@@ -104,16 +105,15 @@ public class ConfigurationPropertiesReportEndpoint implements ApplicationContext
private final Sanitizer sanitizer; private final Sanitizer sanitizer;
private final Show showValues;
private ApplicationContext context; private ApplicationContext context;
private ObjectMapper objectMapper; private ObjectMapper objectMapper;
public ConfigurationPropertiesReportEndpoint() { public ConfigurationPropertiesReportEndpoint(Iterable<SanitizingFunction> sanitizingFunctions, Show showValues) {
this(Collections.emptyList());
}
public ConfigurationPropertiesReportEndpoint(Iterable<SanitizingFunction> sanitizingFunctions) {
this.sanitizer = new Sanitizer(sanitizingFunctions); this.sanitizer = new Sanitizer(sanitizingFunctions);
this.showValues = showValues;
} }
@Override @Override
@@ -121,31 +121,35 @@ public class ConfigurationPropertiesReportEndpoint implements ApplicationContext
this.context = context; this.context = context;
} }
public void setKeysToSanitize(String... keysToSanitize) {
this.sanitizer.setKeysToSanitize(keysToSanitize);
}
public void keysToSanitize(String... keysToSanitize) {
this.sanitizer.keysToSanitize(keysToSanitize);
}
@ReadOperation @ReadOperation
public ApplicationConfigurationProperties configurationProperties() { 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 @ReadOperation
public ApplicationConfigurationProperties configurationPropertiesWithPrefix(@Selector String prefix) { 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, ApplicationConfigurationProperties getConfigurationProperties(String prefix, boolean showUnsanitized) {
Predicate<ConfigurationPropertiesBean> beanFilterPredicate) { return getConfigurationProperties(this.context, (bean) -> bean.getAnnotation().prefix().startsWith(prefix),
showUnsanitized);
}
private ApplicationConfigurationProperties getConfigurationProperties(ApplicationContext context,
Predicate<ConfigurationPropertiesBean> beanFilterPredicate, boolean showUnsanitized) {
ObjectMapper mapper = getObjectMapper(); ObjectMapper mapper = getObjectMapper();
Map<String, ContextConfigurationProperties> contexts = new HashMap<>(); Map<String, ContextConfigurationProperties> contexts = new HashMap<>();
ApplicationContext target = context; ApplicationContext target = context;
while (target != null) { while (target != null) {
contexts.put(target.getId(), describeBeans(mapper, target, beanFilterPredicate)); contexts.put(target.getId(), describeBeans(mapper, target, beanFilterPredicate, showUnsanitized));
target = target.getParent(); target = target.getParent();
} }
return new ApplicationConfigurationProperties(contexts); return new ApplicationConfigurationProperties(contexts);
@@ -196,20 +200,21 @@ public class ConfigurationPropertiesReportEndpoint implements ApplicationContext
} }
private ContextConfigurationProperties describeBeans(ObjectMapper mapper, ApplicationContext context, private ContextConfigurationProperties describeBeans(ObjectMapper mapper, ApplicationContext context,
Predicate<ConfigurationPropertiesBean> beanFilterPredicate) { Predicate<ConfigurationPropertiesBean> beanFilterPredicate, boolean showUnsanitized) {
Map<String, ConfigurationPropertiesBean> beans = ConfigurationPropertiesBean.getAll(context); Map<String, ConfigurationPropertiesBean> beans = ConfigurationPropertiesBean.getAll(context);
Map<String, ConfigurationPropertiesBeanDescriptor> descriptors = beans.values().stream() Map<String, ConfigurationPropertiesBeanDescriptor> descriptors = beans.values().stream()
.filter(beanFilterPredicate) .filter(beanFilterPredicate).collect(Collectors.toMap(ConfigurationPropertiesBean::getName,
.collect(Collectors.toMap(ConfigurationPropertiesBean::getName, (bean) -> describeBean(mapper, bean))); (bean) -> describeBean(mapper, bean, showUnsanitized)));
return new ContextConfigurationProperties(descriptors, return new ContextConfigurationProperties(descriptors,
(context.getParent() != null) ? context.getParent().getId() : null); (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(); String prefix = bean.getAnnotation().prefix();
Map<String, Object> serialized = safeSerialize(mapper, bean.getInstance(), prefix); Map<String, Object> serialized = safeSerialize(mapper, bean.getInstance(), prefix);
Map<String, Object> properties = sanitize(prefix, serialized); Map<String, Object> properties = sanitize(prefix, serialized, showUnsanitized);
Map<String, Object> inputs = getInputs(prefix, serialized); Map<String, Object> inputs = getInputs(prefix, serialized, showUnsanitized);
return new ConfigurationPropertiesBeanDescriptor(prefix, properties, inputs); return new ConfigurationPropertiesBeanDescriptor(prefix, properties, inputs);
} }
@@ -236,35 +241,36 @@ public class ConfigurationPropertiesReportEndpoint implements ApplicationContext
* information. * information.
* @param prefix the property prefix * @param prefix the property prefix
* @param map the source map * @param map the source map
* @param showUnsanitized whether to show the unsanitized values
* @return the sanitized map * @return the sanitized map
*/ */
@SuppressWarnings("unchecked") @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) -> { map.forEach((key, value) -> {
String qualifiedKey = getQualifiedKey(prefix, key); String qualifiedKey = getQualifiedKey(prefix, key);
if (value instanceof Map) { 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) { else if (value instanceof List) {
map.put(key, sanitize(qualifiedKey, (List<Object>) value)); map.put(key, sanitize(qualifiedKey, (List<Object>) value, showUnsanitized));
} }
else { else {
map.put(key, sanitizeWithPropertySourceIfPresent(qualifiedKey, value)); map.put(key, sanitizeWithPropertySourceIfPresent(qualifiedKey, value, showUnsanitized));
} }
}); });
return map; return map;
} }
private Object sanitizeWithPropertySourceIfPresent(String qualifiedKey, Object value) { private Object sanitizeWithPropertySourceIfPresent(String qualifiedKey, Object value, boolean showUnsanitized) {
ConfigurationPropertyName currentName = getCurrentName(qualifiedKey); ConfigurationPropertyName currentName = getCurrentName(qualifiedKey);
ConfigurationProperty candidate = getCandidate(currentName); ConfigurationProperty candidate = getCandidate(currentName);
PropertySource<?> propertySource = getPropertySource(candidate); PropertySource<?> propertySource = getPropertySource(candidate);
if (propertySource != null) { if (propertySource != null) {
SanitizableData data = new SanitizableData(propertySource, qualifiedKey, value); 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); SanitizableData data = new SanitizableData(null, qualifiedKey, value);
return this.sanitizer.sanitize(data); return this.sanitizer.sanitize(data, showUnsanitized);
} }
private PropertySource<?> getPropertySource(ConfigurationProperty configurationProperty) { private PropertySource<?> getPropertySource(ConfigurationProperty configurationProperty) {
@@ -293,69 +299,69 @@ public class ConfigurationPropertiesReportEndpoint implements ApplicationContext
} }
@SuppressWarnings("unchecked") @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<>(); List<Object> sanitized = new ArrayList<>();
int index = 0; int index = 0;
for (Object item : list) { for (Object item : list) {
String name = prefix + "[" + index++ + "]"; String name = prefix + "[" + index++ + "]";
if (item instanceof Map) { 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) { else if (item instanceof List) {
sanitized.add(sanitize(name, (List<Object>) item)); sanitized.add(sanitize(name, (List<Object>) item, showUnsanitized));
} }
else { else {
sanitized.add(sanitizeWithPropertySourceIfPresent(name, item)); sanitized.add(sanitizeWithPropertySourceIfPresent(name, item, showUnsanitized));
} }
} }
return sanitized; return sanitized;
} }
@SuppressWarnings("unchecked") @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<String, Object> augmented = new LinkedHashMap<>(map);
map.forEach((key, value) -> { map.forEach((key, value) -> {
String qualifiedKey = getQualifiedKey(prefix, key); String qualifiedKey = getQualifiedKey(prefix, key);
if (value instanceof Map) { 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) { else if (value instanceof List) {
augmented.put(key, getInputs(qualifiedKey, (List<Object>) value)); augmented.put(key, getInputs(qualifiedKey, (List<Object>) value, showUnsanitized));
} }
else { else {
augmented.put(key, applyInput(qualifiedKey)); augmented.put(key, applyInput(qualifiedKey, showUnsanitized));
} }
}); });
return augmented; return augmented;
} }
@SuppressWarnings("unchecked") @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<>(); List<Object> augmented = new ArrayList<>();
int index = 0; int index = 0;
for (Object item : list) { for (Object item : list) {
String name = prefix + "[" + index++ + "]"; String name = prefix + "[" + index++ + "]";
if (item instanceof Map) { 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) { else if (item instanceof List) {
augmented.add(getInputs(name, (List<Object>) item)); augmented.add(getInputs(name, (List<Object>) item, showUnsanitized));
} }
else { else {
augmented.add(applyInput(name)); augmented.add(applyInput(name, showUnsanitized));
} }
} }
return augmented; return augmented;
} }
private Map<String, Object> applyInput(String qualifiedKey) { private Map<String, Object> applyInput(String qualifiedKey, boolean showUnsanitized) {
ConfigurationPropertyName currentName = getCurrentName(qualifiedKey); ConfigurationPropertyName currentName = getCurrentName(qualifiedKey);
ConfigurationProperty candidate = getCandidate(currentName); ConfigurationProperty candidate = getCandidate(currentName);
PropertySource<?> propertySource = getPropertySource(candidate); PropertySource<?> propertySource = getPropertySource(candidate);
if (propertySource != null) { if (propertySource != null) {
Object value = stringifyIfNecessary(candidate.getValue()); Object value = stringifyIfNecessary(candidate.getValue());
SanitizableData data = new SanitizableData(propertySource, currentName.toString(), value); 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(); return Collections.emptyMap();
} }
@@ -550,7 +556,7 @@ public class ConfigurationPropertiesReportEndpoint implements ApplicationContext
private final Map<String, ContextConfigurationProperties> contexts; private final Map<String, ContextConfigurationProperties> contexts;
private ApplicationConfigurationProperties(Map<String, ContextConfigurationProperties> contexts) { ApplicationConfigurationProperties(Map<String, ContextConfigurationProperties> contexts) {
this.contexts = 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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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; 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.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.ReadOperation;
import org.springframework.boot.actuate.endpoint.annotation.Selector; import org.springframework.boot.actuate.endpoint.annotation.Selector;
import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse; import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse;
@@ -34,15 +38,29 @@ public class ConfigurationPropertiesReportEndpointWebExtension {
private final ConfigurationPropertiesReportEndpoint delegate; 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.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 @ReadOperation
public WebEndpointResponse<ApplicationConfigurationProperties> configurationPropertiesWithPrefix( public WebEndpointResponse<ApplicationConfigurationProperties> configurationPropertiesWithPrefix(
@Selector String prefix) { SecurityContext securityContext, @Selector String prefix) {
ApplicationConfigurationProperties configurationProperties = this.delegate boolean showUnsanitized = this.showValues.isShown(securityContext, this.roles);
.configurationPropertiesWithPrefix(prefix); ApplicationConfigurationProperties configurationProperties = this.delegate.getConfigurationProperties(prefix,
showUnsanitized);
boolean foundMatchingBeans = configurationProperties.getContexts().values().stream() boolean foundMatchingBeans = configurationProperties.getContexts().values().stream()
.anyMatch((context) -> !context.getBeans().isEmpty()); .anyMatch((context) -> !context.getBeans().isEmpty());
return (foundMatchingBeans) ? new WebEndpointResponse<>(configurationProperties, WebEndpointResponse.STATUS_OK) return (foundMatchingBeans) ? new WebEndpointResponse<>(configurationProperties, WebEndpointResponse.STATUS_OK)

View File

@@ -17,17 +17,8 @@
package org.springframework.boot.actuate.endpoint; package org.springframework.boot.actuate.endpoint;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List; 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 * Strategy that should be used by endpoint implementations to sanitize potentially
@@ -46,148 +37,40 @@ import org.springframework.util.StringUtils;
*/ */
public class Sanitizer { 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<>(); 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() { 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. * Create a new {@link Sanitizer} instance with sanitizing functions.
* @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.
* @param sanitizingFunctions the sanitizing functions to apply * @param sanitizingFunctions the sanitizing functions to apply
* @since 2.6.0 * @since 2.6.0
*/ */
public Sanitizer(Iterable<SanitizingFunction> sanitizingFunctions) { 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); 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 * Sanitize the value from the given {@link SanitizableData} using the available
* {@link SanitizingFunction}s. * {@link SanitizingFunction}s.
* @param data the sanitizable data * @param data the sanitizable data
* @param showUnsanitized whether to show the unsanitized values or not
* @return the potentially updated data * @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(); Object value = data.getValue();
if (value == null) { if (value == null) {
return null; return null;
} }
if (!showUnsanitized) {
return SanitizableData.SANITIZED_VALUE;
}
for (SanitizingFunction sanitizingFunction : this.sanitizingFunctions) { for (SanitizingFunction sanitizingFunction : this.sanitizingFunctions) {
data = sanitizingFunction.apply(data); data = sanitizingFunction.apply(data);
Object sanitizedValue = data.getValue(); Object sanitizedValue = data.getValue();
@@ -198,26 +81,4 @@ public class Sanitizer {
return value; 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.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@@ -31,6 +30,7 @@ import com.fasterxml.jackson.annotation.JsonInclude;
import org.springframework.boot.actuate.endpoint.SanitizableData; import org.springframework.boot.actuate.endpoint.SanitizableData;
import org.springframework.boot.actuate.endpoint.Sanitizer; import org.springframework.boot.actuate.endpoint.Sanitizer;
import org.springframework.boot.actuate.endpoint.SanitizingFunction; import org.springframework.boot.actuate.endpoint.SanitizingFunction;
import org.springframework.boot.actuate.endpoint.Show;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint; import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.boot.actuate.endpoint.annotation.Selector; import org.springframework.boot.actuate.endpoint.annotation.Selector;
@@ -48,9 +48,7 @@ import org.springframework.core.env.PropertySource;
import org.springframework.core.env.StandardEnvironment; import org.springframework.core.env.StandardEnvironment;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
import org.springframework.util.ClassUtils; import org.springframework.util.ClassUtils;
import org.springframework.util.PropertyPlaceholderHelper;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import org.springframework.util.SystemPropertyUtils;
/** /**
* {@link Endpoint @Endpoint} to expose {@link ConfigurableEnvironment environment} * {@link Endpoint @Endpoint} to expose {@link ConfigurableEnvironment environment}
@@ -71,50 +69,48 @@ public class EnvironmentEndpoint {
private final Environment environment; private final Environment environment;
public EnvironmentEndpoint(Environment environment) { private final Show showValues;
this(environment, Collections.emptyList());
}
public EnvironmentEndpoint(Environment environment, Iterable<SanitizingFunction> sanitizingFunctions) { public EnvironmentEndpoint(Environment environment, Iterable<SanitizingFunction> sanitizingFunctions,
Show showValues) {
this.environment = environment; this.environment = environment;
this.sanitizer = new Sanitizer(sanitizingFunctions); this.sanitizer = new Sanitizer(sanitizingFunctions);
} this.showValues = showValues;
public void setKeysToSanitize(String... keysToSanitize) {
this.sanitizer.setKeysToSanitize(keysToSanitize);
}
public void keysToSanitize(String... keysToSanitize) {
this.sanitizer.keysToSanitize(keysToSanitize);
} }
@ReadOperation @ReadOperation
public EnvironmentDescriptor environment(@Nullable String pattern) { 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)) { 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 private EnvironmentDescriptor getEnvironmentDescriptor(Predicate<String> propertyNamePredicate,
public EnvironmentEntryDescriptor environmentEntry(@Selector String toMatch) { boolean showUnsanitized) {
return getEnvironmentEntryDescriptor(toMatch);
}
private EnvironmentDescriptor getEnvironmentDescriptor(Predicate<String> propertyNamePredicate) {
PlaceholdersResolver resolver = getResolver();
List<PropertySourceDescriptor> propertySources = new ArrayList<>(); List<PropertySourceDescriptor> propertySources = new ArrayList<>();
getPropertySourcesAsMap().forEach((sourceName, source) -> { getPropertySourcesAsMap().forEach((sourceName, source) -> {
if (source instanceof EnumerablePropertySource) { if (source instanceof EnumerablePropertySource) {
propertySources.add(describeSource(sourceName, (EnumerablePropertySource<?>) source, resolver, propertySources.add(describeSource(sourceName, (EnumerablePropertySource<?>) source,
propertyNamePredicate)); propertyNamePredicate, showUnsanitized));
} }
}); });
return new EnvironmentDescriptor(Arrays.asList(this.environment.getActiveProfiles()), propertySources); return new EnvironmentDescriptor(Arrays.asList(this.environment.getActiveProfiles()), propertySources);
} }
private EnvironmentEntryDescriptor getEnvironmentEntryDescriptor(String propertyName) { @ReadOperation
Map<String, PropertyValueDescriptor> descriptors = getPropertySourceDescriptors(propertyName); 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); PropertySummaryDescriptor summary = getPropertySummaryDescriptor(descriptors);
return new EnvironmentEntryDescriptor(summary, Arrays.asList(this.environment.getActiveProfiles()), return new EnvironmentEntryDescriptor(summary, Arrays.asList(this.environment.getActiveProfiles()),
toPropertySourceDescriptors(descriptors)); toPropertySourceDescriptors(descriptors));
@@ -136,35 +132,31 @@ public class EnvironmentEndpoint {
return null; return null;
} }
private Map<String, PropertyValueDescriptor> getPropertySourceDescriptors(String propertyName) { private Map<String, PropertyValueDescriptor> getPropertySourceDescriptors(String propertyName,
boolean showUnsanitized) {
Map<String, PropertyValueDescriptor> propertySources = new LinkedHashMap<>(); Map<String, PropertyValueDescriptor> propertySources = new LinkedHashMap<>();
PlaceholdersResolver resolver = getResolver();
getPropertySourcesAsMap().forEach((sourceName, source) -> propertySources.put(sourceName, 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; return propertySources;
} }
private PropertySourceDescriptor describeSource(String sourceName, EnumerablePropertySource<?> source, private PropertySourceDescriptor describeSource(String sourceName, EnumerablePropertySource<?> source,
PlaceholdersResolver resolver, Predicate<String> namePredicate) { Predicate<String> namePredicate, boolean showUnsanitized) {
Map<String, PropertyValueDescriptor> properties = new LinkedHashMap<>(); Map<String, PropertyValueDescriptor> properties = new LinkedHashMap<>();
Stream.of(source.getPropertyNames()).filter(namePredicate) 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); return new PropertySourceDescriptor(sourceName, properties);
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
private PropertyValueDescriptor describeValueOf(String name, PropertySource<?> source, private PropertyValueDescriptor describeValueOf(String name, PropertySource<?> source, boolean showUnsanitized) {
PlaceholdersResolver resolver) { PlaceholdersResolver resolver = new PropertySourcesPlaceholdersResolver(getPropertySources());
Object resolved = resolver.resolvePlaceholders(source.getProperty(name)); Object resolved = resolver.resolvePlaceholders(source.getProperty(name));
Origin origin = ((source instanceof OriginLookup) ? ((OriginLookup<Object>) source).getOrigin(name) : null); Origin origin = ((source instanceof OriginLookup) ? ((OriginLookup<Object>) source).getOrigin(name) : null);
Object sanitizedValue = sanitize(source, name, resolved); Object sanitizedValue = sanitize(source, name, resolved, showUnsanitized);
return new PropertyValueDescriptor(stringifyIfNecessary(sanitizedValue), origin); return new PropertyValueDescriptor(stringifyIfNecessary(sanitizedValue), origin);
} }
private PlaceholdersResolver getResolver() {
return new PropertySourcesPlaceholdersSanitizingResolver(getPropertySources(), this.sanitizer);
}
private Map<String, PropertySource<?>> getPropertySourcesAsMap() { private Map<String, PropertySource<?>> getPropertySourcesAsMap() {
Map<String, PropertySource<?>> map = new LinkedHashMap<>(); Map<String, PropertySource<?>> map = new LinkedHashMap<>();
for (PropertySource<?> source : getPropertySources()) { for (PropertySource<?> source : getPropertySources()) {
@@ -193,8 +185,8 @@ public class EnvironmentEndpoint {
} }
} }
private Object sanitize(PropertySource<?> source, String name, Object value) { private Object sanitize(PropertySource<?> source, String name, Object value, boolean showUnsanitized) {
return this.sanitizer.sanitize(new SanitizableData(source, name, value)); return this.sanitizer.sanitize(new SanitizableData(source, name, value), showUnsanitized);
} }
protected Object stringifyIfNecessary(Object value) { protected Object stringifyIfNecessary(Object value) {
@@ -208,40 +200,6 @@ public class EnvironmentEndpoint {
return "Complex property type " + value.getClass().getName(); 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}. * A description of an {@link Environment}.
*/ */
@@ -278,7 +236,7 @@ public class EnvironmentEndpoint {
private final List<PropertySourceEntryDescriptor> propertySources; private final List<PropertySourceEntryDescriptor> propertySources;
private EnvironmentEntryDescriptor(PropertySummaryDescriptor property, List<String> activeProfiles, EnvironmentEntryDescriptor(PropertySummaryDescriptor property, List<String> activeProfiles,
List<PropertySourceEntryDescriptor> propertySources) { List<PropertySourceEntryDescriptor> propertySources) {
this.property = property; this.property = property;
this.activeProfiles = activeProfiles; 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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -16,11 +16,17 @@
package org.springframework.boot.actuate.env; 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.ReadOperation;
import org.springframework.boot.actuate.endpoint.annotation.Selector; import org.springframework.boot.actuate.endpoint.annotation.Selector;
import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse; import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse;
import org.springframework.boot.actuate.endpoint.web.annotation.EndpointWebExtension; 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.boot.actuate.env.EnvironmentEndpoint.EnvironmentEntryDescriptor;
import org.springframework.lang.Nullable;
/** /**
* {@link EndpointWebExtension @EndpointWebExtension} for the {@link EnvironmentEndpoint}. * {@link EndpointWebExtension @EndpointWebExtension} for the {@link EnvironmentEndpoint}.
@@ -34,13 +40,27 @@ public class EnvironmentEndpointWebExtension {
private final EnvironmentEndpoint delegate; 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.delegate = delegate;
this.showValues = showValues;
this.roles = roles;
} }
@ReadOperation @ReadOperation
public WebEndpointResponse<EnvironmentEntryDescriptor> environmentEntry(@Selector String toMatch) { public EnvironmentDescriptor environment(SecurityContext securityContext, @Nullable String pattern) {
EnvironmentEntryDescriptor descriptor = this.delegate.environmentEntry(toMatch); 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) return (descriptor.getProperty() != null) ? new WebEndpointResponse<>(descriptor, WebEndpointResponse.STATUS_OK)
: new WebEndpointResponse<>(WebEndpointResponse.STATUS_NOT_FOUND); : new WebEndpointResponse<>(WebEndpointResponse.STATUS_NOT_FOUND);
} }

View File

@@ -48,7 +48,9 @@ import org.quartz.Trigger.TriggerState;
import org.quartz.TriggerKey; import org.quartz.TriggerKey;
import org.quartz.impl.matchers.GroupMatcher; 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.Sanitizer;
import org.springframework.boot.actuate.endpoint.SanitizingFunction;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint; import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.util.Assert; import org.springframework.util.Assert;
@@ -71,25 +73,10 @@ public class QuartzEndpoint {
private final Sanitizer sanitizer; private final Sanitizer sanitizer;
/** public QuartzEndpoint(Scheduler scheduler, Iterable<SanitizingFunction> sanitizingFunctions) {
* 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) {
Assert.notNull(scheduler, "Scheduler must not be null"); Assert.notNull(scheduler, "Scheduler must not be null");
Assert.notNull(sanitizer, "Sanitizer must not be null");
this.scheduler = scheduler; this.scheduler = scheduler;
this.sanitizer = sanitizer; this.sanitizer = new Sanitizer(sanitizingFunctions);
} }
/** /**
@@ -201,17 +188,19 @@ public class QuartzEndpoint {
* group name and job name. * group name and job name.
* @param groupName the name of the group * @param groupName the name of the group
* @param jobName the name of the job * @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 * @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 * @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); JobKey jobKey = JobKey.jobKey(jobName, groupName);
JobDetail jobDetail = this.scheduler.getJobDetail(jobKey); JobDetail jobDetail = this.scheduler.getJobDetail(jobKey);
if (jobDetail != null) { if (jobDetail != null) {
List<? extends Trigger> triggers = this.scheduler.getTriggersOfJob(jobKey); List<? extends Trigger> triggers = this.scheduler.getTriggersOfJob(jobKey);
return new QuartzJobDetails(jobDetail.getKey().getGroup(), jobDetail.getKey().getName(), return new QuartzJobDetails(jobDetail.getKey().getGroup(), jobDetail.getKey().getName(),
jobDetail.getDescription(), jobDetail.getJobClass().getName(), jobDetail.isDurable(), jobDetail.getDescription(), jobDetail.getJobClass().getName(), jobDetail.isDurable(),
jobDetail.requestsRecovery(), sanitizeJobDataMap(jobDetail.getJobDataMap()), jobDetail.requestsRecovery(), sanitizeJobDataMap(jobDetail.getJobDataMap(), showUnsanitized),
extractTriggersSummary(triggers)); extractTriggersSummary(triggers));
} }
return null; return null;
@@ -236,14 +225,18 @@ public class QuartzEndpoint {
* name. * name.
* @param groupName the name of the group * @param groupName the name of the group
* @param triggerName the name of the trigger * @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 * @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 * @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); TriggerKey triggerKey = TriggerKey.triggerKey(triggerName, groupName);
Trigger trigger = this.scheduler.getTrigger(triggerKey); Trigger trigger = this.scheduler.getTrigger(triggerKey);
return (trigger != null) ? TriggerDescription.of(trigger).buildDetails( return (trigger != null)
this.scheduler.getTriggerState(triggerKey), sanitizeJobDataMap(trigger.getJobDataMap())) : null; ? TriggerDescription.of(trigger).buildDetails(this.scheduler.getTriggerState(triggerKey),
sanitizeJobDataMap(trigger.getJobDataMap(), showUnsanitized))
: null;
} }
private static Duration getIntervalDuration(long amount, IntervalUnit unit) { private static Duration getIntervalDuration(long amount, IntervalUnit unit) {
@@ -255,15 +248,20 @@ public class QuartzEndpoint {
: null; : null;
} }
private Map<String, Object> sanitizeJobDataMap(JobDataMap dataMap) { private Map<String, Object> sanitizeJobDataMap(JobDataMap dataMap, boolean showUnsanitized) {
if (dataMap != null) { if (dataMap != null) {
Map<String, Object> map = new LinkedHashMap<>(dataMap.getWrappedMap()); Map<String, Object> map = new LinkedHashMap<>(dataMap.getWrappedMap());
map.replaceAll(this.sanitizer::sanitize); map.replaceAll((key, value) -> getSanitizedValue(showUnsanitized, key, value));
return map; return map;
} }
return null; 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) { private static TemporalUnit temporalUnit(IntervalUnit unit) {
return switch (unit) { return switch (unit) {
case DAY -> ChronoUnit.DAYS; case DAY -> ChronoUnit.DAYS;

View File

@@ -16,10 +16,14 @@
package org.springframework.boot.actuate.quartz; package org.springframework.boot.actuate.quartz;
import java.util.Set;
import org.quartz.SchedulerException; import org.quartz.SchedulerException;
import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.RuntimeHintsRegistrar; 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.ReadOperation;
import org.springframework.boot.actuate.endpoint.annotation.Selector; import org.springframework.boot.actuate.endpoint.annotation.Selector;
import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse; import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse;
@@ -44,8 +48,14 @@ public class QuartzEndpointWebExtension {
private final QuartzEndpoint delegate; 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.delegate = delegate;
this.showValues = showValues;
this.roles = roles;
} }
@ReadOperation @ReadOperation
@@ -62,10 +72,11 @@ public class QuartzEndpointWebExtension {
} }
@ReadOperation @ReadOperation
public WebEndpointResponse<Object> quartzJobOrTrigger(@Selector String jobsOrTriggers, @Selector String group, public WebEndpointResponse<Object> quartzJobOrTrigger(SecurityContext securityContext,
@Selector String name) throws SchedulerException { @Selector String jobsOrTriggers, @Selector String group, @Selector String name) throws SchedulerException {
return handle(jobsOrTriggers, () -> this.delegate.quartzJob(group, name), boolean showUnsanitized = this.showValues.isShown(securityContext, this.roles);
() -> this.delegate.quartzTrigger(group, name)); 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, 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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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; package org.springframework.boot.actuate.context.properties;
import java.util.Collections;
import java.util.Optional;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.boot.actuate.context.properties.ConfigurationPropertiesReportEndpoint.ApplicationConfigurationProperties; 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.context.properties.ConfigurationPropertiesReportEndpoint.ContextConfigurationProperties;
import org.springframework.boot.actuate.endpoint.Show;
import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
@@ -39,16 +45,7 @@ class ConfigurationPropertiesReportEndpointFilteringTests {
void filterByPrefixSingleMatch() { void filterByPrefixSingleMatch() {
ApplicationContextRunner contextRunner = new ApplicationContextRunner().withUserConfiguration(Config.class) ApplicationContextRunner contextRunner = new ApplicationContextRunner().withUserConfiguration(Config.class)
.withPropertyValues("foo.primary.name:foo1", "foo.secondary.name:foo2", "only.bar.name:solo1"); .withPropertyValues("foo.primary.name:foo1", "foo.secondary.name:foo2", "only.bar.name:solo1");
contextRunner.run((context) -> { assertProperties(contextRunner, "solo1");
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");
});
} }
@Test @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) @Configuration(proxyBeanMethods = false)
@Import(BaseConfiguration.class)
@EnableConfigurationProperties(Bar.class) @EnableConfigurationProperties(Bar.class)
static class Config { static class Config {
@Bean @Bean
ConfigurationPropertiesReportEndpoint endpoint() { 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 @Bean
@ConfigurationProperties(prefix = "foo.primary") @ConfigurationProperties(prefix = "foo.primary")
Foo primaryFoo() { 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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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; package org.springframework.boot.actuate.context.properties;
import java.util.Collections;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.boot.actuate.context.properties.ConfigurationPropertiesReportEndpoint.ApplicationConfigurationProperties; 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.ConfigurationPropertiesBeanDescriptor;
import org.springframework.boot.actuate.context.properties.ConfigurationPropertiesReportEndpoint.ContextConfigurationProperties; import org.springframework.boot.actuate.context.properties.ConfigurationPropertiesReportEndpoint.ContextConfigurationProperties;
import org.springframework.boot.actuate.endpoint.Show;
import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.boot.test.context.runner.ApplicationContextRunner;
@@ -79,7 +82,7 @@ class ConfigurationPropertiesReportEndpointMethodAnnotationsTests {
@Bean @Bean
ConfigurationPropertiesReportEndpoint endpoint() { ConfigurationPropertiesReportEndpoint endpoint() {
return new ConfigurationPropertiesReportEndpoint(); return new ConfigurationPropertiesReportEndpoint(Collections.emptyList(), Show.ALWAYS);
} }
@Bean @Bean
@@ -102,7 +105,7 @@ class ConfigurationPropertiesReportEndpointMethodAnnotationsTests {
@Bean @Bean
ConfigurationPropertiesReportEndpoint endpoint() { ConfigurationPropertiesReportEndpoint endpoint() {
return new ConfigurationPropertiesReportEndpoint(); return new ConfigurationPropertiesReportEndpoint(Collections.emptyList(), Show.ALWAYS);
} }
@Bean @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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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; package org.springframework.boot.actuate.context.properties;
import java.util.Collections;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.boot.actuate.context.properties.ConfigurationPropertiesReportEndpoint.ApplicationConfigurationProperties; 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.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.boot.test.context.runner.ApplicationContextRunner;
@@ -86,7 +89,7 @@ class ConfigurationPropertiesReportEndpointParentTests {
@Bean @Bean
ConfigurationPropertiesReportEndpoint endpoint() { ConfigurationPropertiesReportEndpoint endpoint() {
return new ConfigurationPropertiesReportEndpoint(); return new ConfigurationPropertiesReportEndpoint(Collections.emptyList(), Show.ALWAYS);
} }
@Bean @Bean
@@ -102,7 +105,7 @@ class ConfigurationPropertiesReportEndpointParentTests {
@Bean @Bean
ConfigurationPropertiesReportEndpoint endpoint() { ConfigurationPropertiesReportEndpoint endpoint() {
return new ConfigurationPropertiesReportEndpoint(); return new ConfigurationPropertiesReportEndpoint(Collections.emptyList(), Show.ALWAYS);
} }
@Bean @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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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; package org.springframework.boot.actuate.context.properties;
import java.util.Collections;
import java.util.Map; import java.util.Map;
import javax.sql.DataSource; 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.ApplicationConfigurationProperties;
import org.springframework.boot.actuate.context.properties.ConfigurationPropertiesReportEndpoint.ConfigurationPropertiesBeanDescriptor; 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.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.boot.test.context.runner.ApplicationContextRunner;
@@ -85,7 +87,7 @@ class ConfigurationPropertiesReportEndpointProxyTests {
@Bean @Bean
ConfigurationPropertiesReportEndpoint endpoint() { ConfigurationPropertiesReportEndpoint endpoint() {
return new ConfigurationPropertiesReportEndpoint(); return new ConfigurationPropertiesReportEndpoint(Collections.emptyList(), Show.ALWAYS);
} }
@Bean @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.ApplicationConfigurationProperties;
import org.springframework.boot.actuate.context.properties.ConfigurationPropertiesReportEndpoint.ConfigurationPropertiesBeanDescriptor; 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.ConfigurationProperties;
import org.springframework.boot.context.properties.ConfigurationPropertiesBinding; import org.springframework.boot.context.properties.ConfigurationPropertiesBinding;
import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties;
@@ -290,7 +291,7 @@ class ConfigurationPropertiesReportEndpointSerializationTests {
@Bean @Bean
ConfigurationPropertiesReportEndpoint endpoint() { ConfigurationPropertiesReportEndpoint endpoint() {
return new ConfigurationPropertiesReportEndpoint(); return new ConfigurationPropertiesReportEndpoint(Collections.emptyList(), Show.ALWAYS);
} }
} }
@@ -553,7 +554,7 @@ class ConfigurationPropertiesReportEndpointSerializationTests {
@Bean @Bean
ConfigurationPropertiesReportEndpoint endpoint() { ConfigurationPropertiesReportEndpoint endpoint() {
return new ConfigurationPropertiesReportEndpoint(); return new ConfigurationPropertiesReportEndpoint(Collections.emptyList(), Show.ALWAYS);
} }
@Bean @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.ConfigurationPropertiesBeanDescriptor;
import org.springframework.boot.actuate.context.properties.ConfigurationPropertiesReportEndpoint.ContextConfigurationProperties; import org.springframework.boot.actuate.context.properties.ConfigurationPropertiesReportEndpoint.ContextConfigurationProperties;
import org.springframework.boot.actuate.endpoint.SanitizingFunction; import org.springframework.boot.actuate.endpoint.SanitizingFunction;
import org.springframework.boot.actuate.endpoint.Show;
import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.ConstructorBinding; import org.springframework.boot.context.properties.ConstructorBinding;
import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties;
@@ -45,7 +46,6 @@ import org.springframework.boot.test.context.runner.ContextConsumer;
import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.mock.env.MockPropertySource; import org.springframework.mock.env.MockPropertySource;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
@@ -175,82 +175,10 @@ class ConfigurationPropertiesReportEndpointTests {
(properties) -> assertThat(properties.get("mixedBoolean")).isEqualTo(true))); (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 @Test
void sanitizeLists() { void sanitizeLists() {
this.contextRunner.withUserConfiguration(SensiblePropertiesConfiguration.class) new ApplicationContextRunner()
.withUserConfiguration(EndpointConfigWithShowNever.class, SensiblePropertiesConfiguration.class)
.withPropertyValues("sensible.listItems[0].some-password=password") .withPropertyValues("sensible.listItems[0].some-password=password")
.run(assertProperties("sensible", (properties) -> { .run(assertProperties("sensible", (properties) -> {
assertThat(properties.get("listItems")).isInstanceOf(List.class); assertThat(properties.get("listItems")).isInstanceOf(List.class);
@@ -271,7 +199,8 @@ class ConfigurationPropertiesReportEndpointTests {
@Test @Test
void listsOfListsAreSanitized() { void listsOfListsAreSanitized() {
this.contextRunner.withUserConfiguration(SensiblePropertiesConfiguration.class) new ApplicationContextRunner()
.withUserConfiguration(EndpointConfigWithShowNever.class, SensiblePropertiesConfiguration.class)
.withPropertyValues("sensible.listOfListItems[0][0].some-password=password") .withPropertyValues("sensible.listOfListItems[0][0].some-password=password")
.run(assertProperties("sensible", (properties) -> { .run(assertProperties("sensible", (properties) -> {
assertThat(properties.get("listOfListItems")).isInstanceOf(List.class); assertThat(properties.get("listOfListItems")).isInstanceOf(List.class);
@@ -311,7 +240,7 @@ class ConfigurationPropertiesReportEndpointTests {
.withUserConfiguration(CustomSanitizingEndpointConfig.class, .withUserConfiguration(CustomSanitizingEndpointConfig.class,
PropertySourceBasedSanitizingFunctionConfiguration.class, TestPropertiesConfiguration.class) PropertySourceBasedSanitizingFunctionConfiguration.class, TestPropertiesConfiguration.class)
.withPropertyValues("test.my-test-property=abcde").run(assertProperties("test", (properties) -> { .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("$$$"); 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 @Test
void originParents() { void originParents() {
this.contextRunner.withUserConfiguration(SensiblePropertiesConfiguration.class) this.contextRunner.withUserConfiguration(SensiblePropertiesConfiguration.class)
@@ -367,8 +316,9 @@ class ConfigurationPropertiesReportEndpointTests {
return (context) -> { return (context) -> {
ConfigurationPropertiesReportEndpoint endpoint = context ConfigurationPropertiesReportEndpoint endpoint = context
.getBean(ConfigurationPropertiesReportEndpoint.class); .getBean(ConfigurationPropertiesReportEndpoint.class);
ContextConfigurationProperties allProperties = endpoint.configurationProperties().getContexts() ConfigurationPropertiesReportEndpoint.ApplicationConfigurationProperties configurationProperties = endpoint
.get(context.getId()); .configurationProperties();
ContextConfigurationProperties allProperties = configurationProperties.getContexts().get(context.getId());
Optional<String> key = allProperties.getBeans().keySet().stream() Optional<String> key = allProperties.getBeans().keySet().stream()
.filter((id) -> findIdFromPrefix(prefix, id)).findAny(); .filter((id) -> findIdFromPrefix(prefix, id)).findAny();
assertThat(key).describedAs("No configuration properties with prefix '%s' found", prefix).isPresent(); assertThat(key).describedAs("No configuration properties with prefix '%s' found", prefix).isPresent();
@@ -421,12 +371,33 @@ class ConfigurationPropertiesReportEndpointTests {
static class EndpointConfig { static class EndpointConfig {
@Bean @Bean
ConfigurationPropertiesReportEndpoint endpoint(Environment environment) { ConfigurationPropertiesReportEndpoint endpoint() {
ConfigurationPropertiesReportEndpoint endpoint = new ConfigurationPropertiesReportEndpoint(); ConfigurationPropertiesReportEndpoint endpoint = new ConfigurationPropertiesReportEndpoint(
String[] keys = environment.getProperty("test.keys-to-sanitize", String[].class); Collections.emptyList(), Show.WHEN_AUTHORIZED);
if (keys != null) { return endpoint;
endpoint.setKeysToSanitize(keys); }
}
}
@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; return endpoint;
} }
@@ -891,13 +862,9 @@ class ConfigurationPropertiesReportEndpointTests {
static class CustomSanitizingEndpointConfig { static class CustomSanitizingEndpointConfig {
@Bean @Bean
ConfigurationPropertiesReportEndpoint endpoint(Environment environment, SanitizingFunction sanitizingFunction) { ConfigurationPropertiesReportEndpoint endpoint(SanitizingFunction sanitizingFunction) {
ConfigurationPropertiesReportEndpoint endpoint = new ConfigurationPropertiesReportEndpoint( ConfigurationPropertiesReportEndpoint endpoint = new ConfigurationPropertiesReportEndpoint(
Collections.singletonList(sanitizingFunction)); Collections.singletonList(sanitizingFunction), Show.ALWAYS);
String[] keys = environment.getProperty("test.keys-to-sanitize", String[].class);
if (keys != null) {
endpoint.setKeysToSanitize(keys);
}
return endpoint; 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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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; package org.springframework.boot.actuate.context.properties;
import java.util.Collections;
import org.junit.jupiter.api.BeforeEach; 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.actuate.endpoint.web.test.WebEndpointTest;
import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties;
@@ -77,13 +80,13 @@ class ConfigurationPropertiesReportEndpointWebIntegrationTests {
@Bean @Bean
ConfigurationPropertiesReportEndpoint endpoint() { ConfigurationPropertiesReportEndpoint endpoint() {
return new ConfigurationPropertiesReportEndpoint(); return new ConfigurationPropertiesReportEndpoint(Collections.emptyList(), null);
} }
@Bean @Bean
ConfigurationPropertiesReportEndpointWebExtension endpointWebExtension( ConfigurationPropertiesReportEndpointWebExtension endpointWebExtension(
ConfigurationPropertiesReportEndpoint endpoint) { ConfigurationPropertiesReportEndpoint endpoint) {
return new ConfigurationPropertiesReportEndpointWebExtension(endpoint); return new ConfigurationPropertiesReportEndpointWebExtension(endpoint, Show.ALWAYS, Collections.emptySet());
} }
@Bean @Bean

View File

@@ -19,11 +19,8 @@ package org.springframework.boot.actuate.endpoint;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.stream.Stream;
import org.junit.jupiter.api.Test; 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; import static org.assertj.core.api.Assertions.assertThat;
@@ -39,42 +36,21 @@ import static org.assertj.core.api.Assertions.assertThat;
class SanitizerTests { class SanitizerTests {
@Test @Test
void defaultNonUriKeys() { void whenNoSanitizationFunctionAndShowUnsanitizedIsFalse() {
Sanitizer sanitizer = new Sanitizer(); Sanitizer sanitizer = new Sanitizer();
assertThat(sanitizer.sanitize("password", "secret")).isEqualTo("******"); assertThat(sanitizer.sanitize(new SanitizableData(null, "password", "secret"), false)).isEqualTo("******");
assertThat(sanitizer.sanitize("my-password", "secret")).isEqualTo("******"); assertThat(sanitizer.sanitize(new SanitizableData(null, "other", "something"), false)).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("******");
} }
@Test @Test
void whenAdditionalKeysAreAddedValuesOfBothThemAndTheDefaultKeysAreSanitized() { void whenNoSanitizationFunctionAndShowUnsanitizedIsTrue() {
Sanitizer sanitizer = new Sanitizer(); Sanitizer sanitizer = new Sanitizer();
sanitizer.keysToSanitize("find", "confidential"); assertThat(sanitizer.sanitize(new SanitizableData(null, "password", "secret"), true)).isEqualTo("secret");
assertThat(sanitizer.sanitize("password", "secret")).isEqualTo("******"); assertThat(sanitizer.sanitize(new SanitizableData(null, "other", "something"), true)).isEqualTo("something");
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");
} }
@Test @Test
void whenCustomSanitizingFunctionPresentValueShouldBeSanitized() { void whenCustomSanitizationFunctionAndShowUnsanitizedIsFalse() {
Sanitizer sanitizer = new Sanitizer(Collections.singletonList((data) -> { Sanitizer sanitizer = new Sanitizer(Collections.singletonList((data) -> {
if (data.getKey().equals("custom")) { if (data.getKey().equals("custom")) {
return data.withValue("$$$$$$"); return data.withValue("$$$$$$");
@@ -82,11 +58,27 @@ class SanitizerTests {
return data; return data;
})); }));
SanitizableData secret = new SanitizableData(null, "secret", "xyz"); 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"); 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"); 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 @Test
@@ -98,7 +90,7 @@ class SanitizerTests {
return data; return data;
})); }));
SanitizableData password = new SanitizableData(null, "password", "123456"); SanitizableData password = new SanitizableData(null, "password", "123456");
assertThat(sanitizer.sanitize(password)).isEqualTo("------"); assertThat(sanitizer.sanitize(password, true)).isEqualTo("------");
} }
@Test @Test
@@ -119,101 +111,7 @@ class SanitizerTests {
}); });
Sanitizer sanitizer = new Sanitizer(sanitizingFunctions); Sanitizer sanitizer = new Sanitizer(sanitizingFunctions);
SanitizableData custom = new SanitizableData(null, sameKey, "123456"); SanitizableData custom = new SanitizableData(null, sameKey, "123456");
assertThat(sanitizer.sanitize(custom)).isEqualTo("------"); assertThat(sanitizer.sanitize(custom, true)).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");
} }
} }

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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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.AfterEach;
import org.junit.jupiter.api.Test; 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.EnvironmentDescriptor;
import org.springframework.boot.actuate.env.EnvironmentEndpoint.EnvironmentEntryDescriptor; import org.springframework.boot.actuate.env.EnvironmentEndpoint.EnvironmentEntryDescriptor;
import org.springframework.boot.actuate.env.EnvironmentEndpoint.PropertySourceDescriptor; import org.springframework.boot.actuate.env.EnvironmentEndpoint.PropertySourceDescriptor;
@@ -73,7 +74,8 @@ class EnvironmentEndpointTests {
ConfigurableEnvironment environment = emptyEnvironment(); ConfigurableEnvironment environment = emptyEnvironment();
environment.getPropertySources().addLast(singleKeyPropertySource("one", "my.key", "first")); environment.getPropertySources().addLast(singleKeyPropertySource("one", "my.key", "first"));
environment.getPropertySources().addLast(singleKeyPropertySource("two", "my.key", "second")); 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(); assertThat(descriptor.getActiveProfiles()).isEmpty();
Map<String, PropertySourceDescriptor> sources = propertySources(descriptor); Map<String, PropertySourceDescriptor> sources = propertySources(descriptor);
assertThat(sources.keySet()).containsExactly("one", "two"); assertThat(sources.keySet()).containsExactly("one", "two");
@@ -81,6 +83,38 @@ class EnvironmentEndpointTests {
assertThat(sources.get("two").getProperties()).containsOnlyKeys("my.key"); 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 @Test
void compositeSourceIsHandledCorrectly() { void compositeSourceIsHandledCorrectly() {
ConfigurableEnvironment environment = emptyEnvironment(); ConfigurableEnvironment environment = emptyEnvironment();
@@ -88,82 +122,14 @@ class EnvironmentEndpointTests {
source.addPropertySource(new MapPropertySource("one", Collections.singletonMap("foo", "bar"))); source.addPropertySource(new MapPropertySource("one", Collections.singletonMap("foo", "bar")));
source.addPropertySource(new MapPropertySource("two", Collections.singletonMap("foo", "spam"))); source.addPropertySource(new MapPropertySource("two", Collections.singletonMap("foo", "spam")));
environment.getPropertySources().addFirst(source); 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); Map<String, PropertySourceDescriptor> sources = propertySources(descriptor);
assertThat(sources.keySet()).containsExactly("composite:one", "composite:two"); assertThat(sources.keySet()).containsExactly("composite:one", "composite:two");
assertThat(sources.get("composite:one").getProperties().get("foo").getValue()).isEqualTo("bar"); assertThat(sources.get("composite:one").getProperties().get("foo").getValue()).isEqualTo("bar");
assertThat(sources.get("composite:two").getProperties().get("foo").getValue()).isEqualTo("spam"); 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 @Test
void keysMatchingCustomSanitizingFunctionHaveTheirValuesSanitized() { void keysMatchingCustomSanitizingFunctionHaveTheirValuesSanitized() {
ConfigurableEnvironment environment = new StandardEnvironment(); ConfigurableEnvironment environment = new StandardEnvironment();
@@ -176,7 +142,7 @@ class EnvironmentEndpointTests {
return data.withValue("******"); return data.withValue("******");
} }
return data; return data;
})).environment(null); }), Show.ALWAYS).environment(null);
assertThat(propertySources(descriptor).get("test").getProperties().get("other.service").getValue()) assertThat(propertySources(descriptor).get("test").getProperties().get("other.service").getValue())
.isEqualTo("abcde"); .isEqualTo("abcde");
Map<String, PropertyValueDescriptor> systemProperties = propertySources(descriptor).get("systemProperties") Map<String, PropertyValueDescriptor> systemProperties = propertySources(descriptor).get("systemProperties")
@@ -190,7 +156,8 @@ class EnvironmentEndpointTests {
void propertyWithPlaceholderResolved() { void propertyWithPlaceholderResolved() {
ConfigurableEnvironment environment = emptyEnvironment(); ConfigurableEnvironment environment = emptyEnvironment();
TestPropertyValues.of("my.foo: ${bar.blah}", "bar.blah: hello").applyTo(environment); 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"); assertThat(propertySources(descriptor).get("test").getProperties().get("my.foo").getValue()).isEqualTo("hello");
} }
@@ -198,46 +165,19 @@ class EnvironmentEndpointTests {
void propertyWithPlaceholderNotResolved() { void propertyWithPlaceholderNotResolved() {
ConfigurableEnvironment environment = emptyEnvironment(); ConfigurableEnvironment environment = emptyEnvironment();
TestPropertyValues.of("my.foo: ${bar.blah}").applyTo(environment); 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()) assertThat(propertySources(descriptor).get("test").getProperties().get("my.foo").getValue())
.isEqualTo("${bar.blah}"); .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 @Test
void propertyWithComplexTypeShouldNotFail() { void propertyWithComplexTypeShouldNotFail() {
ConfigurableEnvironment environment = emptyEnvironment(); ConfigurableEnvironment environment = emptyEnvironment();
environment.getPropertySources() environment.getPropertySources()
.addFirst(singleKeyPropertySource("test", "foo", Collections.singletonMap("bar", "baz"))); .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(); String value = (String) propertySources(descriptor).get("test").getProperties().get("foo").getValue();
assertThat(value).isEqualTo("Complex property type java.util.Collections$SingletonMap"); assertThat(value).isEqualTo("Complex property type java.util.Collections$SingletonMap");
} }
@@ -251,7 +191,8 @@ class EnvironmentEndpointTests {
map.put("boolean", true); map.put("boolean", true);
map.put("biginteger", BigInteger.valueOf(200)); map.put("biginteger", BigInteger.valueOf(200));
environment.getPropertySources().addFirst(new MapPropertySource("test", map)); 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(); Map<String, PropertyValueDescriptor> properties = propertySources(descriptor).get("test").getProperties();
assertThat(properties.get("char").getValue()).isEqualTo('a'); assertThat(properties.get("char").getValue()).isEqualTo('a');
assertThat(properties.get("integer").getValue()).isEqualTo(100); assertThat(properties.get("integer").getValue()).isEqualTo(100);
@@ -263,26 +204,42 @@ class EnvironmentEndpointTests {
void propertyWithCharSequenceTypeIsConvertedToString() { void propertyWithCharSequenceTypeIsConvertedToString() {
ConfigurableEnvironment environment = emptyEnvironment(); ConfigurableEnvironment environment = emptyEnvironment();
environment.getPropertySources().addFirst(singleKeyPropertySource("test", "foo", new CharSequenceProperty())); 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(); String value = (String) propertySources(descriptor).get("test").getProperties().get("foo").getValue();
assertThat(value).isEqualTo("test value"); assertThat(value).isEqualTo("test value");
} }
@Test @Test
void propertyEntry() { 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(() -> { TestPropertyValues.of("my.foo=another").applyToSystemProperties(() -> {
StandardEnvironment environment = new StandardEnvironment(); StandardEnvironment environment = new StandardEnvironment();
TestPropertyValues.of("my.foo=bar", "my.foo2=bar2").applyTo(environment, TestPropertyValues.Type.MAP, TestPropertyValues.of("my.foo=bar", "my.foo2=bar2").applyTo(environment, TestPropertyValues.Type.MAP,
"test"); "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).isNotNull();
assertThat(descriptor.getProperty()).isNotNull(); assertThat(descriptor.getProperty()).isNotNull();
assertThat(descriptor.getProperty().getSource()).isEqualTo("test"); assertThat(descriptor.getProperty().getSource()).isEqualTo("test");
assertThat(descriptor.getProperty().getValue()).isEqualTo("bar"); assertThat(descriptor.getProperty().getValue()).isEqualTo(bar);
Map<String, PropertySourceEntryDescriptor> sources = propertySources(descriptor); Map<String, PropertySourceEntryDescriptor> sources = propertySources(descriptor);
assertThat(sources.keySet()).containsExactly("test", "systemProperties", "systemEnvironment"); assertThat(sources.keySet()).containsExactly("test", "systemProperties", "systemEnvironment");
assertPropertySourceEntryDescriptor(sources.get("test"), "bar", null); assertPropertySourceEntryDescriptor(sources.get("test"), bar, null);
assertPropertySourceEntryDescriptor(sources.get("systemProperties"), "another", null); assertPropertySourceEntryDescriptor(sources.get("systemProperties"), another, null);
assertPropertySourceEntryDescriptor(sources.get("systemEnvironment"), null, null); assertPropertySourceEntryDescriptor(sources.get("systemEnvironment"), null, null);
return null; return null;
}); });
@@ -294,7 +251,8 @@ class EnvironmentEndpointTests {
OriginParentMockPropertySource propertySource = new OriginParentMockPropertySource(); OriginParentMockPropertySource propertySource = new OriginParentMockPropertySource();
propertySource.setProperty("name", "test"); propertySource.setProperty("name", "test");
environment.getPropertySources().addFirst(propertySource); 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"); PropertySourceEntryDescriptor entryDescriptor = propertySources(descriptor).get("mockProperties");
assertThat(entryDescriptor.getProperty().getOrigin()).isEqualTo("name"); assertThat(entryDescriptor.getProperty().getOrigin()).isEqualTo("name");
assertThat(entryDescriptor.getProperty().getOriginParents()).containsExactly("spring", "boot"); assertThat(entryDescriptor.getProperty().getOriginParents()).containsExactly("spring", "boot");
@@ -304,7 +262,8 @@ class EnvironmentEndpointTests {
void propertyEntryNotFound() { void propertyEntryNotFound() {
ConfigurableEnvironment environment = emptyEnvironment(); ConfigurableEnvironment environment = emptyEnvironment();
environment.getPropertySources().addFirst(singleKeyPropertySource("test", "foo", "bar")); 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).isNotNull();
assertThat(descriptor.getProperty()).isNull(); assertThat(descriptor.getProperty()).isNull();
Map<String, PropertySourceEntryDescriptor> sources = propertySources(descriptor); Map<String, PropertySourceEntryDescriptor> sources = propertySources(descriptor);
@@ -317,33 +276,14 @@ class EnvironmentEndpointTests {
ConfigurableEnvironment environment = emptyEnvironment(); ConfigurableEnvironment environment = emptyEnvironment();
environment.getPropertySources().addFirst(singleKeyPropertySource("one", "a", "alpha")); environment.getPropertySources().addFirst(singleKeyPropertySource("one", "a", "alpha"));
environment.getPropertySources().addFirst(singleKeyPropertySource("two", "a", "apple")); 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); Map<String, PropertySourceDescriptor> sources = propertySources(descriptor);
assertThat(sources.keySet()).containsExactly("two", "one"); assertThat(sources.keySet()).containsExactly("two", "one");
assertThat(sources.get("one").getProperties().get("a").getValue()).isEqualTo("alpha"); assertThat(sources.get("one").getProperties().get("a").getValue()).isEqualTo("alpha");
assertThat(sources.get("two").getProperties().get("a").getValue()).isEqualTo("apple"); 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() { private static ConfigurableEnvironment emptyEnvironment() {
StandardEnvironment environment = new StandardEnvironment(); StandardEnvironment environment = new StandardEnvironment();
environment.getPropertySources().remove(StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME); environment.getPropertySources().remove(StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME);
@@ -418,7 +358,7 @@ class EnvironmentEndpointTests {
@Bean @Bean
EnvironmentEndpoint environmentEndpoint(Environment environment) { 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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -16,11 +16,13 @@
package org.springframework.boot.actuate.env; package org.springframework.boot.actuate.env;
import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import org.junit.jupiter.api.BeforeEach; 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.actuate.endpoint.web.test.WebEndpointTest;
import org.springframework.boot.test.util.TestPropertyValues; import org.springframework.boot.test.util.TestPropertyValues;
import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.ConfigurableApplicationContext;
@@ -76,17 +78,6 @@ class EnvironmentEndpointWebIntegrationTests {
.isEqualTo("${my.bar}"); .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 @WebEndpointTest
void nestedPathForUnknownKeyShouldReturn404() { void nestedPathForUnknownKeyShouldReturn404() {
this.client.get().uri("/actuator/env/this.does.not.exist").exchange().expectStatus().isNotFound(); this.client.get().uri("/actuator/env/this.does.not.exist").exchange().expectStatus().isNotFound();
@@ -103,16 +94,6 @@ class EnvironmentEndpointWebIntegrationTests {
.isEqualTo("${my.bar}"); .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) { private String forProperty(String source, String name) {
return "propertySources[?(@.name=='" + source + "')].properties.['" + name + "'].value"; return "propertySources[?(@.name=='" + source + "')].properties.['" + name + "'].value";
} }
@@ -126,12 +107,12 @@ class EnvironmentEndpointWebIntegrationTests {
@Bean @Bean
EnvironmentEndpoint endpoint(Environment environment) { EnvironmentEndpoint endpoint(Environment environment) {
return new EnvironmentEndpoint(environment); return new EnvironmentEndpoint(environment, Collections.emptyList(), Show.ALWAYS);
} }
@Bean @Bean
EnvironmentEndpointWebExtension environmentEndpointWebExtension(EnvironmentEndpoint endpoint) { 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.impl.matchers.GroupMatcher;
import org.quartz.spi.OperableTrigger; 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.QuartzJobDetails;
import org.springframework.boot.actuate.quartz.QuartzEndpoint.QuartzJobGroupSummary; import org.springframework.boot.actuate.quartz.QuartzEndpoint.QuartzJobGroupSummary;
import org.springframework.boot.actuate.quartz.QuartzEndpoint.QuartzJobSummary; import org.springframework.boot.actuate.quartz.QuartzEndpoint.QuartzJobSummary;
@@ -107,7 +106,7 @@ class QuartzEndpointTests {
QuartzEndpointTests() { QuartzEndpointTests() {
this.scheduler = mock(Scheduler.class); this.scheduler = mock(Scheduler.class);
this.endpoint = new QuartzEndpoint(this.scheduler); this.endpoint = new QuartzEndpoint(this.scheduler, Collections.emptyList());
} }
@Test @Test
@@ -417,7 +416,7 @@ class QuartzEndpointTests {
mockTriggers(trigger); mockTriggers(trigger);
given(this.scheduler.getTriggerState(TriggerKey.triggerKey("3am-every-day", "samples"))) given(this.scheduler.getTriggerState(TriggerKey.triggerKey("3am-every-day", "samples")))
.willReturn(TriggerState.NORMAL); .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"), assertThat(triggerDetails).contains(entry("group", "samples"), entry("name", "3am-every-day"),
entry("description", "Sample description"), entry("type", "cron"), entry("state", TriggerState.NORMAL), entry("description", "Sample description"), entry("type", "cron"), entry("state", TriggerState.NORMAL),
entry("priority", 3)); entry("priority", 3));
@@ -443,7 +442,7 @@ class QuartzEndpointTests {
mockTriggers(trigger); mockTriggers(trigger);
given(this.scheduler.getTriggerState(TriggerKey.triggerKey("every-hour", "samples"))) given(this.scheduler.getTriggerState(TriggerKey.triggerKey("every-hour", "samples")))
.willReturn(TriggerState.COMPLETE); .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"), assertThat(triggerDetails).contains(entry("group", "samples"), entry("name", "every-hour"),
entry("description", "Every hour"), entry("type", "simple"), entry("state", TriggerState.COMPLETE), entry("description", "Every hour"), entry("type", "simple"), entry("state", TriggerState.COMPLETE),
entry("priority", 20)); entry("priority", 20));
@@ -470,7 +469,7 @@ class QuartzEndpointTests {
mockTriggers(trigger); mockTriggers(trigger);
given(this.scheduler.getTriggerState(TriggerKey.triggerKey("every-hour-mon-wed", "samples"))) given(this.scheduler.getTriggerState(TriggerKey.triggerKey("every-hour-mon-wed", "samples")))
.willReturn(TriggerState.NORMAL); .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"), assertThat(triggerDetails).contains(entry("group", "samples"), entry("name", "every-hour-mon-wed"),
entry("description", "Every working hour Mon Wed"), entry("type", "dailyTimeInterval"), entry("description", "Every working hour Mon Wed"), entry("type", "dailyTimeInterval"),
entry("state", TriggerState.NORMAL), entry("priority", 4)); entry("state", TriggerState.NORMAL), entry("priority", 4));
@@ -499,7 +498,7 @@ class QuartzEndpointTests {
mockTriggers(trigger); mockTriggers(trigger);
given(this.scheduler.getTriggerState(TriggerKey.triggerKey("once-a-week", "samples"))) given(this.scheduler.getTriggerState(TriggerKey.triggerKey("once-a-week", "samples")))
.willReturn(TriggerState.BLOCKED); .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"), assertThat(triggerDetails).contains(entry("group", "samples"), entry("name", "once-a-week"),
entry("description", "Once a week"), entry("type", "calendarInterval"), entry("description", "Once a week"), entry("type", "calendarInterval"),
entry("state", TriggerState.BLOCKED), entry("priority", 8)); entry("state", TriggerState.BLOCKED), entry("priority", 8));
@@ -524,7 +523,7 @@ class QuartzEndpointTests {
mockTriggers(trigger); mockTriggers(trigger);
given(this.scheduler.getTriggerState(TriggerKey.triggerKey("custom", "samples"))) given(this.scheduler.getTriggerState(TriggerKey.triggerKey("custom", "samples")))
.willReturn(TriggerState.ERROR); .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"), assertThat(triggerDetails).contains(entry("group", "samples"), entry("name", "custom"), entry("type", "custom"),
entry("state", TriggerState.ERROR), entry("priority", 9)); entry("state", TriggerState.ERROR), entry("priority", 9));
assertThat(triggerDetails).contains(entry("previousFireTime", previousFireTime), assertThat(triggerDetails).contains(entry("previousFireTime", previousFireTime),
@@ -542,9 +541,22 @@ class QuartzEndpointTests {
mockTriggers(trigger); mockTriggers(trigger);
given(this.scheduler.getTriggerState(TriggerKey.triggerKey("3am-every-day", "samples"))) given(this.scheduler.getTriggerState(TriggerKey.triggerKey("3am-every-day", "samples")))
.willReturn(TriggerState.NORMAL); .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"), 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}") @ParameterizedTest(name = "unit {1}")
@@ -554,7 +566,7 @@ class QuartzEndpointTests {
.withSchedule(CalendarIntervalScheduleBuilder.calendarIntervalSchedule().withInterval(amount, unit)) .withSchedule(CalendarIntervalScheduleBuilder.calendarIntervalSchedule().withInterval(amount, unit))
.build(); .build();
mockTriggers(trigger); 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()) assertThat(triggerDetails).extractingByKey("calendarInterval", nestedMap())
.contains(entry("interval", expectedDuration.toMillis())); .contains(entry("interval", expectedDuration.toMillis()));
} }
@@ -575,7 +587,7 @@ class QuartzEndpointTests {
JobDetail job = JobBuilder.newJob(Job.class).withIdentity("hello", "samples").withDescription("A sample job") JobDetail job = JobBuilder.newJob(Job.class).withIdentity("hello", "samples").withDescription("A sample job")
.storeDurably().requestRecovery(false).build(); .storeDurably().requestRecovery(false).build();
mockJobs(job); mockJobs(job);
QuartzJobDetails jobDetails = this.endpoint.quartzJob("samples", "hello"); QuartzJobDetails jobDetails = this.endpoint.quartzJob("samples", "hello", true);
assertThat(jobDetails.getGroup()).isEqualTo("samples"); assertThat(jobDetails.getGroup()).isEqualTo("samples");
assertThat(jobDetails.getName()).isEqualTo("hello"); assertThat(jobDetails.getName()).isEqualTo("hello");
assertThat(jobDetails.getDescription()).isEqualTo("A sample job"); assertThat(jobDetails.getDescription()).isEqualTo("A sample job");
@@ -600,7 +612,7 @@ class QuartzEndpointTests {
mockTriggers(trigger); mockTriggers(trigger);
given(this.scheduler.getTriggersOfJob(JobKey.jobKey("hello", "samples"))) given(this.scheduler.getTriggersOfJob(JobKey.jobKey("hello", "samples")))
.willAnswer((invocation) -> Collections.singletonList(trigger)); .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); assertThat(jobDetails.getTriggers()).hasSize(1);
Map<String, Object> triggerDetails = jobDetails.getTriggers().get(0); Map<String, Object> triggerDetails = jobDetails.getTriggers().get(0);
assertThat(triggerDetails).containsOnly(entry("group", "samples"), entry("name", "3am-every-day"), assertThat(triggerDetails).containsOnly(entry("group", "samples"), entry("name", "3am-every-day"),
@@ -622,7 +634,7 @@ class QuartzEndpointTests {
mockTriggers(triggerOne, triggerTwo); mockTriggers(triggerOne, triggerTwo);
given(this.scheduler.getTriggersOfJob(JobKey.jobKey("hello", "samples"))) given(this.scheduler.getTriggersOfJob(JobKey.jobKey("hello", "samples")))
.willAnswer((invocation) -> Arrays.asList(triggerOne, triggerTwo)); .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()).hasSize(2);
assertThat(jobDetails.getTriggers().get(0)).containsEntry("name", "two"); assertThat(jobDetails.getTriggers().get(0)).containsEntry("name", "two");
assertThat(jobDetails.getTriggers().get(1)).containsEntry("name", "one"); assertThat(jobDetails.getTriggers().get(1)).containsEntry("name", "one");
@@ -642,35 +654,30 @@ class QuartzEndpointTests {
mockTriggers(triggerOne, triggerTwo); mockTriggers(triggerOne, triggerTwo);
given(this.scheduler.getTriggersOfJob(JobKey.jobKey("hello", "samples"))) given(this.scheduler.getTriggersOfJob(JobKey.jobKey("hello", "samples")))
.willAnswer((invocation) -> Arrays.asList(triggerOne, triggerTwo)); .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()).hasSize(2);
assertThat(jobDetails.getTriggers().get(0)).containsEntry("name", "two"); assertThat(jobDetails.getTriggers().get(0)).containsEntry("name", "two");
assertThat(jobDetails.getTriggers().get(1)).containsEntry("name", "one"); assertThat(jobDetails.getTriggers().get(1)).containsEntry("name", "one");
} }
@Test @Test
void quartzJobWithSensitiveDataMap() throws SchedulerException { void quartzJobWithDataMap() throws SchedulerException {
JobDetail job = JobBuilder.newJob(Job.class).withIdentity("hello", "samples").usingJobData("user", "user") JobDetail job = JobBuilder.newJob(Job.class).withIdentity("hello", "samples").usingJobData("user", "user")
.usingJobData("password", "secret").usingJobData("url", "https://user:secret@example.com").build(); .usingJobData("password", "secret").usingJobData("url", "https://user:secret@example.com").build();
mockJobs(job); mockJobs(job);
QuartzJobDetails jobDetails = this.endpoint.quartzJob("samples", "hello"); QuartzJobDetails jobDetails = this.endpoint.quartzJob("samples", "hello", true);
assertThat(jobDetails.getData()).containsOnly(entry("user", "user"), entry("password", "******"), assertThat(jobDetails.getData()).containsOnly(entry("user", "user"), entry("password", "secret"),
entry("url", "https://user:******@example.com")); entry("url", "https://user:secret@example.com"));
} }
@Test @Test
void quartzJobWithSensitiveDataMapAndCustomSanitizer() throws SchedulerException { void quartzJobWithDataMapAndShowUnsanitizedFalse() throws SchedulerException {
JobDetail job = JobBuilder.newJob(Job.class).withIdentity("hello", "samples").usingJobData("test", "value") JobDetail job = JobBuilder.newJob(Job.class).withIdentity("hello", "samples").usingJobData("user", "user")
.usingJobData("secret", "value").build(); .usingJobData("password", "secret").usingJobData("url", "https://user:secret@example.com").build();
mockJobs(job); mockJobs(job);
Sanitizer sanitizer = mock(Sanitizer.class); QuartzJobDetails jobDetails = this.endpoint.quartzJob("samples", "hello", false);
given(sanitizer.sanitize("test", "value")).willReturn("value"); assertThat(jobDetails.getData()).containsOnly(entry("user", "******"), entry("password", "******"),
given(sanitizer.sanitize("secret", "value")).willReturn("----"); entry("url", "******"));
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();
} }
private void mockJobs(JobDetail... jobs) throws SchedulerException { private void mockJobs(JobDetail... jobs) throws SchedulerException {

View File

@@ -16,13 +16,18 @@
package org.springframework.boot.actuate.quartz; package org.springframework.boot.actuate.quartz;
import java.security.Principal;
import java.util.Collections;
import java.util.Set; import java.util.Set;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.aot.hint.MemberCategory; import org.springframework.aot.hint.MemberCategory;
import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.predicate.RuntimeHintsPredicates; 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.QuartzGroups;
import org.springframework.boot.actuate.quartz.QuartzEndpoint.QuartzJobDetails; import org.springframework.boot.actuate.quartz.QuartzEndpoint.QuartzJobDetails;
import org.springframework.boot.actuate.quartz.QuartzEndpoint.QuartzJobGroupSummary; 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 org.springframework.boot.actuate.quartz.QuartzEndpointWebExtension.QuartzEndpointWebExtensionRuntimeHints;
import static org.assertj.core.api.Assertions.assertThat; 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}. * Tests for {@link QuartzEndpointWebExtension}.
* *
* @author Moritz Halbritter * @author Moritz Halbritter
* @author Madhura Bhave
*/ */
class QuartzEndpointWebExtensionTests { 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 @Test
void shouldRegisterHints() { void shouldRegisterHints() {
RuntimeHints runtimeHints = new RuntimeHints(); 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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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.TriggerKey;
import org.quartz.impl.matchers.GroupMatcher; 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.boot.actuate.endpoint.web.test.WebEndpointTest;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
@@ -175,12 +176,12 @@ class QuartzEndpointWebIntegrationTests {
@Bean @Bean
QuartzEndpoint endpoint(Scheduler scheduler) { QuartzEndpoint endpoint(Scheduler scheduler) {
return new QuartzEndpoint(scheduler); return new QuartzEndpoint(scheduler, Collections.emptyList());
} }
@Bean @Bean
QuartzEndpointWebExtension quartzEndpointWebExtension(QuartzEndpoint endpoint) { 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 { 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]] [[howto.actuator.sanitize-sensitive-values]]
=== 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 `+******+`). Information returned by the `/env`, `/configprops` and `/quartz` endpoints can be somewhat sensitive.
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. All values are sanitized by default (that is replaced by `+******+`).
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. 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` 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.
- `addresses` By default, any authenticated user is authorized.
- `uri` For JMX endpoints, all users are always authorized.
- `uris`
- `url`
- `urls`
The sensitive portion of the URI is identified using the format `<scheme>://<username>:<password>@<host>:<port>/`. [source,yaml,indent=0,subs="verbatim",configprops,configblocks]
For example, for the property `myclient.uri=http://user1:password1@localhost:8081`, the resulting sanitized value is ----
`++http://user1:******@localhost:8081++`. 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 ==== Customizing Sanitization
Sanitization can be customized in two different ways. 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. 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. 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. This allows you to, for example, sanitize every value that comes from a particular property source.