From 5a6f74f0d0b0f13df89fd18f27fab1f54399d9c5 Mon Sep 17 00:00:00 2001 From: Evgeniy Cheban Date: Mon, 7 Sep 2020 04:31:06 +0300 Subject: [PATCH] Add AuthorizationManager Closes gh-8900 --- .../web/builders/FilterComparator.java | 2 + .../annotation/web/builders/HttpSecurity.java | 88 ++- .../AuthorizeHttpRequestsConfigurer.java | 289 ++++++++ .../AuthorizeHttpRequestsConfigurerTests.java | 628 ++++++++++++++++++ .../AuthenticatedAuthorizationManager.java | 65 ++ .../AuthorityAuthorizationManager.java | 135 ++++ .../authorization/AuthorizationManager.java | 57 ++ ...uthenticatedAuthorizationManagerTests.java | 77 +++ .../AuthorityAuthorizationManagerTests.java | 170 +++++ .../AuthorizationManagerTests.java | 65 ++ .../access/intercept/AuthorizationFilter.java | 69 ++ .../DelegatingAuthorizationManager.java | 124 ++++ .../RequestAuthorizationContext.java | 72 ++ .../intercept/AuthorizationFilterTests.java | 128 ++++ .../DelegatingAuthorizationManagerTests.java | 84 +++ 15 files changed, 2052 insertions(+), 1 deletion(-) create mode 100644 config/src/main/java/org/springframework/security/config/annotation/web/configurers/AuthorizeHttpRequestsConfigurer.java create mode 100644 config/src/test/java/org/springframework/security/config/annotation/web/configurers/AuthorizeHttpRequestsConfigurerTests.java create mode 100644 core/src/main/java/org/springframework/security/authorization/AuthenticatedAuthorizationManager.java create mode 100644 core/src/main/java/org/springframework/security/authorization/AuthorityAuthorizationManager.java create mode 100644 core/src/main/java/org/springframework/security/authorization/AuthorizationManager.java create mode 100644 core/src/test/java/org/springframework/security/authorization/AuthenticatedAuthorizationManagerTests.java create mode 100644 core/src/test/java/org/springframework/security/authorization/AuthorityAuthorizationManagerTests.java create mode 100644 core/src/test/java/org/springframework/security/authorization/AuthorizationManagerTests.java create mode 100644 web/src/main/java/org/springframework/security/web/access/intercept/AuthorizationFilter.java create mode 100644 web/src/main/java/org/springframework/security/web/access/intercept/DelegatingAuthorizationManager.java create mode 100644 web/src/main/java/org/springframework/security/web/access/intercept/RequestAuthorizationContext.java create mode 100644 web/src/test/java/org/springframework/security/web/access/intercept/AuthorizationFilterTests.java create mode 100644 web/src/test/java/org/springframework/security/web/access/intercept/DelegatingAuthorizationManagerTests.java diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/builders/FilterComparator.java b/config/src/main/java/org/springframework/security/config/annotation/web/builders/FilterComparator.java index 9c07581daab..c9ffaa80b84 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/builders/FilterComparator.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/builders/FilterComparator.java @@ -25,6 +25,7 @@ import org.springframework.security.web.access.ExceptionTranslationFilter; import org.springframework.security.web.access.channel.ChannelProcessingFilter; +import org.springframework.security.web.access.intercept.AuthorizationFilter; import org.springframework.security.web.access.intercept.FilterSecurityInterceptor; import org.springframework.security.web.authentication.AnonymousAuthenticationFilter; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; @@ -111,6 +112,7 @@ final class FilterComparator implements Comparator, Serializable { put(SessionManagementFilter.class, order.next()); put(ExceptionTranslationFilter.class, order.next()); put(FilterSecurityInterceptor.class, order.next()); + put(AuthorizationFilter.class, order.next()); put(SwitchUserFilter.class, order.next()); } diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java b/config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java index b37195a3567..ba6f047bd20 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2020 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. @@ -40,6 +40,8 @@ import org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.annotation.web.configurers.AnonymousConfigurer; +import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer; +import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer.AuthorizationManagerRequestMatcherRegistry; import org.springframework.security.config.annotation.web.configurers.ChannelSecurityConfigurer; import org.springframework.security.config.annotation.web.configurers.CorsConfigurer; import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer; @@ -1254,6 +1256,90 @@ public HttpSecurity authorizeRequests( return HttpSecurity.this; } + /** + * Allows restricting access based upon the {@link HttpServletRequest} using + * {@link RequestMatcher} implementations (i.e. via URL patterns). + * + *

Example Configurations

+ * + * The most basic example is to configure all URLs to require the role "ROLE_USER". + * The configuration below requires authentication to every URL and will grant access + * to both the user "admin" and "user". + * + *
+	 * @Configuration
+	 * @EnableWebSecurity
+	 * public class AuthorizeUrlsSecurityConfig extends WebSecurityConfigurerAdapter {
+	 *
+	 * 	@Override
+	 * 	protected void configure(HttpSecurity http) throws Exception {
+	 * 		http
+	 * 			.authorizeHttpRequests((authorizeHttpRequests) ->
+	 * 				authorizeHttpRequests
+	 * 					.antMatchers("/**").hasRole("USER")
+	 * 			)
+	 * 			.formLogin(withDefaults());
+	 * 	}
+	 * }
+	 * 
+ * + * We can also configure multiple URLs. The configuration below requires + * authentication to every URL and will grant access to URLs starting with /admin/ to + * only the "admin" user. All other URLs either user can access. + * + *
+	 * @Configuration
+	 * @EnableWebSecurity
+	 * public class AuthorizeUrlsSecurityConfig extends WebSecurityConfigurerAdapter {
+	 *
+	 * 	@Override
+	 * 	protected void configure(HttpSecurity http) throws Exception {
+	 * 		http
+	 * 			.authorizeHttpRequests((authorizeHttpRequests) ->
+	 * 				authorizeHttpRequests
+	 * 					.antMatchers("/admin/**").hasRole("ADMIN")
+	 * 					.antMatchers("/**").hasRole("USER")
+	 * 			)
+	 * 			.formLogin(withDefaults());
+	 * 	}
+	 * }
+	 * 
+ * + * Note that the matchers are considered in order. Therefore, the following is invalid + * because the first matcher matches every request and will never get to the second + * mapping: + * + *
+	 * @Configuration
+	 * @EnableWebSecurity
+	 * public class AuthorizeUrlsSecurityConfig extends WebSecurityConfigurerAdapter {
+	 *
+	 * 	@Override
+	 * 	protected void configure(HttpSecurity http) throws Exception {
+	 * 		http
+	 * 		 	.authorizeHttpRequests((authorizeHttpRequests) ->
+	 * 		 		authorizeHttpRequests
+	 * 			 		.antMatchers("/**").hasRole("USER")
+	 * 			 		.antMatchers("/admin/**").hasRole("ADMIN")
+	 * 		 	);
+	 * 	}
+	 * }
+	 * 
+ * @param authorizeHttpRequestsCustomizer the {@link Customizer} to provide more + * options for the {@link AuthorizationManagerRequestMatcherRegistry} + * @return the {@link HttpSecurity} for further customizations + * @throws Exception + * @see #requestMatcher(RequestMatcher) + */ + public HttpSecurity authorizeHttpRequests( + Customizer.AuthorizationManagerRequestMatcherRegistry> authorizeHttpRequestsCustomizer) + throws Exception { + ApplicationContext context = getContext(); + authorizeHttpRequestsCustomizer + .customize(getOrApply(new AuthorizeHttpRequestsConfigurer<>(context)).getRegistry()); + return HttpSecurity.this; + } + /** * Allows configuring the Request Cache. For example, a protected page (/protected) * may be requested prior to authentication. The application will redirect the user to diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/AuthorizeHttpRequestsConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/AuthorizeHttpRequestsConfigurer.java new file mode 100644 index 00000000000..42e8b140a20 --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/AuthorizeHttpRequestsConfigurer.java @@ -0,0 +1,289 @@ +/* + * Copyright 2002-2020 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.configurers; + +import java.util.List; + +import javax.servlet.http.HttpServletRequest; + +import org.springframework.context.ApplicationContext; +import org.springframework.http.HttpMethod; +import org.springframework.security.authorization.AuthenticatedAuthorizationManager; +import org.springframework.security.authorization.AuthorityAuthorizationManager; +import org.springframework.security.authorization.AuthorizationDecision; +import org.springframework.security.authorization.AuthorizationManager; +import org.springframework.security.config.annotation.ObjectPostProcessor; +import org.springframework.security.config.annotation.web.AbstractRequestMatcherRegistry; +import org.springframework.security.config.annotation.web.HttpSecurityBuilder; +import org.springframework.security.web.access.intercept.AuthorizationFilter; +import org.springframework.security.web.access.intercept.DelegatingAuthorizationManager; +import org.springframework.security.web.access.intercept.RequestAuthorizationContext; +import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher; +import org.springframework.security.web.util.matcher.RequestMatcher; +import org.springframework.util.Assert; + +/** + * Adds a URL based authorization using {@link AuthorizationManager}. + * + * @param the type of {@link HttpSecurityBuilder} that is being configured. + * @author Evgeniy Cheban + */ +public final class AuthorizeHttpRequestsConfigurer> + extends AbstractHttpConfigurer, H> { + + private final AuthorizationManagerRequestMatcherRegistry registry; + + /** + * Creates an instance. + * @param context the {@link ApplicationContext} to use + */ + public AuthorizeHttpRequestsConfigurer(ApplicationContext context) { + this.registry = new AuthorizationManagerRequestMatcherRegistry(context); + } + + /** + * The {@link AuthorizationManagerRequestMatcherRegistry} is what users will interact + * with after applying the {@link AuthorizeHttpRequestsConfigurer}. + * @return the {@link AuthorizationManagerRequestMatcherRegistry} for further + * customizations + */ + public AuthorizationManagerRequestMatcherRegistry getRegistry() { + return this.registry; + } + + @Override + public void configure(H http) { + AuthorizationManager authorizationManager = this.registry.createAuthorizationManager(); + AuthorizationFilter authorizationFilter = new AuthorizationFilter(authorizationManager); + http.addFilter(postProcess(authorizationFilter)); + } + + private AuthorizationManagerRequestMatcherRegistry addMapping(List matchers, + AuthorizationManager manager) { + for (RequestMatcher matcher : matchers) { + this.registry.addMapping(matcher, manager); + } + return this.registry; + } + + /** + * Registry for mapping a {@link RequestMatcher} to an {@link AuthorizationManager}. + * + * @author Evgeniy Cheban + */ + public final class AuthorizationManagerRequestMatcherRegistry + extends AbstractRequestMatcherRegistry { + + private final DelegatingAuthorizationManager.Builder managerBuilder = DelegatingAuthorizationManager.builder(); + + private List unmappedMatchers; + + private int mappingCount; + + private AuthorizationManagerRequestMatcherRegistry(ApplicationContext context) { + setApplicationContext(context); + } + + private void addMapping(RequestMatcher matcher, AuthorizationManager manager) { + this.unmappedMatchers = null; + this.managerBuilder.add(matcher, manager); + this.mappingCount++; + } + + private AuthorizationManager createAuthorizationManager() { + Assert.state(this.unmappedMatchers == null, + () -> "An incomplete mapping was found for " + this.unmappedMatchers + + ". Try completing it with something like requestUrls()..hasRole('USER')"); + Assert.state(this.mappingCount > 0, + "At least one mapping is required (for example, authorizeHttpRequests().anyRequest().authenticated())"); + return postProcess(this.managerBuilder.build()); + } + + @Override + public MvcMatchersAuthorizedUrl mvcMatchers(String... mvcPatterns) { + return mvcMatchers(null, mvcPatterns); + } + + @Override + public MvcMatchersAuthorizedUrl mvcMatchers(HttpMethod method, String... mvcPatterns) { + return new MvcMatchersAuthorizedUrl(createMvcMatchers(method, mvcPatterns)); + } + + @Override + protected AuthorizedUrl chainRequestMatchers(List requestMatchers) { + this.unmappedMatchers = requestMatchers; + return new AuthorizedUrl(requestMatchers); + } + + /** + * Adds an {@link ObjectPostProcessor} for this class. + * @param objectPostProcessor the {@link ObjectPostProcessor} to use + * @return the {@link AuthorizationManagerRequestMatcherRegistry} for further + * customizations + */ + public AuthorizationManagerRequestMatcherRegistry withObjectPostProcessor( + ObjectPostProcessor objectPostProcessor) { + addObjectPostProcessor(objectPostProcessor); + return this; + } + + /** + * Return the {@link HttpSecurityBuilder} when done using the + * {@link AuthorizeHttpRequestsConfigurer}. This is useful for method chaining. + * @return the {@link HttpSecurityBuilder} for further customizations + */ + public H and() { + return AuthorizeHttpRequestsConfigurer.this.and(); + } + + } + + /** + * An {@link AuthorizeHttpRequestsConfigurer.AuthorizedUrl} that allows optionally + * configuring the {@link MvcRequestMatcher#setServletPath(String)}. + * + * @author Evgeniy Cheban + */ + public final class MvcMatchersAuthorizedUrl extends AuthorizedUrl { + + private MvcMatchersAuthorizedUrl(List matchers) { + super(matchers); + } + + /** + * Configures servletPath to {@link MvcRequestMatcher}s. + * @param servletPath the servlet path + * @return the {@link MvcMatchersAuthorizedUrl} for further customizations + */ + @SuppressWarnings("unchecked") + public MvcMatchersAuthorizedUrl servletPath(String servletPath) { + for (MvcRequestMatcher matcher : (List) getMatchers()) { + matcher.setServletPath(servletPath); + } + return this; + } + + } + + /** + * An object that allows configuring the {@link AuthorizationManager} for + * {@link RequestMatcher}s. + * + * @author Evgeniy Cheban + */ + public class AuthorizedUrl { + + private final List matchers; + + /** + * Creates an instance. + * @param matchers the {@link RequestMatcher} instances to map + */ + AuthorizedUrl(List matchers) { + this.matchers = matchers; + } + + protected List getMatchers() { + return this.matchers; + } + + /** + * Specify that URLs are allowed by anyone. + * @return the {@link AuthorizationManagerRequestMatcherRegistry} for further + * customizations + */ + public AuthorizationManagerRequestMatcherRegistry permitAll() { + return access((a, o) -> new AuthorizationDecision(true)); + } + + /** + * Specify that URLs are not allowed by anyone. + * @return the {@link AuthorizationManagerRequestMatcherRegistry} for further + * customizations + */ + public AuthorizationManagerRequestMatcherRegistry denyAll() { + return access((a, o) -> new AuthorizationDecision(false)); + } + + /** + * Specifies a user requires a role. + * @param role the role that should be required which is prepended with ROLE_ + * automatically (i.e. USER, ADMIN, etc). It should not start with ROLE_ + * @return {@link AuthorizationManagerRequestMatcherRegistry} for further + * customizations + */ + public AuthorizationManagerRequestMatcherRegistry hasRole(String role) { + return access(AuthorityAuthorizationManager.hasRole(role)); + } + + /** + * Specifies that a user requires one of many roles. + * @param roles the roles that the user should have at least one of (i.e. ADMIN, + * USER, etc). Each role should not start with ROLE_ since it is automatically + * prepended already + * @return the {@link AuthorizationManagerRequestMatcherRegistry} for further + * customizations + */ + public AuthorizationManagerRequestMatcherRegistry hasAnyRole(String... roles) { + return access(AuthorityAuthorizationManager.hasAnyRole(roles)); + } + + /** + * Specifies a user requires an authority. + * @param authority the authority that should be required + * @return the {@link AuthorizationManagerRequestMatcherRegistry} for further + * customizations + */ + public AuthorizationManagerRequestMatcherRegistry hasAuthority(String authority) { + return access(AuthorityAuthorizationManager.hasAuthority(authority)); + } + + /** + * Specifies that a user requires one of many authorities. + * @param authorities the authorities that the user should have at least one of + * (i.e. ROLE_USER, ROLE_ADMIN, etc) + * @return the {@link AuthorizationManagerRequestMatcherRegistry} for further + * customizations + */ + public AuthorizationManagerRequestMatcherRegistry hasAnyAuthority(String... authorities) { + return access(AuthorityAuthorizationManager.hasAnyAuthority(authorities)); + } + + /** + * Specify that URLs are allowed by any authenticated user. + * @return the {@link AuthorizationManagerRequestMatcherRegistry} for further + * customizations + */ + public AuthorizationManagerRequestMatcherRegistry authenticated() { + return access(AuthenticatedAuthorizationManager.authenticated()); + } + + /** + * Allows specifying a custom {@link AuthorizationManager}. + * @param manager the {@link AuthorizationManager} to use + * @return the {@link AuthorizationManagerRequestMatcherRegistry} for further + * customizations + */ + public AuthorizationManagerRequestMatcherRegistry access( + AuthorizationManager manager) { + Assert.notNull(manager, "manager cannot be null"); + return AuthorizeHttpRequestsConfigurer.this.addMapping(this.matchers, manager); + } + + } + +} 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 new file mode 100644 index 00000000000..0f392ccfedb --- /dev/null +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/AuthorizeHttpRequestsConfigurerTests.java @@ -0,0 +1,628 @@ +/* + * Copyright 2002-2020 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.configurers; + +import org.junit.Rule; +import org.junit.Test; + +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.security.authorization.AuthorizationManager; +import org.springframework.security.config.annotation.ObjectPostProcessor; +import org.springframework.security.config.annotation.web.AbstractRequestMatcherRegistry; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.test.SpringTestRule; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.access.intercept.AuthorizationFilter; +import org.springframework.security.web.access.intercept.DelegatingAuthorizationManager; +import org.springframework.security.web.access.intercept.RequestAuthorizationContext; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; + +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.springframework.security.config.Customizer.withDefaults; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * Tests for {@link AuthorizeHttpRequestsConfigurer}. + * + * @author Evgeniy Cheban + */ +public class AuthorizeHttpRequestsConfigurerTests { + + @Rule + public final SpringTestRule spring = new SpringTestRule(); + + @Autowired + MockMvc mvc; + + @Test + public void configureWhenAuthorizedHttpRequestsAndNoRequestsThenException() { + assertThatExceptionOfType(BeanCreationException.class) + .isThrownBy(() -> this.spring.register(NoRequestsConfig.class).autowire()).withMessageContaining( + "At least one mapping is required (for example, authorizeHttpRequests().anyRequest().authenticated())"); + } + + @Test + public void configureWhenAnyRequestIncompleteMappingThenException() { + assertThatExceptionOfType(BeanCreationException.class) + .isThrownBy(() -> this.spring.register(IncompleteMappingConfig.class).autowire()) + .withMessageContaining("An incomplete mapping was found for "); + } + + @Test + public void configureWhenMvcMatcherAfterAnyRequestThenException() { + assertThatExceptionOfType(BeanCreationException.class) + .isThrownBy(() -> this.spring.register(AfterAnyRequestConfig.class).autowire()) + .withMessageContaining("Can't configure mvcMatchers after anyRequest"); + } + + @Test + public void configureMvcMatcherAccessAuthorizationManagerWhenNotNullThenVerifyUse() throws Exception { + CustomAuthorizationManagerConfig.authorizationManager = mock(AuthorizationManager.class); + this.spring.register(CustomAuthorizationManagerConfig.class, BasicController.class).autowire(); + this.mvc.perform(get("/")).andExpect(status().isOk()); + verify(CustomAuthorizationManagerConfig.authorizationManager).check(any(), any()); + } + + @Test + public void configureMvcMatcherAccessAuthorizationManagerWhenNullThenException() { + CustomAuthorizationManagerConfig.authorizationManager = null; + assertThatExceptionOfType(BeanCreationException.class) + .isThrownBy(() -> this.spring.register(CustomAuthorizationManagerConfig.class).autowire()) + .withMessageContaining("manager cannot be null"); + } + + @Test + public void configureWhenObjectPostProcessorRegisteredThenInvokedOnAuthorizationManagerAndAuthorizationFilter() { + this.spring.register(ObjectPostProcessorConfig.class).autowire(); + verify(ObjectPostProcessorConfig.objectPostProcessor).postProcess(any(DelegatingAuthorizationManager.class)); + verify(ObjectPostProcessorConfig.objectPostProcessor).postProcess(any(AuthorizationFilter.class)); + } + + @Test + public void getWhenHasAnyAuthorityRoleUserConfiguredAndAuthorityIsRoleUserThenRespondsWithOk() throws Exception { + this.spring.register(RoleUserAnyAuthorityConfig.class, BasicController.class).autowire(); + // @formatter:off + MockHttpServletRequestBuilder requestWithUser = get("/") + .with(user("user") + .authorities(new SimpleGrantedAuthority("ROLE_USER"))); + // @formatter:on + this.mvc.perform(requestWithUser).andExpect(status().isOk()); + } + + @Test + public void getWhenHasAnyAuthorityRoleUserConfiguredAndAuthorityIsRoleAdminThenRespondsWithForbidden() + throws Exception { + this.spring.register(RoleUserAnyAuthorityConfig.class, BasicController.class).autowire(); + // @formatter:off + MockHttpServletRequestBuilder requestWithAdmin = get("/") + .with(user("user") + .authorities(new SimpleGrantedAuthority("ROLE_ADMIN"))); + // @formatter:on + this.mvc.perform(requestWithAdmin).andExpect(status().isForbidden()); + } + + @Test + public void getWhenHasAnyAuthorityRoleUserConfiguredAndNoAuthorityThenRespondsWithUnauthorized() throws Exception { + this.spring.register(RoleUserAnyAuthorityConfig.class, BasicController.class).autowire(); + this.mvc.perform(get("/")).andExpect(status().isUnauthorized()); + } + + @Test + public void getWhenHasAuthorityRoleUserConfiguredAndAuthorityIsRoleUserThenRespondsWithOk() throws Exception { + this.spring.register(RoleUserAuthorityConfig.class, BasicController.class).autowire(); + // @formatter:off + MockHttpServletRequestBuilder requestWithUser = get("/") + .with(user("user") + .authorities(new SimpleGrantedAuthority("ROLE_USER"))); + // @formatter:on + this.mvc.perform(requestWithUser).andExpect(status().isOk()); + } + + @Test + public void getWhenHasAuthorityRoleUserConfiguredAndAuthorityIsRoleAdminThenRespondsWithForbidden() + throws Exception { + this.spring.register(RoleUserAuthorityConfig.class, BasicController.class).autowire(); + // @formatter:off + MockHttpServletRequestBuilder requestWithAdmin = get("/") + .with(user("user") + .authorities(new SimpleGrantedAuthority("ROLE_ADMIN"))); + // @formatter:on + this.mvc.perform(requestWithAdmin).andExpect(status().isForbidden()); + } + + @Test + public void getWhenHasAuthorityRoleUserConfiguredAndNoAuthorityThenRespondsWithUnauthorized() throws Exception { + this.spring.register(RoleUserAuthorityConfig.class, BasicController.class).autowire(); + this.mvc.perform(get("/")).andExpect(status().isUnauthorized()); + } + + @Test + public void getWhenAuthorityRoleUserOrAdminRequiredAndAuthorityIsRoleUserThenRespondsWithOk() throws Exception { + this.spring.register(RoleUserOrRoleAdminAuthorityConfig.class, BasicController.class).autowire(); + // @formatter:off + MockHttpServletRequestBuilder requestWithUser = get("/") + .with(user("user") + .authorities(new SimpleGrantedAuthority("ROLE_USER"))); + // @formatter:on + this.mvc.perform(requestWithUser).andExpect(status().isOk()); + } + + @Test + public void getWhenAuthorityRoleUserOrAdminRequiredAndAuthorityIsRoleAdminThenRespondsWithOk() throws Exception { + this.spring.register(RoleUserOrRoleAdminAuthorityConfig.class, BasicController.class).autowire(); + // @formatter:off + MockHttpServletRequestBuilder requestWithAdmin = get("/") + .with(user("user") + .authorities(new SimpleGrantedAuthority("ROLE_ADMIN"))); + // @formatter:on + this.mvc.perform(requestWithAdmin).andExpect(status().isOk()); + } + + @Test + public void getWhenAuthorityRoleUserOrAdminRequiredAndAuthorityIsRoleOtherThenRespondsWithForbidden() + throws Exception { + this.spring.register(RoleUserOrRoleAdminAuthorityConfig.class, BasicController.class).autowire(); + // @formatter:off + MockHttpServletRequestBuilder requestWithOther = get("/") + .with(user("user") + .authorities(new SimpleGrantedAuthority("ROLE_OTHER"))); + // @formatter:on + this.mvc.perform(requestWithOther).andExpect(status().isForbidden()); + } + + @Test + public void getWhenAuthorityRoleUserOrAdminAuthRequiredAndNoUserThenRespondsWithUnauthorized() throws Exception { + this.spring.register(RoleUserOrRoleAdminAuthorityConfig.class, BasicController.class).autowire(); + this.mvc.perform(get("/")).andExpect(status().isUnauthorized()); + } + + @Test + public void getWhenHasRoleUserConfiguredAndRoleIsUserThenRespondsWithOk() throws Exception { + this.spring.register(RoleUserConfig.class, BasicController.class).autowire(); + // @formatter:off + MockHttpServletRequestBuilder requestWithUser = get("/") + .with(user("user") + .roles("USER")); + // @formatter:on + this.mvc.perform(requestWithUser).andExpect(status().isOk()); + } + + @Test + public void getWhenHasRoleUserConfiguredAndRoleIsAdminThenRespondsWithForbidden() throws Exception { + this.spring.register(RoleUserConfig.class, BasicController.class).autowire(); + // @formatter:off + MockHttpServletRequestBuilder requestWithAdmin = get("/") + .with(user("user") + .roles("ADMIN")); + // @formatter:on + this.mvc.perform(requestWithAdmin).andExpect(status().isForbidden()); + } + + @Test + public void getWhenRoleUserOrAdminConfiguredAndRoleIsUserThenRespondsWithOk() throws Exception { + this.spring.register(RoleUserOrAdminConfig.class, BasicController.class).autowire(); + // @formatter:off + MockHttpServletRequestBuilder requestWithUser = get("/") + .with(user("user") + .roles("USER")); + // @formatter:on + this.mvc.perform(requestWithUser).andExpect(status().isOk()); + } + + @Test + public void getWhenRoleUserOrAdminConfiguredAndRoleIsAdminThenRespondsWithOk() throws Exception { + this.spring.register(RoleUserOrAdminConfig.class, BasicController.class).autowire(); + // @formatter:off + MockHttpServletRequestBuilder requestWithAdmin = get("/") + .with(user("user") + .roles("ADMIN")); + // @formatter:on + this.mvc.perform(requestWithAdmin).andExpect(status().isOk()); + } + + @Test + public void getWhenRoleUserOrAdminConfiguredAndRoleIsOtherThenRespondsWithForbidden() throws Exception { + this.spring.register(RoleUserOrAdminConfig.class, BasicController.class).autowire(); + // @formatter:off + MockHttpServletRequestBuilder requestWithRoleOther = get("/") + .with(user("user") + .roles("OTHER")); + // @formatter:on + this.mvc.perform(requestWithRoleOther).andExpect(status().isForbidden()); + } + + @Test + public void getWhenDenyAllConfiguredAndNoUserThenRespondsWithUnauthorized() throws Exception { + this.spring.register(DenyAllConfig.class, BasicController.class).autowire(); + this.mvc.perform(get("/")).andExpect(status().isUnauthorized()); + } + + @Test + public void getWhenDenyAllConfiguredAndUserLoggedInThenRespondsWithForbidden() throws Exception { + this.spring.register(DenyAllConfig.class, BasicController.class).autowire(); + // @formatter:off + MockHttpServletRequestBuilder requestWithUser = get("/") + .with(user("user") + .roles("USER")); + // @formatter:on + this.mvc.perform(requestWithUser).andExpect(status().isForbidden()); + } + + @Test + public void getWhenPermitAllConfiguredAndNoUserThenRespondsWithOk() throws Exception { + this.spring.register(PermitAllConfig.class, BasicController.class).autowire(); + this.mvc.perform(get("/")).andExpect(status().isOk()); + } + + @Test + public void getWhenPermitAllConfiguredAndUserLoggedInThenRespondsWithOk() throws Exception { + this.spring.register(PermitAllConfig.class, BasicController.class).autowire(); + // @formatter:off + MockHttpServletRequestBuilder requestWithUser = get("/") + .with(user("user") + .roles("USER")); + // @formatter:on + this.mvc.perform(requestWithUser).andExpect(status().isOk()); + } + + @Test + public void authorizeHttpRequestsWhenInvokedTwiceThenUsesOriginalConfiguration() throws Exception { + this.spring.register(InvokeTwiceDoesNotResetConfig.class, BasicController.class).autowire(); + this.mvc.perform(post("/").with(csrf())).andExpect(status().isUnauthorized()); + } + + @Test + public void getWhenServletPathRoleAdminConfiguredAndRoleIsUserThenRespondsWithForbidden() throws Exception { + this.spring.register(ServletPathConfig.class, BasicController.class).autowire(); + // @formatter:off + MockHttpServletRequestBuilder requestWithUser = get("/spring/") + .servletPath("/spring") + .with(user("user") + .roles("USER")); + // @formatter:on + this.mvc.perform(requestWithUser).andExpect(status().isForbidden()); + } + + @Test + public void getWhenServletPathRoleAdminConfiguredAndRoleIsAdminThenRespondsWithOk() throws Exception { + this.spring.register(ServletPathConfig.class, BasicController.class).autowire(); + // @formatter:off + MockHttpServletRequestBuilder requestWithAdmin = get("/spring/") + .servletPath("/spring") + .with(user("user") + .roles("ADMIN")); + // @formatter:on + this.mvc.perform(requestWithAdmin).andExpect(status().isOk()); + } + + @Test + public void getWhenAnyRequestAuthenticatedConfiguredAndNoUserThenRespondsWithUnauthorized() throws Exception { + this.spring.register(AuthenticatedConfig.class, BasicController.class).autowire(); + this.mvc.perform(get("/")).andExpect(status().isUnauthorized()); + } + + @Test + public void getWhenAnyRequestAuthenticatedConfiguredAndUserLoggedInThenRespondsWithOk() throws Exception { + this.spring.register(AuthenticatedConfig.class, BasicController.class).autowire(); + // @formatter:off + MockHttpServletRequestBuilder requestWithUser = get("/") + .with(user("user") + .roles("USER")); + // @formatter:on + this.mvc.perform(requestWithUser).andExpect(status().isOk()); + } + + @EnableWebSecurity + static class NoRequestsConfig { + + @Bean + SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + // @formatter:off + return http + .authorizeHttpRequests(withDefaults()) + .build(); + // @formatter:on + } + + } + + @EnableWebSecurity + static class IncompleteMappingConfig { + + @Bean + SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + // @formatter:off + return http + .authorizeHttpRequests(AbstractRequestMatcherRegistry::anyRequest) + .build(); + // @formatter:on + } + + } + + @EnableWebSecurity + static class AfterAnyRequestConfig { + + @Bean + SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + // @formatter:off + return http + .authorizeHttpRequests((requests) -> requests + .anyRequest().authenticated() + .mvcMatchers("/path").hasRole("USER") + ) + .build(); + // @formatter:on + } + + } + + @EnableWebSecurity + static class CustomAuthorizationManagerConfig { + + static AuthorizationManager authorizationManager; + + @Bean + SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + // @formatter:off + return http + .authorizeHttpRequests((requests) -> requests + .anyRequest().access(authorizationManager) + ) + .build(); + // @formatter:on + } + + } + + @EnableWebSecurity + static class ObjectPostProcessorConfig { + + static ObjectPostProcessor objectPostProcessor = spy(ReflectingObjectPostProcessor.class); + + @Bean + SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + // @formatter:off + return http + .authorizeHttpRequests((requests) -> requests + .anyRequest().authenticated() + ) + .build(); + // @formatter:on + } + + @Bean + static ObjectPostProcessor objectPostProcessor() { + return objectPostProcessor; + } + + } + + static class ReflectingObjectPostProcessor implements ObjectPostProcessor { + + @Override + public O postProcess(O object) { + return object; + } + + } + + @EnableWebSecurity + static class RoleUserAnyAuthorityConfig { + + @Bean + SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + // @formatter:off + return http + .httpBasic() + .and() + .authorizeHttpRequests((requests) -> requests + .anyRequest().hasAnyAuthority("ROLE_USER") + ) + .build(); + // @formatter:on + } + + } + + @EnableWebSecurity + static class RoleUserAuthorityConfig { + + @Bean + SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + // @formatter:off + return http + .httpBasic() + .and() + .authorizeHttpRequests((requests) -> requests + .anyRequest().hasAuthority("ROLE_USER") + ) + .build(); + // @formatter:on + } + + } + + @EnableWebSecurity + static class RoleUserOrRoleAdminAuthorityConfig { + + @Bean + SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + // @formatter:off + return http + .httpBasic() + .and() + .authorizeHttpRequests((requests) -> requests + .anyRequest().hasAnyAuthority("ROLE_USER", "ROLE_ADMIN") + ) + .build(); + // @formatter:on + } + + } + + @EnableWebSecurity + static class RoleUserConfig { + + @Bean + SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + // @formatter:off + return http + .authorizeHttpRequests((requests) -> requests + .anyRequest().hasRole("USER") + ) + .build(); + // @formatter:on + } + + } + + @EnableWebSecurity + static class RoleUserOrAdminConfig { + + @Bean + SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + // @formatter:off + return http + .authorizeHttpRequests((requests) -> requests + .anyRequest().hasAnyRole("USER", "ADMIN") + ) + .build(); + // @formatter:on + } + + } + + @EnableWebSecurity + static class DenyAllConfig { + + @Bean + SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + // @formatter:off + return http + .httpBasic() + .and() + .authorizeHttpRequests((requests) -> requests + .anyRequest().denyAll() + ) + .build(); + // @formatter:on + } + + } + + @EnableWebSecurity + static class PermitAllConfig { + + @Bean + SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + // @formatter:off + return http + .authorizeHttpRequests((requests) -> requests + .anyRequest().permitAll() + ) + .build(); + // @formatter:on + } + + } + + @EnableWebSecurity + static class InvokeTwiceDoesNotResetConfig { + + @Bean + SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + // @formatter:off + return http + .httpBasic() + .and() + .authorizeHttpRequests((requests) -> requests + .anyRequest().authenticated() + ) + .authorizeHttpRequests(withDefaults()) + .build(); + // @formatter:on + } + + } + + @EnableWebMvc + @EnableWebSecurity + static class ServletPathConfig { + + @Bean + SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + // @formatter:off + return http + .authorizeHttpRequests((requests) -> requests + .mvcMatchers("/").servletPath("/spring").hasRole("ADMIN") + ) + .build(); + // @formatter:on + } + + } + + @EnableWebSecurity + static class AuthenticatedConfig { + + @Bean + SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + // @formatter:off + return http + .httpBasic() + .and() + .authorizeHttpRequests((requests) -> requests + .anyRequest().authenticated() + ) + .build(); + // @formatter:on + } + + } + + @RestController + static class BasicController { + + @GetMapping("/") + void rootGet() { + } + + @PostMapping("/") + void rootPost() { + } + + } + +} diff --git a/core/src/main/java/org/springframework/security/authorization/AuthenticatedAuthorizationManager.java b/core/src/main/java/org/springframework/security/authorization/AuthenticatedAuthorizationManager.java new file mode 100644 index 00000000000..9ff5e705b29 --- /dev/null +++ b/core/src/main/java/org/springframework/security/authorization/AuthenticatedAuthorizationManager.java @@ -0,0 +1,65 @@ +/* + * Copyright 2002-2020 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.authorization; + +import java.util.function.Supplier; + +import org.springframework.security.authentication.AuthenticationTrustResolver; +import org.springframework.security.authentication.AuthenticationTrustResolverImpl; +import org.springframework.security.core.Authentication; + +/** + * An {@link AuthorizationManager} that determines if the current user is authenticated. + * + * @param the type of object authorization is being performed against. This does not. + * @author Evgeniy Cheban + */ +public final class AuthenticatedAuthorizationManager implements AuthorizationManager { + + private final AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl(); + + /** + * Creates an instance of {@link AuthenticatedAuthorizationManager}. + * @param the type of object being authorized + * @return the new instance + */ + public static AuthenticatedAuthorizationManager authenticated() { + return new AuthenticatedAuthorizationManager<>(); + } + + /** + * Determines if the current user is authorized by evaluating if the + * {@link Authentication} is not anonymous and authenticated. + * @param authentication the {@link Supplier} of the {@link Authentication} to check + * @param object the {@link T} object to check + * @return an {@link AuthorizationDecision} + */ + @Override + public AuthorizationDecision check(Supplier authentication, T object) { + boolean granted = isGranted(authentication.get()); + return new AuthorizationDecision(granted); + } + + private boolean isGranted(Authentication authentication) { + return authentication != null && isNotAnonymous(authentication) && authentication.isAuthenticated(); + } + + private boolean isNotAnonymous(Authentication authentication) { + return !this.trustResolver.isAnonymous(authentication); + } + +} diff --git a/core/src/main/java/org/springframework/security/authorization/AuthorityAuthorizationManager.java b/core/src/main/java/org/springframework/security/authorization/AuthorityAuthorizationManager.java new file mode 100644 index 00000000000..1b3692e3293 --- /dev/null +++ b/core/src/main/java/org/springframework/security/authorization/AuthorityAuthorizationManager.java @@ -0,0 +1,135 @@ +/* + * Copyright 2002-2020 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.authorization; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; +import java.util.function.Supplier; + +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.util.Assert; + +/** + * An {@link AuthorizationManager} that determines if the current user is authorized by + * evaluating if the {@link Authentication} contains a specified authority. + * + * @param the type of object being authorized. + * @author Evgeniy Cheban + */ +public final class AuthorityAuthorizationManager implements AuthorizationManager { + + private static final String ROLE_PREFIX = "ROLE_"; + + private final Set authorities; + + private AuthorityAuthorizationManager(String... authorities) { + this.authorities = new HashSet<>(Arrays.asList(authorities)); + } + + /** + * Creates an instance of {@link AuthorityAuthorizationManager} with the provided + * authority. + * @param role the authority to check for prefixed with "ROLE_" + * @param the type of object being authorized + * @return the new instance + */ + public static AuthorityAuthorizationManager hasRole(String role) { + Assert.notNull(role, "role cannot be null"); + return hasAuthority(ROLE_PREFIX + role); + } + + /** + * Creates an instance of {@link AuthorityAuthorizationManager} with the provided + * authority. + * @param authority the authority to check for + * @param the type of object being authorized + * @return the new instance + */ + public static AuthorityAuthorizationManager hasAuthority(String authority) { + Assert.notNull(authority, "authority cannot be null"); + return new AuthorityAuthorizationManager<>(authority); + } + + /** + * Creates an instance of {@link AuthorityAuthorizationManager} with the provided + * authorities. + * @param roles the authorities to check for prefixed with "ROLE_" + * @param the type of object being authorized + * @return the new instance + */ + public static AuthorityAuthorizationManager hasAnyRole(String... roles) { + Assert.notEmpty(roles, "roles cannot be empty"); + Assert.noNullElements(roles, "roles cannot contain null values"); + return hasAnyAuthority(toNamedRolesArray(roles)); + } + + /** + * Creates an instance of {@link AuthorityAuthorizationManager} with the provided + * authorities. + * @param authorities the authorities to check for + * @param the type of object being authorized + * @return the new instance + */ + public static AuthorityAuthorizationManager hasAnyAuthority(String... authorities) { + Assert.notEmpty(authorities, "authorities cannot be empty"); + Assert.noNullElements(authorities, "authorities cannot contain null values"); + return new AuthorityAuthorizationManager<>(authorities); + } + + private static String[] toNamedRolesArray(String... roles) { + String[] result = new String[roles.length]; + for (int i = 0; i < roles.length; i++) { + result[i] = ROLE_PREFIX + roles[i]; + } + return result; + } + + /** + * Determines if the current user is authorized by evaluating if the + * {@link Authentication} contains a specified authority. + * @param authentication the {@link Supplier} of the {@link Authentication} to check + * @param object the {@link T} object to check + * @return an {@link AuthorizationDecision} + */ + @Override + public AuthorizationDecision check(Supplier authentication, T object) { + boolean granted = isGranted(authentication.get()); + return new AuthorizationDecision(granted); + } + + private boolean isGranted(Authentication authentication) { + return authentication != null && authentication.isAuthenticated() && isAuthorized(authentication); + } + + private boolean isAuthorized(Authentication authentication) { + for (GrantedAuthority grantedAuthority : authentication.getAuthorities()) { + String authority = grantedAuthority.getAuthority(); + if (this.authorities.contains(authority)) { + return true; + } + } + return false; + } + + @Override + public String toString() { + return "AuthorityAuthorizationManager[authorities=" + this.authorities + "]"; + } + +} diff --git a/core/src/main/java/org/springframework/security/authorization/AuthorizationManager.java b/core/src/main/java/org/springframework/security/authorization/AuthorizationManager.java new file mode 100644 index 00000000000..a2a502f0e66 --- /dev/null +++ b/core/src/main/java/org/springframework/security/authorization/AuthorizationManager.java @@ -0,0 +1,57 @@ +/* + * Copyright 2002-2020 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.authorization; + +import java.util.function.Supplier; + +import org.springframework.lang.Nullable; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.core.Authentication; + +/** + * An Authorization manager which can determine if an {@link Authentication} has access to + * a specific object. + * + * @param the type of object that the authorization check is being done one. + * @author Evgeniy Cheban + */ +@FunctionalInterface +public interface AuthorizationManager { + + /** + * Determines if access should be granted for a specific authentication and object. + * @param authentication the {@link Supplier} of the {@link Authentication} to check + * @param object the {@link T} object to check + * @throws AccessDeniedException if access is not granted + */ + default void verify(Supplier authentication, T object) { + AuthorizationDecision decision = check(authentication, object); + if (decision != null && !decision.isGranted()) { + throw new AccessDeniedException("Access Denied"); + } + } + + /** + * Determines if access is granted for a specific authentication and object. + * @param authentication the {@link Supplier} of the {@link Authentication} to check + * @param object the {@link T} object to check + * @return an {@link AuthorizationDecision} or null if no decision could be made + */ + @Nullable + AuthorizationDecision check(Supplier authentication, T object); + +} diff --git a/core/src/test/java/org/springframework/security/authorization/AuthenticatedAuthorizationManagerTests.java b/core/src/test/java/org/springframework/security/authorization/AuthenticatedAuthorizationManagerTests.java new file mode 100644 index 00000000000..375f52fb650 --- /dev/null +++ b/core/src/test/java/org/springframework/security/authorization/AuthenticatedAuthorizationManagerTests.java @@ -0,0 +1,77 @@ +/* + * Copyright 2002-2020 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.authorization; + +import java.util.function.Supplier; + +import org.junit.Test; + +import org.springframework.security.authentication.AnonymousAuthenticationToken; +import org.springframework.security.authentication.TestingAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.authority.AuthorityUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link AuthenticatedAuthorizationManager}. + * + * @author Evgeniy Cheban + */ +public class AuthenticatedAuthorizationManagerTests { + + @Test + public void authenticatedWhenUserNotAnonymousAndAuthenticatedThenGrantedDecision() { + AuthenticatedAuthorizationManager manager = AuthenticatedAuthorizationManager.authenticated(); + Supplier authentication = () -> new TestingAuthenticationToken("user", "password", "ROLE_ADMIN", + "ROLE_USER"); + Object object = new Object(); + + assertThat(manager.check(authentication, object).isGranted()).isTrue(); + } + + @Test + public void authenticatedWhenUserNullThenDeniedDecision() { + AuthenticatedAuthorizationManager manager = AuthenticatedAuthorizationManager.authenticated(); + Supplier authentication = () -> null; + Object object = new Object(); + + assertThat(manager.check(authentication, object).isGranted()).isFalse(); + } + + @Test + public void authenticatedWhenUserAnonymousThenDeniedDecision() { + AuthenticatedAuthorizationManager manager = AuthenticatedAuthorizationManager.authenticated(); + Supplier authentication = () -> new AnonymousAuthenticationToken("key", "principal", + AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS")); + Object object = new Object(); + + assertThat(manager.check(authentication, object).isGranted()).isFalse(); + } + + @Test + public void authenticatedWhenUserNotAuthenticatedThenDeniedDecision() { + AuthenticatedAuthorizationManager manager = AuthenticatedAuthorizationManager.authenticated(); + TestingAuthenticationToken authentication = new TestingAuthenticationToken("user", "password", "ROLE_ADMIN", + "ROLE_USER"); + authentication.setAuthenticated(false); + Object object = new Object(); + + assertThat(manager.check(() -> authentication, object).isGranted()).isFalse(); + } + +} diff --git a/core/src/test/java/org/springframework/security/authorization/AuthorityAuthorizationManagerTests.java b/core/src/test/java/org/springframework/security/authorization/AuthorityAuthorizationManagerTests.java new file mode 100644 index 00000000000..ab0c41563c7 --- /dev/null +++ b/core/src/test/java/org/springframework/security/authorization/AuthorityAuthorizationManagerTests.java @@ -0,0 +1,170 @@ +/* + * Copyright 2002-2020 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.authorization; + +import java.util.function.Supplier; + +import org.junit.Test; + +import org.springframework.security.authentication.TestingAuthenticationToken; +import org.springframework.security.core.Authentication; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link AuthorityAuthorizationManager}. + * + * @author Evgeniy Cheban + */ +public class AuthorityAuthorizationManagerTests { + + @Test + public void hasRoleWhenNullThenException() { + assertThatIllegalArgumentException().isThrownBy(() -> AuthorityAuthorizationManager.hasRole(null)) + .withMessage("role cannot be null"); + } + + @Test + public void hasAuthorityWhenNullThenException() { + assertThatIllegalArgumentException().isThrownBy(() -> AuthorityAuthorizationManager.hasAuthority(null)) + .withMessage("authority cannot be null"); + } + + @Test + public void hasAnyRoleWhenNullThenException() { + assertThatIllegalArgumentException().isThrownBy(() -> AuthorityAuthorizationManager.hasAnyRole(null)) + .withMessage("roles cannot be empty"); + } + + @Test + public void hasAnyRoleWhenEmptyThenException() { + assertThatIllegalArgumentException().isThrownBy(() -> AuthorityAuthorizationManager.hasAnyRole(new String[] {})) + .withMessage("roles cannot be empty"); + } + + @Test + public void hasAnyRoleWhenContainNullThenException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> AuthorityAuthorizationManager.hasAnyRole("ADMIN", null, "USER")) + .withMessage("roles cannot contain null values"); + } + + @Test + public void hasAnyAuthorityWhenNullThenException() { + assertThatIllegalArgumentException().isThrownBy(() -> AuthorityAuthorizationManager.hasAnyAuthority(null)) + .withMessage("authorities cannot be empty"); + } + + @Test + public void hasAnyAuthorityWhenEmptyThenException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> AuthorityAuthorizationManager.hasAnyAuthority(new String[] {})) + .withMessage("authorities cannot be empty"); + } + + @Test + public void hasAnyAuthorityWhenContainNullThenException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> AuthorityAuthorizationManager.hasAnyAuthority("ADMIN", null, "USER")) + .withMessage("authorities cannot contain null values"); + } + + @Test + public void hasRoleWhenUserHasRoleThenGrantedDecision() { + AuthorityAuthorizationManager manager = AuthorityAuthorizationManager.hasRole("ADMIN"); + + Supplier authentication = () -> new TestingAuthenticationToken("user", "password", "ROLE_ADMIN", + "ROLE_USER"); + Object object = new Object(); + + assertThat(manager.check(authentication, object).isGranted()).isTrue(); + } + + @Test + public void hasRoleWhenUserHasNotRoleThenDeniedDecision() { + AuthorityAuthorizationManager manager = AuthorityAuthorizationManager.hasRole("ADMIN"); + + Supplier authentication = () -> new TestingAuthenticationToken("user", "password", "ROLE_USER"); + Object object = new Object(); + + assertThat(manager.check(authentication, object).isGranted()).isFalse(); + } + + @Test + public void hasAuthorityWhenUserHasAuthorityThenGrantedDecision() { + AuthorityAuthorizationManager manager = AuthorityAuthorizationManager.hasAuthority("ADMIN"); + + Supplier authentication = () -> new TestingAuthenticationToken("user", "password", "ADMIN", + "USER"); + Object object = new Object(); + + assertThat(manager.check(authentication, object).isGranted()).isTrue(); + } + + @Test + public void hasAuthorityWhenUserHasNotAuthorityThenDeniedDecision() { + AuthorityAuthorizationManager manager = AuthorityAuthorizationManager.hasAuthority("ADMIN"); + + Supplier authentication = () -> new TestingAuthenticationToken("user", "password", "USER"); + Object object = new Object(); + + assertThat(manager.check(authentication, object).isGranted()).isFalse(); + } + + @Test + public void hasAnyRoleWhenUserHasAnyRoleThenGrantedDecision() { + AuthorityAuthorizationManager manager = AuthorityAuthorizationManager.hasAnyRole("ADMIN", "USER"); + + Supplier authentication = () -> new TestingAuthenticationToken("user", "password", "ROLE_USER"); + Object object = new Object(); + + assertThat(manager.check(authentication, object).isGranted()).isTrue(); + } + + @Test + public void hasAnyRoleWhenUserHasNotAnyRoleThenDeniedDecision() { + AuthorityAuthorizationManager manager = AuthorityAuthorizationManager.hasAnyRole("ADMIN", "USER"); + + Supplier authentication = () -> new TestingAuthenticationToken("user", "password", + "ROLE_ANONYMOUS"); + Object object = new Object(); + + assertThat(manager.check(authentication, object).isGranted()).isFalse(); + } + + @Test + public void hasAnyAuthorityWhenUserHasAnyAuthorityThenGrantedDecision() { + AuthorityAuthorizationManager manager = AuthorityAuthorizationManager.hasAnyAuthority("ADMIN", "USER"); + + Supplier authentication = () -> new TestingAuthenticationToken("user", "password", "USER"); + Object object = new Object(); + + assertThat(manager.check(authentication, object).isGranted()).isTrue(); + } + + @Test + public void hasAnyAuthorityWhenUserHasNotAnyAuthorityThenDeniedDecision() { + AuthorityAuthorizationManager manager = AuthorityAuthorizationManager.hasAnyAuthority("ADMIN", "USER"); + + Supplier authentication = () -> new TestingAuthenticationToken("user", "password", "ANONYMOUS"); + Object object = new Object(); + + assertThat(manager.check(authentication, object).isGranted()).isFalse(); + } + +} diff --git a/core/src/test/java/org/springframework/security/authorization/AuthorizationManagerTests.java b/core/src/test/java/org/springframework/security/authorization/AuthorizationManagerTests.java new file mode 100644 index 00000000000..ee990d5f4c7 --- /dev/null +++ b/core/src/test/java/org/springframework/security/authorization/AuthorizationManagerTests.java @@ -0,0 +1,65 @@ +/* + * Copyright 2002-2020 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.authorization; + +import org.junit.Test; + +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.authentication.TestingAuthenticationToken; +import org.springframework.security.core.Authentication; + +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Tests for {@link AuthorizationManager}. + * + * @author Evgeniy Cheban + */ +public class AuthorizationManagerTests { + + @Test + public void verifyWhenCheckReturnedGrantedDecisionThenPasses() { + AuthorizationManager manager = (a, o) -> new AuthorizationDecision(true); + + Authentication authentication = new TestingAuthenticationToken("user", "password", "ROLE_1", "ROLE_2"); + Object object = new Object(); + + manager.verify(() -> authentication, object); + } + + @Test + public void verifyWhenCheckReturnedNullThenPasses() { + AuthorizationManager manager = (a, o) -> null; + + Authentication authentication = new TestingAuthenticationToken("user", "password", "ROLE_1", "ROLE_2"); + Object object = new Object(); + + manager.verify(() -> authentication, object); + } + + @Test + public void verifyWhenCheckReturnedDeniedDecisionThenAccessDeniedException() { + AuthorizationManager manager = (a, o) -> new AuthorizationDecision(false); + + Authentication authentication = new TestingAuthenticationToken("user", "password", "ROLE_1", "ROLE_2"); + Object object = new Object(); + + assertThatExceptionOfType(AccessDeniedException.class) + .isThrownBy(() -> manager.verify(() -> authentication, object)).withMessage("Access Denied"); + } + +} 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 new file mode 100644 index 00000000000..2893fb1ae3b --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/access/intercept/AuthorizationFilter.java @@ -0,0 +1,69 @@ +/* + * Copyright 2002-2020 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.intercept; + +import java.io.IOException; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException; +import org.springframework.security.authorization.AuthorizationManager; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.util.Assert; +import org.springframework.web.filter.OncePerRequestFilter; + +/** + * An authorization filter that restricts access to the URL using + * {@link AuthorizationManager}. + * + * @author Evgeniy Cheban + */ +public class AuthorizationFilter extends OncePerRequestFilter { + + private final AuthorizationManager authorizationManager; + + /** + * Creates an instance. + * @param authorizationManager the {@link AuthorizationManager} to use + */ + public AuthorizationFilter(AuthorizationManager authorizationManager) { + Assert.notNull(authorizationManager, "authorizationManager cannot be null"); + this.authorizationManager = authorizationManager; + } + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + + this.authorizationManager.verify(this::getAuthentication, request); + filterChain.doFilter(request, response); + } + + private Authentication getAuthentication() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication == null) { + throw new AuthenticationCredentialsNotFoundException( + "An Authentication object was not found in the SecurityContext"); + } + return authentication; + } + +} diff --git a/web/src/main/java/org/springframework/security/web/access/intercept/DelegatingAuthorizationManager.java b/web/src/main/java/org/springframework/security/web/access/intercept/DelegatingAuthorizationManager.java new file mode 100644 index 00000000000..a7169b12401 --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/access/intercept/DelegatingAuthorizationManager.java @@ -0,0 +1,124 @@ +/* + * Copyright 2002-2020 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.intercept; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.function.Supplier; + +import javax.servlet.http.HttpServletRequest; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.core.log.LogMessage; +import org.springframework.security.authorization.AuthorizationDecision; +import org.springframework.security.authorization.AuthorizationManager; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.util.matcher.RequestMatcher; +import org.springframework.security.web.util.matcher.RequestMatcher.MatchResult; +import org.springframework.util.Assert; + +/** + * An {@link AuthorizationManager} which delegates to a specific + * {@link AuthorizationManager} based on a {@link RequestMatcher} evaluation. + * + * @author Evgeniy Cheban + */ +public final class DelegatingAuthorizationManager implements AuthorizationManager { + + private final Log logger = LogFactory.getLog(getClass()); + + private final Map> mappings; + + private DelegatingAuthorizationManager( + Map> mappings) { + Assert.notEmpty(mappings, "mappings cannot be empty"); + this.mappings = mappings; + } + + /** + * Delegates to a specific {@link AuthorizationManager} based on a + * {@link RequestMatcher} evaluation. + * @param authentication the {@link Supplier} of the {@link Authentication} to check + * @param request the {@link HttpServletRequest} to check + * @return an {@link AuthorizationDecision}. If there is no {@link RequestMatcher} + * matching the request, or the {@link AuthorizationManager} could not decide, then + * null is returned + */ + @Override + public AuthorizationDecision check(Supplier authentication, HttpServletRequest request) { + if (this.logger.isTraceEnabled()) { + this.logger.trace(LogMessage.format("Authorizing %s", request)); + } + for (Map.Entry> mapping : this.mappings + .entrySet()) { + + RequestMatcher matcher = mapping.getKey(); + MatchResult matchResult = matcher.matcher(request); + if (matchResult.isMatch()) { + AuthorizationManager manager = mapping.getValue(); + if (this.logger.isTraceEnabled()) { + this.logger.trace(LogMessage.format("Checking authorization on %s using %s", request, manager)); + } + return manager.check(authentication, + new RequestAuthorizationContext(request, matchResult.getVariables())); + } + } + this.logger.trace("Abstaining since did not find matching RequestMatcher"); + return null; + } + + /** + * Creates a builder for {@link DelegatingAuthorizationManager}. + * @return the new {@link Builder} instance + */ + public static Builder builder() { + return new Builder(); + } + + /** + * A builder for {@link DelegatingAuthorizationManager}. + */ + public static final class Builder { + + private final Map> mappings = new LinkedHashMap<>(); + + /** + * Maps a {@link RequestMatcher} to an {@link AuthorizationManager}. + * @param matcher the {@link RequestMatcher} to use + * @param manager the {@link AuthorizationManager} to use + * @return the {@link Builder} for further customizations + */ + public Builder add(RequestMatcher matcher, AuthorizationManager manager) { + Assert.notNull(matcher, "matcher cannot be null"); + Assert.notNull(manager, "manager cannot be null"); + this.mappings.put(matcher, manager); + return this; + } + + /** + * Creates a {@link DelegatingAuthorizationManager} instance. + * @return the {@link DelegatingAuthorizationManager} instance + */ + public DelegatingAuthorizationManager build() { + return new DelegatingAuthorizationManager(this.mappings); + } + + } + +} diff --git a/web/src/main/java/org/springframework/security/web/access/intercept/RequestAuthorizationContext.java b/web/src/main/java/org/springframework/security/web/access/intercept/RequestAuthorizationContext.java new file mode 100644 index 00000000000..d1b639612e4 --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/access/intercept/RequestAuthorizationContext.java @@ -0,0 +1,72 @@ +/* + * Copyright 2002-2020 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.intercept; + +import java.util.Collections; +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; + +/** + * An {@link HttpServletRequest} authorization context. + * + * @author Evgeniy Cheban + */ +public final class RequestAuthorizationContext { + + private final HttpServletRequest request; + + private final Map variables; + + /** + * Creates an instance. + * @param request the {@link HttpServletRequest} to use + */ + public RequestAuthorizationContext(HttpServletRequest request) { + this(request, Collections.emptyMap()); + } + + /** + * Creates an instance. + * @param request the {@link HttpServletRequest} to use + * @param variables a map containing key-value pairs representing extracted variable + * names and variable values + */ + public RequestAuthorizationContext(HttpServletRequest request, Map variables) { + this.request = request; + this.variables = variables; + } + + /** + * Returns the {@link HttpServletRequest}. + * @return the {@link HttpServletRequest} to use + */ + public HttpServletRequest getRequest() { + return this.request; + } + + /** + * Returns the extracted variable values where the key is the variable name and the + * value is the variable value. + * @return a map containing key-value pairs representing extracted variable names and + * variable values + */ + public Map getVariables() { + return this.variables; + } + +} 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 new file mode 100644 index 00000000000..bbddf92f831 --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/access/intercept/AuthorizationFilterTests.java @@ -0,0 +1,128 @@ +/* + * Copyright 2002-2020 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.intercept; + +import java.util.function.Supplier; + +import javax.servlet.FilterChain; +import javax.servlet.http.HttpServletRequest; + +import org.junit.After; +import org.junit.Test; +import org.mockito.ArgumentCaptor; + +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException; +import org.springframework.security.authentication.TestingAuthenticationToken; +import org.springframework.security.authorization.AuthenticatedAuthorizationManager; +import org.springframework.security.authorization.AuthorizationManager; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.context.SecurityContextImpl; + +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.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.willThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; + +/** + * Tests for {@link AuthorizationFilter}. + * + * @author Evgeniy Cheban + */ +public class AuthorizationFilterTests { + + @After + public void tearDown() { + SecurityContextHolder.clearContext(); + } + + @Test + public void filterWhenAuthorizationManagerVerifyPassesThenNextFilter() throws Exception { + AuthorizationManager mockAuthorizationManager = mock(AuthorizationManager.class); + AuthorizationFilter filter = new AuthorizationFilter(mockAuthorizationManager); + TestingAuthenticationToken authenticationToken = new TestingAuthenticationToken("user", "password"); + + SecurityContext securityContext = new SecurityContextImpl(); + securityContext.setAuthentication(authenticationToken); + SecurityContextHolder.setContext(securityContext); + + MockHttpServletRequest mockRequest = new MockHttpServletRequest(null, "/path"); + MockHttpServletResponse mockResponse = new MockHttpServletResponse(); + FilterChain mockFilterChain = mock(FilterChain.class); + + filter.doFilter(mockRequest, mockResponse, mockFilterChain); + + ArgumentCaptor> authenticationCaptor = ArgumentCaptor.forClass(Supplier.class); + verify(mockAuthorizationManager).verify(authenticationCaptor.capture(), eq(mockRequest)); + Supplier authentication = authenticationCaptor.getValue(); + assertThat(authentication.get()).isEqualTo(authenticationToken); + + verify(mockFilterChain).doFilter(mockRequest, mockResponse); + } + + @Test + public void filterWhenAuthorizationManagerVerifyThrowsAccessDeniedExceptionThenStopFilterChain() { + AuthorizationManager mockAuthorizationManager = mock(AuthorizationManager.class); + AuthorizationFilter filter = new AuthorizationFilter(mockAuthorizationManager); + TestingAuthenticationToken authenticationToken = new TestingAuthenticationToken("user", "password"); + + SecurityContext securityContext = new SecurityContextImpl(); + securityContext.setAuthentication(authenticationToken); + SecurityContextHolder.setContext(securityContext); + + MockHttpServletRequest mockRequest = new MockHttpServletRequest(null, "/path"); + MockHttpServletResponse mockResponse = new MockHttpServletResponse(); + FilterChain mockFilterChain = mock(FilterChain.class); + + willThrow(new AccessDeniedException("Access Denied")).given(mockAuthorizationManager).verify(any(), + eq(mockRequest)); + + assertThatExceptionOfType(AccessDeniedException.class) + .isThrownBy(() -> filter.doFilter(mockRequest, mockResponse, mockFilterChain)) + .withMessage("Access Denied"); + + ArgumentCaptor> authenticationCaptor = ArgumentCaptor.forClass(Supplier.class); + verify(mockAuthorizationManager).verify(authenticationCaptor.capture(), eq(mockRequest)); + Supplier authentication = authenticationCaptor.getValue(); + assertThat(authentication.get()).isEqualTo(authenticationToken); + + verifyNoInteractions(mockFilterChain); + } + + @Test + public void filterWhenAuthenticationNullThenAuthenticationCredentialsNotFoundException() { + AuthorizationFilter filter = new AuthorizationFilter(AuthenticatedAuthorizationManager.authenticated()); + MockHttpServletRequest mockRequest = new MockHttpServletRequest(null, "/path"); + MockHttpServletResponse mockResponse = new MockHttpServletResponse(); + FilterChain mockFilterChain = mock(FilterChain.class); + + assertThatExceptionOfType(AuthenticationCredentialsNotFoundException.class) + .isThrownBy(() -> filter.doFilter(mockRequest, mockResponse, mockFilterChain)) + .withMessage("An Authentication object was not found in the SecurityContext"); + + verifyNoInteractions(mockFilterChain); + } + +} diff --git a/web/src/test/java/org/springframework/security/web/access/intercept/DelegatingAuthorizationManagerTests.java b/web/src/test/java/org/springframework/security/web/access/intercept/DelegatingAuthorizationManagerTests.java new file mode 100644 index 00000000000..04fa818b39d --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/access/intercept/DelegatingAuthorizationManagerTests.java @@ -0,0 +1,84 @@ +/* + * Copyright 2002-2020 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.intercept; + +import java.util.function.Supplier; + +import org.junit.Test; + +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.security.authentication.TestingAuthenticationToken; +import org.springframework.security.authorization.AuthorizationDecision; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link DelegatingAuthorizationManager}. + * + * @author Evgeniy Cheban + */ +public class DelegatingAuthorizationManagerTests { + + @Test + public void buildWhenMappingsEmptyThenException() { + assertThatIllegalArgumentException().isThrownBy(() -> DelegatingAuthorizationManager.builder().build()) + .withMessage("mappings cannot be empty"); + } + + @Test + public void addWhenMatcherNullThenException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> DelegatingAuthorizationManager.builder() + .add(null, (a, o) -> new AuthorizationDecision(true)).build()) + .withMessage("matcher cannot be null"); + } + + @Test + public void addWhenManagerNullThenException() { + assertThatIllegalArgumentException().isThrownBy( + () -> DelegatingAuthorizationManager.builder().add(new MvcRequestMatcher(null, "/grant"), null).build()) + .withMessage("manager cannot be null"); + } + + @Test + public void checkWhenMultipleMappingsConfiguredThenDelegatesMatchingManager() { + DelegatingAuthorizationManager manager = DelegatingAuthorizationManager.builder() + .add(new MvcRequestMatcher(null, "/grant"), (a, o) -> new AuthorizationDecision(true)) + .add(new MvcRequestMatcher(null, "/deny"), (a, o) -> new AuthorizationDecision(false)) + .add(new MvcRequestMatcher(null, "/neutral"), (a, o) -> null).build(); + + Supplier authentication = () -> new TestingAuthenticationToken("user", "password", "ROLE_USER"); + + AuthorizationDecision grant = manager.check(authentication, new MockHttpServletRequest(null, "/grant")); + assertThat(grant).isNotNull(); + assertThat(grant.isGranted()).isTrue(); + + AuthorizationDecision deny = manager.check(authentication, new MockHttpServletRequest(null, "/deny")); + assertThat(deny).isNotNull(); + assertThat(deny.isGranted()).isFalse(); + + AuthorizationDecision neutral = manager.check(authentication, new MockHttpServletRequest(null, "/neutral")); + assertThat(neutral).isNull(); + + AuthorizationDecision abstain = manager.check(authentication, new MockHttpServletRequest(null, "/abstain")); + assertThat(abstain).isNull(); + } + +}