diff --git a/config/spring-security-config.gradle b/config/spring-security-config.gradle index d641eda0ab3..0bda161bd49 100644 --- a/config/spring-security-config.gradle +++ b/config/spring-security-config.gradle @@ -32,6 +32,7 @@ dependencies { optional'org.springframework:spring-websocket' optional 'org.jetbrains.kotlin:kotlin-reflect' optional 'org.jetbrains.kotlin:kotlin-stdlib-jdk8' + optional 'javax.annotation:jsr250-api' provided 'javax.servlet:javax.servlet-api' diff --git a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/EnableMethodSecurity.java b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/EnableMethodSecurity.java new file mode 100644 index 00000000000..c64a4355be7 --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/EnableMethodSecurity.java @@ -0,0 +1,87 @@ +/* + * 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.config.annotation.method.configuration; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.context.annotation.AdviceMode; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.core.Ordered; +import org.springframework.security.access.annotation.Secured; + +/** + * Enables Spring Security Method Security. + * @author Evgeniy Cheban + * @since 5.5 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +@Documented +@Import(MethodSecuritySelector.class) +@Configuration +public @interface EnableMethodSecurity { + + /** + * Determines if Spring Security's {@link Secured} annotation should be enabled. + * Default is false. + * @return true if {@link Secured} annotation should be enabled false otherwise + */ + boolean securedEnabled() default false; + + /** + * Determines if JSR-250 annotations should be enabled. Default is false. + * @return true if JSR-250 should be enabled false otherwise + */ + boolean jsr250Enabled() default false; + + /** + * Indicate whether subclass-based (CGLIB) proxies are to be created as opposed to + * standard Java interface-based proxies. The default is {@code false}. + * Applicable only if {@link #mode()} is set to {@link AdviceMode#PROXY}. + *

+ * Note that setting this attribute to {@code true} will affect all + * Spring-managed beans requiring proxying, not just those marked with + * {@code @Cacheable}. For example, other beans marked with Spring's + * {@code @Transactional} annotation will be upgraded to subclass proxying at the same + * time. This approach has no negative impact in practice unless one is explicitly + * expecting one type of proxy vs another, e.g. in tests. + * @return true if subclass-based (CGLIB) proxies are to be created + */ + boolean proxyTargetClass() default false; + + /** + * Indicate how security advice should be applied. The default is + * {@link AdviceMode#PROXY}. + * @see AdviceMode + * @return the {@link AdviceMode} to use + */ + AdviceMode mode() default AdviceMode.PROXY; + + /** + * Indicate the ordering of the execution of the security advisor when multiple + * advices are applied at a specific joinpoint. The default is + * {@link Ordered#LOWEST_PRECEDENCE}. + * @return the order the security advisor should be applied + */ + int order() default Ordered.LOWEST_PRECEDENCE; + +} diff --git a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/MethodSecurityConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/MethodSecurityConfiguration.java new file mode 100644 index 00000000000..529a78f45c4 --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/MethodSecurityConfiguration.java @@ -0,0 +1,252 @@ +/* + * 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.config.annotation.method.configuration; + +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.annotation.security.DenyAll; +import javax.annotation.security.PermitAll; +import javax.annotation.security.RolesAllowed; + +import org.springframework.aop.MethodMatcher; +import org.springframework.aop.Pointcut; +import org.springframework.aop.support.AopUtils; +import org.springframework.aop.support.DefaultPointcutAdvisor; +import org.springframework.aop.support.Pointcuts; +import org.springframework.aop.support.StaticMethodMatcher; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.ImportAware; +import org.springframework.context.annotation.Role; +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.core.annotation.AnnotationAttributes; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.security.access.annotation.Jsr250AuthorizationManager; +import org.springframework.security.access.annotation.Secured; +import org.springframework.security.access.annotation.SecuredAuthorizationManager; +import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler; +import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler; +import org.springframework.security.access.intercept.aopalliance.AuthorizationMethodInterceptor; +import org.springframework.security.access.method.AuthorizationManagerMethodAfterAdvice; +import org.springframework.security.access.method.AuthorizationManagerMethodBeforeAdvice; +import org.springframework.security.access.method.AuthorizationMethodAfterAdvice; +import org.springframework.security.access.method.AuthorizationMethodBeforeAdvice; +import org.springframework.security.access.method.DelegatingAuthorizationMethodAfterAdvice; +import org.springframework.security.access.method.DelegatingAuthorizationMethodBeforeAdvice; +import org.springframework.security.access.method.MethodAuthorizationContext; +import org.springframework.security.access.prepost.PostAuthorize; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.authorization.method.PostAuthorizeAuthorizationManager; +import org.springframework.security.authorization.method.PostFilterAuthorizationMethodAfterAdvice; +import org.springframework.security.authorization.method.PreAuthorizeAuthorizationManager; +import org.springframework.security.authorization.method.PreFilterAuthorizationMethodBeforeAdvice; +import org.springframework.security.config.core.GrantedAuthorityDefaults; + +/** + * Base {@link Configuration} for enabling Spring Security Method Security. + * + * @author Evgeniy Cheban + * @see EnableMethodSecurity + * @since 5.5 + */ +@Configuration(proxyBeanMethods = false) +@Role(BeanDefinition.ROLE_INFRASTRUCTURE) +final class MethodSecurityConfiguration implements ImportAware { + + private MethodSecurityExpressionHandler methodSecurityExpressionHandler; + + private GrantedAuthorityDefaults grantedAuthorityDefaults; + + private AuthorizationMethodBeforeAdvice authorizationMethodBeforeAdvice; + + private AuthorizationMethodAfterAdvice authorizationMethodAfterAdvice; + + private AnnotationAttributes enableMethodSecurity; + + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + DefaultPointcutAdvisor methodSecurityAdvisor(AuthorizationMethodInterceptor interceptor) { + Pointcut pointcut = Pointcuts.union(getAuthorizationMethodBeforeAdvice(), getAuthorizationMethodAfterAdvice()); + DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(pointcut, interceptor); + advisor.setOrder(order()); + return advisor; + } + + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + AuthorizationMethodInterceptor authorizationMethodInterceptor() { + return new AuthorizationMethodInterceptor(getAuthorizationMethodBeforeAdvice(), + getAuthorizationMethodAfterAdvice()); + } + + private MethodSecurityExpressionHandler getMethodSecurityExpressionHandler() { + if (this.methodSecurityExpressionHandler == null) { + this.methodSecurityExpressionHandler = new DefaultMethodSecurityExpressionHandler(); + } + return this.methodSecurityExpressionHandler; + } + + @Autowired(required = false) + void setMethodSecurityExpressionHandler(MethodSecurityExpressionHandler methodSecurityExpressionHandler) { + this.methodSecurityExpressionHandler = methodSecurityExpressionHandler; + } + + @Autowired(required = false) + void setGrantedAuthorityDefaults(GrantedAuthorityDefaults grantedAuthorityDefaults) { + this.grantedAuthorityDefaults = grantedAuthorityDefaults; + } + + private AuthorizationMethodBeforeAdvice getAuthorizationMethodBeforeAdvice() { + if (this.authorizationMethodBeforeAdvice == null) { + this.authorizationMethodBeforeAdvice = createDefaultAuthorizationMethodBeforeAdvice(); + } + return this.authorizationMethodBeforeAdvice; + } + + private AuthorizationMethodBeforeAdvice createDefaultAuthorizationMethodBeforeAdvice() { + List> beforeAdvices = new ArrayList<>(); + beforeAdvices.add(getPreFilterAuthorizationMethodBeforeAdvice()); + beforeAdvices.add(getPreAuthorizeAuthorizationMethodBeforeAdvice()); + if (securedEnabled()) { + beforeAdvices.add(getSecuredAuthorizationMethodBeforeAdvice()); + } + if (jsr250Enabled()) { + beforeAdvices.add(getJsr250AuthorizationMethodBeforeAdvice()); + } + return new DelegatingAuthorizationMethodBeforeAdvice(beforeAdvices); + } + + private PreFilterAuthorizationMethodBeforeAdvice getPreFilterAuthorizationMethodBeforeAdvice() { + PreFilterAuthorizationMethodBeforeAdvice preFilterBeforeAdvice = new PreFilterAuthorizationMethodBeforeAdvice(); + preFilterBeforeAdvice.setExpressionHandler(getMethodSecurityExpressionHandler()); + return preFilterBeforeAdvice; + } + + private AuthorizationMethodBeforeAdvice getPreAuthorizeAuthorizationMethodBeforeAdvice() { + MethodMatcher methodMatcher = new SecurityAnnotationsStaticMethodMatcher(PreAuthorize.class); + PreAuthorizeAuthorizationManager authorizationManager = new PreAuthorizeAuthorizationManager(); + authorizationManager.setExpressionHandler(getMethodSecurityExpressionHandler()); + return new AuthorizationManagerMethodBeforeAdvice<>(methodMatcher, authorizationManager); + } + + private AuthorizationManagerMethodBeforeAdvice getSecuredAuthorizationMethodBeforeAdvice() { + MethodMatcher methodMatcher = new SecurityAnnotationsStaticMethodMatcher(Secured.class); + SecuredAuthorizationManager authorizationManager = new SecuredAuthorizationManager(); + return new AuthorizationManagerMethodBeforeAdvice<>(methodMatcher, authorizationManager); + } + + private AuthorizationManagerMethodBeforeAdvice getJsr250AuthorizationMethodBeforeAdvice() { + MethodMatcher methodMatcher = new SecurityAnnotationsStaticMethodMatcher(DenyAll.class, PermitAll.class, + RolesAllowed.class); + Jsr250AuthorizationManager authorizationManager = new Jsr250AuthorizationManager(); + if (this.grantedAuthorityDefaults != null) { + authorizationManager.setRolePrefix(this.grantedAuthorityDefaults.getRolePrefix()); + } + return new AuthorizationManagerMethodBeforeAdvice<>(methodMatcher, authorizationManager); + } + + @Autowired(required = false) + void setAuthorizationMethodBeforeAdvice( + AuthorizationMethodBeforeAdvice authorizationMethodBeforeAdvice) { + this.authorizationMethodBeforeAdvice = authorizationMethodBeforeAdvice; + } + + private AuthorizationMethodAfterAdvice getAuthorizationMethodAfterAdvice() { + if (this.authorizationMethodAfterAdvice == null) { + this.authorizationMethodAfterAdvice = createDefaultAuthorizationMethodAfterAdvice(); + } + return this.authorizationMethodAfterAdvice; + } + + private AuthorizationMethodAfterAdvice createDefaultAuthorizationMethodAfterAdvice() { + List> afterAdvices = new ArrayList<>(); + afterAdvices.add(getPostFilterAuthorizationMethodAfterAdvice()); + afterAdvices.add(getPostAuthorizeAuthorizationMethodAfterAdvice()); + return new DelegatingAuthorizationMethodAfterAdvice(afterAdvices); + } + + private PostFilterAuthorizationMethodAfterAdvice getPostFilterAuthorizationMethodAfterAdvice() { + PostFilterAuthorizationMethodAfterAdvice postFilterAfterAdvice = new PostFilterAuthorizationMethodAfterAdvice(); + postFilterAfterAdvice.setExpressionHandler(getMethodSecurityExpressionHandler()); + return postFilterAfterAdvice; + } + + private AuthorizationManagerMethodAfterAdvice getPostAuthorizeAuthorizationMethodAfterAdvice() { + MethodMatcher methodMatcher = new SecurityAnnotationsStaticMethodMatcher(PostAuthorize.class); + PostAuthorizeAuthorizationManager authorizationManager = new PostAuthorizeAuthorizationManager(); + authorizationManager.setExpressionHandler(getMethodSecurityExpressionHandler()); + return new AuthorizationManagerMethodAfterAdvice<>(methodMatcher, authorizationManager); + } + + @Autowired(required = false) + void setAuthorizationMethodAfterAdvice( + AuthorizationMethodAfterAdvice authorizationMethodAfterAdvice) { + this.authorizationMethodAfterAdvice = authorizationMethodAfterAdvice; + } + + @Override + public void setImportMetadata(AnnotationMetadata importMetadata) { + Map attributes = importMetadata.getAnnotationAttributes(EnableMethodSecurity.class.getName()); + this.enableMethodSecurity = AnnotationAttributes.fromMap(attributes); + } + + private boolean securedEnabled() { + return this.enableMethodSecurity.getBoolean("securedEnabled"); + } + + private boolean jsr250Enabled() { + return this.enableMethodSecurity.getBoolean("jsr250Enabled"); + } + + private int order() { + return this.enableMethodSecurity.getNumber("order"); + } + + private static final class SecurityAnnotationsStaticMethodMatcher extends StaticMethodMatcher { + + private final Set> annotationClasses; + + @SafeVarargs + private SecurityAnnotationsStaticMethodMatcher(Class... annotationClasses) { + this.annotationClasses = new HashSet<>(Arrays.asList(annotationClasses)); + } + + @Override + public boolean matches(Method method, Class targetClass) { + Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass); + return hasAnnotations(specificMethod) || hasAnnotations(specificMethod.getDeclaringClass()); + } + + private boolean hasAnnotations(AnnotatedElement annotatedElement) { + Set annotations = AnnotatedElementUtils.findAllMergedAnnotations(annotatedElement, + this.annotationClasses); + return !annotations.isEmpty(); + } + + } + +} diff --git a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/MethodSecuritySelector.java b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/MethodSecuritySelector.java new file mode 100644 index 00000000000..ed0df62454a --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/MethodSecuritySelector.java @@ -0,0 +1,50 @@ +/* + * 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.config.annotation.method.configuration; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.context.annotation.AdviceMode; +import org.springframework.context.annotation.AdviceModeImportSelector; +import org.springframework.context.annotation.AutoProxyRegistrar; + +/** + * Dynamically determines which imports to include using the {@link EnableMethodSecurity} + * annotation. + * + * @author Evgeniy Cheban + * @since 5.5 + */ +final class MethodSecuritySelector extends AdviceModeImportSelector { + + @Override + protected String[] selectImports(AdviceMode adviceMode) { + if (adviceMode == AdviceMode.PROXY) { + return getProxyImports(); + } + throw new IllegalStateException("AdviceMode '" + adviceMode + "' is not supported"); + } + + private String[] getProxyImports() { + List result = new ArrayList<>(); + result.add(AutoProxyRegistrar.class.getName()); + result.add(MethodSecurityConfiguration.class.getName()); + return result.toArray(new String[0]); + } + +} diff --git a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/MethodSecurityConfigurationTests.java b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/MethodSecurityConfigurationTests.java new file mode 100644 index 00000000000..41bf8f7c3c0 --- /dev/null +++ b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/MethodSecurityConfigurationTests.java @@ -0,0 +1,360 @@ +/* + * 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.config.annotation.method.configuration; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Supplier; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.springframework.aop.MethodMatcher; +import org.springframework.aop.support.JdkRegexpMethodPointcut; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.access.PermissionEvaluator; +import org.springframework.security.access.annotation.BusinessService; +import org.springframework.security.access.annotation.ExpressionProtectedBusinessServiceImpl; +import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler; +import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler; +import org.springframework.security.access.method.AuthorizationManagerMethodBeforeAdvice; +import org.springframework.security.access.method.AuthorizationMethodAfterAdvice; +import org.springframework.security.access.method.AuthorizationMethodBeforeAdvice; +import org.springframework.security.access.method.MethodAuthorizationContext; +import org.springframework.security.authorization.AuthorizationDecision; +import org.springframework.security.authorization.AuthorizationManager; +import org.springframework.security.config.test.SpringTestRule; +import org.springframework.security.core.Authentication; +import org.springframework.security.test.context.annotation.SecurityTestExecutionListeners; +import org.springframework.security.test.context.support.WithAnonymousUser; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.context.junit4.SpringRunner; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Tests for {@link MethodSecurityConfiguration}. + * + * @author Evgeniy Cheban + */ +@RunWith(SpringRunner.class) +@SecurityTestExecutionListeners +public class MethodSecurityConfigurationTests { + + @Rule + public final SpringTestRule spring = new SpringTestRule(); + + @Autowired(required = false) + MethodSecurityService methodSecurityService; + + @Autowired(required = false) + BusinessService businessService; + + @WithMockUser(roles = "ADMIN") + @Test + public void preAuthorizeWhenRoleAdminThenAccessDeniedException() { + this.spring.register(MethodSecurityServiceConfig.class).autowire(); + assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(this.methodSecurityService::preAuthorize) + .withMessage("Access Denied"); + } + + @WithAnonymousUser + @Test + public void preAuthorizePermitAllWhenRoleAnonymousThenPasses() { + this.spring.register(MethodSecurityServiceConfig.class).autowire(); + String result = this.methodSecurityService.preAuthorizePermitAll(); + assertThat(result).isNull(); + } + + @WithAnonymousUser + @Test + public void preAuthorizeNotAnonymousWhenRoleAnonymousThenAccessDeniedException() { + this.spring.register(MethodSecurityServiceConfig.class).autowire(); + assertThatExceptionOfType(AccessDeniedException.class) + .isThrownBy(this.methodSecurityService::preAuthorizeNotAnonymous).withMessage("Access Denied"); + } + + @WithMockUser + @Test + public void preAuthorizeNotAnonymousWhenRoleUserThenPasses() { + this.spring.register(MethodSecurityServiceConfig.class).autowire(); + this.methodSecurityService.preAuthorizeNotAnonymous(); + } + + @WithMockUser + @Test + public void securedWhenRoleUserThenAccessDeniedException() { + this.spring.register(MethodSecurityServiceConfig.class).autowire(); + assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(this.methodSecurityService::secured) + .withMessage("Access Denied"); + } + + @WithMockUser(roles = "ADMIN") + @Test + public void securedWhenRoleAdminThenPasses() { + this.spring.register(MethodSecurityServiceConfig.class).autowire(); + String result = this.methodSecurityService.secured(); + assertThat(result).isNull(); + } + + @WithMockUser(roles = "ADMIN") + @Test + public void securedUserWhenRoleAdminThenAccessDeniedException() { + this.spring.register(MethodSecurityServiceConfig.class).autowire(); + assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(this.methodSecurityService::securedUser) + .withMessage("Access Denied"); + } + + @WithMockUser + @Test + public void securedUserWhenRoleUserThenPasses() { + this.spring.register(MethodSecurityServiceConfig.class).autowire(); + String result = this.methodSecurityService.securedUser(); + assertThat(result).isNull(); + } + + @WithMockUser + @Test + public void preAuthorizeAdminWhenRoleUserThenAccessDeniedException() { + this.spring.register(MethodSecurityServiceConfig.class).autowire(); + assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(this.methodSecurityService::preAuthorizeAdmin) + .withMessage("Access Denied"); + } + + @WithMockUser(roles = "ADMIN") + @Test + public void preAuthorizeAdminWhenRoleAdminThenPasses() { + this.spring.register(MethodSecurityServiceConfig.class).autowire(); + this.methodSecurityService.preAuthorizeAdmin(); + } + + @WithMockUser + @Test + public void postHasPermissionWhenParameterIsNotGrantThenAccessDeniedException() { + this.spring.register(CustomPermissionEvaluatorConfig.class, MethodSecurityServiceConfig.class).autowire(); + assertThatExceptionOfType(AccessDeniedException.class) + .isThrownBy(() -> this.methodSecurityService.postHasPermission("deny")).withMessage("Access Denied"); + } + + @WithMockUser + @Test + public void postHasPermissionWhenParameterIsGrantThenPasses() { + this.spring.register(CustomPermissionEvaluatorConfig.class, MethodSecurityServiceConfig.class).autowire(); + String result = this.methodSecurityService.postHasPermission("grant"); + assertThat(result).isNull(); + } + + @WithMockUser + @Test + public void postAnnotationWhenParameterIsNotGrantThenAccessDeniedException() { + this.spring.register(MethodSecurityServiceConfig.class).autowire(); + assertThatExceptionOfType(AccessDeniedException.class) + .isThrownBy(() -> this.methodSecurityService.postAnnotation("deny")).withMessage("Access Denied"); + } + + @WithMockUser + @Test + public void postAnnotationWhenParameterIsGrantThenPasses() { + this.spring.register(MethodSecurityServiceConfig.class).autowire(); + String result = this.methodSecurityService.postAnnotation("grant"); + assertThat(result).isNull(); + } + + @WithMockUser("bob") + @Test + public void methodReturningAListWhenPrePostFiltersConfiguredThenFiltersList() { + this.spring.register(BusinessServiceConfig.class).autowire(); + List names = new ArrayList<>(); + names.add("bob"); + names.add("joe"); + names.add("sam"); + List result = this.businessService.methodReturningAList(names); + assertThat(result).hasSize(1); + assertThat(result.get(0)).isEqualTo("bob"); + } + + @WithMockUser("bob") + @Test + public void methodReturningAnArrayWhenPostFilterConfiguredThenFiltersArray() { + this.spring.register(BusinessServiceConfig.class).autowire(); + List names = new ArrayList<>(); + names.add("bob"); + names.add("joe"); + names.add("sam"); + Object[] result = this.businessService.methodReturningAnArray(names.toArray()); + assertThat(result).hasSize(1); + assertThat(result[0]).isEqualTo("bob"); + } + + @WithMockUser("bob") + @Test + public void securedUserWhenCustomBeforeAdviceConfiguredAndNameBobThenPasses() { + this.spring.register(CustomAuthorizationManagerBeforeAdviceConfig.class, MethodSecurityServiceConfig.class) + .autowire(); + String result = this.methodSecurityService.securedUser(); + assertThat(result).isNull(); + } + + @WithMockUser("joe") + @Test + public void securedUserWhenCustomBeforeAdviceConfiguredAndNameNotBobThenAccessDeniedException() { + this.spring.register(CustomAuthorizationManagerBeforeAdviceConfig.class, MethodSecurityServiceConfig.class) + .autowire(); + assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(this.methodSecurityService::securedUser) + .withMessage("Access Denied"); + } + + @WithMockUser("bob") + @Test + public void securedUserWhenCustomAfterAdviceConfiguredAndNameBobThenGranted() { + this.spring.register(CustomAuthorizationManagerAfterAdviceConfig.class, MethodSecurityServiceConfig.class) + .autowire(); + String result = this.methodSecurityService.securedUser(); + assertThat(result).isEqualTo("granted"); + } + + @WithMockUser("joe") + @Test + public void securedUserWhenCustomAfterAdviceConfiguredAndNameNotBobThenAccessDeniedException() { + this.spring.register(CustomAuthorizationManagerAfterAdviceConfig.class, MethodSecurityServiceConfig.class) + .autowire(); + assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(this.methodSecurityService::securedUser) + .withMessage("Access Denied for User 'joe'"); + } + + @WithMockUser(roles = "ADMIN") + @Test + public void jsr250WhenRoleAdminThenAccessDeniedException() { + this.spring.register(MethodSecurityServiceConfig.class).autowire(); + assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(this.methodSecurityService::jsr250) + .withMessage("Access Denied"); + } + + @WithAnonymousUser + @Test + public void jsr250PermitAllWhenRoleAnonymousThenPasses() { + this.spring.register(MethodSecurityServiceConfig.class).autowire(); + String result = this.methodSecurityService.jsr250PermitAll(); + assertThat(result).isNull(); + } + + @WithMockUser(roles = "ADMIN") + @Test + public void rolesAllowedUserWhenRoleAdminThenAccessDeniedException() { + this.spring.register(BusinessServiceConfig.class).autowire(); + assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(this.businessService::rolesAllowedUser) + .withMessage("Access Denied"); + } + + @WithMockUser + @Test + public void rolesAllowedUserWhenRoleUserThenPasses() { + this.spring.register(BusinessServiceConfig.class).autowire(); + this.businessService.rolesAllowedUser(); + } + + @EnableMethodSecurity(securedEnabled = true, jsr250Enabled = true) + static class MethodSecurityServiceConfig { + + @Bean + MethodSecurityService methodSecurityService() { + return new MethodSecurityServiceImpl(); + } + + } + + @EnableMethodSecurity(jsr250Enabled = true) + static class BusinessServiceConfig { + + @Bean + BusinessService businessService() { + return new ExpressionProtectedBusinessServiceImpl(); + } + + } + + @EnableMethodSecurity + static class CustomPermissionEvaluatorConfig { + + @Bean + MethodSecurityExpressionHandler methodSecurityExpressionHandler() { + DefaultMethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler(); + expressionHandler.setPermissionEvaluator(new PermissionEvaluator() { + @Override + public boolean hasPermission(Authentication authentication, Object targetDomainObject, + Object permission) { + return "grant".equals(targetDomainObject); + } + + @Override + public boolean hasPermission(Authentication authentication, Serializable targetId, String targetType, + Object permission) { + throw new UnsupportedOperationException(); + } + }); + return expressionHandler; + } + + } + + @EnableMethodSecurity + static class CustomAuthorizationManagerBeforeAdviceConfig { + + @Bean + AuthorizationMethodBeforeAdvice customBeforeAdvice() { + JdkRegexpMethodPointcut methodMatcher = new JdkRegexpMethodPointcut(); + methodMatcher.setPattern(".*MethodSecurityServiceImpl.*securedUser"); + AuthorizationManager authorizationManager = (a, + o) -> new AuthorizationDecision("bob".equals(a.get().getName())); + return new AuthorizationManagerMethodBeforeAdvice<>(methodMatcher, authorizationManager); + } + + } + + @EnableMethodSecurity + static class CustomAuthorizationManagerAfterAdviceConfig { + + @Bean + AuthorizationMethodAfterAdvice customAfterAdvice() { + JdkRegexpMethodPointcut methodMatcher = new JdkRegexpMethodPointcut(); + methodMatcher.setPattern(".*MethodSecurityServiceImpl.*securedUser"); + return new AuthorizationMethodAfterAdvice() { + @Override + public MethodMatcher getMethodMatcher() { + return methodMatcher; + } + + @Override + public Object after(Supplier authentication, + MethodAuthorizationContext methodAuthorizationContext, Object returnedObject) { + Authentication auth = authentication.get(); + if ("bob".equals(auth.getName())) { + return "granted"; + } + throw new AccessDeniedException("Access Denied for User '" + auth.getName() + "'"); + } + }; + } + + } + +} diff --git a/core/src/main/java/org/springframework/security/access/annotation/AbstractAuthorizationManagerRegistry.java b/core/src/main/java/org/springframework/security/access/annotation/AbstractAuthorizationManagerRegistry.java new file mode 100644 index 00000000000..855f2ab0c01 --- /dev/null +++ b/core/src/main/java/org/springframework/security/access/annotation/AbstractAuthorizationManagerRegistry.java @@ -0,0 +1,67 @@ +/* + * 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.access.annotation; + +import java.lang.reflect.Method; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.aopalliance.intercept.MethodInvocation; + +import org.springframework.core.MethodClassKey; +import org.springframework.lang.NonNull; +import org.springframework.security.access.method.MethodAuthorizationContext; +import org.springframework.security.authorization.AuthorizationManager; + +/** + * An abstract registry which provides an {@link AuthorizationManager} for the + * {@link MethodInvocation}. + * + * @author Evgeniy Cheban + * @since 5.5 + */ +abstract class AbstractAuthorizationManagerRegistry { + + static final AuthorizationManager NULL_MANAGER = (a, o) -> null; + + private final Map> cachedManagers = new ConcurrentHashMap<>(); + + /** + * Returns an {@link AuthorizationManager} for the {@link MethodAuthorizationContext}. + * @param methodAuthorizationContext the {@link MethodAuthorizationContext} to use + * @return an {@link AuthorizationManager} to use + */ + final AuthorizationManager getManager( + MethodAuthorizationContext methodAuthorizationContext) { + MethodInvocation methodInvocation = methodAuthorizationContext.getMethodInvocation(); + Method method = methodInvocation.getMethod(); + Class targetClass = methodAuthorizationContext.getTargetClass(); + MethodClassKey cacheKey = new MethodClassKey(method, targetClass); + return this.cachedManagers.computeIfAbsent(cacheKey, (k) -> resolveManager(method, targetClass)); + } + + /** + * Subclasses should implement this method to provide the non-null + * {@link AuthorizationManager} for the method and the target class. + * @param method the method + * @param targetClass the target class + * @return the non-null {@link AuthorizationManager} + */ + @NonNull + abstract AuthorizationManager resolveManager(Method method, Class targetClass); + +} diff --git a/core/src/main/java/org/springframework/security/access/annotation/Jsr250AuthorizationManager.java b/core/src/main/java/org/springframework/security/access/annotation/Jsr250AuthorizationManager.java new file mode 100644 index 00000000000..19ed88ee23c --- /dev/null +++ b/core/src/main/java/org/springframework/security/access/annotation/Jsr250AuthorizationManager.java @@ -0,0 +1,123 @@ +/* + * 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.access.annotation; + +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Method; +import java.util.HashSet; +import java.util.Set; +import java.util.function.Supplier; + +import javax.annotation.security.DenyAll; +import javax.annotation.security.PermitAll; +import javax.annotation.security.RolesAllowed; + +import org.aopalliance.intercept.MethodInvocation; + +import org.springframework.aop.support.AopUtils; +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.lang.NonNull; +import org.springframework.security.access.method.MethodAuthorizationContext; +import org.springframework.security.authorization.AuthorityAuthorizationManager; +import org.springframework.security.authorization.AuthorizationDecision; +import org.springframework.security.authorization.AuthorizationManager; +import org.springframework.security.core.Authentication; +import org.springframework.util.Assert; + +/** + * An {@link AuthorizationManager} which can determine if an {@link Authentication} has + * access to the {@link MethodInvocation} by evaluating if the {@link Authentication} + * contains a specified authority from the JSR-250 security annotations. + * + * @author Evgeniy Cheban + * @since 5.5 + */ +public final class Jsr250AuthorizationManager implements AuthorizationManager { + + private static final Set> JSR250_ANNOTATIONS = new HashSet<>(); + + static { + JSR250_ANNOTATIONS.add(DenyAll.class); + JSR250_ANNOTATIONS.add(PermitAll.class); + JSR250_ANNOTATIONS.add(RolesAllowed.class); + } + + private final Jsr250AuthorizationManagerRegistry registry = new Jsr250AuthorizationManagerRegistry(); + + private String rolePrefix = "ROLE_"; + + /** + * Sets the role prefix. Defaults to "ROLE_". + * @param rolePrefix the role prefix to use + */ + public void setRolePrefix(String rolePrefix) { + Assert.notNull(rolePrefix, "rolePrefix cannot be null"); + this.rolePrefix = rolePrefix; + } + + /** + * Determines if an {@link Authentication} has access to the {@link MethodInvocation} + * by evaluating if the {@link Authentication} contains a specified authority from the + * JSR-250 security annotations. + * @param authentication the {@link Supplier} of the {@link Authentication} to check + * @param methodAuthorizationContext the {@link MethodAuthorizationContext} to check + * @return an {@link AuthorizationDecision} or null if the JSR-250 security + * annotations is not present + */ + @Override + public AuthorizationDecision check(Supplier authentication, + MethodAuthorizationContext methodAuthorizationContext) { + AuthorizationManager delegate = this.registry + .getManager(methodAuthorizationContext); + return delegate.check(authentication, methodAuthorizationContext); + } + + private final class Jsr250AuthorizationManagerRegistry extends AbstractAuthorizationManagerRegistry { + + @NonNull + @Override + AuthorizationManager resolveManager(Method method, Class targetClass) { + for (Annotation annotation : findJsr250Annotations(method, targetClass)) { + if (annotation instanceof DenyAll) { + return (a, o) -> new AuthorizationDecision(false); + } + if (annotation instanceof PermitAll) { + return (a, o) -> new AuthorizationDecision(true); + } + if (annotation instanceof RolesAllowed) { + RolesAllowed rolesAllowed = (RolesAllowed) annotation; + return AuthorityAuthorizationManager.hasAnyRole(Jsr250AuthorizationManager.this.rolePrefix, + rolesAllowed.value()); + } + } + return NULL_MANAGER; + } + + private Set findJsr250Annotations(Method method, Class targetClass) { + Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass); + Set annotations = findAnnotations(specificMethod); + return (annotations.isEmpty()) ? findAnnotations(specificMethod.getDeclaringClass()) : annotations; + } + + private Set findAnnotations(AnnotatedElement annotatedElement) { + return AnnotatedElementUtils.findAllMergedAnnotations(annotatedElement, JSR250_ANNOTATIONS); + } + + } + +} diff --git a/core/src/main/java/org/springframework/security/access/annotation/SecuredAuthorizationManager.java b/core/src/main/java/org/springframework/security/access/annotation/SecuredAuthorizationManager.java new file mode 100644 index 00000000000..ca962669f26 --- /dev/null +++ b/core/src/main/java/org/springframework/security/access/annotation/SecuredAuthorizationManager.java @@ -0,0 +1,80 @@ +/* + * 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.access.annotation; + +import java.lang.reflect.Method; +import java.util.function.Supplier; + +import org.aopalliance.intercept.MethodInvocation; + +import org.springframework.aop.support.AopUtils; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.lang.NonNull; +import org.springframework.security.access.method.MethodAuthorizationContext; +import org.springframework.security.authorization.AuthorityAuthorizationManager; +import org.springframework.security.authorization.AuthorizationDecision; +import org.springframework.security.authorization.AuthorizationManager; +import org.springframework.security.core.Authentication; + +/** + * An {@link AuthorizationManager} which can determine if an {@link Authentication} has + * access to the {@link MethodInvocation} by evaluating if the {@link Authentication} + * contains a specified authority from the Spring Security's {@link Secured} annotation. + * + * @author Evgeniy Cheban + * @since 5.5 + */ +public final class SecuredAuthorizationManager implements AuthorizationManager { + + private final SecuredAuthorizationManagerRegistry registry = new SecuredAuthorizationManagerRegistry(); + + /** + * Determines if an {@link Authentication} has access to the {@link MethodInvocation} + * by evaluating if the {@link Authentication} contains a specified authority from the + * Spring Security's {@link Secured} annotation. + * @param authentication the {@link Supplier} of the {@link Authentication} to check + * @param methodAuthorizationContext the {@link MethodAuthorizationContext} to check + * @return an {@link AuthorizationDecision} or null if the {@link Secured} annotation + * is not present + */ + @Override + public AuthorizationDecision check(Supplier authentication, + MethodAuthorizationContext methodAuthorizationContext) { + AuthorizationManager delegate = this.registry + .getManager(methodAuthorizationContext); + return delegate.check(authentication, methodAuthorizationContext); + } + + private static final class SecuredAuthorizationManagerRegistry extends AbstractAuthorizationManagerRegistry { + + @NonNull + @Override + AuthorizationManager resolveManager(Method method, Class targetClass) { + Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass); + Secured secured = findSecuredAnnotation(specificMethod); + return (secured != null) ? AuthorityAuthorizationManager.hasAnyAuthority(secured.value()) : NULL_MANAGER; + } + + private Secured findSecuredAnnotation(Method method) { + Secured secured = AnnotationUtils.findAnnotation(method, Secured.class); + return (secured != null) ? secured + : AnnotationUtils.findAnnotation(method.getDeclaringClass(), Secured.class); + } + + } + +} diff --git a/core/src/main/java/org/springframework/security/access/intercept/aopalliance/AuthorizationMethodInterceptor.java b/core/src/main/java/org/springframework/security/access/intercept/aopalliance/AuthorizationMethodInterceptor.java new file mode 100644 index 00000000000..37de6d2df6e --- /dev/null +++ b/core/src/main/java/org/springframework/security/access/intercept/aopalliance/AuthorizationMethodInterceptor.java @@ -0,0 +1,82 @@ +/* + * 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.access.intercept.aopalliance; + +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; + +import org.springframework.aop.support.AopUtils; +import org.springframework.lang.NonNull; +import org.springframework.security.access.method.AuthorizationMethodAfterAdvice; +import org.springframework.security.access.method.AuthorizationMethodBeforeAdvice; +import org.springframework.security.access.method.MethodAuthorizationContext; +import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; + +/** + * Provides security interception of AOP Alliance based method invocations. + * + * @author Evgeniy Cheban + * @since 5.5 + */ +public final class AuthorizationMethodInterceptor implements MethodInterceptor { + + private final AuthorizationMethodBeforeAdvice beforeAdvice; + + private final AuthorizationMethodAfterAdvice afterAdvice; + + /** + * Creates an instance. + * @param beforeAdvice the {@link AuthorizationMethodBeforeAdvice} to use + * @param afterAdvice the {@link AuthorizationMethodAfterAdvice} to use + */ + public AuthorizationMethodInterceptor(AuthorizationMethodBeforeAdvice beforeAdvice, + AuthorizationMethodAfterAdvice afterAdvice) { + this.beforeAdvice = beforeAdvice; + this.afterAdvice = afterAdvice; + } + + /** + * This method should be used to enforce security on a {@link MethodInvocation}. + * @param mi the method being invoked which requires a security decision + * @return the returned value from the {@link MethodInvocation} + */ + @Override + public Object invoke(@NonNull MethodInvocation mi) throws Throwable { + MethodAuthorizationContext methodAuthorizationContext = getMethodAuthorizationContext(mi); + this.beforeAdvice.before(this::getAuthentication, methodAuthorizationContext); + Object returnedObject = mi.proceed(); + return this.afterAdvice.after(this::getAuthentication, methodAuthorizationContext, returnedObject); + } + + private MethodAuthorizationContext getMethodAuthorizationContext(MethodInvocation mi) { + Object target = mi.getThis(); + Class targetClass = (target != null) ? AopUtils.getTargetClass(target) : null; + return new MethodAuthorizationContext(mi, targetClass); + } + + 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/core/src/main/java/org/springframework/security/access/method/AuthorizationManagerMethodAfterAdvice.java b/core/src/main/java/org/springframework/security/access/method/AuthorizationManagerMethodAfterAdvice.java new file mode 100644 index 00000000000..776cc3e032b --- /dev/null +++ b/core/src/main/java/org/springframework/security/access/method/AuthorizationManagerMethodAfterAdvice.java @@ -0,0 +1,73 @@ +/* + * 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.access.method; + +import java.util.function.Supplier; + +import org.springframework.aop.MethodMatcher; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.authorization.AuthorizationManager; +import org.springframework.security.core.Authentication; +import org.springframework.util.Assert; + +/** + * An {@link AuthorizationMethodAfterAdvice} which can determine if an + * {@link Authentication} has access to the {@link T} object using an + * {@link AuthorizationManager} if a {@link MethodMatcher} matches. + * + * @param the type of object that the authorization check is being done one. + * @author Evgeniy Cheban + * @since 5.5 + */ +public final class AuthorizationManagerMethodAfterAdvice implements AuthorizationMethodAfterAdvice { + + private final MethodMatcher methodMatcher; + + private final AuthorizationManager authorizationManager; + + /** + * Creates an instance. + * @param methodMatcher the {@link MethodMatcher} to use + * @param authorizationManager the {@link AuthorizationManager} to use + */ + public AuthorizationManagerMethodAfterAdvice(MethodMatcher methodMatcher, + AuthorizationManager authorizationManager) { + Assert.notNull(methodMatcher, "methodMatcher cannot be null"); + Assert.notNull(authorizationManager, "authorizationManager cannot be null"); + this.methodMatcher = methodMatcher; + this.authorizationManager = authorizationManager; + } + + /** + * Determines if an {@link Authentication} has access to the {@link T} object using + * the {@link AuthorizationManager}. + * @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 + */ + @Override + public Object after(Supplier authentication, T object, Object returnedObject) { + this.authorizationManager.verify(authentication, object); + return returnedObject; + } + + @Override + public MethodMatcher getMethodMatcher() { + return this.methodMatcher; + } + +} diff --git a/core/src/main/java/org/springframework/security/access/method/AuthorizationManagerMethodBeforeAdvice.java b/core/src/main/java/org/springframework/security/access/method/AuthorizationManagerMethodBeforeAdvice.java new file mode 100644 index 00000000000..e8d583d92f2 --- /dev/null +++ b/core/src/main/java/org/springframework/security/access/method/AuthorizationManagerMethodBeforeAdvice.java @@ -0,0 +1,72 @@ +/* + * 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.access.method; + +import java.util.function.Supplier; + +import org.springframework.aop.MethodMatcher; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.authorization.AuthorizationManager; +import org.springframework.security.core.Authentication; +import org.springframework.util.Assert; + +/** + * An {@link AuthorizationMethodBeforeAdvice} which can determine if an + * {@link Authentication} has access to the {@link T} object using an + * {@link AuthorizationManager} if a {@link MethodMatcher} matches. + * + * @param the type of object that the authorization check is being done one. + * @author Evgeniy Cheban + * @since 5.5 + */ +public final class AuthorizationManagerMethodBeforeAdvice implements AuthorizationMethodBeforeAdvice { + + private final MethodMatcher methodMatcher; + + private final AuthorizationManager authorizationManager; + + /** + * Creates an instance. + * @param methodMatcher the {@link MethodMatcher} to use + * @param authorizationManager the {@link AuthorizationManager} to use + */ + public AuthorizationManagerMethodBeforeAdvice(MethodMatcher methodMatcher, + AuthorizationManager authorizationManager) { + Assert.notNull(methodMatcher, "methodMatcher cannot be null"); + Assert.notNull(authorizationManager, "authorizationManager cannot be null"); + this.methodMatcher = methodMatcher; + this.authorizationManager = authorizationManager; + } + + /** + * Determines if an {@link Authentication} has access to the {@link T} object using + * the {@link AuthorizationManager}. + * @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 + */ + @Override + public void before(Supplier authentication, T object) { + this.authorizationManager.verify(authentication, object); + } + + @Override + public MethodMatcher getMethodMatcher() { + return this.methodMatcher; + } + +} diff --git a/core/src/main/java/org/springframework/security/access/method/AuthorizationMethodAfterAdvice.java b/core/src/main/java/org/springframework/security/access/method/AuthorizationMethodAfterAdvice.java new file mode 100644 index 00000000000..ac95eb62029 --- /dev/null +++ b/core/src/main/java/org/springframework/security/access/method/AuthorizationMethodAfterAdvice.java @@ -0,0 +1,61 @@ +/* + * 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.access.method; + +import java.util.function.Supplier; + +import org.aopalliance.intercept.MethodInvocation; + +import org.springframework.aop.ClassFilter; +import org.springframework.aop.Pointcut; +import org.springframework.security.core.Authentication; + +/** + * An Authorization advice that can determine if an {@link Authentication} has access to + * the returned object from the {@link MethodInvocation}. The {@link #getMethodMatcher()} + * describes when the advice applies for the method. + * + * @param the type of object that the authorization check is being done one. + * @author Evgeniy Cheban + * @since 5.5 + */ +public interface AuthorizationMethodAfterAdvice extends Pointcut { + + /** + * Returns the default {@link ClassFilter}. + * @return the {@link ClassFilter#TRUE} to use + */ + @Override + default ClassFilter getClassFilter() { + return ClassFilter.TRUE; + } + + /** + * Determines if an {@link Authentication} has access to the returned object from the + * {@link MethodInvocation}. + * @param authentication the {@link Supplier} of the {@link Authentication} to check + * @param object the {@link T} object to check + * @param returnedObject the returned object from the {@link MethodInvocation} to + * check + * @return the Object that will ultimately be returned to the caller (if + * an implementation does not wish to modify the object to be returned to the caller, + * the implementation should simply return the same object it was passed by the + * returnedObject method argument) + */ + Object after(Supplier authentication, T object, Object returnedObject); + +} diff --git a/core/src/main/java/org/springframework/security/access/method/AuthorizationMethodBeforeAdvice.java b/core/src/main/java/org/springframework/security/access/method/AuthorizationMethodBeforeAdvice.java new file mode 100644 index 00000000000..d367d525147 --- /dev/null +++ b/core/src/main/java/org/springframework/security/access/method/AuthorizationMethodBeforeAdvice.java @@ -0,0 +1,52 @@ +/* + * 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.access.method; + +import java.util.function.Supplier; + +import org.springframework.aop.ClassFilter; +import org.springframework.aop.Pointcut; +import org.springframework.security.core.Authentication; + +/** + * An advice which can determine if an {@link Authentication} has access to the {@link T} + * object. The {@link #getMethodMatcher()} describes when the advice applies for the + * method. + * + * @param the type of object that the authorization check is being done one. + * @author Evgeniy Cheban + * @since 5.5 + */ +public interface AuthorizationMethodBeforeAdvice extends Pointcut { + + /** + * Returns the default {@link ClassFilter}. + * @return the {@link ClassFilter#TRUE} to use + */ + @Override + default ClassFilter getClassFilter() { + return ClassFilter.TRUE; + } + + /** + * Determines if an {@link Authentication} has access to the {@link T} object. + * @param authentication the {@link Supplier} of the {@link Authentication} to check + * @param object the {@link T} object to check + */ + void before(Supplier authentication, T object); + +} diff --git a/core/src/main/java/org/springframework/security/access/method/DelegatingAuthorizationMethodAfterAdvice.java b/core/src/main/java/org/springframework/security/access/method/DelegatingAuthorizationMethodAfterAdvice.java new file mode 100644 index 00000000000..b4d6c0548db --- /dev/null +++ b/core/src/main/java/org/springframework/security/access/method/DelegatingAuthorizationMethodAfterAdvice.java @@ -0,0 +1,102 @@ +/* + * 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.access.method; + +import java.lang.reflect.Method; +import java.util.List; +import java.util.function.Supplier; + +import org.aopalliance.intercept.MethodInvocation; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.aop.MethodMatcher; +import org.springframework.aop.support.StaticMethodMatcher; +import org.springframework.core.log.LogMessage; +import org.springframework.security.core.Authentication; + +/** + * An {@link AuthorizationMethodAfterAdvice} which delegates to specific + * {@link AuthorizationMethodAfterAdvice}s and returns the result (possibly modified) from + * the {@link MethodInvocation}. + * + * @author Evgeniy Cheban + * @since 5.5 + */ +public final class DelegatingAuthorizationMethodAfterAdvice + implements AuthorizationMethodAfterAdvice { + + private final Log logger = LogFactory.getLog(getClass()); + + private final MethodMatcher methodMatcher = new StaticMethodMatcher() { + @Override + public boolean matches(Method method, Class targetClass) { + for (AuthorizationMethodAfterAdvice delegate : DelegatingAuthorizationMethodAfterAdvice.this.delegates) { + MethodMatcher methodMatcher = delegate.getMethodMatcher(); + if (methodMatcher.matches(method, targetClass)) { + return true; + } + } + return false; + } + }; + + private final List> delegates; + + /** + * Creates an instance. + * @param delegates the {@link AuthorizationMethodAfterAdvice}s to use + */ + public DelegatingAuthorizationMethodAfterAdvice( + List> delegates) { + this.delegates = delegates; + } + + @Override + public MethodMatcher getMethodMatcher() { + return this.methodMatcher; + } + + /** + * Delegates to specific {@link AuthorizationMethodAfterAdvice}s and returns the + * returnedObject (possibly modified) from the method argument. + * @param authentication the {@link Supplier} of the {@link Authentication} to check + * @param methodAuthorizationContext the {@link MethodAuthorizationContext} to check + * @param returnedObject the returned object from the {@link MethodInvocation} to + * check + * @return the returnedObject (possibly modified) from the method + * argument + */ + @Override + public Object after(Supplier authentication, MethodAuthorizationContext methodAuthorizationContext, + Object returnedObject) { + if (this.logger.isTraceEnabled()) { + this.logger.trace( + LogMessage.format("Post Authorizing %s from %s", returnedObject, methodAuthorizationContext)); + } + Object result = returnedObject; + for (AuthorizationMethodAfterAdvice delegate : this.delegates) { + if (this.logger.isTraceEnabled()) { + this.logger.trace(LogMessage.format("Checking authorization on %s from %s using %s", result, + methodAuthorizationContext, delegate)); + } + result = delegate.after(authentication, methodAuthorizationContext, result); + } + return result; + } + +} diff --git a/core/src/main/java/org/springframework/security/access/method/DelegatingAuthorizationMethodBeforeAdvice.java b/core/src/main/java/org/springframework/security/access/method/DelegatingAuthorizationMethodBeforeAdvice.java new file mode 100644 index 00000000000..ee4d4a937eb --- /dev/null +++ b/core/src/main/java/org/springframework/security/access/method/DelegatingAuthorizationMethodBeforeAdvice.java @@ -0,0 +1,95 @@ +/* + * 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.access.method; + +import java.lang.reflect.Method; +import java.util.List; +import java.util.function.Supplier; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.aop.MethodMatcher; +import org.springframework.aop.support.StaticMethodMatcher; +import org.springframework.core.log.LogMessage; +import org.springframework.security.core.Authentication; + +/** + * An {@link AuthorizationMethodBeforeAdvice} which delegates to a specific + * {@link AuthorizationMethodBeforeAdvice} and grants access if all + * {@link AuthorizationMethodBeforeAdvice}s granted or abstained. Denies access only if + * one of the {@link AuthorizationMethodBeforeAdvice}s denied. + * + * @author Evgeniy Cheban + * @since 5.5 + */ +public final class DelegatingAuthorizationMethodBeforeAdvice + implements AuthorizationMethodBeforeAdvice { + + private final Log logger = LogFactory.getLog(getClass()); + + private final MethodMatcher methodMatcher = new StaticMethodMatcher() { + @Override + public boolean matches(Method method, Class targetClass) { + for (AuthorizationMethodBeforeAdvice delegate : DelegatingAuthorizationMethodBeforeAdvice.this.delegates) { + MethodMatcher methodMatcher = delegate.getMethodMatcher(); + if (methodMatcher.matches(method, targetClass)) { + return true; + } + } + return false; + } + }; + + private final List> delegates; + + /** + * Creates an instance. + * @param delegates the {@link AuthorizationMethodBeforeAdvice}s to use + */ + public DelegatingAuthorizationMethodBeforeAdvice( + List> delegates) { + this.delegates = delegates; + } + + @Override + public MethodMatcher getMethodMatcher() { + return this.methodMatcher; + } + + /** + * Delegates to a specific {@link AuthorizationMethodBeforeAdvice} and grants access + * if all {@link AuthorizationMethodBeforeAdvice}s granted or abstained. Denies only + * if one of the {@link AuthorizationMethodBeforeAdvice}s denied. + * @param authentication the {@link Supplier} of the {@link Authentication} to check + * @param methodAuthorizationContext the {@link MethodAuthorizationContext} to check + */ + @Override + public void before(Supplier authentication, MethodAuthorizationContext methodAuthorizationContext) { + if (this.logger.isTraceEnabled()) { + this.logger.trace(LogMessage.format("Pre Authorizing %s", methodAuthorizationContext)); + } + for (AuthorizationMethodBeforeAdvice delegate : this.delegates) { + if (this.logger.isTraceEnabled()) { + this.logger.trace(LogMessage.format("Checking authorization on %s using %s", methodAuthorizationContext, + delegate)); + } + delegate.before(authentication, methodAuthorizationContext); + } + } + +} diff --git a/core/src/main/java/org/springframework/security/access/method/MethodAuthorizationContext.java b/core/src/main/java/org/springframework/security/access/method/MethodAuthorizationContext.java new file mode 100644 index 00000000000..4361f9ac9a0 --- /dev/null +++ b/core/src/main/java/org/springframework/security/access/method/MethodAuthorizationContext.java @@ -0,0 +1,84 @@ +/* + * 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.access.method; + +import org.aopalliance.intercept.MethodInvocation; + +/** + * An authorization context which is holds the {@link MethodInvocation}, the target class + * and the returned object. + * + * @author Evgeniy Cheban + * @since 5.5 + */ +public final class MethodAuthorizationContext { + + private final MethodInvocation methodInvocation; + + private final Class targetClass; + + private Object returnObject; + + /** + * Creates an instance. + * @param methodInvocation the {@link MethodInvocation} to use + * @param targetClass the target class to use + */ + public MethodAuthorizationContext(MethodInvocation methodInvocation, Class targetClass) { + this.methodInvocation = methodInvocation; + this.targetClass = targetClass; + } + + /** + * Returns the {@link MethodInvocation}. + * @return the {@link MethodInvocation} to use + */ + public MethodInvocation getMethodInvocation() { + return this.methodInvocation; + } + + /** + * Returns the target class. + * @return the target class to use + */ + public Class getTargetClass() { + return this.targetClass; + } + + /** + * Returns the returned object from the {@link MethodInvocation}. + * @return the returned object from the {@link MethodInvocation} to use + */ + public Object getReturnObject() { + return this.returnObject; + } + + /** + * Sets the returned object from the {@link MethodInvocation}. + * @param returnObject the returned object from the {@link MethodInvocation} to use + */ + public void setReturnObject(Object returnObject) { + this.returnObject = returnObject; + } + + @Override + public String toString() { + return "MethodAuthorizationContext[methodInvocation=" + this.methodInvocation + ", targetClass=" + + this.targetClass + ", returnObject=" + this.returnObject + ']'; + } + +} diff --git a/core/src/main/java/org/springframework/security/authorization/AuthorityAuthorizationManager.java b/core/src/main/java/org/springframework/security/authorization/AuthorityAuthorizationManager.java index ea6f3d80272..7d245aed6ed 100644 --- a/core/src/main/java/org/springframework/security/authorization/AuthorityAuthorizationManager.java +++ b/core/src/main/java/org/springframework/security/authorization/AuthorityAuthorizationManager.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. @@ -75,9 +75,22 @@ public static AuthorityAuthorizationManager hasAuthority(String authority * @return the new instance */ public static AuthorityAuthorizationManager hasAnyRole(String... roles) { + return hasAnyRole(ROLE_PREFIX, roles); + } + + /** + * Creates an instance of {@link AuthorityAuthorizationManager} with the provided + * authorities. + * @param rolePrefix the role prefix for roles + * @param roles the authorities to check for prefixed with rolePrefix + * @param the type of object being authorized + * @return the new instance + */ + public static AuthorityAuthorizationManager hasAnyRole(String rolePrefix, String[] roles) { + Assert.notNull(rolePrefix, "rolePrefix cannot be null"); Assert.notEmpty(roles, "roles cannot be empty"); Assert.noNullElements(roles, "roles cannot contain null values"); - return hasAnyAuthority(toNamedRolesArray(roles)); + return hasAnyAuthority(toNamedRolesArray(rolePrefix, roles)); } /** @@ -93,10 +106,10 @@ public static AuthorityAuthorizationManager hasAnyAuthority(String... aut return new AuthorityAuthorizationManager<>(authorities); } - private static String[] toNamedRolesArray(String... roles) { + private static String[] toNamedRolesArray(String rolePrefix, String[] roles) { String[] result = new String[roles.length]; for (int i = 0; i < roles.length; i++) { - result[i] = ROLE_PREFIX + roles[i]; + result[i] = rolePrefix + roles[i]; } return result; } diff --git a/core/src/main/java/org/springframework/security/authorization/method/AbstractExpressionAttributeRegistry.java b/core/src/main/java/org/springframework/security/authorization/method/AbstractExpressionAttributeRegistry.java new file mode 100644 index 00000000000..f48d59dd33c --- /dev/null +++ b/core/src/main/java/org/springframework/security/authorization/method/AbstractExpressionAttributeRegistry.java @@ -0,0 +1,73 @@ +/* + * 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.authorization.method; + +import java.lang.reflect.Method; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.aopalliance.intercept.MethodInvocation; + +import org.springframework.core.MethodClassKey; +import org.springframework.lang.NonNull; +import org.springframework.security.access.method.MethodAuthorizationContext; + +/** + * An abstract registry which provides an {@link ExpressionAttribute} for the + * {@link MethodInvocation}. + * + * @author Evgeniy Cheban + * @since 5.5 + */ +abstract class AbstractExpressionAttributeRegistry { + + private final Map cachedAttributes = new ConcurrentHashMap<>(); + + /** + * Returns an {@link ExpressionAttribute} for the {@link MethodAuthorizationContext}. + * @param methodAuthorizationContext the {@link MethodAuthorizationContext} to use + * @return the {@link ExpressionAttribute} to use + */ + final T getAttribute(MethodAuthorizationContext methodAuthorizationContext) { + MethodInvocation methodInvocation = methodAuthorizationContext.getMethodInvocation(); + Method method = methodInvocation.getMethod(); + Class targetClass = methodAuthorizationContext.getTargetClass(); + return getAttribute(method, targetClass); + } + + /** + * Returns an {@link ExpressionAttribute} for the method and the target class. + * @param method the method + * @param targetClass the target class + * @return the {@link ExpressionAttribute} to use + */ + final T getAttribute(Method method, Class targetClass) { + MethodClassKey cacheKey = new MethodClassKey(method, targetClass); + return this.cachedAttributes.computeIfAbsent(cacheKey, (k) -> resolveAttribute(method, targetClass)); + } + + /** + * Subclasses should implement this method to provide the non-null + * {@link ExpressionAttribute} for the method and the target class. + * @param method the method + * @param targetClass the target class + * @return the non-null {@link ExpressionAttribute} + */ + @NonNull + abstract T resolveAttribute(Method method, Class targetClass); + +} diff --git a/core/src/main/java/org/springframework/security/authorization/method/ExpressionAttribute.java b/core/src/main/java/org/springframework/security/authorization/method/ExpressionAttribute.java new file mode 100644 index 00000000000..80e49360e9b --- /dev/null +++ b/core/src/main/java/org/springframework/security/authorization/method/ExpressionAttribute.java @@ -0,0 +1,52 @@ +/* + * 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.authorization.method; + +import org.springframework.expression.Expression; + +/** + * An {@link Expression} attribute. + * + * @author Evgeniy Cheban + * @since 5.5 + */ +class ExpressionAttribute { + + /** + * Represents an empty attribute with null {@link Expression}. + */ + static final ExpressionAttribute NULL_ATTRIBUTE = new ExpressionAttribute(null); + + private final Expression expression; + + /** + * Creates an instance. + * @param expression the {@link Expression} to use + */ + ExpressionAttribute(Expression expression) { + this.expression = expression; + } + + /** + * Returns the {@link Expression}. + * @return the {@link Expression} to use + */ + Expression getExpression() { + return this.expression; + } + +} diff --git a/core/src/main/java/org/springframework/security/authorization/method/PostAuthorizeAuthorizationManager.java b/core/src/main/java/org/springframework/security/authorization/method/PostAuthorizeAuthorizationManager.java new file mode 100644 index 00000000000..11109fb67ed --- /dev/null +++ b/core/src/main/java/org/springframework/security/authorization/method/PostAuthorizeAuthorizationManager.java @@ -0,0 +1,108 @@ +/* + * 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.authorization.method; + +import java.lang.reflect.Method; +import java.util.function.Supplier; + +import org.aopalliance.intercept.MethodInvocation; +import reactor.util.annotation.NonNull; + +import org.springframework.aop.support.AopUtils; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.Expression; +import org.springframework.security.access.expression.ExpressionUtils; +import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler; +import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler; +import org.springframework.security.access.method.MethodAuthorizationContext; +import org.springframework.security.access.prepost.PostAuthorize; +import org.springframework.security.authorization.AuthorizationDecision; +import org.springframework.security.authorization.AuthorizationManager; +import org.springframework.security.core.Authentication; +import org.springframework.util.Assert; + +/** + * An {@link AuthorizationManager} which can determine if an {@link Authentication} has + * access to the {@link MethodInvocation} by evaluating an expression from the + * {@link PostAuthorize} annotation. + * + * @author Evgeniy Cheban + * @since 5.5 + */ +public final class PostAuthorizeAuthorizationManager implements AuthorizationManager { + + private final PostAuthorizeExpressionAttributeRegistry registry = new PostAuthorizeExpressionAttributeRegistry(); + + private MethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler(); + + /** + * Sets the {@link MethodSecurityExpressionHandler}. + * @param expressionHandler the {@link MethodSecurityExpressionHandler} to use + */ + public void setExpressionHandler(MethodSecurityExpressionHandler expressionHandler) { + Assert.notNull(expressionHandler, "expressionHandler cannot be null"); + this.expressionHandler = expressionHandler; + } + + /** + * Determines if an {@link Authentication} has access to the {@link MethodInvocation} + * by evaluating an expression from the {@link PostAuthorize} annotation. + * @param authentication the {@link Supplier} of the {@link Authentication} to check + * @param methodAuthorizationContext the {@link MethodAuthorizationContext} to check + * @return an {@link AuthorizationDecision} or null if the {@link PostAuthorize} + * annotation is not present + */ + @Override + public AuthorizationDecision check(Supplier authentication, + MethodAuthorizationContext methodAuthorizationContext) { + ExpressionAttribute attribute = this.registry.getAttribute(methodAuthorizationContext); + if (attribute == ExpressionAttribute.NULL_ATTRIBUTE) { + return null; + } + EvaluationContext ctx = this.expressionHandler.createEvaluationContext(authentication.get(), + methodAuthorizationContext.getMethodInvocation()); + this.expressionHandler.setReturnObject(methodAuthorizationContext.getReturnObject(), ctx); + boolean granted = ExpressionUtils.evaluateAsBoolean(attribute.getExpression(), ctx); + return new AuthorizationDecision(granted); + } + + private final class PostAuthorizeExpressionAttributeRegistry + extends AbstractExpressionAttributeRegistry { + + @NonNull + @Override + ExpressionAttribute resolveAttribute(Method method, Class targetClass) { + Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass); + PostAuthorize postAuthorize = findPostAuthorizeAnnotation(specificMethod); + if (postAuthorize == null) { + return ExpressionAttribute.NULL_ATTRIBUTE; + } + Expression postAuthorizeExpression = PostAuthorizeAuthorizationManager.this.expressionHandler + .getExpressionParser().parseExpression(postAuthorize.value()); + return new ExpressionAttribute(postAuthorizeExpression); + } + + private PostAuthorize findPostAuthorizeAnnotation(Method method) { + PostAuthorize postAuthorize = AnnotationUtils.findAnnotation(method, PostAuthorize.class); + return (postAuthorize != null) ? postAuthorize + : AnnotationUtils.findAnnotation(method.getDeclaringClass(), PostAuthorize.class); + } + + } + +} diff --git a/core/src/main/java/org/springframework/security/authorization/method/PostFilterAuthorizationMethodAfterAdvice.java b/core/src/main/java/org/springframework/security/authorization/method/PostFilterAuthorizationMethodAfterAdvice.java new file mode 100644 index 00000000000..c573922c9c9 --- /dev/null +++ b/core/src/main/java/org/springframework/security/authorization/method/PostFilterAuthorizationMethodAfterAdvice.java @@ -0,0 +1,126 @@ +/* + * 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.authorization.method; + +import java.lang.reflect.Method; +import java.util.function.Supplier; + +import org.aopalliance.intercept.MethodInvocation; + +import org.springframework.aop.MethodMatcher; +import org.springframework.aop.support.AopUtils; +import org.springframework.aop.support.StaticMethodMatcher; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.Expression; +import org.springframework.lang.NonNull; +import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler; +import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler; +import org.springframework.security.access.method.AuthorizationMethodAfterAdvice; +import org.springframework.security.access.method.MethodAuthorizationContext; +import org.springframework.security.access.prepost.PostFilter; +import org.springframework.security.core.Authentication; +import org.springframework.util.Assert; + +/** + * An {@link AuthorizationMethodAfterAdvice} which filters a returnedObject + * from the {@link MethodInvocation} by evaluating an expression from the + * {@link PostFilter} annotation. + * + * @author Evgeniy Cheban + * @since 5.5 + */ +public final class PostFilterAuthorizationMethodAfterAdvice + implements AuthorizationMethodAfterAdvice { + + private final PostFilterExpressionAttributeRegistry registry = new PostFilterExpressionAttributeRegistry(); + + private final MethodMatcher methodMatcher = new StaticMethodMatcher() { + @Override + public boolean matches(Method method, Class targetClass) { + return PostFilterAuthorizationMethodAfterAdvice.this.registry.getAttribute(method, + targetClass) != ExpressionAttribute.NULL_ATTRIBUTE; + } + }; + + private MethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler(); + + /** + * Sets the {@link MethodSecurityExpressionHandler}. + * @param expressionHandler the {@link MethodSecurityExpressionHandler} to use + */ + public void setExpressionHandler(MethodSecurityExpressionHandler expressionHandler) { + Assert.notNull(expressionHandler, "expressionHandler cannot be null"); + this.expressionHandler = expressionHandler; + } + + @Override + public MethodMatcher getMethodMatcher() { + return this.methodMatcher; + } + + /** + * Filters a returnedObject from the {@link MethodInvocation} by + * evaluating an expression from the {@link PostFilter} annotation. + * @param authentication the {@link Supplier} of the {@link Authentication} to check + * @param methodAuthorizationContext the {@link MethodAuthorizationContext} to check + * @param returnedObject the returned object from the {@link MethodInvocation} to + * check + * @return filtered returnedObject from the {@link MethodInvocation} + */ + @Override + public Object after(Supplier authentication, MethodAuthorizationContext methodAuthorizationContext, + Object returnedObject) { + if (returnedObject == null) { + return null; + } + ExpressionAttribute attribute = this.registry.getAttribute(methodAuthorizationContext); + if (attribute == ExpressionAttribute.NULL_ATTRIBUTE) { + return returnedObject; + } + EvaluationContext ctx = this.expressionHandler.createEvaluationContext(authentication.get(), + methodAuthorizationContext.getMethodInvocation()); + Object result = this.expressionHandler.filter(returnedObject, attribute.getExpression(), ctx); + methodAuthorizationContext.setReturnObject(result); + return result; + } + + private final class PostFilterExpressionAttributeRegistry + extends AbstractExpressionAttributeRegistry { + + @NonNull + @Override + ExpressionAttribute resolveAttribute(Method method, Class targetClass) { + Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass); + PostFilter postFilter = findPostFilterAnnotation(specificMethod); + if (postFilter == null) { + return ExpressionAttribute.NULL_ATTRIBUTE; + } + Expression postFilterExpression = PostFilterAuthorizationMethodAfterAdvice.this.expressionHandler + .getExpressionParser().parseExpression(postFilter.value()); + return new ExpressionAttribute(postFilterExpression); + } + + private PostFilter findPostFilterAnnotation(Method method) { + PostFilter postFilter = AnnotationUtils.findAnnotation(method, PostFilter.class); + return (postFilter != null) ? postFilter + : AnnotationUtils.findAnnotation(method.getDeclaringClass(), PostFilter.class); + } + + } + +} diff --git a/core/src/main/java/org/springframework/security/authorization/method/PreAuthorizeAuthorizationManager.java b/core/src/main/java/org/springframework/security/authorization/method/PreAuthorizeAuthorizationManager.java new file mode 100644 index 00000000000..bebceee0e64 --- /dev/null +++ b/core/src/main/java/org/springframework/security/authorization/method/PreAuthorizeAuthorizationManager.java @@ -0,0 +1,107 @@ +/* + * 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.authorization.method; + +import java.lang.reflect.Method; +import java.util.function.Supplier; + +import org.aopalliance.intercept.MethodInvocation; +import reactor.util.annotation.NonNull; + +import org.springframework.aop.support.AopUtils; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.Expression; +import org.springframework.security.access.expression.ExpressionUtils; +import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler; +import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler; +import org.springframework.security.access.method.MethodAuthorizationContext; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.authorization.AuthorizationDecision; +import org.springframework.security.authorization.AuthorizationManager; +import org.springframework.security.core.Authentication; +import org.springframework.util.Assert; + +/** + * An {@link AuthorizationManager} which can determine if an {@link Authentication} has + * access to the {@link MethodInvocation} by evaluating an expression from the + * {@link PreAuthorize} annotation. + * + * @author Evgeniy Cheban + * @since 5.5 + */ +public final class PreAuthorizeAuthorizationManager implements AuthorizationManager { + + private final PreAuthorizeExpressionAttributeRegistry registry = new PreAuthorizeExpressionAttributeRegistry(); + + private MethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler(); + + /** + * Sets the {@link MethodSecurityExpressionHandler}. + * @param expressionHandler the {@link MethodSecurityExpressionHandler} to use + */ + public void setExpressionHandler(MethodSecurityExpressionHandler expressionHandler) { + Assert.notNull(expressionHandler, "expressionHandler cannot be null"); + this.expressionHandler = expressionHandler; + } + + /** + * Determines if an {@link Authentication} has access to the {@link MethodInvocation} + * by evaluating an expression from the {@link PreAuthorize} annotation. + * @param authentication the {@link Supplier} of the {@link Authentication} to check + * @param methodAuthorizationContext the {@link MethodAuthorizationContext} to check + * @return an {@link AuthorizationDecision} or null if the {@link PreAuthorize} + * annotation is not present + */ + @Override + public AuthorizationDecision check(Supplier authentication, + MethodAuthorizationContext methodAuthorizationContext) { + ExpressionAttribute attribute = this.registry.getAttribute(methodAuthorizationContext); + if (attribute == ExpressionAttribute.NULL_ATTRIBUTE) { + return null; + } + EvaluationContext ctx = this.expressionHandler.createEvaluationContext(authentication.get(), + methodAuthorizationContext.getMethodInvocation()); + boolean granted = ExpressionUtils.evaluateAsBoolean(attribute.getExpression(), ctx); + return new AuthorizationDecision(granted); + } + + private final class PreAuthorizeExpressionAttributeRegistry + extends AbstractExpressionAttributeRegistry { + + @NonNull + @Override + ExpressionAttribute resolveAttribute(Method method, Class targetClass) { + Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass); + PreAuthorize preAuthorize = findPreAuthorizeAnnotation(specificMethod); + if (preAuthorize == null) { + return ExpressionAttribute.NULL_ATTRIBUTE; + } + Expression preAuthorizeExpression = PreAuthorizeAuthorizationManager.this.expressionHandler + .getExpressionParser().parseExpression(preAuthorize.value()); + return new ExpressionAttribute(preAuthorizeExpression); + } + + private PreAuthorize findPreAuthorizeAnnotation(Method method) { + PreAuthorize preAuthorize = AnnotationUtils.findAnnotation(method, PreAuthorize.class); + return (preAuthorize != null) ? preAuthorize + : AnnotationUtils.findAnnotation(method.getDeclaringClass(), PreAuthorize.class); + } + + } + +} diff --git a/core/src/main/java/org/springframework/security/authorization/method/PreFilterAuthorizationMethodBeforeAdvice.java b/core/src/main/java/org/springframework/security/authorization/method/PreFilterAuthorizationMethodBeforeAdvice.java new file mode 100644 index 00000000000..93ab3621030 --- /dev/null +++ b/core/src/main/java/org/springframework/security/authorization/method/PreFilterAuthorizationMethodBeforeAdvice.java @@ -0,0 +1,151 @@ +/* + * 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.authorization.method; + +import java.lang.reflect.Method; +import java.util.function.Supplier; + +import org.aopalliance.intercept.MethodInvocation; + +import org.springframework.aop.MethodMatcher; +import org.springframework.aop.support.AopUtils; +import org.springframework.aop.support.StaticMethodMatcher; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.Expression; +import org.springframework.lang.NonNull; +import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler; +import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler; +import org.springframework.security.access.method.AuthorizationMethodBeforeAdvice; +import org.springframework.security.access.method.MethodAuthorizationContext; +import org.springframework.security.access.prepost.PreFilter; +import org.springframework.security.core.Authentication; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * An {@link AuthorizationMethodBeforeAdvice} which filters a method argument by + * evaluating an expression from the {@link PreFilter} annotation. + * + * @author Evgeniy Cheban + * @since 5.5 + */ +public final class PreFilterAuthorizationMethodBeforeAdvice + implements AuthorizationMethodBeforeAdvice { + + private final PreFilterExpressionAttributeRegistry registry = new PreFilterExpressionAttributeRegistry(); + + private final MethodMatcher methodMatcher = new StaticMethodMatcher() { + @Override + public boolean matches(Method method, Class targetClass) { + return PreFilterAuthorizationMethodBeforeAdvice.this.registry.getAttribute(method, + targetClass) != PreFilterExpressionAttribute.NULL_ATTRIBUTE; + } + }; + + private MethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler(); + + /** + * Sets the {@link MethodSecurityExpressionHandler}. + * @param expressionHandler the {@link MethodSecurityExpressionHandler} to use + */ + public void setExpressionHandler(MethodSecurityExpressionHandler expressionHandler) { + Assert.notNull(expressionHandler, "expressionHandler cannot be null"); + this.expressionHandler = expressionHandler; + } + + @Override + public MethodMatcher getMethodMatcher() { + return this.methodMatcher; + } + + /** + * Filters a method argument by evaluating an expression from the {@link PreFilter} + * annotation. + * @param authentication the {@link Supplier} of the {@link Authentication} to check + * @param methodAuthorizationContext the {@link MethodAuthorizationContext} to check + */ + @Override + public void before(Supplier authentication, MethodAuthorizationContext methodAuthorizationContext) { + PreFilterExpressionAttribute attribute = this.registry.getAttribute(methodAuthorizationContext); + if (attribute == PreFilterExpressionAttribute.NULL_ATTRIBUTE) { + return; + } + MethodInvocation mi = methodAuthorizationContext.getMethodInvocation(); + EvaluationContext ctx = this.expressionHandler.createEvaluationContext(authentication.get(), mi); + Object filterTarget = findFilterTarget(attribute.filterTarget, ctx, mi); + this.expressionHandler.filter(filterTarget, attribute.getExpression(), ctx); + } + + private Object findFilterTarget(String filterTargetName, EvaluationContext ctx, MethodInvocation methodInvocation) { + Object filterTarget; + if (StringUtils.hasText(filterTargetName)) { + filterTarget = ctx.lookupVariable(filterTargetName); + Assert.notNull(filterTarget, () -> "Filter target was null, or no argument with name '" + filterTargetName + + "' found in method."); + } + else { + Object[] arguments = methodInvocation.getArguments(); + Assert.state(arguments.length == 1, + "Unable to determine the method argument for filtering. Specify the filter target."); + filterTarget = arguments[0]; + Assert.notNull(filterTarget, + "Filter target was null. Make sure you passing the correct value in the method argument."); + } + Assert.state(!filterTarget.getClass().isArray(), + "Pre-filtering on array types is not supported. Using a Collection will solve this problem."); + return filterTarget; + } + + private final class PreFilterExpressionAttributeRegistry + extends AbstractExpressionAttributeRegistry { + + @NonNull + @Override + PreFilterExpressionAttribute resolveAttribute(Method method, Class targetClass) { + Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass); + PreFilter preFilter = findPreFilterAnnotation(specificMethod); + if (preFilter == null) { + return PreFilterExpressionAttribute.NULL_ATTRIBUTE; + } + Expression preFilterExpression = PreFilterAuthorizationMethodBeforeAdvice.this.expressionHandler + .getExpressionParser().parseExpression(preFilter.value()); + return new PreFilterExpressionAttribute(preFilterExpression, preFilter.filterTarget()); + } + + private PreFilter findPreFilterAnnotation(Method method) { + PreFilter preFilter = AnnotationUtils.findAnnotation(method, PreFilter.class); + return (preFilter != null) ? preFilter + : AnnotationUtils.findAnnotation(method.getDeclaringClass(), PreFilter.class); + } + + } + + private static final class PreFilterExpressionAttribute extends ExpressionAttribute { + + private static final PreFilterExpressionAttribute NULL_ATTRIBUTE = new PreFilterExpressionAttribute(null, null); + + private final String filterTarget; + + private PreFilterExpressionAttribute(Expression expression, String filterTarget) { + super(expression); + this.filterTarget = filterTarget; + } + + } + +} diff --git a/core/src/test/java/org/springframework/security/access/annotation/Jsr250AuthorizationManagerTests.java b/core/src/test/java/org/springframework/security/access/annotation/Jsr250AuthorizationManagerTests.java new file mode 100644 index 00000000000..a8cdf3273bb --- /dev/null +++ b/core/src/test/java/org/springframework/security/access/annotation/Jsr250AuthorizationManagerTests.java @@ -0,0 +1,167 @@ +/* + * 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.access.annotation; + +import java.util.function.Supplier; + +import javax.annotation.security.DenyAll; +import javax.annotation.security.PermitAll; +import javax.annotation.security.RolesAllowed; + +import org.junit.Test; + +import org.springframework.security.access.intercept.method.MockMethodInvocation; +import org.springframework.security.access.method.MethodAuthorizationContext; +import org.springframework.security.authentication.TestAuthentication; +import org.springframework.security.authentication.TestingAuthenticationToken; +import org.springframework.security.authorization.AuthorizationDecision; +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 Jsr250AuthorizationManager}. + * + * @author Evgeniy Cheban + */ +public class Jsr250AuthorizationManagerTests { + + @Test + public void rolePrefixWhenNotSetThenDefaultsToRole() { + Jsr250AuthorizationManager manager = new Jsr250AuthorizationManager(); + assertThat(manager).extracting("rolePrefix").isEqualTo("ROLE_"); + } + + @Test + public void setRolePrefixWhenNullThenException() { + Jsr250AuthorizationManager manager = new Jsr250AuthorizationManager(); + assertThatIllegalArgumentException().isThrownBy(() -> manager.setRolePrefix(null)) + .withMessage("rolePrefix cannot be null"); + } + + @Test + public void setRolePrefixWhenNotNullThenSets() { + Jsr250AuthorizationManager manager = new Jsr250AuthorizationManager(); + manager.setRolePrefix("CUSTOM_"); + assertThat(manager).extracting("rolePrefix").isEqualTo("CUSTOM_"); + } + + @Test + public void checkDoSomethingWhenNoJsr250AnnotationsThenNullDecision() throws Exception { + MockMethodInvocation methodInvocation = new MockMethodInvocation(new TestClass(), TestClass.class, + "doSomething"); + MethodAuthorizationContext methodAuthorizationContext = new MethodAuthorizationContext(methodInvocation, + TestClass.class); + Jsr250AuthorizationManager manager = new Jsr250AuthorizationManager(); + AuthorizationDecision decision = manager.check(TestAuthentication::authenticatedUser, + methodAuthorizationContext); + assertThat(decision).isNull(); + } + + @Test + public void checkPermitAllRolesAllowedAdminWhenRoleUserThenGrantedDecision() throws Exception { + MockMethodInvocation methodInvocation = new MockMethodInvocation(new TestClass(), TestClass.class, + "permitAllRolesAllowedAdmin"); + MethodAuthorizationContext methodAuthorizationContext = new MethodAuthorizationContext(methodInvocation, + TestClass.class); + Jsr250AuthorizationManager manager = new Jsr250AuthorizationManager(); + AuthorizationDecision decision = manager.check(TestAuthentication::authenticatedUser, + methodAuthorizationContext); + assertThat(decision).isNotNull(); + assertThat(decision.isGranted()).isTrue(); + } + + @Test + public void checkDenyAllRolesAllowedAdminWhenRoleAdminThenDeniedDecision() throws Exception { + MockMethodInvocation methodInvocation = new MockMethodInvocation(new TestClass(), TestClass.class, + "denyAllRolesAllowedAdmin"); + MethodAuthorizationContext methodAuthorizationContext = new MethodAuthorizationContext(methodInvocation, + TestClass.class); + Jsr250AuthorizationManager manager = new Jsr250AuthorizationManager(); + AuthorizationDecision decision = manager.check(TestAuthentication::authenticatedAdmin, + methodAuthorizationContext); + assertThat(decision).isNotNull(); + assertThat(decision.isGranted()).isFalse(); + } + + @Test + public void checkRolesAllowedUserOrAdminWhenRoleUserThenGrantedDecision() throws Exception { + MockMethodInvocation methodInvocation = new MockMethodInvocation(new TestClass(), TestClass.class, + "rolesAllowedUserOrAdmin"); + MethodAuthorizationContext methodAuthorizationContext = new MethodAuthorizationContext(methodInvocation, + TestClass.class); + Jsr250AuthorizationManager manager = new Jsr250AuthorizationManager(); + AuthorizationDecision decision = manager.check(TestAuthentication::authenticatedUser, + methodAuthorizationContext); + assertThat(decision).isNotNull(); + assertThat(decision.isGranted()).isTrue(); + } + + @Test + public void checkRolesAllowedUserOrAdminWhenRoleAdminThenGrantedDecision() throws Exception { + MockMethodInvocation methodInvocation = new MockMethodInvocation(new TestClass(), TestClass.class, + "rolesAllowedUserOrAdmin"); + MethodAuthorizationContext methodAuthorizationContext = new MethodAuthorizationContext(methodInvocation, + TestClass.class); + Jsr250AuthorizationManager manager = new Jsr250AuthorizationManager(); + AuthorizationDecision decision = manager.check(TestAuthentication::authenticatedAdmin, + methodAuthorizationContext); + assertThat(decision).isNotNull(); + assertThat(decision.isGranted()).isTrue(); + } + + @Test + public void checkRolesAllowedUserOrAdminWhenRoleAnonymousThenDeniedDecision() throws Exception { + Supplier authentication = () -> new TestingAuthenticationToken("user", "password", + "ROLE_ANONYMOUS"); + MockMethodInvocation methodInvocation = new MockMethodInvocation(new TestClass(), TestClass.class, + "rolesAllowedUserOrAdmin"); + MethodAuthorizationContext methodAuthorizationContext = new MethodAuthorizationContext(methodInvocation, + TestClass.class); + Jsr250AuthorizationManager manager = new Jsr250AuthorizationManager(); + AuthorizationDecision decision = manager.check(authentication, methodAuthorizationContext); + assertThat(decision).isNotNull(); + assertThat(decision.isGranted()).isFalse(); + } + + public static class TestClass { + + public void doSomething() { + + } + + @DenyAll + @RolesAllowed("ADMIN") + public void denyAllRolesAllowedAdmin() { + + } + + @PermitAll + @RolesAllowed("ADMIN") + public void permitAllRolesAllowedAdmin() { + + } + + @RolesAllowed({ "USER", "ADMIN" }) + public void rolesAllowedUserOrAdmin() { + + } + + } + +} diff --git a/core/src/test/java/org/springframework/security/access/annotation/SecuredAuthorizationManagerTests.java b/core/src/test/java/org/springframework/security/access/annotation/SecuredAuthorizationManagerTests.java new file mode 100644 index 00000000000..0d94919219b --- /dev/null +++ b/core/src/test/java/org/springframework/security/access/annotation/SecuredAuthorizationManagerTests.java @@ -0,0 +1,104 @@ +/* + * 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.access.annotation; + +import java.util.function.Supplier; + +import org.junit.Test; + +import org.springframework.security.access.intercept.method.MockMethodInvocation; +import org.springframework.security.access.method.MethodAuthorizationContext; +import org.springframework.security.authentication.TestAuthentication; +import org.springframework.security.authentication.TestingAuthenticationToken; +import org.springframework.security.authorization.AuthorizationDecision; +import org.springframework.security.core.Authentication; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link SecuredAuthorizationManager}. + * + * @author Evgeniy Cheban + */ +public class SecuredAuthorizationManagerTests { + + @Test + public void checkDoSomethingWhenNoSecuredAnnotationThenNullDecision() throws Exception { + MockMethodInvocation methodInvocation = new MockMethodInvocation(new TestClass(), TestClass.class, + "doSomething"); + MethodAuthorizationContext methodAuthorizationContext = new MethodAuthorizationContext(methodInvocation, + TestClass.class); + SecuredAuthorizationManager manager = new SecuredAuthorizationManager(); + AuthorizationDecision decision = manager.check(TestAuthentication::authenticatedUser, + methodAuthorizationContext); + assertThat(decision).isNull(); + } + + @Test + public void checkSecuredUserOrAdminWhenRoleUserThenGrantedDecision() throws Exception { + MockMethodInvocation methodInvocation = new MockMethodInvocation(new TestClass(), TestClass.class, + "securedUserOrAdmin"); + MethodAuthorizationContext methodAuthorizationContext = new MethodAuthorizationContext(methodInvocation, + TestClass.class); + SecuredAuthorizationManager manager = new SecuredAuthorizationManager(); + AuthorizationDecision decision = manager.check(TestAuthentication::authenticatedUser, + methodAuthorizationContext); + assertThat(decision).isNotNull(); + assertThat(decision.isGranted()).isTrue(); + } + + @Test + public void checkSecuredUserOrAdminWhenRoleAdminThenGrantedDecision() throws Exception { + MockMethodInvocation methodInvocation = new MockMethodInvocation(new TestClass(), TestClass.class, + "securedUserOrAdmin"); + MethodAuthorizationContext methodAuthorizationContext = new MethodAuthorizationContext(methodInvocation, + TestClass.class); + SecuredAuthorizationManager manager = new SecuredAuthorizationManager(); + AuthorizationDecision decision = manager.check(TestAuthentication::authenticatedAdmin, + methodAuthorizationContext); + assertThat(decision).isNotNull(); + assertThat(decision.isGranted()).isTrue(); + } + + @Test + public void checkSecuredUserOrAdminWhenRoleAnonymousThenDeniedDecision() throws Exception { + Supplier authentication = () -> new TestingAuthenticationToken("user", "password", + "ROLE_ANONYMOUS"); + MockMethodInvocation methodInvocation = new MockMethodInvocation(new TestClass(), TestClass.class, + "securedUserOrAdmin"); + MethodAuthorizationContext methodAuthorizationContext = new MethodAuthorizationContext(methodInvocation, + TestClass.class); + SecuredAuthorizationManager manager = new SecuredAuthorizationManager(); + AuthorizationDecision decision = manager.check(authentication, methodAuthorizationContext); + assertThat(decision).isNotNull(); + assertThat(decision.isGranted()).isFalse(); + } + + public static class TestClass { + + public void doSomething() { + + } + + @Secured({ "ROLE_USER", "ROLE_ADMIN" }) + public void securedUserOrAdmin() { + + } + + } + +} diff --git a/core/src/test/java/org/springframework/security/access/intercept/aopalliance/AuthorizationMethodInterceptorTests.java b/core/src/test/java/org/springframework/security/access/intercept/aopalliance/AuthorizationMethodInterceptorTests.java new file mode 100644 index 00000000000..3f647cc9bf7 --- /dev/null +++ b/core/src/test/java/org/springframework/security/access/intercept/aopalliance/AuthorizationMethodInterceptorTests.java @@ -0,0 +1,107 @@ +/* + * 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.access.intercept.aopalliance; + +import java.util.function.Supplier; + +import org.junit.After; +import org.junit.Test; + +import org.springframework.aop.MethodMatcher; +import org.springframework.security.access.intercept.method.MockMethodInvocation; +import org.springframework.security.access.method.AuthorizationMethodAfterAdvice; +import org.springframework.security.access.method.AuthorizationMethodBeforeAdvice; +import org.springframework.security.access.method.MethodAuthorizationContext; +import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException; +import org.springframework.security.authentication.TestAuthentication; +import org.springframework.security.core.Authentication; +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.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; + +/** + * Tests for {@link AuthorizationMethodInterceptor}. + * + * @author Evgeniy Cheban + */ +public class AuthorizationMethodInterceptorTests { + + @After + public void tearDown() { + SecurityContextHolder.clearContext(); + } + + @Test + public void invokeWhenAuthenticatedThenVerifyAdvicesUsage() throws Throwable { + Authentication authentication = TestAuthentication.authenticatedUser(); + SecurityContextHolder.setContext(new SecurityContextImpl(authentication)); + MockMethodInvocation mockMethodInvocation = new MockMethodInvocation(new TestClass(), TestClass.class, + "doSomethingString"); + AuthorizationMethodBeforeAdvice mockBeforeAdvice = mock( + AuthorizationMethodBeforeAdvice.class); + AuthorizationMethodAfterAdvice mockAfterAdvice = mock( + AuthorizationMethodAfterAdvice.class); + given(mockAfterAdvice.after(any(), any(MethodAuthorizationContext.class), eq(null))).willReturn("abc"); + AuthorizationMethodInterceptor interceptor = new AuthorizationMethodInterceptor(mockBeforeAdvice, + mockAfterAdvice); + Object result = interceptor.invoke(mockMethodInvocation); + assertThat(result).isEqualTo("abc"); + verify(mockAfterAdvice).after(any(), any(MethodAuthorizationContext.class), eq(null)); + } + + @Test + public void invokeWhenNotAuthenticatedThenAuthenticationCredentialsNotFoundException() throws Exception { + MockMethodInvocation mockMethodInvocation = new MockMethodInvocation(new TestClass(), TestClass.class, + "doSomethingString"); + AuthorizationMethodBeforeAdvice beforeAdvice = new AuthorizationMethodBeforeAdvice() { + @Override + public MethodMatcher getMethodMatcher() { + return MethodMatcher.TRUE; + } + + @Override + public void before(Supplier authentication, + MethodAuthorizationContext methodAuthorizationContext) { + authentication.get(); + } + }; + AuthorizationMethodAfterAdvice mockAfterAdvice = mock( + AuthorizationMethodAfterAdvice.class); + AuthorizationMethodInterceptor interceptor = new AuthorizationMethodInterceptor(beforeAdvice, mockAfterAdvice); + assertThatExceptionOfType(AuthenticationCredentialsNotFoundException.class) + .isThrownBy(() -> interceptor.invoke(mockMethodInvocation)) + .withMessage("An Authentication object was not found in the SecurityContext"); + verifyNoInteractions(mockAfterAdvice); + } + + public static class TestClass { + + public String doSomethingString() { + return null; + } + + } + +} diff --git a/core/src/test/java/org/springframework/security/access/method/AuthorizationManagerMethodAfterAdviceTests.java b/core/src/test/java/org/springframework/security/access/method/AuthorizationManagerMethodAfterAdviceTests.java new file mode 100644 index 00000000000..f2c8a96ea52 --- /dev/null +++ b/core/src/test/java/org/springframework/security/access/method/AuthorizationManagerMethodAfterAdviceTests.java @@ -0,0 +1,68 @@ +/* + * 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.access.method; + +import java.util.function.Supplier; + +import org.aopalliance.intercept.MethodInvocation; +import org.junit.Test; + +import org.springframework.aop.MethodMatcher; +import org.springframework.security.authentication.TestAuthentication; +import org.springframework.security.authorization.AuthorizationManager; +import org.springframework.security.core.Authentication; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +/** + * Tests for {@link AuthorizationManagerMethodAfterAdvice}. + * + * @author Evgeniy Cheban + */ +public class AuthorizationManagerMethodAfterAdviceTests { + + @Test + public void instantiateWhenMethodMatcherNullThenException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> new AuthorizationManagerMethodAfterAdvice<>(null, mock(AuthorizationManager.class))) + .withMessage("methodMatcher cannot be null"); + } + + @Test + public void instantiateWhenAuthorizationManagerNullThenException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> new AuthorizationManagerMethodAfterAdvice<>(mock(MethodMatcher.class), null)) + .withMessage("authorizationManager cannot be null"); + } + + @Test + public void beforeWhenMockAuthorizationManagerThenVerifyAndReturnedObject() { + Supplier authentication = TestAuthentication::authenticatedUser; + MethodInvocation mockMethodInvocation = mock(MethodInvocation.class); + Object returnedObject = new Object(); + AuthorizationManager mockAuthorizationManager = mock(AuthorizationManager.class); + AuthorizationManagerMethodAfterAdvice advice = new AuthorizationManagerMethodAfterAdvice<>( + mock(MethodMatcher.class), mockAuthorizationManager); + Object result = advice.after(authentication, mockMethodInvocation, returnedObject); + assertThat(result).isEqualTo(returnedObject); + verify(mockAuthorizationManager).verify(authentication, mockMethodInvocation); + } + +} diff --git a/core/src/test/java/org/springframework/security/access/method/AuthorizationManagerMethodBeforeAdviceTests.java b/core/src/test/java/org/springframework/security/access/method/AuthorizationManagerMethodBeforeAdviceTests.java new file mode 100644 index 00000000000..72da6d2d8fa --- /dev/null +++ b/core/src/test/java/org/springframework/security/access/method/AuthorizationManagerMethodBeforeAdviceTests.java @@ -0,0 +1,65 @@ +/* + * 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.access.method; + +import java.util.function.Supplier; + +import org.aopalliance.intercept.MethodInvocation; +import org.junit.Test; + +import org.springframework.aop.MethodMatcher; +import org.springframework.security.authentication.TestAuthentication; +import org.springframework.security.authorization.AuthorizationManager; +import org.springframework.security.core.Authentication; + +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +/** + * Tests for {@link AuthorizationManagerMethodBeforeAdvice}. + * + * @author Evgeniy Cheban + */ +public class AuthorizationManagerMethodBeforeAdviceTests { + + @Test + public void instantiateWhenMethodMatcherNullThenException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> new AuthorizationManagerMethodBeforeAdvice<>(null, mock(AuthorizationManager.class))) + .withMessage("methodMatcher cannot be null"); + } + + @Test + public void instantiateWhenAuthorizationManagerNullThenException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> new AuthorizationManagerMethodBeforeAdvice<>(mock(MethodMatcher.class), null)) + .withMessage("authorizationManager cannot be null"); + } + + @Test + public void beforeWhenMockAuthorizationManagerThenVerify() { + Supplier authentication = TestAuthentication::authenticatedUser; + MethodInvocation mockMethodInvocation = mock(MethodInvocation.class); + AuthorizationManager mockAuthorizationManager = mock(AuthorizationManager.class); + AuthorizationManagerMethodBeforeAdvice advice = new AuthorizationManagerMethodBeforeAdvice<>( + mock(MethodMatcher.class), mockAuthorizationManager); + advice.before(authentication, mockMethodInvocation); + verify(mockAuthorizationManager).verify(authentication, mockMethodInvocation); + } + +} diff --git a/core/src/test/java/org/springframework/security/access/method/DelegatingAuthorizationMethodAfterAdviceTests.java b/core/src/test/java/org/springframework/security/access/method/DelegatingAuthorizationMethodAfterAdviceTests.java new file mode 100644 index 00000000000..79bb042457d --- /dev/null +++ b/core/src/test/java/org/springframework/security/access/method/DelegatingAuthorizationMethodAfterAdviceTests.java @@ -0,0 +1,164 @@ +/* + * 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.access.method; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Supplier; + +import org.junit.Test; + +import org.springframework.aop.MethodMatcher; +import org.springframework.aop.support.StaticMethodMatcher; +import org.springframework.security.access.intercept.method.MockMethodInvocation; +import org.springframework.security.authentication.TestAuthentication; +import org.springframework.security.core.Authentication; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link DelegatingAuthorizationMethodAfterAdvice}. + * + * @author Evgeniy Cheban + */ +public class DelegatingAuthorizationMethodAfterAdviceTests { + + @Test + public void methodMatcherWhenNoneMatchesThenNotMatches() throws Exception { + List> delegates = new ArrayList<>(); + delegates.add(new AuthorizationMethodAfterAdvice() { + @Override + public Object after(Supplier authentication, MethodAuthorizationContext object, + Object returnedObject) { + return returnedObject; + } + + @Override + public MethodMatcher getMethodMatcher() { + return new StaticMethodMatcher() { + @Override + public boolean matches(Method method, Class targetClass) { + return false; + } + }; + } + }); + delegates.add(new AuthorizationMethodAfterAdvice() { + @Override + public Object after(Supplier authentication, MethodAuthorizationContext object, + Object returnedObject) { + return returnedObject; + } + + @Override + public MethodMatcher getMethodMatcher() { + return new StaticMethodMatcher() { + @Override + public boolean matches(Method method, Class targetClass) { + return false; + } + }; + } + }); + DelegatingAuthorizationMethodAfterAdvice advice = new DelegatingAuthorizationMethodAfterAdvice(delegates); + MethodMatcher methodMatcher = advice.getMethodMatcher(); + assertThat(methodMatcher.matches(TestClass.class.getMethod("doSomething"), TestClass.class)).isFalse(); + } + + @Test + public void methodMatcherWhenAnyMatchesThenMatches() throws Exception { + List> delegates = new ArrayList<>(); + delegates.add(new AuthorizationMethodAfterAdvice() { + @Override + public Object after(Supplier authentication, MethodAuthorizationContext object, + Object returnedObject) { + return returnedObject; + } + + @Override + public MethodMatcher getMethodMatcher() { + return new StaticMethodMatcher() { + @Override + public boolean matches(Method method, Class targetClass) { + return false; + } + }; + } + }); + delegates.add(new AuthorizationMethodAfterAdvice() { + @Override + public Object after(Supplier authentication, MethodAuthorizationContext object, + Object returnedObject) { + return returnedObject; + } + + @Override + public MethodMatcher getMethodMatcher() { + return MethodMatcher.TRUE; + } + }); + DelegatingAuthorizationMethodAfterAdvice advice = new DelegatingAuthorizationMethodAfterAdvice(delegates); + MethodMatcher methodMatcher = advice.getMethodMatcher(); + assertThat(methodMatcher.matches(TestClass.class.getMethod("doSomething"), TestClass.class)).isTrue(); + } + + @Test + public void checkWhenDelegatingAdviceModifiesReturnedObjectThenModifiedReturnedObject() throws Exception { + List> delegates = new ArrayList<>(); + delegates.add(new AuthorizationMethodAfterAdvice() { + @Override + public Object after(Supplier authentication, MethodAuthorizationContext object, + Object returnedObject) { + return returnedObject + "b"; + } + + @Override + public MethodMatcher getMethodMatcher() { + return MethodMatcher.TRUE; + } + }); + delegates.add(new AuthorizationMethodAfterAdvice() { + @Override + public Object after(Supplier authentication, MethodAuthorizationContext object, + Object returnedObject) { + return returnedObject + "c"; + } + + @Override + public MethodMatcher getMethodMatcher() { + return MethodMatcher.TRUE; + } + }); + MockMethodInvocation mockMethodInvocation = new MockMethodInvocation(new TestClass(), TestClass.class, + "doSomething"); + MethodAuthorizationContext methodAuthorizationContext = new MethodAuthorizationContext(mockMethodInvocation, + TestClass.class); + DelegatingAuthorizationMethodAfterAdvice advice = new DelegatingAuthorizationMethodAfterAdvice(delegates); + Object result = advice.after(TestAuthentication::authenticatedUser, methodAuthorizationContext, "a"); + assertThat(result).isEqualTo("abc"); + } + + public static class TestClass { + + public String doSomething() { + return null; + } + + } + +} diff --git a/core/src/test/java/org/springframework/security/access/method/DelegatingAuthorizationMethodBeforeAdviceTests.java b/core/src/test/java/org/springframework/security/access/method/DelegatingAuthorizationMethodBeforeAdviceTests.java new file mode 100644 index 00000000000..5e82783f7e2 --- /dev/null +++ b/core/src/test/java/org/springframework/security/access/method/DelegatingAuthorizationMethodBeforeAdviceTests.java @@ -0,0 +1,168 @@ +/* + * 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.access.method; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.function.Supplier; + +import org.junit.Test; + +import org.springframework.aop.MethodMatcher; +import org.springframework.aop.support.StaticMethodMatcher; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.access.intercept.method.MockMethodInvocation; +import org.springframework.security.authentication.TestAuthentication; +import org.springframework.security.authorization.AuthorizationDecision; +import org.springframework.security.core.Authentication; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Tests for {@link DelegatingAuthorizationMethodBeforeAdvice}. + * + * @author Evgeniy Cheban + */ +public class DelegatingAuthorizationMethodBeforeAdviceTests { + + @Test + public void methodMatcherWhenNoneMatchesThenNotMatches() throws Exception { + List> delegates = new ArrayList<>(); + delegates.add(new AuthorizationMethodBeforeAdvice() { + @Override + public MethodMatcher getMethodMatcher() { + return new StaticMethodMatcher() { + @Override + public boolean matches(Method method, Class targetClass) { + return false; + } + }; + } + + @Override + public void before(Supplier authentication, MethodAuthorizationContext object) { + } + }); + delegates.add(new AuthorizationMethodBeforeAdvice() { + @Override + public MethodMatcher getMethodMatcher() { + return new StaticMethodMatcher() { + @Override + public boolean matches(Method method, Class targetClass) { + return false; + } + }; + } + + @Override + public void before(Supplier authentication, MethodAuthorizationContext object) { + } + }); + DelegatingAuthorizationMethodBeforeAdvice advice = new DelegatingAuthorizationMethodBeforeAdvice(delegates); + MethodMatcher methodMatcher = advice.getMethodMatcher(); + assertThat(methodMatcher.matches(TestClass.class.getMethod("doSomething"), TestClass.class)).isFalse(); + } + + @Test + public void methodMatcherWhenAnyMatchesThenMatches() throws Exception { + List> delegates = new ArrayList<>(); + delegates.add(new AuthorizationMethodBeforeAdvice() { + @Override + public MethodMatcher getMethodMatcher() { + return new StaticMethodMatcher() { + @Override + public boolean matches(Method method, Class targetClass) { + return false; + } + }; + } + + @Override + public void before(Supplier authentication, MethodAuthorizationContext object) { + } + }); + delegates.add(new AuthorizationMethodBeforeAdvice() { + @Override + public MethodMatcher getMethodMatcher() { + return MethodMatcher.TRUE; + } + + @Override + public void before(Supplier authentication, MethodAuthorizationContext object) { + } + }); + DelegatingAuthorizationMethodBeforeAdvice advice = new DelegatingAuthorizationMethodBeforeAdvice(delegates); + MethodMatcher methodMatcher = advice.getMethodMatcher(); + assertThat(methodMatcher.matches(TestClass.class.getMethod("doSomething"), TestClass.class)).isTrue(); + } + + @Test + public void checkWhenAllGrantsOrAbstainsThenPasses() throws Exception { + List> delegates = new ArrayList<>(); + delegates.add(new AuthorizationManagerMethodBeforeAdvice<>(MethodMatcher.TRUE, (a, o) -> null)); + delegates.add(new AuthorizationManagerMethodBeforeAdvice<>(MethodMatcher.TRUE, + (a, o) -> new AuthorizationDecision(true))); + delegates.add(new AuthorizationManagerMethodBeforeAdvice<>(MethodMatcher.TRUE, (a, o) -> null)); + DelegatingAuthorizationMethodBeforeAdvice advice = new DelegatingAuthorizationMethodBeforeAdvice(delegates); + MockMethodInvocation mockMethodInvocation = new MockMethodInvocation(new TestClass(), TestClass.class, + "doSomething"); + MethodAuthorizationContext methodAuthorizationContext = new MethodAuthorizationContext(mockMethodInvocation, + TestClass.class); + advice.before(TestAuthentication::authenticatedUser, methodAuthorizationContext); + } + + @Test + public void checkWhenAnyDeniesThenAccessDeniedException() throws Exception { + List> delegates = new ArrayList<>(); + delegates.add(new AuthorizationManagerMethodBeforeAdvice<>(MethodMatcher.TRUE, (a, o) -> null)); + delegates.add(new AuthorizationManagerMethodBeforeAdvice<>(MethodMatcher.TRUE, + (a, o) -> new AuthorizationDecision(true))); + delegates.add(new AuthorizationManagerMethodBeforeAdvice<>(MethodMatcher.TRUE, + (a, o) -> new AuthorizationDecision(false))); + DelegatingAuthorizationMethodBeforeAdvice advice = new DelegatingAuthorizationMethodBeforeAdvice(delegates); + MockMethodInvocation mockMethodInvocation = new MockMethodInvocation(new TestClass(), TestClass.class, + "doSomething"); + MethodAuthorizationContext methodAuthorizationContext = new MethodAuthorizationContext(mockMethodInvocation, + TestClass.class); + assertThatExceptionOfType(AccessDeniedException.class) + .isThrownBy(() -> advice.before(TestAuthentication::authenticatedUser, methodAuthorizationContext)) + .withMessage("Access Denied"); + } + + @Test + public void checkWhenDelegatesEmptyThenPasses() throws Exception { + DelegatingAuthorizationMethodBeforeAdvice advice = new DelegatingAuthorizationMethodBeforeAdvice( + Collections.emptyList()); + MockMethodInvocation mockMethodInvocation = new MockMethodInvocation(new TestClass(), TestClass.class, + "doSomething"); + MethodAuthorizationContext methodAuthorizationContext = new MethodAuthorizationContext(mockMethodInvocation, + TestClass.class); + advice.before(TestAuthentication::authenticatedUser, methodAuthorizationContext); + } + + public static class TestClass { + + public void doSomething() { + + } + + } + +} diff --git a/core/src/test/java/org/springframework/security/authorization/AuthorityAuthorizationManagerTests.java b/core/src/test/java/org/springframework/security/authorization/AuthorityAuthorizationManagerTests.java index ab0c41563c7..5c092d54d43 100644 --- a/core/src/test/java/org/springframework/security/authorization/AuthorityAuthorizationManagerTests.java +++ b/core/src/test/java/org/springframework/security/authorization/AuthorityAuthorizationManagerTests.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. @@ -64,6 +64,13 @@ public void hasAnyRoleWhenContainNullThenException() { .withMessage("roles cannot contain null values"); } + @Test + public void hasAnyRoleWhenCustomRolePrefixNullThenException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> AuthorityAuthorizationManager.hasAnyRole(null, new String[] { "ADMIN", "USER" })) + .withMessage("rolePrefix cannot be null"); + } + @Test public void hasAnyAuthorityWhenNullThenException() { assertThatIllegalArgumentException().isThrownBy(() -> AuthorityAuthorizationManager.hasAnyAuthority(null)) @@ -147,6 +154,17 @@ public void hasAnyRoleWhenUserHasNotAnyRoleThenDeniedDecision() { assertThat(manager.check(authentication, object).isGranted()).isFalse(); } + @Test + public void hasAnyRoleWhenCustomRolePrefixProvidedThenUseCustomRolePrefix() { + AuthorityAuthorizationManager manager = AuthorityAuthorizationManager.hasAnyRole("CUSTOM_", + new String[] { "USER" }); + Supplier authentication = () -> new TestingAuthenticationToken("user", "password", + "CUSTOM_USER"); + Object object = new Object(); + + assertThat(manager.check(authentication, object).isGranted()).isTrue(); + } + @Test public void hasAnyAuthorityWhenUserHasAnyAuthorityThenGrantedDecision() { AuthorityAuthorizationManager manager = AuthorityAuthorizationManager.hasAnyAuthority("ADMIN", "USER"); diff --git a/core/src/test/java/org/springframework/security/authorization/method/PostAuthorizeAuthorizationManagerTests.java b/core/src/test/java/org/springframework/security/authorization/method/PostAuthorizeAuthorizationManagerTests.java new file mode 100644 index 00000000000..62da757d15e --- /dev/null +++ b/core/src/test/java/org/springframework/security/authorization/method/PostAuthorizeAuthorizationManagerTests.java @@ -0,0 +1,144 @@ +/* + * 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.authorization.method; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.junit.Test; + +import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler; +import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler; +import org.springframework.security.access.intercept.method.MockMethodInvocation; +import org.springframework.security.access.method.MethodAuthorizationContext; +import org.springframework.security.access.prepost.PostAuthorize; +import org.springframework.security.authentication.TestAuthentication; +import org.springframework.security.authorization.AuthorizationDecision; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link PostAuthorizeAuthorizationManager}. + * + * @author Evgeniy Cheban + */ +public class PostAuthorizeAuthorizationManagerTests { + + @Test + public void setExpressionHandlerWhenNotNullThenSetsExpressionHandler() { + MethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler(); + PostAuthorizeAuthorizationManager manager = new PostAuthorizeAuthorizationManager(); + manager.setExpressionHandler(expressionHandler); + assertThat(manager).extracting("expressionHandler").isEqualTo(expressionHandler); + } + + @Test + public void setExpressionHandlerWhenNullThenException() { + PostAuthorizeAuthorizationManager manager = new PostAuthorizeAuthorizationManager(); + assertThatIllegalArgumentException().isThrownBy(() -> manager.setExpressionHandler(null)) + .withMessage("expressionHandler cannot be null"); + } + + @Test + public void checkDoSomethingWhenNoPostAuthorizeAnnotationThenNullDecision() throws Exception { + MockMethodInvocation mockMethodInvocation = new MockMethodInvocation(new TestClass(), TestClass.class, + "doSomething", new Class[] {}, new Object[] {}); + MethodAuthorizationContext methodAuthorizationContext = new MethodAuthorizationContext(mockMethodInvocation, + TestClass.class); + PostAuthorizeAuthorizationManager manager = new PostAuthorizeAuthorizationManager(); + AuthorizationDecision decision = manager.check(TestAuthentication::authenticatedUser, + methodAuthorizationContext); + assertThat(decision).isNull(); + } + + @Test + public void checkDoSomethingStringWhenArgIsGrantThenGrantedDecision() throws Exception { + MockMethodInvocation mockMethodInvocation = new MockMethodInvocation(new TestClass(), TestClass.class, + "doSomethingString", new Class[] { String.class }, new Object[] { "grant" }); + MethodAuthorizationContext methodAuthorizationContext = new MethodAuthorizationContext(mockMethodInvocation, + TestClass.class); + PostAuthorizeAuthorizationManager manager = new PostAuthorizeAuthorizationManager(); + AuthorizationDecision decision = manager.check(TestAuthentication::authenticatedUser, + methodAuthorizationContext); + assertThat(decision).isNotNull(); + assertThat(decision.isGranted()).isTrue(); + } + + @Test + public void checkDoSomethingStringWhenArgIsNotGrantThenDeniedDecision() throws Exception { + MockMethodInvocation mockMethodInvocation = new MockMethodInvocation(new TestClass(), TestClass.class, + "doSomethingString", new Class[] { String.class }, new Object[] { "deny" }); + MethodAuthorizationContext methodAuthorizationContext = new MethodAuthorizationContext(mockMethodInvocation, + TestClass.class); + PostAuthorizeAuthorizationManager manager = new PostAuthorizeAuthorizationManager(); + AuthorizationDecision decision = manager.check(TestAuthentication::authenticatedUser, + methodAuthorizationContext); + assertThat(decision).isNotNull(); + assertThat(decision.isGranted()).isFalse(); + } + + @Test + public void checkDoSomethingListWhenReturnObjectContainsGrantThenGrantedDecision() throws Exception { + List list = Arrays.asList("grant", "deny"); + MockMethodInvocation mockMethodInvocation = new MockMethodInvocation(new TestClass(), TestClass.class, + "doSomethingList", new Class[] { List.class }, new Object[] { list }); + MethodAuthorizationContext methodAuthorizationContext = new MethodAuthorizationContext(mockMethodInvocation, + TestClass.class); + methodAuthorizationContext.setReturnObject(list); + PostAuthorizeAuthorizationManager manager = new PostAuthorizeAuthorizationManager(); + AuthorizationDecision decision = manager.check(TestAuthentication::authenticatedUser, + methodAuthorizationContext); + assertThat(decision).isNotNull(); + assertThat(decision.isGranted()).isTrue(); + } + + @Test + public void checkDoSomethingListWhenReturnObjectNotContainsGrantThenDeniedDecision() throws Exception { + List list = Collections.singletonList("deny"); + MockMethodInvocation mockMethodInvocation = new MockMethodInvocation(new TestClass(), TestClass.class, + "doSomethingList", new Class[] { List.class }, new Object[] { list }); + MethodAuthorizationContext methodAuthorizationContext = new MethodAuthorizationContext(mockMethodInvocation, + TestClass.class); + methodAuthorizationContext.setReturnObject(list); + PostAuthorizeAuthorizationManager manager = new PostAuthorizeAuthorizationManager(); + AuthorizationDecision decision = manager.check(TestAuthentication::authenticatedUser, + methodAuthorizationContext); + assertThat(decision).isNotNull(); + assertThat(decision.isGranted()).isFalse(); + } + + public static class TestClass { + + public void doSomething() { + + } + + @PostAuthorize("#s == 'grant'") + public String doSomethingString(String s) { + return s; + } + + @PostAuthorize("returnObject.contains('grant')") + public List doSomethingList(List list) { + return list; + } + + } + +} diff --git a/core/src/test/java/org/springframework/security/authorization/method/PostFilterAuthorizationMethodAfterAdviceTests.java b/core/src/test/java/org/springframework/security/authorization/method/PostFilterAuthorizationMethodAfterAdviceTests.java new file mode 100644 index 00000000000..32e5784fe3f --- /dev/null +++ b/core/src/test/java/org/springframework/security/authorization/method/PostFilterAuthorizationMethodAfterAdviceTests.java @@ -0,0 +1,96 @@ +/* + * 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.authorization.method; + +import org.assertj.core.api.InstanceOfAssertFactories; +import org.junit.Test; + +import org.springframework.aop.MethodMatcher; +import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler; +import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler; +import org.springframework.security.access.intercept.method.MockMethodInvocation; +import org.springframework.security.access.method.MethodAuthorizationContext; +import org.springframework.security.access.prepost.PostFilter; +import org.springframework.security.authentication.TestAuthentication; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link PostFilterAuthorizationMethodAfterAdvice}. + * + * @author Evgeniy Cheban + */ +public class PostFilterAuthorizationMethodAfterAdviceTests { + + @Test + public void setExpressionHandlerWhenNotNullThenSetsExpressionHandler() { + MethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler(); + PostFilterAuthorizationMethodAfterAdvice advice = new PostFilterAuthorizationMethodAfterAdvice(); + advice.setExpressionHandler(expressionHandler); + assertThat(advice).extracting("expressionHandler").isEqualTo(expressionHandler); + } + + @Test + public void setExpressionHandlerWhenNullThenException() { + PostFilterAuthorizationMethodAfterAdvice advice = new PostFilterAuthorizationMethodAfterAdvice(); + assertThatIllegalArgumentException().isThrownBy(() -> advice.setExpressionHandler(null)) + .withMessage("expressionHandler cannot be null"); + } + + @Test + public void methodMatcherWhenMethodHasNotPostFilterAnnotationThenNotMatches() throws Exception { + PostFilterAuthorizationMethodAfterAdvice advice = new PostFilterAuthorizationMethodAfterAdvice(); + MethodMatcher methodMatcher = advice.getMethodMatcher(); + assertThat(methodMatcher.matches(TestClass.class.getMethod("doSomething"), TestClass.class)).isFalse(); + } + + @Test + public void methodMatcherWhenMethodHasPostFilterAnnotationThenMatches() throws Exception { + PostFilterAuthorizationMethodAfterAdvice advice = new PostFilterAuthorizationMethodAfterAdvice(); + MethodMatcher methodMatcher = advice.getMethodMatcher(); + assertThat( + methodMatcher.matches(TestClass.class.getMethod("doSomethingArray", String[].class), TestClass.class)) + .isTrue(); + } + + @Test + public void afterWhenArrayNotNullThenFilteredArray() throws Exception { + String[] array = { "john", "bob" }; + MockMethodInvocation mockMethodInvocation = new MockMethodInvocation(new TestClass(), TestClass.class, + "doSomethingArray", new Class[] { String[].class }, new Object[] { array }); + MethodAuthorizationContext methodAuthorizationContext = new MethodAuthorizationContext(mockMethodInvocation, + TestClass.class); + PostFilterAuthorizationMethodAfterAdvice advice = new PostFilterAuthorizationMethodAfterAdvice(); + Object result = advice.after(TestAuthentication::authenticatedUser, methodAuthorizationContext, array); + assertThat(result).asInstanceOf(InstanceOfAssertFactories.array(String[].class)).containsOnly("john"); + } + + public static class TestClass { + + public void doSomething() { + + } + + @PostFilter("filterObject == 'john'") + public String[] doSomethingArray(String[] array) { + return array; + } + + } + +} diff --git a/core/src/test/java/org/springframework/security/authorization/method/PreAuthorizeAuthorizationManagerTests.java b/core/src/test/java/org/springframework/security/authorization/method/PreAuthorizeAuthorizationManagerTests.java new file mode 100644 index 00000000000..ebabf1c5e36 --- /dev/null +++ b/core/src/test/java/org/springframework/security/authorization/method/PreAuthorizeAuthorizationManagerTests.java @@ -0,0 +1,105 @@ +/* + * 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.authorization.method; + +import org.junit.Test; + +import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler; +import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler; +import org.springframework.security.access.intercept.method.MockMethodInvocation; +import org.springframework.security.access.method.MethodAuthorizationContext; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.authentication.TestAuthentication; +import org.springframework.security.authorization.AuthorizationDecision; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link PreAuthorizeAuthorizationManager}. + * + * @author Evgeniy Cheban + */ +public class PreAuthorizeAuthorizationManagerTests { + + @Test + public void setExpressionHandlerWhenNotNullThenSetsExpressionHandler() { + MethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler(); + PreAuthorizeAuthorizationManager manager = new PreAuthorizeAuthorizationManager(); + manager.setExpressionHandler(expressionHandler); + assertThat(manager).extracting("expressionHandler").isEqualTo(expressionHandler); + } + + @Test + public void setExpressionHandlerWhenNullThenException() { + PreAuthorizeAuthorizationManager manager = new PreAuthorizeAuthorizationManager(); + assertThatIllegalArgumentException().isThrownBy(() -> manager.setExpressionHandler(null)) + .withMessage("expressionHandler cannot be null"); + } + + @Test + public void checkDoSomethingWhenNoPostAuthorizeAnnotationThenNullDecision() throws Exception { + MockMethodInvocation mockMethodInvocation = new MockMethodInvocation(new TestClass(), TestClass.class, + "doSomething", new Class[] {}, new Object[] {}); + MethodAuthorizationContext methodAuthorizationContext = new MethodAuthorizationContext(mockMethodInvocation, + TestClass.class); + PreAuthorizeAuthorizationManager manager = new PreAuthorizeAuthorizationManager(); + AuthorizationDecision decision = manager.check(TestAuthentication::authenticatedUser, + methodAuthorizationContext); + assertThat(decision).isNull(); + } + + @Test + public void checkDoSomethingStringWhenArgIsGrantThenGrantedDecision() throws Exception { + MockMethodInvocation mockMethodInvocation = new MockMethodInvocation(new TestClass(), TestClass.class, + "doSomethingString", new Class[] { String.class }, new Object[] { "grant" }); + MethodAuthorizationContext methodAuthorizationContext = new MethodAuthorizationContext(mockMethodInvocation, + TestClass.class); + PreAuthorizeAuthorizationManager manager = new PreAuthorizeAuthorizationManager(); + AuthorizationDecision decision = manager.check(TestAuthentication::authenticatedUser, + methodAuthorizationContext); + assertThat(decision).isNotNull(); + assertThat(decision.isGranted()).isTrue(); + } + + @Test + public void checkDoSomethingStringWhenArgIsNotGrantThenDeniedDecision() throws Exception { + MockMethodInvocation mockMethodInvocation = new MockMethodInvocation(new TestClass(), TestClass.class, + "doSomethingString", new Class[] { String.class }, new Object[] { "deny" }); + MethodAuthorizationContext methodAuthorizationContext = new MethodAuthorizationContext(mockMethodInvocation, + TestClass.class); + PreAuthorizeAuthorizationManager manager = new PreAuthorizeAuthorizationManager(); + AuthorizationDecision decision = manager.check(TestAuthentication::authenticatedUser, + methodAuthorizationContext); + assertThat(decision).isNotNull(); + assertThat(decision.isGranted()).isFalse(); + } + + public static class TestClass { + + public void doSomething() { + + } + + @PreAuthorize("#s == 'grant'") + public String doSomethingString(String s) { + return s; + } + + } + +} diff --git a/core/src/test/java/org/springframework/security/authorization/method/PreFilterAuthorizationMethodBeforeAdviceTests.java b/core/src/test/java/org/springframework/security/authorization/method/PreFilterAuthorizationMethodBeforeAdviceTests.java new file mode 100644 index 00000000000..c38b4b165a8 --- /dev/null +++ b/core/src/test/java/org/springframework/security/authorization/method/PreFilterAuthorizationMethodBeforeAdviceTests.java @@ -0,0 +1,200 @@ +/* + * 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.authorization.method; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.Test; + +import org.springframework.aop.MethodMatcher; +import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler; +import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler; +import org.springframework.security.access.intercept.method.MockMethodInvocation; +import org.springframework.security.access.method.MethodAuthorizationContext; +import org.springframework.security.access.prepost.PreFilter; +import org.springframework.security.authentication.TestAuthentication; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + +/** + * Tests for {@link PreFilterAuthorizationMethodBeforeAdvice}. + * + * @author Evgeniy Cheban + */ +public class PreFilterAuthorizationMethodBeforeAdviceTests { + + @Test + public void setExpressionHandlerWhenNotNullThenSetsExpressionHandler() { + MethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler(); + PreFilterAuthorizationMethodBeforeAdvice advice = new PreFilterAuthorizationMethodBeforeAdvice(); + advice.setExpressionHandler(expressionHandler); + assertThat(advice).extracting("expressionHandler").isEqualTo(expressionHandler); + } + + @Test + public void setExpressionHandlerWhenNullThenException() { + PreFilterAuthorizationMethodBeforeAdvice advice = new PreFilterAuthorizationMethodBeforeAdvice(); + assertThatIllegalArgumentException().isThrownBy(() -> advice.setExpressionHandler(null)) + .withMessage("expressionHandler cannot be null"); + } + + @Test + public void methodMatcherWhenMethodHasNotPreFilterAnnotationThenNotMatches() throws Exception { + PreFilterAuthorizationMethodBeforeAdvice advice = new PreFilterAuthorizationMethodBeforeAdvice(); + MethodMatcher methodMatcher = advice.getMethodMatcher(); + assertThat(methodMatcher.matches(TestClass.class.getMethod("doSomething"), TestClass.class)).isFalse(); + } + + @Test + public void methodMatcherWhenMethodHasPreFilterAnnotationThenMatches() throws Exception { + PreFilterAuthorizationMethodBeforeAdvice advice = new PreFilterAuthorizationMethodBeforeAdvice(); + MethodMatcher methodMatcher = advice.getMethodMatcher(); + assertThat(methodMatcher.matches(TestClass.class.getMethod("doSomethingListFilterTargetMatch", List.class), + TestClass.class)).isTrue(); + } + + @Test + public void findFilterTargetWhenNameProvidedAndNotMatchThenException() throws Exception { + MockMethodInvocation mockMethodInvocation = new MockMethodInvocation(new TestClass(), TestClass.class, + "doSomethingListFilterTargetNotMatch", new Class[] { List.class }, new Object[] { new ArrayList<>() }); + MethodAuthorizationContext methodAuthorizationContext = new MethodAuthorizationContext(mockMethodInvocation, + TestClass.class); + PreFilterAuthorizationMethodBeforeAdvice advice = new PreFilterAuthorizationMethodBeforeAdvice(); + assertThatIllegalArgumentException() + .isThrownBy(() -> advice.before(TestAuthentication::authenticatedUser, methodAuthorizationContext)) + .withMessage( + "Filter target was null, or no argument with name 'filterTargetNotMatch' found in method."); + } + + @Test + public void findFilterTargetWhenNameProvidedAndMatchAndNullThenException() throws Exception { + MockMethodInvocation mockMethodInvocation = new MockMethodInvocation(new TestClass(), TestClass.class, + "doSomethingListFilterTargetMatch", new Class[] { List.class }, new Object[] { null }); + MethodAuthorizationContext methodAuthorizationContext = new MethodAuthorizationContext(mockMethodInvocation, + TestClass.class); + PreFilterAuthorizationMethodBeforeAdvice advice = new PreFilterAuthorizationMethodBeforeAdvice(); + assertThatIllegalArgumentException() + .isThrownBy(() -> advice.before(TestAuthentication::authenticatedUser, methodAuthorizationContext)) + .withMessage("Filter target was null, or no argument with name 'list' found in method."); + } + + @Test + public void findFilterTargetWhenNameProvidedAndMatchAndNotNullThenFiltersList() throws Exception { + List list = new ArrayList<>(); + list.add("john"); + list.add("bob"); + MockMethodInvocation mockMethodInvocation = new MockMethodInvocation(new TestClass(), TestClass.class, + "doSomethingListFilterTargetMatch", new Class[] { List.class }, new Object[] { list }); + MethodAuthorizationContext methodAuthorizationContext = new MethodAuthorizationContext(mockMethodInvocation, + TestClass.class); + PreFilterAuthorizationMethodBeforeAdvice advice = new PreFilterAuthorizationMethodBeforeAdvice(); + advice.before(TestAuthentication::authenticatedUser, methodAuthorizationContext); + assertThat(list).hasSize(1); + assertThat(list.get(0)).isEqualTo("john"); + } + + @Test + public void findFilterTargetWhenNameNotProvidedAndSingleArgListNullThenException() throws Exception { + MockMethodInvocation mockMethodInvocation = new MockMethodInvocation(new TestClass(), TestClass.class, + "doSomethingListFilterTargetNotProvided", new Class[] { List.class }, new Object[] { null }); + MethodAuthorizationContext methodAuthorizationContext = new MethodAuthorizationContext(mockMethodInvocation, + TestClass.class); + PreFilterAuthorizationMethodBeforeAdvice advice = new PreFilterAuthorizationMethodBeforeAdvice(); + assertThatIllegalArgumentException() + .isThrownBy(() -> advice.before(TestAuthentication::authenticatedUser, methodAuthorizationContext)) + .withMessage("Filter target was null. Make sure you passing the correct value in the method argument."); + } + + @Test + public void findFilterTargetWhenNameNotProvidedAndSingleArgListThenFiltersList() throws Exception { + List list = new ArrayList<>(); + list.add("john"); + list.add("bob"); + MockMethodInvocation mockMethodInvocation = new MockMethodInvocation(new TestClass(), TestClass.class, + "doSomethingListFilterTargetNotProvided", new Class[] { List.class }, new Object[] { list }); + MethodAuthorizationContext methodAuthorizationContext = new MethodAuthorizationContext(mockMethodInvocation, + TestClass.class); + PreFilterAuthorizationMethodBeforeAdvice advice = new PreFilterAuthorizationMethodBeforeAdvice(); + advice.before(TestAuthentication::authenticatedUser, methodAuthorizationContext); + assertThat(list).hasSize(1); + assertThat(list.get(0)).isEqualTo("john"); + } + + @Test + public void findFilterTargetWhenNameNotProvidedAndSingleArgArrayThenException() throws Exception { + MockMethodInvocation mockMethodInvocation = new MockMethodInvocation(new TestClass(), TestClass.class, + "doSomethingArrayFilterTargetNotProvided", new Class[] { String[].class }, + new Object[] { new String[] {} }); + MethodAuthorizationContext methodAuthorizationContext = new MethodAuthorizationContext(mockMethodInvocation, + TestClass.class); + PreFilterAuthorizationMethodBeforeAdvice advice = new PreFilterAuthorizationMethodBeforeAdvice(); + assertThatIllegalStateException() + .isThrownBy(() -> advice.before(TestAuthentication::authenticatedUser, methodAuthorizationContext)) + .withMessage( + "Pre-filtering on array types is not supported. Using a Collection will solve this problem."); + } + + @Test + public void findFilterTargetWhenNameNotProvidedAndNotSingleArgThenException() throws Exception { + MockMethodInvocation mockMethodInvocation = new MockMethodInvocation(new TestClass(), TestClass.class, + "doSomethingTwoArgsFilterTargetNotProvided", new Class[] { String.class, List.class }, + new Object[] { "", new ArrayList<>() }); + MethodAuthorizationContext methodAuthorizationContext = new MethodAuthorizationContext(mockMethodInvocation, + TestClass.class); + PreFilterAuthorizationMethodBeforeAdvice advice = new PreFilterAuthorizationMethodBeforeAdvice(); + assertThatIllegalStateException() + .isThrownBy(() -> advice.before(TestAuthentication::authenticatedUser, methodAuthorizationContext)) + .withMessage("Unable to determine the method argument for filtering. Specify the filter target."); + } + + public static class TestClass { + + public void doSomething() { + + } + + @PreFilter(value = "filterObject == 'john'", filterTarget = "filterTargetNotMatch") + public List doSomethingListFilterTargetNotMatch(List list) { + return list; + } + + @PreFilter(value = "filterObject == 'john'", filterTarget = "list") + public List doSomethingListFilterTargetMatch(List list) { + return list; + } + + @PreFilter("filterObject == 'john'") + public List doSomethingListFilterTargetNotProvided(List list) { + return list; + } + + @PreFilter("filterObject == 'john'") + public String[] doSomethingArrayFilterTargetNotProvided(String[] array) { + return array; + } + + @PreFilter("filterObject == 'john'") + public List doSomethingTwoArgsFilterTargetNotProvided(String s, List list) { + return list; + } + + } + +}