diff --git a/core/src/main/java/org/springframework/security/core/annotation/AnnotationSynthesizer.java b/core/src/main/java/org/springframework/security/core/annotation/AnnotationSynthesizer.java index 34c9ac67f8..712c840905 100644 --- a/core/src/main/java/org/springframework/security/core/annotation/AnnotationSynthesizer.java +++ b/core/src/main/java/org/springframework/security/core/annotation/AnnotationSynthesizer.java @@ -17,29 +17,30 @@ package org.springframework.security.core.annotation; import java.lang.annotation.Annotation; -import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Method; import java.lang.reflect.Parameter; -import org.springframework.core.annotation.MergedAnnotation; import org.springframework.lang.Nullable; -import org.springframework.util.Assert; /** - * A strategy for synthesizing an annotation from an {@link AnnotatedElement}. + * An interface to search for and synthesize an annotation on a type, method, or method + * parameter into an annotation of type {@code }. + * + *

+ * Implementations should support meta-annotations. This is usually by way of the + * {@link org.springframework.core.annotation.MergedAnnotations} API. * *

* Synthesis generally refers to the process of taking an annotation's meta-annotations * and placeholders, resolving them, and then combining these elements into a facade of * the raw annotation instance. - *

* *

- * Since the process of synthesizing an annotation can be expensive, it is recommended to + * Since the process of synthesizing an annotation can be expensive, it's recommended to * cache the synthesized annotation to prevent multiple computations. *

* - * @param
the annotation type + * @param the annotation to search for and synthesize * @author Josh Cummings * @since 6.4 * @see UniqueMergedAnnotationSynthesizer @@ -48,41 +49,36 @@ import org.springframework.util.Assert; public interface AnnotationSynthesizer { /** - * Synthesize an annotation of type {@code A} from the given {@link AnnotatedElement}. + * Synthesize an annotation of type {@code A} from the given method. * *

* Implementations should fail if they encounter more than one annotation of that type - * on the element. - *

+ * attributable to the method. + * + *

+ * Implementations should describe their strategy for searching the element and any + * surrounding class, interfaces, or super-class. + * @param method the method to search from + * @param targetClass the target class for the method + * @return the synthesized annotation or {@code null} if not found + */ + @Nullable + A synthesize(Method method, Class targetClass); + + /** + * Synthesize an annotation of type {@code A} from the given method parameter. + * + *

+ * Implementations should fail if they encounter more than one annotation of that type + * attributable to the parameter. * *

* Implementations should describe their strategy for searching the element and any * surrounding class, interfaces, or super-class. - *

* @param element the element to search * @return the synthesized annotation or {@code null} if not found */ @Nullable - default A synthesize(AnnotatedElement element, Class targetClass) { - Assert.notNull(targetClass, "targetClass cannot be null"); - MergedAnnotation
annotation = merge(element, targetClass); - if (annotation == null) { - return null; - } - return annotation.synthesize(); - } - - @Nullable - default A synthesize(AnnotatedElement element) { - if (element instanceof Method method) { - return synthesize(element, method.getDeclaringClass()); - } - if (element instanceof Parameter parameter) { - return synthesize(parameter, parameter.getDeclaringExecutable().getDeclaringClass()); - } - throw new UnsupportedOperationException("Unsupported element of type " + element.getClass()); - } - - MergedAnnotation merge(AnnotatedElement element, Class targetClass); + A synthesize(Parameter parameter); } diff --git a/core/src/main/java/org/springframework/security/core/annotation/ExpressionTemplateAnnotationSynthesizer.java b/core/src/main/java/org/springframework/security/core/annotation/ExpressionTemplateAnnotationSynthesizer.java index c5da99a800..3a6f2342f0 100644 --- a/core/src/main/java/org/springframework/security/core/annotation/ExpressionTemplateAnnotationSynthesizer.java +++ b/core/src/main/java/org/springframework/security/core/annotation/ExpressionTemplateAnnotationSynthesizer.java @@ -30,8 +30,16 @@ import org.springframework.util.Assert; import org.springframework.util.PropertyPlaceholderHelper; /** - * A strategy for synthesizing an annotation from an {@link AnnotatedElement} that - * supports meta-annotations with placeholders, like the following: + * Searches for and synthesizes an annotation on a type, method, or method parameter into + * an annotation of type {@code }, resolving any placeholders in the annotation value. + * + *

+ * Note that in all cases, Spring Security does not allow for repeatable annotations. So + * this class delegates to {@link UniqueMergedAnnotationSynthesizer} in order to error if + * a repeat is discovered. + * + *

+ * It supports meta-annotations with placeholders, like the following: * *

  *	@PreAuthorize("hasRole({role})")
@@ -46,19 +54,14 @@ import org.springframework.util.PropertyPlaceholderHelper;
  * {@code @HasRole} annotation found on a given {@link AnnotatedElement}.
  *
  * 

- * Note that in all cases, Spring Security does not allow for repeatable annotations. So - * this class delegates to {@link UniqueMergedAnnotationSynthesizer} in order to error if - * a repeat is discovered. - * - *

* Since the process of synthesis is expensive, it is recommended to cache the synthesized * result to prevent multiple computations. * - * @param the annotation type + * @param the annotation to search for and synthesize * @author Josh Cummings * @since 6.4 */ -final class ExpressionTemplateAnnotationSynthesizer implements AnnotationSynthesizer { +final class ExpressionTemplateAnnotationSynthesizer extends AbstractAnnotationSynthesizer { private final Class type; @@ -79,7 +82,7 @@ final class ExpressionTemplateAnnotationSynthesizer implem } @Override - public MergedAnnotation merge(AnnotatedElement element, Class targetClass) { + MergedAnnotation merge(AnnotatedElement element, Class targetClass) { if (element instanceof Parameter parameter) { MergedAnnotation annotation = this.uniqueParameterAnnotationCache.computeIfAbsent(parameter, (p) -> this.unique.merge(p, targetClass)); diff --git a/core/src/main/java/org/springframework/security/core/annotation/UniqueMergedAnnotationSynthesizer.java b/core/src/main/java/org/springframework/security/core/annotation/UniqueMergedAnnotationSynthesizer.java index 6eb5bfb237..e2715cc2f5 100644 --- a/core/src/main/java/org/springframework/security/core/annotation/UniqueMergedAnnotationSynthesizer.java +++ b/core/src/main/java/org/springframework/security/core/annotation/UniqueMergedAnnotationSynthesizer.java @@ -34,8 +34,33 @@ import org.springframework.util.Assert; import org.springframework.util.ClassUtils; /** - * A strategy for synthesizing an annotation from an {@link AnnotatedElement} that - * supports meta-annotations, like the following: + * Searches for and synthesizes annotations found on types, methods, or method parameters + * into an annotation of type {@code }, ensuring that there is a unique match. + * + *

+ * Note that in all cases, Spring Security does not allow for repeatable annotations. As + * such, this class errors if a repeat is discovered. + * + *

+ * For example, if a class extends two interfaces, and each interface is annotated with + * `@PreAuthorize("hasRole('ADMIN')")` and `@PreAuthorize("hasRole('USER')")` + * respectively, it's not clear which of these should apply, and so this class will throw + * an exception. + * + *

+ * If the given annotation can be applied to types or methods, this class will traverse + * the type hierarchy, starting from the target class and method; in case of a method + * parameter, it will only consider annotations on the parameter. In all cases, it will + * consider meta-annotations in its traversal. + * + *

+ * When traversing the type hierarchy, this class will first look for annotations on the + * given method, then on any methods that method overrides. If no annotations are found, + * it will then search for annotations on the given class, then on any classes that class + * extends and on any interfaces that class implements. + * + *

+ * It supports meta-annotations, like the following: * *

  *	@PreAuthorize("hasRole('ROLE_ADMIN')")
@@ -46,33 +71,18 @@ import org.springframework.util.ClassUtils;
  * 

* In that case, you can use an {@link UniqueMergedAnnotationSynthesizer} of type * {@link org.springframework.security.access.prepost.PreAuthorize} to synthesize any - * {@code @HasRole} annotation found on a given {@link AnnotatedElement}. + * {@code @HasRole} annotation found on a given method or class into its + * {@link org.springframework.security.access.prepost.PreAuthorize} meta-annotation. * *

- * Note that in all cases, Spring Security does not allow for repeatable annotations. As - * such, this class errors if a repeat is discovered. - * - *

- * If the given annotation can be applied to types, this class will search for annotations - * across the entire {@link MergedAnnotations.SearchStrategy type hierarchy}; otherwise, - * it will only look for annotations {@link MergedAnnotations.SearchStrategy directly} - * attributed to the element. - * - *

- * When traversing the type hierarchy, this class will first look for annotations on the - * given method, then on any methods that method overrides. If no annotations are found, - * it will then search for annotations on the given class, then on any classes that class - * extends and on any interfaces that class implements. - * - *

- * Since the process of synthesis is expensive, it is recommended to cache the synthesized + * Since the process of synthesis is expensive, it's recommended to cache the synthesized * result to prevent multiple computations. * - * @param the annotation type + * @param the annotation to search for and synthesize * @author Josh Cummings * @since 6.4 */ -final class UniqueMergedAnnotationSynthesizer implements AnnotationSynthesizer { +final class UniqueMergedAnnotationSynthesizer extends AbstractAnnotationSynthesizer { private final List> types; @@ -87,26 +97,18 @@ final class UniqueMergedAnnotationSynthesizer implements A } @Override - public MergedAnnotation merge(AnnotatedElement element, Class targetClass) { + MergedAnnotation merge(AnnotatedElement element, Class targetClass) { if (element instanceof Parameter parameter) { - return handleParameterElement(parameter); + List> annotations = findDirectAnnotations(parameter); + return requireUnique(parameter, annotations); } if (element instanceof Method method) { - return handleMethodElement(method, targetClass); + List> annotations = findMethodAnnotations(method, targetClass); + return requireUnique(method, annotations); } throw new AnnotationConfigurationException("Unsupported element of type " + element.getClass()); } - private MergedAnnotation handleParameterElement(Parameter parameter) { - List> annotations = findDirectAnnotations(parameter); - return requireUnique(parameter, annotations); - } - - private MergedAnnotation handleMethodElement(Method method, Class targetClass) { - List> annotations = findMethodAnnotations(method, targetClass); - return requireUnique(method, annotations); - } - private MergedAnnotation requireUnique(AnnotatedElement element, List> annotations) { return switch (annotations.size()) { case 0 -> null; diff --git a/core/src/test/java/org/springframework/security/core/annotation/UniqueMergedAnnotationSynthesizerTests.java b/core/src/test/java/org/springframework/security/core/annotation/UniqueMergedAnnotationSynthesizerTests.java index 444dc04e83..fd9c1ac8c0 100644 --- a/core/src/test/java/org/springframework/security/core/annotation/UniqueMergedAnnotationSynthesizerTests.java +++ b/core/src/test/java/org/springframework/security/core/annotation/UniqueMergedAnnotationSynthesizerTests.java @@ -37,35 +37,35 @@ public class UniqueMergedAnnotationSynthesizerTests { @Test void synthesizeWhenAnnotationOnInterfaceThenResolves() throws Exception { Method method = AnnotationOnInterface.class.getDeclaredMethod("method"); - PreAuthorize preAuthorize = this.synthesizer.synthesize(method); + PreAuthorize preAuthorize = this.synthesizer.synthesize(method, method.getDeclaringClass()); assertThat(preAuthorize.value()).isEqualTo("one"); } @Test void synthesizeWhenAnnotationOnMethodThenResolves() throws Exception { Method method = AnnotationOnInterfaceMethod.class.getDeclaredMethod("method"); - PreAuthorize preAuthorize = this.synthesizer.synthesize(method); + PreAuthorize preAuthorize = this.synthesizer.synthesize(method, method.getDeclaringClass()); assertThat(preAuthorize.value()).isEqualTo("three"); } @Test void synthesizeWhenAnnotationOnClassThenResolves() throws Exception { Method method = AnnotationOnClass.class.getDeclaredMethod("method"); - PreAuthorize preAuthorize = this.synthesizer.synthesize(method); + PreAuthorize preAuthorize = this.synthesizer.synthesize(method, method.getDeclaringClass()); assertThat(preAuthorize.value()).isEqualTo("five"); } @Test void synthesizeWhenAnnotationOnClassMethodThenResolves() throws Exception { Method method = AnnotationOnClassMethod.class.getDeclaredMethod("method"); - PreAuthorize preAuthorize = this.synthesizer.synthesize(method); + PreAuthorize preAuthorize = this.synthesizer.synthesize(method, method.getDeclaringClass()); assertThat(preAuthorize.value()).isEqualTo("six"); } @Test void synthesizeWhenInterfaceOverridingAnnotationOnInterfaceThenResolves() throws Exception { Method method = InterfaceMethodOverridingAnnotationOnInterface.class.getDeclaredMethod("method"); - PreAuthorize preAuthorize = this.synthesizer.synthesize(method); + PreAuthorize preAuthorize = this.synthesizer.synthesize(method, method.getDeclaringClass()); assertThat(preAuthorize.value()).isEqualTo("eight"); } @@ -73,14 +73,14 @@ public class UniqueMergedAnnotationSynthesizerTests { void synthesizeWhenInterfaceOverridingMultipleInterfaceInheritanceThenResolves() throws Exception { Method method = ClassInheritingInterfaceOverridingMultipleInterfaceInheritance.class .getDeclaredMethod("method"); - PreAuthorize preAuthorize = this.synthesizer.synthesize(method); + PreAuthorize preAuthorize = this.synthesizer.synthesize(method, method.getDeclaringClass()); assertThat(preAuthorize.value()).isEqualTo("ten"); } @Test void synthesizeWhenInterfaceMethodOverridingAnnotationOnInterfaceThenResolves() throws Exception { Method method = InterfaceMethodOverridingMultipleInterfaceInheritance.class.getDeclaredMethod("method"); - PreAuthorize preAuthorize = this.synthesizer.synthesize(method); + PreAuthorize preAuthorize = this.synthesizer.synthesize(method, method.getDeclaringClass()); assertThat(preAuthorize.value()).isEqualTo("eleven"); } @@ -88,63 +88,63 @@ public class UniqueMergedAnnotationSynthesizerTests { void synthesizeWhenClassMultipleInheritanceThenException() throws Exception { Method method = ClassAttemptingMultipleInterfaceInheritance.class.getDeclaredMethod("method"); assertThatExceptionOfType(AnnotationConfigurationException.class) - .isThrownBy(() -> this.synthesizer.synthesize(method)); + .isThrownBy(() -> this.synthesizer.synthesize(method, method.getDeclaringClass())); } // gh-15097 @Test void synthesizeWhenClassOverridingMultipleInterfaceInheritanceThenResolves() throws Exception { Method method = ClassOverridingMultipleInterfaceInheritance.class.getDeclaredMethod("method"); - PreAuthorize preAuthorize = this.synthesizer.synthesize(method); + PreAuthorize preAuthorize = this.synthesizer.synthesize(method, method.getDeclaringClass()); assertThat(preAuthorize.value()).isEqualTo("thirteen"); } @Test void synthesizeWhenClassMethodOverridingMultipleInterfaceInheritanceThenResolves() throws Exception { Method method = ClassMethodOverridingMultipleInterfaceInheritance.class.getDeclaredMethod("method"); - PreAuthorize preAuthorize = this.synthesizer.synthesize(method); + PreAuthorize preAuthorize = this.synthesizer.synthesize(method, method.getDeclaringClass()); assertThat(preAuthorize.value()).isEqualTo("fourteen"); } @Test void synthesizeWhenClassInheritingInterfaceOverridingInterfaceAnnotationThenResolves() throws Exception { Method method = ClassInheritingInterfaceOverridingInterfaceAnnotation.class.getDeclaredMethod("method"); - PreAuthorize preAuthorize = this.synthesizer.synthesize(method); + PreAuthorize preAuthorize = this.synthesizer.synthesize(method, method.getDeclaringClass()); assertThat(preAuthorize.value()).isEqualTo("seven"); } @Test void synthesizeWhenClassOverridingGrandparentInterfaceAnnotationThenResolves() throws Exception { Method method = ClassOverridingGrandparentInterfaceAnnotation.class.getDeclaredMethod("method"); - PreAuthorize preAuthorize = this.synthesizer.synthesize(method); + PreAuthorize preAuthorize = this.synthesizer.synthesize(method, method.getDeclaringClass()); assertThat(preAuthorize.value()).isEqualTo("sixteen"); } @Test void synthesizeWhenMethodOverridingGrandparentInterfaceAnnotationThenResolves() throws Exception { Method method = MethodOverridingGrandparentInterfaceAnnotation.class.getDeclaredMethod("method"); - PreAuthorize preAuthorize = this.synthesizer.synthesize(method); + PreAuthorize preAuthorize = this.synthesizer.synthesize(method, method.getDeclaringClass()); assertThat(preAuthorize.value()).isEqualTo("seventeen"); } @Test void synthesizeWhenClassInheritingMethodOverriddenAnnotationThenResolves() throws Exception { Method method = ClassInheritingMethodOverriddenAnnotation.class.getDeclaredMethod("method"); - PreAuthorize preAuthorize = this.synthesizer.synthesize(method); + PreAuthorize preAuthorize = this.synthesizer.synthesize(method, method.getDeclaringClass()); assertThat(preAuthorize.value()).isEqualTo("eight"); } @Test void synthesizeWhenClassOverridingMethodOverriddenAnnotationThenResolves() throws Exception { Method method = ClassOverridingMethodOverriddenAnnotation.class.getDeclaredMethod("method"); - PreAuthorize preAuthorize = this.synthesizer.synthesize(method); + PreAuthorize preAuthorize = this.synthesizer.synthesize(method, method.getDeclaringClass()); assertThat(preAuthorize.value()).isEqualTo("eight"); } @Test void synthesizeWhenMethodOverridingMethodOverriddenAnnotationThenResolves() throws Exception { Method method = MethodOverridingMethodOverriddenAnnotation.class.getDeclaredMethod("method"); - PreAuthorize preAuthorize = this.synthesizer.synthesize(method); + PreAuthorize preAuthorize = this.synthesizer.synthesize(method, method.getDeclaringClass()); assertThat(preAuthorize.value()).isEqualTo("twenty"); } @@ -152,41 +152,41 @@ public class UniqueMergedAnnotationSynthesizerTests { void synthesizeWhenClassInheritingMultipleInheritanceThenException() throws Exception { Method method = ClassInheritingMultipleInheritance.class.getDeclaredMethod("method"); assertThatExceptionOfType(AnnotationConfigurationException.class) - .isThrownBy(() -> this.synthesizer.synthesize(method)); + .isThrownBy(() -> this.synthesizer.synthesize(method, method.getDeclaringClass())); } @Test void synthesizeWhenClassOverridingMultipleInheritanceThenResolves() throws Exception { Method method = ClassOverridingMultipleInheritance.class.getDeclaredMethod("method"); - PreAuthorize preAuthorize = this.synthesizer.synthesize(method); + PreAuthorize preAuthorize = this.synthesizer.synthesize(method, method.getDeclaringClass()); assertThat(preAuthorize.value()).isEqualTo("twentytwo"); } @Test void synthesizeWhenMethodOverridingMultipleInheritanceThenResolves() throws Exception { Method method = MethodOverridingMultipleInheritance.class.getDeclaredMethod("method"); - PreAuthorize preAuthorize = this.synthesizer.synthesize(method); + PreAuthorize preAuthorize = this.synthesizer.synthesize(method, method.getDeclaringClass()); assertThat(preAuthorize.value()).isEqualTo("twentythree"); } @Test void synthesizeWhenInheritingInterfaceAndMethodAnnotationsThenResolves() throws Exception { Method method = InheritingInterfaceAndMethodAnnotations.class.getDeclaredMethod("method"); - PreAuthorize preAuthorize = this.synthesizer.synthesize(method); + PreAuthorize preAuthorize = this.synthesizer.synthesize(method, method.getDeclaringClass()); assertThat(preAuthorize.value()).isEqualTo("three"); } @Test void synthesizeWhenClassOverridingInterfaceAndMethodInheritanceThenResolves() throws Exception { Method method = ClassOverridingInterfaceAndMethodInheritance.class.getDeclaredMethod("method"); - PreAuthorize preAuthorize = this.synthesizer.synthesize(method); + PreAuthorize preAuthorize = this.synthesizer.synthesize(method, method.getDeclaringClass()); assertThat(preAuthorize.value()).isEqualTo("three"); } @Test void synthesizeWhenMethodOverridingInterfaceAndMethodInheritanceThenResolves() throws Exception { Method method = MethodOverridingInterfaceAndMethodInheritance.class.getDeclaredMethod("method"); - PreAuthorize preAuthorize = this.synthesizer.synthesize(method); + PreAuthorize preAuthorize = this.synthesizer.synthesize(method, method.getDeclaringClass()); assertThat(preAuthorize.value()).isEqualTo("twentysix"); } @@ -194,21 +194,21 @@ public class UniqueMergedAnnotationSynthesizerTests { void synthesizeWhenMultipleMethodInheritanceThenException() throws Exception { Method method = MultipleMethodInheritance.class.getDeclaredMethod("method"); assertThatExceptionOfType(AnnotationConfigurationException.class) - .isThrownBy(() -> this.synthesizer.synthesize(method)); + .isThrownBy(() -> this.synthesizer.synthesize(method, method.getDeclaringClass())); } // gh-13234 @Test void synthesizeWhenClassInheritingInterfaceAnnotationThenResolves() throws Exception { Method method = ClassInheritingInterfaceMethodAnnotation.class.getDeclaredMethod("method"); - PreAuthorize preAuthorize = this.synthesizer.synthesize(method); + PreAuthorize preAuthorize = this.synthesizer.synthesize(method, method.getDeclaringClass()); assertThat(preAuthorize.value()).isEqualTo("three"); } @Test void synthesizeWhenMethodInheritingMethodOverridingInterfaceAndMethodInheritanceThenResolves() throws Exception { Method method = MethodInheritingMethodOverridingInterfaceAndMethodInheritance.class.getMethod("method"); - PreAuthorize preAuthorize = this.synthesizer.synthesize(method); + PreAuthorize preAuthorize = this.synthesizer.synthesize(method, method.getDeclaringClass()); assertThat(preAuthorize.value()).isEqualTo("twentysix"); } @@ -224,13 +224,13 @@ public class UniqueMergedAnnotationSynthesizerTests { void synthesizeWhenInterfaceInheritingAnnotationsAtDifferentLevelsThenException() throws Exception { Method method = InterfaceInheritingAnnotationsAtDifferentLevels.class.getMethod("method"); assertThatExceptionOfType(AnnotationConfigurationException.class) - .isThrownBy(() -> this.synthesizer.synthesize(method)); + .isThrownBy(() -> this.synthesizer.synthesize(method, method.getDeclaringClass())); } @Test void synthesizeWhenClassMethodOverridingAnnotationOnMethodThenResolves() throws Exception { Method method = ClassMethodOverridingAnnotationOnMethod.class.getDeclaredMethod("method"); - PreAuthorize preAuthorize = this.synthesizer.synthesize(method); + PreAuthorize preAuthorize = this.synthesizer.synthesize(method, method.getDeclaringClass()); assertThat(preAuthorize.value()).isEqualTo("twentyeight"); } @@ -238,7 +238,7 @@ public class UniqueMergedAnnotationSynthesizerTests { @Test void synthesizeWhenClassInheritingInterfaceInheritingInterfaceMethodAnnotationThenResolves() throws Exception { Method method = ClassInheritingInterfaceInheritingInterfaceMethodAnnotation.class.getDeclaredMethod("method"); - PreAuthorize preAuthorize = this.synthesizer.synthesize(method); + PreAuthorize preAuthorize = this.synthesizer.synthesize(method, method.getDeclaringClass()); assertThat(preAuthorize.value()).isEqualTo("three"); }