From b93c2b9a9fba057470b03944cdd59e37f1ebbc59 Mon Sep 17 00:00:00 2001 From: Madhura Bhave Date: Tue, 31 Jul 2018 13:51:19 -0700 Subject: [PATCH] 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 --- .../security/servlet/EndpointRequest.java | 42 +++++--- .../servlet/EndpointRequestTests.java | 32 ++++++- .../AbstractWebMvcEndpointHandlerMapping.java | 35 ++++++- .../MvcWebEndpointIntegrationTests.java | 23 +++++ .../servlet/MvcRequestMatcherProvider.java | 41 ++++++++ .../servlet/RequestMatcherProvider.java | 32 +++++++ ...questMatcherProviderAutoConfiguration.java | 45 +++++++++ .../main/resources/META-INF/spring.factories | 1 + ...MatcherProviderAutoConfigurationTests.java | 95 +++++++++++++++++++ .../customsecurity/SecurityConfiguration.java | 3 + ...ctuatorCustomSecurityApplicationTests.java | 14 +++ 11 files changed, 345 insertions(+), 18 deletions(-) create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/servlet/MvcRequestMatcherProvider.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/servlet/RequestMatcherProvider.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/servlet/SecurityRequestMatcherProviderAutoConfiguration.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/servlet/SecurityRequestMatcherProviderAutoConfigurationTests.java diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/security/servlet/EndpointRequest.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/security/servlet/EndpointRequest.java index d5eceea98d..ac6413006f 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/security/servlet/EndpointRequest.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/security/servlet/EndpointRequest.java @@ -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 getLinksMatchers( - RequestMatcherFactory requestMatcherFactory, String basePath) { + RequestMatcherFactory requestMatcherFactory, + RequestMatcherProvider matcherProvider, String basePath) { List 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 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 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 getDelegateMatchers( - RequestMatcherFactory requestMatcherFactory, Set paths) { - return paths.stream() - .map((path) -> requestMatcherFactory.antPath(path, "/**")) + RequestMatcherFactory requestMatcherFactory, + RequestMatcherProvider matcherProvider, Set 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); } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/security/servlet/EndpointRequestTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/security/servlet/EndpointRequestTests.java index 20cdb11c0a..4d194c7393 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/security/servlet/EndpointRequestTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/security/servlet/EndpointRequestTests.java @@ -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)); } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/servlet/AbstractWebMvcEndpointHandlerMapping.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/servlet/AbstractWebMvcEndpointHandlerMapping.java index a689ec4d0d..dd0f620f60 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/servlet/AbstractWebMvcEndpointHandlerMapping.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/servlet/AbstractWebMvcEndpointHandlerMapping.java @@ -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 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 diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/servlet/MvcWebEndpointIntegrationTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/servlet/MvcWebEndpointIntegrationTests.java index 4134ca7590..a7aac7de5c 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/servlet/MvcWebEndpointIntegrationTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/servlet/MvcWebEndpointIntegrationTests.java @@ -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(); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/servlet/MvcRequestMatcherProvider.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/servlet/MvcRequestMatcherProvider.java new file mode 100644 index 0000000000..3bf8e1cadc --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/servlet/MvcRequestMatcherProvider.java @@ -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); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/servlet/RequestMatcherProvider.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/servlet/RequestMatcherProvider.java new file mode 100644 index 0000000000..81a8f2180d --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/servlet/RequestMatcherProvider.java @@ -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); + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/servlet/SecurityRequestMatcherProviderAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/servlet/SecurityRequestMatcherProviderAutoConfiguration.java new file mode 100644 index 0000000000..f0a01b53e3 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/servlet/SecurityRequestMatcherProviderAutoConfiguration.java @@ -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); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories b/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories index b140c1f750..5904fac377 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories @@ -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,\ diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/servlet/SecurityRequestMatcherProviderAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/servlet/SecurityRequestMatcherProviderAutoConfigurationTests.java new file mode 100644 index 0000000000..8309842d21 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/servlet/SecurityRequestMatcherProviderAutoConfigurationTests.java @@ -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(); + } + + } + +} diff --git a/spring-boot-samples/spring-boot-sample-actuator-custom-security/src/main/java/sample/actuator/customsecurity/SecurityConfiguration.java b/spring-boot-samples/spring-boot-sample-actuator-custom-security/src/main/java/sample/actuator/customsecurity/SecurityConfiguration.java index 69ffd45d33..b4cf99dfb2 100644 --- a/spring-boot-samples/spring-boot-sample-actuator-custom-security/src/main/java/sample/actuator/customsecurity/SecurityConfiguration.java +++ b/spring-boot-samples/spring-boot-sample-actuator-custom-security/src/main/java/sample/actuator/customsecurity/SecurityConfiguration.java @@ -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() diff --git a/spring-boot-samples/spring-boot-sample-actuator-custom-security/src/test/java/sample/actuator/customsecurity/SampleActuatorCustomSecurityApplicationTests.java b/spring-boot-samples/spring-boot-sample-actuator-custom-security/src/test/java/sample/actuator/customsecurity/SampleActuatorCustomSecurityApplicationTests.java index 1f02ea3ef3..b479b2b6c8 100644 --- a/spring-boot-samples/spring-boot-sample-actuator-custom-security/src/test/java/sample/actuator/customsecurity/SampleActuatorCustomSecurityApplicationTests.java +++ b/spring-boot-samples/spring-boot-sample-actuator-custom-security/src/test/java/sample/actuator/customsecurity/SampleActuatorCustomSecurityApplicationTests.java @@ -141,6 +141,16 @@ public class SampleActuatorCustomSecurityApplicationTests { assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); } + @Test + public void mvcMatchersCanBeUsedToSecureActuators() { + ResponseEntity 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));