Skip to content

Commit b93c2b9

Browse files
committed
Allow actuator endpoints to be used with mvcMatchers
This commit changes AbstractWebMvcEndpointHandlerMapping to be a MatchableHandlerMapping. Additionally, EndpointRequest, now delegates to MvcRequestMatcher for Spring MVC applications. For all other applications, AntPathRequestMatcher is used as a delegate. Closes gh-13962
1 parent a75a847 commit b93c2b9

File tree

11 files changed

+345
-18
lines changed

11 files changed

+345
-18
lines changed

spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/security/servlet/EndpointRequest.java

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties;
3434
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
3535
import org.springframework.boot.actuate.endpoint.web.PathMappedEndpoints;
36+
import org.springframework.boot.autoconfigure.security.servlet.RequestMatcherProvider;
3637
import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletPath;
3738
import org.springframework.boot.security.servlet.ApplicationContextRequestMatcher;
3839
import org.springframework.core.annotation.AnnotatedElementUtils;
@@ -158,13 +159,25 @@ protected abstract RequestMatcher createDelegate(WebApplicationContext context,
158159
RequestMatcherFactory requestMatcherFactory);
159160

160161
protected List<RequestMatcher> getLinksMatchers(
161-
RequestMatcherFactory requestMatcherFactory, String basePath) {
162+
RequestMatcherFactory requestMatcherFactory,
163+
RequestMatcherProvider matcherProvider, String basePath) {
162164
List<RequestMatcher> linksMatchers = new ArrayList<>();
163-
linksMatchers.add(requestMatcherFactory.antPath(basePath));
164-
linksMatchers.add(requestMatcherFactory.antPath(basePath, "/"));
165+
linksMatchers.add(requestMatcherFactory.antPath(matcherProvider, basePath));
166+
linksMatchers
167+
.add(requestMatcherFactory.antPath(matcherProvider, basePath, "/"));
165168
return linksMatchers;
166169
}
167170

171+
protected RequestMatcherProvider getRequestMatcherProvider(
172+
WebApplicationContext context) {
173+
try {
174+
return context.getBean(RequestMatcherProvider.class);
175+
}
176+
catch (NoSuchBeanDefinitionException ex) {
177+
return AntPathRequestMatcher::new;
178+
}
179+
}
180+
168181
}
169182

