mirror of
https://github.com/spring-projects/spring-boot.git
synced 2026-09-17 12:09:16 +00:00
Merge branch '4.1.x'
Closes gh-51617
This commit is contained in:
+19
-35
@@ -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,25 @@ 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.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 +134,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 +159,21 @@ public final class CloudFoundryReactiveActuatorAutoConfiguration {
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ConditionalOnClass(MatcherSecurityWebFilterChain.class)
|
||||
static class IgnoredPathsSecurityConfiguration {
|
||||
@ConditionalOnClass({ ServerHttpSecurity.class, SecurityWebFilterChain.class, WebFilterChainProxy.class })
|
||||
static class PermitAllCloudFoundrySecurityConfiguration {
|
||||
|
||||
private static final int FILTER_CHAIN_ORDER = Ordered.HIGHEST_PRECEDENCE;
|
||||
|
||||
@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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+81
-7
@@ -42,6 +42,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,27 +67,35 @@ 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 {
|
||||
|
||||
@@ -186,8 +195,9 @@ class CloudFoundryReactiveActuatorAutoConfigurationTests {
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void cloudFoundryPathsIgnoredBySpringSecurity() {
|
||||
this.contextRunner.withBean(TestEndpoint.class, TestEndpoint::new)
|
||||
void cloudFoundryPathsPermittedBySpringSecurity() {
|
||||
this.contextRunner.withUserConfiguration(SecurityConfiguration.class)
|
||||
.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) -> {
|
||||
@@ -206,14 +216,64 @@ 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<? extends SecurityWebFilterChain> filters, String urlTemplate) {
|
||||
return filters.get(0)
|
||||
.matches(MockServerWebExchange.from(MockServerHttpRequest.get(urlTemplate).build()))
|
||||
@@ -257,7 +317,7 @@ class CloudFoundryReactiveActuatorAutoConfigurationTests {
|
||||
.filter((candidate) -> EndpointId.of("test").equals(candidate.getEndpointId()))
|
||||
.findFirst()
|
||||
.get();
|
||||
assertThat(endpoint.getOperations()).hasSize(1);
|
||||
assertThat(endpoint.getOperations()).hasSize(2);
|
||||
WebOperation operation = endpoint.getOperations().iterator().next();
|
||||
assertThat(operation.getRequestPredicate().getPath()).isEqualTo("test");
|
||||
});
|
||||
@@ -387,6 +447,10 @@ class CloudFoundryReactiveActuatorAutoConfigurationTests {
|
||||
return "hello world";
|
||||
}
|
||||
|
||||
@WriteOperation
|
||||
void update(String name) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@@ -400,4 +464,14 @@ class CloudFoundryReactiveActuatorAutoConfigurationTests {
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class SecurityConfiguration {
|
||||
|
||||
@Bean
|
||||
SecurityWebFilterChain appSecurity(ServerHttpSecurity httpSecurity) {
|
||||
return httpSecurity.authorizeExchange((spec) -> spec.anyExchange().denyAll()).build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user