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 3bd7678862..5e0116abae 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 @@ -30,6 +30,7 @@ import java.util.stream.Stream; import reactor.core.publisher.Mono; import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties; import org.springframework.boot.actuate.endpoint.annotation.Endpoint; import org.springframework.boot.actuate.endpoint.web.PathMappedEndpoints; import org.springframework.boot.security.reactive.ApplicationContextServerWebExchangeMatcher; @@ -39,6 +40,7 @@ import org.springframework.security.web.server.util.matcher.PathPatternParserSer import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher; import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher.MatchResult; import org.springframework.util.Assert; +import org.springframework.util.StringUtils; import org.springframework.web.server.ServerWebExchange; /** @@ -57,7 +59,8 @@ public final class EndpointRequest { } /** - * Returns a matcher that includes all {@link Endpoint actuator endpoints}. The + * Returns a matcher that includes all {@link Endpoint actuator endpoints}. It also includes + * the links endpoint which is present at the base path of the actuator endpoints. The * {@link EndpointServerWebExchangeMatcher#excluding(Class...) excluding} method can * be used to further remove specific endpoints if required. For example: *
@@ -66,7 +69,7 @@ public final class EndpointRequest {
 	 * @return the configured {@link ServerWebExchangeMatcher}
 	 */
 	public static EndpointServerWebExchangeMatcher toAnyEndpoint() {
-		return new EndpointServerWebExchangeMatcher();
+		return new EndpointServerWebExchangeMatcher(true);
 	}
 
 	/**
@@ -78,7 +81,7 @@ public final class EndpointRequest {
 	 * @return the configured {@link ServerWebExchangeMatcher}
 	 */
 	public static EndpointServerWebExchangeMatcher to(Class... endpoints) {
-		return new EndpointServerWebExchangeMatcher(endpoints);
+		return new EndpointServerWebExchangeMatcher(endpoints, false);
 	}
 
 	/**
@@ -90,7 +93,21 @@ public final class EndpointRequest {
 	 * @return the configured {@link ServerWebExchangeMatcher}
 	 */
 	public static EndpointServerWebExchangeMatcher to(String... endpoints) {
-		return new EndpointServerWebExchangeMatcher(endpoints);
+		return new EndpointServerWebExchangeMatcher(endpoints, false);
+	}
+
+	/**
+	 * Returns a matcher that matches only on the links endpoint. It can be used when security configuration
+	 * for the links endpoint is different from the other {@link Endpoint actuator endpoints}. The
+	 * {@link EndpointServerWebExchangeMatcher#excludingLinks() excludingLinks} method can be used in combination with this
+	 * to remove the links endpoint from {@link EndpointRequest#toAnyEndpoint() toAnyEndpoint}.
+	 * For example: 
+	 * EndpointRequest.toLinks()
+	 * 
+ * @return the configured {@link ServerWebExchangeMatcher} + */ + public static LinksServerWebExchangeMatcher toLinks() { + return new LinksServerWebExchangeMatcher(); } /** @@ -106,35 +123,42 @@ public final class EndpointRequest { private ServerWebExchangeMatcher delegate; - private EndpointServerWebExchangeMatcher() { - this(Collections.emptyList(), Collections.emptyList()); + private boolean includeLinks; + + private EndpointServerWebExchangeMatcher(boolean includeLinks) { + this(Collections.emptyList(), Collections.emptyList(), includeLinks); } - private EndpointServerWebExchangeMatcher(Class[] endpoints) { - this(Arrays.asList((Object[]) endpoints), Collections.emptyList()); + private EndpointServerWebExchangeMatcher(Class[] endpoints, boolean includeLinks) { + this(Arrays.asList((Object[]) endpoints), Collections.emptyList(), includeLinks); } - private EndpointServerWebExchangeMatcher(String[] endpoints) { - this(Arrays.asList((Object[]) endpoints), Collections.emptyList()); + private EndpointServerWebExchangeMatcher(String[] endpoints, boolean includeLinks) { + this(Arrays.asList((Object[]) endpoints), Collections.emptyList(), includeLinks); } private EndpointServerWebExchangeMatcher(List includes, - List excludes) { + List excludes, boolean includeLinks) { super(PathMappedEndpoints.class); this.includes = includes; this.excludes = excludes; + this.includeLinks = includeLinks; } public EndpointServerWebExchangeMatcher excluding(Class... endpoints) { List excludes = new ArrayList<>(this.excludes); excludes.addAll(Arrays.asList((Object[]) endpoints)); - return new EndpointServerWebExchangeMatcher(this.includes, excludes); + return new EndpointServerWebExchangeMatcher(this.includes, excludes, this.includeLinks); } public EndpointServerWebExchangeMatcher excluding(String... endpoints) { List excludes = new ArrayList<>(this.excludes); excludes.addAll(Arrays.asList((Object[]) endpoints)); - return new EndpointServerWebExchangeMatcher(this.includes, excludes); + return new EndpointServerWebExchangeMatcher(this.includes, excludes, this.includeLinks); + } + + public EndpointServerWebExchangeMatcher excludingLinks() { + return new EndpointServerWebExchangeMatcher(this.includes, this.excludes, false); } @Override @@ -160,7 +184,11 @@ public final class EndpointRequest { } streamPaths(this.includes, pathMappedEndpoints).forEach(paths::add); streamPaths(this.excludes, pathMappedEndpoints).forEach(paths::remove); - return new OrServerWebExchangeMatcher(getDelegateMatchers(paths)); + List delegateMatchers = getDelegateMatchers(paths); + if (this.includeLinks && StringUtils.hasText(pathMappedEndpoints.getBasePath())) { + delegateMatchers.add(new PathPatternParserServerWebExchangeMatcher(pathMappedEndpoints.getBasePath())); + } + return new OrServerWebExchangeMatcher(delegateMatchers); } private Stream streamPaths(List source, @@ -200,4 +228,36 @@ public final class EndpointRequest { } + /** + * The The {@link ServerWebExchangeMatcher} used to match against the links endpoint. + */ + public static final class LinksServerWebExchangeMatcher + extends ApplicationContextServerWebExchangeMatcher { + + private ServerWebExchangeMatcher delegate; + + private LinksServerWebExchangeMatcher() { + super(WebEndpointProperties.class); + } + + @Override + protected void initialized(Supplier propertiesSupplier) { + WebEndpointProperties webEndpointProperties = propertiesSupplier.get(); + if (StringUtils.hasText(webEndpointProperties.getBasePath())) { + this.delegate = new PathPatternParserServerWebExchangeMatcher( + webEndpointProperties.getBasePath()); + } + else { + this.delegate = EMPTY_MATCHER; + } + } + + @Override + protected Mono matches(ServerWebExchange exchange, + Supplier context) { + return this.delegate.matches(exchange); + } + + } + } 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 6e6b285304..e720dfa560 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 @@ -30,6 +30,7 @@ import java.util.stream.Stream; import javax.servlet.http.HttpServletRequest; import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties; import org.springframework.boot.actuate.endpoint.annotation.Endpoint; import org.springframework.boot.actuate.endpoint.web.PathMappedEndpoints; import org.springframework.boot.security.servlet.ApplicationContextRequestMatcher; @@ -38,6 +39,7 @@ 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.StringUtils; /** * Factory that can be used to create a {@link RequestMatcher} for actuator endpoint @@ -55,7 +57,8 @@ public final class EndpointRequest { } /** - * Returns a matcher that includes all {@link Endpoint actuator endpoints}. The + * Returns a matcher that includes all {@link Endpoint actuator endpoints}. It also includes + * the links endpoint which is present at the base path of the actuator endpoints. The * {@link EndpointRequestMatcher#excluding(Class...) excluding} method can be used to * further remove specific endpoints if required. For example:
 	 * EndpointRequest.toAnyEndpoint().excluding(ShutdownEndpoint.class)
@@ -63,7 +66,7 @@ public final class EndpointRequest {
 	 * @return the configured {@link RequestMatcher}
 	 */
 	public static EndpointRequestMatcher toAnyEndpoint() {
-		return new EndpointRequestMatcher();
+		return new EndpointRequestMatcher(true);
 	}
 
 	/**
@@ -75,7 +78,7 @@ public final class EndpointRequest {
 	 * @return the configured {@link RequestMatcher}
 	 */
 	public static EndpointRequestMatcher to(Class... endpoints) {
-		return new EndpointRequestMatcher(endpoints);
+		return new EndpointRequestMatcher(endpoints, false);
 	}
 
 	/**
@@ -87,7 +90,21 @@ public final class EndpointRequest {
 	 * @return the configured {@link RequestMatcher}
 	 */
 	public static EndpointRequestMatcher to(String... endpoints) {
-		return new EndpointRequestMatcher(endpoints);
+		return new EndpointRequestMatcher(endpoints, false);
+	}
+
+	/**
+	 * Returns a matcher that matches only on the links endpoint. It can be used when security configuration
+	 * for the links endpoint is different from the other {@link Endpoint actuator endpoints}. The
+	 * {@link EndpointRequestMatcher#excludingLinks() excludingLinks} method can be used in combination with this
+	 * to remove the links endpoint from {@link EndpointRequest#toAnyEndpoint() toAnyEndpoint}.
+	 * For example: 
+	 * EndpointRequest.toLinks()
+	 * 
+ * @return the configured {@link RequestMatcher} + */ + public static LinksRequestMatcher toLinks() { + return new LinksRequestMatcher(); } /** @@ -100,36 +117,43 @@ public final class EndpointRequest { private final List excludes; + private final boolean includeLinks; + private volatile RequestMatcher delegate; - private EndpointRequestMatcher() { - this(Collections.emptyList(), Collections.emptyList()); + private EndpointRequestMatcher(boolean includeLinks) { + this(Collections.emptyList(), Collections.emptyList(), includeLinks); } - private EndpointRequestMatcher(Class[] endpoints) { - this(Arrays.asList((Object[]) endpoints), Collections.emptyList()); + private EndpointRequestMatcher(Class[] endpoints, boolean includeLinks) { + this(Arrays.asList((Object[]) endpoints), Collections.emptyList(), includeLinks); } - private EndpointRequestMatcher(String[] endpoints) { - this(Arrays.asList((Object[]) endpoints), Collections.emptyList()); + private EndpointRequestMatcher(String[] endpoints, boolean includeLinks) { + this(Arrays.asList((Object[]) endpoints), Collections.emptyList(), includeLinks); } - private EndpointRequestMatcher(List includes, List excludes) { + private EndpointRequestMatcher(List includes, List excludes, boolean includeLinks) { super(PathMappedEndpoints.class); this.includes = includes; this.excludes = excludes; + this.includeLinks = includeLinks; } public EndpointRequestMatcher excluding(Class... endpoints) { List excludes = new ArrayList<>(this.excludes); excludes.addAll(Arrays.asList((Object[]) endpoints)); - return new EndpointRequestMatcher(this.includes, excludes); + return new EndpointRequestMatcher(this.includes, excludes, this.includeLinks); } public EndpointRequestMatcher excluding(String... endpoints) { List excludes = new ArrayList<>(this.excludes); excludes.addAll(Arrays.asList((Object[]) endpoints)); - return new EndpointRequestMatcher(this.includes, excludes); + return new EndpointRequestMatcher(this.includes, excludes, this.includeLinks); + } + + public EndpointRequestMatcher excludingLinks() { + return new EndpointRequestMatcher(this.includes, this.excludes, false); } @Override @@ -154,7 +178,11 @@ public final class EndpointRequest { } streamPaths(this.includes, pathMappedEndpoints).forEach(paths::add); streamPaths(this.excludes, pathMappedEndpoints).forEach(paths::remove); - return new OrRequestMatcher(getDelegateMatchers(paths)); + List delegateMatchers = getDelegateMatchers(paths); + if (this.includeLinks && StringUtils.hasText(pathMappedEndpoints.getBasePath())) { + delegateMatchers.add(new AntPathRequestMatcher(pathMappedEndpoints.getBasePath())); + } + return new OrRequestMatcher(delegateMatchers); } private Stream streamPaths(List source, @@ -193,4 +221,33 @@ public final class EndpointRequest { } + /** + * The request matcher used to match against the links endpoint. + */ + public static final class LinksRequestMatcher + extends ApplicationContextRequestMatcher { + + private RequestMatcher delegate; + + private LinksRequestMatcher() { + super(WebEndpointProperties.class); + } + + @Override + protected void initialized(Supplier propertiesSupplier) { + WebEndpointProperties webEndpointProperties = propertiesSupplier.get(); + if (StringUtils.hasText(webEndpointProperties.getBasePath())) { + this.delegate = new AntPathRequestMatcher(webEndpointProperties.getBasePath()); + } + else { + this.delegate = EMPTY_MATCHER; + } + } + + @Override + protected boolean matches(HttpServletRequest request, Supplier context) { + return this.delegate.matches(request); + } + } + } 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 5afe932450..0d46f124d4 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 @@ -22,6 +22,7 @@ import java.util.List; import org.assertj.core.api.AssertDelegateTarget; import org.junit.Test; +import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties; import org.springframework.boot.actuate.endpoint.ExposableEndpoint; import org.springframework.boot.actuate.endpoint.Operation; import org.springframework.boot.actuate.endpoint.annotation.Endpoint; @@ -54,6 +55,16 @@ public class EndpointRequestTests { ServerWebExchangeMatcher matcher = EndpointRequest.toAnyEndpoint(); assertMatcher(matcher).matches("/actuator/foo"); assertMatcher(matcher).matches("/actuator/bar"); + assertMatcher(matcher).matches("/actuator"); + } + + @Test + public void toAnyEndpointWhenBasePathIsEmptyShouldNotMatchLinks() { + ServerWebExchangeMatcher matcher = EndpointRequest.toAnyEndpoint(); + RequestMatcherAssert assertMatcher = assertMatcher(matcher, ""); + assertMatcher.doesNotMatch("/"); + assertMatcher.matches("/foo"); + assertMatcher.matches("/bar"); } @Test @@ -86,12 +97,38 @@ public class EndpointRequestTests { assertMatcher(matcher).doesNotMatch("/actuator/bar"); } + @Test + public void toLinksShouldOnlyMatchLinks() { + ServerWebExchangeMatcher matcher = EndpointRequest.toLinks(); + assertMatcher(matcher).doesNotMatch("/actuator/foo"); + assertMatcher(matcher).doesNotMatch("/actuator/bar"); + assertMatcher(matcher).matches("/actuator"); + } + + @Test + public void toLinksWhenBasePathEmptyShouldNotMatch() { + ServerWebExchangeMatcher matcher = EndpointRequest.toLinks(); + RequestMatcherAssert assertMatcher = assertMatcher(matcher, ""); + assertMatcher.doesNotMatch("/actuator/foo"); + assertMatcher.doesNotMatch("/actuator/bar"); + assertMatcher.doesNotMatch("/"); + } + @Test public void excludeByClassShouldNotMatchExcluded() { ServerWebExchangeMatcher matcher = EndpointRequest.toAnyEndpoint() .excluding(FooEndpoint.class); assertMatcher(matcher).doesNotMatch("/actuator/foo"); assertMatcher(matcher).matches("/actuator/bar"); + assertMatcher(matcher).matches("/actuator"); + } + + @Test + public void excludeByClassShouldNotMatchLinksIfExcluded() { + ServerWebExchangeMatcher matcher = EndpointRequest.toAnyEndpoint() + .excludingLinks().excluding(FooEndpoint.class); + assertMatcher(matcher).doesNotMatch("/actuator/foo"); + assertMatcher(matcher).doesNotMatch("/actuator"); } @Test @@ -100,24 +137,54 @@ public class EndpointRequestTests { .excluding("foo"); assertMatcher(matcher).doesNotMatch("/actuator/foo"); assertMatcher(matcher).matches("/actuator/bar"); + assertMatcher(matcher).matches("/actuator"); + } + + @Test + public void excludeByIdShouldNotMatchLinksIfExcluded() { + ServerWebExchangeMatcher matcher = EndpointRequest.toAnyEndpoint() + .excludingLinks().excluding("foo"); + assertMatcher(matcher).doesNotMatch("/actuator/foo"); + assertMatcher(matcher).doesNotMatch("/actuator"); + } + + @Test + public void excludeLinksShouldNotMatchBasePath() { + ServerWebExchangeMatcher matcher = EndpointRequest.toAnyEndpoint().excludingLinks(); + assertMatcher(matcher).doesNotMatch("/actuator"); + assertMatcher(matcher).matches("/actuator/foo"); + assertMatcher(matcher).matches("/actuator/bar"); + } + + @Test + public void excludeLinksShouldNotMatchBasePathIfEmptyAndExcluded() { + ServerWebExchangeMatcher matcher = EndpointRequest.toAnyEndpoint().excludingLinks(); + RequestMatcherAssert assertMatcher = assertMatcher(matcher, ""); + assertMatcher.doesNotMatch("/"); + assertMatcher.matches("/foo"); + assertMatcher.matches("/bar"); } @Test public void noEndpointPathsBeansShouldNeverMatch() { ServerWebExchangeMatcher matcher = EndpointRequest.toAnyEndpoint(); - assertMatcher(matcher, null).doesNotMatch("/actuator/foo"); - assertMatcher(matcher, null).doesNotMatch("/actuator/bar"); + assertMatcher(matcher, (PathMappedEndpoints) null).doesNotMatch("/actuator/foo"); + assertMatcher(matcher, (PathMappedEndpoints) null).doesNotMatch("/actuator/bar"); } private RequestMatcherAssert assertMatcher(ServerWebExchangeMatcher matcher) { - return assertMatcher(matcher, mockPathMappedEndpoints()); + return assertMatcher(matcher, mockPathMappedEndpoints("/actuator")); } - private PathMappedEndpoints mockPathMappedEndpoints() { + private RequestMatcherAssert assertMatcher(ServerWebExchangeMatcher matcher, String basePath) { + return assertMatcher(matcher, mockPathMappedEndpoints(basePath)); + } + + private PathMappedEndpoints mockPathMappedEndpoints(String basePath) { List> endpoints = new ArrayList<>(); endpoints.add(mockEndpoint("foo", "foo")); endpoints.add(mockEndpoint("bar", "bar")); - return new PathMappedEndpoints("/actuator", () -> endpoints); + return new PathMappedEndpoints(basePath, () -> endpoints); } private TestEndpoint mockEndpoint(String id, String rootPath) { @@ -130,8 +197,13 @@ public class EndpointRequestTests { private RequestMatcherAssert assertMatcher(ServerWebExchangeMatcher matcher, PathMappedEndpoints pathMappedEndpoints) { StaticApplicationContext context = new StaticApplicationContext(); + context.registerBean(WebEndpointProperties.class); if (pathMappedEndpoints != null) { context.registerBean(PathMappedEndpoints.class, () -> pathMappedEndpoints); + WebEndpointProperties properties = context.getBean(WebEndpointProperties.class); + if (!properties.getBasePath().equals(pathMappedEndpoints.getBasePath())) { + properties.setBasePath(pathMappedEndpoints.getBasePath()); + } } return assertThat(new RequestMatcherAssert(context, matcher)); } 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 ab9443e88b..e33a6e49ac 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 @@ -24,6 +24,7 @@ import javax.servlet.http.HttpServletRequest; import org.assertj.core.api.AssertDelegateTarget; import org.junit.Test; +import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties; import org.springframework.boot.actuate.endpoint.ExposableEndpoint; import org.springframework.boot.actuate.endpoint.Operation; import org.springframework.boot.actuate.endpoint.annotation.Endpoint; @@ -43,6 +44,7 @@ import static org.mockito.Mockito.mock; * Tests for {@link EndpointRequest}. * * @author Phillip Webb + * @author Madhura Bhave */ public class EndpointRequestTests { @@ -51,6 +53,16 @@ public class EndpointRequestTests { RequestMatcher matcher = EndpointRequest.toAnyEndpoint(); assertMatcher(matcher).matches("/actuator/foo"); assertMatcher(matcher).matches("/actuator/bar"); + assertMatcher(matcher).matches("/actuator"); + } + + @Test + public void toAnyEndpointWhenBasePathIsEmptyShouldNotMatchLinks() { + RequestMatcher matcher = EndpointRequest.toAnyEndpoint(); + RequestMatcherAssert assertMatcher = assertMatcher(matcher, ""); + assertMatcher.doesNotMatch("/"); + assertMatcher.matches("/foo"); + assertMatcher.matches("/bar"); } @Test @@ -69,6 +81,7 @@ public class EndpointRequestTests { public void toEndpointClassShouldNotMatchOtherPath() { RequestMatcher matcher = EndpointRequest.to(FooEndpoint.class); assertMatcher(matcher).doesNotMatch("/actuator/bar"); + assertMatcher(matcher).doesNotMatch("/actuator"); } @Test @@ -81,6 +94,24 @@ public class EndpointRequestTests { public void toEndpointIdShouldNotMatchOtherPath() { RequestMatcher matcher = EndpointRequest.to("foo"); assertMatcher(matcher).doesNotMatch("/actuator/bar"); + assertMatcher(matcher).doesNotMatch("/actuator"); + } + + @Test + public void toLinksShouldOnlyMatchLinks() { + RequestMatcher matcher = EndpointRequest.toLinks(); + assertMatcher(matcher).doesNotMatch("/actuator/foo"); + assertMatcher(matcher).doesNotMatch("/actuator/bar"); + assertMatcher(matcher).matches("/actuator"); + } + + @Test + public void toLinksWhenBasePathEmptyShouldNotMatch() { + RequestMatcher matcher = EndpointRequest.toLinks(); + RequestMatcherAssert assertMatcher = assertMatcher(matcher, ""); + assertMatcher.doesNotMatch("/actuator/foo"); + assertMatcher.doesNotMatch("/actuator/bar"); + assertMatcher.doesNotMatch("/"); } @Test @@ -89,6 +120,15 @@ public class EndpointRequestTests { .excluding(FooEndpoint.class); assertMatcher(matcher).doesNotMatch("/actuator/foo"); assertMatcher(matcher).matches("/actuator/bar"); + assertMatcher(matcher).matches("/actuator"); + } + + @Test + public void excludeByClassShouldNotMatchLinksIfExcluded() { + RequestMatcher matcher = EndpointRequest.toAnyEndpoint() + .excludingLinks().excluding(FooEndpoint.class); + assertMatcher(matcher).doesNotMatch("/actuator/foo"); + assertMatcher(matcher).doesNotMatch("/actuator"); } @Test @@ -96,24 +136,54 @@ public class EndpointRequestTests { RequestMatcher matcher = EndpointRequest.toAnyEndpoint().excluding("foo"); assertMatcher(matcher).doesNotMatch("/actuator/foo"); assertMatcher(matcher).matches("/actuator/bar"); + assertMatcher(matcher).matches("/actuator"); + } + + @Test + public void excludeByIdShouldNotMatchLinksIfExcluded() { + RequestMatcher matcher = EndpointRequest.toAnyEndpoint() + .excludingLinks().excluding("foo"); + assertMatcher(matcher).doesNotMatch("/actuator/foo"); + assertMatcher(matcher).doesNotMatch("/actuator"); + } + + @Test + public void excludeLinksShouldNotMatchBasePath() { + RequestMatcher matcher = EndpointRequest.toAnyEndpoint().excludingLinks(); + assertMatcher(matcher).doesNotMatch("/actuator"); + assertMatcher(matcher).matches("/actuator/foo"); + assertMatcher(matcher).matches("/actuator/bar"); + } + + @Test + public void excludeLinksShouldNotMatchBasePathIfEmptyAndExcluded() { + RequestMatcher matcher = EndpointRequest.toAnyEndpoint().excludingLinks(); + RequestMatcherAssert assertMatcher = assertMatcher(matcher, ""); + assertMatcher.doesNotMatch("/"); + assertMatcher.matches("/foo"); + assertMatcher.matches("/bar"); } @Test public void noEndpointPathsBeansShouldNeverMatch() { RequestMatcher matcher = EndpointRequest.toAnyEndpoint(); - assertMatcher(matcher, null).doesNotMatch("/actuator/foo"); - assertMatcher(matcher, null).doesNotMatch("/actuator/bar"); + assertMatcher(matcher, (PathMappedEndpoints) null).doesNotMatch("/actuator/foo"); + assertMatcher(matcher, (PathMappedEndpoints) null).doesNotMatch("/actuator/bar"); } private RequestMatcherAssert assertMatcher(RequestMatcher matcher) { - return assertMatcher(matcher, mockPathMappedEndpoints()); + return assertMatcher(matcher, mockPathMappedEndpoints("/actuator")); } - private PathMappedEndpoints mockPathMappedEndpoints() { + private RequestMatcherAssert assertMatcher(RequestMatcher matcher, String basePath) { + return assertMatcher(matcher, mockPathMappedEndpoints(basePath)); + } + + private PathMappedEndpoints mockPathMappedEndpoints(String basePath) { List> endpoints = new ArrayList<>(); endpoints.add(mockEndpoint("foo", "foo")); endpoints.add(mockEndpoint("bar", "bar")); - return new PathMappedEndpoints("/actuator", () -> endpoints); + return new PathMappedEndpoints(basePath, () -> endpoints); } private TestEndpoint mockEndpoint(String id, String rootPath) { @@ -126,8 +196,13 @@ public class EndpointRequestTests { private RequestMatcherAssert assertMatcher(RequestMatcher matcher, PathMappedEndpoints pathMappedEndpoints) { StaticWebApplicationContext context = new StaticWebApplicationContext(); + context.registerBean(WebEndpointProperties.class); if (pathMappedEndpoints != null) { context.registerBean(PathMappedEndpoints.class, () -> pathMappedEndpoints); + WebEndpointProperties properties = context.getBean(WebEndpointProperties.class); + if (!properties.getBasePath().equals(pathMappedEndpoints.getBasePath())) { + properties.setBasePath(pathMappedEndpoints.getBasePath()); + } } return assertThat(new RequestMatcherAssert(context, matcher)); } 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 81d6a093b3..16fe456d1d 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 @@ -75,6 +75,14 @@ public class PathMappedEndpoints implements Iterable { return Collections.unmodifiableMap(endpoints); } + /** + * Return the base path for the endpoints. + * @return the base path + */ + public String getBasePath() { + return this.basePath; + } + /** * Return the root path for the endpoint with the given ID or {@code null} if the * endpoint cannot be found. diff --git a/spring-boot-samples/spring-boot-sample-actuator-custom-security/src/test/java/sample/actuator/customsecurity/SampleActuatorCustomSecurityApplicationTests.java b/spring-boot-samples/spring-boot-sample-actuator-custom-security/src/test/java/sample/actuator/customsecurity/SampleActuatorCustomSecurityApplicationTests.java index b6e2e94b34..3d8ca81e52 100644 --- a/spring-boot-samples/spring-boot-sample-actuator-custom-security/src/test/java/sample/actuator/customsecurity/SampleActuatorCustomSecurityApplicationTests.java +++ b/spring-boot-samples/spring-boot-sample-actuator-custom-security/src/test/java/sample/actuator/customsecurity/SampleActuatorCustomSecurityApplicationTests.java @@ -81,6 +81,16 @@ public class SampleActuatorCustomSecurityApplicationTests { assertThat(entity.getBody()).contains("\"status\":\"UP\""); } + @Test + public void actuatorLinksIsSecure() { + ResponseEntity entity = restTemplate().getForEntity("/actuator", + Object.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); + entity = adminRestTemplate().getForEntity("/actuator", + Object.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); + } + @Test public void actuatorSecureEndpointWithAnonymous() { ResponseEntity entity = restTemplate().getForEntity("/actuator/env", diff --git a/spring-boot-samples/spring-boot-sample-secure-webflux/src/test/java/sample/secure/webflux/SampleSecureWebFluxCustomSecurityTests.java b/spring-boot-samples/spring-boot-sample-secure-webflux/src/test/java/sample/secure/webflux/SampleSecureWebFluxCustomSecurityTests.java index 23d5b7aee6..75f12485f4 100644 --- a/spring-boot-samples/spring-boot-sample-secure-webflux/src/test/java/sample/secure/webflux/SampleSecureWebFluxCustomSecurityTests.java +++ b/spring-boot-samples/spring-boot-sample-secure-webflux/src/test/java/sample/secure/webflux/SampleSecureWebFluxCustomSecurityTests.java @@ -92,6 +92,16 @@ public class SampleSecureWebFluxCustomSecurityTests { .accept(MediaType.APPLICATION_JSON).exchange().expectStatus().isOk(); } + @Test + public void actuatorLinksIsSecure() { + this.webClient.get().uri("/actuator").accept(MediaType.APPLICATION_JSON) + .exchange() + .expectStatus().isUnauthorized(); + this.webClient.get().uri("/actuator").accept(MediaType.APPLICATION_JSON) + .header("Authorization", "basic " + getBasicAuthForAdmin()).exchange() + .expectStatus().isOk(); + } + @Configuration static class SecurityConfiguration {