diff --git a/module/spring-boot-cloudfoundry/src/main/java/org/springframework/boot/cloudfoundry/autoconfigure/actuate/endpoint/reactive/CloudFoundryReactiveActuatorAutoConfiguration.java b/module/spring-boot-cloudfoundry/src/main/java/org/springframework/boot/cloudfoundry/autoconfigure/actuate/endpoint/reactive/CloudFoundryReactiveActuatorAutoConfiguration.java index 696b8804607..2ca73137091 100644 --- a/module/spring-boot-cloudfoundry/src/main/java/org/springframework/boot/cloudfoundry/autoconfigure/actuate/endpoint/reactive/CloudFoundryReactiveActuatorAutoConfiguration.java +++ b/module/spring-boot-cloudfoundry/src/main/java/org/springframework/boot/cloudfoundry/autoconfigure/actuate/endpoint/reactive/CloudFoundryReactiveActuatorAutoConfiguration.java @@ -24,7 +24,9 @@ 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; @@ -54,25 +56,22 @@ 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.Ordered; -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.config.web.server.ServerHttpSecurity; -import org.springframework.security.web.server.SecurityWebFilterChain; +import org.springframework.security.web.server.MatcherSecurityWebFilterChain; 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, @@ -134,7 +133,7 @@ public final class CloudFoundryReactiveActuatorAutoConfiguration { ? new SecurityService(webClientBuilder, cloudControllerUrl, skipSslValidation) : null; } - private static CorsConfiguration getCorsConfiguration() { + private CorsConfiguration getCorsConfiguration() { CorsConfiguration corsConfiguration = new CorsConfiguration(); corsConfiguration.addAllowedOrigin(CorsConfiguration.ALL); corsConfiguration.setAllowedMethods(Arrays.asList(HttpMethod.GET.name(), HttpMethod.POST.name())); @@ -159,21 +158,38 @@ public final class CloudFoundryReactiveActuatorAutoConfiguration { } @Configuration(proxyBeanMethods = false) - @ConditionalOnClass({ ServerHttpSecurity.class, SecurityWebFilterChain.class, WebFilterChainProxy.class }) - static class PermitAllCloudFoundrySecurityConfiguration { - - private static final int FILTER_CHAIN_ORDER = Ordered.HIGHEST_PRECEDENCE; + @ConditionalOnClass(MatcherSecurityWebFilterChain.class) + static class IgnoredPathsSecurityConfiguration { @Bean - @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(); + 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); } } diff --git a/module/spring-boot-cloudfoundry/src/test/java/org/springframework/boot/cloudfoundry/autoconfigure/actuate/endpoint/reactive/CloudFoundryReactiveActuatorAutoConfigurationTests.java b/module/spring-boot-cloudfoundry/src/test/java/org/springframework/boot/cloudfoundry/autoconfigure/actuate/endpoint/reactive/CloudFoundryReactiveActuatorAutoConfigurationTests.java index 10964be67ff..57f0ce66a0a 100644 --- a/module/spring-boot-cloudfoundry/src/test/java/org/springframework/boot/cloudfoundry/autoconfigure/actuate/endpoint/reactive/CloudFoundryReactiveActuatorAutoConfigurationTests.java +++ b/module/spring-boot-cloudfoundry/src/test/java/org/springframework/boot/cloudfoundry/autoconfigure/actuate/endpoint/reactive/CloudFoundryReactiveActuatorAutoConfigurationTests.java @@ -42,7 +42,6 @@ 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; @@ -67,35 +66,27 @@ 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; -import org.springframework.security.config.web.server.ServerHttpSecurity; import org.springframework.security.core.userdetails.MapReactiveUserDetailsService; import org.springframework.security.core.userdetails.User; 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.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.springSecurity; /** * Tests for {@link CloudFoundryReactiveActuatorAutoConfiguration}. * * @author Madhura Bhave * @author Moritz Halbritter - * @author Aashikant Kumar */ class CloudFoundryReactiveActuatorAutoConfigurationTests { @@ -195,9 +186,8 @@ class CloudFoundryReactiveActuatorAutoConfigurationTests { @Test @SuppressWarnings("unchecked") - void cloudFoundryPathsPermittedBySpringSecurity() { - this.contextRunner.withUserConfiguration(SecurityConfiguration.class) - .withBean(TestEndpoint.class, TestEndpoint::new) + void cloudFoundryPathsIgnoredBySpringSecurity() { + 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") .run((context) -> { @@ -216,64 +206,14 @@ 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.withUserConfiguration(SecurityConfiguration.class) - .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.withUserConfiguration(SecurityConfiguration.class) - .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()) - .configureClient() - .baseUrl("https://app.example.com") - .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 otherPathsRejectedBySpringSecurity() { - this.contextRunner.withUserConfiguration(SecurityConfiguration.class) - .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.get().uri("/test").exchange().expectStatus().isEqualTo(HttpStatus.UNAUTHORIZED); - }); - } - private static @Nullable Boolean getMatches(List filters, String urlTemplate) { return filters.get(0) .matches(MockServerWebExchange.from(MockServerHttpRequest.get(urlTemplate).build())) @@ -317,7 +257,7 @@ class CloudFoundryReactiveActuatorAutoConfigurationTests { .filter((candidate) -> EndpointId.of("test").equals(candidate.getEndpointId())) .findFirst() .get(); - assertThat(endpoint.getOperations()).hasSize(2); + assertThat(endpoint.getOperations()).hasSize(1); WebOperation operation = endpoint.getOperations().iterator().next(); assertThat(operation.getRequestPredicate().getPath()).isEqualTo("test"); }); @@ -447,10 +387,6 @@ class CloudFoundryReactiveActuatorAutoConfigurationTests { return "hello world"; } - @WriteOperation - void update(String name) { - } - } @Configuration(proxyBeanMethods = false) @@ -464,14 +400,4 @@ class CloudFoundryReactiveActuatorAutoConfigurationTests { } - @Configuration(proxyBeanMethods = false) - static class SecurityConfiguration { - - @Bean - SecurityWebFilterChain appSecurity(ServerHttpSecurity httpSecurity) { - return httpSecurity.authorizeExchange((spec) -> spec.anyExchange().denyAll()).build(); - } - - } - }