Commit 89e42d40 authored by Madhura Bhave's avatar Madhura Bhave

Provide security matchers for actuator links

Fixes gh-12353
parent 7d1faa1c
...@@ -30,6 +30,7 @@ import java.util.stream.Stream; ...@@ -30,6 +30,7 @@ import java.util.stream.Stream;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import org.springframework.beans.factory.NoSuchBeanDefinitionException; 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.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.web.PathMappedEndpoints; import org.springframework.boot.actuate.endpoint.web.PathMappedEndpoints;
import org.springframework.boot.security.reactive.ApplicationContextServerWebExchangeMatcher; import org.springframework.boot.security.reactive.ApplicationContextServerWebExchangeMatcher;
...@@ -39,6 +40,7 @@ import org.springframework.security.web.server.util.matcher.PathPatternParserSer ...@@ -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;
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher.MatchResult; import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher.MatchResult;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.ServerWebExchange;
/** /**
...@@ -57,7 +59,8 @@ public final class EndpointRequest { ...@@ -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 * {@link EndpointServerWebExchangeMatcher#excluding(Class...) excluding} method can
* be used to further remove specific endpoints if required. For example: * be used to further remove specific endpoints if required. For example:
* <pre class="code"> * <pre class="code">
...@@ -66,7 +69,7 @@ public final class EndpointRequest { ...@@ -66,7 +69,7 @@ public final class EndpointRequest {
* @return the configured {@link ServerWebExchangeMatcher} * @return the configured {@link ServerWebExchangeMatcher}
*/ */
public static EndpointServerWebExchangeMatcher toAnyEndpoint() { public static EndpointServerWebExchangeMatcher toAnyEndpoint() {
return new EndpointServerWebExchangeMatcher(); return new EndpointServerWebExchangeMatcher(true);
} }
/** /**
...@@ -78,7 +81,7 @@ public final class EndpointRequest { ...@@ -78,7 +81,7 @@ public final class EndpointRequest {
* @return the configured {@link ServerWebExchangeMatcher} * @return the configured {@link ServerWebExchangeMatcher}
*/ */
public static EndpointServerWebExchangeMatcher to(Class<?>... endpoints) { public static EndpointServerWebExchangeMatcher to(Class<?>... endpoints) {
return new EndpointServerWebExchangeMatcher(endpoints); return new EndpointServerWebExchangeMatcher(endpoints, false);
} }
/** /**
...@@ -90,7 +93,21 @@ public final class EndpointRequest { ...@@ -90,7 +93,21 @@ public final class EndpointRequest {
* @return the configured {@link ServerWebExchangeMatcher} * @return the configured {@link ServerWebExchangeMatcher}
*/ */
public static EndpointServerWebExchangeMatcher to(String... endpoints) { 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: <pre class="code">
* EndpointRequest.toLinks()
* </pre>
* @return the configured {@link ServerWebExchangeMatcher}
*/
public static LinksServerWebExchangeMatcher toLinks() {
return new LinksServerWebExchangeMatcher();
} }
/** /**
...@@ -106,35 +123,42 @@ public final class EndpointRequest { ...@@ -106,35 +123,42 @@ public final class EndpointRequest {
private ServerWebExchangeMatcher delegate; private ServerWebExchangeMatcher delegate;
private EndpointServerWebExchangeMatcher() { private boolean includeLinks;
this(Collections.emptyList(), Collections.emptyList());
private EndpointServerWebExchangeMatcher(boolean includeLinks) {
this(Collections.emptyList(), Collections.emptyList(), includeLinks);
} }
private EndpointServerWebExchangeMatcher(Class<?>[] endpoints) { private EndpointServerWebExchangeMatcher(Class<?>[] endpoints, boolean includeLinks) {
this(Arrays.asList((Object[]) endpoints), Collections.emptyList()); this(Arrays.asList((Object[]) endpoints), Collections.emptyList(), includeLinks);
} }
private EndpointServerWebExchangeMatcher(String[] endpoints) { private EndpointServerWebExchangeMatcher(String[] endpoints, boolean includeLinks) {
this(Arrays.asList((Object[]) endpoints), Collections.emptyList()); this(Arrays.asList((Object[]) endpoints), Collections.emptyList(), includeLinks);
} }
private EndpointServerWebExchangeMatcher(List<Object> includes, private EndpointServerWebExchangeMatcher(List<Object> includes,
List<Object> excludes) { List<Object> excludes, boolean includeLinks) {
super(PathMappedEndpoints.class); super(PathMappedEndpoints.class);
this.includes = includes; this.includes = includes;
this.excludes = excludes; this.excludes = excludes;
this.includeLinks = includeLinks;
} }
public EndpointServerWebExchangeMatcher excluding(Class<?>... endpoints) { public EndpointServerWebExchangeMatcher excluding(Class<?>... endpoints) {
List<Object> excludes = new ArrayList<>(this.excludes); List<Object> excludes = new ArrayList<>(this.excludes);
excludes.addAll(Arrays.asList((Object[]) endpoints)); 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) { public EndpointServerWebExchangeMatcher excluding(String... endpoints) {
List<Object> excludes = new ArrayList<>(this.excludes); List<Object> excludes = new ArrayList<>(this.excludes);
excludes.addAll(Arrays.asList((Object[]) endpoints)); 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 @Override
...@@ -160,7 +184,11 @@ public final class EndpointRequest { ...@@ -160,7 +184,11 @@ public final class EndpointRequest {
} }
streamPaths(this.includes, pathMappedEndpoints).forEach(paths::add); streamPaths(this.includes, pathMappedEndpoints).forEach(paths::add);
streamPaths(this.excludes, pathMappedEndpoints).forEach(paths::remove); streamPaths(this.excludes, pathMappedEndpoints).forEach(paths::remove);
return new OrServerWebExchangeMatcher(getDelegateMatchers(paths)); List<ServerWebExchangeMatcher> delegateMatchers = getDelegateMatchers(paths);
if (this.includeLinks && StringUtils.hasText(pathMappedEndpoints.getBasePath())) {
delegateMatchers.add(new PathPatternParserServerWebExchangeMatcher(pathMappedEndpoints.getBasePath()));
}
return new OrServerWebExchangeMatcher(delegateMatchers);
} }
private Stream<String> streamPaths(List<Object> source, private Stream<String> streamPaths(List<Object> source,
...@@ -200,4 +228,36 @@ public final class EndpointRequest { ...@@ -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<WebEndpointProperties> {
private ServerWebExchangeMatcher delegate;
private LinksServerWebExchangeMatcher() {
super(WebEndpointProperties.class);
}
@Override
protected void initialized(Supplier<WebEndpointProperties> propertiesSupplier) {
WebEndpointProperties webEndpointProperties = propertiesSupplier.get();
if (StringUtils.hasText(webEndpointProperties.getBasePath())) {
this.delegate = new PathPatternParserServerWebExchangeMatcher(
webEndpointProperties.getBasePath());
}
else {
this.delegate = EMPTY_MATCHER;
}
}
@Override
protected Mono<MatchResult> matches(ServerWebExchange exchange,
Supplier<WebEndpointProperties> context) {
return this.delegate.matches(exchange);
}
}
} }
...@@ -30,6 +30,7 @@ import java.util.stream.Stream; ...@@ -30,6 +30,7 @@ import java.util.stream.Stream;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.NoSuchBeanDefinitionException; 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.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.web.PathMappedEndpoints; import org.springframework.boot.actuate.endpoint.web.PathMappedEndpoints;
import org.springframework.boot.security.servlet.ApplicationContextRequestMatcher; import org.springframework.boot.security.servlet.ApplicationContextRequestMatcher;
...@@ -38,6 +39,7 @@ import org.springframework.security.web.util.matcher.AntPathRequestMatcher; ...@@ -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.OrRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/** /**
* Factory that can be used to create a {@link RequestMatcher} for actuator endpoint * Factory that can be used to create a {@link RequestMatcher} for actuator endpoint
...@@ -55,7 +57,8 @@ public final class EndpointRequest { ...@@ -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 * {@link EndpointRequestMatcher#excluding(Class...) excluding} method can be used to
* further remove specific endpoints if required. For example: <pre class="code"> * further remove specific endpoints if required. For example: <pre class="code">
* EndpointRequest.toAnyEndpoint().excluding(ShutdownEndpoint.class) * EndpointRequest.toAnyEndpoint().excluding(ShutdownEndpoint.class)
...@@ -63,7 +66,7 @@ public final class EndpointRequest { ...@@ -63,7 +66,7 @@ public final class EndpointRequest {
* @return the configured {@link RequestMatcher} * @return the configured {@link RequestMatcher}
*/ */
public static EndpointRequestMatcher toAnyEndpoint() { public static EndpointRequestMatcher toAnyEndpoint() {
return new EndpointRequestMatcher(); return new EndpointRequestMatcher(true);
} }
/** /**
...@@ -75,7 +78,7 @@ public final class EndpointRequest { ...@@ -75,7 +78,7 @@ public final class EndpointRequest {
* @return the configured {@link RequestMatcher} * @return the configured {@link RequestMatcher}
*/ */
public static EndpointRequestMatcher to(Class<?>... endpoints) { public static EndpointRequestMatcher to(Class<?>... endpoints) {
return new EndpointRequestMatcher(endpoints); return new EndpointRequestMatcher(endpoints, false);
} }
/** /**
...@@ -87,7 +90,21 @@ public final class EndpointRequest { ...@@ -87,7 +90,21 @@ public final class EndpointRequest {
* @return the configured {@link RequestMatcher} * @return the configured {@link RequestMatcher}
*/ */
public static EndpointRequestMatcher to(String... endpoints) { 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: <pre class="code">
* EndpointRequest.toLinks()
* </pre>
* @return the configured {@link RequestMatcher}
*/
public static LinksRequestMatcher toLinks() {
return new LinksRequestMatcher();
} }
/** /**
...@@ -100,36 +117,43 @@ public final class EndpointRequest { ...@@ -100,36 +117,43 @@ public final class EndpointRequest {
private final List<Object> excludes; private final List<Object> excludes;
private final boolean includeLinks;
private volatile RequestMatcher delegate; private volatile RequestMatcher delegate;
private EndpointRequestMatcher() { private EndpointRequestMatcher(boolean includeLinks) {
this(Collections.emptyList(), Collections.emptyList()); this(Collections.emptyList(), Collections.emptyList(), includeLinks);
} }
private EndpointRequestMatcher(Class<?>[] endpoints) { private EndpointRequestMatcher(Class<?>[] endpoints, boolean includeLinks) {
this(Arrays.asList((Object[]) endpoints), Collections.emptyList()); this(Arrays.asList((Object[]) endpoints), Collections.emptyList(), includeLinks);
} }
private EndpointRequestMatcher(String[] endpoints) { private EndpointRequestMatcher(String[] endpoints, boolean includeLinks) {
this(Arrays.asList((Object[]) endpoints), Collections.emptyList()); this(Arrays.asList((Object[]) endpoints), Collections.emptyList(), includeLinks);
} }
private EndpointRequestMatcher(List<Object> includes, List<Object> excludes) { private EndpointRequestMatcher(List<Object> includes, List<Object> excludes, boolean includeLinks) {
super(PathMappedEndpoints.class); super(PathMappedEndpoints.class);
this.includes = includes; this.includes = includes;
this.excludes = excludes; this.excludes = excludes;
this.includeLinks = includeLinks;
} }
public EndpointRequestMatcher excluding(Class<?>... endpoints) { public EndpointRequestMatcher excluding(Class<?>... endpoints) {
List<Object> excludes = new ArrayList<>(this.excludes); List<Object> excludes = new ArrayList<>(this.excludes);
excludes.addAll(Arrays.asList((Object[]) endpoints)); 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) { public EndpointRequestMatcher excluding(String... endpoints) {
List<Object> excludes = new ArrayList<>(this.excludes); List<Object> excludes = new ArrayList<>(this.excludes);
excludes.addAll(Arrays.asList((Object[]) endpoints)); 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 @Override
...@@ -154,7 +178,11 @@ public final class EndpointRequest { ...@@ -154,7 +178,11 @@ public final class EndpointRequest {
} }
streamPaths(this.includes, pathMappedEndpoints).forEach(paths::add); streamPaths(this.includes, pathMappedEndpoints).forEach(paths::add);
streamPaths(this.excludes, pathMappedEndpoints).forEach(paths::remove); streamPaths(this.excludes, pathMappedEndpoints).forEach(paths::remove);
return new OrRequestMatcher(getDelegateMatchers(paths)); List<RequestMatcher> delegateMatchers = getDelegateMatchers(paths);
if (this.includeLinks && StringUtils.hasText(pathMappedEndpoints.getBasePath())) {
delegateMatchers.add(new AntPathRequestMatcher(pathMappedEndpoints.getBasePath()));
}
return new OrRequestMatcher(delegateMatchers);
} }
private Stream<String> streamPaths(List<Object> source, private Stream<String> streamPaths(List<Object> source,
...@@ -193,4 +221,33 @@ public final class EndpointRequest { ...@@ -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<WebEndpointProperties> {
private RequestMatcher delegate;
private LinksRequestMatcher() {
super(WebEndpointProperties.class);
}
@Override
protected void initialized(Supplier<WebEndpointProperties> 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<WebEndpointProperties> context) {
return this.delegate.matches(request);
}
}
} }
...@@ -22,6 +22,7 @@ import java.util.List; ...@@ -22,6 +22,7 @@ import java.util.List;
import org.assertj.core.api.AssertDelegateTarget; import org.assertj.core.api.AssertDelegateTarget;
import org.junit.Test; 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.ExposableEndpoint;
import org.springframework.boot.actuate.endpoint.Operation; import org.springframework.boot.actuate.endpoint.Operation;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint; import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
...@@ -54,6 +55,16 @@ public class EndpointRequestTests { ...@@ -54,6 +55,16 @@ public class EndpointRequestTests {
ServerWebExchangeMatcher matcher = EndpointRequest.toAnyEndpoint(); ServerWebExchangeMatcher matcher = EndpointRequest.toAnyEndpoint();
assertMatcher(matcher).matches("/actuator/foo"); assertMatcher(matcher).matches("/actuator/foo");
assertMatcher(matcher).matches("/actuator/bar"); 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 @Test
...@@ -86,12 +97,38 @@ public class EndpointRequestTests { ...@@ -86,12 +97,38 @@ public class EndpointRequestTests {
assertMatcher(matcher).doesNotMatch("/actuator/bar"); 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 @Test
public void excludeByClassShouldNotMatchExcluded() { public void excludeByClassShouldNotMatchExcluded() {
ServerWebExchangeMatcher matcher = EndpointRequest.toAnyEndpoint() ServerWebExchangeMatcher matcher = EndpointRequest.toAnyEndpoint()
.excluding(FooEndpoint.class); .excluding(FooEndpoint.class);
assertMatcher(matcher).doesNotMatch("/actuator/foo"); assertMatcher(matcher).doesNotMatch("/actuator/foo");
assertMatcher(matcher).matches("/actuator/bar"); 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 @Test
...@@ -100,24 +137,54 @@ public class EndpointRequestTests { ...@@ -100,24 +137,54 @@ public class EndpointRequestTests {
.excluding("foo"); .excluding("foo");
assertMatcher(matcher).doesNotMatch("/actuator/foo"); assertMatcher(matcher).doesNotMatch("/actuator/foo");
assertMatcher(matcher).matches("/actuator/bar"); 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 @Test
public void noEndpointPathsBeansShouldNeverMatch() { public void noEndpointPathsBeansShouldNeverMatch() {
ServerWebExchangeMatcher matcher = EndpointRequest.toAnyEndpoint(); ServerWebExchangeMatcher matcher = EndpointRequest.toAnyEndpoint();
assertMatcher(matcher, null).doesNotMatch("/actuator/foo"); assertMatcher(matcher, (PathMappedEndpoints) null).doesNotMatch("/actuator/foo");
assertMatcher(matcher, null).doesNotMatch("/actuator/bar"); assertMatcher(matcher, (PathMappedEndpoints) null).doesNotMatch("/actuator/bar");
} }
private RequestMatcherAssert assertMatcher(ServerWebExchangeMatcher matcher) { private RequestMatcherAssert assertMatcher(ServerWebExchangeMatcher matcher) {
return assertMatcher(matcher, mockPathMappedEndpoints()); return assertMatcher(matcher, mockPathMappedEndpoints("/actuator"));
}
private RequestMatcherAssert assertMatcher(ServerWebExchangeMatcher matcher, String basePath) {
return assertMatcher(matcher, mockPathMappedEndpoints(basePath));
} }
private PathMappedEndpoints mockPathMappedEndpoints() { private PathMappedEndpoints mockPathMappedEndpoints(String basePath) {
List<ExposableEndpoint<?>> endpoints = new ArrayList<>(); List<ExposableEndpoint<?>> endpoints = new ArrayList<>();
endpoints.add(mockEndpoint("foo", "foo")); endpoints.add(mockEndpoint("foo", "foo"));
endpoints.add(mockEndpoint("bar", "bar")); endpoints.add(mockEndpoint("bar", "bar"));
return new PathMappedEndpoints("/actuator", () -> endpoints); return new PathMappedEndpoints(basePath, () -> endpoints);
} }
private TestEndpoint mockEndpoint(String id, String rootPath) { private TestEndpoint mockEndpoint(String id, String rootPath) {
...@@ -130,8 +197,13 @@ public class EndpointRequestTests { ...@@ -130,8 +197,13 @@ public class EndpointRequestTests {
private RequestMatcherAssert assertMatcher(ServerWebExchangeMatcher matcher, private RequestMatcherAssert assertMatcher(ServerWebExchangeMatcher matcher,
PathMappedEndpoints pathMappedEndpoints) { PathMappedEndpoints pathMappedEndpoints) {
StaticApplicationContext context = new StaticApplicationContext(); StaticApplicationContext context = new StaticApplicationContext();
context.registerBean(WebEndpointProperties.class);
if (pathMappedEndpoints != null) { if (pathMappedEndpoints != null) {
context.registerBean(PathMappedEndpoints.class, () -> pathMappedEndpoints); 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)); return assertThat(new RequestMatcherAssert(context, matcher));
} }
......
...@@ -24,6 +24,7 @@ import javax.servlet.http.HttpServletRequest; ...@@ -24,6 +24,7 @@ import javax.servlet.http.HttpServletRequest;
import org.assertj.core.api.AssertDelegateTarget; import org.assertj.core.api.AssertDelegateTarget;
import org.junit.Test; 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.ExposableEndpoint;
import org.springframework.boot.actuate.endpoint.Operation; import org.springframework.boot.actuate.endpoint.Operation;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint; import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
...@@ -43,6 +44,7 @@ import static org.mockito.Mockito.mock; ...@@ -43,6 +44,7 @@ import static org.mockito.Mockito.mock;
* Tests for {@link EndpointRequest}. * Tests for {@link EndpointRequest}.
* *
* @author Phillip Webb * @author Phillip Webb
* @author Madhura Bhave
*/ */
public class EndpointRequestTests { public class EndpointRequestTests {
...@@ -51,6 +53,16 @@ public class EndpointRequestTests { ...@@ -51,6 +53,16 @@ public class EndpointRequestTests {
RequestMatcher matcher = EndpointRequest.toAnyEndpoint(); RequestMatcher matcher = EndpointRequest.toAnyEndpoint();
assertMatcher(matcher).matches("/actuator/foo"); assertMatcher(matcher).matches("/actuator/foo");
assertMatcher(matcher).matches("/actuator/bar"); 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 @Test
...@@ -69,6 +81,7 @@ public class EndpointRequestTests { ...@@ -69,6 +81,7 @@ public class EndpointRequestTests {
public void toEndpointClassShouldNotMatchOtherPath() { public void toEndpointClassShouldNotMatchOtherPath() {
RequestMatcher matcher = EndpointRequest.to(FooEndpoint.class); RequestMatcher matcher = EndpointRequest.to(FooEndpoint.class);
assertMatcher(matcher).doesNotMatch("/actuator/bar"); assertMatcher(matcher).doesNotMatch("/actuator/bar");
assertMatcher(matcher).doesNotMatch("/actuator");
} }
@Test @Test
...@@ -81,6 +94,24 @@ public class EndpointRequestTests { ...@@ -81,6 +94,24 @@ public class EndpointRequestTests {
public void toEndpointIdShouldNotMatchOtherPath() { public void toEndpointIdShouldNotMatchOtherPath() {
RequestMatcher matcher = EndpointRequest.to("foo"); RequestMatcher matcher = EndpointRequest.to("foo");
assertMatcher(matcher).doesNotMatch("/actuator/bar"); 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 @Test
...@@ -89,6 +120,15 @@ public class EndpointRequestTests { ...@@ -89,6 +120,15 @@ public class EndpointRequestTests {
.excluding(FooEndpoint.class); .excluding(FooEndpoint.class);
assertMatcher(matcher).doesNotMatch("/actuator/foo"); assertMatcher(matcher).doesNotMatch("/actuator/foo");
assertMatcher(matcher).matches("/actuator/bar"); 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 @Test
...@@ -96,24 +136,54 @@ public class EndpointRequestTests { ...@@ -96,24 +136,54 @@ public class EndpointRequestTests {
RequestMatcher matcher = EndpointRequest.toAnyEndpoint().excluding("foo"); RequestMatcher matcher = EndpointRequest.toAnyEndpoint().excluding("foo");
assertMatcher(matcher).doesNotMatch("/actuator/foo"); assertMatcher(matcher).doesNotMatch("/actuator/foo");
assertMatcher(matcher).matches("/actuator/bar"); 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 @Test
public void noEndpointPathsBeansShouldNeverMatch() { public void noEndpointPathsBeansShouldNeverMatch() {
RequestMatcher matcher = EndpointRequest.toAnyEndpoint(); RequestMatcher matcher = EndpointRequest.toAnyEndpoint();
assertMatcher(matcher, null).doesNotMatch("/actuator/foo"); assertMatcher(matcher, (PathMappedEndpoints) null).doesNotMatch("/actuator/foo");
assertMatcher(matcher, null).doesNotMatch("/actuator/bar"); assertMatcher(matcher, (PathMappedEndpoints) null).doesNotMatch("/actuator/bar");
} }
private RequestMatcherAssert assertMatcher(RequestMatcher matcher) { private RequestMatcherAssert assertMatcher(RequestMatcher matcher) {
return assertMatcher(matcher, mockPathMappedEndpoints()); return assertMatcher(matcher, mockPathMappedEndpoints("/actuator"));
}
private RequestMatcherAssert assertMatcher(RequestMatcher matcher, String basePath) {
return assertMatcher(matcher, mockPathMappedEndpoints(basePath));
} }
private PathMappedEndpoints mockPathMappedEndpoints() { private PathMappedEndpoints mockPathMappedEndpoints(String basePath) {
List<ExposableEndpoint<?>> endpoints = new ArrayList<>(); List<ExposableEndpoint<?>> endpoints = new ArrayList<>();
endpoints.add(mockEndpoint("foo", "foo")); endpoints.add(mockEndpoint("foo", "foo"));
endpoints.add(mockEndpoint("bar", "bar")); endpoints.add(mockEndpoint("bar", "bar"));
return new PathMappedEndpoints("/actuator", () -> endpoints); return new PathMappedEndpoints(basePath, () -> endpoints);
} }
private TestEndpoint mockEndpoint(String id, String rootPath) { private TestEndpoint mockEndpoint(String id, String rootPath) {
...@@ -126,8 +196,13 @@ public class EndpointRequestTests { ...@@ -126,8 +196,13 @@ public class EndpointRequestTests {
private RequestMatcherAssert assertMatcher(RequestMatcher matcher, private RequestMatcherAssert assertMatcher(RequestMatcher matcher,
PathMappedEndpoints pathMappedEndpoints) { PathMappedEndpoints pathMappedEndpoints) {
StaticWebApplicationContext context = new StaticWebApplicationContext(); StaticWebApplicationContext context = new StaticWebApplicationContext();
context.registerBean(WebEndpointProperties.class);
if (pathMappedEndpoints != null) { if (pathMappedEndpoints != null) {
context.registerBean(PathMappedEndpoints.class, () -> pathMappedEndpoints); 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)); return assertThat(new RequestMatcherAssert(context, matcher));
} }
......
...@@ -75,6 +75,14 @@ public class PathMappedEndpoints implements Iterable<PathMappedEndpoint> { ...@@ -75,6 +75,14 @@ public class PathMappedEndpoints implements Iterable<PathMappedEndpoint> {
return Collections.unmodifiableMap(endpoints); 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 * Return the root path for the endpoint with the given ID or {@code null} if the
* endpoint cannot be found. * endpoint cannot be found.
......
...@@ -81,6 +81,16 @@ public class SampleActuatorCustomSecurityApplicationTests { ...@@ -81,6 +81,16 @@ public class SampleActuatorCustomSecurityApplicationTests {
assertThat(entity.getBody()).contains("\"status\":\"UP\""); assertThat(entity.getBody()).contains("\"status\":\"UP\"");
} }
@Test
public void actuatorLinksIsSecure() {
ResponseEntity<Object> 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 @Test
public void actuatorSecureEndpointWithAnonymous() { public void actuatorSecureEndpointWithAnonymous() {
ResponseEntity<Object> entity = restTemplate().getForEntity("/actuator/env", ResponseEntity<Object> entity = restTemplate().getForEntity("/actuator/env",
......
...@@ -92,6 +92,16 @@ public class SampleSecureWebFluxCustomSecurityTests { ...@@ -92,6 +92,16 @@ public class SampleSecureWebFluxCustomSecurityTests {
.accept(MediaType.APPLICATION_JSON).exchange().expectStatus().isOk(); .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 @Configuration
static class SecurityConfiguration { static class SecurityConfiguration {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment