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) {