Skip to content

Commit

Permalink
Add config prop for endpoints' CORS allowed origin patterns
Browse files Browse the repository at this point in the history
  • Loading branch information
Pedro Ivo Machado authored and wilkinsona committed Jan 19, 2021
1 parent 743343c commit d7f891b
Show file tree
Hide file tree
Showing 3 changed files with 65 additions and 2 deletions.
Expand Up @@ -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<String> 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<String> allowedOriginPatterns = new ArrayList<>();

/**
* Comma-separated list of methods to allow. '*' allows all methods. When not set,
* defaults to GET.
Expand Down Expand Up @@ -78,6 +85,14 @@ public void setAllowedOrigins(List<String> allowedOrigins) {
this.allowedOrigins = allowedOrigins;
}

public List<String> getAllowedOriginPatterns() {
return this.allowedOriginPatterns;
}

public void setAllowedOriginPatterns(List<String> allowedOriginPatterns) {
this.allowedOriginPatterns = allowedOriginPatterns;
}

public List<String> getAllowedMethods() {
return this.allowedMethods;
}
Expand Down Expand Up @@ -119,12 +134,13 @@ public void setMaxAge(Duration maxAge) {
}

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);
Expand Down
Expand Up @@ -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;
Expand Down Expand Up @@ -145,6 +148,29 @@ void credentialsCanBeDisabled() {
.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<ReactiveWebApplicationContext> withWebTestClient(Consumer<WebTestClient> webTestClient) {
return (context) -> webTestClient.accept(WebTestClient.bindToApplicationContext(context).configureClient()
.baseUrl("https://spring.example.org").build());
Expand Down
Expand Up @@ -156,6 +156,27 @@ void credentialsCanBeDisabled() {
.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<WebApplicationContext> withMockMvc(MockMvcConsumer mockMvc) {
return (context) -> mockMvc.accept(MockMvcBuilders.webAppContextSetup(context).build());
}
Expand Down

0 comments on commit d7f891b

Please sign in to comment.