diff --git a/core/src/main/java/org/springframework/security/core/annotation/UniqueSecurityAnnotationScanner.java b/core/src/main/java/org/springframework/security/core/annotation/UniqueSecurityAnnotationScanner.java index 5093e1bd1d1..e3f5c9297c6 100644 --- a/core/src/main/java/org/springframework/security/core/annotation/UniqueSecurityAnnotationScanner.java +++ b/core/src/main/java/org/springframework/security/core/annotation/UniqueSecurityAnnotationScanner.java @@ -18,6 +18,7 @@ import java.lang.annotation.Annotation; import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Executable; import java.lang.reflect.Method; import java.lang.reflect.Parameter; import java.util.ArrayList; @@ -83,6 +84,7 @@ * * @param the annotation to search for and synthesize * @author Josh Cummings + * @author DingHao * @since 6.4 */ final class UniqueSecurityAnnotationScanner extends AbstractSecurityAnnotationScanner { @@ -107,7 +109,7 @@ final class UniqueSecurityAnnotationScanner extends Abstra MergedAnnotation merge(AnnotatedElement element, Class targetClass) { if (element instanceof Parameter parameter) { return this.uniqueParameterAnnotationCache.computeIfAbsent(parameter, (p) -> { - List> annotations = findDirectAnnotations(p); + List> annotations = findParameterAnnotations(p); return requireUnique(p, annotations); }); } @@ -137,6 +139,56 @@ private MergedAnnotation requireUnique(AnnotatedElement element, List> findParameterAnnotations(Parameter current) { + List> directAnnotations = findDirectAnnotations(current); + if (!directAnnotations.isEmpty()) { + return directAnnotations; + } + Executable executable = current.getDeclaringExecutable(); + if (executable instanceof Method method) { + Class clazz = method.getDeclaringClass(); + Set> visited = new HashSet<>(); + while (clazz != null && clazz != Object.class) { + directAnnotations = findClosestParameterAnnotations(method, clazz, current, visited); + if (!directAnnotations.isEmpty()) { + return directAnnotations; + } + clazz = clazz.getSuperclass(); + } + } + return Collections.emptyList(); + } + + private List> findClosestParameterAnnotations(Method method, Class clazz, Parameter current, + Set> visited) { + if (!visited.add(clazz)) { + return Collections.emptyList(); + } + List> annotations = new ArrayList<>(findDirectParameterAnnotations(method, clazz, current)); + for (Class ifc : clazz.getInterfaces()) { + annotations.addAll(findClosestParameterAnnotations(method, ifc, current, visited)); + } + return annotations; + } + + private List> findDirectParameterAnnotations(Method method, Class clazz, Parameter current) { + try { + Method methodToUse = clazz.getDeclaredMethod(method.getName(), method.getParameterTypes()); + for (Parameter parameter : methodToUse.getParameters()) { + if (parameter.getName().equals(current.getName())) { + List> directAnnotations = findDirectAnnotations(parameter); + if (!directAnnotations.isEmpty()) { + return directAnnotations; + } + } + } + } + catch (NoSuchMethodException ex) { + // move on + } + return Collections.emptyList(); + } + private List> findMethodAnnotations(Method method, Class targetClass) { // The method may be on an interface, but we need attributes from the target // class. diff --git a/core/src/test/java/org/springframework/security/core/annotation/UniqueSecurityAnnotationScannerTests.java b/core/src/test/java/org/springframework/security/core/annotation/UniqueSecurityAnnotationScannerTests.java index b1a7a779aac..976e0879ab1 100644 --- a/core/src/test/java/org/springframework/security/core/annotation/UniqueSecurityAnnotationScannerTests.java +++ b/core/src/test/java/org/springframework/security/core/annotation/UniqueSecurityAnnotationScannerTests.java @@ -16,7 +16,13 @@ package org.springframework.security.core.annotation; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.util.List; import org.junit.jupiter.api.Test; @@ -34,6 +40,9 @@ public class UniqueSecurityAnnotationScannerTests { private UniqueSecurityAnnotationScanner scanner = new UniqueSecurityAnnotationScanner<>( PreAuthorize.class); + private UniqueSecurityAnnotationScanner parameterScanner = new UniqueSecurityAnnotationScanner<>( + CustomParameterAnnotation.class); + @Test void scanWhenAnnotationOnInterfaceThenResolves() throws Exception { Method method = AnnotationOnInterface.class.getDeclaredMethod("method"); @@ -251,6 +260,101 @@ void scanWhenClassInheritingAbstractClassNoAnnotationsThenNoAnnotation() throws assertThat(preAuthorize).isNull(); } + @Test + void scanParameterAnnotationWhenAnnotationOnInterface() throws Exception { + Parameter parameter = UserService.class.getDeclaredMethod("add", String.class).getParameters()[0]; + CustomParameterAnnotation customParameterAnnotation = this.parameterScanner.scan(parameter); + assertThat(customParameterAnnotation.value()).isEqualTo("one"); + } + + @Test + void scanParameterAnnotationWhenClassInheritingInterfaceAnnotation() throws Exception { + Parameter parameter = UserServiceImpl.class.getDeclaredMethod("add", String.class).getParameters()[0]; + CustomParameterAnnotation customParameterAnnotation = this.parameterScanner.scan(parameter); + assertThat(customParameterAnnotation.value()).isEqualTo("one"); + } + + @Test + void scanParameterAnnotationWhenClassOverridingMethodOverridingInterface() throws Exception { + Parameter parameter = UserServiceImpl.class.getDeclaredMethod("get", String.class).getParameters()[0]; + CustomParameterAnnotation customParameterAnnotation = this.parameterScanner.scan(parameter); + assertThat(customParameterAnnotation.value()).isEqualTo("five"); + } + + @Test + void scanParameterAnnotationWhenMultipleMethodInheritanceThenException() throws Exception { + Parameter parameter = UserServiceImpl.class.getDeclaredMethod("list", String.class).getParameters()[0]; + assertThatExceptionOfType(AnnotationConfigurationException.class) + .isThrownBy(() -> this.parameterScanner.scan(parameter)); + } + + @Test + void scanParameterAnnotationWhenInterfaceNoAnnotationsThenException() throws Exception { + Parameter parameter = UserServiceImpl.class.getDeclaredMethod("delete", String.class).getParameters()[0]; + assertThatExceptionOfType(AnnotationConfigurationException.class) + .isThrownBy(() -> this.parameterScanner.scan(parameter)); + } + + interface UserService { + + void add(@CustomParameterAnnotation("one") String user); + + List list(@CustomParameterAnnotation("two") String user); + + String get(@CustomParameterAnnotation("three") String user); + + void delete(@CustomParameterAnnotation("five") String user); + + } + + interface OtherUserService { + + List list(@CustomParameterAnnotation("four") String user); + + } + + interface ThirdPartyUserService { + + void delete(@CustomParameterAnnotation("five") String user); + + } + + interface RemoteUserService extends ThirdPartyUserService { + + } + + static class UserServiceImpl implements UserService, OtherUserService, RemoteUserService { + + @Override + public void add(String user) { + + } + + @Override + public List list(String user) { + return List.of(user); + } + + @Override + public String get(@CustomParameterAnnotation("five") String user) { + return user; + } + + @Override + public void delete(String user) { + + } + + } + + @Target({ ElementType.PARAMETER }) + @Retention(RetentionPolicy.RUNTIME) + @interface CustomParameterAnnotation { + + String value(); + + } + @PreAuthorize("one") private interface AnnotationOnInterface {