Add CORS support to Actuator WebFlux

Closes gh-11308
pull/11314/merge
Brian Clozel 7 years ago
parent deb16e1617
commit 5f3c2bef50

@ -17,17 +17,24 @@
package org.springframework.boot.actuate.autoconfigure.endpoint.web.reactive;
import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties;
import org.springframework.boot.actuate.autoconfigure.endpoint.web.servlet.CorsEndpointProperties;
import org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfiguration;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes;
import org.springframework.boot.actuate.endpoint.web.annotation.WebAnnotationEndpointDiscoverer;
import org.springframework.boot.actuate.endpoint.web.reactive.WebFluxEndpointHandlerMapping;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.endpoint.web.EndpointMapping;
import org.springframework.context.annotation.Bean;
import org.springframework.http.server.reactive.HttpHandler;
import org.springframework.util.CollectionUtils;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.reactive.DispatcherHandler;
/**
* {@link ManagementContextConfiguration} for Reactive {@link Endpoint} concerns.
@ -38,18 +45,44 @@ import org.springframework.context.annotation.Bean;
*/
@ManagementContextConfiguration
@ConditionalOnWebApplication(type = Type.REACTIVE)
@ConditionalOnClass({ DispatcherHandler.class, HttpHandler.class })
@ConditionalOnBean(WebAnnotationEndpointDiscoverer.class)
@EnableConfigurationProperties(CorsEndpointProperties.class)
public class WebFluxEndpointManagementContextConfiguration {
@Bean
@ConditionalOnMissingBean
public WebFluxEndpointHandlerMapping webEndpointReactiveHandlerMapping(
WebAnnotationEndpointDiscoverer endpointDiscoverer,
EndpointMediaTypes endpointMediaTypes,
EndpointMediaTypes endpointMediaTypes, CorsEndpointProperties corsProperties,
WebEndpointProperties webEndpointProperties) {
return new WebFluxEndpointHandlerMapping(
new EndpointMapping(webEndpointProperties.getBasePath()),
endpointDiscoverer.discoverEndpoints(), endpointMediaTypes);
endpointDiscoverer.discoverEndpoints(), endpointMediaTypes, getCorsConfiguration(corsProperties));
}
private CorsConfiguration getCorsConfiguration(CorsEndpointProperties properties) {
if (CollectionUtils.isEmpty(properties.getAllowedOrigins())) {
return null;
}
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(properties.getAllowedOrigins());
if (!CollectionUtils.isEmpty(properties.getAllowedHeaders())) {
configuration.setAllowedHeaders(properties.getAllowedHeaders());
}
if (!CollectionUtils.isEmpty(properties.getAllowedMethods())) {
configuration.setAllowedMethods(properties.getAllowedMethods());
}
if (!CollectionUtils.isEmpty(properties.getExposedHeaders())) {
configuration.setExposedHeaders(properties.getExposedHeaders());
}
if (properties.getMaxAge() != null) {
configuration.setMaxAge(properties.getMaxAge().getSeconds());
}
if (properties.getAllowCredentials() != null) {
configuration.setAllowCredentials(properties.getAllowCredentials());
}
return configuration;
}
}

