From 8244164c9d1ee4a2fd0c2330a7eec4470ea95fee Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Sat, 16 Sep 2023 08:26:58 -0600 Subject: [PATCH] An Attempt --- .../web/AbstractRequestMatcherRegistry.java | 86 +---- .../web/builders/RequestMatchersBuilder.java | 270 ++++++++++++++++ .../HttpSecurityConfiguration.java | 6 + .../AbstractRequestMatcherRegistryTests.java | 14 +- .../builders/RequestMatchersBuilderTests.java | 299 ++++++++++++++++++ .../AuthorizeHttpRequestsConfigurerTests.java | 2 +- 6 files changed, 597 insertions(+), 80 deletions(-) create mode 100644 config/src/main/java/org/springframework/security/config/annotation/web/builders/RequestMatchersBuilder.java create mode 100644 config/src/test/java/org/springframework/security/config/annotation/web/builders/RequestMatchersBuilderTests.java diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/AbstractRequestMatcherRegistry.java b/config/src/main/java/org/springframework/security/config/annotation/web/AbstractRequestMatcherRegistry.java index 5c1dd6118f0..6ca92b73d19 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/AbstractRequestMatcherRegistry.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/AbstractRequestMatcherRegistry.java @@ -18,20 +18,16 @@ import java.util.ArrayList; import java.util.Arrays; -import java.util.Collection; -import java.util.LinkedHashMap; import java.util.List; -import java.util.Map; import jakarta.servlet.DispatcherType; -import jakarta.servlet.ServletContext; -import jakarta.servlet.ServletRegistration; import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.context.ApplicationContext; import org.springframework.http.HttpMethod; import org.springframework.lang.Nullable; import org.springframework.security.config.annotation.ObjectPostProcessor; +import org.springframework.security.config.annotation.web.builders.RequestMatchersBuilder; import org.springframework.security.config.annotation.web.configurers.AbstractConfigAttributeRequestMatcherRegistry; import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; @@ -41,7 +37,6 @@ import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; -import org.springframework.web.context.WebApplicationContext; import org.springframework.web.servlet.handler.HandlerMappingIntrospector; /** @@ -185,73 +180,8 @@ public C requestMatchers(RequestMatcher... requestMatchers) { * @since 5.8 */ public C requestMatchers(HttpMethod method, String... patterns) { - if (!mvcPresent) { - return requestMatchers(RequestMatchers.antMatchersAsArray(method, patterns)); - } - if (!(this.context instanceof WebApplicationContext)) { - return requestMatchers(RequestMatchers.antMatchersAsArray(method, patterns)); - } - WebApplicationContext context = (WebApplicationContext) this.context; - ServletContext servletContext = context.getServletContext(); - if (servletContext == null) { - return requestMatchers(RequestMatchers.antMatchersAsArray(method, patterns)); - } - Map registrations = mappableServletRegistrations(servletContext); - if (registrations.isEmpty()) { - return requestMatchers(RequestMatchers.antMatchersAsArray(method, patterns)); - } - if (!hasDispatcherServlet(registrations)) { - return requestMatchers(RequestMatchers.antMatchersAsArray(method, patterns)); - } - if (registrations.size() > 1) { - String errorMessage = computeErrorMessage(registrations.values()); - throw new IllegalArgumentException(errorMessage); - } - return requestMatchers(createMvcMatchers(method, patterns).toArray(new RequestMatcher[0])); - } - - private Map mappableServletRegistrations(ServletContext servletContext) { - Map mappable = new LinkedHashMap<>(); - for (Map.Entry entry : servletContext.getServletRegistrations() - .entrySet()) { - if (!entry.getValue().getMappings().isEmpty()) { - mappable.put(entry.getKey(), entry.getValue()); - } - } - return mappable; - } - - private boolean hasDispatcherServlet(Map registrations) { - if (registrations == null) { - return false; - } - Class dispatcherServlet = ClassUtils.resolveClassName("org.springframework.web.servlet.DispatcherServlet", - null); - for (ServletRegistration registration : registrations.values()) { - try { - Class clazz = Class.forName(registration.getClassName()); - if (dispatcherServlet.isAssignableFrom(clazz)) { - return true; - } - } - catch (ClassNotFoundException ex) { - return false; - } - } - return false; - } - - private String computeErrorMessage(Collection registrations) { - String template = "This method cannot decide whether these patterns are Spring MVC patterns or not. " - + "If this endpoint is a Spring MVC endpoint, please use requestMatchers(MvcRequestMatcher); " - + "otherwise, please use requestMatchers(AntPathRequestMatcher).\n\n" - + "This is because there is more than one mappable servlet in your servlet context: %s.\n\n" - + "For each MvcRequestMatcher, call MvcRequestMatcher#setServletPath to indicate the servlet path."; - Map> mappings = new LinkedHashMap<>(); - for (ServletRegistration registration : registrations) { - mappings.put(registration.getClassName(), registration.getMappings()); - } - return String.format(template, mappings); + RequestMatchersBuilder builder = getRequestMatchersBuilder(); + return requestMatchers(builder.matchers(method, patterns)); } /** @@ -307,6 +237,16 @@ public C requestMatchers(HttpMethod method) { */ protected abstract C chainRequestMatchers(List requestMatchers); + private RequestMatchersBuilder getRequestMatchersBuilder() { + if (this.context == null) { + return new RequestMatchersBuilder(null); + } + if (this.context.getBeanNamesForType(RequestMatchersBuilder.class).length > 0) { + return this.context.getBean(RequestMatchersBuilder.class); + } + return new RequestMatchersBuilder(this.context); + } + /** * Utilities for creating {@link RequestMatcher} instances. * diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/builders/RequestMatchersBuilder.java b/config/src/main/java/org/springframework/security/config/annotation/web/builders/RequestMatchersBuilder.java new file mode 100644 index 00000000000..37fd1d7f86f --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/web/builders/RequestMatchersBuilder.java @@ -0,0 +1,270 @@ +/* + * Copyright 2012-2023 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 + * + * https://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.security.config.annotation.web.builders; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; + +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletRegistration; + +import org.springframework.context.ApplicationContext; +import org.springframework.http.HttpMethod; +import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.security.web.util.matcher.RequestMatcher; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.CollectionUtils; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.servlet.handler.HandlerMappingIntrospector; + +public class RequestMatchersBuilder { + + private static final String HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME = "mvcHandlerMappingIntrospector"; + + private final ApplicationContext context; + + private final RequestMatcherBuilder builder; + + private final Collection registrations; + + private final String servletPath; + + public RequestMatchersBuilder(ApplicationContext context) { + this(context, null); + } + + private RequestMatchersBuilder(ApplicationContext context, String servletPath) { + this.context = context; + this.registrations = registrations(context, servletPath); + this.builder = requestMatcherBuilder(context, this.registrations, servletPath); + this.servletPath = servletPath; + } + + private static RequestMatcherBuilder requestMatcherBuilder(ApplicationContext context, + Collection registrations, String servletPath) { + boolean hasIntrospector = context != null && context.containsBean(HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME); + if (!hasIntrospector) { + return new AntPathRequestMatcherBuilder(servletPath); + } + if (!hasDispatcherServlet(registrations)) { + return new AntPathRequestMatcherBuilder(servletPath); + } + if (registrations.isEmpty()) { + return new MvcRequestMatcherBuilder(context, servletPath); + } + if (registrations.size() == 1) { + ServletRegistration registration = registrations.iterator().next(); + if (servletPath == null) { + servletPath = deduceServletPath(registration); + } + return isDispatcherServlet(registration) ? new MvcRequestMatcherBuilder(context, servletPath) + : new AntPathRequestMatcherBuilder(servletPath); + } + return null; + } + + private static Collection registrations(ApplicationContext context, String servletPath) { + if (!(context instanceof WebApplicationContext web)) { + return Collections.emptyList(); + } + ServletContext servletContext = web.getServletContext(); + if (servletContext == null) { + return Collections.emptyList(); + } + Map registrations = servletContext.getServletRegistrations(); + if (registrations == null) { + return Collections.emptyList(); + } + Collection filtered = new ArrayList<>(); + for (ServletRegistration registration : registrations.values()) { + Collection mappings = registration.getMappings(); + if (CollectionUtils.isEmpty(mappings)) { + continue; + } + if (servletPath == null) { + for (String mapping : mappings) { + if (mapping.equals("/") || mapping.endsWith("/*")) { + filtered.add(registration); + break; + } + } + continue; + } + if (mappings.contains(servletPath) || mappings.contains(servletPath + "/*")) { + filtered.add(registration); + } + } + return filtered; + } + + private static boolean hasDispatcherServlet(Collection registrations) { + for (ServletRegistration registration : registrations) { + if (isDispatcherServlet(registration)) { + return true; + } + } + return false; + } + + private static boolean isDispatcherServlet(ServletRegistration registration) { + Class dispatcherServlet = ClassUtils.resolveClassName("org.springframework.web.servlet.DispatcherServlet", + null); + try { + Class clazz = Class.forName(registration.getClassName()); + if (dispatcherServlet.isAssignableFrom(clazz)) { + return true; + } + } + catch (ClassNotFoundException ex) { + return false; + } + return false; + } + + private static String deduceServletPath(ServletRegistration registration) { + Collection mappings = registration.getMappings(); + if (mappings.size() > 1) { + return null; + } + String mapping = mappings.iterator().next(); + if (mapping.endsWith("/*")) { + return mapping.substring(0, mapping.length() - 2); + } + return null; + } + + public RequestMatcher matcher() { + Assert.notNull(this.servletPath, computeErrorMessage()); + return new AntPathRequestMatcher(this.servletPath); + } + + public RequestMatcher[] matchers(HttpMethod method, String... patterns) { + checkServletPath(); + RequestMatcher[] matchers = new RequestMatcher[patterns.length]; + for (int index = 0; index < patterns.length; index++) { + matchers[index] = this.builder.matcher(method, patterns[index]); + } + return matchers; + } + + public RequestMatcher[] matchers(String... patterns) { + checkServletPath(); + RequestMatcher[] matchers = new RequestMatcher[patterns.length]; + for (int index = 0; index < patterns.length; index++) { + matchers[index] = this.builder.matcher(patterns[index]); + } + return matchers; + } + + public RequestMatchersBuilder servletPath(String path) { + return new RequestMatchersBuilder(this.context, path); + } + + private void checkServletPath() { + if (this.builder == null) { + throw new IllegalArgumentException(computeErrorMessage()); + } + } + + private String computeErrorMessage() { + String template = "This method cannot decide whether these patterns are Spring MVC patterns or not. " + + "You will need to specify the servlet path for each endpoint to assist with disambiguation. " + + "\n\nFor your reference, these are the servlets that have potentially ambiguous paths: %s" + + "\n\nTo do this, you can use the RequestMatchersBuilder bean in conjunction with requestMatchers like so: " + + "\n\n\t.requestMatchers(builder.servletPath(\"/\").matchers(\"/my\", \"/controller\", \"endpoints\"))."; + Map> mappings = new LinkedHashMap<>(); + for (ServletRegistration registration : this.registrations) { + mappings.put(registration.getClassName(), registration.getMappings()); + } + return String.format(template, mappings); + } + + private interface RequestMatcherBuilder { + + RequestMatcher matcher(String pattern); + + RequestMatcher matcher(HttpMethod method, String pattern); + + } + + private static final class MvcRequestMatcherBuilder implements RequestMatcherBuilder { + + private final HandlerMappingIntrospector introspector; + + private final String servletPath; + + private MvcRequestMatcherBuilder(ApplicationContext context, String servletPath) { + this.introspector = context.getBean(HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME, + HandlerMappingIntrospector.class); + this.servletPath = servletPath; + } + + @Override + public RequestMatcher matcher(String pattern) { + MvcRequestMatcher matcher = new MvcRequestMatcher(this.introspector, pattern); + if (this.servletPath != null) { + matcher.setServletPath(this.servletPath); + } + return matcher; + } + + @Override + public RequestMatcher matcher(HttpMethod method, String pattern) { + MvcRequestMatcher matcher = new MvcRequestMatcher(this.introspector, pattern); + matcher.setMethod(method); + if (this.servletPath != null) { + matcher.setServletPath(this.servletPath); + } + return matcher; + } + + } + + private static final class AntPathRequestMatcherBuilder implements RequestMatcherBuilder { + + private final String servletPath; + + private AntPathRequestMatcherBuilder(String servletPath) { + this.servletPath = servletPath; + } + + @Override + public RequestMatcher matcher(String pattern) { + return matcher((String) null, pattern); + } + + @Override + public RequestMatcher matcher(HttpMethod method, String pattern) { + return matcher((method != null) ? method.name() : null, pattern); + } + + private RequestMatcher matcher(String method, String pattern) { + return new AntPathRequestMatcher(prependServletPath(pattern), method); + } + + private String prependServletPath(String pattern) { + return (this.servletPath != null && !"/".equals(this.servletPath)) ? this.servletPath + pattern : pattern; + } + + } + +} diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/HttpSecurityConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/HttpSecurityConfiguration.java index ae3815de38e..0dbd8b07073 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/HttpSecurityConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/HttpSecurityConfiguration.java @@ -37,6 +37,7 @@ import org.springframework.security.config.annotation.authentication.configurers.provisioning.JdbcUserDetailsManagerConfigurer; import org.springframework.security.config.annotation.authentication.configurers.userdetails.DaoAuthenticationConfigurer; import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.builders.RequestMatchersBuilder; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.config.annotation.web.configurers.DefaultLoginPageConfigurer; import org.springframework.security.core.context.SecurityContextHolder; @@ -130,6 +131,11 @@ HttpSecurity httpSecurity() throws Exception { return http; } + @Bean + RequestMatchersBuilder requestMatchersBuilder(ApplicationContext context) { + return new RequestMatchersBuilder(context); + } + private void applyCorsIfAvailable(HttpSecurity http) throws Exception { String[] beanNames = this.context.getBeanNamesForType(CorsConfigurationSource.class); if (beanNames.length == 1) { diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/AbstractRequestMatcherRegistryTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/AbstractRequestMatcherRegistryTests.java index 6ade531ca15..795772e1e43 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/AbstractRequestMatcherRegistryTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/AbstractRequestMatcherRegistryTests.java @@ -23,7 +23,6 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.context.ApplicationContext; import org.springframework.http.HttpMethod; import org.springframework.security.config.MockServletContext; @@ -38,6 +37,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; @@ -65,6 +65,7 @@ public void setUp() { this.matcherRegistry = new TestRequestMatcherRegistry(); this.context = mock(WebApplicationContext.class); given(this.context.getBean(ObjectPostProcessor.class)).willReturn(NO_OP_OBJECT_POST_PROCESSOR); + given(this.context.getBeanNamesForType((Class) any())).willReturn(new String[0]); given(this.context.getServletContext()).willReturn(MockServletContext.mvc()); this.matcherRegistry.setApplicationContext(this.context); mockMvcIntrospector(true); @@ -147,11 +148,12 @@ public void requestMatchersWhenHttpMethodAndMvcPresentThenReturnMvcRequestMatche } @Test - public void requestMatchersWhenMvcPresentInClassPathAndMvcIntrospectorBeanNotAvailableThenException() { + public void requestMatchersWhenMvcPresentInClassPathAndMvcIntrospectorBeanNotAvailableThenAnt() { mockMvcIntrospector(false); - assertThatExceptionOfType(NoSuchBeanDefinitionException.class) - .isThrownBy(() -> this.matcherRegistry.requestMatchers("/path")).withMessageContaining( - "Please ensure Spring Security & Spring MVC are configured in a shared ApplicationContext"); + List requestMatchers = this.matcherRegistry.requestMatchers("/path"); + assertThat(requestMatchers).isNotEmpty(); + assertThat(requestMatchers).hasSize(1); + assertThat(requestMatchers.get(0)).isExactlyInstanceOf(AntPathRequestMatcher.class); } @Test @@ -175,7 +177,7 @@ public void requestMatchersWhenAmbiguousServletsThenException() { MockServletContext servletContext = new MockServletContext(); given(this.context.getServletContext()).willReturn(servletContext); servletContext.addServlet("dispatcherServlet", DispatcherServlet.class).addMapping("/"); - servletContext.addServlet("servletTwo", Servlet.class).addMapping("/servlet/**"); + servletContext.addServlet("servletTwo", Servlet.class).addMapping("/servlet/*"); assertThatExceptionOfType(IllegalArgumentException.class) .isThrownBy(() -> this.matcherRegistry.requestMatchers("/**")); } diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/builders/RequestMatchersBuilderTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/builders/RequestMatchersBuilderTests.java new file mode 100644 index 00000000000..19cc7e2b736 --- /dev/null +++ b/config/src/test/java/org/springframework/security/config/annotation/web/builders/RequestMatchersBuilderTests.java @@ -0,0 +1,299 @@ +/* + * Copyright 2012-2023 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 + * + * https://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.security.config.annotation.web.builders; + +import java.util.Arrays; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; +import java.util.function.Consumer; + +import jakarta.servlet.MultipartConfigElement; +import jakarta.servlet.Servlet; +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletRegistration; +import jakarta.servlet.ServletSecurityElement; +import org.junit.jupiter.api.Test; + +import org.springframework.http.HttpMethod; +import org.springframework.lang.NonNull; +import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.security.web.util.matcher.RequestMatcher; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.web.context.support.GenericWebApplicationContext; +import org.springframework.web.servlet.DispatcherServlet; +import org.springframework.web.servlet.handler.HandlerMappingIntrospector; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +class RequestMatchersBuilderTests { + + @Test + void matchersWhenDefaultDispatcherServletThenMvc() { + MockServletContext servletContext = MockServletContext.mvc(); + RequestMatchersBuilder builder = requestMatchersBuilder(servletContext); + RequestMatcher[] matchers = builder.matchers("/mvc"); + assertThat(matchers[0]).isInstanceOf(MvcRequestMatcher.class); + MvcRequestMatcher matcher = (MvcRequestMatcher) matchers[0]; + assertThat(ReflectionTestUtils.getField(matcher, "servletPath")).isNull(); + assertThat(ReflectionTestUtils.getField(matcher, "pattern")).isEqualTo("/mvc"); + } + + @Test + void httpMethodMatchersWhenDefaultDispatcherServletThenMvc() { + MockServletContext servletContext = MockServletContext.mvc(); + RequestMatchersBuilder builder = requestMatchersBuilder(servletContext); + RequestMatcher[] matchers = builder.matchers(HttpMethod.GET, "/mvc"); + assertThat(matchers[0]).isInstanceOf(MvcRequestMatcher.class); + MvcRequestMatcher matcher = (MvcRequestMatcher) matchers[0]; + assertThat(ReflectionTestUtils.getField(matcher, "servletPath")).isNull(); + assertThat(ReflectionTestUtils.getField(matcher, "pattern")).isEqualTo("/mvc"); + assertThat(ReflectionTestUtils.getField(matcher, "method")).isEqualTo(HttpMethod.GET); + } + + @Test + void matchersWhenPathDispatcherServletThenMvc() { + MockServletContext servletContext = new MockServletContext(); + servletContext.addServlet("dispatcherServlet", DispatcherServlet.class).addMapping("/mvc/*"); + RequestMatchersBuilder builder = requestMatchersBuilder(servletContext); + RequestMatcher[] matchers = builder.matchers("/controller"); + assertThat(matchers[0]).isInstanceOf(MvcRequestMatcher.class); + MvcRequestMatcher matcher = (MvcRequestMatcher) matchers[0]; + assertThat(ReflectionTestUtils.getField(matcher, "servletPath")).isEqualTo("/mvc"); + assertThat(ReflectionTestUtils.getField(matcher, "pattern")).isEqualTo("/controller"); + } + + @Test + void matchersWhenAlsoExtraServletContainerMappingsThenMvc() { + MockServletContext servletContext = new MockServletContext(); + servletContext.addServlet("default", Servlet.class); + servletContext.addServlet("jspServlet", Servlet.class).addMapping("*.jsp", "*.jspx"); + servletContext.addServlet("dispatcherServlet", DispatcherServlet.class).addMapping("/mvc/*"); + RequestMatchersBuilder builder = requestMatchersBuilder(servletContext); + RequestMatcher[] matchers = builder.matchers("/controller"); + assertThat(matchers[0]).isInstanceOf(MvcRequestMatcher.class); + MvcRequestMatcher matcher = (MvcRequestMatcher) matchers[0]; + assertThat(ReflectionTestUtils.getField(matcher, "servletPath")).isEqualTo("/mvc"); + assertThat(ReflectionTestUtils.getField(matcher, "pattern")).isEqualTo("/controller"); + } + + @Test + void matchersWhenOnlyDefaultServletThenAnt() { + MockServletContext servletContext = new MockServletContext(); + servletContext.addServlet("default", Servlet.class).addMapping("/"); + RequestMatchersBuilder builder = requestMatchersBuilder(servletContext); + RequestMatcher[] matchers = builder.matchers("/controller"); + assertThat(matchers[0]).isInstanceOf(AntPathRequestMatcher.class); + AntPathRequestMatcher matcher = (AntPathRequestMatcher) matchers[0]; + assertThat(ReflectionTestUtils.getField(matcher, "pattern")).isEqualTo("/controller"); + } + + @Test + void matchersWhenNoHandlerMappingIntrospectorThenAnt() { + MockServletContext servletContext = MockServletContext.mvc(); + RequestMatchersBuilder builder = requestMatchersBuilder(servletContext, (context) -> { + }); + RequestMatcher[] matchers = builder.matchers("/controller"); + assertThat(matchers[0]).isInstanceOf(AntPathRequestMatcher.class); + AntPathRequestMatcher matcher = (AntPathRequestMatcher) matchers[0]; + assertThat(ReflectionTestUtils.getField(matcher, "pattern")).isEqualTo("/controller"); + } + + @Test + void matchersWhenNoDispatchServletThenAnt() { + MockServletContext servletContext = new MockServletContext(); + servletContext.addServlet("default", Servlet.class).addMapping("/"); + servletContext.addServlet("messageDispatcherServlet", Servlet.class).addMapping("/services/*"); + RequestMatchersBuilder builder = requestMatchersBuilder(servletContext); + RequestMatcher[] matchers = builder.matchers("/services/endpoint"); + assertThat(matchers[0]).isInstanceOf(AntPathRequestMatcher.class); + AntPathRequestMatcher matcher = (AntPathRequestMatcher) matchers[0]; + assertThat(ReflectionTestUtils.getField(matcher, "pattern")).isEqualTo("/services/endpoint"); + } + + @Test + void matchersWhenMixedServletsThenRequiresServletPath() { + MockServletContext servletContext = MockServletContext.mvc(); + servletContext.addServlet("messageDispatcherServlet", Servlet.class).addMapping("/services/*"); + RequestMatchersBuilder builder = requestMatchersBuilder(servletContext); + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> builder.matchers("/services/endpoint")); + RequestMatcher[] matchers = builder.servletPath("/services").matchers("/endpoint"); + assertThat(matchers[0]).isInstanceOf(AntPathRequestMatcher.class); + assertThat(ReflectionTestUtils.getField(matchers[0], "pattern")).isEqualTo("/services/endpoint"); + matchers = builder.servletPath("/").matchers("/controller"); + assertThat(matchers[0]).isInstanceOf(MvcRequestMatcher.class); + assertThat(ReflectionTestUtils.getField(matchers[0], "servletPath")).isEqualTo("/"); + assertThat(ReflectionTestUtils.getField(matchers[0], "pattern")).isEqualTo("/controller"); + } + + @Test + void matchersWhenDispatcherServletNotDefaultThenRequiresServletPath() { + MockServletContext servletContext = new MockServletContext(); + servletContext.addServlet("default", Servlet.class).addMapping("/"); + servletContext.addServlet("dispatcherServlet", DispatcherServlet.class).addMapping("/mvc/*"); + RequestMatchersBuilder builder = requestMatchersBuilder(servletContext); + assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> builder.matchers("/controller")); + RequestMatcher[] matchers = builder.servletPath("/mvc").matchers("/controller"); + assertThat(matchers[0]).isInstanceOf(MvcRequestMatcher.class); + assertThat(ReflectionTestUtils.getField(matchers[0], "servletPath")).isEqualTo("/mvc"); + assertThat(ReflectionTestUtils.getField(matchers[0], "pattern")).isEqualTo("/controller"); + matchers = builder.servletPath("/").matchers("/endpoint"); + assertThat(matchers[0]).isInstanceOf(AntPathRequestMatcher.class); + assertThat(ReflectionTestUtils.getField(matchers[0], "pattern")).isEqualTo("/endpoint"); + } + + @Test + void matcherWhenFacesServletThenAnt() { + MockServletContext servletContext = MockServletContext.mvc(); + servletContext.addServlet("facesServlet", Servlet.class).addMapping("/faces/", "*.jsf", "*.faces", "*.xhtml"); + RequestMatchersBuilder builder = requestMatchersBuilder(servletContext); + RequestMatcher matcher = builder.servletPath("/faces/").matcher(); + assertThat(matcher).isInstanceOf(AntPathRequestMatcher.class); + assertThat(ReflectionTestUtils.getField(matcher, "pattern")).isEqualTo("/faces/"); + } + + RequestMatchersBuilder requestMatchersBuilder(ServletContext servletContext) { + return requestMatchersBuilder(servletContext, + (context) -> context.registerBean("mvcHandlerMappingIntrospector", HandlerMappingIntrospector.class)); + } + + RequestMatchersBuilder requestMatchersBuilder(ServletContext servletContext, + Consumer consumer) { + GenericWebApplicationContext context = new GenericWebApplicationContext(servletContext); + consumer.accept(context); + context.refresh(); + return new RequestMatchersBuilder(context); + } + + static class MockServletContext extends org.springframework.mock.web.MockServletContext { + + private final Map registrations = new LinkedHashMap<>(); + + static MockServletContext mvc() { + MockServletContext servletContext = new MockServletContext(); + servletContext.addServlet("dispatcherServlet", DispatcherServlet.class).addMapping("/"); + return servletContext; + } + + @NonNull + @Override + public ServletRegistration.Dynamic addServlet(@NonNull String servletName, Class clazz) { + ServletRegistration.Dynamic dynamic = new MockServletRegistration(servletName, clazz); + this.registrations.put(servletName, dynamic); + return dynamic; + } + + @NonNull + @Override + public Map getServletRegistrations() { + return this.registrations; + } + + private static class MockServletRegistration implements ServletRegistration.Dynamic { + + private final String name; + + private final Class clazz; + + private final Set mappings = new LinkedHashSet<>(); + + MockServletRegistration(String name, Class clazz) { + this.name = name; + this.clazz = clazz; + } + + @Override + public void setLoadOnStartup(int loadOnStartup) { + + } + + @Override + public Set setServletSecurity(ServletSecurityElement constraint) { + return null; + } + + @Override + public void setMultipartConfig(MultipartConfigElement multipartConfig) { + + } + + @Override + public void setRunAsRole(String roleName) { + + } + + @Override + public void setAsyncSupported(boolean isAsyncSupported) { + + } + + @Override + public Set addMapping(String... urlPatterns) { + this.mappings.addAll(Arrays.asList(urlPatterns)); + return this.mappings; + } + + @Override + public Collection getMappings() { + return this.mappings; + } + + @Override + public String getRunAsRole() { + return null; + } + + @Override + public String getName() { + return this.name; + } + + @Override + public String getClassName() { + return this.clazz.getName(); + } + + @Override + public boolean setInitParameter(String name, String value) { + return false; + } + + @Override + public String getInitParameter(String name) { + return null; + } + + @Override + public Set setInitParameters(Map initParameters) { + return null; + } + + @Override + public Map getInitParameters() { + return null; + } + + } + + } + +} diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/AuthorizeHttpRequestsConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/AuthorizeHttpRequestsConfigurerTests.java index bae3731fbea..9dbbdd3b258 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/AuthorizeHttpRequestsConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/AuthorizeHttpRequestsConfigurerTests.java @@ -120,7 +120,7 @@ public void configureNoParameterWhenAnyRequestIncompleteMappingThenException() { public void configureWhenMvcMatcherAfterAnyRequestThenException() { assertThatExceptionOfType(BeanCreationException.class) .isThrownBy(() -> this.spring.register(AfterAnyRequestConfig.class).autowire()) - .withMessageContaining("Can't configure mvcMatchers after anyRequest"); + .withMessageContaining("Can't configure requestMatchers after anyRequest"); } @Test