Browse Source
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 gh-1987
Closes gh-2936
pull/2869/merge
7 changed files with 369 additions and 19 deletions
@ -0,0 +1,138 @@
@@ -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; |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,193 @@
@@ -0,0 +1,193 @@
|
||||
/* |
||||
* 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.endpoint.mvc; |
||||
|
||||
import org.junit.Before; |
||||
import org.junit.Test; |
||||
import org.springframework.boot.actuate.autoconfigure.EndpointAutoConfiguration; |
||||
import org.springframework.boot.actuate.autoconfigure.EndpointWebMvcAutoConfiguration; |
||||
import org.springframework.boot.actuate.autoconfigure.JolokiaAutoConfiguration; |
||||
import org.springframework.boot.actuate.autoconfigure.ManagementServerPropertiesAutoConfiguration; |
||||
import org.springframework.boot.autoconfigure.PropertyPlaceholderAutoConfiguration; |
||||
import org.springframework.boot.autoconfigure.web.HttpMessageConvertersAutoConfiguration; |
||||
import org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration; |
||||
import org.springframework.boot.test.EnvironmentTestUtils; |
||||
import org.springframework.http.HttpHeaders; |
||||
import org.springframework.mock.web.MockServletContext; |
||||
import org.springframework.test.web.servlet.MockMvc; |
||||
import org.springframework.test.web.servlet.ResultActions; |
||||
import org.springframework.test.web.servlet.setup.MockMvcBuilders; |
||||
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; |
||||
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.options; |
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; |
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; |
||||
|
||||
/** |
||||
* Integration tests for the actuator endpoints' CORS support |
||||
* |
||||
* @author Andy Wilkinson |
||||
*/ |
||||
public class MvcEndpointCorsIntegrationTests { |
||||
|
||||
private AnnotationConfigWebApplicationContext context; |
||||
|
||||
@Before |
||||
public void createContext() { |
||||
this.context = new AnnotationConfigWebApplicationContext(); |
||||
this.context.setServletContext(new MockServletContext()); |
||||
this.context.register(HttpMessageConvertersAutoConfiguration.class, |
||||
EndpointAutoConfiguration.class, EndpointWebMvcAutoConfiguration.class, |
||||
ManagementServerPropertiesAutoConfiguration.class, |
||||
PropertyPlaceholderAutoConfiguration.class, |
||||
JolokiaAutoConfiguration.class, WebMvcAutoConfiguration.class); |
||||
} |
||||
|
||||
@Test |
||||
public void corsIsDisabledByDefault() throws Exception { |
||||
createMockMvc().perform( |
||||
options("/beans").header("Origin", "foo.example.com").header( |
||||
HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "GET")).andExpect( |
||||
header().doesNotExist(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)); |
||||
} |
||||
|
||||
@Test |
||||
public void settingAllowedOriginsEnablesCors() throws Exception { |
||||
EnvironmentTestUtils.addEnvironment(this.context, |
||||
"endpoints.cors.allowed-origins:foo.example.com"); |
||||
createMockMvc().perform( |
||||
options("/beans").header("Origin", "bar.example.com").header( |
||||
HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "GET")).andExpect( |
||||
status().isForbidden()); |
||||
performAcceptedCorsRequest(); |
||||
} |
||||
|
||||
@Test |
||||
public void maxAgeDefaultsTo30Minutes() throws Exception { |
||||
EnvironmentTestUtils.addEnvironment(this.context, |
||||
"endpoints.cors.allowed-origins:foo.example.com"); |
||||
createMockMvc().perform( |
||||
options("/beans").header("Origin", "bar.example.com").header( |
||||
HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "GET")).andExpect( |
||||
status().isForbidden()); |
||||
performAcceptedCorsRequest().andExpect( |
||||
header().string(HttpHeaders.ACCESS_CONTROL_MAX_AGE, "1800")); |
||||
} |
||||
|
||||
@Test |
||||
public void maxAgeCanBeConfigured() throws Exception { |
||||
EnvironmentTestUtils.addEnvironment(this.context, |
||||
"endpoints.cors.allowed-origins:foo.example.com", |
||||
"endpoints.cors.max-age: 2400"); |
||||
performAcceptedCorsRequest().andExpect( |
||||
header().string(HttpHeaders.ACCESS_CONTROL_MAX_AGE, "2400")); |
||||
} |
||||
|
||||
@Test |
||||
public void requestsWithDisallowedHeadersAreRejected() throws Exception { |
||||
EnvironmentTestUtils.addEnvironment(this.context, |
||||
"endpoints.cors.allowed-origins:foo.example.com"); |
||||
createMockMvc().perform( |
||||
options("/beans").header("Origin", "foo.example.com") |
||||
.header(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "GET") |
||||
.header(HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS, "Alpha")) |
||||
.andExpect(status().isForbidden()); |
||||
} |
||||
|
||||
@Test |
||||
public void allowedHeadersCanBeConfigured() throws Exception { |
||||
EnvironmentTestUtils.addEnvironment(this.context, |
||||
"endpoints.cors.allowed-origins:foo.example.com", |
||||
"endpoints.cors.allowed-headers:Alpha,Bravo"); |
||||
createMockMvc() |
||||
.perform( |
||||
options("/beans") |
||||
.header("Origin", "foo.example.com") |
||||
.header(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "GET") |
||||
.header(HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS, |
||||
"Alpha")) |
||||
.andExpect(status().isOk()) |
||||
.andExpect( |
||||
header().string(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS, "Alpha")); |
||||
} |
||||
|
||||
@Test |
||||
public void requestsWithDisallowedMethodsAreRejected() throws Exception { |
||||
EnvironmentTestUtils.addEnvironment(this.context, |
||||
"endpoints.cors.allowed-origins:foo.example.com"); |
||||
createMockMvc().perform( |
||||
options("/health").header(HttpHeaders.ORIGIN, "foo.example.com").header( |
||||
HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "HEAD")).andExpect( |
||||
status().isForbidden()); |
||||
} |
||||
|
||||
@Test |
||||
public void allowedMethodsCanBeConfigured() throws Exception { |
||||
EnvironmentTestUtils.addEnvironment(this.context, |
||||
"endpoints.cors.allowed-origins:foo.example.com", |
||||
"endpoints.cors.allowed-methods:GET,HEAD"); |
||||
createMockMvc() |
||||
.perform( |
||||
options("/health") |
||||
.header(HttpHeaders.ORIGIN, "foo.example.com") |
||||
.header(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "HEAD")) |
||||
.andExpect(status().isOk()) |
||||
.andExpect( |
||||
header().string(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS, |
||||
"GET,HEAD")); |
||||
} |
||||
|
||||
@Test |
||||
public void credentialsCanBeAllowed() throws Exception { |
||||
EnvironmentTestUtils.addEnvironment(this.context, |
||||
"endpoints.cors.allowed-origins:foo.example.com", |
||||
"endpoints.cors.allow-credentials:true"); |
||||
performAcceptedCorsRequest().andExpect( |
||||
header().string(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true")); |
||||
} |
||||
|
||||
@Test |
||||
public void jolokiaEndpointUsesGlobalCorsConfiguration() throws Exception { |
||||
EnvironmentTestUtils.addEnvironment(this.context, |
||||
"endpoints.cors.allowed-origins:foo.example.com"); |
||||
createMockMvc().perform( |
||||
options("/jolokia").header("Origin", "bar.example.com").header( |
||||
HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "GET")).andExpect( |
||||
status().isForbidden()); |
||||
performAcceptedCorsRequest("/jolokia"); |
||||
} |
||||
|
||||
private MockMvc createMockMvc() { |
||||
this.context.refresh(); |
||||
return MockMvcBuilders.webAppContextSetup(this.context).build(); |
||||
} |
||||
|
||||
private ResultActions performAcceptedCorsRequest() throws Exception { |
||||
return performAcceptedCorsRequest("/beans"); |
||||
} |
||||
|
||||
private ResultActions performAcceptedCorsRequest(String url) throws Exception { |
||||
return createMockMvc() |
||||
.perform( |
||||
options(url).header(HttpHeaders.ORIGIN, "foo.example.com") |
||||
.header(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "GET")) |
||||
.andExpect( |
||||
header().string(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, |
||||
"foo.example.com")).andExpect(status().isOk()); |
||||
} |
||||
|
||||
} |
||||
Loading…
Reference in new issue