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
pull/14085/merge
Madhura Bhave 6 years ago
parent a75a84781e
commit b93c2b9a9f

@ -33,6 +33,7 @@ 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.autoconfigure.security.servlet.RequestMatcherProvider;
import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletPath;
import org.springframework.boot.security.servlet.ApplicationContextRequestMatcher;
import org.springframework.core.annotation.AnnotatedElementUtils;
@ -158,13 +159,25 @@ public final class EndpointRequest {
RequestMatcherFactory requestMatcherFactory);
protected List<RequestMatcher> getLinksMatchers(
RequestMatcherFactory requestMatcherFactory, String basePath) {
RequestMatcherFactory requestMatcherFactory,
RequestMatcherProvider matcherProvider, String basePath) {
List<RequestMatcher> linksMatchers = new ArrayList<>();
linksMatchers.add(requestMatcherFactory.antPath(basePath));
linksMatchers.add(requestMatcherFactory.antPath(basePath, "/"));
linksMatchers.add(requestMatcherFactory.antPath(matcherProvider, basePath));
linksMatchers
.add(requestMatcherFactory.antPath(matcherProvider, basePath, "/"));
return linksMatchers;
}
protected RequestMatcherProvider getRequestMatcherProvider(
WebApplicationContext context) {
try {
return context.getBean(RequestMatcherProvider.class);
}
catch (NoSuchBeanDefinitionException ex) {
return AntPathRequestMatcher::new;
}
}
}
/**
@ -220,6 +233,7 @@ public final class EndpointRequest {
RequestMatcherFactory requestMatcherFactory) {
PathMappedEndpoints pathMappedEndpoints = context
.getBean(PathMappedEndpoints.class);
RequestMatcherProvider matcherProvider = getRequestMatcherProvider(context);
Set<String> paths = new LinkedHashSet<>();
if (this.includes.isEmpty()) {
paths.addAll(pathMappedEndpoints.getAllPaths());
@ -227,11 +241,11 @@ public final class EndpointRequest {
streamPaths(this.includes, pathMappedEndpoints).forEach(paths::add);
streamPaths(this.excludes, pathMappedEndpoints).forEach(paths::remove);
List<RequestMatcher> delegateMatchers = getDelegateMatchers(
requestMatcherFactory, paths);
requestMatcherFactory, matcherProvider, paths);
String basePath = pathMappedEndpoints.getBasePath();
if (this.includeLinks && StringUtils.hasText(basePath)) {
delegateMatchers
.addAll(getLinksMatchers(requestMatcherFactory, basePath));
delegateMatchers.addAll(getLinksMatchers(requestMatcherFactory,
matcherProvider, basePath));
}
return new OrRequestMatcher(delegateMatchers);
}
@ -261,9 +275,10 @@ public final class EndpointRequest {
}
private List<RequestMatcher> getDelegateMatchers(
RequestMatcherFactory requestMatcherFactory, Set<String> paths) {
return paths.stream()
.map((path) -> requestMatcherFactory.antPath(path, "/**"))
RequestMatcherFactory requestMatcherFactory,
RequestMatcherProvider matcherProvider, Set<String> paths) {
return paths.stream().map(
(path) -> requestMatcherFactory.antPath(matcherProvider, path, "/**"))
.collect(Collectors.toList());
}
@ -281,8 +296,8 @@ public final class EndpointRequest {
.getBean(WebEndpointProperties.class);
String basePath = properties.getBasePath();
if (StringUtils.hasText(basePath)) {
return new OrRequestMatcher(
getLinksMatchers(requestMatcherFactory, basePath));
return new OrRequestMatcher(getLinksMatchers(requestMatcherFactory,
getRequestMatcherProvider(context), basePath));
}
return EMPTY_MATCHER;
}
@ -300,12 +315,13 @@ public final class EndpointRequest {
this.prefix = prefix;
}
public RequestMatcher antPath(String... parts) {
public RequestMatcher antPath(RequestMatcherProvider matcherProvider,
String... parts) {
String pattern = this.prefix;
for (String part : parts) {
pattern += part;
}
return new AntPathRequestMatcher(pattern);
return matcherProvider.getRequestMatcher(pattern);
}
}

@ -31,6 +31,7 @@ import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.web.PathMappedEndpoint;
import org.springframework.boot.actuate.endpoint.web.PathMappedEndpoints;
import org.springframework.boot.actuate.endpoint.web.annotation.ServletEndpoint;
import org.springframework.boot.autoconfigure.security.servlet.RequestMatcherProvider;
import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletPath;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockServletContext;
@ -214,6 +215,26 @@ public class EndpointRequestTests {
assertMatcher.matches("/bar");
}
@Test
public void endpointRequestMatcherShouldUseCustomRequestMatcherProvider() {
RequestMatcher matcher = EndpointRequest.toAnyEndpoint();
RequestMatcher mockRequestMatcher = (request) -> false;
RequestMatcherAssert assertMatcher = assertMatcher(matcher,
mockPathMappedEndpoints(""), "", (pattern) -> mockRequestMatcher);
assertMatcher.doesNotMatch("/foo");
assertMatcher.doesNotMatch("/bar");
}
@Test
public void linksRequestMatcherShouldUseCustomRequestMatcherProvider() {
RequestMatcher matcher = EndpointRequest.toLinks();
RequestMatcher mockRequestMatcher = (request) -> false;
RequestMatcherAssert assertMatcher = assertMatcher(matcher,
mockPathMappedEndpoints("/actuator"), "",
(pattern) -> mockRequestMatcher);
assertMatcher.doesNotMatch("/actuator");
}
@Test
public void noEndpointPathsBeansShouldNeverMatch() {
RequestMatcher matcher = EndpointRequest.toAnyEndpoint();
@ -231,7 +252,8 @@ public class EndpointRequestTests {
private RequestMatcherAssert assertMatcher(RequestMatcher matcher, String basePath,
String servletPath) {
return assertMatcher(matcher, mockPathMappedEndpoints(basePath), servletPath);
return assertMatcher(matcher, mockPathMappedEndpoints(basePath), servletPath,
null);
}
private PathMappedEndpoints mockPathMappedEndpoints(String basePath) {
@ -250,11 +272,12 @@ public class EndpointRequestTests {
private RequestMatcherAssert assertMatcher(RequestMatcher matcher,
PathMappedEndpoints pathMappedEndpoints) {
return assertMatcher(matcher, pathMappedEndpoints, "");
return assertMatcher(matcher, pathMappedEndpoints, "", null);
}
private RequestMatcherAssert assertMatcher(RequestMatcher matcher,
PathMappedEndpoints pathMappedEndpoints, String dispatcherServletPath) {
PathMappedEndpoints pathMappedEndpoints, String dispatcherServletPath,
RequestMatcherProvider matcherProvider) {
StaticWebApplicationContext context = new StaticWebApplicationContext();
context.registerBean(WebEndpointProperties.class);
if (pathMappedEndpoints != null) {
@ -269,6 +292,9 @@ public class EndpointRequestTests {
DispatcherServletPath path = () -> dispatcherServletPath;
context.registerBean(DispatcherServletPath.class, () -> path);
}
if (matcherProvider != null) {
context.registerBean(RequestMatcherProvider.class, () -> matcherProvider);
}
return assertThat(new RequestMatcherAssert(context, matcher));
}

@ -23,6 +23,7 @@ import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@ -49,6 +50,8 @@ import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.servlet.HandlerMapping;
import org.springframework.web.servlet.handler.MatchableHandlerMapping;
import org.springframework.web.servlet.handler.RequestMatchResult;
import org.springframework.web.servlet.mvc.condition.ConsumesRequestCondition;
import org.springframework.web.servlet.mvc.condition.PatternsRequestCondition;
import org.springframework.web.servlet.mvc.condition.ProducesRequestCondition;
@ -66,7 +69,8 @@ import org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMappi
* @since 2.0.0
*/
public abstract class AbstractWebMvcEndpointHandlerMapping
extends RequestMappingInfoHandlerMapping implements InitializingBean {
extends RequestMappingInfoHandlerMapping
implements InitializingBean, MatchableHandlerMapping {
private final EndpointMapping endpointMapping;
@ -82,6 +86,8 @@ public abstract class AbstractWebMvcEndpointHandlerMapping
private final Method handleMethod = ReflectionUtils.findMethod(OperationHandler.class,
"handle", HttpServletRequest.class, Map.class);
private static final RequestMappingInfo.BuilderConfiguration builderConfig = getBuilderConfig();
/**
* Creates a new {@code WebEndpointHandlerMapping} that provides mappings for the
* operations of the given {@code webEndpoints}.
@ -125,6 +131,29 @@ public abstract class AbstractWebMvcEndpointHandlerMapping
}
}
@Override
public RequestMatchResult match(HttpServletRequest request, String pattern) {
RequestMappingInfo info = RequestMappingInfo.paths(pattern).options(builderConfig)
.build();
RequestMappingInfo matchingInfo = info.getMatchingCondition(request);
if (matchingInfo == null) {
return null;
}
Set<String> patterns = matchingInfo.getPatternsCondition().getPatterns();
String lookupPath = getUrlPathHelper().getLookupPathForRequest(request);
return new RequestMatchResult(patterns.iterator().next(), lookupPath,
getPathMatcher());
}
private static RequestMappingInfo.BuilderConfiguration getBuilderConfig() {
RequestMappingInfo.BuilderConfiguration config = new RequestMappingInfo.BuilderConfiguration();
config.setUrlPathHelper(null);
config.setPathMatcher(null);
config.setSuffixPatternMatch(false);
config.setTrailingSlashMatch(true);
return config;
}
private void registerMappingForOperation(ExposableWebEndpoint endpoint,
WebOperation operation) {
OperationInvoker invoker = operation::invoke;
@ -176,7 +205,9 @@ public abstract class AbstractWebMvcEndpointHandlerMapping
private PatternsRequestCondition patternsRequestConditionForPattern(String path) {
String[] patterns = new String[] { this.endpointMapping.createSubPath(path) };
return new PatternsRequestCondition(patterns, null, null, false, true);
return new PatternsRequestCondition(patterns, builderConfig.getUrlPathHelper(),
builderConfig.getPathMatcher(), builderConfig.useSuffixPatternMatch(),
builderConfig.useTrailingSlashMatch());
}
@Override

@ -46,6 +46,7 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContext;
@ -53,6 +54,7 @@ import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestWrapper;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.servlet.handler.RequestMatchResult;
import static org.assertj.core.api.Assertions.assertThat;
@ -104,6 +106,27 @@ public class MvcWebEndpointIntegrationTests extends
});
}
@Test
public void matchWhenRequestHasTrailingSlashShouldNotBeNull() {
assertThat(getMatchResult("/spring/")).isNotNull();
}
@Test
public void matchWhenRequestHasSuffixShouldBeNull() {
assertThat(getMatchResult("/spring.do")).isNull();
}
private RequestMatchResult getMatchResult(String s) {
MockHttpServletRequest request = new MockHttpServletRequest();
request.setServletPath(s);
AnnotationConfigServletWebServerApplicationContext context = createApplicationContext();
context.register(TestEndpointConfiguration.class);
context.refresh();
WebMvcEndpointHandlerMapping bean = context
.getBean(WebMvcEndpointHandlerMapping.class);
return bean.match(request, "/spring");
}
@Override
protected int getPort(AnnotationConfigServletWebServerApplicationContext context) {
return context.getWebServer().getPort();

@ -0,0 +1,41 @@
/*
* Copyright 2012-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.autoconfigure.security.servlet;
import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.web.servlet.handler.HandlerMappingIntrospector;
/**
* {@link RequestMatcherProvider} that provides an {@link MvcRequestMatcher} that can be
* used for Spring MVC applications.
*
* @author Madhura Bhave
*/
public class MvcRequestMatcherProvider implements RequestMatcherProvider {
private final HandlerMappingIntrospector introspector;
public MvcRequestMatcherProvider(HandlerMappingIntrospector introspector) {
this.introspector = introspector;
}
@Override
public RequestMatcher getRequestMatcher(String pattern) {
return new MvcRequestMatcher(this.introspector, pattern);
}
}

@ -0,0 +1,32 @@
/*
* Copyright 2012-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.autoconfigure.security.servlet;
import org.springframework.security.web.util.matcher.RequestMatcher;
/**
* Interface that can be used to provide a {@link RequestMatcher} that can be used with
* Spring Security.
*
* @author Madhura Bhave
* @since 2.0.5
*/
@FunctionalInterface
public interface RequestMatcherProvider {
RequestMatcher getRequestMatcher(String pattern);
}

@ -0,0 +1,45 @@
/*
* Copyright 2012-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.autoconfigure.security.servlet;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.handler.HandlerMappingIntrospector;
/**
* Auto-configuration for {@link RequestMatcherProvider}.
*
* @author Madhura Bhave
* @since 2.0.5
*/
@Configuration
@ConditionalOnClass({ RequestMatcher.class, DispatcherServlet.class })
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
@ConditionalOnBean(HandlerMappingIntrospector.class)
public class SecurityRequestMatcherProviderAutoConfiguration {
@Bean
public RequestMatcherProvider requestMatcherProvider(
HandlerMappingIntrospector introspector) {
return new MvcRequestMatcherProvider(introspector);
}
}

@ -97,6 +97,7 @@ org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration,\
org.springframework.boot.autoconfigure.quartz.QuartzAutoConfiguration,\
org.springframework.boot.autoconfigure.reactor.core.ReactorCoreAutoConfiguration,\
org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration,\
org.springframework.boot.autoconfigure.security.servlet.SecurityRequestMatcherProviderAutoConfiguration,\
org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration,\
org.springframework.boot.autoconfigure.security.servlet.SecurityFilterAutoConfiguration,\
org.springframework.boot.autoconfigure.security.reactive.ReactiveSecurityAutoConfiguration,\

@ -0,0 +1,95 @@
/*
* Copyright 2012-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.autoconfigure.security.servlet;
import org.junit.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.FilteredClassLoader;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.handler.HandlerMappingIntrospector;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link SecurityRequestMatcherProviderAutoConfiguration}.
*
* @author Madhura Bhave
*/
public class SecurityRequestMatcherProviderAutoConfigurationTests {
private WebApplicationContextRunner contextRunner = new WebApplicationContextRunner()
.withConfiguration(AutoConfigurations
.of(SecurityRequestMatcherProviderAutoConfiguration.class))
.withUserConfiguration(TestConfiguration.class);
@Test
public void registersMvcRequestMatcherProviderForIfMvcPresent() {
this.contextRunner.run((context) -> assertThat(context)
.hasSingleBean(MvcRequestMatcherProvider.class));
}
@Test
public void mvcRequestMatcherProviderConditionalOnWebApplication() {
new ApplicationContextRunner()
.withConfiguration(AutoConfigurations
.of(SecurityRequestMatcherProviderAutoConfiguration.class))
.withUserConfiguration(TestConfiguration.class)
.run((context) -> assertThat(context)
.doesNotHaveBean(MvcRequestMatcherProvider.class));
}
@Test
public void mvcRequestMatcherProviderConditionalOnDispatcherServletClass() {
this.contextRunner
.withClassLoader(new FilteredClassLoader(
"org.springframework.web.servlet.DispatcherServlet"))
.run((context) -> assertThat(context)
.doesNotHaveBean(MvcRequestMatcherProvider.class));
}
@Test
public void mvcRequestMatcherProviderConditionalOnRequestMatcherClass() {
this.contextRunner
.withClassLoader(new FilteredClassLoader(
"org.springframework.security.web.util.matcher.RequestMatcher"))
.run((context) -> assertThat(context)
.doesNotHaveBean(MvcRequestMatcherProvider.class));
}
@Test
public void mvcRequestMatcherProviderConditionalOnHandlerMappingIntrospectorBean() {
new WebApplicationContextRunner()
.withConfiguration(AutoConfigurations
.of(SecurityRequestMatcherProviderAutoConfiguration.class))
.run((context) -> assertThat(context)
.doesNotHaveBean(MvcRequestMatcherProvider.class));
}
@Configuration
static class TestConfiguration {
@Bean
public HandlerMappingIntrospector introspector() {
return new HandlerMappingIntrospector();
}
}
}

@ -35,6 +35,8 @@ public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
return new InMemoryUserDetailsManager(
User.withDefaultPasswordEncoder().username("user").password("password")
.authorities("ROLE_USER").build(),
User.withDefaultPasswordEncoder().username("beans").password("beans")
.authorities("ROLE_BEANS").build(),
User.withDefaultPasswordEncoder().username("admin").password("admin")
.authorities("ROLE_ACTUATOR", "ROLE_USER").build());
}
@ -43,6 +45,7 @@ public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity http) throws Exception {
// @formatter:off
http.authorizeRequests()
.mvcMatchers("/actuator/beans").hasRole("BEANS")
.requestMatchers(EndpointRequest.to("health", "info")).permitAll()
.requestMatchers(EndpointRequest.toAnyEndpoint().excluding(MappingsEndpoint.class)).hasRole("ACTUATOR")
.requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll()

@ -141,6 +141,16 @@ public class SampleActuatorCustomSecurityApplicationTests {
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
}
@Test
public void mvcMatchersCanBeUsedToSecureActuators() {
ResponseEntity<Object> entity = beansRestTemplate()
.getForEntity("/actuator/beans", Object.class);
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
entity = beansRestTemplate()
.getForEntity("/actuator/beans/", Object.class);
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
}
private TestRestTemplate restTemplate() {
return configure(new TestRestTemplate());
}
@ -153,6 +163,10 @@ public class SampleActuatorCustomSecurityApplicationTests {
return configure(new TestRestTemplate("user", "password"));
}
private TestRestTemplate beansRestTemplate() {
return configure(new TestRestTemplate("beans", "beans"));
}
private TestRestTemplate configure(TestRestTemplate restTemplate) {
restTemplate
.setUriTemplateHandler(new LocalHostUriTemplateHandler(this.environment));

Loading…
Cancel
Save