Browse Source

Configure WebInvocationPrivilegeEvaluator bean for multiple filter chains

Closes gh-10554
Marcus Da Coregio 3 năm trước cách đây
mục cha
commit
d884d9a461

+ 50 - 3
config/src/main/java/org/springframework/security/config/annotation/web/builders/WebSecurity.java

@@ -20,6 +20,7 @@ import java.util.ArrayList;
 import java.util.List;
 
 import jakarta.servlet.Filter;
+import jakarta.servlet.ServletContext;
 import jakarta.servlet.http.HttpServletRequest;
 
 import org.apache.commons.logging.Log;
@@ -33,6 +34,7 @@ import org.springframework.http.HttpMethod;
 import org.springframework.security.access.PermissionEvaluator;
 import org.springframework.security.access.expression.SecurityExpressionHandler;
 import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
+import org.springframework.security.authorization.AuthorizationManager;
 import org.springframework.security.config.annotation.AbstractConfiguredSecurityBuilder;
 import org.springframework.security.config.annotation.ObjectPostProcessor;
 import org.springframework.security.config.annotation.SecurityBuilder;
@@ -47,9 +49,12 @@ import org.springframework.security.web.DefaultSecurityFilterChain;
 import org.springframework.security.web.FilterChainProxy;
 import org.springframework.security.web.FilterInvocation;
 import org.springframework.security.web.SecurityFilterChain;
+import org.springframework.security.web.access.AuthorizationManagerWebInvocationPrivilegeEvaluator;
 import org.springframework.security.web.access.DefaultWebInvocationPrivilegeEvaluator;
+import org.springframework.security.web.access.RequestMatcherDelegatingWebInvocationPrivilegeEvaluator;
 import org.springframework.security.web.access.WebInvocationPrivilegeEvaluator;
 import org.springframework.security.web.access.expression.DefaultWebSecurityExpressionHandler;
+import org.springframework.security.web.access.intercept.AuthorizationFilter;
 import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
 import org.springframework.security.web.debug.DebugFilter;
 import org.springframework.security.web.firewall.HttpFirewall;
@@ -57,7 +62,9 @@ import org.springframework.security.web.firewall.RequestRejectedHandler;
 import org.springframework.security.web.firewall.StrictHttpFirewall;
 import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher;
 import org.springframework.security.web.util.matcher.RequestMatcher;
+import org.springframework.security.web.util.matcher.RequestMatcherEntry;
 import org.springframework.util.Assert;
+import org.springframework.web.context.ServletContextAware;
 import org.springframework.web.filter.DelegatingFilterProxy;
 
 /**
@@ -81,7 +88,7 @@ import org.springframework.web.filter.DelegatingFilterProxy;
  * @see WebSecurityConfiguration
  */
 public final class WebSecurity extends AbstractConfiguredSecurityBuilder<Filter, WebSecurity>
