Skip to content

Commit 0484e3c

Browse files
committed
Adjust Annotation Hierarchy Search
- Now searches methods and classes together in the hierarchical search instead of first the method hierarchy and then the class hierarchy - Stops when it finds annotations on a method or class, sees if the closest annotation is not duplicated. Closes spring-projectsgh-15097
1 parent f7657fe commit 0484e3c

9 files changed

+536
-104
lines changed

core/src/main/java/org/springframework/security/authorization/method/AbstractExpressionAttributeRegistry.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,10 @@
1717
package org.springframework.security.authorization.method;
1818

1919
import java.lang.annotation.Annotation;
20-
import java.lang.reflect.AnnotatedElement;
2120
import java.lang.reflect.Method;
2221
import java.util.Map;
2322
import java.util.concurrent.ConcurrentHashMap;
24-
import java.util.function.Function;
23+
import java.util.function.BiFunction;
2524

2625
import org.aopalliance.intercept.MethodInvocation;
2726

@@ -68,7 +67,7 @@ final T getAttribute(Method method, Class<?> targetClass) {
6867
return this.cachedAttributes.computeIfAbsent(cacheKey, (k) -> resolveAttribute(method, targetClass));
6968
}
7069

71-
final <A extends Annotation> Function<AnnotatedElement, A> findUniqueAnnotation(Class<A> type) {
70+
final <A extends Annotation> BiFunction<Method, Class<?>, A> findUniqueAnnotation(Class<A> type) {
7271
return (this.defaults != null) ? AuthorizationAnnotationUtils.withDefaults(type, this.defaults)
7372
: AuthorizationAnnotationUtils.withDefaults(type);
7473
}

core/src/main/java/org/springframework/security/authorization/method/AuthorizationAnnotationUtils.java

Lines changed: 113 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,15 @@
1919
import java.lang.annotation.Annotation;
2020
import java.lang.reflect.AnnotatedElement;
2121
import java.lang.reflect.Method;
22+
import java.util.ArrayList;
23+
import java.util.Collection;
24+
import java.util.Collections;
2225
import java.util.HashMap;
26+
import java.util.HashSet;
2327
import java.util.List;
2428
import java.util.Map;
29+
import java.util.Set;
30+
import java.util.function.BiFunction;
2531
import java.util.function.Function;
2632

2733
import org.springframework.core.annotation.AnnotationConfigurationException;
@@ -55,7 +61,7 @@
5561
*/
5662
final class AuthorizationAnnotationUtils {
5763

58-
static <A extends Annotation> Function<AnnotatedElement, A> withDefaults(Class<A> type,
64+
static <A extends Annotation> BiFunction<Method, Class<?>, A> withDefaults(Class<A> type,
5965
PrePostTemplateDefaults defaults) {
6066
Function<MergedAnnotation<A>, A> map = (mergedAnnotation) -> {
6167
if (mergedAnnotation.getMetaSource() == null) {
@@ -79,19 +85,25 @@ static <A extends Annotation> Function<AnnotatedElement, A> withDefaults(Class<A
7985
properties.put("value", value);
8086
return MergedAnnotation.of(annotatedElement, type, properties).synthesize();
8187
};
82-
return (annotatedElement) -> findDistinctAnnotation(annotatedElement, type, map);
88+
return (method, targetClass) -> findDistinctAnnotation(method, targetClass, type, map);
8389
}
8490

85-
static <A extends Annotation> Function<AnnotatedElement, A> withDefaults(Class<A> type) {
86-
return (annotatedElement) -> findDistinctAnnotation(annotatedElement, type, MergedAnnotation::synthesize);
91+
static <A extends Annotation> BiFunction<Method, Class<?>, A> withDefaults(Class<A> type) {
92+
return (method, targetClass) -> findDistinctAnnotation(method, targetClass, type, MergedAnnotation::synthesize);
93+
}
94+
95+
static BiFunction<Method, Class<?>, Annotation> withDefaults(
96+
Collection<Class<? extends Annotation>> annotationTypes) {
97+
return (method, targetClass) -> findDistinctAnnotation(method, targetClass, annotationTypes,
98+
MergedAnnotation::synthesize);
8799
}
88100

89101
static <A extends Annotation> A findUniqueAnnotation(Method method, Class<A> annotationType) {
90-
return findDistinctAnnotation(method, annotationType, MergedAnnotation::synthesize);
102+
return findDistinctAnnotation(method, method.getDeclaringClass(), annotationType, MergedAnnotation::synthesize);
91103
}
92104

93105
static <A extends Annotation> A findUniqueAnnotation(Class<?> type, Class<A> annotationType) {
94-
return findDistinctAnnotation(type, annotationType, MergedAnnotation::synthesize);
106+
return findDistinctAnnotation(null, type, annotationType, MergedAnnotation::synthesize);
95107
}
96108

97109
/**
@@ -110,7 +122,7 @@ static <A extends Annotation> A findUniqueAnnotation(Class<?> type, Class<A> ann
110122
*/
111123
static <A extends Annotation> A findUniqueAnnotation(Method method, Class<A> annotationType,
112124
Function<MergedAnnotation<A>, A> map) {
113-
return findDistinctAnnotation(method, annotationType, map);
125+
return findDistinctAnnotation(method, method.getDeclaringClass(), annotationType, map);
114126
}
115127

116128
/**
@@ -129,41 +141,114 @@ static <A extends Annotation> A findUniqueAnnotation(Method method, Class<A> ann
129141
*/
130142
static <A extends Annotation> A findUniqueAnnotation(Class<?> type, Class<A> annotationType,
131143
Function<MergedAnnotation<A>, A> map) {
132-
return findDistinctAnnotation(type, annotationType, map);
144+
return findDistinctAnnotation(null, type, annotationType, map);
133145
}
134146

135-
private static <A extends Annotation> A findDistinctAnnotation(AnnotatedElement annotatedElement,
147+
private static <A extends Annotation> A findDistinctAnnotation(Method method, Class<?> targetClass,
136148
Class<A> annotationType, Function<MergedAnnotation<A>, A> map) {
137-
MergedAnnotations mergedAnnotations = MergedAnnotations.from(annotatedElement, SearchStrategy.DIRECT,
138-
RepeatableContainers.none());
139-
List<A> annotations = mergedAnnotations.stream(annotationType)
140-
.map(MergedAnnotation::withNonMergedAttributes)
141-
.map(map)
142-
.distinct()
143-
.toList();
149+
Function<MergedAnnotation<Annotation>, Annotation> generic = (merged) -> map
150+
.apply((MergedAnnotation<A>) merged);
151+
return (A) findDistinctAnnotation(method, targetClass, List.of(annotationType), generic);
152+
}
144153

145-
if (annotations.isEmpty()) {
146-
mergedAnnotations = MergedAnnotations.from(annotatedElement, SearchStrategy.TYPE_HIERARCHY,
147-
RepeatableContainers.none());
148-
annotations = mergedAnnotations.stream(annotationType)
149-
.map(MergedAnnotation::withNonMergedAttributes)
150-
.map(map)
151-
.distinct()
152-
.toList();
154+
private static <A extends Annotation> A findDistinctAnnotation(Method method, Class<?> targetClass,
155+
Collection<Class<? extends Annotation>> annotationTypes,
156+
Function<MergedAnnotation<Annotation>, Annotation> map) {
157+
List<AnnotationScore<Annotation>> scores = findAnnotations(method, targetClass, annotationTypes, map,
158+
new HashSet<>(), 1);
159+
int lowestScore = Integer.MAX_VALUE;
160+
List<Annotation> annotations = new ArrayList<>();
161+
for (AnnotationScore<Annotation> score : scores) {
162+
if (score.score < lowestScore) {
163+
annotations = new ArrayList<>();
164+
annotations.add(score.annotation);
165+
lowestScore = score.score;
166+
}
167+
else if (score.score == lowestScore) {
168+
annotations.add(score.annotation);
169+
}
153170
}
154-
155171
return switch (annotations.size()) {
156172
case 0 -> null;
157-
case 1 -> annotations.get(0);
173+
case 1 -> (A) annotations.get(0);
158174
default -> throw new AnnotationConfigurationException("""
159175
Please ensure there is one unique annotation of type @%s attributed to %s. \
160-
Found %d competing annotations: %s""".formatted(annotationType.getName(), annotatedElement,
161-
annotations.size(), annotations));
176+
Found %d competing annotations: %s""".formatted(annotationTypes, method, annotations.size(),
177+
annotations));
162178
};
163179
}
164180

181+
private static List<AnnotationScore<Annotation>> findAnnotations(Method method, Class<?> declaringClass,
182+
Collection<Class<? extends Annotation>> annotationTypes,
183+
Function<MergedAnnotation<Annotation>, Annotation> map, Set<Class<?>> visited, int distance) {
184+
if (declaringClass == null || visited.contains(declaringClass) || declaringClass == Object.class) {
185+
return Collections.emptyList();
186+
}
187+
visited.add(declaringClass);
188+
189+
Method methodToUse = methodMatching(method, declaringClass);
190+
if (methodToUse != null) {
191+
List<AnnotationScore<Annotation>> scores = findAnnotations(methodToUse, annotationTypes, map,
192+
distance * 11);
193+
if (!scores.isEmpty()) {
194+
return scores;
195+
}
196+
}
197+
198+
List<AnnotationScore<Annotation>> scores = findAnnotations(declaringClass, annotationTypes, map, distance * 13);
199+
if (!scores.isEmpty()) {
200+
return scores;
201+
}
202+
203+
scores.addAll(findAnnotations(methodToUse, declaringClass.getSuperclass(), annotationTypes, map, visited,
204+
distance + 1));
205+
for (Class<?> inter : declaringClass.getInterfaces()) {
206+
scores.addAll(findAnnotations(methodToUse, inter, annotationTypes, map, visited, distance + 1));
207+
}
208+
return scores;
209+
}
210+
211+
private static Method methodMatching(Method method, Class<?> candidate) {
212+
if (method == null) {
213+
return null;
214+
}
215+
if (method.getDeclaringClass() == candidate) {
216+
return method;
217+
}
218+
try {
219+
return candidate.getDeclaredMethod(method.getName(), method.getParameterTypes());
220+
}
221+
catch (Exception ex) {
222+
return method;
223+
}
224+
}
225+
226+
private static List<AnnotationScore<Annotation>> findAnnotations(AnnotatedElement element,
227+
Collection<Class<? extends Annotation>> annotationTypes,
228+
Function<MergedAnnotation<Annotation>, Annotation> map, int score) {
229+
List<Annotation> annotations = MergedAnnotations
230+
.from(element, SearchStrategy.DIRECT, RepeatableContainers.none())
231+
.stream()
232+
.filter((annotation) -> annotationTypes.contains(annotation.getType()))
233+
.map(MergedAnnotation::withNonMergedAttributes)
234+
.map(map)
235+
.toList();
236+
List<AnnotationScore<Annotation>> scores = new ArrayList<>();
237+
for (Annotation annotation : annotations) {
238+
scores.add(new AnnotationScore<>(annotation, score));
239+
}
240+
return scores;
241+
}
242+
165243
private AuthorizationAnnotationUtils() {
166244

167245
}
168246

247+
private record AnnotationScore<A extends Annotation>(A annotation, int score) {
248+
@Override
249+
public String toString() {
250+
return this.annotation.toString();
251+
}
252+
}
253+
169254
}

core/src/main/java/org/springframework/security/authorization/method/Jsr250AuthorizationManager.java

Lines changed: 2 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@
2929
import org.aopalliance.intercept.MethodInvocation;
3030

3131
import org.springframework.aop.support.AopUtils;
32-
import org.springframework.core.annotation.AnnotationConfigurationException;
3332
import org.springframework.lang.NonNull;
3433
import org.springframework.security.authorization.AuthoritiesAuthorizationManager;
3534
import org.springframework.security.authorization.AuthorizationDecision;
@@ -121,45 +120,8 @@ AuthorizationManager<MethodInvocation> resolveManager(Method method, Class<?> ta
121120

122121
private Annotation findJsr250Annotation(Method method, Class<?> targetClass) {
123122
Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass);
124-
Annotation annotation = findAnnotation(specificMethod);
125-
return (annotation != null) ? annotation
126-
: findAnnotation((targetClass != null) ? targetClass : specificMethod.getDeclaringClass());
127-
}
128-
129-
private Annotation findAnnotation(Method method) {
130-
Set<Annotation> annotations = new HashSet<>();
131-
for (Class<? extends Annotation> annotationClass : JSR250_ANNOTATIONS) {
132-
Annotation annotation = AuthorizationAnnotationUtils.findUniqueAnnotation(method, annotationClass);
133-
if (annotation != null) {
134-
annotations.add(annotation);
135-
}
136-
}
137-
if (annotations.isEmpty()) {
138-
return null;
139-
}
140-
if (annotations.size() > 1) {
141-
throw new AnnotationConfigurationException(
142-
"The JSR-250 specification disallows DenyAll, PermitAll, and RolesAllowed from appearing on the same method.");
143-
}
144-
return annotations.iterator().next();
145-
}
146-
147-
private Annotation findAnnotation(Class<?> clazz) {
148-
Set<Annotation> annotations = new HashSet<>();
149-
for (Class<? extends Annotation> annotationClass : JSR250_ANNOTATIONS) {
150-
Annotation annotation = AuthorizationAnnotationUtils.findUniqueAnnotation(clazz, annotationClass);
151-
if (annotation != null) {
152-
annotations.add(annotation);
153-
}
154-
}
155-
if (annotations.isEmpty()) {
156-
return null;
157-
}
158-
if (annotations.size() > 1) {
159-
throw new AnnotationConfigurationException(
160-
"The JSR-250 specification disallows DenyAll, PermitAll, and RolesAllowed from appearing on the same class definition.");
161-
}
162-
return annotations.iterator().next();
123+
return AuthorizationAnnotationUtils.withDefaults(JSR250_ANNOTATIONS)
124+
.apply(specificMethod, (targetClass != null) ? targetClass : specificMethod.getDeclaringClass());
163125
}
164126

165127
private Set<String> getAllowedRolesWithPrefix(RolesAllowed rolesAllowed) {

core/src/main/java/org/springframework/security/authorization/method/PostAuthorizeExpressionAttributeRegistry.java

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@
1616

1717
package org.springframework.security.authorization.method;
1818

19-
import java.lang.reflect.AnnotatedElement;
2019
import java.lang.reflect.Method;
2120
import java.util.Arrays;
21+
import java.util.function.BiFunction;
2222
import java.util.function.Function;
2323

2424
import reactor.util.annotation.NonNull;
@@ -60,23 +60,17 @@ ExpressionAttribute resolveAttribute(Method method, Class<?> targetClass) {
6060
}
6161

6262
private MethodAuthorizationDeniedHandler resolveHandler(Method method, Class<?> targetClass) {
63-
Function<AnnotatedElement, HandleAuthorizationDenied> lookup = AuthorizationAnnotationUtils
63+
BiFunction<Method, Class<?>, HandleAuthorizationDenied> lookup = AuthorizationAnnotationUtils
6464
.withDefaults(HandleAuthorizationDenied.class);
65-
HandleAuthorizationDenied deniedHandler = lookup.apply(method);
66-
if (deniedHandler != null) {
67-
return this.handlerResolver.apply(deniedHandler.handlerClass());
68-
}
69-
deniedHandler = lookup.apply(targetClass(method, targetClass));
65+
HandleAuthorizationDenied deniedHandler = lookup.apply(method, targetClass(method, targetClass));
7066
if (deniedHandler != null) {
7167
return this.handlerResolver.apply(deniedHandler.handlerClass());
7268
}
7369
return this.defaultHandler;
7470
}
7571

7672
private PostAuthorize findPostAuthorizeAnnotation(Method method, Class<?> targetClass) {
77-
Function<AnnotatedElement, PostAuthorize> lookup = findUniqueAnnotation(PostAuthorize.class);
78-
PostAuthorize postAuthorize = lookup.apply(method);
79-
return (postAuthorize != null) ? postAuthorize : lookup.apply(targetClass(method, targetClass));
73+
return findUniqueAnnotation(PostAuthorize.class).apply(method, targetClass(method, targetClass));
8074
}
8175

8276
/**

core/src/main/java/org/springframework/security/authorization/method/PostFilterExpressionAttributeRegistry.java

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,7 @@
1616

1717
package org.springframework.security.authorization.method;
1818

19-
import java.lang.reflect.AnnotatedElement;
2019
import java.lang.reflect.Method;
21-
import java.util.function.Function;
2220

2321
import org.springframework.aop.support.AopUtils;
2422
import org.springframework.expression.Expression;
@@ -48,9 +46,7 @@ ExpressionAttribute resolveAttribute(Method method, Class<?> targetClass) {
4846
}
4947

5048
private PostFilter findPostFilterAnnotation(Method method, Class<?> targetClass) {
51-
Function<AnnotatedElement, PostFilter> lookup = findUniqueAnnotation(PostFilter.class);
52-
PostFilter postFilter = lookup.apply(method);
53-
return (postFilter != null) ? postFilter : lookup.apply(targetClass(method, targetClass));
49+
return findUniqueAnnotation(PostFilter.class).apply(method, targetClass(method, targetClass));
5450
}
5551

5652
}

core/src/main/java/org/springframework/security/authorization/method/PreAuthorizeExpressionAttributeRegistry.java

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@
1616

1717
package org.springframework.security.authorization.method;
1818

19-
import java.lang.reflect.AnnotatedElement;
2019
import java.lang.reflect.Method;
2120
import java.util.Arrays;
21+
import java.util.function.BiFunction;
2222
import java.util.function.Function;
2323

2424
import reactor.util.annotation.NonNull;
@@ -60,23 +60,17 @@ ExpressionAttribute resolveAttribute(Method method, Class<?> targetClass) {
6060
}
6161

6262
private MethodAuthorizationDeniedHandler resolveHandler(Method method, Class<?> targetClass) {
63-
Function<AnnotatedElement, HandleAuthorizationDenied> lookup = AuthorizationAnnotationUtils
63+
BiFunction<Method, Class<?>, HandleAuthorizationDenied> lookup = AuthorizationAnnotationUtils
6464
.withDefaults(HandleAuthorizationDenied.class);
65-
HandleAuthorizationDenied deniedHandler = lookup.apply(method);
66-
if (deniedHandler != null) {
67-
return this.handlerResolver.apply(deniedHandler.handlerClass());
68-
}
69-
deniedHandler = lookup.apply(targetClass(method, targetClass));
65+
HandleAuthorizationDenied deniedHandler = lookup.apply(method, targetClass(method, targetClass));
7066
if (deniedHandler != null) {
7167
return this.handlerResolver.apply(deniedHandler.handlerClass());
7268
}
7369
return this.defaultHandler;
7470
}
7571

7672
private PreAuthorize findPreAuthorizeAnnotation(Method method, Class<?> targetClass) {
77-
Function<AnnotatedElement, PreAuthorize> lookup = findUniqueAnnotation(PreAuthorize.class);
78-
PreAuthorize preAuthorize = lookup.apply(method);
79-
return (preAuthorize != null) ? preAuthorize : lookup.apply(targetClass(method, targetClass));
73+
return findUniqueAnnotation(PreAuthorize.class).apply(method, targetClass(method, targetClass));
8074
}
8175

8276
/**

core/src/main/java/org/springframework/security/authorization/method/PreFilterExpressionAttributeRegistry.java

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,7 @@
1616

1717
package org.springframework.security.authorization.method;
1818

19-
import java.lang.reflect.AnnotatedElement;
2019
import java.lang.reflect.Method;
21-
import java.util.function.Function;
2220

2321
import org.springframework.aop.support.AopUtils;
2422
import org.springframework.expression.Expression;
@@ -49,9 +47,7 @@ PreFilterExpressionAttribute resolveAttribute(Method method, Class<?> targetClas
4947
}
5048

5149
private PreFilter findPreFilterAnnotation(Method method, Class<?> targetClass) {
52-
Function<AnnotatedElement, PreFilter> lookup = findUniqueAnnotation(PreFilter.class);
53-
PreFilter preFilter = lookup.apply(method);
54-
return (preFilter != null) ? preFilter : lookup.apply(targetClass(method, targetClass));
50+
return findUniqueAnnotation(PreFilter.class).apply(method, targetClass(method, targetClass));
5551
}
5652

5753
static final class PreFilterExpressionAttribute extends ExpressionAttribute {

0 commit comments

Comments
 (0)