mirror of
https://github.com/spring-projects/spring-boot.git
synced 2026-09-29 13:39:16 +00:00
Preserve firewall in reactive CF security auto-configuration
Prior to this commit, CloudFoundryReactiveActuatorAutoConfiguration registered a BeanPostProcessor replaced the WebFilterChainProxy bean with one that handled CF security and delegated to the existing chain. Constructing a new WebFilterChainProxy resulted in the loss of any firewall customization on the existing chain as Spring Security does not provide an API to retreive the firewall from the existing chain and apply it to the new chain. This commit changes the approach and aligns it with its Servlet counterpart. Instead of post-processing the filter chain proxy, a new SecurityWebFilterChain that handles cloudfoundryapplication/** is defined. This chain becomes part of the existing WebFilterChainProxy, preserving any firewall customization. Signed-off-by: aashikantkumar <aashikantkumar2@gmail.com> See gh-51549
This commit is contained in:
committed by
Andy Wilkinson
parent
1d79f56ad7
commit
4f3d0349f3
+17
-34
@@ -24,9 +24,7 @@ import java.util.List;
|
||||
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.beans.factory.config.BeanPostProcessor;
|
||||
import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnAvailableEndpoint;
|
||||
import org.springframework.boot.actuate.autoconfigure.info.InfoEndpointAutoConfiguration;
|
||||
import org.springframework.boot.actuate.endpoint.ExposableEndpoint;
|
||||
@@ -56,22 +54,24 @@ import org.springframework.boot.info.GitProperties;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.security.web.server.MatcherSecurityWebFilterChain;
|
||||
import org.springframework.security.config.web.server.ServerHttpSecurity;
|
||||
import org.springframework.security.web.server.SecurityWebFilterChain;
|
||||
import org.springframework.security.web.server.WebFilterChainProxy;
|
||||
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
|
||||
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers;
|
||||
import org.springframework.web.cors.CorsConfiguration;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
import org.springframework.web.server.WebFilter;
|
||||
|
||||
/**
|
||||
* {@link EnableAutoConfiguration Auto-configuration} to expose actuator endpoints for
|
||||
* Cloud Foundry to use in a reactive environment.
|
||||
*
|
||||
* @author Madhura Bhave
|
||||
* @author Aashikant Kumar
|
||||
* @since 4.0.0
|
||||
*/
|
||||
@AutoConfiguration(after = InfoEndpointAutoConfiguration.class,
|
||||
@@ -133,7 +133,7 @@ public final class CloudFoundryReactiveActuatorAutoConfiguration {
|
||||
? new SecurityService(webClientBuilder, cloudControllerUrl, skipSslValidation) : null;
|
||||
}
|
||||
|
||||
private CorsConfiguration getCorsConfiguration() {
|
||||
private static CorsConfiguration getCorsConfiguration() {
|
||||
CorsConfiguration corsConfiguration = new CorsConfiguration();
|
||||
corsConfiguration.addAllowedOrigin(CorsConfiguration.ALL);
|
||||
corsConfiguration.setAllowedMethods(Arrays.asList(HttpMethod.GET.name(), HttpMethod.POST.name()));
|
||||
@@ -158,38 +158,21 @@ public final class CloudFoundryReactiveActuatorAutoConfiguration {
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ConditionalOnClass(MatcherSecurityWebFilterChain.class)
|
||||
@ConditionalOnClass({ ServerHttpSecurity.class, SecurityWebFilterChain.class, WebFilterChainProxy.class })
|
||||
static class IgnoredPathsSecurityConfiguration {
|
||||
|
||||
private static final int FILTER_CHAIN_ORDER = -1;
|
||||
|
||||
@Bean
|
||||
static WebFilterChainPostProcessor webFilterChainPostProcessor() {
|
||||
return new WebFilterChainPostProcessor();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static class WebFilterChainPostProcessor implements BeanPostProcessor {
|
||||
|
||||
WebFilterChainPostProcessor() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
|
||||
if (bean instanceof WebFilterChainProxy webFilterChainProxy) {
|
||||
return postProcess(webFilterChainProxy);
|
||||
}
|
||||
return bean;
|
||||
}
|
||||
|
||||
private WebFilterChainProxy postProcess(WebFilterChainProxy existing) {
|
||||
ServerWebExchangeMatcher cloudFoundryRequestMatcher = ServerWebExchangeMatchers
|
||||
.pathMatchers(BASE_PATH + "/**");
|
||||
WebFilter noOpFilter = (exchange, chain) -> chain.filter(exchange);
|
||||
MatcherSecurityWebFilterChain ignoredRequestFilterChain = new MatcherSecurityWebFilterChain(
|
||||
cloudFoundryRequestMatcher, Collections.singletonList(noOpFilter));
|
||||
MatcherSecurityWebFilterChain allRequestsFilterChain = new MatcherSecurityWebFilterChain(
|
||||
ServerWebExchangeMatchers.anyExchange(), Collections.singletonList(existing));
|
||||
return new WebFilterChainProxy(ignoredRequestFilterChain, allRequestsFilterChain);
|
||||
@Order(FILTER_CHAIN_ORDER)
|
||||
SecurityWebFilterChain cloudFoundrySecurityWebFilterChain(ServerHttpSecurity http) {
|
||||
ServerWebExchangeMatcher cloudFoundryRequest = ServerWebExchangeMatchers.pathMatchers(BASE_PATH + "/**");
|
||||
http.securityMatcher(cloudFoundryRequest);
|
||||
http.authorizeExchange((exchanges) -> exchanges.anyExchange().permitAll());
|
||||
http.csrf((csrf) -> csrf.disable());
|
||||
CorsConfiguration corsConfiguration = getCorsConfiguration();
|
||||
http.cors((cors) -> cors.configurationSource((exchange) -> corsConfiguration));
|
||||
return http.build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+69
-5
@@ -31,6 +31,7 @@ import org.assertj.core.api.InstanceOfAssertFactories;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.netty.http.HttpResources;
|
||||
|
||||
import org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration;
|
||||
@@ -42,6 +43,7 @@ import org.springframework.boot.actuate.endpoint.ApiVersion;
|
||||
import org.springframework.boot.actuate.endpoint.EndpointId;
|
||||
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
|
||||
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
|
||||
import org.springframework.boot.actuate.endpoint.annotation.WriteOperation;
|
||||
import org.springframework.boot.actuate.endpoint.web.ExposableWebEndpoint;
|
||||
import org.springframework.boot.actuate.endpoint.web.WebOperation;
|
||||
import org.springframework.boot.actuate.endpoint.web.WebOperationRequestPredicate;
|
||||
@@ -66,8 +68,11 @@ import org.springframework.boot.webflux.autoconfigure.WebFluxAutoConfiguration;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.HttpStatusCode;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
|
||||
import org.springframework.mock.web.server.MockServerWebExchange;
|
||||
@@ -77,16 +82,21 @@ import org.springframework.security.web.server.SecurityWebFilterChain;
|
||||
import org.springframework.security.web.server.WebFilterChainProxy;
|
||||
import org.springframework.test.web.reactive.server.WebTestClient;
|
||||
import org.springframework.web.cors.CorsConfiguration;
|
||||
import org.springframework.web.cors.reactive.CorsConfigurationSource;
|
||||
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.springSecurity;
|
||||
|
||||
/**
|
||||
* Tests for {@link CloudFoundryReactiveActuatorAutoConfiguration}.
|
||||
*
|
||||
* @author Madhura Bhave
|
||||
* @author Moritz Halbritter
|
||||
* @author Aashikant Kumar
|
||||
*/
|
||||
class CloudFoundryReactiveActuatorAutoConfigurationTests {
|
||||
|
||||
@@ -186,7 +196,7 @@ class CloudFoundryReactiveActuatorAutoConfigurationTests {
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void cloudFoundryPathsIgnoredBySpringSecurity() {
|
||||
void cloudFoundryPathsPermittedBySpringSecurity() {
|
||||
this.contextRunner.withBean(TestEndpoint.class, TestEndpoint::new)
|
||||
.withPropertyValues("VCAP_APPLICATION:---", "vcap.application.application_id:my-app-id",
|
||||
"vcap.application.cf_api:https://my-cloud-controller.com")
|
||||
@@ -206,10 +216,60 @@ class CloudFoundryReactiveActuatorAutoConfigurationTests {
|
||||
assertThat(cfRequestWithAdditionalPathMatches).isTrue();
|
||||
assertThat(otherCfRequestMatches).isTrue();
|
||||
assertThat(otherRequestMatches).isFalse();
|
||||
otherRequestMatches = filters.get(1)
|
||||
.matches(MockServerWebExchange.from(MockServerHttpRequest.get("/some-other-path").build()))
|
||||
.block(Duration.ofSeconds(30));
|
||||
assertThat(otherRequestMatches).isTrue();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void cloudFoundryPathsPermittedWithCsrfBySpringSecurity() {
|
||||
this.contextRunner.withBean(TestEndpoint.class, TestEndpoint::new)
|
||||
.withPropertyValues("VCAP_APPLICATION:---", "vcap.application.application_id:my-app-id")
|
||||
.run((context) -> {
|
||||
WebTestClient client = WebTestClient.bindToApplicationContext(context).apply(springSecurity()).build();
|
||||
client.post()
|
||||
.uri(BASE_PATH + "/test?name=test")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isEqualTo(HttpStatus.SERVICE_UNAVAILABLE);
|
||||
// If CSRF fails we'll get a 403, if it works we get service unavailable
|
||||
// because of "Cloud controller URL is not available"
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void crossOriginRequestToCloudFoundryPathsPermittedBySpringSecurity() {
|
||||
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
||||
source.registerCorsConfiguration("/**", new CorsConfiguration());
|
||||
this.contextRunner.withBean(TestEndpoint.class, TestEndpoint::new)
|
||||
.withBean("corsConfigurationSource", CorsConfigurationSource.class, () -> source)
|
||||
.withPropertyValues("VCAP_APPLICATION:---", "vcap.application.application_id:my-app-id")
|
||||
.run((context) -> {
|
||||
WebTestClient client = WebTestClient.bindToApplicationContext(context).apply(springSecurity()).build();
|
||||
client.get()
|
||||
.uri(BASE_PATH + "/test")
|
||||
.header(HttpHeaders.ORIGIN, "elsewhere.example.com")
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isEqualTo(HttpStatus.SERVICE_UNAVAILABLE);
|
||||
// If CORS fails we'll get a 403, if it works we get service unavailable
|
||||
// because of "Cloud controller URL is not available"
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void userSecurityWebFilterChainIsPreserved() {
|
||||
SecurityWebFilterChain userChain = mock(SecurityWebFilterChain.class);
|
||||
this.contextRunner.withBean(SecurityWebFilterChain.class, () -> userChain)
|
||||
.withPropertyValues("VCAP_APPLICATION:---", "vcap.application.application_id:my-app-id",
|
||||
"vcap.application.cf_api:https://my-cloud-controller.com")
|
||||
.run((context) -> {
|
||||
assertThat(context.getBean(WebFilterChainProxy.class))
|
||||
.extracting("filters", InstanceOfAssertFactories.list(SecurityWebFilterChain.class))
|
||||
.hasSize(2)
|
||||
.satisfies((filters) -> {
|
||||
assertThat(getMatches(filters, BASE_PATH)).isTrue();
|
||||
assertThat(filters.get(1)).isSameAs(userChain);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -387,6 +447,10 @@ class CloudFoundryReactiveActuatorAutoConfigurationTests {
|
||||
return "hello world";
|
||||
}
|
||||
|
||||
@WriteOperation
|
||||
void update(String name) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
|
||||
Reference in New Issue
Block a user