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 {