From 5f3c2bef507c403adb0aeb6c2db072b13ab7d577 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Mon, 11 Dec 2017 22:33:51 +0100 Subject: [PATCH] Add CORS support to Actuator WebFlux Closes gh-11308 --- ...ndpointManagementContextConfiguration.java | 37 +++- .../WebFluxEndpointCorsIntegrationTests.java | 203 ++++++++++++++++++ .../WebMvcEndpointCorsIntegrationTests.java | 29 +-- 3 files changed, 239 insertions(+), 30 deletions(-) create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/WebFluxEndpointCorsIntegrationTests.java diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/reactive/WebFluxEndpointManagementContextConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/reactive/WebFluxEndpointManagementContextConfiguration.java index d09729664b..1324fc65d4 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/reactive/WebFluxEndpointManagementContextConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/reactive/WebFluxEndpointManagementContextConfiguration.java @@ -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; } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/WebFluxEndpointCorsIntegrationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/WebFluxEndpointCorsIntegrationTests.java new file mode 100644 index 0000000000..5d751e9f89 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/WebFluxEndpointCorsIntegrationTests.java @@ -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(); + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/WebMvcEndpointCorsIntegrationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/WebMvcEndpointCorsIntegrationTests.java index 6cd895944e..d3e2afecc2 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/WebMvcEndpointCorsIntegrationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/WebMvcEndpointCorsIntegrationTests.java @@ -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(); - } - - } - }