diff --git a/core/src/main/java/org/springframework/security/access/expression/method/DefaultMethodSecurityExpressionHandler.java b/core/src/main/java/org/springframework/security/access/expression/method/DefaultMethodSecurityExpressionHandler.java index 76cf8cf2dc4..e71c13063fd 100644 --- a/core/src/main/java/org/springframework/security/access/expression/method/DefaultMethodSecurityExpressionHandler.java +++ b/core/src/main/java/org/springframework/security/access/expression/method/DefaultMethodSecurityExpressionHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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. @@ -52,6 +52,7 @@ * * @author Luke Taylor * @author Evgeniy Cheban + * @author DingHao * @since 3.0 */ public class DefaultMethodSecurityExpressionHandler extends AbstractSecurityExpressionHandler @@ -65,6 +66,8 @@ public class DefaultMethodSecurityExpressionHandler extends AbstractSecurityExpr private PermissionCacheOptimizer permissionCacheOptimizer = null; + private Map variables = null; + private String defaultRolePrefix = "ROLE_"; public DefaultMethodSecurityExpressionHandler() { @@ -76,14 +79,14 @@ public DefaultMethodSecurityExpressionHandler() { */ @Override public StandardEvaluationContext createEvaluationContextInternal(Authentication auth, MethodInvocation mi) { - return new MethodSecurityEvaluationContext(auth, mi, getParameterNameDiscoverer()); + return new MethodSecurityEvaluationContext(auth, mi, getParameterNameDiscoverer(), this.variables); } @Override public EvaluationContext createEvaluationContext(Supplier authentication, MethodInvocation mi) { MethodSecurityExpressionOperations root = createSecurityExpressionRoot(authentication, mi); MethodSecurityEvaluationContext ctx = new MethodSecurityEvaluationContext(root, mi, - getParameterNameDiscoverer()); + getParameterNameDiscoverer(), this.variables); ctx.setBeanResolver(getBeanResolver()); return ctx; } @@ -245,6 +248,11 @@ public void setReturnObject(Object returnObject, EvaluationContext ctx) { ((MethodSecurityExpressionOperations) ctx.getRootObject().getValue()).setReturnObject(returnObject); } + @Override + public void setVariables(Map variables) { + this.variables = variables; + } + /** *

* Sets the default prefix to be added to diff --git a/core/src/main/java/org/springframework/security/access/expression/method/MethodSecurityEvaluationContext.java b/core/src/main/java/org/springframework/security/access/expression/method/MethodSecurityEvaluationContext.java index b3413565516..c5c20cf211f 100644 --- a/core/src/main/java/org/springframework/security/access/expression/method/MethodSecurityEvaluationContext.java +++ b/core/src/main/java/org/springframework/security/access/expression/method/MethodSecurityEvaluationContext.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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. @@ -17,6 +17,7 @@ package org.springframework.security.access.expression.method; import java.lang.reflect.Method; +import java.util.Map; import org.aopalliance.intercept.MethodInvocation; @@ -35,10 +36,13 @@ * @author Luke Taylor * @author Daniel Bustamante * @author Evgeniy Cheban + * @author DingHao * @since 3.0 */ class MethodSecurityEvaluationContext extends MethodBasedEvaluationContext { + private final Map variables; + /** * Intended for testing. Don't use in practice as it creates a new parameter resolver * for each instance. Use the constructor which takes the resolver, as an argument @@ -50,16 +54,36 @@ class MethodSecurityEvaluationContext extends MethodBasedEvaluationContext { MethodSecurityEvaluationContext(Authentication user, MethodInvocation mi, ParameterNameDiscoverer parameterNameDiscoverer) { + this(user, mi, parameterNameDiscoverer, null); + } + + MethodSecurityEvaluationContext(Authentication user, MethodInvocation mi, + ParameterNameDiscoverer parameterNameDiscoverer, Map variables) { super(mi.getThis(), getSpecificMethod(mi), mi.getArguments(), parameterNameDiscoverer); + this.variables = variables; } MethodSecurityEvaluationContext(MethodSecurityExpressionOperations root, MethodInvocation mi, ParameterNameDiscoverer parameterNameDiscoverer) { + this(root, mi, parameterNameDiscoverer, null); + } + + MethodSecurityEvaluationContext(MethodSecurityExpressionOperations root, MethodInvocation mi, + ParameterNameDiscoverer parameterNameDiscoverer, Map variables) { super(root, getSpecificMethod(mi), mi.getArguments(), parameterNameDiscoverer); + this.variables = variables; } private static Method getSpecificMethod(MethodInvocation mi) { return AopUtils.getMostSpecificMethod(mi.getMethod(), AopProxyUtils.ultimateTargetClass(mi.getThis())); } + @Override + protected void lazyLoadArguments() { + if (this.variables != null) { + setVariables(this.variables); + } + super.lazyLoadArguments(); + } + } diff --git a/core/src/main/java/org/springframework/security/access/expression/method/MethodSecurityExpressionHandler.java b/core/src/main/java/org/springframework/security/access/expression/method/MethodSecurityExpressionHandler.java index 50e0bfa76a3..468342e1895 100644 --- a/core/src/main/java/org/springframework/security/access/expression/method/MethodSecurityExpressionHandler.java +++ b/core/src/main/java/org/springframework/security/access/expression/method/MethodSecurityExpressionHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-2024 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. @@ -16,6 +16,8 @@ package org.springframework.security.access.expression.method; +import java.util.Map; + import org.aopalliance.intercept.MethodInvocation; import org.springframework.expression.EvaluationContext; @@ -27,6 +29,7 @@ * method invocations. * * @author Luke Taylor + * @author DingHao * @since 3.0 */ public interface MethodSecurityExpressionHandler extends SecurityExpressionHandler { @@ -53,4 +56,12 @@ public interface MethodSecurityExpressionHandler extends SecurityExpressionHandl */ void setReturnObject(Object returnObject, EvaluationContext ctx); + /** + * Set multiple named variables in this evaluation context to given values. + *

+ * Note: the variables has a lower priority than the method parameter priority + * @param variables the names and values of the variables to set + */ + void setVariables(Map variables); + } 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 index 42b7cd92c03..14ec5c5fad5 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/AbstractExpressionAttributeRegistry.java +++ b/core/src/main/java/org/springframework/security/authorization/method/AbstractExpressionAttributeRegistry.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 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. @@ -23,12 +23,14 @@ import org.aopalliance.intercept.MethodInvocation; import org.springframework.core.MethodClassKey; +import org.springframework.core.annotation.MergedAnnotation; import org.springframework.lang.NonNull; /** * For internal use only, as this contract is likely to change * * @author Evgeniy Cheban + * @author DingHao */ abstract class AbstractExpressionAttributeRegistry { @@ -67,4 +69,12 @@ final T getAttribute(Method method, Class targetClass) { @NonNull abstract T resolveAttribute(Method method, Class targetClass); + Map getMetaAnnotationAttribute(MergedAnnotation mergedAnnotation) { + MergedAnnotation metaSource = mergedAnnotation.getMetaSource(); + if (metaSource != null) { + return metaSource.asMap(); + } + return null; + } + } diff --git a/core/src/main/java/org/springframework/security/authorization/method/AuthorizationAnnotationUtils.java b/core/src/main/java/org/springframework/security/authorization/method/AuthorizationAnnotationUtils.java index 9f37603826c..3f0e90bcfec 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/AuthorizationAnnotationUtils.java +++ b/core/src/main/java/org/springframework/security/authorization/method/AuthorizationAnnotationUtils.java @@ -47,6 +47,7 @@ * * @author Josh Cummings * @author Sam Brannen + * @author DingHao */ final class AuthorizationAnnotationUtils { @@ -107,6 +108,26 @@ private static A findDistinctAnnotation(AnnotatedElement }; } + static MergedAnnotation findUniqueMergedAnnotation(AnnotatedElement annotatedElement, + Class annotationType) { + MergedAnnotations mergedAnnotations = MergedAnnotations.from(annotatedElement, + MergedAnnotations.SearchStrategy.TYPE_HIERARCHY, RepeatableContainers.none()); + + List> annotations = mergedAnnotations.stream(annotationType) + .map(MergedAnnotation::withNonMergedAttributes) + .distinct() + .toList(); + + return switch (annotations.size()) { + case 0 -> null; + case 1 -> annotations.get(0); + default -> throw new AnnotationConfigurationException(""" + Please ensure there is one unique annotation of type @%s attributed to %s. \ + Found %d competing annotations: %s""".formatted(annotationType.getName(), annotatedElement, + annotations.size(), annotations)); + }; + } + private AuthorizationAnnotationUtils() { } diff --git a/core/src/main/java/org/springframework/security/authorization/method/PostAuthorizeExpressionAttributeRegistry.java b/core/src/main/java/org/springframework/security/authorization/method/PostAuthorizeExpressionAttributeRegistry.java index c89bbc3e312..f2fb5735192 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/PostAuthorizeExpressionAttributeRegistry.java +++ b/core/src/main/java/org/springframework/security/authorization/method/PostAuthorizeExpressionAttributeRegistry.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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. @@ -18,10 +18,10 @@ import java.lang.reflect.Method; -import reactor.util.annotation.NonNull; - import org.springframework.aop.support.AopUtils; +import org.springframework.core.annotation.MergedAnnotation; 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.prepost.PostAuthorize; @@ -31,6 +31,7 @@ * For internal use only, as this contract is likely to change. * * @author Evgeniy Cheban + * @author DingHao * @since 5.8 */ final class PostAuthorizeExpressionAttributeRegistry extends AbstractExpressionAttributeRegistry { @@ -54,19 +55,21 @@ MethodSecurityExpressionHandler getExpressionHandler() { @Override ExpressionAttribute resolveAttribute(Method method, Class targetClass) { Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass); - PostAuthorize postAuthorize = findPostAuthorizeAnnotation(specificMethod); + MergedAnnotation postAuthorize = findPostAuthorizeAnnotation(specificMethod); if (postAuthorize == null) { return ExpressionAttribute.NULL_ATTRIBUTE; } Expression postAuthorizeExpression = this.expressionHandler.getExpressionParser() - .parseExpression(postAuthorize.value()); + .parseExpression(postAuthorize.getString(MergedAnnotation.VALUE)); + this.expressionHandler.setVariables(getMetaAnnotationAttribute(postAuthorize)); return new ExpressionAttribute(postAuthorizeExpression); } - private PostAuthorize findPostAuthorizeAnnotation(Method method) { - PostAuthorize postAuthorize = AuthorizationAnnotationUtils.findUniqueAnnotation(method, PostAuthorize.class); - return (postAuthorize != null) ? postAuthorize - : AuthorizationAnnotationUtils.findUniqueAnnotation(method.getDeclaringClass(), PostAuthorize.class); + private MergedAnnotation findPostAuthorizeAnnotation(Method method) { + MergedAnnotation postAuthorize = AuthorizationAnnotationUtils.findUniqueMergedAnnotation(method, + PostAuthorize.class); + return (postAuthorize != null) ? postAuthorize : AuthorizationAnnotationUtils + .findUniqueMergedAnnotation(method.getDeclaringClass(), PostAuthorize.class); } } diff --git a/core/src/main/java/org/springframework/security/authorization/method/PostFilterExpressionAttributeRegistry.java b/core/src/main/java/org/springframework/security/authorization/method/PostFilterExpressionAttributeRegistry.java index 4bc33bc493d..7e70c073abc 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/PostFilterExpressionAttributeRegistry.java +++ b/core/src/main/java/org/springframework/security/authorization/method/PostFilterExpressionAttributeRegistry.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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. @@ -19,6 +19,7 @@ import java.lang.reflect.Method; import org.springframework.aop.support.AopUtils; +import org.springframework.core.annotation.MergedAnnotation; import org.springframework.expression.Expression; import org.springframework.lang.NonNull; import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler; @@ -30,6 +31,7 @@ * For internal use only, as this contract is likely to change. * * @author Evgeniy Cheban + * @author DingHao * @since 5.8 */ final class PostFilterExpressionAttributeRegistry extends AbstractExpressionAttributeRegistry { @@ -53,19 +55,21 @@ MethodSecurityExpressionHandler getExpressionHandler() { @Override ExpressionAttribute resolveAttribute(Method method, Class targetClass) { Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass); - PostFilter postFilter = findPostFilterAnnotation(specificMethod); + MergedAnnotation postFilter = findPostFilterAnnotation(specificMethod); if (postFilter == null) { return ExpressionAttribute.NULL_ATTRIBUTE; } Expression postFilterExpression = this.expressionHandler.getExpressionParser() - .parseExpression(postFilter.value()); + .parseExpression(postFilter.getString(MergedAnnotation.VALUE)); + this.expressionHandler.setVariables(getMetaAnnotationAttribute(postFilter)); return new ExpressionAttribute(postFilterExpression); } - private PostFilter findPostFilterAnnotation(Method method) { - PostFilter postFilter = AuthorizationAnnotationUtils.findUniqueAnnotation(method, PostFilter.class); + private MergedAnnotation findPostFilterAnnotation(Method method) { + MergedAnnotation postFilter = AuthorizationAnnotationUtils.findUniqueMergedAnnotation(method, + PostFilter.class); return (postFilter != null) ? postFilter - : AuthorizationAnnotationUtils.findUniqueAnnotation(method.getDeclaringClass(), PostFilter.class); + : AuthorizationAnnotationUtils.findUniqueMergedAnnotation(method.getDeclaringClass(), PostFilter.class); } } diff --git a/core/src/main/java/org/springframework/security/authorization/method/PreAuthorizeExpressionAttributeRegistry.java b/core/src/main/java/org/springframework/security/authorization/method/PreAuthorizeExpressionAttributeRegistry.java index dcae13eb205..34020fea12e 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/PreAuthorizeExpressionAttributeRegistry.java +++ b/core/src/main/java/org/springframework/security/authorization/method/PreAuthorizeExpressionAttributeRegistry.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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. @@ -18,10 +18,10 @@ import java.lang.reflect.Method; -import reactor.util.annotation.NonNull; - import org.springframework.aop.support.AopUtils; +import org.springframework.core.annotation.MergedAnnotation; 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.prepost.PreAuthorize; @@ -31,6 +31,7 @@ * For internal use only, as this contract is likely to change. * * @author Evgeniy Cheban + * @author DingHao * @since 5.8 */ final class PreAuthorizeExpressionAttributeRegistry extends AbstractExpressionAttributeRegistry { @@ -58,19 +59,21 @@ MethodSecurityExpressionHandler getExpressionHandler() { @Override ExpressionAttribute resolveAttribute(Method method, Class targetClass) { Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass); - PreAuthorize preAuthorize = findPreAuthorizeAnnotation(specificMethod); + MergedAnnotation preAuthorize = findPreAuthorizeAnnotation(specificMethod); if (preAuthorize == null) { return ExpressionAttribute.NULL_ATTRIBUTE; } Expression preAuthorizeExpression = this.expressionHandler.getExpressionParser() - .parseExpression(preAuthorize.value()); + .parseExpression(preAuthorize.getString(MergedAnnotation.VALUE)); + this.expressionHandler.setVariables(getMetaAnnotationAttribute(preAuthorize)); return new ExpressionAttribute(preAuthorizeExpression); } - private PreAuthorize findPreAuthorizeAnnotation(Method method) { - PreAuthorize preAuthorize = AuthorizationAnnotationUtils.findUniqueAnnotation(method, PreAuthorize.class); - return (preAuthorize != null) ? preAuthorize - : AuthorizationAnnotationUtils.findUniqueAnnotation(method.getDeclaringClass(), PreAuthorize.class); + private MergedAnnotation findPreAuthorizeAnnotation(Method method) { + MergedAnnotation preAuthorize = AuthorizationAnnotationUtils.findUniqueMergedAnnotation(method, + PreAuthorize.class); + return (preAuthorize != null) ? preAuthorize : AuthorizationAnnotationUtils + .findUniqueMergedAnnotation(method.getDeclaringClass(), PreAuthorize.class); } } diff --git a/core/src/main/java/org/springframework/security/authorization/method/PreFilterExpressionAttributeRegistry.java b/core/src/main/java/org/springframework/security/authorization/method/PreFilterExpressionAttributeRegistry.java index 6fa8448355a..87018d410be 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/PreFilterExpressionAttributeRegistry.java +++ b/core/src/main/java/org/springframework/security/authorization/method/PreFilterExpressionAttributeRegistry.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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. @@ -19,6 +19,7 @@ import java.lang.reflect.Method; import org.springframework.aop.support.AopUtils; +import org.springframework.core.annotation.MergedAnnotation; import org.springframework.expression.Expression; import org.springframework.lang.NonNull; import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler; @@ -30,6 +31,7 @@ * For internal use only, as this contract is likely to change. * * @author Evgeniy Cheban + * @author DingHao * @since 5.8 */ final class PreFilterExpressionAttributeRegistry @@ -54,19 +56,21 @@ MethodSecurityExpressionHandler getExpressionHandler() { @Override PreFilterExpressionAttribute resolveAttribute(Method method, Class targetClass) { Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass); - PreFilter preFilter = findPreFilterAnnotation(specificMethod); + MergedAnnotation preFilter = findPreFilterAnnotation(specificMethod); if (preFilter == null) { return PreFilterExpressionAttribute.NULL_ATTRIBUTE; } Expression preFilterExpression = this.expressionHandler.getExpressionParser() - .parseExpression(preFilter.value()); - return new PreFilterExpressionAttribute(preFilterExpression, preFilter.filterTarget()); + .parseExpression(preFilter.getString(MergedAnnotation.VALUE)); + this.expressionHandler.setVariables(getMetaAnnotationAttribute(preFilter)); + return new PreFilterExpressionAttribute(preFilterExpression, preFilter.getString("filterTarget")); } - private PreFilter findPreFilterAnnotation(Method method) { - PreFilter preFilter = AuthorizationAnnotationUtils.findUniqueAnnotation(method, PreFilter.class); + private MergedAnnotation findPreFilterAnnotation(Method method) { + MergedAnnotation preFilter = AuthorizationAnnotationUtils.findUniqueMergedAnnotation(method, + PreFilter.class); return (preFilter != null) ? preFilter - : AuthorizationAnnotationUtils.findUniqueAnnotation(method.getDeclaringClass(), PreFilter.class); + : AuthorizationAnnotationUtils.findUniqueMergedAnnotation(method.getDeclaringClass(), PreFilter.class); } static final class PreFilterExpressionAttribute extends ExpressionAttribute { 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 index 37383b40d57..31f218ed469 100644 --- a/core/src/test/java/org/springframework/security/authorization/method/PostAuthorizeAuthorizationManagerTests.java +++ b/core/src/test/java/org/springframework/security/authorization/method/PostAuthorizeAuthorizationManagerTests.java @@ -16,8 +16,10 @@ package org.springframework.security.authorization.method; +import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -167,6 +169,46 @@ public void checkInheritedAnnotationsWhenConflictingThenAnnotationConfigurationE .isThrownBy(() -> manager.check(authentication, result)); } + @Test + public void checkRequiresUserWhenMetaAnnotationClassThenApplies() throws Exception { + MockMethodInvocation methodInvocation = new MockMethodInvocation(new MetaAnnotationClass(), + MetaAnnotationClass.class, "hasSameNameParameter", new Class[] { String.class }, + new Object[] { "ROLE_USER" }); + PostAuthorizeAuthorizationManager manager = new PostAuthorizeAuthorizationManager(); + AuthorizationDecision decision = manager.check(TestAuthentication::authenticatedUser, + new MethodInvocationResult(methodInvocation, null)); + assertThat(decision.isGranted()).isTrue(); + + methodInvocation = new MockMethodInvocation(new MetaAnnotationClass(), MetaAnnotationClass.class, + "noneSameNameParameter", new Class[] { String.class }, new Object[] { "hello" }); + decision = manager.check(TestAuthentication::authenticatedUser, + new MethodInvocationResult(methodInvocation, null)); + assertThat(decision.isGranted()).isFalse(); + } + + public static class MetaAnnotationClass { + + @HasAuthority("message:read") + public void hasSameNameParameter(String value) { + + } + + @HasAuthority("message:read") + public void noneSameNameParameter(String object) { + + } + + } + + @Target({ ElementType.METHOD, ElementType.TYPE }) + @Retention(RetentionPolicy.RUNTIME) + @PostAuthorize("hasAuthority(#value)") + public @interface HasAuthority { + + String value(); + + } + public static class TestClass implements InterfaceAnnotationsOne, InterfaceAnnotationsTwo { public void doSomething() { diff --git a/core/src/test/java/org/springframework/security/authorization/method/PostFilterAuthorizationMethodInterceptorTests.java b/core/src/test/java/org/springframework/security/authorization/method/PostFilterAuthorizationMethodInterceptorTests.java index 00f2aed42dd..86dc313d49f 100644 --- a/core/src/test/java/org/springframework/security/authorization/method/PostFilterAuthorizationMethodInterceptorTests.java +++ b/core/src/test/java/org/springframework/security/authorization/method/PostFilterAuthorizationMethodInterceptorTests.java @@ -16,8 +16,10 @@ package org.springframework.security.authorization.method; +import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.AfterEach; @@ -170,6 +172,55 @@ public Object proceed() { SecurityContextHolder.setContextHolderStrategy(saved); } + @Test + public void postFilterWhenProvideMetaAnnotation() throws Throwable { + String[] array = { "john", "bob" }; + MockMethodInvocation methodInvocation = new MockMethodInvocation(new MetaAnnotationClass(), + MetaAnnotationClass.class, "filterHasSameNameParameter", new Class[] { String[].class, int.class }, + new Object[] { array, 2 }) { + @Override + public Object proceed() { + return array; + } + }; + PostFilterAuthorizationMethodInterceptor advice = new PostFilterAuthorizationMethodInterceptor(); + Object result = advice.invoke(methodInvocation); + assertThat(result).asInstanceOf(InstanceOfAssertFactories.array(String[].class)).containsOnly("bob"); + + methodInvocation = new MockMethodInvocation(new MetaAnnotationClass(), MetaAnnotationClass.class, + "filterNoneSameNameParameter", new Class[] { String[].class, int.class }, new Object[] { array, 2 }) { + @Override + public Object proceed() { + return array; + } + }; + result = advice.invoke(methodInvocation); + assertThat(result).asInstanceOf(InstanceOfAssertFactories.array(String[].class)).containsOnly("john"); + } + + public static class MetaAnnotationClass { + + @MetaPostFilter(1) + public String[] filterHasSameNameParameter(String[] array, int value) { + return array; + } + + @MetaPostFilter(1) + public String[] filterNoneSameNameParameter(String[] array, int object) { + return array; + } + + } + + @Target({ ElementType.METHOD, ElementType.TYPE }) + @Retention(RetentionPolicy.RUNTIME) + @PostFilter("filterObject == (#value == 1 ? 'john' : 'bob')") + public @interface MetaPostFilter { + + int value(); + + } + @PostFilter("filterObject == 'john'") public static class TestClass implements InterfaceAnnotationsOne, InterfaceAnnotationsTwo { 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 index cb43868dbf0..9fb6e9458be 100644 --- a/core/src/test/java/org/springframework/security/authorization/method/PreAuthorizeAuthorizationManagerTests.java +++ b/core/src/test/java/org/springframework/security/authorization/method/PreAuthorizeAuthorizationManagerTests.java @@ -16,8 +16,10 @@ package org.springframework.security.authorization.method; +import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; import java.util.function.Supplier; import org.junit.jupiter.api.Test; @@ -147,6 +149,44 @@ public void checkTargetClassAwareWhenInterfaceLevelAnnotationsThenApplies() thro assertThat(decision.isGranted()).isTrue(); } + @Test + public void checkRequiresUserWhenMetaAnnotationClassThenApplies() throws Exception { + MockMethodInvocation methodInvocation = new MockMethodInvocation(new MetaAnnotationClass(), + MetaAnnotationClass.class, "hasSameNameParameter", new Class[] { String.class }, + new Object[] { "ROLE_USER" }); + PreAuthorizeAuthorizationManager manager = new PreAuthorizeAuthorizationManager(); + AuthorizationDecision decision = manager.check(TestAuthentication::authenticatedUser, methodInvocation); + assertThat(decision.isGranted()).isTrue(); + + methodInvocation = new MockMethodInvocation(new MetaAnnotationClass(), MetaAnnotationClass.class, + "noneSameNameParameter", new Class[] { String.class }, new Object[] { "hello" }); + decision = manager.check(TestAuthentication::authenticatedUser, methodInvocation); + assertThat(decision.isGranted()).isFalse(); + } + + public static class MetaAnnotationClass { + + @HasAuthority("message:read") + public void hasSameNameParameter(String value) { + + } + + @HasAuthority("message:read") + public void noneSameNameParameter(String object) { + + } + + } + + @Target({ ElementType.METHOD, ElementType.TYPE }) + @Retention(RetentionPolicy.RUNTIME) + @PreAuthorize("hasAuthority(#value)") + public @interface HasAuthority { + + String value(); + + } + public static class TestClass implements InterfaceAnnotationsOne, InterfaceAnnotationsTwo { public void doSomething() { diff --git a/core/src/test/java/org/springframework/security/authorization/method/PreFilterAuthorizationMethodInterceptorTests.java b/core/src/test/java/org/springframework/security/authorization/method/PreFilterAuthorizationMethodInterceptorTests.java index 4f1d56fb146..e458c34f9e7 100644 --- a/core/src/test/java/org/springframework/security/authorization/method/PreFilterAuthorizationMethodInterceptorTests.java +++ b/core/src/test/java/org/springframework/security/authorization/method/PreFilterAuthorizationMethodInterceptorTests.java @@ -16,8 +16,10 @@ package org.springframework.security.authorization.method; +import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; import java.util.ArrayList; import java.util.List; @@ -224,6 +226,50 @@ public void preFilterWhenStaticSecurityContextHolderStrategyAfterConstructorThen SecurityContextHolder.setContextHolderStrategy(saved); } + @Test + public void preFilterWhenProvideMetaAnnotation() throws Throwable { + List list = new ArrayList<>(); + list.add("john"); + list.add("bob"); + MockMethodInvocation methodInvocation = new MockMethodInvocation(new MetaAnnotationClass(), + MetaAnnotationClass.class, "filterHasSameNameParameter", new Class[] { List.class, int.class }, + new Object[] { list, 2 }); + PreFilterAuthorizationMethodInterceptor advice = new PreFilterAuthorizationMethodInterceptor(); + advice.invoke(methodInvocation); + assertThat(list).hasSize(1); + assertThat(list.get(0)).isEqualTo("bob"); + + list.add("john"); + methodInvocation = new MockMethodInvocation(new MetaAnnotationClass(), MetaAnnotationClass.class, + "filterNoneSameNameParameter", new Class[] { List.class, int.class }, new Object[] { list, 2 }); + advice.invoke(methodInvocation); + assertThat(list).hasSize(1); + assertThat(list.get(0)).isEqualTo("john"); + } + + public static class MetaAnnotationClass { + + @MetaPreFilter(1) + public void filterHasSameNameParameter(List list, int value) { + + } + + @MetaPreFilter(1) + public void filterNoneSameNameParameter(List list, int object) { + + } + + } + + @Target({ ElementType.METHOD, ElementType.TYPE }) + @Retention(RetentionPolicy.RUNTIME) + @PreFilter(value = "filterObject == (#value == 1 ? 'john' : 'bob')", filterTarget = "list") + public @interface MetaPreFilter { + + int value(); + + } + @PreFilter("filterObject == 'john'") public static class TestClass implements InterfaceAnnotationsOne, InterfaceAnnotationsTwo {