mirror of
https://github.com/spring-projects/spring-boot.git
synced 2026-09-26 11:39:14 +00:00
Handle all requests in CloudFoundry mapping
Prior to this commit, the Servlet and Reactive CloudFoundry Actuator Handler mapping would only handle requests to actual Actuator endpoints. Here, our original intent has always been to reserve this URL namespace for the CloudFoundry use case and not delegate to other handler mappings. This commit ensures that requests under this path should be processed by known endpoints, or result in a "HTTP 403 Forbidden" response. Fixes gh-49645
This commit is contained in:
+6
@@ -77,6 +77,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
-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.springframework.beans.BeansException;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
@@ -36,7 +35,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.health.HealthEndpoint;
|
||||
import org.springframework.boot.actuate.health.ReactiveHealthEndpointWebExtension;
|
||||
import org.springframework.boot.actuate.info.GitInfoContributor;
|
||||
@@ -64,7 +62,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 class ReactiveCloudFoundryActuatorAutoConfiguration {
|
||||
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 class ReactiveCloudFoundryActuatorAutoConfiguration {
|
||||
}
|
||||
|
||||
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 class ReactiveCloudFoundryActuatorAutoConfiguration {
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+4
-12
@@ -33,7 +33,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.health.HealthEndpoint;
|
||||
import org.springframework.boot.actuate.health.HealthEndpointWebExtension;
|
||||
import org.springframework.boot.actuate.info.GitInfoContributor;
|
||||
@@ -65,7 +64,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.cors.CorsConfiguration;
|
||||
import org.springframework.web.servlet.DispatcherServlet;
|
||||
@@ -172,22 +170,16 @@ public class CloudFoundryActuatorAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
@Order(FILTER_CHAIN_ORDER)
|
||||
SecurityFilterChain cloudFoundrySecurityFilterChain(HttpSecurity http,
|
||||
CloudFoundryWebEndpointServletHandlerMapping handlerMapping) throws Exception {
|
||||
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
@@ -80,6 +80,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) {
|
||||
|
||||
+6
@@ -210,6 +210,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
-1
@@ -192,7 +192,7 @@ class ReactiveCloudFoundryActuatorAutoConfigurationTests {
|
||||
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()))
|
||||
|
||||
+1
-3
@@ -188,9 +188,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
@@ -199,6 +199,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)
|
||||
|
||||
+39
@@ -56,6 +56,7 @@ import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
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;
|
||||
@@ -104,6 +105,9 @@ public abstract class AbstractWebFluxEndpointHandlerMapping extends RequestMappi
|
||||
private final Method handleReadMethod = ReflectionUtils.findMethod(ReadOperationHandler.class, "handle",
|
||||
ServerWebExchange.class);
|
||||
|
||||
private final Method handleCatchAllMethod = ReflectionUtils.findMethod(CatchAllHandler.class, "handle",
|
||||
ServerWebExchange.class);
|
||||
|
||||
private final boolean shouldRegisterLinksMapping;
|
||||
|
||||
/**
|
||||
@@ -177,6 +181,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());
|
||||
@@ -483,6 +498,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) {
|
||||
|
||||
+31
@@ -103,6 +103,9 @@ public abstract class AbstractWebMvcEndpointHandlerMapping extends RequestMappin
|
||||
private final Method handleMethod = ReflectionUtils.findMethod(OperationHandler.class, "handle",
|
||||
HttpServletRequest.class, Map.class);
|
||||
|
||||
private final Method catchAllMethod = ReflectionUtils.findMethod(CatchAllHandler.class, "handle",
|
||||
HttpServletResponse.class);
|
||||
|
||||
private RequestMappingInfo.BuilderConfiguration builderConfig = new RequestMappingInfo.BuilderConfiguration();
|
||||
|
||||
/**
|
||||
@@ -195,6 +198,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<>();
|
||||
@@ -441,6 +455,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