Add CORS support for Private Network Access

This commit adds CORS support for Private Network Access
by adding an Access-Control-Allow-Private-Network response
header when the preflight request is sent with an
Access-Control-Request-Private-Network header and that
Private Network Access has been enabled in the CORS
configuration.

See https://developer.chrome.com/blog/private-network-access-preflight/
for more details.

Closes gh-28546
This commit is contained in:
Sébastien Deleuze
2024-01-04 21:38:59 +01:00
parent d7cfdc633a
commit 318d460256
18 changed files with 328 additions and 15 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2023 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.
@@ -50,6 +50,8 @@ class CorsConfigurationTests {
assertThat(config.getExposedHeaders()).isNull();
config.setAllowCredentials(null);
assertThat(config.getAllowCredentials()).isNull();
config.setAllowPrivateNetwork(null);
assertThat(config.getAllowPrivateNetwork()).isNull();
config.setMaxAge((Long) null);
assertThat(config.getMaxAge()).isNull();
}
@@ -63,6 +65,7 @@ class CorsConfigurationTests {
config.addAllowedMethod("*");
config.addExposedHeader("*");
config.setAllowCredentials(true);
config.setAllowPrivateNetwork(true);
config.setMaxAge(123L);
assertThat(config.getAllowedOrigins()).containsExactly("*");
@@ -71,6 +74,7 @@ class CorsConfigurationTests {
assertThat(config.getAllowedMethods()).containsExactly("*");
assertThat(config.getExposedHeaders()).containsExactly("*");
assertThat(config.getAllowCredentials()).isTrue();
assertThat(config.getAllowPrivateNetwork()).isTrue();
assertThat(config.getMaxAge()).isEqualTo(Long.valueOf(123));
}
@@ -93,6 +97,7 @@ class CorsConfigurationTests {
config.addAllowedMethod(HttpMethod.GET.name());
config.setMaxAge(123L);
config.setAllowCredentials(true);
config.setAllowPrivateNetwork(true);
CorsConfiguration other = new CorsConfiguration();
config = config.combine(other);
@@ -105,6 +110,7 @@ class CorsConfigurationTests {
assertThat(config.getAllowedMethods()).containsExactly(HttpMethod.GET.name());
assertThat(config.getMaxAge()).isEqualTo(Long.valueOf(123));
assertThat(config.getAllowCredentials()).isTrue();
assertThat(config.getAllowPrivateNetwork()).isTrue();
}
@Test // SPR-15772
@@ -258,6 +264,7 @@ class CorsConfigurationTests {
config.addAllowedMethod(HttpMethod.GET.name());
config.setMaxAge(123L);
config.setAllowCredentials(true);
config.setAllowPrivateNetwork(true);
CorsConfiguration other = new CorsConfiguration();
other.addAllowedOrigin("https://domain2.com");
@@ -267,6 +274,7 @@ class CorsConfigurationTests {
other.addAllowedMethod(HttpMethod.PUT.name());
other.setMaxAge(456L);
other.setAllowCredentials(false);
other.setAllowPrivateNetwork(false);
config = config.combine(other);
assertThat(config).isNotNull();
@@ -277,6 +285,7 @@ class CorsConfigurationTests {
assertThat(config.getMaxAge()).isEqualTo(Long.valueOf(456));
assertThat(config).isNotNull();
assertThat(config.getAllowCredentials()).isFalse();
assertThat(config.getAllowPrivateNetwork()).isFalse();
assertThat(config.getAllowedOriginPatterns()).containsExactly("http://*.domain1.com", "http://*.domain2.com");
}

View File

@@ -351,6 +351,32 @@ public class DefaultCorsProcessorTests {
assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_OK);
}
@Test
public void preflightRequestPrivateNetworkWithWildcardOrigin() throws Exception {
this.request.setMethod(HttpMethod.OPTIONS.name());
this.request.addHeader(HttpHeaders.ORIGIN, "https://domain2.com");
this.request.addHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "GET");
this.request.addHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS, "Header1");
this.request.addHeader(DefaultCorsProcessor.ACCESS_CONTROL_REQUEST_PRIVATE_NETWORK, "true");
this.conf.setAllowedOrigins(Arrays.asList("https://domain1.com", "*", "http://domain3.example"));
this.conf.addAllowedHeader("Header1");
this.conf.setAllowPrivateNetwork(true);
assertThatIllegalArgumentException().isThrownBy(() ->
this.processor.processRequest(this.conf, this.request, this.response));
this.conf.setAllowedOrigins(null);
this.conf.addAllowedOriginPattern("*");
this.processor.processRequest(this.conf, this.request, this.response);
assertThat(this.response.containsHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue();
assertThat(this.response.containsHeader(DefaultCorsProcessor.ACCESS_CONTROL_ALLOW_PRIVATE_NETWORK)).isTrue();
assertThat(this.response.getHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)).isEqualTo("https://domain2.com");
assertThat(this.response.getHeaders(HttpHeaders.VARY)).contains(HttpHeaders.ORIGIN,
HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS);
assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_OK);
}
@Test
public void preflightRequestAllowedHeaders() throws Exception {
this.request.setMethod(HttpMethod.OPTIONS.name());
@@ -434,4 +460,49 @@ public class DefaultCorsProcessorTests {
HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS);
}
@Test
public void preflightRequestWithoutAccessControlRequestPrivateNetwork() throws Exception {
this.request.setMethod(HttpMethod.OPTIONS.name());
this.request.addHeader(HttpHeaders.ORIGIN, "https://domain2.com");
this.request.addHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "GET");
this.conf.addAllowedHeader("*");
this.conf.addAllowedOrigin("https://domain2.com");
this.processor.processRequest(this.conf, this.request, this.response);
assertThat(this.response.containsHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue();
assertThat(this.response.containsHeader(DefaultCorsProcessor.ACCESS_CONTROL_ALLOW_PRIVATE_NETWORK)).isFalse();
assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_OK);
}
@Test
public void preflightRequestWithAccessControlRequestPrivateNetworkNotAllowed() throws Exception {
this.request.setMethod(HttpMethod.OPTIONS.name());
this.request.addHeader(HttpHeaders.ORIGIN, "https://domain2.com");
this.request.addHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "GET");
this.request.addHeader(DefaultCorsProcessor.ACCESS_CONTROL_REQUEST_PRIVATE_NETWORK, "true");
this.conf.addAllowedHeader("*");
this.conf.addAllowedOrigin("https://domain2.com");
this.processor.processRequest(this.conf, this.request, this.response);
assertThat(this.response.containsHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue();
assertThat(this.response.containsHeader(DefaultCorsProcessor.ACCESS_CONTROL_ALLOW_PRIVATE_NETWORK)).isFalse();
assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_OK);
}
@Test
public void preflightRequestWithAccessControlRequestPrivateNetworkAllowed() throws Exception {
this.request.setMethod(HttpMethod.OPTIONS.name());
this.request.addHeader(HttpHeaders.ORIGIN, "https://domain2.com");
this.request.addHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "GET");
this.request.addHeader(DefaultCorsProcessor.ACCESS_CONTROL_REQUEST_PRIVATE_NETWORK, "true");
this.conf.addAllowedHeader("*");
this.conf.addAllowedOrigin("https://domain2.com");
this.conf.setAllowPrivateNetwork(true);
this.processor.processRequest(this.conf, this.request, this.response);
assertThat(this.response.containsHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue();
assertThat(this.response.containsHeader(DefaultCorsProcessor.ACCESS_CONTROL_ALLOW_PRIVATE_NETWORK)).isTrue();
assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_OK);
}
}