@ -0,0 +1,203 @@
/*
* Copyright 2012-2017 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.integrationtest;
import org.junit.Before;
import org.junit.Test;
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.endpoint.web.reactive.WebFluxEndpointManagementContextConfiguration;
import org.springframework.boot.actuate.autoconfigure.web.reactive.ReactiveManagementContextAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.web.server.ManagementContextAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionEvaluationReport;
import org.springframework.boot.autoconfigure.http.codec.CodecsAutoConfiguration;
import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
import org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportMessage;
import org.springframework.boot.autoconfigure.web.reactive.HttpHandlerAutoConfiguration;
import org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration;
import org.springframework.boot.test.util.TestPropertyValues;
import org.springframework.boot.web.reactive.context.AnnotationConfigReactiveWebApplicationContext;
import org.springframework.http.HttpHeaders;
import org.springframework.test.web.reactive.server.WebTestClient;
/**
* Integration tests for the WebFlux actuator endpoints' CORS support
*
* @author Brian Clozel
* @see WebFluxEndpointManagementContextConfiguration
*/
public class WebFluxEndpointCorsIntegrationTests {
private AnnotationConfigReactiveWebApplicationContext context;
@Before
public void createContext() {
this.context = new AnnotationConfigReactiveWebApplicationContext();
this.context.register(JacksonAutoConfiguration.class,
CodecsAutoConfiguration.class,
WebFluxAutoConfiguration.class, HttpHandlerAutoConfiguration.class,
EndpointAutoConfiguration.class, WebEndpointAutoConfiguration.class,
ManagementContextAutoConfiguration.class,
ReactiveManagementContextAutoConfiguration.class,
BeansEndpointAutoConfiguration.class);
TestPropertyValues.of("management.endpoints.web.expose:*").applyTo(this.context);
}
@Test
public void corsIsDisabledByDefault() throws Exception {
WebTestClient client = createWebTestClient();
System.out.println(new ConditionEvaluationReportMessage(
this.context.getBean(ConditionEvaluationReport.class)));
client.options().uri("/actuator/beans")
.header("Origin", "spring.example.org")
.header(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "GET")
.exchange()
.expectStatus().isForbidden();
//TODO: .expectHeader().doesNotExist(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN);
}
@Test
public void settingAllowedOriginsEnablesCors() throws Exception {
TestPropertyValues
.of("management.endpoints.web.cors.allowed-origins:spring.example.org")
.applyTo(this.context);
createWebTestClient()
.options().uri("/actuator/beans")
.header("Origin", "test.example.org")
.header(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "GET")
.exchange()
.expectStatus().isForbidden();
performAcceptedCorsRequest("/actuator/beans");
}
@Test
public void maxAgeDefaultsTo30Minutes() throws Exception {
TestPropertyValues
.of("management.endpoints.web.cors.allowed-origins:spring.example.org")
.applyTo(this.context);
performAcceptedCorsRequest("/actuator/beans")
.expectHeader().valueEquals(HttpHeaders.ACCESS_CONTROL_MAX_AGE, "1800");
}
@Test
public void maxAgeCanBeConfigured() throws Exception {
TestPropertyValues
.of("management.endpoints.web.cors.allowed-origins:spring.example.org",
"management.endpoints.web.cors.max-age: 2400")
.applyTo(this.context);
performAcceptedCorsRequest("/actuator/beans")
.expectHeader().valueEquals(HttpHeaders.ACCESS_CONTROL_MAX_AGE, "2400");
}
@Test
public void requestsWithDisallowedHeadersAreRejected() throws Exception {
TestPropertyValues
.of("management.endpoints.web.cors.allowed-origins:spring.example.org")
.applyTo(this.context);
createWebTestClient()
.options().uri("/actuator/beans")
.header("Origin", "spring.example.org")
.header(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "GET")
.header(HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS, "Alpha")
.exchange()
.expectStatus().isForbidden();
}
@Test
public void allowedHeadersCanBeConfigured() throws Exception {
TestPropertyValues
.of("management.endpoints.web.cors.allowed-origins:spring.example.org",
"management.endpoints.web.cors.allowed-headers:Alpha,Bravo")
.applyTo(this.context);
createWebTestClient()
.options().uri("/actuator/beans")
.header("Origin", "spring.example.org")
.header(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "GET")
.header(HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS, "Alpha")
.exchange()
.expectStatus().isOk()
.expectHeader().valueEquals(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS, "Alpha");
}
@Test
public void requestsWithDisallowedMethodsAreRejected() throws Exception {
TestPropertyValues
.of("management.endpoints.web.cors.allowed-origins:spring.example.org")
.applyTo(this.context);
createWebTestClient()
.options().uri("/actuator/beans")
.header("Origin", "spring.example.org")
.header(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "PATCH")
.exchange()
.expectStatus().isForbidden();
}
@Test
public void allowedMethodsCanBeConfigured() throws Exception {
TestPropertyValues
.of("management.endpoints.web.cors.allowed-origins:spring.example.org",
"management.endpoints.web.cors.allowed-methods:GET,HEAD")
.applyTo(this.context);
createWebTestClient()
.options().uri("/actuator/beans")
.header("Origin", "spring.example.org")
.header(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "HEAD")
.exchange()
.expectStatus().isOk()
.expectHeader().valueEquals(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS, "GET,HEAD");
}
@Test
public void credentialsCanBeAllowed() throws Exception {
TestPropertyValues
.of("management.endpoints.web.cors.allowed-origins:spring.example.org",
"management.endpoints.web.cors.allow-credentials:true")
.applyTo(this.context);
performAcceptedCorsRequest("/actuator/beans")
.expectHeader().valueEquals(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true");
}
@Test
public void credentialsCanBeDisabled() throws Exception {
TestPropertyValues
.of("management.endpoints.web.cors.allowed-origins:spring.example.org",
"management.endpoints.web.cors.allow-credentials:false")
.applyTo(this.context);
performAcceptedCorsRequest("/actuator/beans");
//TODO .expectHeader().doesNotExist(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS);
}
private WebTestClient createWebTestClient() {
this.context.refresh();
return WebTestClient.bindToApplicationContext(this.context)
.configureClient().baseUrl("https://spring.example.org").build();
}
private WebTestClient.ResponseSpec performAcceptedCorsRequest(String url) throws Exception {
return createWebTestClient()
.options().uri(url)
.header(HttpHeaders.ORIGIN, "spring.example.org")
.header(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "GET")
.exchange()
.expectHeader().valueEquals(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, "spring.example.org")
.expectStatus().isOk();
}
}

@ -25,7 +25,6 @@ import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointAu
import org.springframework.boot.actuate.autoconfigure.endpoint.web.servlet.WebMvcEndpointManagementContextConfiguration;
import org.springframework.boot.actuate.autoconfigure.web.server.ManagementContextAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.web.servlet.ServletManagementContextAutoConfiguration;
import org.springframework.boot.actuate.beans.BeansEndpoint;
import org.springframework.boot.autoconfigure.condition.ConditionEvaluationReport;
import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration;
import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
@ -33,13 +32,8 @@ import org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportM
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.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpHeaders;
import org.springframework.mock.web.MockServletContext;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.ResultActions;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
@ -50,7 +44,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* Integration tests for the actuator endpoints' CORS support
* Integration tests for the MVC actuator endpoints' CORS support
*
* @author Andy Wilkinson
* @see WebMvcEndpointManagementContextConfiguration
@ -204,25 +198,4 @@ public class WebMvcEndpointCorsIntegrationTests {
.andExpect(status().isOk());
}
@Configuration
static class EndpointConfiguration {
@Bean
public BeansEndpoint beansEndpoint(
ConfigurableApplicationContext applicationContext) {
return new BeansEndpoint(applicationContext);
}
}
@Configuration
static class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors();
}
}
}

Loading…
Cancel
Save