diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/builders/WebSecurity.java b/config/src/main/java/org/springframework/security/config/annotation/web/builders/WebSecurity.java index 953d8f57758..8bd1c67d83a 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/builders/WebSecurity.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/builders/WebSecurity.java @@ -20,6 +20,7 @@ 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.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.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.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 @@ * @see WebSecurityConfiguration */ public final class WebSecurity extends AbstractConfiguredSecurityBuilder - implements SecurityBuilder, ApplicationContextAware { + implements SecurityBuilder, ApplicationContextAware, ServletContextAware { private final Log logger = LogFactory.getLog(getClass()); @@ -108,6 +115,8 @@ public final class WebSecurity extends AbstractConfiguredSecurityBuilder { }; + private ServletContext servletContext; + /** * Creates a new instance * @param objectPostProcessor the {@link ObjectPostProcessor} to use @@ -252,6 +261,8 @@ public WebInvocationPrivilegeEvaluator getPrivilegeEvaluator() { * {@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 @@ protected Filter performBuild() throws Exception { + ".addSecurityFilterChainBuilder directly"); int chainSize = this.ignoredRequests.size() + this.securityFilterChainBuilders.size(); List securityFilterChains = new ArrayList<>(chainSize); + List>> 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 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 @@ protected Filter performBuild() throws Exception { return result; } + private RequestMatcherEntry> getRequestMatcherPrivilegeEvaluatorsEntry( + SecurityFilterChain securityFilterChain) { + List 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 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 void setApplicationContext(ApplicationContext applicationContext) throws } } + @Override + public void setServletContext(ServletContext servletContext) { + this.servletContext = servletContext; + } + /** * An {@link IgnoredRequestConfigurer} that allows optionally configuring the * {@link MvcRequestMatcher#setMethod(HttpMethod)} diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfiguration.java index af1506aaed3..1bd9857bbff 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfiguration.java @@ -127,8 +127,8 @@ public Filter springSecurityFilterChain() throws Exception { } /** - * 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 diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfigurationTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfigurationTests.java index 7fa1908957f..9a4985cc55c 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfigurationTests.java +++ b/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.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.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 @@ * @author Rob Winch * @author Joe Grandja * @author Evgeniy Cheban + * @author Marcus Da Coregio */ @ExtendWith(SpringTestContextExtension.class) public class WebSecurityConfigurationTests { @@ -218,10 +220,10 @@ public void securityExpressionHandlerWhenPermissionEvaluatorBeanThenPermissionEv } @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 void loadConfigWhenSecurityFilterChainBeanThenDefaultWebInvocationPrivile 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 void loadConfigWhenMultipleAuthenticationManagersAndWebSecurityConfigurer 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 @@ protected AuthenticationManager authenticationManager() { } + @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(); + } + + } + } diff --git a/web/src/main/java/org/springframework/security/web/access/RequestMatcherDelegatingWebInvocationPrivilegeEvaluator.java b/web/src/main/java/org/springframework/security/web/access/RequestMatcherDelegatingWebInvocationPrivilegeEvaluator.java new file mode 100644 index 00000000000..75de7201ca6 --- /dev/null +++ b/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>> delegates; + + public RequestMatcherDelegatingWebInvocationPrivilegeEvaluator( + List>> requestMatcherPrivilegeEvaluatorsEntries) { + Assert.notNull(requestMatcherPrivilegeEvaluatorsEntries, "requestMatcherPrivilegeEvaluators cannot be null"); + for (RequestMatcherEntry> 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 Authentication + * object is allowed to invoke the supplied URI. + *

+ * 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 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 Authentication + * object is allowed to invoke the supplied URI. + *

+ * 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 Authentication 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 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 getDelegate(String contextPath, String uri, String method) { + FilterInvocation filterInvocation = new FilterInvocation(contextPath, uri, method); + for (RequestMatcherEntry> delegate : this.delegates) { + if (delegate.getRequestMatcher().matches(filterInvocation.getHttpRequest())) { + return delegate.getEntry(); + } + } + return Collections.emptyList(); + } + +} diff --git a/web/src/main/java/org/springframework/security/web/access/intercept/AuthorizationFilter.java b/web/src/main/java/org/springframework/security/web/access/intercept/AuthorizationFilter.java index 397bd2b8828..929e4cb8d03 100644 --- a/web/src/main/java/org/springframework/security/web/access/intercept/AuthorizationFilter.java +++ b/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 @@ private Authentication getAuthentication() { return authentication; } + /** + * Gets the {@link AuthorizationManager} used by this filter + * @return the {@link AuthorizationManager} + */ + public AuthorizationManager getAuthorizationManager() { + return this.authorizationManager; + } + } diff --git a/web/src/main/java/org/springframework/security/web/debug/DebugFilter.java b/web/src/main/java/org/springframework/security/web/debug/DebugFilter.java index 6e98b27bcc9..47d80e6da97 100644 --- a/web/src/main/java/org/springframework/security/web/debug/DebugFilter.java +++ b/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 void init(FilterConfig filterConfig) { public void destroy() { } + public FilterChainProxy getFilterChainProxy() { + return this.filterChainProxy; + } + static class DebugRequestWrapper extends HttpServletRequestWrapper { private static final Logger logger = new Logger(); diff --git a/web/src/test/java/org/springframework/security/web/access/RequestMatcherDelegatingWebInvocationPrivilegeEvaluatorTests.java b/web/src/test/java/org/springframework/security/web/access/RequestMatcherDelegatingWebInvocationPrivilegeEvaluatorTests.java new file mode 100644 index 00000000000..95feee1b568 --- /dev/null +++ b/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> 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> 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> 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> notMatchDelegate = new RequestMatcherEntry<>( + this.alwaysDeny, Collections.singletonList(TestWebInvocationPrivilegeEvaluator.alwaysAllow())); + RequestMatcherEntry> matchDelegate = new RequestMatcherEntry<>( + this.alwaysMatch, Collections.singletonList(TestWebInvocationPrivilegeEvaluator.alwaysAllow())); + RequestMatcherEntry> spyNotMatchDelegate = spy(notMatchDelegate); + RequestMatcherEntry> 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> 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> 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> 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> entry = new RequestMatcherEntry<>(this.alwaysMatch, + null); + assertThatIllegalArgumentException().isThrownBy( + () -> new RequestMatcherDelegatingWebInvocationPrivilegeEvaluator(Collections.singletonList(entry))) + .withMessageContaining("webInvocationPrivilegeEvaluators cannot be null"); + } + + @Test + void constructorWhenRequestMatcherNullThenException() { + RequestMatcherEntry> entry = new RequestMatcherEntry<>(null, + Collections.singletonList(mock(WebInvocationPrivilegeEvaluator.class))); + assertThatIllegalArgumentException().isThrownBy( + () -> new RequestMatcherDelegatingWebInvocationPrivilegeEvaluator(Collections.singletonList(entry))) + .withMessageContaining("requestMatcher cannot be null"); + } + +} diff --git a/web/src/test/java/org/springframework/security/web/access/TestWebInvocationPrivilegeEvaluator.java b/web/src/test/java/org/springframework/security/web/access/TestWebInvocationPrivilegeEvaluator.java new file mode 100644 index 00000000000..54ab666cd52 --- /dev/null +++ b/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; + } + + } + +} diff --git a/web/src/test/java/org/springframework/security/web/access/intercept/AuthorizationFilterTests.java b/web/src/test/java/org/springframework/security/web/access/intercept/AuthorizationFilterTests.java index 1b4e4e8392a..46680fac478 100644 --- a/web/src/test/java/org/springframework/security/web/access/intercept/AuthorizationFilterTests.java +++ b/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 void filterWhenAuthenticationNullThenAuthenticationCredentialsNotFoundExc verifyNoInteractions(mockFilterChain); } + @Test + public void getAuthorizationManager() { + AuthorizationManager authorizationManager = mock(AuthorizationManager.class); + AuthorizationFilter authorizationFilter = new AuthorizationFilter(authorizationManager); + assertThat(authorizationFilter.getAuthorizationManager()).isSameAs(authorizationManager); + } + }