diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/CloudFoundryWebFluxEndpointHandlerMapping.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/CloudFoundryWebFluxEndpointHandlerMapping.java index c8643f797da..50833eb8fce 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/CloudFoundryWebFluxEndpointHandlerMapping.java +++ b/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 this.securityInterceptor = securityInterceptor; } + @Override + protected void initHandlerMethods() { + super.initHandlerMethods(); + registerCatchAllMapping(HttpStatus.FORBIDDEN); + } + @Override protected ReactiveWebOperation wrapReactiveWebOperation(ExposableWebEndpoint endpoint, WebOperation operation, ReactiveWebOperation reactiveWebOperation) { diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveCloudFoundryActuatorAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveCloudFoundryActuatorAutoConfiguration.java index e81ee899271..508fd828819 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveCloudFoundryActuatorAutoConfiguration.java +++ b/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; 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 handlerMapping) { - return new WebFilterChainPostProcessor(handlerMapping); + static WebFilterChainPostProcessor webFilterChainPostProcessor() { + return new WebFilterChainPostProcessor(); } } static class WebFilterChainPostProcessor implements BeanPostProcessor { - private final Supplier pathMappedEndpoints; - - WebFilterChainPostProcessor(ObjectProvider 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 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 getPaths(PathMappedEndpoints pathMappedEndpoints) { - List paths = new ArrayList<>(); - pathMappedEndpoints.getAllPaths().forEach((path) -> paths.add(path + "/**")); - paths.add(BASE_PATH); - paths.add(BASE_PATH + "/"); - return paths; - } - } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryActuatorAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryActuatorAutoConfiguration.java index e8a01f0121f..0b2ce6c0c5a 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryActuatorAutoConfiguration.java +++ b/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; 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 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) { diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryWebEndpointServletHandlerMapping.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryWebEndpointServletHandlerMapping.java index f9aeed9a374..0d885784c00 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryWebEndpointServletHandlerMapping.java +++ b/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 this.allEndpoints = allEndpoints; } + @Override + protected void initHandlerMethods() { + super.initHandlerMethods(); + registerCatchAllMapping(HttpStatus.FORBIDDEN); + } + @Override protected ServletWebOperation wrapServletWebOperation(ExposableWebEndpoint endpoint, WebOperation operation, ServletWebOperation servletWebOperation) { diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/CloudFoundryWebFluxEndpointIntegrationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/CloudFoundryWebFluxEndpointIntegrationTests.java index 2f2aaa9bed6..ed973e4c3aa 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/CloudFoundryWebFluxEndpointIntegrationTests.java +++ b/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 { .doesNotExist())); } + @Test + void unknownEndpointsAreForbidden() { + this.contextRunner.run(withWebTestClient( + (client) -> client.get().uri("/cfApplication/unknown").exchange().expectStatus().isForbidden())); + } + private ContextConsumer withWebTestClient( Consumer clientConsumer) { return (context) -> { diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveCloudFoundryActuatorAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveCloudFoundryActuatorAutoConfigurationTests.java index 3a96362756c..92484f7ae96 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveCloudFoundryActuatorAutoConfigurationTests.java +++ b/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 { 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())) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryActuatorAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryActuatorAutoConfigurationTests.java index b98b9f17203..39e90f0daa2 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryActuatorAutoConfigurationTests.java +++ b/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 { 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(); diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryMvcWebEndpointIntegrationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryMvcWebEndpointIntegrationTests.java index 9ea193654c0..fda66771094 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryMvcWebEndpointIntegrationTests.java +++ b/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 { .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 clientConsumer) { BiConsumer consumer = (context, client) -> clientConsumer.accept(client); new WebApplicationContextRunner(AnnotationConfigServletWebServerApplicationContext::new) diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/reactive/AbstractWebFluxEndpointHandlerMapping.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/reactive/AbstractWebFluxEndpointHandlerMapping.java index 603421386f8..4d9da585699 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/reactive/AbstractWebFluxEndpointHandlerMapping.java +++ b/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; 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 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) { diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/servlet/AbstractWebMvcEndpointHandlerMapping.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/servlet/AbstractWebMvcEndpointHandlerMapping.java index 719de6b5026..66cd1eef84f 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/servlet/AbstractWebMvcEndpointHandlerMapping.java +++ b/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 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 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. */