From 1d0bb9c12b1205337e88a385ad5fecddf78cc864 Mon Sep 17 00:00:00 2001 From: bbbbooo Date: Wed, 11 Feb 2026 21:46:52 +0900 Subject: [PATCH] Fix EndpointRequest links matching for separate management port When management.endpoints.web.base-path is empty and management runs on a different port, EndpointRequest.toLinks() and toAnyEndpoint() do not match the links endpoint consistently. Derive the links path based on the management port type on both servlet and reactive sides, and add regression tests for each implementation. Fixes #34834 Signed-off-by: bbbbooo --- .../actuate/web/reactive/EndpointRequest.java | 25 +++++++++--- .../actuate/web/servlet/EndpointRequest.java | 31 ++++++++++---- .../web/reactive/EndpointRequestTests.java | 40 ++++++++++++++++++- .../web/servlet/EndpointRequestTests.java | 40 ++++++++++++++++++- 4 files changed, 119 insertions(+), 17 deletions(-) diff --git a/module/spring-boot-security/src/main/java/org/springframework/boot/security/autoconfigure/actuate/web/reactive/EndpointRequest.java b/module/spring-boot-security/src/main/java/org/springframework/boot/security/autoconfigure/actuate/web/reactive/EndpointRequest.java index 46601f6cccc6..7a3eddac6646 100644 --- a/module/spring-boot-security/src/main/java/org/springframework/boot/security/autoconfigure/actuate/web/reactive/EndpointRequest.java +++ b/module/spring-boot-security/src/main/java/org/springframework/boot/security/autoconfigure/actuate/web/reactive/EndpointRequest.java @@ -236,6 +236,13 @@ && getServerNamespace(applicationContext) == null return (applicationContext != null) ? applicationContext.getParent() : null; } + protected final @Nullable String getLinksPath(String basePath) { + if (StringUtils.hasText(basePath)) { + return basePath; + } + return (this.managementPortType == ManagementPortType.DIFFERENT) ? "/" : null; + } + protected final String toString(List endpoints, String emptyValue) { return (!endpoints.isEmpty()) ? endpoints.stream() .map(this::getEndpointId) @@ -334,7 +341,8 @@ protected ServerWebExchangeMatcher createDelegate(PathMappedEndpoints endpoints) streamPaths(this.includes, endpoints).forEach(paths::add); streamPaths(this.excludes, endpoints).forEach(paths::remove); List delegateMatchers = getDelegateMatchers(paths, this.httpMethod); - if (this.includeLinks && StringUtils.hasText(endpoints.getBasePath())) { + String linksPath = getLinksPath(endpoints.getBasePath()); + if (this.includeLinks && linksPath != null) { delegateMatchers.add(new LinksServerWebExchangeMatcher()); } if (delegateMatchers.isEmpty()) { @@ -370,10 +378,17 @@ private LinksServerWebExchangeMatcher() { @Override protected ServerWebExchangeMatcher createDelegate(WebEndpointProperties properties) { - if (StringUtils.hasText(properties.getBasePath())) { - return new OrServerWebExchangeMatcher( - new PathPatternParserServerWebExchangeMatcher(properties.getBasePath()), - new PathPatternParserServerWebExchangeMatcher(properties.getBasePath() + "/")); + String linksPath = getLinksPath(properties.getBasePath()); + if (linksPath != null) { + List linksMatchers = new ArrayList<>(); + linksMatchers.add(new PathPatternParserServerWebExchangeMatcher(linksPath)); + if ("/".equals(linksPath)) { + linksMatchers.add(new PathPatternParserServerWebExchangeMatcher("")); + } + else { + linksMatchers.add(new PathPatternParserServerWebExchangeMatcher(linksPath + "/")); + } + return new OrServerWebExchangeMatcher(linksMatchers); } return EMPTY_MATCHER; } diff --git a/module/spring-boot-security/src/main/java/org/springframework/boot/security/autoconfigure/actuate/web/servlet/EndpointRequest.java b/module/spring-boot-security/src/main/java/org/springframework/boot/security/autoconfigure/actuate/web/servlet/EndpointRequest.java index 3f6f50a03f08..28faf06aabb2 100644 --- a/module/spring-boot-security/src/main/java/org/springframework/boot/security/autoconfigure/actuate/web/servlet/EndpointRequest.java +++ b/module/spring-boot-security/src/main/java/org/springframework/boot/security/autoconfigure/actuate/web/servlet/EndpointRequest.java @@ -224,13 +224,27 @@ protected final List getDelegateMatchers(RequestMatcherFactory r } protected List getLinksMatchers(RequestMatcherFactory requestMatcherFactory, - RequestMatcherProvider matcherProvider, String basePath) { + RequestMatcherProvider matcherProvider, String linksPath) { List linksMatchers = new ArrayList<>(); - linksMatchers.add(requestMatcherFactory.antPath(matcherProvider, null, basePath)); - linksMatchers.add(requestMatcherFactory.antPath(matcherProvider, null, basePath, "/")); + linksMatchers.add(requestMatcherFactory.antPath(matcherProvider, null, linksPath)); + if (!"/".equals(linksPath)) { + linksMatchers.add(requestMatcherFactory.antPath(matcherProvider, null, linksPath, "/")); + } return linksMatchers; } + protected @Nullable String getLinksPath(WebApplicationContext context, String basePath) { + if (StringUtils.hasText(basePath)) { + return basePath; + } + ManagementPortType managementPortType = this.managementPortType; + if (managementPortType == null) { + managementPortType = ManagementPortType.get(context.getEnvironment()); + this.managementPortType = managementPortType; + } + return (managementPortType == ManagementPortType.DIFFERENT) ? "/" : null; + } + protected RequestMatcherProvider getRequestMatcherProvider(WebApplicationContext context) { try { return context.getBean(RequestMatcherProvider.class); @@ -341,8 +355,9 @@ protected RequestMatcher createDelegate(WebApplicationContext context, List delegateMatchers = getDelegateMatchers(requestMatcherFactory, matcherProvider, paths, this.httpMethod); String basePath = endpoints.getBasePath(); - if (this.includeLinks && StringUtils.hasText(basePath)) { - delegateMatchers.addAll(getLinksMatchers(requestMatcherFactory, matcherProvider, basePath)); + String linksPath = getLinksPath(context, basePath); + if (this.includeLinks && linksPath != null) { + delegateMatchers.addAll(getLinksMatchers(requestMatcherFactory, matcherProvider, linksPath)); } if (delegateMatchers.isEmpty()) { return EMPTY_MATCHER; @@ -375,10 +390,10 @@ public static final class LinksRequestMatcher extends AbstractRequestMatcher { protected RequestMatcher createDelegate(WebApplicationContext context, RequestMatcherFactory requestMatcherFactory) { WebEndpointProperties properties = context.getBean(WebEndpointProperties.class); - String basePath = properties.getBasePath(); - if (StringUtils.hasText(basePath)) { + String linksPath = getLinksPath(context, properties.getBasePath()); + if (linksPath != null) { return new OrRequestMatcher( - getLinksMatchers(requestMatcherFactory, getRequestMatcherProvider(context), basePath)); + getLinksMatchers(requestMatcherFactory, getRequestMatcherProvider(context), linksPath)); } return EMPTY_MATCHER; } diff --git a/module/spring-boot-security/src/test/java/org/springframework/boot/security/autoconfigure/actuate/web/reactive/EndpointRequestTests.java b/module/spring-boot-security/src/test/java/org/springframework/boot/security/autoconfigure/actuate/web/reactive/EndpointRequestTests.java index 630913a10ec2..74123cb201b4 100644 --- a/module/spring-boot-security/src/test/java/org/springframework/boot/security/autoconfigure/actuate/web/reactive/EndpointRequestTests.java +++ b/module/spring-boot-security/src/test/java/org/springframework/boot/security/autoconfigure/actuate/web/reactive/EndpointRequestTests.java @@ -20,6 +20,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Map; import org.assertj.core.api.AssertDelegateTarget; import org.jspecify.annotations.Nullable; @@ -36,6 +37,7 @@ import org.springframework.boot.web.server.WebServer; import org.springframework.boot.web.server.context.WebServerApplicationContext; import org.springframework.context.support.StaticApplicationContext; +import org.springframework.core.env.MapPropertySource; import org.springframework.http.HttpMethod; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpResponse; @@ -93,6 +95,15 @@ void toAnyEndpointWhenBasePathIsEmptyShouldNotMatchLinks() { assertMatcher.matches("/bar"); } + @Test + void toAnyEndpointWhenBasePathIsEmptyAndManagementPortDifferentShouldMatchLinks() { + ServerWebExchangeMatcher matcher = EndpointRequest.toAnyEndpoint(); + RequestMatcherAssert assertMatcher = assertMatcher(matcher, mockPathMappedEndpoints(""), + WebServerNamespace.MANAGEMENT, true); + assertMatcher.matches("/"); + assertMatcher.matches("/foo"); + } + @Test void toAnyEndpointShouldNotMatchOtherPath() { ServerWebExchangeMatcher matcher = EndpointRequest.toAnyEndpoint(); @@ -145,6 +156,15 @@ void toLinksWhenBasePathEmptyShouldNotMatch() { assertMatcher.doesNotMatch("/"); } + @Test + void toLinksWhenBasePathEmptyAndManagementPortDifferentShouldMatchRoot() { + ServerWebExchangeMatcher matcher = EndpointRequest.toLinks(); + RequestMatcherAssert assertMatcher = assertMatcher(matcher, mockPathMappedEndpoints(""), + WebServerNamespace.MANAGEMENT, true); + assertMatcher.matches("/"); + assertMatcher.doesNotMatch("/foo"); + } + @Test void excludeByClassShouldNotMatchExcluded() { ServerWebExchangeMatcher matcher = EndpointRequest.toAnyEndpoint() @@ -327,10 +347,26 @@ private RequestMatcherAssert assertMatcher(ServerWebExchangeMatcher matcher, private RequestMatcherAssert assertMatcher(ServerWebExchangeMatcher matcher, @Nullable PathMappedEndpoints pathMappedEndpoints, @Nullable WebServerNamespace namespace) { + return assertMatcher(matcher, pathMappedEndpoints, namespace, false); + } + + private RequestMatcherAssert assertMatcher(ServerWebExchangeMatcher matcher, + @Nullable PathMappedEndpoints pathMappedEndpoints, @Nullable WebServerNamespace namespace, + boolean managementPortDifferent) { StaticApplicationContext context = new StaticApplicationContext(); if (namespace != null && !WebServerNamespace.SERVER.equals(namespace)) { - NamedStaticWebApplicationContext parentContext = new NamedStaticWebApplicationContext(namespace); - context.setParent(parentContext); + if (managementPortDifferent) { + context = new NamedStaticWebApplicationContext(namespace); + } + else { + NamedStaticWebApplicationContext parentContext = new NamedStaticWebApplicationContext(namespace); + context.setParent(parentContext); + } + } + if (managementPortDifferent) { + context.getEnvironment() + .getPropertySources() + .addFirst(new MapPropertySource("test", Map.of("management.server.port", 0))); } context.registerBean(WebEndpointProperties.class); if (pathMappedEndpoints != null) { diff --git a/module/spring-boot-security/src/test/java/org/springframework/boot/security/autoconfigure/actuate/web/servlet/EndpointRequestTests.java b/module/spring-boot-security/src/test/java/org/springframework/boot/security/autoconfigure/actuate/web/servlet/EndpointRequestTests.java index dade805d37fe..8939d265b5ba 100644 --- a/module/spring-boot-security/src/test/java/org/springframework/boot/security/autoconfigure/actuate/web/servlet/EndpointRequestTests.java +++ b/module/spring-boot-security/src/test/java/org/springframework/boot/security/autoconfigure/actuate/web/servlet/EndpointRequestTests.java @@ -19,6 +19,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Map; import jakarta.servlet.http.HttpServletRequest; import org.assertj.core.api.AssertDelegateTarget; @@ -37,6 +38,7 @@ import org.springframework.boot.security.autoconfigure.actuate.web.servlet.EndpointRequest.EndpointRequestMatcher; import org.springframework.boot.web.server.WebServer; import org.springframework.boot.web.server.context.WebServerApplicationContext; +import org.springframework.core.env.MapPropertySource; import org.springframework.http.HttpMethod; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockServletContext; @@ -92,6 +94,15 @@ void toAnyEndpointWhenBasePathIsEmptyShouldNotMatchLinks() { assertMatcher.matches("/bar"); } + @Test + void toAnyEndpointWhenBasePathIsEmptyAndManagementPortDifferentShouldMatchLinks() { + RequestMatcher matcher = EndpointRequest.toAnyEndpoint(); + RequestMatcherAssert assertMatcher = assertMatcher(matcher, mockPathMappedEndpoints(""), null, + WebServerNamespace.MANAGEMENT, true); + assertMatcher.matches("/"); + assertMatcher.matches("/foo"); + } + @Test void toAnyEndpointShouldNotMatchOtherPath() { RequestMatcher matcher = EndpointRequest.toAnyEndpoint(); @@ -151,6 +162,15 @@ void toLinksWhenBasePathEmptyShouldNotMatch() { assertMatcher.doesNotMatch("/"); } + @Test + void toLinksWhenBasePathEmptyAndManagementPortDifferentShouldMatchRoot() { + RequestMatcher matcher = EndpointRequest.toLinks(); + RequestMatcherAssert assertMatcher = assertMatcher(matcher, mockPathMappedEndpoints(""), null, + WebServerNamespace.MANAGEMENT, true); + assertMatcher.matches("/"); + assertMatcher.doesNotMatch("/foo"); + } + @Test void excludeByClassShouldNotMatchExcluded() { RequestMatcher matcher = EndpointRequest.toAnyEndpoint().excluding(FooEndpoint.class, BazServletEndpoint.class); @@ -353,10 +373,26 @@ private RequestMatcherAssert assertMatcher(RequestMatcher matcher, private RequestMatcherAssert assertMatcher(RequestMatcher matcher, @Nullable PathMappedEndpoints pathMappedEndpoints, @Nullable RequestMatcherProvider matcherProvider, @Nullable WebServerNamespace namespace) { + return assertMatcher(matcher, pathMappedEndpoints, matcherProvider, namespace, false); + } + + private RequestMatcherAssert assertMatcher(RequestMatcher matcher, + @Nullable PathMappedEndpoints pathMappedEndpoints, @Nullable RequestMatcherProvider matcherProvider, + @Nullable WebServerNamespace namespace, boolean managementPortDifferent) { StaticWebApplicationContext context = new StaticWebApplicationContext(); if (namespace != null && !WebServerNamespace.SERVER.equals(namespace)) { - NamedStaticWebApplicationContext parentContext = new NamedStaticWebApplicationContext(namespace); - context.setParent(parentContext); + if (managementPortDifferent) { + context = new NamedStaticWebApplicationContext(namespace); + } + else { + NamedStaticWebApplicationContext parentContext = new NamedStaticWebApplicationContext(namespace); + context.setParent(parentContext); + } + } + if (managementPortDifferent) { + context.getEnvironment() + .getPropertySources() + .addFirst(new MapPropertySource("test", Map.of("management.server.port", 0))); } context.registerBean(WebEndpointProperties.class); if (pathMappedEndpoints != null) {