170183
/**
@@ -220,18 +233,19 @@ protected RequestMatcher createDelegate(WebApplicationContext context,
220233
RequestMatcherFactory requestMatcherFactory) {
221234
PathMappedEndpoints pathMappedEndpoints = context
222235
.getBean(PathMappedEndpoints.class);
236+
RequestMatcherProvider matcherProvider = getRequestMatcherProvider(context);
223237
Set<String> paths = new LinkedHashSet<>();
224238
if (this.includes.isEmpty()) {
225239
paths.addAll(pathMappedEndpoints.getAllPaths());
226240
}
227241
streamPaths(this.includes, pathMappedEndpoints).forEach(paths::add);
228242
streamPaths(this.excludes, pathMappedEndpoints).forEach(paths::remove);
229243
List<RequestMatcher> delegateMatchers = getDelegateMatchers(
230-
requestMatcherFactory, paths);
244+
requestMatcherFactory, matcherProvider, paths);
231245
String basePath = pathMappedEndpoints.getBasePath();
232246
if (this.includeLinks && StringUtils.hasText(basePath)) {
233-
delegateMatchers
234-
.addAll(getLinksMatchers(requestMatcherFactory, basePath));
247+
delegateMatchers.addAll(getLinksMatchers(requestMatcherFactory,
248+
matcherProvider, basePath));
235249
}
236250
return new OrRequestMatcher(delegateMatchers);
237251
}
@@ -261,9 +275,10 @@ private String getEndpointId(Class<?> source) {
261275
}
262276

263277
private List<RequestMatcher> getDelegateMatchers(
264-
RequestMatcherFactory requestMatcherFactory, Set<String> paths) {
265-
return paths.stream()
266-
.map((path) -> requestMatcherFactory.antPath(path, "/**"))
278+
RequestMatcherFactory requestMatcherFactory,
279+
RequestMatcherProvider matcherProvider, Set<String> paths) {
280+
return paths.stream().map(
281+
(path) -> requestMatcherFactory.antPath(matcherProvider, path, "/**"))
267282
.collect(Collectors.toList());
268283
}
269284

@@ -281,8 +296,8 @@ protected RequestMatcher createDelegate(WebApplicationContext context,
281296
.getBean(WebEndpointProperties.class);
282297
String basePath = properties.getBasePath();
283298
if (StringUtils.hasText(basePath)) {
284-
return new OrRequestMatcher(
285-
getLinksMatchers(requestMatcherFactory, basePath));
299+
return new OrRequestMatcher(getLinksMatchers(requestMatcherFactory,
300+
getRequestMatcherProvider(context), basePath));
286301
}
287302
return EMPTY_MATCHER;
288303
}
@@ -300,12 +315,13 @@ private static class RequestMatcherFactory {
300315
this.prefix = prefix;
301316
}
302317

303-
public RequestMatcher antPath(String... parts) {
318+
public RequestMatcher antPath(RequestMatcherProvider matcherProvider,
319+
String... parts) {
304320
String pattern = this.prefix;
305321
for (String part : parts) {
306322
pattern += part;
307323
}
308-
return new AntPathRequestMatcher(pattern);
324+
return matcherProvider.getRequestMatcher(pattern);
309325
}
310326

311327
}

spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/security/servlet/EndpointRequestTests.java

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import org.springframework.boot.actuate.endpoint.web.PathMappedEndpoint;
3232
import org.springframework.boot.actuate.endpoint.web.PathMappedEndpoints;
3333
import org.springframework.boot.actuate.endpoint.web.annotation.ServletEndpoint;
34+
import org.springframework.boot.autoconfigure.security.servlet.RequestMatcherProvider;
3435
import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletPath;
3536
import org.springframework.mock.web.MockHttpServletRequest;
3637
import org.springframework.mock.web.MockServletContext;
@@ -214,6 +215,26 @@ public void excludeLinksShouldNotMatchBasePathIfEmptyAndExcluded() {
214215
assertMatcher.matches("/bar");
215216
}
216217

218+
@Test
219+
public void endpointRequestMatcherShouldUseCustomRequestMatcherProvider() {
220+
RequestMatcher matcher = EndpointRequest.toAnyEndpoint();
221+
RequestMatcher mockRequestMatcher = (request) -> false;
222+
RequestMatcherAssert assertMatcher = assertMatcher(matcher,
223+
mockPathMappedEndpoints(""), "", (pattern) -> mockRequestMatcher);
224+
assertMatcher.doesNotMatch("/foo");
225+
assertMatcher.doesNotMatch("/bar");
226+
}
227+
228+
@Test
229+
public void linksRequestMatcherShouldUseCustomRequestMatcherProvider() {
230+
RequestMatcher matcher = EndpointRequest.toLinks();
231+
RequestMatcher mockRequestMatcher = (request) -> false;
232+
RequestMatcherAssert assertMatcher = assertMatcher(matcher,
233+
mockPathMappedEndpoints("/actuator"), "",
234+
(pattern) -> mockRequestMatcher);
235+
assertMatcher.doesNotMatch("/actuator");
236+
}
237+
217238
@Test
218239
public void noEndpointPathsBeansShouldNeverMatch() {
219240
RequestMatcher matcher = EndpointRequest.toAnyEndpoint();
@@ -231,7 +252,8 @@ private RequestMatcherAssert assertMatcher(RequestMatcher matcher, String basePa
231252

232253
private RequestMatcherAssert assertMatcher(RequestMatcher matcher, String basePath,
233254
String servletPath) {
234-
return assertMatcher(matcher, mockPathMappedEndpoints(basePath), servletPath);
255+
return assertMatcher(matcher, mockPathMappedEndpoints(basePath), servletPath,
256+
null);
235257
}
236258

237259
private PathMappedEndpoints mockPathMappedEndpoints(String basePath) {
@@ -250,11 +272,12 @@ private TestEndpoint mockEndpoint(String id, String rootPath) {
250272

251273
private RequestMatcherAssert assertMatcher(RequestMatcher matcher,
252274
PathMappedEndpoints pathMappedEndpoints) {
253-
return assertMatcher(matcher, pathMappedEndpoints, "");
275+
return assertMatcher(matcher, pathMappedEndpoints, "", null);
254276
}
255277

256278
private RequestMatcherAssert assertMatcher(RequestMatcher matcher,
257-
PathMappedEndpoints pathMappedEndpoints, String dispatcherServletPath) {
279+
PathMappedEndpoints pathMappedEndpoints, String dispatcherServletPath,
280+
RequestMatcherProvider matcherProvider) {
258281
StaticWebApplicationContext context = new StaticWebApplicationContext();
259282
context.registerBean(WebEndpointProperties.class);
260283
if (pathMappedEndpoints != null) {
@@ -269,6 +292,9 @@ private RequestMatcherAssert assertMatcher(RequestMatcher matcher,
269292
DispatcherServletPath path = () -> dispatcherServletPath;
270293
context.registerBean(DispatcherServletPath.class, () -> path);
271294
}
295+
if (matcherProvider != null) {
296+
context.registerBean(RequestMatcherProvider.class, () -> matcherProvider);
297+
}
272298
return assertThat(new RequestMatcherAssert(context, matcher));
273299
}
274300

spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/servlet/AbstractWebMvcEndpointHandlerMapping.java

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import java.util.LinkedHashMap;
2424
import java.util.List;
2525
import java.util.Map;
26+
import java.util.Set;
2627

2728
import javax.servlet.http.HttpServletRequest;
2829
import javax.servlet.http.HttpServletResponse;
@@ -49,6 +50,8 @@
4950
import org.springframework.web.bind.annotation.ResponseStatus;
5051
import org.springframework.web.cors.CorsConfiguration;
5152
import org.springframework.web.servlet.HandlerMapping;
53+
import org.springframework.web.servlet.handler.MatchableHandlerMapping;
54+
import org.springframework.web.servlet.handler.RequestMatchResult;
5255
import org.springframework.web.servlet.mvc.condition.ConsumesRequestCondition;
5356
import org.springframework.web.servlet.mvc.condition.PatternsRequestCondition;
5457
import org.springframework.web.servlet.mvc.condition.ProducesRequestCondition;
@@ -66,7 +69,8 @@
6669
* @since 2.0.0
6770
*/
6871
public abstract class AbstractWebMvcEndpointHandlerMapping
69-
extends RequestMappingInfoHandlerMapping implements InitializingBean {
72+
extends RequestMappingInfoHandlerMapping
73+
implements InitializingBean, MatchableHandlerMapping {
7074

7175
private final EndpointMapping endpointMapping;
7276

@@ -82,6 +86,8 @@ public abstract class AbstractWebMvcEndpointHandlerMapping
8286
private final Method handleMethod = ReflectionUtils.findMethod(OperationHandler.class,
8387
"handle", HttpServletRequest.class, Map.class);
8488

89+
private static final RequestMappingInfo.BuilderConfiguration builderConfig = getBuilderConfig();
90+
8591
/**
8692
* Creates a new {@code WebEndpointHandlerMapping} that provides mappings for the
8793
* operations of the given {@code webEndpoints}.
@@ -125,6 +131,29 @@ protected void initHandlerMethods() {
125131
}
126132
}
127133

134+
@Override
135+
public RequestMatchResult match(HttpServletRequest request, String pattern) {
136+
RequestMappingInfo info = RequestMappingInfo.paths(pattern).options(builderConfig)
137+
.build();
138+
RequestMappingInfo matchingInfo = info.getMatchingCondition(request);
139+
if (matchingInfo == null) {
140+
return null;
141+
}
142+
Set<String> patterns = matchingInfo.getPatternsCondition().getPatterns();
143+
String lookupPath = getUrlPathHelper().getLookupPathForRequest(request);
144+
return new RequestMatchResult(patterns.iterator().next(), lookupPath,
145+
getPathMatcher());
146+
}
147+
148+
private static RequestMappingInfo.BuilderConfiguration getBuilderConfig() {
149+
RequestMappingInfo.BuilderConfiguration config = new RequestMappingInfo.BuilderConfiguration();
150+
config.setUrlPathHelper(null);
151+
config.setPathMatcher(null);
152+
config.setSuffixPatternMatch(false);
153+
config.setTrailingSlashMatch(true);
154+
return config;
155+
}
156+
128157
private void registerMappingForOperation(ExposableWebEndpoint endpoint,
129158
WebOperation operation) {
130159
OperationInvoker invoker = operation::invoke;
@@ -176,7 +205,9 @@ private void registerLinksMapping() {
176205

177206
private PatternsRequestCondition patternsRequestConditionForPattern(String path) {
178207
String[] patterns = new String[] { this.endpointMapping.createSubPath(path) };
179-
return new PatternsRequestCondition(patterns, null, null, false, true);
208+
return new PatternsRequestCondition(patterns, builderConfig.getUrlPathHelper(),
209+
builderConfig.getPathMatcher(), builderConfig.useSuffixPatternMatch(),
210+
builderConfig.useTrailingSlashMatch());
180211
}
181212

182213
@Override

spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/servlet/MvcWebEndpointIntegrationTests.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,13 +46,15 @@
4646
import org.springframework.core.env.Environment;
4747
import org.springframework.http.HttpStatus;
4848
import org.springframework.http.MediaType;
49+
import org.springframework.mock.web.MockHttpServletRequest;
4950
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
5051
import org.springframework.security.core.authority.SimpleGrantedAuthority;
5152
import org.springframework.security.core.context.SecurityContext;
5253
import org.springframework.security.core.context.SecurityContextHolder;
5354
import org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestWrapper;
5455
import org.springframework.web.cors.CorsConfiguration;
5556
import org.springframework.web.filter.OncePerRequestFilter;
57+
import org.springframework.web.servlet.handler.RequestMatchResult;
5658

5759
import static org.assertj.core.api.Assertions.assertThat;
5860

@@ -104,6 +106,27 @@ public void readOperationsThatReturnAResourceSupportRangeRequests() {
104106
});
105107
}
106108

109+
@Test
110+
public void matchWhenRequestHasTrailingSlashShouldNotBeNull() {
111+
assertThat(getMatchResult("/spring/")).isNotNull();
112+
}
113+
114+
@Test
115+
public void matchWhenRequestHasSuffixShouldBeNull() {
116+
assertThat(getMatchResult("/spring.do")).isNull();
117+
}
118+
119+
private RequestMatchResult getMatchResult(String s) {
120+
MockHttpServletRequest request = new MockHttpServletRequest();
121+
request.setServletPath(s);
122+
AnnotationConfigServletWebServerApplicationContext context = createApplicationContext();
123+
context.register(TestEndpointConfiguration.class);
124+
context.refresh();
125+
WebMvcEndpointHandlerMapping bean = context
126+
.getBean(WebMvcEndpointHandlerMapping.class);
127+
return bean.match(request, "/spring");
128+
}
129+
107130
@Override
108131
protected int getPort(AnnotationConfigServletWebServerApplicationContext context) {
109132
return context.getWebServer().getPort();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
* Copyright 2012-2018 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.boot.autoconfigure.security.servlet;
17+
18+
import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher;
19+
import org.springframework.security.web.util.matcher.RequestMatcher;
20+
import org.springframework.web.servlet.handler.HandlerMappingIntrospector;
21+
22+
/**
23+
* {@link RequestMatcherProvider} that provides an {@link MvcRequestMatcher} that can be
24+
* used for Spring MVC applications.
25+
*
26+
* @author Madhura Bhave
27+
*/
28+
public class MvcRequestMatcherProvider implements RequestMatcherProvider {
29+
30+
private final HandlerMappingIntrospector introspector;
31+
32+
public MvcRequestMatcherProvider(HandlerMappingIntrospector introspector) {
33+
this.introspector = introspector;
34+
}
35+
36+
@Override
37+
public RequestMatcher getRequestMatcher(String pattern) {
38+
return new MvcRequestMatcher(this.introspector, pattern);
39+
}
40+
41+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
* Copyright 2012-2018 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.boot.autoconfigure.security.servlet;
17+
18+
import org.springframework.security.web.util.matcher.RequestMatcher;
19+
20+
/**
21+
* Interface that can be used to provide a {@link RequestMatcher} that can be used with
22+
* Spring Security.
23+
*
24+
* @author Madhura Bhave
25+
* @since 2.0.5
26+
*/
27+
@FunctionalInterface
28+
public interface RequestMatcherProvider {
29+
30+
RequestMatcher getRequestMatcher(String pattern);
31+
32+
}

0 commit comments

Comments
 (0)