diff --git a/module/spring-boot-cloudfoundry/src/main/java/org/springframework/boot/cloudfoundry/autoconfigure/actuate/endpoint/reactive/CloudFoundryReactiveActuatorAutoConfiguration.java b/module/spring-boot-cloudfoundry/src/main/java/org/springframework/boot/cloudfoundry/autoconfigure/actuate/endpoint/reactive/CloudFoundryReactiveActuatorAutoConfiguration.java index 189b65c6541..7a472afcf96 100644 --- a/module/spring-boot-cloudfoundry/src/main/java/org/springframework/boot/cloudfoundry/autoconfigure/actuate/endpoint/reactive/CloudFoundryReactiveActuatorAutoConfiguration.java +++ b/module/spring-boot-cloudfoundry/src/main/java/org/springframework/boot/cloudfoundry/autoconfigure/actuate/endpoint/reactive/CloudFoundryReactiveActuatorAutoConfiguration.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.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 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 final class CloudFoundryReactiveActuatorAutoConfiguration { } 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 final class CloudFoundryReactiveActuatorAutoConfiguration { 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/module/spring-boot-cloudfoundry/src/main/java/org/springframework/boot/cloudfoundry/autoconfigure/actuate/endpoint/reactive/CloudFoundryWebFluxEndpointHandlerMapping.java b/module/spring-boot-cloudfoundry/src/main/java/org/springframework/boot/cloudfoundry/autoconfigure/actuate/endpoint/reactive/CloudFoundryWebFluxEndpointHandlerMapping.java index 0bfdad58dc0..466e00df79b 100644 --- a/module/spring-boot-cloudfoundry/src/main/java/org/springframework/boot/cloudfoundry/autoconfigure/actuate/endpoint/reactive/CloudFoundryWebFluxEndpointHandlerMapping.java +++ b/module/spring-boot-cloudfoundry/src/main/java/org/springframework/boot/cloudfoundry/autoconfigure/actuate/endpoint/reactive/CloudFoundryWebFluxEndpointHandlerMapping.java @@ -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) { diff --git a/module/spring-boot-cloudfoundry/src/main/java/org/springframework/boot/cloudfoundry/autoconfigure/actuate/endpoint/servlet/CloudFoundryActuatorAutoConfiguration.java b/module/spring-boot-cloudfoundry/src/main/java/org/springframework/boot/cloudfoundry/autoconfigure/actuate/endpoint/servlet/CloudFoundryActuatorAutoConfiguration.java index 705d011ef80..8caffb08bb3 100644 --- a/module/spring-boot-cloudfoundry/src/main/java/org/springframework/boot/cloudfoundry/autoconfigure/actuate/endpoint/servlet/CloudFoundryActuatorAutoConfiguration.java +++ b/module/spring-boot-cloudfoundry/src/main/java/org/springframework/boot/cloudfoundry/autoconfigure/actuate/endpoint/servlet/CloudFoundryActuatorAutoConfiguration.java @@ -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 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/module/spring-boot-cloudfoundry/src/main/java/org/springframework/boot/cloudfoundry/autoconfigure/actuate/endpoint/servlet/CloudFoundryWebEndpointServletHandlerMapping.java b/module/spring-boot-cloudfoundry/src/main/java/org/springframework/boot/cloudfoundry/autoconfigure/actuate/endpoint/servlet/CloudFoundryWebEndpointServletHandlerMapping.java index 616a7fc1b2a..26e32572e2f 100644 --- a/module/spring-boot-cloudfoundry/src/main/java/org/springframework/boot/cloudfoundry/autoconfigure/actuate/endpoint/servlet/CloudFoundryWebEndpointServletHandlerMapping.java +++ b/module/spring-boot-cloudfoundry/src/main/java/org/springframework/boot/cloudfoundry/autoconfigure/actuate/endpoint/servlet/CloudFoundryWebEndpointServletHandlerMapping.java @@ -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) { diff --git a/module/spring-boot-cloudfoundry/src/test/java/org/springframework/boot/cloudfoundry/autoconfigure/actuate/endpoint/reactive/CloudFoundryReactiveActuatorAutoConfigurationTests.java b/module/spring-boot-cloudfoundry/src/test/java/org/springframework/boot/cloudfoundry/autoconfigure/actuate/endpoint/reactive/CloudFoundryReactiveActuatorAutoConfigurationTests.java index 2841e8a3c96..67925383d25 100644 --- a/module/spring-boot-cloudfoundry/src/test/java/org/springframework/boot/cloudfoundry/autoconfigure/actuate/endpoint/reactive/CloudFoundryReactiveActuatorAutoConfigurationTests.java +++ b/module/spring-boot-cloudfoundry/src/test/java/org/springframework/boot/cloudfoundry/autoconfigure/actuate/endpoint/reactive/CloudFoundryReactiveActuatorAutoConfigurationTests.java @@ -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())) diff --git a/module/spring-boot-cloudfoundry/src/test/java/org/springframework/boot/cloudfoundry/autoconfigure/actuate/endpoint/reactive/CloudFoundryWebFluxEndpointIntegrationTests.java b/module/spring-boot-cloudfoundry/src/test/java/org/springframework/boot/cloudfoundry/autoconfigure/actuate/endpoint/reactive/CloudFoundryWebFluxEndpointIntegrationTests.java index e131b2f6dce..9c4634c3da7 100644 --- a/module/spring-boot-cloudfoundry/src/test/java/org/springframework/boot/cloudfoundry/autoconfigure/actuate/endpoint/reactive/CloudFoundryWebFluxEndpointIntegrationTests.java +++ b/module/spring-boot-cloudfoundry/src/test/java/org/springframework/boot/cloudfoundry/autoconfigure/actuate/endpoint/reactive/CloudFoundryWebFluxEndpointIntegrationTests.java @@ -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 withWebTestClient( Consumer clientConsumer) { return (context) -> { diff --git a/module/spring-boot-cloudfoundry/src/test/java/org/springframework/boot/cloudfoundry/autoconfigure/actuate/endpoint/servlet/CloudFoundryActuatorAutoConfigurationTests.java b/module/spring-boot-cloudfoundry/src/test/java/org/springframework/boot/cloudfoundry/autoconfigure/actuate/endpoint/servlet/CloudFoundryActuatorAutoConfigurationTests.java index b9f15a5a36e..aa3cbc1bb0a 100644 --- a/module/spring-boot-cloudfoundry/src/test/java/org/springframework/boot/cloudfoundry/autoconfigure/actuate/endpoint/servlet/CloudFoundryActuatorAutoConfigurationTests.java +++ b/module/spring-boot-cloudfoundry/src/test/java/org/springframework/boot/cloudfoundry/autoconfigure/actuate/endpoint/servlet/CloudFoundryActuatorAutoConfigurationTests.java @@ -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(); diff --git a/module/spring-boot-cloudfoundry/src/test/java/org/springframework/boot/cloudfoundry/autoconfigure/actuate/endpoint/servlet/CloudFoundryMvcWebEndpointIntegrationTests.java b/module/spring-boot-cloudfoundry/src/test/java/org/springframework/boot/cloudfoundry/autoconfigure/actuate/endpoint/servlet/CloudFoundryMvcWebEndpointIntegrationTests.java index 4f927ebc932..29848f0a6cb 100644 --- a/module/spring-boot-cloudfoundry/src/test/java/org/springframework/boot/cloudfoundry/autoconfigure/actuate/endpoint/servlet/CloudFoundryMvcWebEndpointIntegrationTests.java +++ b/module/spring-boot-cloudfoundry/src/test/java/org/springframework/boot/cloudfoundry/autoconfigure/actuate/endpoint/servlet/CloudFoundryMvcWebEndpointIntegrationTests.java @@ -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 clientConsumer) { BiConsumer consumer = (context, client) -> clientConsumer.accept(client); new WebApplicationContextRunner(AnnotationConfigServletWebServerApplicationContext::new) diff --git a/module/spring-boot-webflux/src/main/java/org/springframework/boot/webflux/actuate/endpoint/web/AbstractWebFluxEndpointHandlerMapping.java b/module/spring-boot-webflux/src/main/java/org/springframework/boot/webflux/actuate/endpoint/web/AbstractWebFluxEndpointHandlerMapping.java index c2b674360be..9e6d62bb392 100644 --- a/module/spring-boot-webflux/src/main/java/org/springframework/boot/webflux/actuate/endpoint/web/AbstractWebFluxEndpointHandlerMapping.java +++ b/module/spring-boot-webflux/src/main/java/org/springframework/boot/webflux/actuate/endpoint/web/AbstractWebFluxEndpointHandlerMapping.java @@ -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 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/module/spring-boot-webmvc/src/main/java/org/springframework/boot/webmvc/actuate/endpoint/web/AbstractWebMvcEndpointHandlerMapping.java b/module/spring-boot-webmvc/src/main/java/org/springframework/boot/webmvc/actuate/endpoint/web/AbstractWebMvcEndpointHandlerMapping.java index cadeb10760f..b102879e893 100644 --- a/module/spring-boot-webmvc/src/main/java/org/springframework/boot/webmvc/actuate/endpoint/web/AbstractWebMvcEndpointHandlerMapping.java +++ b/module/spring-boot-webmvc/src/main/java/org/springframework/boot/webmvc/actuate/endpoint/web/AbstractWebMvcEndpointHandlerMapping.java @@ -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 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. */