Add actuator specific ObjectMapper

Prior to this commit, Actuator endpoints would use the application
ObjectMapper instance for serializing payloads as JSON. This was
problematic in several cases:

* application-specific configuration would change the actuator endpoint
output.
* choosing a different JSON mapper implementation in the application
would break completely some endpoints.

Spring Boot Actuator already has a hard dependency on Jackson, and this
commit uses that fact to configure a shared `ObjectMapper` instance that
will be used by the Actuator infrastructure consistently, without
polluting the application context.

This `ObjectMapper` is used in Actuator for:

* JMX endpoints
* Spring MVC endpoints with an HTTP message converter
* Spring WebFlux endpoints with an `Encoder`
* Jersey endpoints with a `ContextResolver<ObjectMapper>`

For all web endpoints, this configuration is limited to the
actuator-specific media types such as
`"application/vnd.spring-boot.actuator.v3+json"`.

Fixes gh-12951
This commit is contained in:
Brian Clozel 2020-02-11 17:52:42 +01:00
parent 420af17570
commit 97af0b2f3a
14 changed files with 373 additions and 117 deletions

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2019 the original author or authors.
* Copyright 2012-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -25,6 +25,7 @@ import org.springframework.boot.actuate.endpoint.annotation.EndpointConverter;
import org.springframework.boot.actuate.endpoint.invoke.ParameterValueMapper;
import org.springframework.boot.actuate.endpoint.invoke.convert.ConversionServiceParameterValueMapper;
import org.springframework.boot.actuate.endpoint.invoker.cache.CachingOperationInvokerAdvisor;
import org.springframework.boot.actuate.endpoint.json.ActuatorJsonMapperProvider;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.convert.ApplicationConversionService;
@ -75,4 +76,9 @@ public class EndpointAutoConfiguration {
return new CachingOperationInvokerAdvisor(new EndpointIdTimeToLivePropertyFunction(environment));
}
@Bean
public ActuatorJsonMapperProvider actuatorJsonMapperProvider() {
return new ActuatorJsonMapperProvider();
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2019 the original author or authors.
* Copyright 2012-2020 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.
@ -20,8 +20,6 @@ import java.util.stream.Collectors;
import javax.management.MBeanServer;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.actuate.autoconfigure.endpoint.ExposeExcludePropertyEndpointFilter;
import org.springframework.boot.actuate.endpoint.EndpointFilter;
@ -35,6 +33,7 @@ import org.springframework.boot.actuate.endpoint.jmx.JmxEndpointExporter;
import org.springframework.boot.actuate.endpoint.jmx.JmxEndpointsSupplier;
import org.springframework.boot.actuate.endpoint.jmx.JmxOperationResponseMapper;
import org.springframework.boot.actuate.endpoint.jmx.annotation.JmxEndpointDiscoverer;
import org.springframework.boot.actuate.endpoint.json.ActuatorJsonMapperProvider;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
@ -85,12 +84,12 @@ public class JmxEndpointAutoConfiguration {
@Bean
@ConditionalOnSingleCandidate(MBeanServer.class)
public JmxEndpointExporter jmxMBeanExporter(MBeanServer mBeanServer, Environment environment,
ObjectProvider<ObjectMapper> objectMapper, JmxEndpointsSupplier jmxEndpointsSupplier) {
ActuatorJsonMapperProvider actuatorJsonMapperProvider, JmxEndpointsSupplier jmxEndpointsSupplier) {
String contextId = ObjectUtils.getIdentityHexString(this.applicationContext);
EndpointObjectNameFactory objectNameFactory = new DefaultEndpointObjectNameFactory(this.properties, environment,
mBeanServer, contextId);
JmxOperationResponseMapper responseMapper = new JacksonJmxOperationResponseMapper(
objectMapper.getIfAvailable());
actuatorJsonMapperProvider.getInstance());
return new JmxEndpointExporter(mBeanServer, objectNameFactory, responseMapper,
jmxEndpointsSupplier.getEndpoints());

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2019 the original author or authors.
* Copyright 2012-2020 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.
@ -22,7 +22,10 @@ import java.util.HashSet;
import java.util.List;
import javax.annotation.PostConstruct;
import javax.ws.rs.Produces;
import javax.ws.rs.ext.ContextResolver;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.server.model.Resource;
@ -32,6 +35,8 @@ import org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfi
import org.springframework.boot.actuate.autoconfigure.web.server.ManagementPortType;
import org.springframework.boot.actuate.endpoint.ExposableEndpoint;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.http.ActuatorMediaType;
import org.springframework.boot.actuate.endpoint.json.ActuatorJsonMapperProvider;
import org.springframework.boot.actuate.endpoint.web.EndpointLinksResolver;
import org.springframework.boot.actuate.endpoint.web.EndpointMapping;
import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes;
@ -84,6 +89,12 @@ class JerseyWebEndpointManagementContextConfiguration {
|| ManagementPortType.get(environment).equals(ManagementPortType.DIFFERENT);
}
@Bean
ResourceConfigCustomizer actuatorResourceConfigCustomizer(ActuatorJsonMapperProvider jsonMapperProvider) {
return (ResourceConfig config) -> config.register(
new ActuatorJsonMapperContextResolver(jsonMapperProvider.getInstance()), ContextResolver.class);
}
/**
* Register endpoints with the {@link ResourceConfig}. The
* {@link ResourceConfigCustomizer} cannot be used because we don't want to apply
@ -145,4 +156,20 @@ class JerseyWebEndpointManagementContextConfiguration {
}
@Produces({ ActuatorMediaType.V3_JSON, ActuatorMediaType.V2_JSON })
private static final class ActuatorJsonMapperContextResolver implements ContextResolver<ObjectMapper> {
private final ObjectMapper objectMapper;
private ActuatorJsonMapperContextResolver(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
@Override
public ObjectMapper getContext(Class<?> type) {
return this.objectMapper;
}
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2019 the original author or authors.
* Copyright 2012-2020 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.
@ -26,6 +26,8 @@ import org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfi
import org.springframework.boot.actuate.autoconfigure.web.server.ManagementPortType;
import org.springframework.boot.actuate.endpoint.ExposableEndpoint;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.http.ActuatorMediaType;
import org.springframework.boot.actuate.endpoint.json.ActuatorJsonMapperProvider;
import org.springframework.boot.actuate.endpoint.web.EndpointLinksResolver;
import org.springframework.boot.actuate.endpoint.web.EndpointMapping;
import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes;
@ -40,8 +42,13 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.codec.CodecCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.core.annotation.Order;
import org.springframework.core.env.Environment;
import org.springframework.http.MediaType;
import org.springframework.http.codec.CodecConfigurer;
import org.springframework.http.codec.json.Jackson2JsonEncoder;
import org.springframework.http.server.reactive.HttpHandler;
import org.springframework.util.StringUtils;
import org.springframework.web.reactive.DispatcherHandler;
@ -52,6 +59,7 @@ import org.springframework.web.reactive.DispatcherHandler;
*
* @author Andy Wilkinson
* @author Phillip Webb
* @author Brian Clozel
* @since 2.0.0
*/
@ManagementContextConfiguration(proxyBeanMethods = false)
@ -93,4 +101,16 @@ public class WebFluxEndpointManagementContextConfiguration {
corsProperties.toCorsConfiguration());
}
@Bean
@Order(-1)
public CodecCustomizer actuatorJsonCodec(ActuatorJsonMapperProvider actuatorJsonMapperProvider) {
return (configurer) -> {
MediaType v3MediaType = MediaType.parseMediaType(ActuatorMediaType.V3_JSON);
MediaType v2MediaType = MediaType.parseMediaType(ActuatorMediaType.V2_JSON);
CodecConfigurer.CustomCodecs customCodecs = configurer.customCodecs();
customCodecs.register(
new Jackson2JsonEncoder(actuatorJsonMapperProvider.getInstance(), v3MediaType, v2MediaType));
};
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2019 the original author or authors.
* Copyright 2012-2020 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.
@ -20,12 +20,16 @@ import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.boot.actuate.autoconfigure.endpoint.web.CorsEndpointProperties;
import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties;
import org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfiguration;
import org.springframework.boot.actuate.autoconfigure.web.server.ManagementPortType;
import org.springframework.boot.actuate.endpoint.ExposableEndpoint;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.http.ActuatorMediaType;
import org.springframework.boot.actuate.endpoint.json.ActuatorJsonMapperProvider;
import org.springframework.boot.actuate.endpoint.web.EndpointLinksResolver;
import org.springframework.boot.actuate.endpoint.web.EndpointMapping;
import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes;
@ -42,9 +46,15 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplicat
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.env.Environment;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* {@link ManagementContextConfiguration @ManagementContextConfiguration} for Spring MVC
@ -91,4 +101,35 @@ public class WebMvcEndpointManagementContextConfiguration {
corsProperties.toCorsConfiguration());
}
@Configuration(proxyBeanMethods = false)
public static class JsonWebMvcConfigurer implements WebMvcConfigurer, Ordered {
private final ActuatorJsonMapperProvider actuatorJsonMapperProvider;
public JsonWebMvcConfigurer(ActuatorJsonMapperProvider objectMapperFactory) {
this.actuatorJsonMapperProvider = objectMapperFactory;
}
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(new ActuatorJsonHttpMessageConverter(this.actuatorJsonMapperProvider.getInstance()));
}
// WebMvcAutoConfiguration is ordered at 0
@Override
public int getOrder() {
return -1;
}
}
static class ActuatorJsonHttpMessageConverter extends AbstractJackson2HttpMessageConverter {
ActuatorJsonHttpMessageConverter(ObjectMapper objectMapper) {
super(objectMapper, MediaType.parseMediaType(ActuatorMediaType.V3_JSON),
MediaType.parseMediaType(ActuatorMediaType.V2_JSON));
}
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2019 the original author or authors.
* Copyright 2012-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -16,9 +16,13 @@
package org.springframework.boot.actuate.autoconfigure.web.servlet;
import java.util.List;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfiguration;
import org.springframework.boot.actuate.autoconfigure.web.ManagementContextType;
import org.springframework.boot.actuate.endpoint.json.ActuatorJsonMapperProvider;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
@ -33,11 +37,15 @@ import org.springframework.boot.web.servlet.error.ErrorAttributes;
import org.springframework.boot.web.servlet.filter.OrderedRequestContextFilter;
import org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.context.request.RequestContextListener;
import org.springframework.web.filter.RequestContextFilter;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* {@link ManagementContextConfiguration @ManagementContextConfiguration} for Spring MVC
@ -108,6 +116,29 @@ class WebMvcEndpointChildContextConfiguration {
return new OrderedRequestContextFilter();
}
/**
* Since {@code WebMvcEndpointManagementContextConfiguration} is adding an
* actuator-specific JSON message converter, {@code @EnableWebMvc} will not register
* default converters. We need to register a JSON converter for plain
* {@code "application/json"} still.
* WebMvcEndpointChildContextConfigurationIntegrationTests
*/
@Configuration(proxyBeanMethods = false)
public static class FallbackJsonConverterConfigurer implements WebMvcConfigurer {
private final ActuatorJsonMapperProvider actuatorJsonMapperProvider;
FallbackJsonConverterConfigurer(ObjectProvider<ActuatorJsonMapperProvider> objectMapperSupplier) {
this.actuatorJsonMapperProvider = objectMapperSupplier.getIfAvailable(ActuatorJsonMapperProvider::new);
}
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(new MappingJackson2HttpMessageConverter(this.actuatorJsonMapperProvider.getInstance()));
}
}
/**
* {@link WebServerFactoryCustomizer} to add an {@link ErrorPage} so that the
* {@link ManagementErrorEndpoint} can be used.

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2019 the original author or authors.
* Copyright 2012-2020 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.
@ -52,7 +52,7 @@ class ScheduledTasksEndpointDocumentationTests extends MockMvcEndpointDocumentat
@Test
void scheduledTasks() throws Exception {
this.mockMvc.perform(get("/actuator/scheduledtasks")).andExpect(status().isOk())
this.mockMvc.perform(get("/actuator/scheduledtasks").accept("application/json")).andExpect(status().isOk())
.andDo(document("scheduled-tasks",
preprocessResponse(replacePattern(
Pattern.compile("org.*\\.ScheduledTasksEndpointDocumentationTests\\$TestConfiguration"),

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2019 the original author or authors.
* Copyright 2012-2020 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.
@ -21,6 +21,7 @@ import java.util.Collections;
import org.glassfish.jersey.server.ResourceConfig;
import org.junit.jupiter.api.Test;
import org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.endpoint.web.jersey.JerseyWebEndpointManagementContextConfiguration.JerseyWebEndpointsResourcesRegistrar;
import org.springframework.boot.actuate.autoconfigure.web.jersey.JerseySameManagementContextConfiguration;
@ -41,8 +42,8 @@ import static org.assertj.core.api.Assertions.assertThat;
class JerseyWebEndpointManagementContextConfigurationTests {
private final WebApplicationContextRunner runner = new WebApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(WebEndpointAutoConfiguration.class,
JerseyWebEndpointManagementContextConfiguration.class))
.withConfiguration(AutoConfigurations.of(EndpointAutoConfiguration.class,
WebEndpointAutoConfiguration.class, JerseyWebEndpointManagementContextConfiguration.class))
.withBean(WebEndpointsSupplier.class, () -> Collections::emptyList);
@Test

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2019 the original author or authors.
* Copyright 2012-2020 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.
@ -20,7 +20,6 @@ import java.util.function.Supplier;
import javax.servlet.http.HttpServlet;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.springframework.boot.actuate.autoconfigure.audit.AuditAutoConfiguration;
@ -33,32 +32,23 @@ import org.springframework.boot.actuate.endpoint.web.EndpointServlet;
import org.springframework.boot.actuate.endpoint.web.annotation.ControllerEndpoint;
import org.springframework.boot.actuate.endpoint.web.annotation.RestControllerEndpoint;
import org.springframework.boot.actuate.endpoint.web.annotation.ServletEndpoint;
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
import org.springframework.boot.autoconfigure.data.rest.RepositoryRestMvcAutoConfiguration;
import org.springframework.boot.autoconfigure.hateoas.HypermediaAutoConfiguration;
import org.springframework.boot.autoconfigure.gson.GsonAutoConfiguration;
import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration;
import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration;
import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration;
import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration;
import org.springframework.boot.test.util.TestPropertyValues;
import org.springframework.boot.web.servlet.context.AnnotationConfigServletWebApplicationContext;
import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockServletContext;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.test.context.TestSecurityContextHolder;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.DefaultMockMvcBuilder;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.test.web.servlet.setup.MockMvcConfigurer;
import static org.hamcrest.Matchers.both;
import static org.hamcrest.Matchers.hasKey;
import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity;
import static org.hamcrest.Matchers.startsWith;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@ -67,98 +57,39 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
* Integration tests for the Actuator's MVC endpoints.
*
* @author Andy Wilkinson
* @author Brian Clozel
*/
class WebMvcEndpointIntegrationTests {
public class WebMvcEndpointIntegrationTests {
private AnnotationConfigServletWebApplicationContext context;
@AfterEach
void close() {
TestSecurityContextHolder.clearContext();
this.context.close();
}
@Test
void endpointsAreSecureByDefault() throws Exception {
this.context = new AnnotationConfigServletWebApplicationContext();
this.context.register(SecureConfiguration.class);
MockMvc mockMvc = createSecureMockMvc();
mockMvc.perform(get("/actuator/beans").accept(MediaType.APPLICATION_JSON)).andExpect(status().isUnauthorized());
}
@Test
void endpointsAreSecureByDefaultWithCustomBasePath() throws Exception {
this.context = new AnnotationConfigServletWebApplicationContext();
this.context.register(SecureConfiguration.class);
TestPropertyValues.of("management.endpoints.web.base-path:/management").applyTo(this.context);
MockMvc mockMvc = createSecureMockMvc();
mockMvc.perform(get("/management/beans").accept(MediaType.APPLICATION_JSON))
.andExpect(status().isUnauthorized());
}
@Test
void endpointsAreSecureWithActuatorRoleWithCustomBasePath() throws Exception {
TestSecurityContextHolder.getContext()
.setAuthentication(new TestingAuthenticationToken("user", "N/A", "ROLE_ACTUATOR"));
this.context = new AnnotationConfigServletWebApplicationContext();
this.context.register(SecureConfiguration.class);
TestPropertyValues
.of("management.endpoints.web.base-path:/management", "management.endpoints.web.exposure.include=*")
.applyTo(this.context);
MockMvc mockMvc = createSecureMockMvc();
mockMvc.perform(get("/management/beans")).andExpect(status().isOk());
}
private WebApplicationContextRunner contextRunner = new WebApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(JacksonAutoConfiguration.class, GsonAutoConfiguration.class,
HttpMessageConvertersAutoConfiguration.class, EndpointAutoConfiguration.class,
WebEndpointAutoConfiguration.class, ServletManagementContextAutoConfiguration.class,
AuditAutoConfiguration.class, PropertyPlaceholderAutoConfiguration.class,
WebMvcAutoConfiguration.class, ManagementContextAutoConfiguration.class,
AuditAutoConfiguration.class, DispatcherServletAutoConfiguration.class,
BeansEndpointAutoConfiguration.class));
@Test
void linksAreProvidedToAllEndpointTypes() throws Exception {
this.context = new AnnotationConfigServletWebApplicationContext();
this.context.register(DefaultConfiguration.class, EndpointsConfiguration.class);
TestPropertyValues.of("management.endpoints.web.exposure.include=*").applyTo(this.context);
MockMvc mockMvc = doCreateMockMvc();
mockMvc.perform(get("/actuator").accept("*/*")).andExpect(status().isOk()).andExpect(jsonPath("_links",
both(hasKey("beans")).and(hasKey("servlet")).and(hasKey("restcontroller")).and(hasKey("controller"))));
this.contextRunner.withUserConfiguration(EndpointsConfiguration.class)
.withPropertyValues("management.endpoints.web.exposure.include=*").run((context) -> {
MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(context).build();
mockMvc.perform(get("/actuator").accept("*/*")).andExpect(status().isOk())
.andExpect(jsonPath("_links", both(hasKey("beans")).and(hasKey("servlet"))
.and(hasKey("restcontroller")).and(hasKey("controller"))));
});
}
private MockMvc createSecureMockMvc() {
return doCreateMockMvc(springSecurity());
}
private MockMvc doCreateMockMvc(MockMvcConfigurer... configurers) {
this.context.setServletContext(new MockServletContext());
this.context.refresh();
DefaultMockMvcBuilder builder = MockMvcBuilders.webAppContextSetup(this.context);
for (MockMvcConfigurer configurer : configurers) {
builder.apply(configurer);
}
return builder.build();
}
@ImportAutoConfiguration({ JacksonAutoConfiguration.class, HttpMessageConvertersAutoConfiguration.class,
EndpointAutoConfiguration.class, WebEndpointAutoConfiguration.class,
ServletManagementContextAutoConfiguration.class, AuditAutoConfiguration.class,
PropertyPlaceholderAutoConfiguration.class, WebMvcAutoConfiguration.class,
ManagementContextAutoConfiguration.class, AuditAutoConfiguration.class,
DispatcherServletAutoConfiguration.class, BeansEndpointAutoConfiguration.class })
static class DefaultConfiguration {
}
@Import(SecureConfiguration.class)
@ImportAutoConfiguration({ HypermediaAutoConfiguration.class })
static class SpringHateoasConfiguration {
}
@Import(SecureConfiguration.class)
@ImportAutoConfiguration({ HypermediaAutoConfiguration.class, RepositoryRestMvcAutoConfiguration.class })
static class SpringDataRestConfiguration {
}
@Import(DefaultConfiguration.class)
@ImportAutoConfiguration({ SecurityAutoConfiguration.class })
static class SecureConfiguration {
@Test
void dedicatedJsonMapperIsUsed() throws Exception {
this.contextRunner.withPropertyValues("spring.mvc.converters.preferred-json-mapper:gson",
"management.endpoints.web.exposure.include=*").run((context) -> {
MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(context).build();
mockMvc.perform(get("/actuator/beans").accept("*/*")).andExpect(status().isOk())
.andExpect(MockMvcResultMatchers.header().string("Content-Type",
startsWith("application/vnd.spring-boot.actuator")));
});
}
@ServletEndpoint(id = "servlet")

View File

@ -0,0 +1,127 @@
/*
* Copyright 2012-2020 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.integrationtest;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.springframework.boot.actuate.autoconfigure.audit.AuditAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.beans.BeansEndpointAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.web.server.ManagementContextAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.web.servlet.ServletManagementContextAutoConfiguration;
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration;
import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration;
import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration;
import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration;
import org.springframework.boot.test.util.TestPropertyValues;
import org.springframework.boot.web.servlet.context.AnnotationConfigServletWebApplicationContext;
import org.springframework.context.annotation.Import;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockServletContext;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.test.context.TestSecurityContextHolder;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.DefaultMockMvcBuilder;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.test.web.servlet.setup.MockMvcConfigurer;
import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* Integration tests for the Actuator's MVC endpoints security features.
*
* @author Andy Wilkinson
*/
class WebMvcEndpointSecurityIntegrationTests {
private AnnotationConfigServletWebApplicationContext context;
@AfterEach
void close() {
TestSecurityContextHolder.clearContext();
this.context.close();
}
@Test
void endpointsAreSecureByDefault() throws Exception {
this.context = new AnnotationConfigServletWebApplicationContext();
this.context.register(SecureConfiguration.class);
MockMvc mockMvc = createSecureMockMvc();
mockMvc.perform(get("/actuator/beans").accept(MediaType.APPLICATION_JSON)).andExpect(status().isUnauthorized());
}
@Test
void endpointsAreSecureByDefaultWithCustomBasePath() throws Exception {
this.context = new AnnotationConfigServletWebApplicationContext();
this.context.register(SecureConfiguration.class);
TestPropertyValues.of("management.endpoints.web.base-path:/management").applyTo(this.context);
MockMvc mockMvc = createSecureMockMvc();
mockMvc.perform(get("/management/beans").accept(MediaType.APPLICATION_JSON))
.andExpect(status().isUnauthorized());
}
@Test
void endpointsAreSecureWithActuatorRoleWithCustomBasePath() throws Exception {
TestSecurityContextHolder.getContext()
.setAuthentication(new TestingAuthenticationToken("user", "N/A", "ROLE_ACTUATOR"));
this.context = new AnnotationConfigServletWebApplicationContext();
this.context.register(SecureConfiguration.class);
TestPropertyValues
.of("management.endpoints.web.base-path:/management", "management.endpoints.web.exposure.include=*")
.applyTo(this.context);
MockMvc mockMvc = createSecureMockMvc();
mockMvc.perform(get("/management/beans")).andExpect(status().isOk());
}
private MockMvc createSecureMockMvc() {
return doCreateMockMvc(springSecurity());
}
private MockMvc doCreateMockMvc(MockMvcConfigurer... configurers) {
this.context.setServletContext(new MockServletContext());
this.context.refresh();
DefaultMockMvcBuilder builder = MockMvcBuilders.webAppContextSetup(this.context);
for (MockMvcConfigurer configurer : configurers) {
builder.apply(configurer);
}
return builder.build();
}
@ImportAutoConfiguration({ JacksonAutoConfiguration.class, HttpMessageConvertersAutoConfiguration.class,
EndpointAutoConfiguration.class, WebEndpointAutoConfiguration.class,
ServletManagementContextAutoConfiguration.class, AuditAutoConfiguration.class,
PropertyPlaceholderAutoConfiguration.class, WebMvcAutoConfiguration.class,
ManagementContextAutoConfiguration.class, AuditAutoConfiguration.class,
DispatcherServletAutoConfiguration.class, BeansEndpointAutoConfiguration.class })
static class DefaultConfiguration {
}
@Import(DefaultConfiguration.class)
@ImportAutoConfiguration({ SecurityAutoConfiguration.class })
static class SecureConfiguration {
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2019 the original author or authors.
* Copyright 2012-2020 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.
@ -59,6 +59,7 @@ class WebMvcEndpointChildContextConfigurationIntegrationTests {
WebClient client = WebClient.create("http://localhost:" + port);
ClientResponse response = client.get().uri("actuator/fail").accept(MediaType.APPLICATION_JSON)
.exchange().block();
assertThat(response.headers().contentType().get()).isEqualTo(MediaType.APPLICATION_JSON);
assertThat(response.bodyToMono(String.class).block()).contains("message\":\"Epic Fail");
});
}

View File

@ -0,0 +1,49 @@
/*
* Copyright 2012-2020 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.json;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
/**
* Factory for an {@code ObjectMapper} instance to be shared within Actuator
* infrastructure.
*
* <p>
* The goal is to have a Jackson configuration separate from the rest of the application
* to keep a consistent serialization behavior between applications.
*
* @author Brian Clozel
* @since 2.3.0
*/
public class ActuatorJsonMapperProvider {
private ObjectMapper objectMapper;
public ObjectMapper getInstance() {
if (this.objectMapper == null) {
Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder();
builder.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
builder.featuresToDisable(SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS);
this.objectMapper = builder.build();
}
return this.objectMapper;
}
}

View File

@ -0,0 +1,20 @@
/*
* Copyright 2012-2020 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.
*/
/**
* JSON support for actuator endpoints.
*/
package org.springframework.boot.actuate.endpoint.json;

View File

@ -46,6 +46,9 @@
<disallow pkg="org.springframework.web.servlet" />
</subpackage>
</subpackage>
<subpackage name="json">
<allow pkg="org.springframework.http.converter" />
</subpackage>
</subpackage>
<!-- Logging -->