From d7f891be39c84effded13c31ee4c8d3263494227 Mon Sep 17 00:00:00 2001 From: Pedro Ivo Machado Date: Wed, 23 Dec 2020 16:53:43 -0300 Subject: [PATCH 1/2] Add config prop for endpoints' CORS allowed origin patterns See gh-24680 --- .../endpoint/web/CorsEndpointProperties.java | 20 ++++++++++++-- .../WebFluxEndpointCorsIntegrationTests.java | 26 +++++++++++++++++++ .../WebMvcEndpointCorsIntegrationTests.java | 21 +++++++++++++++ 3 files changed, 65 insertions(+), 2 deletions(-) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/CorsEndpointProperties.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/CorsEndpointProperties.java index 979309da25..42fba6fa12 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/CorsEndpointProperties.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/CorsEndpointProperties.java @@ -38,10 +38,17 @@ public class CorsEndpointProperties { /** * Comma-separated list of origins to allow. '*' allows all origins. When not set, - * CORS support is disabled. + * CORS support is disabled. When credentials are supported only explicit urls are + * allowed. */ private List allowedOrigins = new ArrayList<>(); + /** + * Comma-separated list of origins patterns to allow. Must be used when credentials + * are supported and do you want to use wildcard urls. + */ + private List allowedOriginPatterns = new ArrayList<>(); + /** * Comma-separated list of methods to allow. '*' allows all methods. When not set, * defaults to GET. @@ -78,6 +85,14 @@ public class CorsEndpointProperties { this.allowedOrigins = allowedOrigins; } + public List getAllowedOriginPatterns() { + return this.allowedOriginPatterns; + } + + public void setAllowedOriginPatterns(List allowedOriginPatterns) { + this.allowedOriginPatterns = allowedOriginPatterns; + } + public List getAllowedMethods() { return this.allowedMethods; } @@ -119,12 +134,13 @@ public class CorsEndpointProperties { } public CorsConfiguration toCorsConfiguration() { - if (CollectionUtils.isEmpty(this.allowedOrigins)) { + if (CollectionUtils.isEmpty(this.allowedOrigins) && CollectionUtils.isEmpty(this.allowedOriginPatterns)) { return null; } PropertyMapper map = PropertyMapper.get(); CorsConfiguration configuration = new CorsConfiguration(); map.from(this::getAllowedOrigins).to(configuration::setAllowedOrigins); + map.from(this::getAllowedOriginPatterns).to(configuration::setAllowedOriginPatterns); map.from(this::getAllowedHeaders).whenNot(CollectionUtils::isEmpty).to(configuration::setAllowedHeaders); map.from(this::getAllowedMethods).whenNot(CollectionUtils::isEmpty).to(configuration::setAllowedMethods); map.from(this::getExposedHeaders).whenNot(CollectionUtils::isEmpty).to(configuration::setExposedHeaders); 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 index 154b80e8f4..97f47f598b 100644 --- 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 @@ -16,6 +16,9 @@ package org.springframework.boot.actuate.autoconfigure.integrationtest; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.options; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + import java.util.function.Consumer; import org.junit.jupiter.api.Test; @@ -145,6 +148,29 @@ class WebFluxEndpointCorsIntegrationTests { .expectHeader().doesNotExist(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS))); } + @Test + void settingAllowedOriginsPattern() { + this.contextRunner + .withPropertyValues("management.endpoints.web.cors.allowed-origin-patterns:*.example.com", + "management.endpoints.web.cors.allow-credentials:true") + .run(withWebTestClient((webTestClient) -> webTestClient.options().uri("/actuator/beans") + .header("Origin", "spring.example.com") + .header(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "HEAD").exchange().expectStatus().isOk() + .expectHeader().valueEquals(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS, "GET,HEAD"))); + } + + @Test + void requestsWithDisallowedOriginPatternsAreRejected() { + this.contextRunner + .withPropertyValues("management.endpoints.web.cors.allowed-origin-patterns:*.example.com", + "management.endpoints.web.cors.allow-credentials:true") + .run(withWebTestClient((webTestClient) -> webTestClient.options().uri("/actuator/beans") + .header("Origin", "spring.example.org") + .header(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "HEAD").exchange().expectStatus() + .isForbidden())); + + } + private ContextConsumer withWebTestClient(Consumer webTestClient) { return (context) -> webTestClient.accept(WebTestClient.bindToApplicationContext(context).configureClient() .baseUrl("https://spring.example.org").build()); 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 518567586f..cc82302656 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 @@ -156,6 +156,27 @@ class WebMvcEndpointCorsIntegrationTests { .andExpect(header().doesNotExist(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS)))); } + @Test + void settingAllowedOriginsPattern() { + this.contextRunner.withPropertyValues("management.endpoints.web.cors.allowed-origin-patterns:*.example.com", + "management.endpoints.web.cors.allow-credentials:true").run(withMockMvc((mockMvc) -> { + mockMvc.perform(options("/actuator/beans").header("Origin", "bar.example.com") + .header(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "GET")).andExpect(status().isOk()); + performAcceptedCorsRequest(mockMvc); + })); + } + + @Test + void requestsWithDisallowedOriginPatternsAreRejected() { + this.contextRunner.withPropertyValues("management.endpoints.web.cors.allowed-origin-patterns:*.example.com", + "management.endpoints.web.cors.allow-credentials:true").run(withMockMvc((mockMvc) -> { + mockMvc.perform(options("/actuator/beans").header("Origin", "bar.domain.com") + .header(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "GET")) + .andExpect(status().isForbidden()); + performAcceptedCorsRequest(mockMvc); + })); + } + private ContextConsumer withMockMvc(MockMvcConsumer mockMvc) { return (context) -> mockMvc.accept(MockMvcBuilders.webAppContextSetup(context).build()); } From b095c7761aa4b126f94c104ae9a70a51f396c5bc Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 19 Jan 2021 15:51:14 +0000 Subject: [PATCH 2/2] Polish "Add config prop for endpoints' CORS allowed origin patterns" See gh-24680 --- .../endpoint/web/CorsEndpointProperties.java | 15 ++++--- .../WebFluxEndpointCorsIntegrationTests.java | 41 +++++++------------ .../WebMvcEndpointCorsIntegrationTests.java | 34 ++++++--------- 3 files changed, 35 insertions(+), 55 deletions(-) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/CorsEndpointProperties.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/CorsEndpointProperties.java index 42fba6fa12..d5dde9984d 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/CorsEndpointProperties.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/CorsEndpointProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 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. @@ -37,15 +37,18 @@ import org.springframework.web.cors.CorsConfiguration; public class CorsEndpointProperties { /** - * Comma-separated list of origins to allow. '*' allows all origins. When not set, - * CORS support is disabled. When credentials are supported only explicit urls are - * allowed. + * Comma-separated list of origins to allow. '*' allows all origins. When credentials + * are allowed, '*' cannot be used and origin patterns should be configured instead. + * When no allowed origins or allowed origin patterns are set, CORS support is + * disabled. */ private List allowedOrigins = new ArrayList<>(); /** - * Comma-separated list of origins patterns to allow. Must be used when credentials - * are supported and do you want to use wildcard urls. + * Comma-separated list of origin patterns to allow. Unlike allowed origins which only + * supports '*', origin patterns are more flexible (for example + * 'https://*.example.com') and can be used when credentials are allowed. When no + * allowed origin patterns or allowed origins are set, CORS support is disabled. */ private List allowedOriginPatterns = new ArrayList<>(); 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 index 97f47f598b..2cb180b832 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2021 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,6 @@ package org.springframework.boot.actuate.autoconfigure.integrationtest; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.options; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - import java.util.function.Consumer; import org.junit.jupiter.api.Test; @@ -74,6 +71,19 @@ class WebFluxEndpointCorsIntegrationTests { })); } + @Test + void settingAllowedOriginPatternsEnablesCors() { + this.contextRunner + .withPropertyValues("management.endpoints.web.cors.allowed-origin-patterns:*.example.org", + "management.endpoints.web.cors.allow-credentials:true") + .run(withWebTestClient((webTestClient) -> { + webTestClient.options().uri("/actuator/beans").header("Origin", "spring.example.com") + .header(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "GET").exchange().expectStatus() + .isForbidden(); + performAcceptedCorsRequest(webTestClient, "/actuator/beans"); + })); + } + @Test void maxAgeDefaultsTo30Minutes() { this.contextRunner.withPropertyValues("management.endpoints.web.cors.allowed-origins:spring.example.org") @@ -148,29 +158,6 @@ class WebFluxEndpointCorsIntegrationTests { .expectHeader().doesNotExist(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS))); } - @Test - void settingAllowedOriginsPattern() { - this.contextRunner - .withPropertyValues("management.endpoints.web.cors.allowed-origin-patterns:*.example.com", - "management.endpoints.web.cors.allow-credentials:true") - .run(withWebTestClient((webTestClient) -> webTestClient.options().uri("/actuator/beans") - .header("Origin", "spring.example.com") - .header(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "HEAD").exchange().expectStatus().isOk() - .expectHeader().valueEquals(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS, "GET,HEAD"))); - } - - @Test - void requestsWithDisallowedOriginPatternsAreRejected() { - this.contextRunner - .withPropertyValues("management.endpoints.web.cors.allowed-origin-patterns:*.example.com", - "management.endpoints.web.cors.allow-credentials:true") - .run(withWebTestClient((webTestClient) -> webTestClient.options().uri("/actuator/beans") - .header("Origin", "spring.example.org") - .header(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "HEAD").exchange().expectStatus() - .isForbidden())); - - } - private ContextConsumer withWebTestClient(Consumer webTestClient) { return (context) -> webTestClient.accept(WebTestClient.bindToApplicationContext(context).configureClient() .baseUrl("https://spring.example.org").build()); 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 cc82302656..0d063be271 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 @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 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. @@ -77,6 +77,17 @@ class WebMvcEndpointCorsIntegrationTests { })); } + @Test + void settingAllowedOriginPatternsEnablesCors() { + this.contextRunner.withPropertyValues("management.endpoints.web.cors.allowed-origin-patterns:*.example.com", + "management.endpoints.web.cors.allow-credentials:true").run(withMockMvc((mockMvc) -> { + mockMvc.perform(options("/actuator/beans").header("Origin", "bar.example.org") + .header(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "GET")) + .andExpect(status().isForbidden()); + performAcceptedCorsRequest(mockMvc); + })); + } + @Test void maxAgeDefaultsTo30Minutes() { this.contextRunner.withPropertyValues("management.endpoints.web.cors.allowed-origins:foo.example.com") @@ -156,27 +167,6 @@ class WebMvcEndpointCorsIntegrationTests { .andExpect(header().doesNotExist(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS)))); } - @Test - void settingAllowedOriginsPattern() { - this.contextRunner.withPropertyValues("management.endpoints.web.cors.allowed-origin-patterns:*.example.com", - "management.endpoints.web.cors.allow-credentials:true").run(withMockMvc((mockMvc) -> { - mockMvc.perform(options("/actuator/beans").header("Origin", "bar.example.com") - .header(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "GET")).andExpect(status().isOk()); - performAcceptedCorsRequest(mockMvc); - })); - } - - @Test - void requestsWithDisallowedOriginPatternsAreRejected() { - this.contextRunner.withPropertyValues("management.endpoints.web.cors.allowed-origin-patterns:*.example.com", - "management.endpoints.web.cors.allow-credentials:true").run(withMockMvc((mockMvc) -> { - mockMvc.perform(options("/actuator/beans").header("Origin", "bar.domain.com") - .header(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "GET")) - .andExpect(status().isForbidden()); - performAcceptedCorsRequest(mockMvc); - })); - } - private ContextConsumer withMockMvc(MockMvcConsumer mockMvc) { return (context) -> mockMvc.accept(MockMvcBuilders.webAppContextSetup(context).build()); }