diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/CloudFoundryWebEndpointDiscoverer.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/CloudFoundryWebEndpointDiscoverer.java index c401f5cf7801..4950f22ee466 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/CloudFoundryWebEndpointDiscoverer.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/CloudFoundryWebEndpointDiscoverer.java @@ -59,7 +59,7 @@ public CloudFoundryWebEndpointDiscoverer(ApplicationContext applicationContext, ParameterValueMapper parameterValueMapper, EndpointMediaTypes endpointMediaTypes, List endpointPathMappers, Collection invokerAdvisors, Collection> filters) { - super(applicationContext, parameterValueMapper, endpointMediaTypes, endpointPathMappers, invokerAdvisors, + super(applicationContext, parameterValueMapper, endpointMediaTypes, endpointPathMappers, null, invokerAdvisors, filters); } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/WebEndpointAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/WebEndpointAutoConfiguration.java index e3c4e5a4fa46..6ce0eab94f80 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/WebEndpointAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/WebEndpointAutoConfiguration.java @@ -28,6 +28,7 @@ import org.springframework.boot.actuate.endpoint.annotation.Endpoint; import org.springframework.boot.actuate.endpoint.invoke.OperationInvokerAdvisor; import org.springframework.boot.actuate.endpoint.invoke.ParameterValueMapper; +import org.springframework.boot.actuate.endpoint.web.AdditionalPathsMapper; import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes; import org.springframework.boot.actuate.endpoint.web.ExposableWebEndpoint; import org.springframework.boot.actuate.endpoint.web.PathMappedEndpoints; @@ -81,11 +82,12 @@ public EndpointMediaTypes endpointMediaTypes() { @ConditionalOnMissingBean(WebEndpointsSupplier.class) public WebEndpointDiscoverer webEndpointDiscoverer(ParameterValueMapper parameterValueMapper, EndpointMediaTypes endpointMediaTypes, ObjectProvider endpointPathMappers, + ObjectProvider additionalPathsMappers, ObjectProvider invokerAdvisors, ObjectProvider> filters) { return new WebEndpointDiscoverer(this.applicationContext, parameterValueMapper, endpointMediaTypes, - endpointPathMappers.orderedStream().toList(), invokerAdvisors.orderedStream().toList(), - filters.orderedStream().toList()); + endpointPathMappers.orderedStream().toList(), additionalPathsMappers.orderedStream().toList(), + invokerAdvisors.orderedStream().toList(), filters.orderedStream().toList()); } @Bean diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/AutoConfiguredHealthEndpointGroups.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/AutoConfiguredHealthEndpointGroups.java index ff47f03252eb..b7c1ff72a3a4 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/AutoConfiguredHealthEndpointGroups.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/AutoConfiguredHealthEndpointGroups.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,9 +21,11 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.function.Predicate; import java.util.function.Supplier; +import java.util.stream.Stream; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryUtils; @@ -32,8 +34,12 @@ import org.springframework.beans.factory.annotation.BeanFactoryAnnotationUtils; import org.springframework.boot.actuate.autoconfigure.health.HealthEndpointProperties.Group; import org.springframework.boot.actuate.autoconfigure.health.HealthProperties.Status; +import org.springframework.boot.actuate.endpoint.EndpointId; import org.springframework.boot.actuate.endpoint.Show; +import org.springframework.boot.actuate.endpoint.web.AdditionalPathsMapper; +import org.springframework.boot.actuate.endpoint.web.WebServerNamespace; import org.springframework.boot.actuate.health.AdditionalHealthEndpointPath; +import org.springframework.boot.actuate.health.HealthEndpoint; import org.springframework.boot.actuate.health.HealthEndpointGroup; import org.springframework.boot.actuate.health.HealthEndpointGroups; import org.springframework.boot.actuate.health.HttpCodeStatusMapper; @@ -51,7 +57,7 @@ * @author Phillip Webb * @author Madhura Bhave */ -class AutoConfiguredHealthEndpointGroups implements HealthEndpointGroups { +class AutoConfiguredHealthEndpointGroups implements HealthEndpointGroups, AdditionalPathsMapper { private static final Predicate ALL = (name) -> true; @@ -159,4 +165,20 @@ public HealthEndpointGroup get(String name) { return this.groups.get(name); } + @Override + public List getAdditionalPaths(EndpointId endpointId, WebServerNamespace webServerNamespace) { + if (!HealthEndpoint.ID.equals(endpointId)) { + return null; + } + return streamAllGroups().map(HealthEndpointGroup::getAdditionalPath) + .filter(Objects::nonNull) + .filter((additionalPath) -> additionalPath.hasNamespace(webServerNamespace)) + .map(AdditionalHealthEndpointPath::getValue) + .toList(); + } + + private Stream streamAllGroups() { + return Stream.concat(Stream.of(this.primaryGroup), this.groups.values().stream()); + } + } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointConfiguration.java index 8badfc736bd7..c4b78bb3b9c7 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -73,8 +73,8 @@ HttpCodeStatusMapper healthHttpCodeStatusMapper(HealthEndpointProperties propert } @Bean - @ConditionalOnMissingBean - HealthEndpointGroups healthEndpointGroups(ApplicationContext applicationContext, + @ConditionalOnMissingBean(HealthEndpointGroups.class) + AutoConfiguredHealthEndpointGroups healthEndpointGroups(ApplicationContext applicationContext, HealthEndpointProperties properties) { return new AutoConfiguredHealthEndpointGroups(applicationContext, properties); } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/security/reactive/EndpointRequest.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/security/reactive/EndpointRequest.java index bed3b8f5c068..16e91c6e9063 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/security/reactive/EndpointRequest.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/security/reactive/EndpointRequest.java @@ -35,6 +35,7 @@ import org.springframework.boot.actuate.endpoint.EndpointId; import org.springframework.boot.actuate.endpoint.annotation.Endpoint; import org.springframework.boot.actuate.endpoint.web.PathMappedEndpoints; +import org.springframework.boot.actuate.endpoint.web.WebServerNamespace; import org.springframework.boot.security.reactive.ApplicationContextServerWebExchangeMatcher; import org.springframework.context.ApplicationContext; import org.springframework.core.annotation.MergedAnnotation; @@ -43,7 +44,9 @@ import org.springframework.security.web.server.util.matcher.PathPatternParserServerWebExchangeMatcher; import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher; import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher.MatchResult; +import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; import org.springframework.web.server.ServerWebExchange; @@ -52,6 +55,7 @@ * endpoint locations. * * @author Madhura Bhave + * @author Phillip Webb * @since 2.0.0 */ public final class EndpointRequest { @@ -115,30 +119,129 @@ public static LinksServerWebExchangeMatcher toLinks() { return new LinksServerWebExchangeMatcher(); } + /** + * Returns a matcher that includes additional paths under a {@link WebServerNamespace} + * for the specified {@link Endpoint actuator endpoints}. For example: + *
+	 * EndpointRequest.toAdditionalPaths(WebServerNamespace.SERVER, "health")
+	 * 
+ * @param webServerNamespace the web server namespace + * @param endpoints the endpoints to include + * @return the configured {@link RequestMatcher} + * @since 3.4.0 + */ + public static AdditionalPathsEndpointServerWebExchangeMatcher toAdditionalPaths( + WebServerNamespace webServerNamespace, Class... endpoints) { + return new AdditionalPathsEndpointServerWebExchangeMatcher(webServerNamespace, endpoints); + } + + /** + * Returns a matcher that includes additional paths under a {@link WebServerNamespace} + * for the specified {@link Endpoint actuator endpoints}. For example: + *
+	 * EndpointRequest.toAdditionalPaths(WebServerNamespace.SERVER, HealthEndpoint.class)
+	 * 
+ * @param webServerNamespace the web server namespace + * @param endpoints the endpoints to include + * @return the configured {@link RequestMatcher} + * @since 3.4.0 + */ + public static AdditionalPathsEndpointServerWebExchangeMatcher toAdditionalPaths( + WebServerNamespace webServerNamespace, String... endpoints) { + return new AdditionalPathsEndpointServerWebExchangeMatcher(webServerNamespace, endpoints); + } + /** * Base class for supported request matchers. */ - private abstract static class AbstractWebExchangeMatcher extends ApplicationContextServerWebExchangeMatcher { + private abstract static class AbstractWebExchangeMatcher extends ApplicationContextServerWebExchangeMatcher { - private ManagementPortType managementPortType; + private volatile ServerWebExchangeMatcher delegate; - AbstractWebExchangeMatcher(Class contextClass) { + private volatile ManagementPortType managementPortType; + + AbstractWebExchangeMatcher(Class contextClass) { super(contextClass); } + @Override + protected void initialized(Supplier supplier) { + this.delegate = createDelegate(supplier); + } + + private ServerWebExchangeMatcher createDelegate(Supplier context) { + try { + return createDelegate(context.get()); + } + catch (NoSuchBeanDefinitionException ex) { + return EMPTY_MATCHER; + } + } + + protected abstract ServerWebExchangeMatcher createDelegate(C context); + + protected final List getDelegateMatchers(Set paths) { + return paths.stream().map(this::getDelegateMatcher).collect(Collectors.toCollection(ArrayList::new)); + } + + private PathPatternParserServerWebExchangeMatcher getDelegateMatcher(String path) { + return new PathPatternParserServerWebExchangeMatcher(path + "/**"); + } + + @Override + protected Mono matches(ServerWebExchange exchange, Supplier context) { + return this.delegate.matches(exchange); + } + @Override protected boolean ignoreApplicationContext(ApplicationContext applicationContext) { - if (this.managementPortType == null) { - this.managementPortType = ManagementPortType.get(applicationContext.getEnvironment()); + ManagementPortType managementPortType = this.managementPortType; + if (managementPortType == null) { + managementPortType = ManagementPortType.get(applicationContext.getEnvironment()); + this.managementPortType = managementPortType; + } + return ignoreApplicationContext(applicationContext, managementPortType); + } + + protected boolean ignoreApplicationContext(ApplicationContext applicationContext, + ManagementPortType managementPortType) { + return managementPortType == ManagementPortType.DIFFERENT + && !hasWebServerNamespace(applicationContext, WebServerNamespace.MANAGEMENT); + } + + protected final boolean hasWebServerNamespace(ApplicationContext applicationContext, + WebServerNamespace webServerNamespace) { + if (applicationContext.getParent() == null) { + return WebServerNamespace.SERVER.equals(webServerNamespace); + } + String parentContextId = applicationContext.getParent().getId(); + return applicationContext.getId().equals(parentContextId + ":" + webServerNamespace); + } + + protected final String toString(List endpoints, String emptyValue) { + return (!endpoints.isEmpty()) ? endpoints.stream() + .map(this::getEndpointId) + .map(Object::toString) + .collect(Collectors.joining(", ", "[", "]")) : emptyValue; + } + + protected final EndpointId getEndpointId(Object source) { + if (source instanceof EndpointId endpointId) { + return endpointId; + } + if (source instanceof String string) { + return EndpointId.of(string); } - if (this.managementPortType == ManagementPortType.DIFFERENT) { - if (applicationContext.getParent() == null) { - return true; - } - String managementContextId = applicationContext.getParent().getId() + ":management"; - return !managementContextId.equals(applicationContext.getId()); + if (source instanceof Class) { + return getEndpointId((Class) source); } - return false; + throw new IllegalStateException("Unsupported source " + source); + } + + private EndpointId getEndpointId(Class source) { + MergedAnnotation annotation = MergedAnnotations.from(source).get(Endpoint.class); + Assert.state(annotation.isPresent(), () -> "Class " + source + " is not annotated with @Endpoint"); + return EndpointId.of(annotation.getString("id")); } } @@ -155,8 +258,6 @@ public static final class EndpointServerWebExchangeMatcher extends AbstractWebEx private final boolean includeLinks; - private volatile ServerWebExchangeMatcher delegate; - private EndpointServerWebExchangeMatcher(boolean includeLinks) { this(Collections.emptyList(), Collections.emptyList(), includeLinks); } @@ -193,48 +294,22 @@ public EndpointServerWebExchangeMatcher excludingLinks() { } @Override - protected void initialized(Supplier pathMappedEndpoints) { - this.delegate = createDelegate(pathMappedEndpoints); - } - - private ServerWebExchangeMatcher createDelegate(Supplier pathMappedEndpoints) { - try { - return createDelegate(pathMappedEndpoints.get()); - } - catch (NoSuchBeanDefinitionException ex) { - return EMPTY_MATCHER; - } - } - - private ServerWebExchangeMatcher createDelegate(PathMappedEndpoints pathMappedEndpoints) { + protected ServerWebExchangeMatcher createDelegate(PathMappedEndpoints endpoints) { Set paths = new LinkedHashSet<>(); if (this.includes.isEmpty()) { - paths.addAll(pathMappedEndpoints.getAllPaths()); + paths.addAll(endpoints.getAllPaths()); } - streamPaths(this.includes, pathMappedEndpoints).forEach(paths::add); - streamPaths(this.excludes, pathMappedEndpoints).forEach(paths::remove); + streamPaths(this.includes, endpoints).forEach(paths::add); + streamPaths(this.excludes, endpoints).forEach(paths::remove); List delegateMatchers = getDelegateMatchers(paths); - if (this.includeLinks && StringUtils.hasText(pathMappedEndpoints.getBasePath())) { + if (this.includeLinks && StringUtils.hasText(endpoints.getBasePath())) { delegateMatchers.add(new LinksServerWebExchangeMatcher()); } return new OrServerWebExchangeMatcher(delegateMatchers); } - private Stream streamPaths(List source, PathMappedEndpoints pathMappedEndpoints) { - return source.stream().filter(Objects::nonNull).map(this::getEndpointId).map(pathMappedEndpoints::getPath); - } - - @Override - protected Mono matches(ServerWebExchange exchange, Supplier context) { - return this.delegate.matches(exchange); - } - - private List getDelegateMatchers(Set paths) { - return paths.stream().map(this::getDelegateMatcher).collect(Collectors.toCollection(ArrayList::new)); - } - - private PathPatternParserServerWebExchangeMatcher getDelegateMatcher(String path) { - return new PathPatternParserServerWebExchangeMatcher(path + "/**"); + private Stream streamPaths(List source, PathMappedEndpoints endpoints) { + return source.stream().filter(Objects::nonNull).map(this::getEndpointId).map(endpoints::getPath); } @Override @@ -243,32 +318,6 @@ public String toString() { toString(this.includes, "[*]"), toString(this.excludes, "[]"), this.includeLinks); } - private String toString(List endpoints, String emptyValue) { - return (!endpoints.isEmpty()) ? endpoints.stream() - .map(this::getEndpointId) - .map(Object::toString) - .collect(Collectors.joining(", ", "[", "]")) : emptyValue; - } - - private EndpointId getEndpointId(Object source) { - if (source instanceof EndpointId endpointId) { - return endpointId; - } - if (source instanceof String string) { - return EndpointId.of(string); - } - if (source instanceof Class) { - return getEndpointId((Class) source); - } - throw new IllegalStateException("Unsupported source " + source); - } - - private EndpointId getEndpointId(Class source) { - MergedAnnotation annotation = MergedAnnotations.from(source).get(Endpoint.class); - Assert.state(annotation.isPresent(), () -> "Class " + source + " is not annotated with @Endpoint"); - return EndpointId.of(annotation.getString("id")); - } - } /** @@ -276,18 +325,12 @@ private EndpointId getEndpointId(Class source) { */ public static final class LinksServerWebExchangeMatcher extends AbstractWebExchangeMatcher { - private volatile ServerWebExchangeMatcher delegate; - private LinksServerWebExchangeMatcher() { super(WebEndpointProperties.class); } @Override - protected void initialized(Supplier properties) { - this.delegate = createDelegate(properties.get()); - } - - private ServerWebExchangeMatcher createDelegate(WebEndpointProperties properties) { + protected ServerWebExchangeMatcher createDelegate(WebEndpointProperties properties) { if (StringUtils.hasText(properties.getBasePath())) { return new OrServerWebExchangeMatcher( new PathPatternParserServerWebExchangeMatcher(properties.getBasePath()), @@ -297,8 +340,67 @@ private ServerWebExchangeMatcher createDelegate(WebEndpointProperties properties } @Override - protected Mono matches(ServerWebExchange exchange, Supplier context) { - return this.delegate.matches(exchange); + public String toString() { + return String.format("LinksServerWebExchangeMatcher"); + } + + } + + /** + * The {@link ServerWebExchangeMatcher} used to match against additional paths for + * {@link Endpoint actuator endpoints}. + */ + public static class AdditionalPathsEndpointServerWebExchangeMatcher + extends AbstractWebExchangeMatcher { + + private final WebServerNamespace webServerNamespace; + + private final List endpoints; + + AdditionalPathsEndpointServerWebExchangeMatcher(WebServerNamespace webServerNamespace, String... endpoints) { + this(webServerNamespace, Arrays.asList((Object[]) endpoints)); + } + + AdditionalPathsEndpointServerWebExchangeMatcher(WebServerNamespace webServerNamespace, Class... endpoints) { + this(webServerNamespace, Arrays.asList((Object[]) endpoints)); + } + + private AdditionalPathsEndpointServerWebExchangeMatcher(WebServerNamespace webServerNamespace, + List endpoints) { + super(PathMappedEndpoints.class); + Assert.notNull(webServerNamespace, "'webServerNamespace' must not be null"); + Assert.notNull(endpoints, "'endpoints' must not be null"); + Assert.notEmpty(endpoints, "'endpoints' must not be empty"); + this.webServerNamespace = webServerNamespace; + this.endpoints = endpoints; + } + + @Override + protected boolean ignoreApplicationContext(ApplicationContext applicationContext, + ManagementPortType managementPortType) { + return !hasWebServerNamespace(applicationContext, this.webServerNamespace); + } + + @Override + protected ServerWebExchangeMatcher createDelegate(PathMappedEndpoints endpoints) { + Set paths = this.endpoints.stream() + .filter(Objects::nonNull) + .map(this::getEndpointId) + .flatMap((endpointId) -> streamAdditionalPaths(endpoints, endpointId)) + .collect(Collectors.toCollection(LinkedHashSet::new)); + List delegateMatchers = getDelegateMatchers(paths); + return (!CollectionUtils.isEmpty(delegateMatchers)) ? new OrServerWebExchangeMatcher(delegateMatchers) + : EMPTY_MATCHER; + } + + private Stream streamAdditionalPaths(PathMappedEndpoints pathMappedEndpoints, EndpointId endpointId) { + return pathMappedEndpoints.getAdditionalPaths(this.webServerNamespace, endpointId).stream(); + } + + @Override + public String toString() { + return String.format("AdditionalPathsEndpointServerWebExchangeMatcher endpoints=%s, webServerNamespace=%s", + toString(this.endpoints, ""), this.webServerNamespace); } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/security/reactive/ReactiveManagementWebSecurityAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/security/reactive/ReactiveManagementWebSecurityAutoConfiguration.java index e9da837d148e..3567604974b0 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/security/reactive/ReactiveManagementWebSecurityAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/security/reactive/ReactiveManagementWebSecurityAutoConfiguration.java @@ -21,6 +21,7 @@ import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.health.HealthEndpointAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.info.InfoEndpointAutoConfiguration; +import org.springframework.boot.actuate.endpoint.web.WebServerNamespace; import org.springframework.boot.actuate.health.HealthEndpoint; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; @@ -40,6 +41,7 @@ import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.web.server.SecurityWebFilterChain; import org.springframework.security.web.server.WebFilterChainProxy; +import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher; import org.springframework.web.cors.reactive.PreFlightRequestHandler; import org.springframework.web.cors.reactive.PreFlightRequestWebFilter; @@ -66,7 +68,7 @@ public class ReactiveManagementWebSecurityAutoConfiguration { @Bean public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http, PreFlightRequestHandler handler) { http.authorizeExchange((exchanges) -> { - exchanges.matchers(EndpointRequest.to(HealthEndpoint.class)).permitAll(); + exchanges.matchers(healthMatcher(), additionalHealthPathsMatcher()).permitAll(); exchanges.anyExchange().authenticated(); }); PreFlightRequestWebFilter filter = new PreFlightRequestWebFilter(handler); @@ -76,6 +78,14 @@ public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http, return http.build(); } + private ServerWebExchangeMatcher healthMatcher() { + return EndpointRequest.to(HealthEndpoint.class); + } + + private ServerWebExchangeMatcher additionalHealthPathsMatcher() { + return EndpointRequest.toAdditionalPaths(WebServerNamespace.SERVER, HealthEndpoint.class); + } + @Bean @ConditionalOnMissingBean({ ReactiveAuthenticationManager.class, ReactiveUserDetailsService.class }) ReactiveAuthenticationManager denyAllAuthenticationManager() { diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/security/servlet/EndpointRequest.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/security/servlet/EndpointRequest.java index b8a63d0c4cc0..3a85bc894529 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/security/servlet/EndpointRequest.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/security/servlet/EndpointRequest.java @@ -35,15 +35,18 @@ import org.springframework.boot.actuate.endpoint.EndpointId; import org.springframework.boot.actuate.endpoint.annotation.Endpoint; import org.springframework.boot.actuate.endpoint.web.PathMappedEndpoints; +import org.springframework.boot.actuate.endpoint.web.WebServerNamespace; import org.springframework.boot.autoconfigure.security.servlet.RequestMatcherProvider; import org.springframework.boot.security.servlet.ApplicationContextRequestMatcher; import org.springframework.boot.web.context.WebServerApplicationContext; +import org.springframework.context.ApplicationContext; import org.springframework.core.annotation.MergedAnnotation; import org.springframework.core.annotation.MergedAnnotations; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.util.matcher.OrRequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; import org.springframework.web.context.WebApplicationContext; @@ -116,6 +119,38 @@ public static LinksRequestMatcher toLinks() { return new LinksRequestMatcher(); } + /** + * Returns a matcher that includes additional paths under a {@link WebServerNamespace} + * for the specified {@link Endpoint actuator endpoints}. For example: + *
+	 * EndpointRequest.toAdditionalPaths(WebServerNamespace.SERVER, "health")
+	 * 
+ * @param webServerNamespace the web server namespace + * @param endpoints the endpoints to include + * @return the configured {@link RequestMatcher} + * @since 3.4.0 + */ + public static AdditionalPathsEndpointRequestMatcher toAdditionalPaths(WebServerNamespace webServerNamespace, + Class... endpoints) { + return new AdditionalPathsEndpointRequestMatcher(webServerNamespace, endpoints); + } + + /** + * Returns a matcher that includes additional paths under a {@link WebServerNamespace} + * for the specified {@link Endpoint actuator endpoints}. For example: + *
+	 * EndpointRequest.toAdditionalPaths(WebServerNamespace.SERVER, HealthEndpoint.class)
+	 * 
+ * @param webServerNamespace the web server namespace + * @param endpoints the endpoints to include + * @return the configured {@link RequestMatcher} + * @since 3.4.0 + */ + public static AdditionalPathsEndpointRequestMatcher toAdditionalPaths(WebServerNamespace webServerNamespace, + String... endpoints) { + return new AdditionalPathsEndpointRequestMatcher(webServerNamespace, endpoints); + } + /** * Base class for supported request matchers. */ @@ -124,7 +159,7 @@ private abstract static class AbstractRequestMatcher private volatile RequestMatcher delegate; - private ManagementPortType managementPortType; + private volatile ManagementPortType managementPortType; AbstractRequestMatcher() { super(WebApplicationContext.class); @@ -132,11 +167,25 @@ private abstract static class AbstractRequestMatcher @Override protected boolean ignoreApplicationContext(WebApplicationContext applicationContext) { - if (this.managementPortType == null) { - this.managementPortType = ManagementPortType.get(applicationContext.getEnvironment()); + ManagementPortType managementPortType = this.managementPortType; + if (managementPortType == null) { + managementPortType = ManagementPortType.get(applicationContext.getEnvironment()); + this.managementPortType = managementPortType; } - return this.managementPortType == ManagementPortType.DIFFERENT - && !WebServerApplicationContext.hasServerNamespace(applicationContext, "management"); + return ignoreApplicationContext(applicationContext, managementPortType); + } + + protected boolean ignoreApplicationContext(WebApplicationContext applicationContext, + ManagementPortType managementPortType) { + return managementPortType == ManagementPortType.DIFFERENT + && !hasWebServerNamespace(applicationContext, WebServerNamespace.MANAGEMENT); + } + + protected final boolean hasWebServerNamespace(ApplicationContext applicationContext, + WebServerNamespace webServerNamespace) { + return WebServerApplicationContext.hasServerNamespace(applicationContext, webServerNamespace.getValue()) + || (webServerNamespace.equals(WebServerNamespace.SERVER) + && !(applicationContext instanceof WebServerApplicationContext)); } @Override @@ -161,6 +210,13 @@ private RequestMatcher createDelegate(WebApplicationContext context) { protected abstract RequestMatcher createDelegate(WebApplicationContext context, RequestMatcherFactory requestMatcherFactory); + protected final List getDelegateMatchers(RequestMatcherFactory requestMatcherFactory, + RequestMatcherProvider matcherProvider, Set paths) { + return paths.stream() + .map((path) -> requestMatcherFactory.antPath(matcherProvider, path, "/**")) + .collect(Collectors.toCollection(ArrayList::new)); + } + protected List getLinksMatchers(RequestMatcherFactory requestMatcherFactory, RequestMatcherProvider matcherProvider, String basePath) { List linksMatchers = new ArrayList<>(); @@ -178,6 +234,32 @@ protected RequestMatcherProvider getRequestMatcherProvider(WebApplicationContext } } + protected final String toString(List endpoints, String emptyValue) { + return (!endpoints.isEmpty()) ? endpoints.stream() + .map(this::getEndpointId) + .map(Object::toString) + .collect(Collectors.joining(", ", "[", "]")) : emptyValue; + } + + protected final EndpointId getEndpointId(Object source) { + if (source instanceof EndpointId endpointId) { + return endpointId; + } + if (source instanceof String string) { + return EndpointId.of(string); + } + if (source instanceof Class sourceClass) { + return getEndpointId(sourceClass); + } + throw new IllegalStateException("Unsupported source " + source); + } + + private EndpointId getEndpointId(Class source) { + MergedAnnotation annotation = MergedAnnotations.from(source).get(Endpoint.class); + Assert.state(annotation.isPresent(), () -> "Class " + source + " is not annotated with @Endpoint"); + return EndpointId.of(annotation.getString("id")); + } + } /** @@ -228,31 +310,24 @@ public EndpointRequestMatcher excludingLinks() { @Override protected RequestMatcher createDelegate(WebApplicationContext context, RequestMatcherFactory requestMatcherFactory) { - PathMappedEndpoints pathMappedEndpoints = context.getBean(PathMappedEndpoints.class); + PathMappedEndpoints endpoints = context.getBean(PathMappedEndpoints.class); RequestMatcherProvider matcherProvider = getRequestMatcherProvider(context); Set paths = new LinkedHashSet<>(); if (this.includes.isEmpty()) { - paths.addAll(pathMappedEndpoints.getAllPaths()); + paths.addAll(endpoints.getAllPaths()); } - streamPaths(this.includes, pathMappedEndpoints).forEach(paths::add); - streamPaths(this.excludes, pathMappedEndpoints).forEach(paths::remove); + streamPaths(this.includes, endpoints).forEach(paths::add); + streamPaths(this.excludes, endpoints).forEach(paths::remove); List delegateMatchers = getDelegateMatchers(requestMatcherFactory, matcherProvider, paths); - String basePath = pathMappedEndpoints.getBasePath(); + String basePath = endpoints.getBasePath(); if (this.includeLinks && StringUtils.hasText(basePath)) { delegateMatchers.addAll(getLinksMatchers(requestMatcherFactory, matcherProvider, basePath)); } return new OrRequestMatcher(delegateMatchers); } - private Stream streamPaths(List source, PathMappedEndpoints pathMappedEndpoints) { - return source.stream().filter(Objects::nonNull).map(this::getEndpointId).map(pathMappedEndpoints::getPath); - } - - private List getDelegateMatchers(RequestMatcherFactory requestMatcherFactory, - RequestMatcherProvider matcherProvider, Set paths) { - return paths.stream() - .map((path) -> requestMatcherFactory.antPath(matcherProvider, path, "/**")) - .collect(Collectors.toCollection(ArrayList::new)); + private Stream streamPaths(List source, PathMappedEndpoints endpoints) { + return source.stream().filter(Objects::nonNull).map(this::getEndpointId).map(endpoints::getPath); } @Override @@ -261,32 +336,6 @@ public String toString() { toString(this.includes, "[*]"), toString(this.excludes, "[]"), this.includeLinks); } - private String toString(List endpoints, String emptyValue) { - return (!endpoints.isEmpty()) ? endpoints.stream() - .map(this::getEndpointId) - .map(Object::toString) - .collect(Collectors.joining(", ", "[", "]")) : emptyValue; - } - - private EndpointId getEndpointId(Object source) { - if (source instanceof EndpointId endpointId) { - return endpointId; - } - if (source instanceof String string) { - return EndpointId.of(string); - } - if (source instanceof Class) { - return getEndpointId((Class) source); - } - throw new IllegalStateException("Unsupported source " + source); - } - - private EndpointId getEndpointId(Class source) { - MergedAnnotation annotation = MergedAnnotations.from(source).get(Endpoint.class); - Assert.state(annotation.isPresent(), () -> "Class " + source + " is not annotated with @Endpoint"); - return EndpointId.of(annotation.getString("id")); - } - } /** @@ -306,6 +355,70 @@ protected RequestMatcher createDelegate(WebApplicationContext context, return EMPTY_MATCHER; } + @Override + public String toString() { + return String.format("LinksRequestMatcher"); + } + + } + + /** + * The request matcher used to match against additional paths for {@link Endpoint + * actuator endpoints}. + */ + public static class AdditionalPathsEndpointRequestMatcher extends AbstractRequestMatcher { + + private final WebServerNamespace webServerNamespace; + + private final List endpoints; + + AdditionalPathsEndpointRequestMatcher(WebServerNamespace webServerNamespace, String... endpoints) { + this(webServerNamespace, Arrays.asList((Object[]) endpoints)); + } + + AdditionalPathsEndpointRequestMatcher(WebServerNamespace webServerNamespace, Class... endpoints) { + this(webServerNamespace, Arrays.asList((Object[]) endpoints)); + } + + private AdditionalPathsEndpointRequestMatcher(WebServerNamespace webServerNamespace, List endpoints) { + Assert.notNull(webServerNamespace, "'webServerNamespace' must not be null"); + Assert.notNull(endpoints, "'endpoints' must not be null"); + Assert.notEmpty(endpoints, "'endpoints' must not be empty"); + this.webServerNamespace = webServerNamespace; + this.endpoints = endpoints; + } + + @Override + protected boolean ignoreApplicationContext(WebApplicationContext applicationContext, + ManagementPortType managementPortType) { + return !hasWebServerNamespace(applicationContext, this.webServerNamespace); + } + + @Override + protected RequestMatcher createDelegate(WebApplicationContext context, + RequestMatcherFactory requestMatcherFactory) { + PathMappedEndpoints endpoints = context.getBean(PathMappedEndpoints.class); + RequestMatcherProvider matcherProvider = getRequestMatcherProvider(context); + Set paths = this.endpoints.stream() + .filter(Objects::nonNull) + .map(this::getEndpointId) + .flatMap((endpointId) -> streamAdditionalPaths(endpoints, endpointId)) + .collect(Collectors.toCollection(LinkedHashSet::new)); + List delegateMatchers = getDelegateMatchers(requestMatcherFactory, matcherProvider, paths); + return (!CollectionUtils.isEmpty(delegateMatchers)) ? new OrRequestMatcher(delegateMatchers) + : EMPTY_MATCHER; + } + + private Stream streamAdditionalPaths(PathMappedEndpoints pathMappedEndpoints, EndpointId endpointId) { + return pathMappedEndpoints.getAdditionalPaths(this.webServerNamespace, endpointId).stream(); + } + + @Override + public String toString() { + return String.format("AdditionalPathsEndpointRequestMatcher endpoints=%s, webServerNamespace=%s", + toString(this.endpoints, ""), this.webServerNamespace); + } + } /** diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/security/servlet/ManagementWebSecurityAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/security/servlet/ManagementWebSecurityAutoConfiguration.java index d6bc5b11a072..704100462f65 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/security/servlet/ManagementWebSecurityAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/security/servlet/ManagementWebSecurityAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,7 @@ import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.health.HealthEndpointAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.info.InfoEndpointAutoConfiguration; +import org.springframework.boot.actuate.endpoint.web.WebServerNamespace; import org.springframework.boot.actuate.health.HealthEndpoint; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; @@ -31,8 +32,10 @@ import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.core.annotation.Order; +import org.springframework.core.env.Environment; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.util.ClassUtils; import static org.springframework.security.config.Customizer.withDefaults; @@ -40,7 +43,7 @@ /** * {@link EnableAutoConfiguration Auto-configuration} for Spring Security when actuator is * on the classpath. It allows unauthenticated access to the {@link HealthEndpoint}. If - * the user specifies their own{@link SecurityFilterChain} bean, this will back-off + * the user specifies their own {@link SecurityFilterChain} bean, this will back-off * completely and the user should specify all the bits that they want to configure as part * of the custom security configuration. * @@ -58,9 +61,9 @@ public class ManagementWebSecurityAutoConfiguration { @Bean @Order(SecurityProperties.BASIC_AUTH_ORDER) - SecurityFilterChain managementSecurityFilterChain(HttpSecurity http) throws Exception { + SecurityFilterChain managementSecurityFilterChain(Environment environment, HttpSecurity http) throws Exception { http.authorizeHttpRequests((requests) -> { - requests.requestMatchers(EndpointRequest.to(HealthEndpoint.class)).permitAll(); + requests.requestMatchers(healthMatcher(), additionalHealthPathsMatcher()).permitAll(); requests.anyRequest().authenticated(); }); if (ClassUtils.isPresent("org.springframework.web.servlet.DispatcherServlet", null)) { @@ -71,4 +74,12 @@ SecurityFilterChain managementSecurityFilterChain(HttpSecurity http) throws Exce return http.build(); } + private RequestMatcher healthMatcher() { + return EndpointRequest.to(HealthEndpoint.class); + } + + private RequestMatcher additionalHealthPathsMatcher() { + return EndpointRequest.toAdditionalPaths(WebServerNamespace.SERVER, HealthEndpoint.class); + } + } 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 99bb7885735d..7846c80c5cc3 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 @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -262,7 +262,7 @@ WebEndpointDiscoverer webEndpointDiscoverer(ApplicationContext applicationContex EndpointMediaTypes endpointMediaTypes) { ParameterValueMapper parameterMapper = new ConversionServiceParameterValueMapper( DefaultConversionService.getSharedInstance()); - return new WebEndpointDiscoverer(applicationContext, parameterMapper, endpointMediaTypes, null, + return new WebEndpointDiscoverer(applicationContext, parameterMapper, endpointMediaTypes, null, null, Collections.emptyList(), Collections.emptyList()); } 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 09c2e72a25a9..01383c6abf69 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 @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -256,7 +256,7 @@ WebEndpointDiscoverer webEndpointDiscoverer(ApplicationContext applicationContex EndpointMediaTypes endpointMediaTypes) { ParameterValueMapper parameterMapper = new ConversionServiceParameterValueMapper( DefaultConversionService.getSharedInstance()); - return new WebEndpointDiscoverer(applicationContext, parameterMapper, endpointMediaTypes, null, + return new WebEndpointDiscoverer(applicationContext, parameterMapper, endpointMediaTypes, null, null, Collections.emptyList(), Collections.emptyList()); } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/AutoConfiguredHealthEndpointGroupsTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/AutoConfiguredHealthEndpointGroupsTests.java index f967126a9dc7..c00790d80632 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/AutoConfiguredHealthEndpointGroupsTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/AutoConfiguredHealthEndpointGroupsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,7 +21,11 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.actuate.endpoint.EndpointId; import org.springframework.boot.actuate.endpoint.SecurityContext; +import org.springframework.boot.actuate.endpoint.web.AdditionalPathsMapper; +import org.springframework.boot.actuate.endpoint.web.WebServerNamespace; +import org.springframework.boot.actuate.health.HealthEndpoint; import org.springframework.boot.actuate.health.HealthEndpointGroup; import org.springframework.boot.actuate.health.HealthEndpointGroups; import org.springframework.boot.actuate.health.HttpCodeStatusMapper; @@ -333,6 +337,24 @@ void createWhenGroupWithNoShowDetailsOverrideInheritsShowDetails() { }); } + @Test + void getAdditionalPathsReturnsAllAdditionalPaths() { + this.contextRunner + .withPropertyValues("management.endpoint.health.group.a.additional-path=server:/a", + "management.endpoint.health.group.b.additional-path=server:/b", + "management.endpoint.health.group.c.additional-path=management:/c", + "management.endpoint.health.group.d.additional-path=management:/d") + .run((context) -> { + AdditionalPathsMapper additionalPathsMapper = context.getBean(AdditionalPathsMapper.class); + assertThat(additionalPathsMapper.getAdditionalPaths(HealthEndpoint.ID, WebServerNamespace.SERVER)) + .containsExactlyInAnyOrder("/a", "/b"); + assertThat(additionalPathsMapper.getAdditionalPaths(HealthEndpoint.ID, WebServerNamespace.MANAGEMENT)) + .containsExactlyInAnyOrder("/c", "/d"); + assertThat(additionalPathsMapper.getAdditionalPaths(EndpointId.of("other"), WebServerNamespace.SERVER)) + .isNull(); + }); + } + @Configuration(proxyBeanMethods = false) @EnableConfigurationProperties(HealthEndpointProperties.class) static class AutoConfiguredHealthEndpointGroupsTestConfiguration { diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/security/reactive/EndpointRequestTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/security/reactive/EndpointRequestTests.java index e71258a986ab..792ed54ca9c0 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/security/reactive/EndpointRequestTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/security/reactive/EndpointRequestTests.java @@ -18,6 +18,7 @@ import java.time.Duration; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import org.assertj.core.api.AssertDelegateTarget; @@ -30,6 +31,7 @@ import org.springframework.boot.actuate.endpoint.annotation.Endpoint; import org.springframework.boot.actuate.endpoint.web.PathMappedEndpoint; import org.springframework.boot.actuate.endpoint.web.PathMappedEndpoints; +import org.springframework.boot.actuate.endpoint.web.WebServerNamespace; import org.springframework.context.support.StaticApplicationContext; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpResponse; @@ -234,6 +236,13 @@ void toStringWhenIncludedExcludedEndpoints() { assertThat(matcher).hasToString("EndpointRequestMatcher includes=[*], excludes=[bar], includeLinks=false"); } + @Test + void toStringWhenToAdditionalPaths() { + ServerWebExchangeMatcher matcher = EndpointRequest.toAdditionalPaths(WebServerNamespace.SERVER, "test"); + assertThat(matcher) + .hasToString("AdditionalPathsEndpointServerWebExchangeMatcher endpoints=[test], webServerNamespace=server"); + } + @Test void toAnyEndpointWhenEndpointPathMappedToRootIsExcludedShouldNotMatchRoot() { ServerWebExchangeMatcher matcher = EndpointRequest.toAnyEndpoint().excluding("root"); @@ -252,6 +261,43 @@ void toEndpointWhenEndpointPathMappedToRootShouldMatchRoot() { assertMatcher.matches("/"); } + @Test + void toAdditionalPathsWithEndpointClassShouldMatchAdditionalPath() { + ServerWebExchangeMatcher matcher = EndpointRequest.toAdditionalPaths(WebServerNamespace.SERVER, + FooEndpoint.class); + RequestMatcherAssert assertMatcher = assertMatcher(matcher, new PathMappedEndpoints("", + () -> List.of(mockEndpoint(EndpointId.of("foo"), "test", WebServerNamespace.SERVER, "/additional")))); + assertMatcher.matches("/additional"); + } + + @Test + void toAdditionalPathsWithEndpointIdShouldMatchAdditionalPath() { + ServerWebExchangeMatcher matcher = EndpointRequest.toAdditionalPaths(WebServerNamespace.SERVER, "foo"); + RequestMatcherAssert assertMatcher = assertMatcher(matcher, new PathMappedEndpoints("", + () -> List.of(mockEndpoint(EndpointId.of("foo"), "test", WebServerNamespace.SERVER, "/additional")))); + assertMatcher.matches("/additional"); + } + + @Test + void toAdditionalPathsWithEndpointClassShouldNotMatchOtherPaths() { + ServerWebExchangeMatcher matcher = EndpointRequest.toAdditionalPaths(WebServerNamespace.SERVER, + FooEndpoint.class); + RequestMatcherAssert assertMatcher = assertMatcher(matcher, new PathMappedEndpoints("", + () -> List.of(mockEndpoint(EndpointId.of("foo"), "test", WebServerNamespace.SERVER, "/additional")))); + assertMatcher.doesNotMatch("/foo"); + assertMatcher.doesNotMatch("/bar"); + } + + @Test + void toAdditionalPathsWithEndpointClassShouldNotMatchOtherNamespace() { + ServerWebExchangeMatcher matcher = EndpointRequest.toAdditionalPaths(WebServerNamespace.SERVER, + FooEndpoint.class); + RequestMatcherAssert assertMatcher = assertMatcher(matcher, new PathMappedEndpoints("", + () -> List.of(mockEndpoint(EndpointId.of("foo"), "test", WebServerNamespace.SERVER, "/additional"))), + WebServerNamespace.MANAGEMENT); + assertMatcher.doesNotMatch("/additional"); + } + private RequestMatcherAssert assertMatcher(ServerWebExchangeMatcher matcher) { return assertMatcher(matcher, mockPathMappedEndpoints("/actuator")); } @@ -260,23 +306,20 @@ private RequestMatcherAssert assertMatcher(ServerWebExchangeMatcher matcher, Str return assertMatcher(matcher, mockPathMappedEndpoints(basePath)); } - private PathMappedEndpoints mockPathMappedEndpoints(String basePath) { - List> endpoints = new ArrayList<>(); - endpoints.add(mockEndpoint(EndpointId.of("foo"), "foo")); - endpoints.add(mockEndpoint(EndpointId.of("bar"), "bar")); - return new PathMappedEndpoints(basePath, () -> endpoints); - } - - private TestEndpoint mockEndpoint(EndpointId id, String rootPath) { - TestEndpoint endpoint = mock(TestEndpoint.class); - given(endpoint.getEndpointId()).willReturn(id); - given(endpoint.getRootPath()).willReturn(rootPath); - return endpoint; + private RequestMatcherAssert assertMatcher(ServerWebExchangeMatcher matcher, + PathMappedEndpoints pathMappedEndpoints) { + return assertMatcher(matcher, pathMappedEndpoints, null); } private RequestMatcherAssert assertMatcher(ServerWebExchangeMatcher matcher, - PathMappedEndpoints pathMappedEndpoints) { + PathMappedEndpoints pathMappedEndpoints, WebServerNamespace namespace) { StaticApplicationContext context = new StaticApplicationContext(); + if (namespace != null && !WebServerNamespace.SERVER.equals(namespace)) { + StaticApplicationContext parentContext = new StaticApplicationContext(); + parentContext.setId("app"); + context.setParent(parentContext); + context.setId(parentContext.getId() + ":" + namespace); + } context.registerBean(WebEndpointProperties.class); if (pathMappedEndpoints != null) { context.registerBean(PathMappedEndpoints.class, () -> pathMappedEndpoints); @@ -288,6 +331,26 @@ private RequestMatcherAssert assertMatcher(ServerWebExchangeMatcher matcher, return assertThat(new RequestMatcherAssert(context, matcher)); } + private PathMappedEndpoints mockPathMappedEndpoints(String basePath) { + List> endpoints = new ArrayList<>(); + endpoints.add(mockEndpoint(EndpointId.of("foo"), "foo")); + endpoints.add(mockEndpoint(EndpointId.of("bar"), "bar")); + return new PathMappedEndpoints(basePath, () -> endpoints); + } + + private TestEndpoint mockEndpoint(EndpointId id, String rootPath) { + return mockEndpoint(id, rootPath, WebServerNamespace.SERVER); + } + + private TestEndpoint mockEndpoint(EndpointId id, String rootPath, WebServerNamespace webServerNamespace, + String... additionalPaths) { + TestEndpoint endpoint = mock(TestEndpoint.class); + given(endpoint.getEndpointId()).willReturn(id); + given(endpoint.getRootPath()).willReturn(rootPath); + given(endpoint.getAdditionalPaths(webServerNamespace)).willReturn(Arrays.asList(additionalPaths)); + return endpoint; + } + static class RequestMatcherAssert implements AssertDelegateTarget { private final StaticApplicationContext context; diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/security/reactive/ReactiveManagementWebSecurityAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/security/reactive/ReactiveManagementWebSecurityAutoConfigurationTests.java index 66b39ded5a73..f632721a5608 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/security/reactive/ReactiveManagementWebSecurityAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/security/reactive/ReactiveManagementWebSecurityAutoConfigurationTests.java @@ -79,6 +79,35 @@ void permitAllForHealth() { .run((context) -> assertThat(getAuthenticateHeader(context, "/actuator/health")).isNull()); } + @Test + void withAdditionalPathsOnSamePort() { + this.contextRunner.withUserConfiguration(UserDetailsServiceConfiguration.class) + .withPropertyValues("management.endpoint.health.group.test1.include=*", + "management.endpoint.health.group.test2.include=*", + "management.endpoint.health.group.test1.additional-path=server:/check1", + "management.endpoint.health.group.test2.additional-path=management:/check2") + .run((context) -> { + assertThat(getAuthenticateHeader(context, "/check1")).isNull(); + assertThat(getAuthenticateHeader(context, "/check2").get(0)).contains("Basic realm="); + assertThat(getAuthenticateHeader(context, "/actuator/health")).isNull(); + }); + } + + @Test + void withAdditionalPathsOnDifferentPort() { + this.contextRunner.withUserConfiguration(UserDetailsServiceConfiguration.class) + .withPropertyValues("management.endpoint.health.group.test1.include=*", + "management.endpoint.health.group.test2.include=*", + "management.endpoint.health.group.test1.additional-path=server:/check1", + "management.endpoint.health.group.test2.additional-path=management:/check2", + "management.server.port=0") + .run((context) -> { + assertThat(getAuthenticateHeader(context, "/check1")).isNull(); + assertThat(getAuthenticateHeader(context, "/check2").get(0)).contains("Basic realm="); + assertThat(getAuthenticateHeader(context, "/actuator/health").get(0)).contains("Basic realm="); + }); + } + @Test void securesEverythingElse() { this.contextRunner.withUserConfiguration(UserDetailsServiceConfiguration.class).run((context) -> { diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/security/servlet/EndpointRequestTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/security/servlet/EndpointRequestTests.java index d3cf4c26d103..ac07f1d6ef7c 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/security/servlet/EndpointRequestTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/security/servlet/EndpointRequestTests.java @@ -17,6 +17,7 @@ package org.springframework.boot.actuate.autoconfigure.security.servlet; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import jakarta.servlet.http.HttpServletRequest; @@ -24,6 +25,7 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties; +import org.springframework.boot.actuate.autoconfigure.security.servlet.EndpointRequest.AdditionalPathsEndpointRequestMatcher; import org.springframework.boot.actuate.autoconfigure.security.servlet.EndpointRequest.EndpointRequestMatcher; import org.springframework.boot.actuate.endpoint.EndpointId; import org.springframework.boot.actuate.endpoint.ExposableEndpoint; @@ -31,7 +33,10 @@ import org.springframework.boot.actuate.endpoint.annotation.Endpoint; import org.springframework.boot.actuate.endpoint.web.PathMappedEndpoint; import org.springframework.boot.actuate.endpoint.web.PathMappedEndpoints; +import org.springframework.boot.actuate.endpoint.web.WebServerNamespace; import org.springframework.boot.autoconfigure.security.servlet.RequestMatcherProvider; +import org.springframework.boot.web.context.WebServerApplicationContext; +import org.springframework.boot.web.server.WebServer; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockServletContext; import org.springframework.security.web.util.matcher.RequestMatcher; @@ -194,7 +199,7 @@ void endpointRequestMatcherShouldUseCustomRequestMatcherProvider() { RequestMatcher matcher = EndpointRequest.toAnyEndpoint(); RequestMatcher mockRequestMatcher = (request) -> false; RequestMatcherAssert assertMatcher = assertMatcher(matcher, mockPathMappedEndpoints(""), - (pattern) -> mockRequestMatcher); + (pattern) -> mockRequestMatcher, null); assertMatcher.doesNotMatch("/foo"); assertMatcher.doesNotMatch("/bar"); } @@ -204,7 +209,7 @@ void linksRequestMatcherShouldUseCustomRequestMatcherProvider() { RequestMatcher matcher = EndpointRequest.toLinks(); RequestMatcher mockRequestMatcher = (request) -> false; RequestMatcherAssert assertMatcher = assertMatcher(matcher, mockPathMappedEndpoints("/actuator"), - (pattern) -> mockRequestMatcher); + (pattern) -> mockRequestMatcher, null); assertMatcher.doesNotMatch("/actuator"); } @@ -239,6 +244,13 @@ void toStringWhenIncludedExcludedEndpoints() { assertThat(matcher).hasToString("EndpointRequestMatcher includes=[*], excludes=[bar], includeLinks=false"); } + @Test + void toStringWhenToAdditionalPaths() { + RequestMatcher matcher = EndpointRequest.toAdditionalPaths(WebServerNamespace.SERVER, "test"); + assertThat(matcher) + .hasToString("AdditionalPathsEndpointRequestMatcher endpoints=[test], webServerNamespace=server"); + } + @Test void toAnyEndpointWhenEndpointPathMappedToRootIsExcludedShouldNotMatchRoot() { EndpointRequestMatcher matcher = EndpointRequest.toAnyEndpoint().excluding("root"); @@ -257,12 +269,50 @@ void toEndpointWhenEndpointPathMappedToRootShouldMatchRoot() { assertMatcher.matches("/"); } + @Test + void toAdditionalPathsWithEndpointClassShouldMatchAdditionalPath() { + AdditionalPathsEndpointRequestMatcher matcher = EndpointRequest.toAdditionalPaths(WebServerNamespace.SERVER, + FooEndpoint.class); + RequestMatcherAssert assertMatcher = assertMatcher(matcher, new PathMappedEndpoints("", + () -> List.of(mockEndpoint(EndpointId.of("foo"), "test", WebServerNamespace.SERVER, "/additional")))); + assertMatcher.matches("/additional"); + } + + @Test + void toAdditionalPathsWithEndpointIdShouldMatchAdditionalPath() { + AdditionalPathsEndpointRequestMatcher matcher = EndpointRequest.toAdditionalPaths(WebServerNamespace.SERVER, + "foo"); + RequestMatcherAssert assertMatcher = assertMatcher(matcher, new PathMappedEndpoints("", + () -> List.of(mockEndpoint(EndpointId.of("foo"), "test", WebServerNamespace.SERVER, "/additional")))); + assertMatcher.matches("/additional"); + } + + @Test + void toAdditionalPathsWithEndpointClassShouldNotMatchOtherPaths() { + AdditionalPathsEndpointRequestMatcher matcher = EndpointRequest.toAdditionalPaths(WebServerNamespace.SERVER, + FooEndpoint.class); + RequestMatcherAssert assertMatcher = assertMatcher(matcher, new PathMappedEndpoints("", + () -> List.of(mockEndpoint(EndpointId.of("foo"), "test", WebServerNamespace.SERVER, "/additional")))); + assertMatcher.doesNotMatch("/foo"); + assertMatcher.doesNotMatch("/bar"); + } + + @Test + void toAdditionalPathsWithEndpointClassShouldNotMatchOtherNamespace() { + AdditionalPathsEndpointRequestMatcher matcher = EndpointRequest.toAdditionalPaths(WebServerNamespace.SERVER, + FooEndpoint.class); + RequestMatcherAssert assertMatcher = assertMatcher(matcher, new PathMappedEndpoints("", + () -> List.of(mockEndpoint(EndpointId.of("foo"), "test", WebServerNamespace.SERVER, "/additional"))), + null, WebServerNamespace.MANAGEMENT); + assertMatcher.doesNotMatch("/additional"); + } + private RequestMatcherAssert assertMatcher(RequestMatcher matcher) { return assertMatcher(matcher, mockPathMappedEndpoints("/actuator")); } private RequestMatcherAssert assertMatcher(RequestMatcher matcher, String basePath) { - return assertMatcher(matcher, mockPathMappedEndpoints(basePath), null); + return assertMatcher(matcher, mockPathMappedEndpoints(basePath), null, null); } private PathMappedEndpoints mockPathMappedEndpoints(String basePath) { @@ -273,19 +323,26 @@ private PathMappedEndpoints mockPathMappedEndpoints(String basePath) { } private TestEndpoint mockEndpoint(EndpointId id, String rootPath) { + return mockEndpoint(id, rootPath, WebServerNamespace.SERVER); + } + + private TestEndpoint mockEndpoint(EndpointId id, String rootPath, WebServerNamespace webServerNamespace, + String... additionalPaths) { TestEndpoint endpoint = mock(TestEndpoint.class); given(endpoint.getEndpointId()).willReturn(id); given(endpoint.getRootPath()).willReturn(rootPath); + given(endpoint.getAdditionalPaths(webServerNamespace)).willReturn(Arrays.asList(additionalPaths)); return endpoint; } private RequestMatcherAssert assertMatcher(RequestMatcher matcher, PathMappedEndpoints pathMappedEndpoints) { - return assertMatcher(matcher, pathMappedEndpoints, null); + return assertMatcher(matcher, pathMappedEndpoints, null, null); } private RequestMatcherAssert assertMatcher(RequestMatcher matcher, PathMappedEndpoints pathMappedEndpoints, - RequestMatcherProvider matcherProvider) { - StaticWebApplicationContext context = new StaticWebApplicationContext(); + RequestMatcherProvider matcherProvider, WebServerNamespace webServerNamespace) { + StaticWebApplicationContext context = (webServerNamespace != null) + ? new NamedStaticWebApplicationContext(webServerNamespace) : new StaticWebApplicationContext(); context.registerBean(WebEndpointProperties.class); if (pathMappedEndpoints != null) { context.registerBean(PathMappedEndpoints.class, () -> pathMappedEndpoints); @@ -300,6 +357,27 @@ private RequestMatcherAssert assertMatcher(RequestMatcher matcher, PathMappedEnd return assertThat(new RequestMatcherAssert(context, matcher)); } + static class NamedStaticWebApplicationContext extends StaticWebApplicationContext + implements WebServerApplicationContext { + + private final WebServerNamespace webServerNamespace; + + NamedStaticWebApplicationContext(WebServerNamespace webServerNamespace) { + this.webServerNamespace = webServerNamespace; + } + + @Override + public WebServer getWebServer() { + return null; + } + + @Override + public String getServerNamespace() { + return this.webServerNamespace.getValue(); + } + + } + static class RequestMatcherAssert implements AssertDelegateTarget { private final WebApplicationContext context; diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/security/servlet/ManagementWebSecurityAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/security/servlet/ManagementWebSecurityAutoConfigurationTests.java index 869ee26aa4af..b60d93ee2dfa 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/security/servlet/ManagementWebSecurityAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/security/servlet/ManagementWebSecurityAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,7 @@ import java.io.IOException; import java.util.List; +import java.util.function.Supplier; import org.junit.jupiter.api.Test; @@ -36,6 +37,9 @@ import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.assertj.AssertableWebApplicationContext; import org.springframework.boot.test.context.runner.WebApplicationContextRunner; +import org.springframework.boot.web.context.WebServerApplicationContext; +import org.springframework.boot.web.server.WebServer; +import org.springframework.boot.web.servlet.context.AnnotationConfigServletWebApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.annotation.Order; @@ -48,6 +52,7 @@ import org.springframework.security.web.FilterChainProxy; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.web.context.ConfigurableWebApplicationContext; import org.springframework.web.context.WebApplicationContext; import static org.assertj.core.api.Assertions.assertThat; @@ -63,11 +68,17 @@ class ManagementWebSecurityAutoConfigurationTests { private static final String MANAGEMENT_SECURITY_FILTER_CHAIN_BEAN = "managementSecurityFilterChain"; - private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner().withConfiguration( - AutoConfigurations.of(HealthContributorAutoConfiguration.class, HealthEndpointAutoConfiguration.class, - InfoEndpointAutoConfiguration.class, EnvironmentEndpointAutoConfiguration.class, - EndpointAutoConfiguration.class, WebMvcAutoConfiguration.class, WebEndpointAutoConfiguration.class, - SecurityAutoConfiguration.class, ManagementWebSecurityAutoConfiguration.class)); + private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner(contextSupplier(), + WebServerApplicationContext.class) + .withConfiguration(AutoConfigurations.of(HealthContributorAutoConfiguration.class, + HealthEndpointAutoConfiguration.class, InfoEndpointAutoConfiguration.class, + EnvironmentEndpointAutoConfiguration.class, EndpointAutoConfiguration.class, + WebMvcAutoConfiguration.class, WebEndpointAutoConfiguration.class, SecurityAutoConfiguration.class, + ManagementWebSecurityAutoConfiguration.class)); + + private static Supplier contextSupplier() { + return WebApplicationContextRunner.withMockServletContext(MockWebServerApplicationContext::new); + } @Test void permitAllForHealth() { @@ -159,6 +170,33 @@ void backOffIfRemoteDevToolsSecurityFilterChainIsPresent() { }); } + @Test + void withAdditionalPathsOnSamePort() { + this.contextRunner + .withPropertyValues("management.endpoint.health.group.test1.include=*", + "management.endpoint.health.group.test2.include=*", + "management.endpoint.health.group.test1.additional-path=server:/check1", + "management.endpoint.health.group.test2.additional-path=management:/check2") + .run((context) -> { + assertThat(getResponseStatus(context, "/check1")).isEqualTo(HttpStatus.OK); + assertThat(getResponseStatus(context, "/check2")).isEqualTo(HttpStatus.UNAUTHORIZED); + assertThat(getResponseStatus(context, "/actuator/health")).isEqualTo(HttpStatus.OK); + }); + } + + @Test + void withAdditionalPathsOnDifferentPort() { + this.contextRunner.withPropertyValues("management.endpoint.health.group.test1.include=*", + "management.endpoint.health.group.test2.include=*", + "management.endpoint.health.group.test1.additional-path=server:/check1", + "management.endpoint.health.group.test2.additional-path=management:/check2", "management.server.port=0") + .run((context) -> { + assertThat(getResponseStatus(context, "/check1")).isEqualTo(HttpStatus.OK); + assertThat(getResponseStatus(context, "/check2")).isEqualTo(HttpStatus.UNAUTHORIZED); + assertThat(getResponseStatus(context, "/actuator/health")).isEqualTo(HttpStatus.UNAUTHORIZED); + }); + } + private HttpStatus getResponseStatus(AssertableWebApplicationContext context, String path) throws IOException, jakarta.servlet.ServletException { FilterChainProxy filterChainProxy = context.getBean(FilterChainProxy.class); @@ -214,4 +252,19 @@ SecurityFilterChain testRemoteDevToolsSecurityFilterChain(HttpSecurity http) thr } + static class MockWebServerApplicationContext extends AnnotationConfigServletWebApplicationContext + implements WebServerApplicationContext { + + @Override + public WebServer getWebServer() { + return null; + } + + @Override + public String getServerNamespace() { + return "server"; + } + + } + } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/AdditionalPathsMapper.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/AdditionalPathsMapper.java new file mode 100644 index 000000000000..0b390c8fb913 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/AdditionalPathsMapper.java @@ -0,0 +1,43 @@ +/* + * Copyright 2012-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.endpoint.web; + +import java.util.List; + +import org.springframework.boot.actuate.endpoint.EndpointId; + +/** + * Strategy interface used to provide a mapping between an endpoint ID and any additional + * paths where it will be exposed. + * + * @author Phillip Webb + * @since 3.4.0 + */ +@FunctionalInterface +public interface AdditionalPathsMapper { + + /** + * Resolve the additional paths for the specified {@code endpointId} and web server + * namespace. + * @param endpointId the id of an endpoint + * @param webServerNamespace the web server namespace + * @return the additional paths of the endpoint or {@code null} if this mapper doesn't + * support the given endpoint ID. + */ + List getAdditionalPaths(EndpointId endpointId, WebServerNamespace webServerNamespace); + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/PathMappedEndpoint.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/PathMappedEndpoint.java index 0a39892c9123..f75f9f1207cd 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/PathMappedEndpoint.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/PathMappedEndpoint.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,9 @@ package org.springframework.boot.actuate.endpoint.web; +import java.util.Collections; +import java.util.List; + import org.springframework.boot.actuate.endpoint.ExposableEndpoint; /** @@ -30,11 +33,23 @@ public interface PathMappedEndpoint { /** - * Return the root path of the endpoint, relative to the context that exposes it. For - * example, a root path of {@code example} would be exposed under the URL - * "/{actuator-context}/example". + * Return the root path of the endpoint (relative to the context and base path) that + * exposes it. For example, a root path of {@code example} would be exposed under the + * URL "/{actuator-context}/example". * @return the root path for the endpoint + * @see PathMappedEndpoints#getBasePath */ String getRootPath(); + /** + * Return any additional paths (relative to the context) for the given + * {@link WebServerNamespace}. + * @param webServerNamespace the web server namespace + * @return a list of additional paths + * @since 3.4.0 + */ + default List getAdditionalPaths(WebServerNamespace webServerNamespace) { + return Collections.emptyList(); + } + } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/PathMappedEndpoints.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/PathMappedEndpoints.java index c8be88751b18..96995b2e0daf 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/PathMappedEndpoints.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/PathMappedEndpoints.java @@ -20,12 +20,14 @@ import java.util.Collections; import java.util.Iterator; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import java.util.stream.Stream; import org.springframework.boot.actuate.endpoint.EndpointId; import org.springframework.boot.actuate.endpoint.EndpointsSupplier; import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; /** * A collection of {@link PathMappedEndpoint path mapped endpoints}. @@ -101,7 +103,7 @@ public String getPath(EndpointId endpointId) { } /** - * Return the root paths for each mapped endpoint. + * Return the root paths for each mapped endpoint (excluding additional paths). * @return all root paths */ public Collection getAllRootPaths() { @@ -109,13 +111,36 @@ public Collection getAllRootPaths() { } /** - * Return the full paths for each mapped endpoint. + * Return the full paths for each mapped endpoint (excluding additional paths). * @return all root paths */ public Collection getAllPaths() { return stream().map(this::getPath).toList(); } + /** + * Return the additional paths for each mapped endpoint. + * @param webServerNamespace the web server namespace + * @param endpointId the endpoint ID + * @return all additional paths + * @since 3.4.0 + */ + public Collection getAdditionalPaths(WebServerNamespace webServerNamespace, EndpointId endpointId) { + return getAdditionalPaths(webServerNamespace, getEndpoint(endpointId)).toList(); + } + + private Stream getAdditionalPaths(WebServerNamespace webServerNamespace, PathMappedEndpoint endpoint) { + List additionalPaths = (endpoint != null) ? endpoint.getAdditionalPaths(webServerNamespace) : null; + if (CollectionUtils.isEmpty(additionalPaths)) { + return Stream.empty(); + } + return additionalPaths.stream().map(this::getAdditionalPath); + } + + private String getAdditionalPath(String path) { + return path.startsWith("/") ? path : "/" + path; + } + /** * Return the {@link PathMappedEndpoint} with the given ID or {@code null} if the * endpoint cannot be found. diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/WebServerNamespace.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/WebServerNamespace.java index 97b1ccee36d0..f0637492d01e 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/WebServerNamespace.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/WebServerNamespace.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,7 +19,8 @@ import org.springframework.util.StringUtils; /** - * Enumeration of server namespaces. + * A web server namespace used for disambiguation when multiple web servers are running in + * the same application (for example a management context running on a different port). * * @author Phillip Webb * @author Madhura Bhave @@ -43,17 +44,14 @@ private WebServerNamespace(String value) { this.value = value; } + /** + * Return the value of the namespace. + * @return the value + */ public String getValue() { return this.value; } - public static WebServerNamespace from(String value) { - if (StringUtils.hasText(value)) { - return new WebServerNamespace(value); - } - return SERVER; - } - @Override public boolean equals(Object obj) { if (this == obj) { @@ -71,4 +69,22 @@ public int hashCode() { return this.value.hashCode(); } + @Override + public String toString() { + return this.value; + } + + /** + * Factory method to create a new {@link WebServerNamespace} from a value. If the + * value is empty or {@code null} then {@link #SERVER} is returned. + * @param value the namespace value or {@code null} + * @return the web server namespace + */ + public static WebServerNamespace from(String value) { + if (StringUtils.hasText(value)) { + return new WebServerNamespace(value); + } + return SERVER; + } + } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/annotation/DiscoveredWebEndpoint.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/annotation/DiscoveredWebEndpoint.java index b62f4f6dda92..7543284f4645 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/annotation/DiscoveredWebEndpoint.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/annotation/DiscoveredWebEndpoint.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,12 +17,16 @@ package org.springframework.boot.actuate.endpoint.web.annotation; import java.util.Collection; +import java.util.List; +import java.util.stream.Stream; import org.springframework.boot.actuate.endpoint.EndpointId; import org.springframework.boot.actuate.endpoint.annotation.AbstractDiscoveredEndpoint; import org.springframework.boot.actuate.endpoint.annotation.EndpointDiscoverer; +import org.springframework.boot.actuate.endpoint.web.AdditionalPathsMapper; import org.springframework.boot.actuate.endpoint.web.ExposableWebEndpoint; import org.springframework.boot.actuate.endpoint.web.WebOperation; +import org.springframework.boot.actuate.endpoint.web.WebServerNamespace; /** * A discovered {@link ExposableWebEndpoint web endpoint}. @@ -33,10 +37,14 @@ class DiscoveredWebEndpoint extends AbstractDiscoveredEndpoint imp private final String rootPath; + private Collection additionalPathsMappers; + DiscoveredWebEndpoint(EndpointDiscoverer discoverer, Object endpointBean, EndpointId id, String rootPath, - boolean enabledByDefault, Collection operations) { + boolean enabledByDefault, Collection operations, + Collection additionalPathsMappers) { super(discoverer, endpointBean, id, enabledByDefault, operations); this.rootPath = rootPath; + this.additionalPathsMappers = additionalPathsMappers; } @Override @@ -44,4 +52,15 @@ public String getRootPath() { return this.rootPath; } + @Override + public List getAdditionalPaths(WebServerNamespace webServerNamespace) { + return this.additionalPathsMappers.stream() + .flatMap((mapper) -> getAdditionalPaths(webServerNamespace, mapper)) + .toList(); + } + + private Stream getAdditionalPaths(WebServerNamespace webServerNamespace, AdditionalPathsMapper mapper) { + return mapper.getAdditionalPaths(getEndpointId(), webServerNamespace).stream(); + } + } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/annotation/WebEndpointDiscoverer.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/annotation/WebEndpointDiscoverer.java index 7b6dc7f510e9..aa8f9c3b0112 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/annotation/WebEndpointDiscoverer.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/annotation/WebEndpointDiscoverer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,7 @@ package org.springframework.boot.actuate.endpoint.web.annotation; import java.util.Collection; +import java.util.Collections; import java.util.List; import org.springframework.aot.hint.MemberCategory; @@ -29,6 +30,7 @@ import org.springframework.boot.actuate.endpoint.invoke.OperationInvoker; import org.springframework.boot.actuate.endpoint.invoke.OperationInvokerAdvisor; import org.springframework.boot.actuate.endpoint.invoke.ParameterValueMapper; +import org.springframework.boot.actuate.endpoint.web.AdditionalPathsMapper; import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes; import org.springframework.boot.actuate.endpoint.web.ExposableWebEndpoint; import org.springframework.boot.actuate.endpoint.web.PathMapper; @@ -51,6 +53,8 @@ public class WebEndpointDiscoverer extends EndpointDiscoverer endpointPathMappers; + private final List additionalPathsMappers; + private final RequestPredicateFactory requestPredicateFactory; /** @@ -61,13 +65,37 @@ public class WebEndpointDiscoverer extends EndpointDiscoverer endpointPathMappers, Collection invokerAdvisors, Collection> filters) { + this(applicationContext, parameterValueMapper, endpointMediaTypes, endpointPathMappers, Collections.emptyList(), + invokerAdvisors, filters); + } + + /** + * Create a new {@link WebEndpointDiscoverer} instance. + * @param applicationContext the source application context + * @param parameterValueMapper the parameter value mapper + * @param endpointMediaTypes the endpoint media types + * @param endpointPathMappers the endpoint path mappers + * @param additionalPathsMappers the + * @param invokerAdvisors invoker advisors to apply + * @param filters filters to apply + * @since 3.4.0 + */ + public WebEndpointDiscoverer(ApplicationContext applicationContext, ParameterValueMapper parameterValueMapper, + EndpointMediaTypes endpointMediaTypes, List endpointPathMappers, + List additionalPathsMappers, Collection invokerAdvisors, + Collection> filters) { super(applicationContext, parameterValueMapper, invokerAdvisors, filters); - this.endpointPathMappers = endpointPathMappers; + this.endpointPathMappers = (endpointPathMappers != null) ? endpointPathMappers : Collections.emptyList(); + this.additionalPathsMappers = (additionalPathsMappers != null) ? additionalPathsMappers + : Collections.emptyList(); this.requestPredicateFactory = new RequestPredicateFactory(endpointMediaTypes); } @@ -75,7 +103,8 @@ public WebEndpointDiscoverer(ApplicationContext applicationContext, ParameterVal protected ExposableWebEndpoint createEndpoint(Object endpointBean, EndpointId id, boolean enabledByDefault, Collection operations) { String rootPath = PathMapper.getRootPath(this.endpointPathMappers, id); - return new DiscoveredWebEndpoint(this, endpointBean, id, rootPath, enabledByDefault, operations); + return new DiscoveredWebEndpoint(this, endpointBean, id, rootPath, enabledByDefault, operations, + this.additionalPathsMappers); } @Override diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/PathMappedEndpointsTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/PathMappedEndpointsTests.java index 37e3596fdfb0..1d5b2b466c15 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/PathMappedEndpointsTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/PathMappedEndpointsTests.java @@ -129,19 +129,36 @@ void getEndpointWhenMissingIdShouldReturnNull() { assertThat(mapped.getEndpoint(EndpointId.of("xx"))).isNull(); } + @Test + void getAdditionalPathsShouldReturnCanonicalAdditionalPaths() { + PathMappedEndpoints mapped = createTestMapped(null); + assertThat(mapped.getAdditionalPaths(WebServerNamespace.SERVER, EndpointId.of("e2"))).containsExactly("/a2", + "/A2"); + assertThat(mapped.getAdditionalPaths(WebServerNamespace.MANAGEMENT, EndpointId.of("e2"))).isEmpty(); + assertThat(mapped.getAdditionalPaths(WebServerNamespace.SERVER, EndpointId.of("e3"))).isEmpty(); + } + private PathMappedEndpoints createTestMapped(String basePath) { List> endpoints = new ArrayList<>(); endpoints.add(mockEndpoint(EndpointId.of("e1"))); - endpoints.add(mockEndpoint(EndpointId.of("e2"), "p2")); + endpoints.add(mockEndpoint(EndpointId.of("e2"), "p2", WebServerNamespace.SERVER, List.of("/a2", "A2"))); endpoints.add(mockEndpoint(EndpointId.of("e3"), "p3")); endpoints.add(mockEndpoint(EndpointId.of("e4"))); return new PathMappedEndpoints(basePath, () -> endpoints); } private TestPathMappedEndpoint mockEndpoint(EndpointId id, String rootPath) { + return mockEndpoint(id, rootPath, null, null); + } + + private TestPathMappedEndpoint mockEndpoint(EndpointId id, String rootPath, WebServerNamespace webServerNamespace, + List additionalPaths) { TestPathMappedEndpoint endpoint = mock(TestPathMappedEndpoint.class); given(endpoint.getEndpointId()).willReturn(id); given(endpoint.getRootPath()).willReturn(rootPath); + if (webServerNamespace != null && additionalPaths != null) { + given(endpoint.getAdditionalPaths(webServerNamespace)).willReturn(additionalPaths); + } return endpoint; } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/WebServerNamespaceTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/WebServerNamespaceTests.java index 91eaebb7f0e9..add2c43fc2e3 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/WebServerNamespaceTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/WebServerNamespaceTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -53,4 +53,9 @@ void namespaceWithDifferentValuesAreNotEqual() { assertThat(WebServerNamespace.from("value")).isNotEqualTo(WebServerNamespace.from("other")); } + @Test + void toStringReturnsString() { + assertThat(WebServerNamespace.from("value")).hasToString("value"); + } + } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/annotation/BaseConfiguration.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/annotation/BaseConfiguration.java index 6645dc5b64a9..55ef50552b88 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/annotation/BaseConfiguration.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/annotation/BaseConfiguration.java @@ -68,7 +68,8 @@ WebEndpointDiscoverer webEndpointDiscoverer(EndpointMediaTypes endpointMediaType ParameterValueMapper parameterMapper = new ConversionServiceParameterValueMapper( DefaultConversionService.getSharedInstance()); return new WebEndpointDiscoverer(applicationContext, parameterMapper, endpointMediaTypes, - pathMappers.orderedStream().toList(), Collections.emptyList(), Collections.emptyList()); + pathMappers.orderedStream().toList(), Collections.emptyList(), Collections.emptyList(), + Collections.emptyList()); } @Bean diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/annotation/WebEndpointDiscovererTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/annotation/WebEndpointDiscovererTests.java index 02cc46336e3a..def37498209a 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/annotation/WebEndpointDiscovererTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/annotation/WebEndpointDiscovererTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -43,12 +43,14 @@ import org.springframework.boot.actuate.endpoint.invoker.cache.CachingOperationInvoker; import org.springframework.boot.actuate.endpoint.invoker.cache.CachingOperationInvokerAdvisor; import org.springframework.boot.actuate.endpoint.jmx.annotation.JmxEndpoint; +import org.springframework.boot.actuate.endpoint.web.AdditionalPathsMapper; import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes; import org.springframework.boot.actuate.endpoint.web.ExposableWebEndpoint; import org.springframework.boot.actuate.endpoint.web.PathMapper; import org.springframework.boot.actuate.endpoint.web.WebEndpointHttpMethod; import org.springframework.boot.actuate.endpoint.web.WebOperation; import org.springframework.boot.actuate.endpoint.web.WebOperationRequestPredicate; +import org.springframework.boot.actuate.endpoint.web.WebServerNamespace; import org.springframework.boot.actuate.endpoint.web.annotation.WebEndpointDiscoverer.WebEndpointDiscovererRuntimeHints; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; @@ -223,6 +225,23 @@ void getEndpointsWhenHasCustomPathShouldReturnCustomPath() { }); } + @Test + void getEndpointsWhenHasAdditionalPaths() { + AdditionalPathsMapper additionalPathsMapper = (id, webServerNamespace) -> { + if (!WebServerNamespace.SERVER.equals(webServerNamespace)) { + return Collections.emptyList(); + } + return List.of("/test"); + }; + load((id) -> null, EndpointId::toString, additionalPathsMapper, + AdditionalOperationWebEndpointConfiguration.class, (discoverer) -> { + Map endpoints = mapEndpoints(discoverer.getEndpoints()); + ExposableWebEndpoint endpoint = endpoints.get(EndpointId.of("test")); + assertThat(endpoint.getAdditionalPaths(WebServerNamespace.SERVER)).containsExactly("/test"); + assertThat(endpoint.getAdditionalPaths(WebServerNamespace.MANAGEMENT)).isEmpty(); + }); + } + @Test void shouldRegisterHints() { RuntimeHints runtimeHints = new RuntimeHints(); @@ -230,7 +249,6 @@ void shouldRegisterHints() { assertThat(RuntimeHintsPredicates.reflection() .onType(WebEndpointFilter.class) .withMemberCategories(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS)).accepts(runtimeHints); - } private void load(Class configuration, Consumer consumer) { @@ -239,6 +257,12 @@ private void load(Class configuration, Consumer consum private void load(Function timeToLive, PathMapper endpointPathMapper, Class configuration, Consumer consumer) { + load(timeToLive, endpointPathMapper, null, configuration, consumer); + } + + private void load(Function timeToLive, PathMapper endpointPathMapper, + AdditionalPathsMapper additionalPathsMapper, Class configuration, + Consumer consumer) { try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(configuration)) { ConversionServiceParameterValueMapper parameterMapper = new ConversionServiceParameterValueMapper( DefaultConversionService.getSharedInstance()); @@ -246,6 +270,7 @@ private void load(Function timeToLive, PathMapper endpointPath Collections.singletonList("application/json")); WebEndpointDiscoverer discoverer = new WebEndpointDiscoverer(context, parameterMapper, mediaTypes, Collections.singletonList(endpointPathMapper), + (additionalPathsMapper != null) ? Collections.singletonList(additionalPathsMapper) : null, Collections.singleton(new CachingOperationInvokerAdvisor(timeToLive)), Collections.emptyList()); consumer.accept(discoverer); } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/test/WebEndpointTestInvocationContextProvider.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/test/WebEndpointTestInvocationContextProvider.java index c0ac708f205e..ffc396f20e7e 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/test/WebEndpointTestInvocationContextProvider.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/test/WebEndpointTestInvocationContextProvider.java @@ -243,7 +243,7 @@ private void customize(ResourceConfig config) { EndpointMediaTypes endpointMediaTypes = EndpointMediaTypes.DEFAULT; WebEndpointDiscoverer discoverer = new WebEndpointDiscoverer(this.applicationContext, new ConversionServiceParameterValueMapper(), endpointMediaTypes, null, Collections.emptyList(), - Collections.emptyList()); + Collections.emptyList(), Collections.emptyList()); Collection resources = new JerseyEndpointResourceFactory().createEndpointResources( new EndpointMapping("/actuator"), discoverer.getEndpoints(), endpointMediaTypes, new EndpointLinksResolver(discoverer.getEndpoints()), true); @@ -288,8 +288,8 @@ HttpHandler httpHandler(ApplicationContext applicationContext) { WebFluxEndpointHandlerMapping webEndpointReactiveHandlerMapping() { EndpointMediaTypes endpointMediaTypes = EndpointMediaTypes.DEFAULT; WebEndpointDiscoverer discoverer = new WebEndpointDiscoverer(this.applicationContext, - new ConversionServiceParameterValueMapper(), endpointMediaTypes, null, Collections.emptyList(), - Collections.emptyList()); + new ConversionServiceParameterValueMapper(), endpointMediaTypes, Collections.emptyList(), + Collections.emptyList(), Collections.emptyList(), Collections.emptyList()); return new WebFluxEndpointHandlerMapping(new EndpointMapping("/actuator"), discoverer.getEndpoints(), endpointMediaTypes, new CorsConfiguration(), new EndpointLinksResolver(discoverer.getEndpoints()), true); @@ -317,8 +317,8 @@ TomcatServletWebServerFactory tomcat() { WebMvcEndpointHandlerMapping webEndpointServletHandlerMapping() { EndpointMediaTypes endpointMediaTypes = EndpointMediaTypes.DEFAULT; WebEndpointDiscoverer discoverer = new WebEndpointDiscoverer(this.applicationContext, - new ConversionServiceParameterValueMapper(), endpointMediaTypes, null, Collections.emptyList(), - Collections.emptyList()); + new ConversionServiceParameterValueMapper(), endpointMediaTypes, Collections.emptyList(), + Collections.emptyList(), Collections.emptyList(), Collections.emptyList()); return new WebMvcEndpointHandlerMapping(new EndpointMapping("/actuator"), discoverer.getEndpoints(), endpointMediaTypes, new CorsConfiguration(), new EndpointLinksResolver(discoverer.getEndpoints()), true);