Skip to content

Commit

Permalink
Add CORS support to the actuator’s endpoints
Browse files Browse the repository at this point in the history
This commit adds CORS support to the Actuator’s MVC endpoints. CORS
support is disabled by default and is only enabled once the
endpoints.cors.allowed-origins property has been set.

The new properties to control the endpoints’ CORS configuration are:

endpoints.cors.allow-credentials
endpoints.cors.allowed-origins
endpoints.cors.allowed-methods
endpoints.cors.allowed-headers
endpoints.cors.exposed-headers

The changes to enable Jolokia-specific CORS support (57a51ed) have been
reverted as part of this commit. This provides a consistent approach
to CORS configuration across all endpoints, rather than Jolokia using
its own configuration.

See spring-projectsgh-1987
Closes spring-projectsgh-2936
  • Loading branch information
wilkinsona committed May 14, 2015
1 parent b466229 commit 84d3a34
Show file tree
Hide file tree
Showing 7 changed files with 369 additions and 19 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,8 @@
@AutoConfigureAfter({ PropertyPlaceholderAutoConfiguration.class,
EmbeddedServletContainerAutoConfiguration.class, WebMvcAutoConfiguration.class,
ManagementServerPropertiesAutoConfiguration.class })
@EnableConfigurationProperties(HealthMvcEndpointProperties.class)
@EnableConfigurationProperties({ HealthMvcEndpointProperties.class,
MvcEndpointCorsProperties.class })
public class EndpointWebMvcAutoConfiguration implements ApplicationContextAware,
SmartInitializingSingleton {

Expand All @@ -117,6 +118,9 @@ public class EndpointWebMvcAutoConfiguration implements ApplicationContextAware,
@Autowired
private ManagementServerProperties managementServerProperties;

@Autowired
private MvcEndpointCorsProperties corsMvcEndpointProperties;

@Autowired(required = false)
private List<EndpointHandlerMappingCustomizer> mappingCustomizers;

Expand All @@ -130,7 +134,7 @@ public void setApplicationContext(ApplicationContext applicationContext)
@ConditionalOnMissingBean
public EndpointHandlerMapping endpointHandlerMapping() {
EndpointHandlerMapping mapping = new EndpointHandlerMapping(mvcEndpoints()
.getEndpoints());
.getEndpoints(), this.corsMvcEndpointProperties.toCorsConfiguration());
boolean disabled = ManagementServerPort.get(this.applicationContext) != ManagementServerPort.SAME;
mapping.setDisabled(disabled);
if (!disabled) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
/*
* Copyright 2012-2015 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
*
* http://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;

import java.util.ArrayList;
import java.util.List;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.util.CollectionUtils;
import org.springframework.web.cors.CorsConfiguration;

/**
* Configuration properties for MVC endpoints' CORS support.
*
* @author Andy Wilkinson
* @since 1.3.0
*/
@ConfigurationProperties(prefix = "endpoints.cors")
public class MvcEndpointCorsProperties {

/**
* List of origins to allow.
*/
private List<String> allowedOrigins = new ArrayList<String>();

/**
* List of methods to allow.
*/
private List<String> allowedMethods = new ArrayList<String>();

/**
* List of headers to allow in a request
*/
private List<String> allowedHeaders = new ArrayList<String>();

/**
* List of headers to include in a response.
*/
private List<String> exposedHeaders = new ArrayList<String>();

/**
* Whether credentials are supported
*/
private Boolean allowCredentials;

/**
* How long, in seconds, the response from a pre-flight request can be cached by
* clients.
*/
private Long maxAge = 1800L;

public List<String> getAllowedOrigins() {
return this.allowedOrigins;
}

public void setAllowedOrigins(List<String> allowedOrigins) {
this.allowedOrigins = allowedOrigins;
}

public List<String> getAllowedMethods() {
return this.allowedMethods;
}

public void setAllowedMethods(List<String> allowedMethods) {
this.allowedMethods = allowedMethods;
}

public List<String> getAllowedHeaders() {
return this.allowedHeaders;
}

public void setAllowedHeaders(List<String> allowedHeaders) {
this.allowedHeaders = allowedHeaders;
}

public List<String> getExposedHeaders() {
return this.exposedHeaders;
}

public void setExposedHeaders(List<String> exposedHeaders) {
this.exposedHeaders = exposedHeaders;
}

public Boolean getAllowCredentials() {
return this.allowCredentials;
}

public void setAllowCredentials(Boolean allowCredentials) {
this.allowCredentials = allowCredentials;
}

public Long getMaxAge() {
return this.maxAge;
}

public void setMaxAge(Long maxAge) {
this.maxAge = maxAge;
}

CorsConfiguration toCorsConfiguration() {
if (CollectionUtils.isEmpty(this.allowedOrigins)) {
return null;
}
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.setAllowedOrigins(this.allowedOrigins);
if (!CollectionUtils.isEmpty(this.allowedHeaders)) {
corsConfiguration.setAllowedHeaders(this.allowedHeaders);
}
if (!CollectionUtils.isEmpty(this.allowedMethods)) {
corsConfiguration.setAllowedMethods(this.allowedMethods);
}
if (!CollectionUtils.isEmpty(this.exposedHeaders)) {
corsConfiguration.setExposedHeaders(this.exposedHeaders);
}
if (this.maxAge != null) {
corsConfiguration.setMaxAge(this.maxAge);
}
if (this.allowCredentials != null) {
corsConfiguration.setAllowCredentials(true);
}
return corsConfiguration;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import org.springframework.context.ApplicationContextAware;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.servlet.HandlerMapping;
import org.springframework.web.servlet.mvc.condition.PatternsRequestCondition;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
Expand All @@ -47,24 +48,40 @@
* @author Phillip Webb
* @author Christian Dupuis
* @author Dave Syer
* @author Andy Wilkinson
*/
public class EndpointHandlerMapping extends RequestMappingHandlerMapping implements
ApplicationContextAware {

private final Set<MvcEndpoint> endpoints;

private final CorsConfiguration corsConfiguration;

private String prefix = "";

private boolean disabled = false;

/**
* Create a new {@link EndpointHandlerMapping} instance. All {@link Endpoint}s will be
* detected from the {@link ApplicationContext}.
* detected from the {@link ApplicationContext}. The endpoints will not accept CORS
* requests.
* @param endpoints the endpoints
*/
public EndpointHandlerMapping(Collection<? extends MvcEndpoint> endpoints) {
this(endpoints, null);
}

/**
* Create a new {@link EndpointHandlerMapping} instance. All {@link Endpoint}s will be
* detected from the {@link ApplicationContext}. The endpoints will accepts CORS
* requests based on the given {@code corsConfiguration}.
* @param endpoints the endpoints
* @param corsConfiguration the CORS configuration for the endpoints
* @since 1.3.0
*/
public EndpointHandlerMapping(Collection<? extends MvcEndpoint> endpoints,
CorsConfiguration corsConfiguration) {
this.endpoints = new HashSet<MvcEndpoint>(endpoints);
this.corsConfiguration = corsConfiguration;
// By default the static resource handler mapping is LOWEST_PRECEDENCE - 1
// and the RequestMappingHandlerMapping is 0 (we ideally want to be before both)
setOrder(-100);
Expand Down Expand Up @@ -96,7 +113,7 @@ protected void registerHandlerMethod(Object handler, Method method,
return;
}
String[] patterns = getPatterns(handler, mapping);
super.registerMapping(withNewPatterns(mapping, patterns), handler, method);
super.registerHandlerMethod(handler, method, withNewPatterns(mapping, patterns));
}

private String[] getPatterns(Object handler, RequestMappingInfo mapping) {
Expand Down Expand Up @@ -180,4 +197,9 @@ public Set<? extends MvcEndpoint> getEndpoints() {
return new HashSet<MvcEndpoint>(this.endpoints);
}

@Override
protected CorsConfiguration initCorsConfiguration(Object handler, Method method,
RequestMappingInfo mappingInfo) {
return this.corsConfiguration;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,6 @@ public JolokiaMvcEndpoint() {
this.path = "/jolokia";
this.controller.setServletClass(AgentServlet.class);
this.controller.setServletName("jolokia");
this.controller.setSupportedMethods("GET", "POST", "HEAD", "OPTIONS");
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.http.HttpHeaders;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
Expand All @@ -44,17 +43,14 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.options;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

/**
* Tests for {@link JolokiaMvcEndpoint}
*
* @author Christian Dupuis
* @author Dave Syer
* @author Andy Wilkinson
*/
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = { Config.class })
Expand Down Expand Up @@ -103,15 +99,6 @@ public void list() throws Exception {
.andExpect(content().string(containsString("NonHeapMemoryUsage")));
}

@Test
public void corsOptionsRequest() throws Exception {
this.mvc.perform(
options("/jolokia/read/java.lang:type=Memory").header(HttpHeaders.ORIGIN,
"example.com").header(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD,
"GET")).andExpect(
header().string(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, "example.com"));
}

@Configuration
@EnableConfigurationProperties
@EnableWebMvc
Expand Down
Loading

0 comments on commit 84d3a34

Please sign in to comment.