View File

@@ -364,6 +364,33 @@ public class DefaultCorsProcessorTests {
assertThat(response.getStatusCode()).isNull();
}
@Test
public void preflightRequestPrivateNetworkWithWildcardOrigin() {
ServerWebExchange exchange = MockServerWebExchange.from(preFlightRequest()
.header(ACCESS_CONTROL_REQUEST_METHOD, "GET")
.header(ACCESS_CONTROL_REQUEST_HEADERS, "Header1")
.header(DefaultCorsProcessor.ACCESS_CONTROL_REQUEST_PRIVATE_NETWORK, "true"));
this.conf.addAllowedOrigin("https://domain1.com");
this.conf.addAllowedOrigin("*");
this.conf.addAllowedOrigin("http://domain3.example");
this.conf.addAllowedHeader("Header1");
this.conf.setAllowPrivateNetwork(true);
assertThatIllegalArgumentException().isThrownBy(() -> this.processor.process(this.conf, exchange));
this.conf.setAllowedOrigins(null);
this.conf.addAllowedOriginPattern("*");
this.processor.process(this.conf, exchange);
ServerHttpResponse response = exchange.getResponse();
assertThat(response.getHeaders().containsKey(ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue();
assertThat(response.getHeaders().containsKey(DefaultCorsProcessor.ACCESS_CONTROL_ALLOW_PRIVATE_NETWORK)).isTrue();
assertThat(response.getHeaders().getFirst(ACCESS_CONTROL_ALLOW_ORIGIN)).isEqualTo("https://domain2.com");
assertThat(response.getHeaders().get(VARY)).contains(ORIGIN,
ACCESS_CONTROL_REQUEST_METHOD, ACCESS_CONTROL_REQUEST_HEADERS);
assertThat(response.getStatusCode()).isNull();
}
@Test
public void preflightRequestAllowedHeaders() {
ServerWebExchange exchange = MockServerWebExchange.from(preFlightRequest()
@@ -460,6 +487,57 @@ public class DefaultCorsProcessorTests {
ACCESS_CONTROL_REQUEST_METHOD, ACCESS_CONTROL_REQUEST_HEADERS);
}
@Test
public void preflightRequestWithoutAccessControlRequestPrivateNetwork() {
ServerWebExchange exchange = MockServerWebExchange.from(preFlightRequest()
.header(ACCESS_CONTROL_REQUEST_METHOD, "GET"));
this.conf.addAllowedHeader("*");
this.conf.addAllowedOrigin("https://domain2.com");
this.processor.process(this.conf, exchange);
ServerHttpResponse response = exchange.getResponse();
assertThat(response.getHeaders().containsKey(ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue();
assertThat(response.getHeaders().containsKey(DefaultCorsProcessor.ACCESS_CONTROL_ALLOW_PRIVATE_NETWORK)).isFalse();
assertThat(response.getStatusCode()).isNull();
}
@Test
public void preflightRequestWithAccessControlRequestPrivateNetworkNotAllowed() {
ServerWebExchange exchange = MockServerWebExchange.from(preFlightRequest()
.header(ACCESS_CONTROL_REQUEST_METHOD, "GET")
.header(DefaultCorsProcessor.ACCESS_CONTROL_REQUEST_PRIVATE_NETWORK, "true"));
this.conf.addAllowedHeader("*");
this.conf.addAllowedOrigin("https://domain2.com");
this.processor.process(this.conf, exchange);
ServerHttpResponse response = exchange.getResponse();
assertThat(response.getHeaders().containsKey(ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue();
assertThat(response.getHeaders().containsKey(DefaultCorsProcessor.ACCESS_CONTROL_ALLOW_PRIVATE_NETWORK)).isFalse();
assertThat(response.getStatusCode()).isNull();
}
@Test
public void preflightRequestWithAccessControlRequestPrivateNetworkAllowed() {
ServerWebExchange exchange = MockServerWebExchange.from(preFlightRequest()
.header(ACCESS_CONTROL_REQUEST_METHOD, "GET")
.header(DefaultCorsProcessor.ACCESS_CONTROL_REQUEST_PRIVATE_NETWORK, "true"));
this.conf.addAllowedHeader("*");
this.conf.addAllowedOrigin("https://domain2.com");
this.conf.setAllowPrivateNetwork(true);
this.processor.process(this.conf, exchange);
ServerHttpResponse response = exchange.getResponse();
assertThat(response.getHeaders().containsKey(ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue();
assertThat(response.getHeaders().containsKey(DefaultCorsProcessor.ACCESS_CONTROL_ALLOW_PRIVATE_NETWORK)).isTrue();
assertThat(response.getStatusCode()).isNull();
}
private ServerWebExchange actualRequest() {
return MockServerWebExchange.from(corsRequest(HttpMethod.GET));