Browse Source

Support Meta-Annotation Parameters on Parameter Annotations

Closes gh-16248
github-actions[bot] 8 months ago
parent
commit
95ec49a21d

+ 53 - 1
core/src/main/java/org/springframework/security/core/annotation/UniqueSecurityAnnotationScanner.java

@@ -18,6 +18,7 @@ package org.springframework.security.core.annotation;
 
 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 @@ import org.springframework.util.ClassUtils;
  *
  * @param <A> the annotation to search for and synthesize
  * @author Josh Cummings
+ * @author DingHao
  * @since 6.4
  */
 final class UniqueSecurityAnnotationScanner<A extends Annotation> extends AbstractSecurityAnnotationScanner<A> {
@@ -107,7 +109,7 @@ final class UniqueSecurityAnnotationScanner<A extends Annotation> extends Abstra
 	MergedAnnotation<A> merge(AnnotatedElement element, Class<?> targetClass) {
 		if (element instanceof Parameter parameter) {
 			return this.uniqueParameterAnnotationCache.computeIfAbsent(parameter, (p) -> {
-				List<MergedAnnotation<A>> annotations = findDirectAnnotations(p);
+				List<MergedAnnotation<A>> annotations = findParameterAnnotations(p);
 				return requireUnique(p, annotations);
 			});
 		}
@@ -137,6 +139,56 @@ final class UniqueSecurityAnnotationScanner<A extends Annotation> extends Abstra
 		};
 	}
 
+	private List<MergedAnnotation<A>> findParameterAnnotations(Parameter current) {
+		List<MergedAnnotation<A>> directAnnotations = findDirectAnnotations(current);
+		if (!directAnnotations.isEmpty()) {
+			return directAnnotations;
+		}
+		Executable executable = current.getDeclaringExecutable();
+		if (executable instanceof Method method) {
+			Class<?> clazz = method.getDeclaringClass();
+			Set<Class<?>> 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<MergedAnnotation<A>> findClosestParameterAnnotations(Method method, Class<?> clazz, Parameter current,
+			Set<Class<?>> visited) {
+		if (!visited.add(clazz)) {
+			return Collections.emptyList();
+		}
+		List<MergedAnnotation<A>> annotations = new ArrayList<>(findDirectParameterAnnotations(method, clazz, current));
+		for (Class<?> ifc : clazz.getInterfaces()) {
+			annotations.addAll(findClosestParameterAnnotations(method, ifc, current, visited));
+		}
+		return annotations;
+	}
+
+	private List<MergedAnnotation<A>> 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<MergedAnnotation<A>> directAnnotations = findDirectAnnotations(parameter);
+					if (!directAnnotations.isEmpty()) {
+						return directAnnotations;
+					}
+				}
+			}
+		}
+		catch (NoSuchMethodException ex) {
+			// move on
+		}
+		return Collections.emptyList();
+	}
+
 	private List<MergedAnnotation<A>> findMethodAnnotations(Method method, Class<?> targetClass) {
 		// The method may be on an interface, but we need attributes from the target
 		// class.

+ 104 - 0
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<PreAuthorize> scanner = new UniqueSecurityAnnotationScanner<>(
 			PreAuthorize.class);
 
+	private UniqueSecurityAnnotationScanner<CustomParameterAnnotation> parameterScanner = new UniqueSecurityAnnotationScanner<>(
+			CustomParameterAnnotation.class);
+
 	@Test
 	void scanWhenAnnotationOnInterfaceThenResolves() throws Exception {
 		Method method = AnnotationOnInterface.class.getDeclaredMethod("method");
@@ -251,6 +260,101 @@ public class UniqueSecurityAnnotationScannerTests {
 		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<String> list(@CustomParameterAnnotation("two") String user);
+
+		String get(@CustomParameterAnnotation("three") String user);
+
+		void delete(@CustomParameterAnnotation("five") String user);
+
+	}
+
+	interface OtherUserService {
+
+		List<String> 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<String> 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 {