-		implements SecurityBuilder<Filter>, ApplicationContextAware {
+		implements SecurityBuilder<Filter>, ApplicationContextAware, ServletContextAware {
 
 	private final Log logger = LogFactory.getLog(getClass());
 
@@ -108,6 +115,8 @@ public final class WebSecurity extends AbstractConfiguredSecurityBuilder<Filter,
 	private Runnable postBuildAction = () -> {
 	};
 
+	private ServletContext servletContext;
+
 	/**
 	 * Creates a new instance
 	 * @param objectPostProcessor the {@link ObjectPostProcessor} to use
@@ -252,6 +261,8 @@ public final class WebSecurity extends AbstractConfiguredSecurityBuilder<Filter,
 	 * {@link WebSecurityConfigurerAdapter}.
 	 * @param securityInterceptor the {@link FilterSecurityInterceptor} to use
 	 * @return the {@link WebSecurity} for further customizations
+	 * @deprecated Use {@link #privilegeEvaluator(WebInvocationPrivilegeEvaluator)}
+	 * instead
 	 */
 	public WebSecurity securityInterceptor(FilterSecurityInterceptor securityInterceptor) {
 		this.filterSecurityInterceptor = securityInterceptor;
@@ -278,11 +289,22 @@ public final class WebSecurity extends AbstractConfiguredSecurityBuilder<Filter,
 						+ ".addSecurityFilterChainBuilder directly");
 		int chainSize = this.ignoredRequests.size() + this.securityFilterChainBuilders.size();
 		List<SecurityFilterChain> securityFilterChains = new ArrayList<>(chainSize);
+		List<RequestMatcherEntry<List<WebInvocationPrivilegeEvaluator>>> requestMatcherPrivilegeEvaluatorsEntries = new ArrayList<>();
 		for (RequestMatcher ignoredRequest : this.ignoredRequests) {
-			securityFilterChains.add(new DefaultSecurityFilterChain(ignoredRequest));
+			SecurityFilterChain securityFilterChain = new DefaultSecurityFilterChain(ignoredRequest);
+			securityFilterChains.add(securityFilterChain);
+			requestMatcherPrivilegeEvaluatorsEntries
+					.add(getRequestMatcherPrivilegeEvaluatorsEntry(securityFilterChain));
 		}
 		for (SecurityBuilder<? extends SecurityFilterChain> securityFilterChainBuilder : this.securityFilterChainBuilders) {
-			securityFilterChains.add(securityFilterChainBuilder.build());
+			SecurityFilterChain securityFilterChain = securityFilterChainBuilder.build();
+			securityFilterChains.add(securityFilterChain);
+			requestMatcherPrivilegeEvaluatorsEntries
+					.add(getRequestMatcherPrivilegeEvaluatorsEntry(securityFilterChain));
+		}
+		if (this.privilegeEvaluator == null) {
+			this.privilegeEvaluator = new RequestMatcherDelegatingWebInvocationPrivilegeEvaluator(
+					requestMatcherPrivilegeEvaluatorsEntries);
 		}
 		FilterChainProxy filterChainProxy = new FilterChainProxy(securityFilterChains);
 		if (this.httpFirewall != null) {
@@ -306,6 +328,26 @@ public final class WebSecurity extends AbstractConfiguredSecurityBuilder<Filter,
 		return result;
 	}
 
+	private RequestMatcherEntry<List<WebInvocationPrivilegeEvaluator>> getRequestMatcherPrivilegeEvaluatorsEntry(
+			SecurityFilterChain securityFilterChain) {
+		List<WebInvocationPrivilegeEvaluator> privilegeEvaluators = new ArrayList<>();
+		for (Filter filter : securityFilterChain.getFilters()) {
+			if (filter instanceof FilterSecurityInterceptor) {
+				DefaultWebInvocationPrivilegeEvaluator defaultWebInvocationPrivilegeEvaluator = new DefaultWebInvocationPrivilegeEvaluator(
+						(FilterSecurityInterceptor) filter);
+				defaultWebInvocationPrivilegeEvaluator.setServletContext(this.servletContext);
+				privilegeEvaluators.add(defaultWebInvocationPrivilegeEvaluator);
+				continue;
+			}
+			if (filter instanceof AuthorizationFilter) {
+				AuthorizationManager<HttpServletRequest> authorizationManager = ((AuthorizationFilter) filter)
+						.getAuthorizationManager();
+				privilegeEvaluators.add(new AuthorizationManagerWebInvocationPrivilegeEvaluator(authorizationManager));
+			}
+		}
+		return new RequestMatcherEntry<>(securityFilterChain::matches, privilegeEvaluators);
+	}
+
 	@Override
 	public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
 		this.defaultWebSecurityExpressionHandler.setApplicationContext(applicationContext);
@@ -333,6 +375,11 @@ public final class WebSecurity extends AbstractConfiguredSecurityBuilder<Filter,
 		}
 	}
 
+	@Override
+	public void setServletContext(ServletContext servletContext) {
+		this.servletContext = servletContext;
+	}
+
 	/**
 	 * An {@link IgnoredRequestConfigurer} that allows optionally configuring the
 	 * {@link MvcRequestMatcher#setMethod(HttpMethod)}

+ 2 - 2
config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfiguration.java

@@ -127,8 +127,8 @@ public class WebSecurityConfiguration implements ImportAware, BeanClassLoaderAwa
 	}
 
 	/**
-	 * Creates the {@link WebInvocationPrivilegeEvaluator} that is necessary for the JSP
-	 * tag support.
+	 * Creates the {@link WebInvocationPrivilegeEvaluator} that is necessary to evaluate
+	 * privileges for a given web URI
 	 * @return the {@link WebInvocationPrivilegeEvaluator}
 	 */
 	@Bean

+ 191 - 5
config/src/test/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfigurationTests.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2020 the original author or authors.
+ * Copyright 2002-2021 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.
@@ -33,6 +33,7 @@ import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.context.annotation.Import;
+import org.springframework.core.Ordered;
 import org.springframework.core.annotation.Order;
 import org.springframework.expression.EvaluationContext;
 import org.springframework.expression.Expression;
@@ -62,7 +63,7 @@ import org.springframework.security.core.AuthenticationException;
 import org.springframework.security.web.FilterChainProxy;
 import org.springframework.security.web.FilterInvocation;
 import org.springframework.security.web.SecurityFilterChain;
-import org.springframework.security.web.access.DefaultWebInvocationPrivilegeEvaluator;
+import org.springframework.security.web.access.RequestMatcherDelegatingWebInvocationPrivilegeEvaluator;
 import org.springframework.security.web.access.WebInvocationPrivilegeEvaluator;
 import org.springframework.security.web.access.expression.DefaultWebSecurityExpressionHandler;
 import org.springframework.test.web.servlet.MockMvc;
