Browse Source

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
pull/49662/head
Brian Clozel 3 days ago
parent
commit
01fbede2b2
  1. 6
      spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/CloudFoundryWebFluxEndpointHandlerMapping.java
  2. 25
      spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveCloudFoundryActuatorAutoConfiguration.java
  3. 16
      spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryActuatorAutoConfiguration.java
  4. 6
      spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryWebEndpointServletHandlerMapping.java
  5. 6
      spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/CloudFoundryWebFluxEndpointIntegrationTests.java
  6. 2
      spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveCloudFoundryActuatorAutoConfigurationTests.java
  7. 4
      spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryActuatorAutoConfigurationTests.java
  8. 11
      spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryMvcWebEndpointIntegrationTests.java
  9. 39
      spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/reactive/AbstractWebFluxEndpointHandlerMapping.java
  10. 31
      spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/servlet/AbstractWebMvcEndpointHandlerMapping.java

6
spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/CloudFoundryWebFluxEndpointHandlerMapping.java

@ -77,6 +77,12 @@ class CloudFoundryWebFluxEndpointHandlerMapping extends AbstractWebFluxEndpointH @@ -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) {

25
spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveCloudFoundryActuatorAutoConfiguration.java

@ -21,7 +21,6 @@ import java.util.Arrays; @@ -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; @@ -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; @@ -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 { @@ -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 { @@ -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 { @@ -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;
}
}
}

16
spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryActuatorAutoConfiguration.java

@ -33,7 +33,6 @@ import org.springframework.boot.actuate.endpoint.invoke.ParameterValueMapper; @@ -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; @@ -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 { @@ -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
spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryWebEndpointServletHandlerMapping.java

@ -80,6 +80,12 @@ class CloudFoundryWebEndpointServletHandlerMapping extends AbstractWebMvcEndpoin @@ -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
spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/CloudFoundryWebFluxEndpointIntegrationTests.java

@ -210,6 +210,12 @@ class CloudFoundryWebFluxEndpointIntegrationTests { @@ -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) -> {

2
spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveCloudFoundryActuatorAutoConfigurationTests.java

@ -192,7 +192,7 @@ class ReactiveCloudFoundryActuatorAutoConfigurationTests { @@ -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()))

4
spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryActuatorAutoConfigurationTests.java

@ -188,9 +188,7 @@ class CloudFoundryActuatorAutoConfigurationTests { @@ -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
spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryMvcWebEndpointIntegrationTests.java

@ -199,6 +199,17 @@ class CloudFoundryMvcWebEndpointIntegrationTests { @@ -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
spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/reactive/AbstractWebFluxEndpointHandlerMapping.java

@ -56,6 +56,7 @@ import org.springframework.http.HttpMethod; @@ -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 @@ -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 @@ -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 @@ -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
spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/servlet/AbstractWebMvcEndpointHandlerMapping.java

@ -103,6 +103,9 @@ public abstract class AbstractWebMvcEndpointHandlerMapping extends RequestMappin @@ -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 @@ -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 @@ -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.
*/

Loading…
Cancel
Save