Skip to content

Commit 6855c55

Browse files
mbhavephilwebb
authored andcommitted
EndpointRequest should consider server.servlet.path
Fixes gh-12934
1 parent 5b3cb8a commit 6855c55

File tree

7 files changed

+133
-26
lines changed

7 files changed

+133
-26
lines changed

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

+32-19
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,15 @@
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.web.servlet.DispatcherServletPathProvider;
3637
import org.springframework.boot.security.servlet.ApplicationContextRequestMatcher;
3738
import org.springframework.core.annotation.AnnotationUtils;
3839
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
3940
import org.springframework.security.web.util.matcher.OrRequestMatcher;
4041
import org.springframework.security.web.util.matcher.RequestMatcher;
4142
import org.springframework.util.Assert;
4243
import org.springframework.util.StringUtils;
44+
import org.springframework.web.context.WebApplicationContext;
4345

4446
/**
4547
* Factory that can be used to create a {@link RequestMatcher} for actuator endpoint
@@ -114,7 +116,7 @@ public static LinksRequestMatcher toLinks() {
114116
* The request matcher used to match against {@link Endpoint actuator endpoints}.
115117
*/
116118
public static final class EndpointRequestMatcher
117-
extends ApplicationContextRequestMatcher<PathMappedEndpoints> {
119+
extends ApplicationContextRequestMatcher<WebApplicationContext> {
118120

119121
private final List<Object> includes;
120122

@@ -140,7 +142,7 @@ private EndpointRequestMatcher(String[] endpoints, boolean includeLinks) {
140142

141143
private EndpointRequestMatcher(List<Object> includes, List<Object> excludes,
142144
boolean includeLinks) {
143-
super(PathMappedEndpoints.class);
145+
super(WebApplicationContext.class);
144146
this.includes = includes;
145147
this.excludes = excludes;
146148
this.includeLinks = includeLinks;
@@ -163,32 +165,35 @@ public EndpointRequestMatcher excludingLinks() {
163165
}
164166

165167
@Override
166-
protected void initialized(Supplier<PathMappedEndpoints> pathMappedEndpoints) {
167-
this.delegate = createDelegate(pathMappedEndpoints);
168+
protected void initialized(Supplier<WebApplicationContext> webApplicationContext) {
169+
this.delegate = createDelegate(webApplicationContext);
168170
}
169171

170172
private RequestMatcher createDelegate(
171-
Supplier<PathMappedEndpoints> pathMappedEndpoints) {
173+
Supplier<WebApplicationContext> webApplicationContext) {
172174
try {
173-
return createDelegate(pathMappedEndpoints.get());
175+
WebApplicationContext context = webApplicationContext.get();
176+
PathMappedEndpoints pathMappedEndpoints = context.getBean(PathMappedEndpoints.class);
177+
DispatcherServletPathProvider pathProvider = context.getBean(DispatcherServletPathProvider.class);
178+
return createDelegate(pathMappedEndpoints, pathProvider.getServletPath());
174179
}
175180
catch (NoSuchBeanDefinitionException ex) {
176181
return EMPTY_MATCHER;
177182
}
178183
}
179184

180-
private RequestMatcher createDelegate(PathMappedEndpoints pathMappedEndpoints) {
185+
private RequestMatcher createDelegate(PathMappedEndpoints pathMappedEndpoints, String servletPath) {
181186
Set<String> paths = new LinkedHashSet<>();
182187
if (this.includes.isEmpty()) {
183188
paths.addAll(pathMappedEndpoints.getAllPaths());
184189
}
185190
streamPaths(this.includes, pathMappedEndpoints).forEach(paths::add);
186191
streamPaths(this.excludes, pathMappedEndpoints).forEach(paths::remove);
187-
List<RequestMatcher> delegateMatchers = getDelegateMatchers(paths);
192+
List<RequestMatcher> delegateMatchers = getDelegateMatchers(servletPath, paths);
188193
if (this.includeLinks
189194
&& StringUtils.hasText(pathMappedEndpoints.getBasePath())) {
190195
delegateMatchers.add(
191-
new AntPathRequestMatcher(pathMappedEndpoints.getBasePath()));
196+
new AntPathRequestMatcher(servletPath + pathMappedEndpoints.getBasePath()));
192197
}
193198
return new OrRequestMatcher(delegateMatchers);
194199
}
@@ -216,14 +221,14 @@ private String getEndpointId(Class<?> source) {
216221
return annotation.id();
217222
}
218223

219-
private List<RequestMatcher> getDelegateMatchers(Set<String> paths) {
220-
return paths.stream().map((path) -> new AntPathRequestMatcher(path + "/**"))
224+
private List<RequestMatcher> getDelegateMatchers(String servletPath, Set<String> paths) {
225+
return paths.stream().map((path) -> new AntPathRequestMatcher(servletPath + path + "/**"))
221226
.collect(Collectors.toList());
222227
}
223228

224229
@Override
225230
protected boolean matches(HttpServletRequest request,
226-
Supplier<PathMappedEndpoints> context) {
231+
Supplier<WebApplicationContext> context) {
227232
return this.delegate.matches(request);
228233
}
229234

@@ -233,29 +238,37 @@ protected boolean matches(HttpServletRequest request,
233238
* The request matcher used to match against the links endpoint.
234239
*/
235240
public static final class LinksRequestMatcher
236-
extends ApplicationContextRequestMatcher<WebEndpointProperties> {
241+
extends ApplicationContextRequestMatcher<WebApplicationContext> {
237242

238243
private RequestMatcher delegate;
239244

240245
private LinksRequestMatcher() {
241-
super(WebEndpointProperties.class);
246+
super(WebApplicationContext.class);
242247
}
243248

244249
@Override
245-
protected void initialized(Supplier<WebEndpointProperties> properties) {
246-
this.delegate = createDelegate(properties.get());
250+
protected void initialized(Supplier<WebApplicationContext> webApplicationContext) {
251+
try {
252+
WebApplicationContext context = webApplicationContext.get();
253+
WebEndpointProperties properties = context.getBean(WebEndpointProperties.class);
254+
DispatcherServletPathProvider pathProvider = context.getBean(DispatcherServletPathProvider.class);
255+
this.delegate = createDelegate(pathProvider.getServletPath(), properties);
256+
}
257+
catch (NoSuchBeanDefinitionException ex) {
258+
this.delegate = EMPTY_MATCHER;
259+
}
247260
}
248261

249-
private RequestMatcher createDelegate(WebEndpointProperties properties) {
262+
private RequestMatcher createDelegate(String path, WebEndpointProperties properties) {
250263
if (StringUtils.hasText(properties.getBasePath())) {
251-
return new AntPathRequestMatcher(properties.getBasePath());
264+
return new AntPathRequestMatcher(path + properties.getBasePath());
252265
}
253266
return EMPTY_MATCHER;
254267
}
255268

256269
@Override
257270
protected boolean matches(HttpServletRequest request,
258-
Supplier<WebEndpointProperties> context) {
271+
Supplier<WebApplicationContext> context) {
259272
return this.delegate.matches(request);
260273
}
261274

spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/servlet/WebMvcEndpointChildContextConfiguration.java

+6
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
2626
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type;
2727
import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration;
28+
import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletPathProvider;
2829
import org.springframework.boot.web.servlet.error.ErrorAttributes;
2930
import org.springframework.boot.web.servlet.filter.OrderedRequestContextFilter;
3031
import org.springframework.context.annotation.Bean;
@@ -92,4 +93,9 @@ public RequestContextFilter requestContextFilter() {
9293
return new OrderedRequestContextFilter();
9394
}
9495

96+
@Bean
97+
public DispatcherServletPathProvider childDispatcherServletPathProvider() {
98+
return () -> "";
99+
}
100+
95101
}

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

+42-6
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
3131
import org.springframework.boot.actuate.endpoint.web.PathMappedEndpoint;
3232
import org.springframework.boot.actuate.endpoint.web.PathMappedEndpoints;
33+
import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletPathProvider;
3334
import org.springframework.mock.web.MockHttpServletRequest;
3435
import org.springframework.mock.web.MockServletContext;
3536
import org.springframework.security.web.util.matcher.RequestMatcher;
@@ -71,6 +72,16 @@ public void toAnyEndpointShouldNotMatchOtherPath() {
7172
assertMatcher(matcher).doesNotMatch("/actuator/baz");
7273
}
7374

75+
@Test
76+
public void toAnyEndpointWhenServletPathNotEmptyShouldMatch() {
77+
RequestMatcher matcher = EndpointRequest.toAnyEndpoint();
78+
assertMatcher(matcher, "/actuator", "/spring").matches("/spring", "/actuator/foo");
79+
assertMatcher(matcher, "/actuator", "/spring").matches("/spring", "/actuator/bar");
80+
assertMatcher(matcher, "/actuator", "/spring").matches("/spring", "/actuator");
81+
assertMatcher(matcher, "/actuator", "/spring").doesNotMatch("/spring", "/actuator/baz");
82+
assertMatcher(matcher, "/actuator", "/spring").doesNotMatch("", "/actuator/foo");
83+
}
84+
7485
@Test
7586
public void toEndpointClassShouldMatchEndpointPath() {
7687
RequestMatcher matcher = EndpointRequest.to(FooEndpoint.class);
@@ -114,6 +125,13 @@ public void toLinksWhenBasePathEmptyShouldNotMatch() {
114125
assertMatcher.doesNotMatch("/");
115126
}
116127

128+
@Test
129+
public void toLinksWhenServletPathNotEmptyShouldNotMatch() {
130+
RequestMatcher matcher = EndpointRequest.toLinks();
131+
RequestMatcherAssert assertMatcher = assertMatcher(matcher, "/actuator", "/spring");
132+
assertMatcher.matches("/spring/actuator");
133+
}
134+
117135
@Test
118136
public void excludeByClassShouldNotMatchExcluded() {
119137
RequestMatcher matcher = EndpointRequest.toAnyEndpoint()
@@ -179,6 +197,10 @@ private RequestMatcherAssert assertMatcher(RequestMatcher matcher, String basePa
179197
return assertMatcher(matcher, mockPathMappedEndpoints(basePath));
180198
}
181199

200+
private RequestMatcherAssert assertMatcher(RequestMatcher matcher, String basePath, String servletPath) {
201+
return assertMatcher(matcher, mockPathMappedEndpoints(basePath), servletPath);
202+
}
203+
182204
private PathMappedEndpoints mockPathMappedEndpoints(String basePath) {
183205
List<ExposableEndpoint<?>> endpoints = new ArrayList<>();
184206
endpoints.add(mockEndpoint("foo", "foo"));
@@ -195,6 +217,11 @@ private TestEndpoint mockEndpoint(String id, String rootPath) {
195217

196218
private RequestMatcherAssert assertMatcher(RequestMatcher matcher,
197219
PathMappedEndpoints pathMappedEndpoints) {
220+
return assertMatcher(matcher, pathMappedEndpoints, "");
221+
}
222+
223+
private RequestMatcherAssert assertMatcher(RequestMatcher matcher,
224+
PathMappedEndpoints pathMappedEndpoints, String servletPath) {
198225
StaticWebApplicationContext context = new StaticWebApplicationContext();
199226
context.registerBean(WebEndpointProperties.class);
200227
if (pathMappedEndpoints != null) {
@@ -205,6 +232,7 @@ private RequestMatcherAssert assertMatcher(RequestMatcher matcher,
205232
properties.setBasePath(pathMappedEndpoints.getBasePath());
206233
}
207234
}
235+
context.registerBean(DispatcherServletPathProvider.class, () -> () -> servletPath);
208236
return assertThat(new RequestMatcherAssert(context, matcher));
209237
}
210238

@@ -219,26 +247,34 @@ private static class RequestMatcherAssert implements AssertDelegateTarget {
219247
this.matcher = matcher;
220248
}
221249

222-
public void matches(String path) {
223-
matches(mockRequest(path));
250+
public void matches(String servletPath) {
251+
matches(mockRequest(servletPath));
252+
}
253+
254+
public void matches(String servletPath, String pathInfo) {
255+
matches(mockRequest(servletPath, pathInfo));
224256
}
225257

226258
private void matches(HttpServletRequest request) {
227259
assertThat(this.matcher.matches(request))
228260
.as("Matches " + getRequestPath(request)).isTrue();
229261
}
230262

231-
public void doesNotMatch(String path) {
232-
doesNotMatch(mockRequest(path));
263+
public void doesNotMatch(String servletPath) {
264+
doesNotMatch(mockRequest(servletPath));
265+
}
266+
267+
public void doesNotMatch(String servletPath, String pathInfo) {
268+
doesNotMatch(mockRequest(servletPath, pathInfo));
233269
}
234270

235271
private void doesNotMatch(HttpServletRequest request) {
236272
assertThat(this.matcher.matches(request))
237273
.as("Does not match " + getRequestPath(request)).isFalse();
238274
}
239275

240-
private MockHttpServletRequest mockRequest(String path) {
241-
return mockRequest(null, path);
276+
private MockHttpServletRequest mockRequest(String servletPath) {
277+
return mockRequest(servletPath, null);
242278
}
243279

244280
private MockHttpServletRequest mockRequest(String servletPath, String path) {

spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/servlet/WebMvcEndpointChildContextConfigurationTests.java

+8
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import org.junit.Test;
2020

21+
import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletPathProvider;
2122
import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
2223
import org.springframework.boot.web.servlet.filter.OrderedRequestContextFilter;
2324
import org.springframework.context.annotation.Bean;
@@ -62,6 +63,13 @@ public void contextShouldNotConfigureRequestContextFilterWhenRequestContextListe
6263
});
6364
}
6465

66+
@Test
67+
public void contextShouldConfigureDispatcherServletPathProviderWithEmptyPath() {
68+
this.contextRunner.withUserConfiguration(WebMvcEndpointChildContextConfiguration.class)
69+
.run((context) -> assertThat(context.getBean(DispatcherServletPathProvider.class)
70+
.getServletPath()).isEmpty());
71+
}
72+
6573
static class ExistingConfig {
6674

6775
@Bean

spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/DispatcherServletAutoConfiguration.java

+9-1
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,11 @@ protected static class DispatcherServletConfiguration {
8888

8989
private final WebMvcProperties webMvcProperties;
9090

91-
public DispatcherServletConfiguration(WebMvcProperties webMvcProperties) {
91+
private final ServerProperties serverProperties;
92+
93+
public DispatcherServletConfiguration(WebMvcProperties webMvcProperties, ServerProperties serverProperties) {
9294
this.webMvcProperties = webMvcProperties;
95+
this.serverProperties = serverProperties;
9396
}
9497

9598
@Bean(name = DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)
@@ -112,6 +115,11 @@ public MultipartResolver multipartResolver(MultipartResolver resolver) {
112115
return resolver;
113116
}
114117

118+
@Bean
119+
public DispatcherServletPathProvider mainDispatcherServletPathProvider() {
120+
return () -> DispatcherServletConfiguration.this.serverProperties.getServlet().getPath();
121+
}
122+
115123
}
116124

117125
@Configuration
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
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+
17+
package org.springframework.boot.autoconfigure.web.servlet;
18+
19+
import org.springframework.web.servlet.DispatcherServlet;
20+
21+
/**
22+
* Interface that provides the path of the {@link DispatcherServlet} in
23+
* an application context.
24+
*
25+
* @author Madhura Bhave
26+
* @since 2.0.2
27+
*/
28+
@FunctionalInterface
29+
public interface DispatcherServletPathProvider {
30+
31+
String getServletPath();
32+
33+
}
34+

spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/DispatcherServletAutoConfigurationTests.java

+2
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,8 @@ public void servletPath() {
107107
assertThat(registration.getUrlMappings().toString())
108108
.isEqualTo("[/spring/*]");
109109
assertThat(registration.getMultipartConfig()).isNull();
110+
assertThat(context.getBean(DispatcherServletPathProvider.class)
111+
.getServletPath()).isEqualTo("/spring");
110112
});
111113
}
112114

0 commit comments

Comments
 (0)