@@ -84,6 +85,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
  * @author Rob Winch
  * @author Joe Grandja
  * @author Evgeniy Cheban
+ * @author Marcus Da Coregio
  */
 @ExtendWith(SpringTestContextExtension.class)
 public class WebSecurityConfigurationTests {
@@ -218,10 +220,10 @@ public class WebSecurityConfigurationTests {
 	}
 
 	@Test
-	public void loadConfigWhenDefaultWebInvocationPrivilegeEvaluatorThenDefaultIsRegistered() {
+	public void loadConfigWhenDefaultWebInvocationPrivilegeEvaluatorThenRequestMatcherIsRegistered() {
 		this.spring.register(WebInvocationPrivilegeEvaluatorDefaultsConfig.class).autowire();
 		assertThat(this.spring.getContext().getBean(WebInvocationPrivilegeEvaluator.class))
-				.isInstanceOf(DefaultWebInvocationPrivilegeEvaluator.class);
+				.isInstanceOf(RequestMatcherDelegatingWebInvocationPrivilegeEvaluator.class);
 	}
 
 	@Test
@@ -229,7 +231,7 @@ public class WebSecurityConfigurationTests {
 		this.spring.register(AuthorizeRequestsFilterChainConfig.class).autowire();
 
 		assertThat(this.spring.getContext().getBean(WebInvocationPrivilegeEvaluator.class))
-				.isInstanceOf(DefaultWebInvocationPrivilegeEvaluator.class);
+				.isInstanceOf(RequestMatcherDelegatingWebInvocationPrivilegeEvaluator.class);
 	}
 
 	// SEC-2303
@@ -375,6 +377,69 @@ public class WebSecurityConfigurationTests {
 		assertThat(filterChains.get(1).matches(request)).isTrue();
 	}
 
+	@Test
+	public void loadConfigWhenTwoSecurityFilterChainsThenRequestMatcherDelegatingWebInvocationPrivilegeEvaluator() {
+		this.spring.register(TwoSecurityFilterChainConfig.class).autowire();
+		assertThat(this.spring.getContext().getBean(WebInvocationPrivilegeEvaluator.class))
+				.isInstanceOf(RequestMatcherDelegatingWebInvocationPrivilegeEvaluator.class);
+	}
+
+	@Test
+	public void loadConfigWhenTwoSecurityFilterChainDebugThenRequestMatcherDelegatingWebInvocationPrivilegeEvaluator() {
+		this.spring.register(TwoSecurityFilterChainConfig.class).autowire();
+		assertThat(this.spring.getContext().getBean(WebInvocationPrivilegeEvaluator.class))
+				.isInstanceOf(RequestMatcherDelegatingWebInvocationPrivilegeEvaluator.class);
+	}
+
+	// gh-10554
+	@Test
+	public void loadConfigWhenMultipleSecurityFilterChainsThenWebInvocationPrivilegeEvaluatorApplySecurity() {
+		this.spring.register(MultipleSecurityFilterChainConfig.class).autowire();
+		WebInvocationPrivilegeEvaluator privilegeEvaluator = this.spring.getContext()
+				.getBean(WebInvocationPrivilegeEvaluator.class);
+		assertUserPermissions(privilegeEvaluator);
+		assertAdminPermissions(privilegeEvaluator);
+		assertAnotherUserPermission(privilegeEvaluator);
+	}
+
+	// gh-10554
+	@Test
+	public void loadConfigWhenMultipleSecurityFilterChainAndIgnoringThenWebInvocationPrivilegeEvaluatorAcceptsNullAuthenticationOnIgnored() {
+		this.spring.register(MultipleSecurityFilterChainIgnoringConfig.class).autowire();
+		WebInvocationPrivilegeEvaluator privilegeEvaluator = this.spring.getContext()
+				.getBean(WebInvocationPrivilegeEvaluator.class);
+		assertUserPermissions(privilegeEvaluator);
+		assertAdminPermissions(privilegeEvaluator);
+		assertAnotherUserPermission(privilegeEvaluator);
+		// null authentication
+		assertThat(privilegeEvaluator.isAllowed("/user", null)).isFalse();
+		assertThat(privilegeEvaluator.isAllowed("/admin", null)).isFalse();
+		assertThat(privilegeEvaluator.isAllowed("/another", null)).isFalse();
+		assertThat(privilegeEvaluator.isAllowed("/ignoring1", null)).isTrue();
+		assertThat(privilegeEvaluator.isAllowed("/ignoring1/child", null)).isTrue();
+	}
+
+	private void assertAnotherUserPermission(WebInvocationPrivilegeEvaluator privilegeEvaluator) {
+		Authentication anotherUser = new TestingAuthenticationToken("anotherUser", "password", "ROLE_ANOTHER");
+		assertThat(privilegeEvaluator.isAllowed("/user", anotherUser)).isFalse();
+		assertThat(privilegeEvaluator.isAllowed("/admin", anotherUser)).isFalse();
+		assertThat(privilegeEvaluator.isAllowed("/another", anotherUser)).isTrue();
+	}
+
+	private void assertAdminPermissions(WebInvocationPrivilegeEvaluator privilegeEvaluator) {
+		Authentication admin = new TestingAuthenticationToken("admin", "password", "ROLE_ADMIN");
+		assertThat(privilegeEvaluator.isAllowed("/user", admin)).isFalse();
+		assertThat(privilegeEvaluator.isAllowed("/admin", admin)).isTrue();
+		assertThat(privilegeEvaluator.isAllowed("/another", admin)).isTrue();
+	}
+
+	private void assertUserPermissions(WebInvocationPrivilegeEvaluator privilegeEvaluator) {
+		Authentication user = new TestingAuthenticationToken("user", "password", "ROLE_USER");
+		assertThat(privilegeEvaluator.isAllowed("/user", user)).isTrue();
+		assertThat(privilegeEvaluator.isAllowed("/admin", user)).isFalse();
+		assertThat(privilegeEvaluator.isAllowed("/another", user)).isTrue();
+	}
+
 	@EnableWebSecurity
 	@Import(AuthenticationTestConfiguration.class)
 	static class SortedWebSecurityConfigurerAdaptersConfig {
@@ -1008,4 +1073,125 @@ public class WebSecurityConfigurationTests {
 
 	}
 
+	@EnableWebSecurity
+	static class TwoSecurityFilterChainConfig {
+
+		@Bean
+		@Order(Ordered.HIGHEST_PRECEDENCE)
+		public SecurityFilterChain path1(HttpSecurity http) throws Exception {
+			// @formatter:off
+			http
+				.requestMatchers((requests) -> requests.antMatchers("/path1/**"))
+				.authorizeRequests((requests) -> requests.anyRequest().authenticated());
+			// @formatter:on
+			return http.build();
+		}
+
+		@Bean
+		@Order(Ordered.LOWEST_PRECEDENCE)
+		public SecurityFilterChain permitAll(HttpSecurity http) throws Exception {
+			http.authorizeRequests((requests) -> requests.anyRequest().permitAll());
+			return http.build();
+		}
+
+	}
+
+	@EnableWebSecurity(debug = true)
+	static class TwoSecurityFilterChainDebugConfig {
+
+		@Bean
+		@Order(Ordered.HIGHEST_PRECEDENCE)
+		public SecurityFilterChain path1(HttpSecurity http) throws Exception {
+			// @formatter:off
+			http
+					.requestMatchers((requests) -> requests.antMatchers("/path1/**"))
+					.authorizeRequests((requests) -> requests.anyRequest().authenticated());
+			// @formatter:on
+			return http.build();
+		}
+
+		@Bean
+		@Order(Ordered.LOWEST_PRECEDENCE)
+		public SecurityFilterChain permitAll(HttpSecurity http) throws Exception {
+			http.authorizeRequests((requests) -> requests.anyRequest().permitAll());
+			return http.build();
+		}
+
+	}
+
+	@EnableWebSecurity
+	@Import(AuthenticationTestConfiguration.class)
+	static class MultipleSecurityFilterChainConfig {
+
+		@Bean
+		@Order(Ordered.HIGHEST_PRECEDENCE)
+		public SecurityFilterChain notAuthorized(HttpSecurity http) throws Exception {
+			// @formatter:off
+			http
+				.requestMatchers((requests) -> requests.antMatchers("/user"))
+				.authorizeRequests((requests) -> requests.anyRequest().hasRole("USER"));
+			// @formatter:on
+			return http.build();
+		}
+
+		@Bean
+		@Order(Ordered.HIGHEST_PRECEDENCE + 1)
+		public SecurityFilterChain path1(HttpSecurity http) throws Exception {
+			// @formatter:off
+			http
+				.requestMatchers((requests) -> requests.antMatchers("/admin"))
+				.authorizeRequests((requests) -> requests.anyRequest().hasRole("ADMIN"));
+			// @formatter:on
+			return http.build();
+		}
+
+		@Bean
+		@Order(Ordered.LOWEST_PRECEDENCE)
+		public SecurityFilterChain permitAll(HttpSecurity http) throws Exception {
+			http.authorizeRequests((requests) -> requests.anyRequest().permitAll());
+			return http.build();
+		}
+
+	}
+
+	@EnableWebSecurity
+	@Import(AuthenticationTestConfiguration.class)
+	static class MultipleSecurityFilterChainIgnoringConfig {
+
+		@Bean
+		public WebSecurityCustomizer webSecurityCustomizer() {
+			return (web) -> web.ignoring().antMatchers("/ignoring1/**");
+		}
+
+		@Bean
+		@Order(Ordered.HIGHEST_PRECEDENCE)
+		public SecurityFilterChain notAuthorized(HttpSecurity http) throws Exception {
+			// @formatter:off
+			http
+					.requestMatchers((requests) -> requests.antMatchers("/user"))
+					.authorizeRequests((requests) -> requests.anyRequest().hasRole("USER"));
+			// @formatter:on
+			return http.build();
+		}
+
+		@Bean
+		@Order(Ordered.HIGHEST_PRECEDENCE + 1)
+		public SecurityFilterChain admin(HttpSecurity http) throws Exception {
+			// @formatter:off
+			http
+					.requestMatchers((requests) -> requests.antMatchers("/admin"))
+					.authorizeRequests((requests) -> requests.anyRequest().hasRole("ADMIN"));
+			// @formatter:on
+			return http.build();
+		}
+
+		@Bean
+		@Order(Ordered.LOWEST_PRECEDENCE)
+		public SecurityFilterChain permitAll(HttpSecurity http) throws Exception {
+			http.authorizeRequests((requests) -> requests.anyRequest().permitAll());
+			return http.build();
+		}
+
+	}
+
 }

+ 122 - 0
web/src/main/java/org/springframework/security/web/access/RequestMatcherDelegatingWebInvocationPrivilegeEvaluator.java

@@ -0,0 +1,122 @@
+/*
+ * Copyright 2002-2021 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.web.access;
+
+import java.util.Collections;
+import java.util.List;
+
+import jakarta.servlet.http.HttpServletRequest;
+
+import org.springframework.security.core.Authentication;
+import org.springframework.security.web.FilterInvocation;
+import org.springframework.security.web.util.matcher.RequestMatcherEntry;
+import org.springframework.util.Assert;
+
+/**
+ * A {@link WebInvocationPrivilegeEvaluator} which delegates to a list of
+ * {@link WebInvocationPrivilegeEvaluator} based on a
+ * {@link org.springframework.security.web.util.matcher.RequestMatcher} evaluation
+ *
+ * @author Marcus Da Coregio
+ * @since 5.7
+ */
+public final class RequestMatcherDelegatingWebInvocationPrivilegeEvaluator implements WebInvocationPrivilegeEvaluator {
+
+	private final List<RequestMatcherEntry<List<WebInvocationPrivilegeEvaluator>>> delegates;
+
+	public RequestMatcherDelegatingWebInvocationPrivilegeEvaluator(
+			List<RequestMatcherEntry<List<WebInvocationPrivilegeEvaluator>>> requestMatcherPrivilegeEvaluatorsEntries) {
+		Assert.notNull(requestMatcherPrivilegeEvaluatorsEntries, "requestMatcherPrivilegeEvaluators cannot be null");
+		for (RequestMatcherEntry<List<WebInvocationPrivilegeEvaluator>> entry : requestMatcherPrivilegeEvaluatorsEntries) {
+			Assert.notNull(entry.getRequestMatcher(), "requestMatcher cannot be null");
+			Assert.notNull(entry.getEntry(), "webInvocationPrivilegeEvaluators cannot be null");
+		}
+		this.delegates = requestMatcherPrivilegeEvaluatorsEntries;
+	}
+
+	/**
+	 * Determines whether the user represented by the supplied <tt>Authentication</tt>
+	 * object is allowed to invoke the supplied URI.
+	 * <p>
+	 * Uses the provided URI in the
+	 * {@link org.springframework.security.web.util.matcher.RequestMatcher#matches(HttpServletRequest)}
+	 * for every {@code RequestMatcher} configured. If no {@code RequestMatcher} is
+	 * matched, or if there is not an available {@code WebInvocationPrivilegeEvaluator},
+	 * returns {@code true}.
+	 * @param uri the URI excluding the context path (a default context path setting will
+	 * be used)
+	 * @return true if access is allowed, false if denied
+	 */
+	@Override
+	public boolean isAllowed(String uri, Authentication authentication) {
+		List<WebInvocationPrivilegeEvaluator> privilegeEvaluators = getDelegate(null, uri, null);
+		if (privilegeEvaluators.isEmpty()) {
+			return true;
+		}
+		for (WebInvocationPrivilegeEvaluator evaluator : privilegeEvaluators) {
+			boolean isAllowed = evaluator.isAllowed(uri, authentication);
+			if (!isAllowed) {
+				return false;
+			}
+		}
+		return true;
+	}
+
+	/**
+	 * Determines whether the user represented by the supplied <tt>Authentication</tt>
+	 * object is allowed to invoke the supplied URI.
+	 * <p>
+	 * Uses the provided URI in the
+	 * {@link org.springframework.security.web.util.matcher.RequestMatcher#matches(HttpServletRequest)}
+	 * for every {@code RequestMatcher} configured. If no {@code RequestMatcher} is
+	 * matched, or if there is not an available {@code WebInvocationPrivilegeEvaluator},
+	 * returns {@code true}.
+	 * @param uri the URI excluding the context path (a default context path setting will
+	 * be used)
+	 * @param contextPath the context path (may be null, in which case a default value
+	 * will be used).
+	 * @param method the HTTP method (or null, for any method)
+	 * @param authentication the <tt>Authentication</tt> instance whose authorities should
+	 * be used in evaluation whether access should be granted.
+	 * @return true if access is allowed, false if denied
+	 */
+	@Override
+	public boolean isAllowed(String contextPath, String uri, String method, Authentication authentication) {
+		List<WebInvocationPrivilegeEvaluator> privilegeEvaluators = getDelegate(contextPath, uri, method);
+		if (privilegeEvaluators.isEmpty()) {
+			return true;
+		}
+		for (WebInvocationPrivilegeEvaluator evaluator : privilegeEvaluators) {
+			boolean isAllowed = evaluator.isAllowed(contextPath, uri, method, authentication);
+			if (!isAllowed) {
+				return false;
+			}
+		}
+		return true;
+	}
+
+	private List<WebInvocationPrivilegeEvaluator> getDelegate(String contextPath, String uri, String method) {
+		FilterInvocation filterInvocation = new FilterInvocation(contextPath, uri, method);
+		for (RequestMatcherEntry<List<WebInvocationPrivilegeEvaluator>> delegate : this.delegates) {
+			if (delegate.getRequestMatcher().matches(filterInvocation.getHttpRequest())) {
+				return delegate.getEntry();
+			}
+		}
+		return Collections.emptyList();
+	}
+
+}

+ 9 - 1
web/src/main/java/org/springframework/security/web/access/intercept/AuthorizationFilter.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2020 the original author or authors.
+ * Copyright 2002-2021 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.
@@ -67,4 +67,12 @@ public class AuthorizationFilter extends OncePerRequestFilter {
 		return authentication;
 	}
 
+	/**
+	 * Gets the {@link AuthorizationManager} used by this filter
+	 * @return the {@link AuthorizationManager}
+	 */
+	public AuthorizationManager<HttpServletRequest> getAuthorizationManager() {
+		return this.authorizationManager;
+	}
+
 }

+ 5 - 1
web/src/main/java/org/springframework/security/web/debug/DebugFilter.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2016 the original author or authors.
+ * Copyright 2002-2021 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.
@@ -151,6 +151,10 @@ public final class DebugFilter implements Filter {
 	public void destroy() {
 	}
 
+	public FilterChainProxy getFilterChainProxy() {
+		return this.filterChainProxy;
+	}
+
 	static class DebugRequestWrapper extends HttpServletRequestWrapper {
 
 		private static final Logger logger = new Logger();

+ 179 - 0
web/src/test/java/org/springframework/security/web/access/RequestMatcherDelegatingWebInvocationPrivilegeEvaluatorTests.java

@@ -0,0 +1,179 @@
+/*
+ * Copyright 2002-2021 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.web.access;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import org.springframework.security.authentication.TestingAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.web.util.matcher.RequestMatcher;
+import org.springframework.security.web.util.matcher.RequestMatcherEntry;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+
+/**
+ * Tests for {@link RequestMatcherDelegatingWebInvocationPrivilegeEvaluator}
+ *
+ * @author Marcus Da Coregio
+ */
+class RequestMatcherDelegatingWebInvocationPrivilegeEvaluatorTests {
+
+	private final RequestMatcher alwaysMatch = mock(RequestMatcher.class);
+
+	private final RequestMatcher alwaysDeny = mock(RequestMatcher.class);
+
+	private final String uri = "/test";
+
+	private final Authentication authentication = new TestingAuthenticationToken("user", "password", "ROLE_USER");
+
+	@BeforeEach
+	void setup() {
+		given(this.alwaysMatch.matches(any())).willReturn(true);
+		given(this.alwaysDeny.matches(any())).willReturn(false);
+	}
+
+	@Test
+	void isAllowedWhenDelegatesEmptyThenAllowed() {
+		RequestMatcherDelegatingWebInvocationPrivilegeEvaluator delegating = new RequestMatcherDelegatingWebInvocationPrivilegeEvaluator(
+				Collections.emptyList());
+		assertThat(delegating.isAllowed(this.uri, this.authentication)).isTrue();
+	}
+
+	@Test
+	void isAllowedWhenNotMatchThenAllowed() {
+		RequestMatcherEntry<List<WebInvocationPrivilegeEvaluator>> notMatch = new RequestMatcherEntry<>(this.alwaysDeny,
+				Collections.singletonList(TestWebInvocationPrivilegeEvaluator.alwaysAllow()));
+		RequestMatcherDelegatingWebInvocationPrivilegeEvaluator delegating = new RequestMatcherDelegatingWebInvocationPrivilegeEvaluator(
+				Collections.singletonList(notMatch));
+		assertThat(delegating.isAllowed(this.uri, this.authentication)).isTrue();
+		verify(notMatch.getRequestMatcher()).matches(any());
+	}
+
+	@Test
+	void isAllowedWhenPrivilegeEvaluatorAllowThenAllowedTrue() {
+		RequestMatcherEntry<List<WebInvocationPrivilegeEvaluator>> delegate = new RequestMatcherEntry<>(
+				this.alwaysMatch, Collections.singletonList(TestWebInvocationPrivilegeEvaluator.alwaysAllow()));
+		RequestMatcherDelegatingWebInvocationPrivilegeEvaluator delegating = new RequestMatcherDelegatingWebInvocationPrivilegeEvaluator(
+				Collections.singletonList(delegate));
+		assertThat(delegating.isAllowed(this.uri, this.authentication)).isTrue();
+	}
+
+	@Test
+	void isAllowedWhenPrivilegeEvaluatorDenyThenAllowedFalse() {
+		RequestMatcherEntry<List<WebInvocationPrivilegeEvaluator>> delegate = new RequestMatcherEntry<>(
+				this.alwaysMatch, Collections.singletonList(TestWebInvocationPrivilegeEvaluator.alwaysDeny()));
+		RequestMatcherDelegatingWebInvocationPrivilegeEvaluator delegating = new RequestMatcherDelegatingWebInvocationPrivilegeEvaluator(
+				Collections.singletonList(delegate));
+		assertThat(delegating.isAllowed(this.uri, this.authentication)).isFalse();
+	}
+
+	@Test
+	void isAllowedWhenNotMatchThenMatchThenOnlySecondDelegateInvoked() {
+		RequestMatcherEntry<List<WebInvocationPrivilegeEvaluator>> notMatchDelegate = new RequestMatcherEntry<>(
+				this.alwaysDeny, Collections.singletonList(TestWebInvocationPrivilegeEvaluator.alwaysAllow()));
+		RequestMatcherEntry<List<WebInvocationPrivilegeEvaluator>> matchDelegate = new RequestMatcherEntry<>(
+				this.alwaysMatch, Collections.singletonList(TestWebInvocationPrivilegeEvaluator.alwaysAllow()));
+		RequestMatcherEntry<List<WebInvocationPrivilegeEvaluator>> spyNotMatchDelegate = spy(notMatchDelegate);
+		RequestMatcherEntry<List<WebInvocationPrivilegeEvaluator>> spyMatchDelegate = spy(matchDelegate);
+
+		RequestMatcherDelegatingWebInvocationPrivilegeEvaluator delegating = new RequestMatcherDelegatingWebInvocationPrivilegeEvaluator(
+				Arrays.asList(notMatchDelegate, spyMatchDelegate));
+		assertThat(delegating.isAllowed(this.uri, this.authentication)).isTrue();
+		verify(spyNotMatchDelegate.getRequestMatcher()).matches(any());
+		verify(spyNotMatchDelegate, never()).getEntry();
+		verify(spyMatchDelegate.getRequestMatcher()).matches(any());
+		verify(spyMatchDelegate, times(2)).getEntry(); // 2 times, one for constructor and
+														// other one in isAllowed
+	}
+
+	@Test
+	void isAllowedWhenDelegatePrivilegeEvaluatorsEmptyThenAllowedTrue() {
+		RequestMatcherEntry<List<WebInvocationPrivilegeEvaluator>> delegate = new RequestMatcherEntry<>(
+				this.alwaysMatch, Collections.emptyList());
+		RequestMatcherDelegatingWebInvocationPrivilegeEvaluator delegating = new RequestMatcherDelegatingWebInvocationPrivilegeEvaluator(
+				Collections.singletonList(delegate));
+		assertThat(delegating.isAllowed(this.uri, this.authentication)).isTrue();
+	}
+
+	@Test
+	void isAllowedWhenFirstDelegateDenyThenDoNotInvokeOthers() {
+		WebInvocationPrivilegeEvaluator deny = TestWebInvocationPrivilegeEvaluator.alwaysDeny();
+		WebInvocationPrivilegeEvaluator allow = TestWebInvocationPrivilegeEvaluator.alwaysAllow();
+		WebInvocationPrivilegeEvaluator spyDeny = spy(deny);
+		WebInvocationPrivilegeEvaluator spyAllow = spy(allow);
+		RequestMatcherEntry<List<WebInvocationPrivilegeEvaluator>> delegate = new RequestMatcherEntry<>(
+				this.alwaysMatch, Arrays.asList(spyDeny, spyAllow));
+
+		RequestMatcherDelegatingWebInvocationPrivilegeEvaluator delegating = new RequestMatcherDelegatingWebInvocationPrivilegeEvaluator(
+				Collections.singletonList(delegate));
+
+		assertThat(delegating.isAllowed(this.uri, this.authentication)).isFalse();
+		verify(spyDeny).isAllowed(any(), any());
+		verifyNoInteractions(spyAllow);
+	}
+
+	@Test
+	void isAllowedWhenDifferentArgumentsThenCallSpecificIsAllowedInDelegate() {
+		WebInvocationPrivilegeEvaluator deny = TestWebInvocationPrivilegeEvaluator.alwaysDeny();
+		WebInvocationPrivilegeEvaluator spyDeny = spy(deny);
+		RequestMatcherEntry<List<WebInvocationPrivilegeEvaluator>> delegate = new RequestMatcherEntry<>(
+				this.alwaysMatch, Collections.singletonList(spyDeny));
+
+		RequestMatcherDelegatingWebInvocationPrivilegeEvaluator delegating = new RequestMatcherDelegatingWebInvocationPrivilegeEvaluator(
+				Collections.singletonList(delegate));
+
+		assertThat(delegating.isAllowed(this.uri, this.authentication)).isFalse();
+		assertThat(delegating.isAllowed("/cp", this.uri, "GET", this.authentication)).isFalse();
+		verify(spyDeny).isAllowed(any(), any());
+		verify(spyDeny).isAllowed(any(), any(), any(), any());
+		verifyNoMoreInteractions(spyDeny);
+	}
+
+	@Test
+	void constructorWhenPrivilegeEvaluatorsNullThenException() {
+		RequestMatcherEntry<List<WebInvocationPrivilegeEvaluator>> entry = new RequestMatcherEntry<>(this.alwaysMatch,
+				null);
+		assertThatIllegalArgumentException().isThrownBy(
+				() -> new RequestMatcherDelegatingWebInvocationPrivilegeEvaluator(Collections.singletonList(entry)))
+				.withMessageContaining("webInvocationPrivilegeEvaluators cannot be null");
+	}
+
+	@Test
+	void constructorWhenRequestMatcherNullThenException() {
+		RequestMatcherEntry<List<WebInvocationPrivilegeEvaluator>> entry = new RequestMatcherEntry<>(null,
+				Collections.singletonList(mock(WebInvocationPrivilegeEvaluator.class)));
+		assertThatIllegalArgumentException().isThrownBy(
+				() -> new RequestMatcherDelegatingWebInvocationPrivilegeEvaluator(Collections.singletonList(entry)))
+				.withMessageContaining("requestMatcher cannot be null");
+	}
+
+}

+ 66 - 0
web/src/test/java/org/springframework/security/web/access/TestWebInvocationPrivilegeEvaluator.java

@@ -0,0 +1,66 @@
+/*
+ * Copyright 2002-2021 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.web.access;
+
+import org.springframework.security.core.Authentication;
+
+public final class TestWebInvocationPrivilegeEvaluator {
+
+	private static final AlwaysAllowWebInvocationPrivilegeEvaluator ALWAYS_ALLOW = new AlwaysAllowWebInvocationPrivilegeEvaluator();
+
+	private static final AlwaysDenyWebInvocationPrivilegeEvaluator ALWAYS_DENY = new AlwaysDenyWebInvocationPrivilegeEvaluator();
+
+	private TestWebInvocationPrivilegeEvaluator() {
+	}
+
+	public static WebInvocationPrivilegeEvaluator alwaysAllow() {
+		return ALWAYS_ALLOW;
+	}
+
+	public static WebInvocationPrivilegeEvaluator alwaysDeny() {
+		return ALWAYS_DENY;
+	}
+
+	private static class AlwaysAllowWebInvocationPrivilegeEvaluator implements WebInvocationPrivilegeEvaluator {
+
+		@Override
+		public boolean isAllowed(String uri, Authentication authentication) {
+			return true;
+		}
+
+		@Override
+		public boolean isAllowed(String contextPath, String uri, String method, Authentication authentication) {
+			return true;
+		}
+
+	}
+
+	private static class AlwaysDenyWebInvocationPrivilegeEvaluator implements WebInvocationPrivilegeEvaluator {
+
+		@Override
+		public boolean isAllowed(String uri, Authentication authentication) {
+			return false;
+		}
+
+		@Override
+		public boolean isAllowed(String contextPath, String uri, String method, Authentication authentication) {
+			return false;
+		}
+
+	}
+
+}

+ 8 - 1
web/src/test/java/org/springframework/security/web/access/intercept/AuthorizationFilterTests.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2020 the original author or authors.
+ * Copyright 2002-2021 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.
@@ -125,4 +125,11 @@ public class AuthorizationFilterTests {
 		verifyNoInteractions(mockFilterChain);
 	}
 
+	@Test
+	public void getAuthorizationManager() {
+		AuthorizationManager<HttpServletRequest> authorizationManager = mock(AuthorizationManager.class);
+		AuthorizationFilter authorizationFilter = new AuthorizationFilter(authorizationManager);
+		assertThat(authorizationFilter.getAuthorizationManager()).isSameAs(authorizationManager);
+	}
+
 }