mirror of
https://github.com/spring-projects/spring-boot.git
synced 2026-10-06 01:09:02 +00:00
Merge branch '3.5.x' into 4.0.x
Closes gh-49646
This commit is contained in:
+4
-21
@@ -21,7 +21,6 @@ import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
@@ -35,7 +34,6 @@ import org.springframework.boot.actuate.endpoint.invoke.ParameterValueMapper;
|
||||
import org.springframework.boot.actuate.endpoint.web.EndpointMapping;
|
||||
import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes;
|
||||
import org.springframework.boot.actuate.endpoint.web.ExposableWebEndpoint;
|
||||
import org.springframework.boot.actuate.endpoint.web.PathMappedEndpoints;
|
||||
import org.springframework.boot.actuate.info.GitInfoContributor;
|
||||
import org.springframework.boot.actuate.info.InfoContributor;
|
||||
import org.springframework.boot.actuate.info.InfoEndpoint;
|
||||
@@ -65,7 +63,6 @@ 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.util.function.SingletonSupplier;
|
||||
import org.springframework.web.cors.CorsConfiguration;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
import org.springframework.web.server.WebFilter;
|
||||
@@ -159,20 +156,15 @@ public final class CloudFoundryReactiveActuatorAutoConfiguration {
|
||||
static class IgnoredPathsSecurityConfiguration {
|
||||
|
||||
@Bean
|
||||
static WebFilterChainPostProcessor webFilterChainPostProcessor(
|
||||
ObjectProvider<CloudFoundryWebFluxEndpointHandlerMapping> handlerMapping) {
|
||||
return new WebFilterChainPostProcessor(handlerMapping);
|
||||
static WebFilterChainPostProcessor webFilterChainPostProcessor() {
|
||||
return new WebFilterChainPostProcessor();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static class WebFilterChainPostProcessor implements BeanPostProcessor {
|
||||
|
||||
private final Supplier<PathMappedEndpoints> pathMappedEndpoints;
|
||||
|
||||
WebFilterChainPostProcessor(ObjectProvider<CloudFoundryWebFluxEndpointHandlerMapping> handlerMapping) {
|
||||
this.pathMappedEndpoints = SingletonSupplier
|
||||
.of(() -> new PathMappedEndpoints(BASE_PATH, () -> handlerMapping.getObject().getAllEndpoints()));
|
||||
WebFilterChainPostProcessor() {
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -184,9 +176,8 @@ public final class CloudFoundryReactiveActuatorAutoConfiguration {
|
||||
}
|
||||
|
||||
private WebFilterChainProxy postProcess(WebFilterChainProxy existing) {
|
||||
List<String> paths = getPaths(this.pathMappedEndpoints.get());
|
||||
ServerWebExchangeMatcher cloudFoundryRequestMatcher = ServerWebExchangeMatchers
|
||||
.pathMatchers(paths.toArray(new String[] {}));
|
||||
.pathMatchers(BASE_PATH + "/**");
|
||||
WebFilter noOpFilter = (exchange, chain) -> chain.filter(exchange);
|
||||
MatcherSecurityWebFilterChain ignoredRequestFilterChain = new MatcherSecurityWebFilterChain(
|
||||
cloudFoundryRequestMatcher, Collections.singletonList(noOpFilter));
|
||||
@@ -195,14 +186,6 @@ public final class CloudFoundryReactiveActuatorAutoConfiguration {
|
||||
return new WebFilterChainProxy(ignoredRequestFilterChain, allRequestsFilterChain);
|
||||
}
|
||||
|
||||
private static List<String> getPaths(PathMappedEndpoints pathMappedEndpoints) {
|
||||
List<String> paths = new ArrayList<>();
|
||||
pathMappedEndpoints.getAllPaths().forEach((path) -> paths.add(path + "/**"));
|
||||
paths.add(BASE_PATH);
|
||||
paths.add(BASE_PATH + "/");
|
||||
return paths;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+6
@@ -78,6 +78,12 @@ class CloudFoundryWebFluxEndpointHandlerMapping extends AbstractWebFluxEndpointH
|
||||
this.securityInterceptor = securityInterceptor;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initHandlerMethods() {
|
||||
super.initHandlerMethods();
|
||||
registerCatchAllMapping(HttpStatus.FORBIDDEN);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ReactiveWebOperation wrapReactiveWebOperation(ExposableWebEndpoint endpoint, WebOperation operation,
|
||||
ReactiveWebOperation reactiveWebOperation) {
|
||||
|
||||
+4
-12
@@ -32,7 +32,6 @@ import org.springframework.boot.actuate.endpoint.invoke.ParameterValueMapper;
|
||||
import org.springframework.boot.actuate.endpoint.web.EndpointMapping;
|
||||
import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes;
|
||||
import org.springframework.boot.actuate.endpoint.web.ExposableWebEndpoint;
|
||||
import org.springframework.boot.actuate.endpoint.web.PathMappedEndpoints;
|
||||
import org.springframework.boot.actuate.info.GitInfoContributor;
|
||||
import org.springframework.boot.actuate.info.InfoContributor;
|
||||
import org.springframework.boot.actuate.info.InfoEndpoint;
|
||||
@@ -64,7 +63,6 @@ import org.springframework.security.config.annotation.web.builders.WebSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.OrRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
import org.springframework.web.client.RestClient;
|
||||
import org.springframework.web.cors.CorsConfiguration;
|
||||
@@ -172,22 +170,16 @@ public final class CloudFoundryActuatorAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
@Order(FILTER_CHAIN_ORDER)
|
||||
SecurityFilterChain cloudFoundrySecurityFilterChain(HttpSecurity http,
|
||||
CloudFoundryWebEndpointServletHandlerMapping handlerMapping) {
|
||||
RequestMatcher cloudFoundryRequest = getRequestMatcher(handlerMapping);
|
||||
SecurityFilterChain cloudFoundrySecurityFilterChain(HttpSecurity http) throws Exception {
|
||||
RequestMatcher cloudFoundryRequest = getRequestMatcher();
|
||||
http.csrf((csrf) -> csrf.ignoringRequestMatchers(cloudFoundryRequest));
|
||||
http.securityMatchers((matches) -> matches.requestMatchers(cloudFoundryRequest))
|
||||
.authorizeHttpRequests((authorize) -> authorize.anyRequest().permitAll());
|
||||
return http.build();
|
||||
}
|
||||
|
||||
private RequestMatcher getRequestMatcher(CloudFoundryWebEndpointServletHandlerMapping handlerMapping) {
|
||||
PathMappedEndpoints endpoints = new PathMappedEndpoints(BASE_PATH, handlerMapping::getAllEndpoints);
|
||||
List<RequestMatcher> matchers = new ArrayList<>();
|
||||
endpoints.getAllPaths().forEach((path) -> matchers.add(pathMatcher(path + "/**")));
|
||||
matchers.add(pathMatcher(BASE_PATH));
|
||||
matchers.add(pathMatcher(BASE_PATH + "/"));
|
||||
return new OrRequestMatcher(matchers);
|
||||
private RequestMatcher getRequestMatcher() {
|
||||
return pathMatcher(BASE_PATH + "/**");
|
||||
}
|
||||
|
||||
private PathPatternRequestMatcher pathMatcher(String path) {
|
||||
|
||||
+6
@@ -81,6 +81,12 @@ class CloudFoundryWebEndpointServletHandlerMapping extends AbstractWebMvcEndpoin
|
||||
this.allEndpoints = allEndpoints;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initHandlerMethods() {
|
||||
super.initHandlerMethods();
|
||||
registerCatchAllMapping(HttpStatus.FORBIDDEN);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ServletWebOperation wrapServletWebOperation(ExposableWebEndpoint endpoint, WebOperation operation,
|
||||
ServletWebOperation servletWebOperation) {
|
||||
|
||||
+1
-1
@@ -194,7 +194,7 @@ class CloudFoundryReactiveActuatorAutoConfigurationTests {
|
||||
assertThat(cfBaseWithTrailingSlashRequestMatches).isTrue();
|
||||
assertThat(cfRequestMatches).isTrue();
|
||||
assertThat(cfRequestWithAdditionalPathMatches).isTrue();
|
||||
assertThat(otherCfRequestMatches).isFalse();
|
||||
assertThat(otherCfRequestMatches).isTrue();
|
||||
assertThat(otherRequestMatches).isFalse();
|
||||
otherRequestMatches = filters.get(1)
|
||||
.matches(MockServerWebExchange.from(MockServerHttpRequest.get("/some-other-path").build()))
|
||||
|
||||
+6
@@ -212,6 +212,12 @@ class CloudFoundryWebFluxEndpointIntegrationTests {
|
||||
.doesNotExist()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void unknownEndpointsAreForbidden() {
|
||||
this.contextRunner.run(withWebTestClient(
|
||||
(client) -> client.get().uri("/cfApplication/unknown").exchange().expectStatus().isForbidden()));
|
||||
}
|
||||
|
||||
private ContextConsumer<AssertableReactiveWebApplicationContext> withWebTestClient(
|
||||
Consumer<WebTestClient> clientConsumer) {
|
||||
return (context) -> {
|
||||
|
||||
+1
-3
@@ -192,9 +192,7 @@ class CloudFoundryActuatorAutoConfigurationTests {
|
||||
testCloudFoundrySecurity(request, BASE_PATH + "/", chain);
|
||||
testCloudFoundrySecurity(request, BASE_PATH + "/test", chain);
|
||||
testCloudFoundrySecurity(request, BASE_PATH + "/test/a", chain);
|
||||
request.setServletPath(BASE_PATH + "/other-path");
|
||||
request.setRequestURI(BASE_PATH + "/other-path");
|
||||
assertThat(chain.matches(request)).isFalse();
|
||||
testCloudFoundrySecurity(request, BASE_PATH + "/other-path", chain);
|
||||
request.setServletPath("/some-other-path");
|
||||
request.setRequestURI("/some-other-path");
|
||||
assertThat(chain.matches(request)).isFalse();
|
||||
|
||||
+11
@@ -201,6 +201,17 @@ class CloudFoundryMvcWebEndpointIntegrationTests {
|
||||
.doesNotExist());
|
||||
}
|
||||
|
||||
@Test
|
||||
void unknownEndpointsAreForbidden() {
|
||||
load(TestEndpointConfiguration.class,
|
||||
(client) -> client.get()
|
||||
.uri("/cfApplication/unknown")
|
||||
.accept(MediaType.APPLICATION_JSON)
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isForbidden());
|
||||
}
|
||||
|
||||
private void load(Class<?> configuration, Consumer<WebTestClient> clientConsumer) {
|
||||
BiConsumer<ApplicationContext, WebTestClient> consumer = (context, client) -> clientConsumer.accept(client);
|
||||
new WebApplicationContextRunner(AnnotationConfigServletWebServerApplicationContext::new)
|
||||
|
||||
+44
@@ -59,6 +59,7 @@ import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.http.ResponseEntity.BodyBuilder;
|
||||
import org.springframework.http.server.reactive.ServerHttpResponse;
|
||||
import org.springframework.security.authorization.AuthorityAuthorizationManager;
|
||||
import org.springframework.security.authorization.AuthorizationResult;
|
||||
import org.springframework.security.core.Authentication;
|
||||
@@ -106,6 +107,8 @@ public abstract class AbstractWebFluxEndpointHandlerMapping extends RequestMappi
|
||||
|
||||
private final Method handleReadMethod = getHandleReadMethod();
|
||||
|
||||
private final Method handleCatchAllMethod = getCatchAllMethod();
|
||||
|
||||
private final boolean shouldRegisterLinksMapping;
|
||||
|
||||
/**
|
||||
@@ -141,6 +144,12 @@ public abstract class AbstractWebFluxEndpointHandlerMapping extends RequestMappi
|
||||
return method;
|
||||
}
|
||||
|
||||
private static Method getCatchAllMethod() {
|
||||
Method method = ReflectionUtils.findMethod(CatchAllHandler.class, "handle", ServerWebExchange.class);
|
||||
Assert.state(method != null, "'method' must not be null");
|
||||
return method;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initHandlerMethods() {
|
||||
for (ExposableWebEndpoint endpoint : this.endpoints) {
|
||||
@@ -192,6 +201,17 @@ public abstract class AbstractWebFluxEndpointHandlerMapping extends RequestMappi
|
||||
return reactiveWebOperation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a "catch all" handler for the rest of actuator namespace, ensuring that
|
||||
* all requests are handled by this handler mapping.
|
||||
* @param responseStatus the response status to use for handled requests
|
||||
*/
|
||||
protected void registerCatchAllMapping(HttpStatus responseStatus) {
|
||||
String subPath = this.endpointMapping.createSubPath("/**");
|
||||
registerMapping(RequestMappingInfo.paths(subPath).build(), new CatchAllHandler(responseStatus),
|
||||
this.handleCatchAllMethod);
|
||||
}
|
||||
|
||||
private RequestMappingInfo createRequestMappingInfo(WebOperation operation) {
|
||||
WebOperationRequestPredicate predicate = operation.getRequestPredicate();
|
||||
String path = this.endpointMapping.createSubPath(predicate.getPath());
|
||||
@@ -524,6 +544,30 @@ public abstract class AbstractWebFluxEndpointHandlerMapping extends RequestMappi
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Catch-all handler that always replies with a fixed HTTP status.
|
||||
*/
|
||||
private static final class CatchAllHandler {
|
||||
|
||||
private final HttpStatus responseStatus;
|
||||
|
||||
CatchAllHandler(HttpStatus responseStatus) {
|
||||
this.responseStatus = responseStatus;
|
||||
}
|
||||
|
||||
Mono<Void> handle(ServerWebExchange exchange) {
|
||||
ServerHttpResponse response = exchange.getResponse();
|
||||
response.setStatusCode(this.responseStatus);
|
||||
return response.setComplete();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Actuator catch-all endpoint";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class WebFluxEndpointHandlerMethod extends HandlerMethod {
|
||||
|
||||
WebFluxEndpointHandlerMethod(Object bean, Method method) {
|
||||
|
||||
+36
@@ -103,6 +103,8 @@ public abstract class AbstractWebMvcEndpointHandlerMapping extends RequestMappin
|
||||
|
||||
private final Method handleMethod = getHandleMethod();
|
||||
|
||||
private final Method catchAllMethod = getCatchAllMethod();
|
||||
|
||||
private RequestMappingInfo.BuilderConfiguration builderConfig = new RequestMappingInfo.BuilderConfiguration();
|
||||
|
||||
/**
|
||||
@@ -146,6 +148,12 @@ public abstract class AbstractWebMvcEndpointHandlerMapping extends RequestMappin
|
||||
return method;
|
||||
}
|
||||
|
||||
private static Method getCatchAllMethod() {
|
||||
Method method = ReflectionUtils.findMethod(CatchAllHandler.class, "handle", HttpServletResponse.class);
|
||||
Assert.state(method != null, "'method' must not be null");
|
||||
return method;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() {
|
||||
this.builderConfig = new RequestMappingInfo.BuilderConfiguration();
|
||||
@@ -202,6 +210,17 @@ public abstract class AbstractWebMvcEndpointHandlerMapping extends RequestMappin
|
||||
return servletWebOperation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a "catch all" handler for the rest of actuator namespace, ensuring that
|
||||
* all requests are handled by this handler mapping.
|
||||
* @param responseStatus the response status to use for handled requests
|
||||
*/
|
||||
protected void registerCatchAllMapping(HttpStatus responseStatus) {
|
||||
String subPath = this.endpointMapping.createSubPath("/**");
|
||||
registerMapping(RequestMappingInfo.paths(subPath).options(this.builderConfig).build(),
|
||||
new CatchAllHandler(responseStatus), this.catchAllMethod);
|
||||
}
|
||||
|
||||
private RequestMappingInfo createRequestMappingInfo(WebOperationRequestPredicate predicate, String path) {
|
||||
String subPath = this.endpointMapping.createSubPath(path);
|
||||
List<String> paths = new ArrayList<>();
|
||||
@@ -464,6 +483,23 @@ public abstract class AbstractWebMvcEndpointHandlerMapping extends RequestMappin
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Catch-all handler that always replies with a fixed HTTP status.
|
||||
*/
|
||||
private static final class CatchAllHandler {
|
||||
|
||||
private final HttpStatus responseStatus;
|
||||
|
||||
CatchAllHandler(HttpStatus responseStatus) {
|
||||
this.responseStatus = responseStatus;
|
||||
}
|
||||
|
||||
void handle(HttpServletResponse response) {
|
||||
response.setStatus(this.responseStatus.value());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link HandlerMethod} subclass for endpoint information logging.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user