Add AnnotationSythesizer API

Closes gh-13234
Closes gh-13490
Closes gh-15097
This commit is contained in:
Josh Cummings 2024-07-18 09:15:04 -06:00
parent e3438aa36a
commit c736e075c1
No known key found for this signature in database
GPG Key ID: A306A51F43B8E5A5
17 changed files with 1183 additions and 464 deletions

View File

@ -47,6 +47,8 @@ public class PreAuthorizeAspectTests {
private PrePostSecured prePostSecured = new PrePostSecured();
private MultipleInterfaces multiple = new MultipleInterfaces();
@BeforeEach
public final void setUp() {
MockitoAnnotations.initMocks(this);
@ -110,6 +112,12 @@ public class PreAuthorizeAspectTests {
.isThrownBy(() -> this.secured.myObject().denyAllMethod());
}
@Test
public void multipleInterfacesPreAuthorizeAllows() {
// aspectj doesn't inherit annotations
this.multiple.securedMethod();
}
interface SecuredInterface {
@PreAuthorize("hasRole('X')")
@ -177,4 +185,19 @@ public class PreAuthorizeAspectTests {
}
interface AnotherSecuredInterface {
@PreAuthorize("hasRole('Y')")
void securedMethod();
}
static class MultipleInterfaces implements SecuredInterface, AnotherSecuredInterface {
@Override
public void securedMethod() {
}
}
}

View File

@ -16,12 +16,9 @@
package org.springframework.security.authorization.method;
import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import org.aopalliance.intercept.MethodInvocation;
@ -43,8 +40,6 @@ abstract class AbstractExpressionAttributeRegistry<T extends ExpressionAttribute
private MethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler();
private PrePostTemplateDefaults defaults;
/**
* Returns an {@link ExpressionAttribute} for the {@link MethodInvocation}.
* @param mi the {@link MethodInvocation} to use
@ -68,11 +63,6 @@ abstract class AbstractExpressionAttributeRegistry<T extends ExpressionAttribute
return this.cachedAttributes.computeIfAbsent(cacheKey, (k) -> resolveAttribute(method, targetClass));
}
final <A extends Annotation> Function<AnnotatedElement, A> findUniqueAnnotation(Class<A> type) {
return (this.defaults != null) ? AuthorizationAnnotationUtils.withDefaults(type, this.defaults)
: AuthorizationAnnotationUtils.withDefaults(type);
}
/**
* Returns the {@link MethodSecurityExpressionHandler}.
* @return the {@link MethodSecurityExpressionHandler} to use
@ -86,10 +76,6 @@ abstract class AbstractExpressionAttributeRegistry<T extends ExpressionAttribute
this.expressionHandler = expressionHandler;
}
void setTemplateDefaults(PrePostTemplateDefaults defaults) {
this.defaults = defaults;
}
/**
* Subclasses should implement this method to provide the non-null
* {@link ExpressionAttribute} for the method and the target class.

View File

@ -1,155 +0,0 @@
/*
* 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.authorization.method;
import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import org.springframework.core.annotation.AnnotationConfigurationException;
import org.springframework.core.annotation.MergedAnnotation;
import org.springframework.core.annotation.MergedAnnotations;
import org.springframework.core.annotation.MergedAnnotations.SearchStrategy;
import org.springframework.core.annotation.RepeatableContainers;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.util.PropertyPlaceholderHelper;
/**
* A collection of utility methods that check for, and error on, conflicting annotations.
* This is specifically important for Spring Security annotations which are not designed
* to be repeatable.
*
* <p>
* There are numerous ways that two annotations of the same type may be attached to the
* same method. For example, a class may implement a method defined in two separate
* interfaces. If both of those interfaces have a {@code @PreAuthorize} annotation, then
* it's unclear which {@code @PreAuthorize} expression Spring Security should use.
*
* <p>
* Another way is when one of Spring Security's annotations is used as a meta-annotation.
* In that case, two custom annotations can be declared, each with their own
* {@code @PreAuthorize} declaration. If both custom annotations are used on the same
* method, then it's unclear which {@code @PreAuthorize} expression Spring Security should
* use.
*
* @author Josh Cummings
* @author Sam Brannen
*/
final class AuthorizationAnnotationUtils {
static <A extends Annotation> Function<AnnotatedElement, A> withDefaults(Class<A> type,
PrePostTemplateDefaults defaults) {
Function<MergedAnnotation<A>, A> map = (mergedAnnotation) -> {
if (mergedAnnotation.getMetaSource() == null) {
return mergedAnnotation.synthesize();
}
PropertyPlaceholderHelper helper = new PropertyPlaceholderHelper("{", "}", null,
defaults.isIgnoreUnknown());
String expression = (String) mergedAnnotation.asMap().get("value");
Map<String, Object> annotationProperties = mergedAnnotation.getMetaSource().asMap();
Map<String, String> stringProperties = new HashMap<>();
for (Map.Entry<String, Object> property : annotationProperties.entrySet()) {
String key = property.getKey();
Object value = property.getValue();
String asString = (value instanceof String) ? (String) value
: DefaultConversionService.getSharedInstance().convert(value, String.class);
stringProperties.put(key, asString);
}
AnnotatedElement annotatedElement = (AnnotatedElement) mergedAnnotation.getSource();
String value = helper.replacePlaceholders(expression, stringProperties::get);
Map<String, Object> properties = new HashMap<>(mergedAnnotation.asMap());
properties.put("value", value);
return MergedAnnotation.of(annotatedElement, type, properties).synthesize();
};
return (annotatedElement) -> findDistinctAnnotation(annotatedElement, type, map);
}
static <A extends Annotation> Function<AnnotatedElement, A> withDefaults(Class<A> type) {
return (annotatedElement) -> findDistinctAnnotation(annotatedElement, type, MergedAnnotation::synthesize);
}
static <A extends Annotation> A findUniqueAnnotation(Method method, Class<A> annotationType) {
return findDistinctAnnotation(method, annotationType, MergedAnnotation::synthesize);
}
static <A extends Annotation> A findUniqueAnnotation(Class<?> type, Class<A> annotationType) {
return findDistinctAnnotation(type, annotationType, MergedAnnotation::synthesize);
}
/**
* Perform an exhaustive search on the type hierarchy of the given {@link Method} for
* the annotation of type {@code annotationType}, including any annotations using
* {@code annotationType} as a meta-annotation.
*
* <p>
* If more than one unique annotation is found, then throw an error.
* @param method the method declaration to search from
* @param annotationType the annotation type to search for
* @return a unique instance of the annotation attributed to the method, {@code null}
* otherwise
* @throws AnnotationConfigurationException if more than one unique instance of the
* annotation is found
*/
static <A extends Annotation> A findUniqueAnnotation(Method method, Class<A> annotationType,
Function<MergedAnnotation<A>, A> map) {
return findDistinctAnnotation(method, annotationType, map);
}
/**
* Perform an exhaustive search on the type hierarchy of the given {@link Class} for
* the annotation of type {@code annotationType}, including any annotations using
* {@code annotationType} as a meta-annotation.
*
* <p>
* If more than one unique annotation is found, then throw an error.
* @param type the type to search from
* @param annotationType the annotation type to search for
* @return a unique instance of the annotation attributed to the class, {@code null}
* otherwise
* @throws AnnotationConfigurationException if more than one unique instance of the
* annotation is found
*/
static <A extends Annotation> A findUniqueAnnotation(Class<?> type, Class<A> annotationType,
Function<MergedAnnotation<A>, A> map) {
return findDistinctAnnotation(type, annotationType, map);
}
private static <A extends Annotation> A findDistinctAnnotation(AnnotatedElement annotatedElement,
Class<A> annotationType, Function<MergedAnnotation<A>, A> map) {
MergedAnnotations mergedAnnotations = MergedAnnotations.from(annotatedElement, SearchStrategy.TYPE_HIERARCHY,
RepeatableContainers.none());
List<A> annotations = mergedAnnotations.stream(annotationType).map(map).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() {
}
}

View File

@ -20,6 +20,7 @@ import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Supplier;
@ -29,12 +30,13 @@ import jakarta.annotation.security.RolesAllowed;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.aop.support.AopUtils;
import org.springframework.core.annotation.AnnotationConfigurationException;
import org.springframework.lang.NonNull;
import org.springframework.security.authorization.AuthoritiesAuthorizationManager;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.annotation.AnnotationSynthesizer;
import org.springframework.security.core.annotation.AnnotationSynthesizers;
import org.springframework.util.Assert;
/**
@ -49,14 +51,6 @@ import org.springframework.util.Assert;
*/
public final class Jsr250AuthorizationManager implements AuthorizationManager<MethodInvocation> {
private static final Set<Class<? extends Annotation>> JSR250_ANNOTATIONS = new HashSet<>();
static {
JSR250_ANNOTATIONS.add(DenyAll.class);
JSR250_ANNOTATIONS.add(PermitAll.class);
JSR250_ANNOTATIONS.add(RolesAllowed.class);
}
private final Jsr250AuthorizationManagerRegistry registry = new Jsr250AuthorizationManagerRegistry();
private AuthorizationManager<Collection<String>> authoritiesAuthorizationManager = new AuthoritiesAuthorizationManager();
@ -102,6 +96,9 @@ public final class Jsr250AuthorizationManager implements AuthorizationManager<Me
private final class Jsr250AuthorizationManagerRegistry extends AbstractAuthorizationManagerRegistry {
private final AnnotationSynthesizer<?> synthesizer = AnnotationSynthesizers
.requireUnique(List.of(DenyAll.class, PermitAll.class, RolesAllowed.class));
@NonNull
@Override
AuthorizationManager<MethodInvocation> resolveManager(Method method, Class<?> targetClass) {
@ -121,45 +118,8 @@ public final class Jsr250AuthorizationManager implements AuthorizationManager<Me
private Annotation findJsr250Annotation(Method method, Class<?> targetClass) {
Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass);
Annotation annotation = findAnnotation(specificMethod);
return (annotation != null) ? annotation
: findAnnotation((targetClass != null) ? targetClass : specificMethod.getDeclaringClass());
}
private Annotation findAnnotation(Method method) {
Set<Annotation> annotations = new HashSet<>();
for (Class<? extends Annotation> annotationClass : JSR250_ANNOTATIONS) {
Annotation annotation = AuthorizationAnnotationUtils.findUniqueAnnotation(method, annotationClass);
if (annotation != null) {
annotations.add(annotation);
}
}
if (annotations.isEmpty()) {
return null;
}
if (annotations.size() > 1) {
throw new AnnotationConfigurationException(
"The JSR-250 specification disallows DenyAll, PermitAll, and RolesAllowed from appearing on the same method.");
}
return annotations.iterator().next();
}
private Annotation findAnnotation(Class<?> clazz) {
Set<Annotation> annotations = new HashSet<>();
for (Class<? extends Annotation> annotationClass : JSR250_ANNOTATIONS) {
Annotation annotation = AuthorizationAnnotationUtils.findUniqueAnnotation(clazz, annotationClass);
if (annotation != null) {
annotations.add(annotation);
}
}
if (annotations.isEmpty()) {
return null;
}
if (annotations.size() > 1) {
throw new AnnotationConfigurationException(
"The JSR-250 specification disallows DenyAll, PermitAll, and RolesAllowed from appearing on the same class definition.");
}
return annotations.iterator().next();
Class<?> targetClassToUse = (targetClass != null) ? targetClass : specificMethod.getDeclaringClass();
return this.synthesizer.synthesize(specificMethod, targetClassToUse);
}
private Set<String> getAllowedRolesWithPrefix(RolesAllowed rolesAllowed) {

View File

@ -16,7 +16,6 @@
package org.springframework.security.authorization.method;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.function.Function;
@ -27,6 +26,8 @@ import org.springframework.aop.support.AopUtils;
import org.springframework.context.ApplicationContext;
import org.springframework.expression.Expression;
import org.springframework.security.access.prepost.PostAuthorize;
import org.springframework.security.core.annotation.AnnotationSynthesizer;
import org.springframework.security.core.annotation.AnnotationSynthesizers;
import org.springframework.util.Assert;
/**
@ -40,8 +41,14 @@ final class PostAuthorizeExpressionAttributeRegistry extends AbstractExpressionA
private final MethodAuthorizationDeniedHandler defaultHandler = new ThrowingMethodAuthorizationDeniedHandler();
private final AnnotationSynthesizer<HandleAuthorizationDenied> handleAuthorizationDeniedSynthesizer = AnnotationSynthesizers
.requireUnique(HandleAuthorizationDenied.class);
private Function<Class<? extends MethodAuthorizationDeniedHandler>, MethodAuthorizationDeniedHandler> handlerResolver;
private AnnotationSynthesizer<PostAuthorize> postAuthorizeSynthesizer = AnnotationSynthesizers
.requireUnique(PostAuthorize.class);
PostAuthorizeExpressionAttributeRegistry() {
this.handlerResolver = (clazz) -> this.defaultHandler;
}
@ -60,13 +67,9 @@ final class PostAuthorizeExpressionAttributeRegistry extends AbstractExpressionA
}
private MethodAuthorizationDeniedHandler resolveHandler(Method method, Class<?> targetClass) {
Function<AnnotatedElement, HandleAuthorizationDenied> lookup = AuthorizationAnnotationUtils
.withDefaults(HandleAuthorizationDenied.class);
HandleAuthorizationDenied deniedHandler = lookup.apply(method);
if (deniedHandler != null) {
return this.handlerResolver.apply(deniedHandler.handlerClass());
}
deniedHandler = lookup.apply(targetClass(method, targetClass));
Class<?> targetClassToUse = targetClass(method, targetClass);
HandleAuthorizationDenied deniedHandler = this.handleAuthorizationDeniedSynthesizer.synthesize(method,
targetClassToUse);
if (deniedHandler != null) {
return this.handlerResolver.apply(deniedHandler.handlerClass());
}
@ -74,9 +77,8 @@ final class PostAuthorizeExpressionAttributeRegistry extends AbstractExpressionA
}
private PostAuthorize findPostAuthorizeAnnotation(Method method, Class<?> targetClass) {
Function<AnnotatedElement, PostAuthorize> lookup = findUniqueAnnotation(PostAuthorize.class);
PostAuthorize postAuthorize = lookup.apply(method);
return (postAuthorize != null) ? postAuthorize : lookup.apply(targetClass(method, targetClass));
Class<?> targetClassToUse = targetClass(method, targetClass);
return this.postAuthorizeSynthesizer.synthesize(method, targetClass);
}
/**
@ -89,6 +91,10 @@ final class PostAuthorizeExpressionAttributeRegistry extends AbstractExpressionA
this.handlerResolver = (clazz) -> resolveHandler(context, clazz);
}
void setTemplateDefaults(PrePostTemplateDefaults templateDefaults) {
this.postAuthorizeSynthesizer = AnnotationSynthesizers.requireUnique(PostAuthorize.class, templateDefaults);
}
private MethodAuthorizationDeniedHandler resolveHandler(ApplicationContext context,
Class<? extends MethodAuthorizationDeniedHandler> handlerClass) {
if (handlerClass == this.defaultHandler.getClass()) {

View File

@ -16,14 +16,14 @@
package org.springframework.security.authorization.method;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Method;
import java.util.function.Function;
import org.springframework.aop.support.AopUtils;
import org.springframework.expression.Expression;
import org.springframework.lang.NonNull;
import org.springframework.security.access.prepost.PostFilter;
import org.springframework.security.core.annotation.AnnotationSynthesizer;
import org.springframework.security.core.annotation.AnnotationSynthesizers;
/**
* For internal use only, as this contract is likely to change.
@ -34,6 +34,8 @@ import org.springframework.security.access.prepost.PostFilter;
*/
final class PostFilterExpressionAttributeRegistry extends AbstractExpressionAttributeRegistry<ExpressionAttribute> {
private AnnotationSynthesizer<PostFilter> synthesizer = AnnotationSynthesizers.requireUnique(PostFilter.class);
@NonNull
@Override
ExpressionAttribute resolveAttribute(Method method, Class<?> targetClass) {
@ -47,10 +49,13 @@ final class PostFilterExpressionAttributeRegistry extends AbstractExpressionAttr
return new ExpressionAttribute(postFilterExpression);
}
void setTemplateDefaults(PrePostTemplateDefaults defaults) {
this.synthesizer = AnnotationSynthesizers.requireUnique(PostFilter.class, defaults);
}
private PostFilter findPostFilterAnnotation(Method method, Class<?> targetClass) {
Function<AnnotatedElement, PostFilter> lookup = findUniqueAnnotation(PostFilter.class);
PostFilter postFilter = lookup.apply(method);
return (postFilter != null) ? postFilter : lookup.apply(targetClass(method, targetClass));
Class<?> targetClassToUse = targetClass(method, targetClass);
return this.synthesizer.synthesize(method, targetClassToUse);
}
}

View File

@ -16,7 +16,6 @@
package org.springframework.security.authorization.method;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.function.Function;
@ -27,6 +26,8 @@ import org.springframework.aop.support.AopUtils;
import org.springframework.context.ApplicationContext;
import org.springframework.expression.Expression;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.annotation.AnnotationSynthesizer;
import org.springframework.security.core.annotation.AnnotationSynthesizers;
import org.springframework.util.Assert;
/**
@ -40,8 +41,14 @@ final class PreAuthorizeExpressionAttributeRegistry extends AbstractExpressionAt
private final MethodAuthorizationDeniedHandler defaultHandler = new ThrowingMethodAuthorizationDeniedHandler();
private final AnnotationSynthesizer<HandleAuthorizationDenied> handleAuthorizationDeniedSynthesizer = AnnotationSynthesizers
.requireUnique(HandleAuthorizationDenied.class);
private Function<Class<? extends MethodAuthorizationDeniedHandler>, MethodAuthorizationDeniedHandler> handlerResolver;
private AnnotationSynthesizer<PreAuthorize> preAuthorizeSynthesizer = AnnotationSynthesizers
.requireUnique(PreAuthorize.class);
PreAuthorizeExpressionAttributeRegistry() {
this.handlerResolver = (clazz) -> this.defaultHandler;
}
@ -60,13 +67,9 @@ final class PreAuthorizeExpressionAttributeRegistry extends AbstractExpressionAt
}
private MethodAuthorizationDeniedHandler resolveHandler(Method method, Class<?> targetClass) {
Function<AnnotatedElement, HandleAuthorizationDenied> lookup = AuthorizationAnnotationUtils
.withDefaults(HandleAuthorizationDenied.class);
HandleAuthorizationDenied deniedHandler = lookup.apply(method);
if (deniedHandler != null) {
return this.handlerResolver.apply(deniedHandler.handlerClass());
}
deniedHandler = lookup.apply(targetClass(method, targetClass));
Class<?> targetClassToUse = targetClass(method, targetClass);
HandleAuthorizationDenied deniedHandler = this.handleAuthorizationDeniedSynthesizer.synthesize(method,
targetClassToUse);
if (deniedHandler != null) {
return this.handlerResolver.apply(deniedHandler.handlerClass());
}
@ -74,9 +77,8 @@ final class PreAuthorizeExpressionAttributeRegistry extends AbstractExpressionAt
}
private PreAuthorize findPreAuthorizeAnnotation(Method method, Class<?> targetClass) {
Function<AnnotatedElement, PreAuthorize> lookup = findUniqueAnnotation(PreAuthorize.class);
PreAuthorize preAuthorize = lookup.apply(method);
return (preAuthorize != null) ? preAuthorize : lookup.apply(targetClass(method, targetClass));
Class<?> targetClassToUse = targetClass(method, targetClass);
return this.preAuthorizeSynthesizer.synthesize(method, targetClassToUse);
}
/**
@ -89,6 +91,10 @@ final class PreAuthorizeExpressionAttributeRegistry extends AbstractExpressionAt
this.handlerResolver = (clazz) -> resolveHandler(context, clazz);
}
void setTemplateDefaults(PrePostTemplateDefaults defaults) {
this.preAuthorizeSynthesizer = AnnotationSynthesizers.requireUnique(PreAuthorize.class, defaults);
}
private MethodAuthorizationDeniedHandler resolveHandler(ApplicationContext context,
Class<? extends MethodAuthorizationDeniedHandler> handlerClass) {
if (handlerClass == this.defaultHandler.getClass()) {

View File

@ -16,14 +16,14 @@
package org.springframework.security.authorization.method;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Method;
import java.util.function.Function;
import org.springframework.aop.support.AopUtils;
import org.springframework.expression.Expression;
import org.springframework.lang.NonNull;
import org.springframework.security.access.prepost.PreFilter;
import org.springframework.security.core.annotation.AnnotationSynthesizer;
import org.springframework.security.core.annotation.AnnotationSynthesizers;
/**
* For internal use only, as this contract is likely to change.
@ -35,6 +35,8 @@ import org.springframework.security.access.prepost.PreFilter;
final class PreFilterExpressionAttributeRegistry
extends AbstractExpressionAttributeRegistry<PreFilterExpressionAttributeRegistry.PreFilterExpressionAttribute> {
private AnnotationSynthesizer<PreFilter> synthesizer = AnnotationSynthesizers.requireUnique(PreFilter.class);
@NonNull
@Override
PreFilterExpressionAttribute resolveAttribute(Method method, Class<?> targetClass) {
@ -48,10 +50,13 @@ final class PreFilterExpressionAttributeRegistry
return new PreFilterExpressionAttribute(preFilterExpression, preFilter.filterTarget());
}
void setTemplateDefaults(PrePostTemplateDefaults defaults) {
this.synthesizer = AnnotationSynthesizers.requireUnique(PreFilter.class, defaults);
}
private PreFilter findPreFilterAnnotation(Method method, Class<?> targetClass) {
Function<AnnotatedElement, PreFilter> lookup = findUniqueAnnotation(PreFilter.class);
PreFilter preFilter = lookup.apply(method);
return (preFilter != null) ? preFilter : lookup.apply(targetClass(method, targetClass));
Class<?> targetClassToUse = targetClass(method, targetClass);
return this.synthesizer.synthesize(method, targetClassToUse);
}
static final class PreFilterExpressionAttribute extends ExpressionAttribute {

View File

@ -16,6 +16,8 @@
package org.springframework.security.authorization.method;
import org.springframework.security.core.annotation.AnnotationTemplateExpressionDefaults;
/**
* A component for configuring various cross-cutting aspects of pre/post method security
*
@ -26,31 +28,6 @@ package org.springframework.security.authorization.method;
* @see org.springframework.security.access.prepost.PreFilter
* @see org.springframework.security.access.prepost.PostFilter
*/
public final class PrePostTemplateDefaults {
private boolean ignoreUnknown = true;
/**
* Whether template resolution should ignore placeholders it doesn't recognize.
* <p>
* By default, this value is <code>true</code>.
* @since 6.3
*/
public boolean isIgnoreUnknown() {
return this.ignoreUnknown;
}
/**
* Configure template resolution to ignore unknown placeholders. When set to
* <code>false</code>, template resolution will throw an exception for unknown
* placeholders.
* <p>
* By default, this value is <code>true</code>.
* @param ignoreUnknown - whether to ignore unknown placeholders parameters
* @since 6.3
*/
public void setIgnoreUnknown(boolean ignoreUnknown) {
this.ignoreUnknown = ignoreUnknown;
}
public final class PrePostTemplateDefaults extends AnnotationTemplateExpressionDefaults {
}

View File

@ -33,6 +33,8 @@ import org.springframework.security.authorization.AuthoritiesAuthorizationManage
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.annotation.AnnotationSynthesizer;
import org.springframework.security.core.annotation.AnnotationSynthesizers;
import org.springframework.util.Assert;
/**
@ -50,6 +52,8 @@ public final class SecuredAuthorizationManager implements AuthorizationManager<M
private final Map<MethodClassKey, Set<String>> cachedAuthorities = new ConcurrentHashMap<>();
private final AnnotationSynthesizer<Secured> synthesizer = AnnotationSynthesizers.requireUnique(Secured.class);
/**
* Sets an {@link AuthorizationManager} that accepts a collection of authority
* strings.
@ -92,9 +96,8 @@ public final class SecuredAuthorizationManager implements AuthorizationManager<M
}
private Secured findSecuredAnnotation(Method method, Class<?> targetClass) {
Secured secured = AuthorizationAnnotationUtils.findUniqueAnnotation(method, Secured.class);
return (secured != null) ? secured : AuthorizationAnnotationUtils
.findUniqueAnnotation((targetClass != null) ? targetClass : method.getDeclaringClass(), Secured.class);
Class<?> targetClassToUse = (targetClass != null) ? targetClass : method.getDeclaringClass();
return this.synthesizer.synthesize(method, targetClassToUse);
}
}

View File

@ -0,0 +1,88 @@
/*
* 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
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}.
*
* <p>
* 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.
* </p>
*
* <p>
* Since the process of synthesizing an annotation can be expensive, it is recommended to
* cache the synthesized annotation to prevent multiple computations.
* </p>
*
* @param <A> the annotation type
* @author Josh Cummings
* @since 6.4
* @see UniqueMergedAnnotationSynthesizer
* @see ExpressionTemplateAnnotationSynthesizer
*/
public interface AnnotationSynthesizer<A extends Annotation> {
/**
* Synthesize an annotation of type {@code A} from the given {@link AnnotatedElement}.
*
* <p>
* Implementations should fail if they encounter more than one annotation of that type
* on the element.
* </p>
*
* <p>
* Implementations should describe their strategy for searching the element and any
* surrounding class, interfaces, or super-class.
* </p>
* @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<A> 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<A> merge(AnnotatedElement element, Class<?> targetClass);
}

View File

@ -0,0 +1,81 @@
/*
* 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.core.annotation;
import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.util.ArrayList;
import java.util.List;
/**
* Factory for creating {@link AnnotationSynthesizer} instances.
*
* @author Josh Cummings
* @since 6.4
*/
public final class AnnotationSynthesizers {
private AnnotationSynthesizers() {
}
/**
* Create a {@link AnnotationSynthesizer} that requires synthesized annotations to be
* unique on the given {@link AnnotatedElement}.
* @param type the annotation type
* @param <A> the annotation type
* @return the default {@link AnnotationSynthesizer}
*/
public static <A extends Annotation> AnnotationSynthesizer<A> requireUnique(Class<A> type) {
return new UniqueMergedAnnotationSynthesizer<>(type);
}
/**
* Create a {@link AnnotationSynthesizer} that requires synthesized annotations to be
* unique on the given {@link AnnotatedElement}.
*
* <p>
* When a {@link AnnotationTemplateExpressionDefaults} is provided, it will return a
* synthesizer that supports placeholders in the annotation's attributes in addition
* to the meta-annotation synthesizing provided by {@link #requireUnique(Class)}.
* @param type the annotation type
* @param templateDefaults the defaults for resolving placeholders in the annotation's
* attributes
* @param <A> the annotation type
* @return the default {@link AnnotationSynthesizer}
*/
public static <A extends Annotation> AnnotationSynthesizer<A> requireUnique(Class<A> type,
AnnotationTemplateExpressionDefaults templateDefaults) {
if (templateDefaults == null) {
return new UniqueMergedAnnotationSynthesizer<>(type);
}
return new ExpressionTemplateAnnotationSynthesizer<>(type, templateDefaults);
}
/**
* Create a {@link AnnotationSynthesizer} that requires synthesized annotations to be
* unique on the given {@link AnnotatedElement}. Supplying multiple types implies that
* the synthesized annotation must be unique across all specified types.
* @param types the annotation types
* @return the default {@link AnnotationSynthesizer}
*/
public static AnnotationSynthesizer<Annotation> requireUnique(List<Class<? extends Annotation>> types) {
List<Class<Annotation>> casted = new ArrayList<>();
types.forEach((type) -> casted.add((Class<Annotation>) type));
return new UniqueMergedAnnotationSynthesizer<>(casted);
}
}

View File

@ -0,0 +1,53 @@
/*
* 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.core.annotation;
/**
* A component for configuring the expression attribute template of the parsed Spring
* Security annotation
*
* @author DingHao
* @since 6.4
* @see AuthenticationPrincipal
* @see CurrentSecurityContext
*/
public class AnnotationTemplateExpressionDefaults {
private boolean ignoreUnknown = true;
/**
* Whether template resolution should ignore placeholders it doesn't recognize.
* <p>
* By default, this value is <code>true</code>.
*/
public boolean isIgnoreUnknown() {
return this.ignoreUnknown;
}
/**
* Configure template resolution to ignore unknown placeholders. When set to
* <code>false</code>, template resolution will throw an exception for unknown
* placeholders.
* <p>
* By default, this value is <code>true</code>.
* @param ignoreUnknown - whether to ignore unknown placeholders parameters
*/
public void setIgnoreUnknown(boolean ignoreUnknown) {
this.ignoreUnknown = ignoreUnknown;
}
}

View File

@ -0,0 +1,135 @@
/*
* 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
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 java.util.HashMap;
import java.util.Map;
import org.springframework.core.MethodClassKey;
import org.springframework.core.annotation.MergedAnnotation;
import org.springframework.core.convert.support.DefaultConversionService;
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:
*
* <pre>
* &#64;PreAuthorize("hasRole({role})")
* public @annotation HasRole {
* String role();
* }
* </pre>
*
* <p>
* In that case, you could use an {@link ExpressionTemplateAnnotationSynthesizer} of type
* {@link org.springframework.security.access.prepost.PreAuthorize} to synthesize any
* {@code @HasRole} annotation found on a given {@link AnnotatedElement}.
*
* <p>
* 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.
*
* <p>
* Since the process of synthesis is expensive, it is recommended to cache the synthesized
* result to prevent multiple computations.
*
* @param <A> the annotation type
* @author Josh Cummings
* @since 6.4
*/
final class ExpressionTemplateAnnotationSynthesizer<A extends Annotation> implements AnnotationSynthesizer<A> {
private final Class<A> type;
private final UniqueMergedAnnotationSynthesizer<A> unique;
private final AnnotationTemplateExpressionDefaults templateDefaults;
private final Map<Parameter, MergedAnnotation<A>> uniqueParameterAnnotationCache = new HashMap<>();
private final Map<MethodClassKey, MergedAnnotation<A>> uniqueMethodAnnotationCache = new HashMap<>();
ExpressionTemplateAnnotationSynthesizer(Class<A> type, AnnotationTemplateExpressionDefaults templateDefaults) {
Assert.notNull(type, "type cannot be null");
Assert.notNull(templateDefaults, "templateDefaults cannot be null");
this.type = type;
this.unique = new UniqueMergedAnnotationSynthesizer<>(type);
this.templateDefaults = templateDefaults;
}
@Override
public MergedAnnotation<A> merge(AnnotatedElement element, Class<?> targetClass) {
if (element instanceof Parameter parameter) {
MergedAnnotation<A> annotation = this.uniqueParameterAnnotationCache.computeIfAbsent(parameter,
(p) -> this.unique.merge(p, targetClass));
if (annotation == null) {
return null;
}
return resolvePlaceholders(annotation);
}
if (element instanceof Method method) {
MethodClassKey key = new MethodClassKey(method, targetClass);
MergedAnnotation<A> annotation = this.uniqueMethodAnnotationCache.computeIfAbsent(key,
(k) -> this.unique.merge(method, targetClass));
if (annotation == null) {
return null;
}
return resolvePlaceholders(annotation);
}
throw new IllegalArgumentException("Unsupported element of type " + element.getClass());
}
private MergedAnnotation<A> resolvePlaceholders(MergedAnnotation<A> mergedAnnotation) {
if (this.templateDefaults == null) {
return mergedAnnotation;
}
if (mergedAnnotation.getMetaSource() == null) {
return mergedAnnotation;
}
PropertyPlaceholderHelper helper = new PropertyPlaceholderHelper("{", "}", null, null,
this.templateDefaults.isIgnoreUnknown());
Map<String, Object> properties = new HashMap<>(mergedAnnotation.asMap());
Map<String, Object> metaAnnotationProperties = mergedAnnotation.getMetaSource().asMap();
Map<String, String> stringProperties = new HashMap<>();
for (Map.Entry<String, Object> property : metaAnnotationProperties.entrySet()) {
String key = property.getKey();
Object value = property.getValue();
String asString = (value instanceof String) ? (String) value
: DefaultConversionService.getSharedInstance().convert(value, String.class);
stringProperties.put(key, asString);
}
Map<String, Object> annotationProperties = mergedAnnotation.asMap();
for (Map.Entry<String, Object> annotationProperty : annotationProperties.entrySet()) {
if (!(annotationProperty.getValue() instanceof String)) {
continue;
}
String expression = (String) annotationProperty.getValue();
String value = helper.replacePlaceholders(expression, stringProperties::get);
properties.put(annotationProperty.getKey(), value);
}
AnnotatedElement annotatedElement = (AnnotatedElement) mergedAnnotation.getSource();
return MergedAnnotation.of(annotatedElement, this.type, properties);
}
}

View File

@ -0,0 +1,183 @@
/*
* 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
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 java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.springframework.core.annotation.AnnotationConfigurationException;
import org.springframework.core.annotation.MergedAnnotation;
import org.springframework.core.annotation.MergedAnnotations;
import org.springframework.core.annotation.RepeatableContainers;
import org.springframework.util.Assert;
/**
* A strategy for synthesizing an annotation from an {@link AnnotatedElement} that
* supports meta-annotations, like the following:
*
* <pre>
* &#64;PreAuthorize("hasRole('ROLE_ADMIN')")
* public @annotation HasRole {
* }
* </pre>
*
* <p>
* 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}.
*
* <p>
* Note that in all cases, Spring Security does not allow for repeatable annotations. As
* such, this class errors if a repeat is discovered.
*
* <p>
* 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.
*
* <p>
* 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.
*
* <p>
* Since the process of synthesis is expensive, it is recommended to cache the synthesized
* result to prevent multiple computations.
*
* @param <A> the annotation type
* @author Josh Cummings
* @since 6.4
*/
final class UniqueMergedAnnotationSynthesizer<A extends Annotation> implements AnnotationSynthesizer<A> {
private final List<Class<A>> types;
UniqueMergedAnnotationSynthesizer(Class<A> type) {
Assert.notNull(type, "type cannot be null");
this.types = List.of(type);
}
UniqueMergedAnnotationSynthesizer(List<Class<A>> types) {
Assert.notNull(types, "types cannot be null");
this.types = types;
}
@Override
public MergedAnnotation<A> merge(AnnotatedElement element, Class<?> targetClass) {
if (element instanceof Parameter parameter) {
return handleParameterElement(parameter);
}
if (element instanceof Method method) {
return handleMethodElement(method, targetClass);
}
throw new AnnotationConfigurationException("Unsupported element of type " + element.getClass());
}
private MergedAnnotation<A> handleParameterElement(Parameter parameter) {
List<MergedAnnotation<A>> annotations = findDirectAnnotations(parameter);
return requireUnique(parameter, annotations);
}
private MergedAnnotation<A> handleMethodElement(Method method, Class<?> targetClass) {
List<MergedAnnotation<A>> annotations = findMethodAnnotations(method, targetClass);
return requireUnique(method, annotations);
}
private MergedAnnotation<A> requireUnique(AnnotatedElement element, List<MergedAnnotation<A>> annotations) {
return switch (annotations.size()) {
case 0 -> null;
case 1 -> annotations.get(0);
default -> {
List<Annotation> synthesized = new ArrayList<>();
for (MergedAnnotation<A> annotation : annotations) {
synthesized.add(annotation.synthesize());
}
throw new AnnotationConfigurationException("""
Please ensure there is one unique annotation of type %s attributed to %s. \
Found %d competing annotations: %s""".formatted(this.types, element, annotations.size(),
synthesized));
}
};
}
private List<MergedAnnotation<A>> findMethodAnnotations(Method method, Class<?> targetClass) {
List<MergedAnnotation<A>> annotations = findClosestMethodAnnotations(method, targetClass, new HashSet<>());
if (!annotations.isEmpty()) {
return annotations;
}
return findClosestClassAnnotations(targetClass, new HashSet<>());
}
private List<MergedAnnotation<A>> findClosestMethodAnnotations(Method method, Class<?> targetClass,
Set<Class<?>> classesToSkip) {
if (targetClass == null || classesToSkip.contains(targetClass) || targetClass == Object.class) {
return Collections.emptyList();
}
classesToSkip.add(targetClass);
try {
Method methodToUse = targetClass.getDeclaredMethod(method.getName(), method.getParameterTypes());
List<MergedAnnotation<A>> annotations = findDirectAnnotations(methodToUse);
if (!annotations.isEmpty()) {
return annotations;
}
}
catch (NoSuchMethodException ex) {
// move on
}
List<MergedAnnotation<A>> annotations = new ArrayList<>();
annotations.addAll(findClosestMethodAnnotations(method, targetClass.getSuperclass(), classesToSkip));
for (Class<?> inter : targetClass.getInterfaces()) {
annotations.addAll(findClosestMethodAnnotations(method, inter, classesToSkip));
}
return annotations;
}
private List<MergedAnnotation<A>> findClosestClassAnnotations(Class<?> targetClass, Set<Class<?>> classesToSkip) {
if (targetClass == null || classesToSkip.contains(targetClass) || targetClass == Object.class) {
return Collections.emptyList();
}
classesToSkip.add(targetClass);
List<MergedAnnotation<A>> annotations = new ArrayList<>(findDirectAnnotations(targetClass));
if (!annotations.isEmpty()) {
return annotations;
}
annotations.addAll(findClosestClassAnnotations(targetClass.getSuperclass(), classesToSkip));
for (Class<?> inter : targetClass.getInterfaces()) {
annotations.addAll(findClosestClassAnnotations(inter, classesToSkip));
}
return annotations;
}
private List<MergedAnnotation<A>> findDirectAnnotations(AnnotatedElement element) {
MergedAnnotations mergedAnnotations = MergedAnnotations.from(element, MergedAnnotations.SearchStrategy.DIRECT,
RepeatableContainers.none());
return mergedAnnotations.stream()
.filter((annotation) -> this.types.contains(annotation.getType()))
.map((annotation) -> (MergedAnnotation<A>) annotation)
.toList();
}
}

View File

@ -1,186 +0,0 @@
/*
* 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.authorization.method;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.springframework.core.annotation.AliasFor;
import org.springframework.core.annotation.AnnotationConfigurationException;
import org.springframework.security.access.prepost.PreAuthorize;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
/**
* Tests for {@link AuthorizationAnnotationUtils}.
*
* @author Josh Cummings
* @author Sam Brannen
*/
class AuthorizationAnnotationUtilsTests {
@Test // gh-13132
void annotationsOnSyntheticMethodsShouldNotTriggerAnnotationConfigurationException() throws NoSuchMethodException {
StringRepository proxy = (StringRepository) Proxy.newProxyInstance(
Thread.currentThread().getContextClassLoader(), new Class[] { StringRepository.class },
(p, m, args) -> null);
Method method = proxy.getClass().getDeclaredMethod("findAll");
PreAuthorize preAuthorize = AuthorizationAnnotationUtils.findUniqueAnnotation(method, PreAuthorize.class);
assertThat(preAuthorize.value()).isEqualTo("hasRole('someRole')");
}
@Test // gh-13625
void annotationsFromSuperSuperInterfaceShouldNotTriggerAnnotationConfigurationException() throws Exception {
Method method = HelloImpl.class.getDeclaredMethod("sayHello");
PreAuthorize preAuthorize = AuthorizationAnnotationUtils.findUniqueAnnotation(method, PreAuthorize.class);
assertThat(preAuthorize.value()).isEqualTo("hasRole('someRole')");
}
@Test
void multipleIdenticalAnnotationsOnClassShouldNotTriggerAnnotationConfigurationException() {
Class<?> clazz = MultipleIdenticalPreAuthorizeAnnotationsOnClass.class;
PreAuthorize preAuthorize = AuthorizationAnnotationUtils.findUniqueAnnotation(clazz, PreAuthorize.class);
assertThat(preAuthorize.value()).isEqualTo("hasRole('someRole')");
}
@Test
void multipleIdenticalAnnotationsOnMethodShouldNotTriggerAnnotationConfigurationException() throws Exception {
Method method = MultipleIdenticalPreAuthorizeAnnotationsOnMethod.class.getDeclaredMethod("method");
PreAuthorize preAuthorize = AuthorizationAnnotationUtils.findUniqueAnnotation(method, PreAuthorize.class);
assertThat(preAuthorize.value()).isEqualTo("hasRole('someRole')");
}
@Test
void competingAnnotationsOnClassShouldTriggerAnnotationConfigurationException() {
Class<?> clazz = CompetingPreAuthorizeAnnotationsOnClass.class;
assertThatExceptionOfType(AnnotationConfigurationException.class)
.isThrownBy(() -> AuthorizationAnnotationUtils.findUniqueAnnotation(clazz, PreAuthorize.class))
.withMessageContainingAll("Found 2 competing annotations:", "someRole", "otherRole");
}
@Test
void competingAnnotationsOnMethodShouldTriggerAnnotationConfigurationException() throws Exception {
Method method = CompetingPreAuthorizeAnnotationsOnMethod.class.getDeclaredMethod("method");
assertThatExceptionOfType(AnnotationConfigurationException.class)
.isThrownBy(() -> AuthorizationAnnotationUtils.findUniqueAnnotation(method, PreAuthorize.class))
.withMessageContainingAll("Found 2 competing annotations:", "someRole", "otherRole");
}
@Test
void composedMergedAnnotationsAreNotSupported() {
Class<?> clazz = ComposedPreAuthAnnotationOnClass.class;
PreAuthorize preAuthorize = AuthorizationAnnotationUtils.findUniqueAnnotation(clazz, PreAuthorize.class);
assertThat(preAuthorize.value()).isEqualTo("hasRole('composedRole')");
}
private interface BaseRepository<T> {
Iterable<T> findAll();
}
private interface StringRepository extends BaseRepository<String> {
@Override
@PreAuthorize("hasRole('someRole')")
List<String> findAll();
}
private interface Hello {
@PreAuthorize("hasRole('someRole')")
String sayHello();
}
private interface SayHello extends Hello {
}
private static class HelloImpl implements SayHello {
@Override
public String sayHello() {
return "hello";
}
}
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasRole('someRole')")
private @interface RequireSomeRole {
}
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasRole('otherRole')")
private @interface RequireOtherRole {
}
@RequireSomeRole
@PreAuthorize("hasRole('someRole')")
private static class MultipleIdenticalPreAuthorizeAnnotationsOnClass {
}
private static class MultipleIdenticalPreAuthorizeAnnotationsOnMethod {
@RequireSomeRole
@PreAuthorize("hasRole('someRole')")
void method() {
}
}
@RequireOtherRole
@PreAuthorize("hasRole('someRole')")
private static class CompetingPreAuthorizeAnnotationsOnClass {
}
private static class CompetingPreAuthorizeAnnotationsOnMethod {
@RequireOtherRole
@PreAuthorize("hasRole('someRole')")
void method() {
}
}
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasRole('metaRole')")
private @interface ComposedPreAuth {
@AliasFor(annotation = PreAuthorize.class)
String value();
}
@ComposedPreAuth("hasRole('composedRole')")
private static class ComposedPreAuthAnnotationOnClass {
}
}

View File

@ -0,0 +1,549 @@
/*
* 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.core.annotation;
import java.lang.reflect.Method;
import org.junit.jupiter.api.Test;
import org.springframework.core.annotation.AnnotationConfigurationException;
import org.springframework.security.access.prepost.PreAuthorize;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
/**
* Tests for {@link UniqueMergedAnnotationSynthesizer}
*/
public class UniqueMergedAnnotationSynthesizerTests {
private UniqueMergedAnnotationSynthesizer<PreAuthorize> synthesizer = new UniqueMergedAnnotationSynthesizer<>(
PreAuthorize.class);
@Test
void synthesizeWhenAnnotationOnInterfaceThenResolves() throws Exception {
Method method = AnnotationOnInterface.class.getDeclaredMethod("method");
PreAuthorize preAuthorize = this.synthesizer.synthesize(method);
assertThat(preAuthorize.value()).isEqualTo("one");
}
@Test
void synthesizeWhenAnnotationOnMethodThenResolves() throws Exception {
Method method = AnnotationOnInterfaceMethod.class.getDeclaredMethod("method");
PreAuthorize preAuthorize = this.synthesizer.synthesize(method);
assertThat(preAuthorize.value()).isEqualTo("three");
}
@Test
void synthesizeWhenAnnotationOnClassThenResolves() throws Exception {
Method method = AnnotationOnClass.class.getDeclaredMethod("method");
PreAuthorize preAuthorize = this.synthesizer.synthesize(method);
assertThat(preAuthorize.value()).isEqualTo("five");
}
@Test
void synthesizeWhenAnnotationOnClassMethodThenResolves() throws Exception {
Method method = AnnotationOnClassMethod.class.getDeclaredMethod("method");
PreAuthorize preAuthorize = this.synthesizer.synthesize(method);
assertThat(preAuthorize.value()).isEqualTo("six");
}
@Test
void synthesizeWhenInterfaceOverridingAnnotationOnInterfaceThenResolves() throws Exception {
Method method = InterfaceMethodOverridingAnnotationOnInterface.class.getDeclaredMethod("method");
PreAuthorize preAuthorize = this.synthesizer.synthesize(method);
assertThat(preAuthorize.value()).isEqualTo("eight");
}
@Test
void synthesizeWhenInterfaceOverridingMultipleInterfaceInheritanceThenResolves() throws Exception {
Method method = InterfaceOverridingMultipleInterfaceInheritance.class.getMethod("method");
PreAuthorize preAuthorize = this.synthesizer.synthesize(method,
InterfaceOverridingMultipleInterfaceInheritance.class);
assertThat(preAuthorize.value()).isEqualTo("ten");
}
@Test
void synthesizeWhenInterfaceMethodOverridingAnnotationOnInterfaceThenResolves() throws Exception {
Method method = InterfaceMethodOverridingMultipleInterfaceInheritance.class.getDeclaredMethod("method");
PreAuthorize preAuthorize = this.synthesizer.synthesize(method);
assertThat(preAuthorize.value()).isEqualTo("eleven");
}
@Test
void synthesizeWhenClassMultipleInheritanceThenException() throws Exception {
Method method = ClassAttemptingMultipleInterfaceInheritance.class.getDeclaredMethod("method");
assertThatExceptionOfType(AnnotationConfigurationException.class)
.isThrownBy(() -> this.synthesizer.synthesize(method));
}
// gh-15097
@Test
void synthesizeWhenClassOverridingMultipleInterfaceInheritanceThenResolves() throws Exception {
Method method = ClassOverridingMultipleInterfaceInheritance.class.getDeclaredMethod("method");
PreAuthorize preAuthorize = this.synthesizer.synthesize(method);
assertThat(preAuthorize.value()).isEqualTo("thirteen");
}
@Test
void synthesizeWhenClassMethodOverridingMultipleInterfaceInheritanceThenResolves() throws Exception {
Method method = ClassMethodOverridingMultipleInterfaceInheritance.class.getDeclaredMethod("method");
PreAuthorize preAuthorize = this.synthesizer.synthesize(method);
assertThat(preAuthorize.value()).isEqualTo("fourteen");
}
@Test
void synthesizeWhenClassInheritingInterfaceOverridingInterfaceAnnotationThenResolves() throws Exception {
Method method = ClassInheritingInterfaceOverridingInterfaceAnnotation.class.getDeclaredMethod("method");
PreAuthorize preAuthorize = this.synthesizer.synthesize(method);
assertThat(preAuthorize.value()).isEqualTo("seven");
}
@Test
void synthesizeWhenClassOverridingGrandparentInterfaceAnnotationThenResolves() throws Exception {
Method method = ClassOverridingGrandparentInterfaceAnnotation.class.getDeclaredMethod("method");
PreAuthorize preAuthorize = this.synthesizer.synthesize(method);
assertThat(preAuthorize.value()).isEqualTo("sixteen");
}
@Test
void synthesizeWhenMethodOverridingGrandparentInterfaceAnnotationThenResolves() throws Exception {
Method method = MethodOverridingGrandparentInterfaceAnnotation.class.getDeclaredMethod("method");
PreAuthorize preAuthorize = this.synthesizer.synthesize(method);
assertThat(preAuthorize.value()).isEqualTo("seventeen");
}
@Test
void synthesizeWhenClassInheritingMethodOverriddenAnnotationThenResolves() throws Exception {
Method method = ClassInheritingMethodOverriddenAnnotation.class.getDeclaredMethod("method");
PreAuthorize preAuthorize = this.synthesizer.synthesize(method);
assertThat(preAuthorize.value()).isEqualTo("eight");
}
@Test
void synthesizeWhenClassOverridingMethodOverriddenAnnotationThenResolves() throws Exception {
Method method = ClassOverridingMethodOverriddenAnnotation.class.getDeclaredMethod("method");
PreAuthorize preAuthorize = this.synthesizer.synthesize(method);
assertThat(preAuthorize.value()).isEqualTo("eight");
}
@Test
void synthesizeWhenMethodOverridingMethodOverriddenAnnotationThenResolves() throws Exception {
Method method = MethodOverridingMethodOverriddenAnnotation.class.getDeclaredMethod("method");
PreAuthorize preAuthorize = this.synthesizer.synthesize(method);
assertThat(preAuthorize.value()).isEqualTo("twenty");
}
@Test
void synthesizeWhenClassInheritingMultipleInheritanceThenException() throws Exception {
Method method = ClassInheritingMultipleInheritance.class.getDeclaredMethod("method");
assertThatExceptionOfType(AnnotationConfigurationException.class)
.isThrownBy(() -> this.synthesizer.synthesize(method));
}
@Test
void synthesizeWhenClassOverridingMultipleInheritanceThenResolves() throws Exception {
Method method = ClassOverridingMultipleInheritance.class.getDeclaredMethod("method");
PreAuthorize preAuthorize = this.synthesizer.synthesize(method);
assertThat(preAuthorize.value()).isEqualTo("twentytwo");
}
@Test
void synthesizeWhenMethodOverridingMultipleInheritanceThenResolves() throws Exception {
Method method = MethodOverridingMultipleInheritance.class.getDeclaredMethod("method");
PreAuthorize preAuthorize = this.synthesizer.synthesize(method);
assertThat(preAuthorize.value()).isEqualTo("twentythree");
}
@Test
void synthesizeWhenInheritingInterfaceAndMethodAnnotationsThenResolves() throws Exception {
Method method = InheritingInterfaceAndMethodAnnotations.class.getDeclaredMethod("method");
PreAuthorize preAuthorize = this.synthesizer.synthesize(method);
assertThat(preAuthorize.value()).isEqualTo("three");
}
@Test
void synthesizeWhenClassOverridingInterfaceAndMethodInheritanceThenResolves() throws Exception {
Method method = ClassOverridingInterfaceAndMethodInheritance.class.getDeclaredMethod("method");
PreAuthorize preAuthorize = this.synthesizer.synthesize(method);
assertThat(preAuthorize.value()).isEqualTo("three");
}
@Test
void synthesizeWhenMethodOverridingInterfaceAndMethodInheritanceThenResolves() throws Exception {
Method method = MethodOverridingInterfaceAndMethodInheritance.class.getDeclaredMethod("method");
PreAuthorize preAuthorize = this.synthesizer.synthesize(method);
assertThat(preAuthorize.value()).isEqualTo("twentysix");
}
@Test
void synthesizeWhenMultipleMethodInheritanceThenException() throws Exception {
Method method = MultipleMethodInheritance.class.getDeclaredMethod("method");
assertThatExceptionOfType(AnnotationConfigurationException.class)
.isThrownBy(() -> this.synthesizer.synthesize(method));
}
// gh-13234
@Test
void synthesizeWhenClassInheritingGrandparentInterfaceAnnotationThenResolves() throws Exception {
Method method = ClassInheritingGrandparentInterfaceAnnotation.class.getDeclaredMethod("method");
PreAuthorize preAuthorize = this.synthesizer.synthesize(method);
assertThat(preAuthorize.value()).isEqualTo("one");
}
@Test
void synthesizeWhenMethodInheritingMethodOverridingInterfaceAndMethodInheritanceThenResolves() throws Exception {
Method method = MethodInheritingMethodOverridingInterfaceAndMethodInheritance.class.getMethod("method");
PreAuthorize preAuthorize = this.synthesizer.synthesize(method);
assertThat(preAuthorize.value()).isEqualTo("twentysix");
}
@Test
void synthesizeWhenClassOverridingMethodOverridingInterfaceAndMethodInheritanceThenResolves() throws Exception {
Method method = ClassOverridingMethodOverridingInterfaceAndMethodInheritance.class.getMethod("method");
PreAuthorize preAuthorize = this.synthesizer.synthesize(method,
ClassOverridingMethodOverridingInterfaceAndMethodInheritance.class);
assertThat(preAuthorize.value()).isEqualTo("twentysix");
}
@Test
void synthesizeWhenInterfaceInheritingAnnotationsAtDifferentLevelsThenException() throws Exception {
Method method = InterfaceInheritingAnnotationsAtDifferentLevels.class.getMethod("method");
assertThatExceptionOfType(AnnotationConfigurationException.class)
.isThrownBy(() -> this.synthesizer.synthesize(method));
}
@Test
void synthesizeWhenClassMethodOverridingAnnotationOnMethodThenResolves() throws Exception {
Method method = ClassMethodOverridingAnnotationOnMethod.class.getDeclaredMethod("method");
PreAuthorize preAuthorize = this.synthesizer.synthesize(method);
assertThat(preAuthorize.value()).isEqualTo("twentyeight");
}
// gh-13490
@Test
void synthesizeWhenClassInheritingInterfaceInheritingInterfaceMethodAnnotationThenResolves() throws Exception {
Method method = ClassInheritingInterfaceInheritingInterfaceMethodAnnotation.class.getDeclaredMethod("method");
PreAuthorize preAuthorize = this.synthesizer.synthesize(method);
assertThat(preAuthorize.value()).isEqualTo("three");
}
@PreAuthorize("one")
private interface AnnotationOnInterface {
String method();
}
@PreAuthorize("two")
private interface AlsoAnnotationOnInterface {
String method();
}
private interface AnnotationOnInterfaceMethod {
@PreAuthorize("three")
String method();
}
private interface AlsoAnnotationOnInterfaceMethod {
@PreAuthorize("four")
String method();
}
@PreAuthorize("five")
private static class AnnotationOnClass {
String method() {
return "ok";
}
}
private static class AnnotationOnClassMethod {
@PreAuthorize("six")
String method() {
return "ok";
}
}
@PreAuthorize("seven")
private interface InterfaceOverridingAnnotationOnInterface extends AnnotationOnInterface {
}
private interface InterfaceMethodOverridingAnnotationOnInterface extends AnnotationOnInterface {
@PreAuthorize("eight")
String method();
}
private interface InterfaceAttemptingMultipleInterfaceInheritance
extends AnnotationOnInterface, AlsoAnnotationOnInterface {
}
@PreAuthorize("ten")
private interface InterfaceOverridingMultipleInterfaceInheritance
extends AnnotationOnInterface, AlsoAnnotationOnInterface {
}
private interface InterfaceMethodOverridingMultipleInterfaceInheritance
extends AnnotationOnInterface, AlsoAnnotationOnInterface {
@PreAuthorize("eleven")
String method();
}
private static class ClassAttemptingMultipleInterfaceInheritance
implements AnnotationOnInterface, AlsoAnnotationOnInterface {
@Override
public String method() {
return "ok";
}
}
@PreAuthorize("thirteen")
private static class ClassOverridingMultipleInterfaceInheritance
implements AnnotationOnInterface, AlsoAnnotationOnInterface {
@Override
public String method() {
return "ok";
}
}
private static class ClassMethodOverridingMultipleInterfaceInheritance
implements AnnotationOnInterface, AlsoAnnotationOnInterface {
@Override
@PreAuthorize("fourteen")
public String method() {
return "ok";
}
}
private static class ClassInheritingInterfaceOverridingInterfaceAnnotation
implements InterfaceOverridingAnnotationOnInterface {
@Override
public String method() {
return "ok";
}
}
@PreAuthorize("sixteen")
private static class ClassOverridingGrandparentInterfaceAnnotation
implements InterfaceOverridingAnnotationOnInterface {
@Override
public String method() {
return "ok";
}
}
private static class MethodOverridingGrandparentInterfaceAnnotation
implements InterfaceOverridingAnnotationOnInterface {
@Override
@PreAuthorize("seventeen")
public String method() {
return "ok";
} // unambiguously seventeen
}
private static class ClassInheritingMethodOverriddenAnnotation
implements InterfaceMethodOverridingAnnotationOnInterface {
@Override
public String method() {
return "ok";
}
}
@PreAuthorize("nineteen")
private static class ClassOverridingMethodOverriddenAnnotation
implements InterfaceMethodOverridingAnnotationOnInterface {
@Override
public String method() {
return "ok";
}
}
private static class MethodOverridingMethodOverriddenAnnotation
implements InterfaceMethodOverridingAnnotationOnInterface {
@Override
@PreAuthorize("twenty")
public String method() {
return "ok";
}
}
private static class ClassInheritingMultipleInheritance implements InterfaceAttemptingMultipleInterfaceInheritance {
@Override
public String method() {
return "ok";
}
}
@PreAuthorize("twentytwo")
private static class ClassOverridingMultipleInheritance implements InterfaceAttemptingMultipleInterfaceInheritance {
@Override
public String method() {
return "ok";
}
}
private static class MethodOverridingMultipleInheritance
implements InterfaceAttemptingMultipleInterfaceInheritance {
@Override
@PreAuthorize("twentythree")
public String method() {
return "ok";
}
}
private static class InheritingInterfaceAndMethodAnnotations
implements AnnotationOnInterface, AnnotationOnInterfaceMethod {
@Override
public String method() {
return "ok";
}
}
@PreAuthorize("twentyfive")
private static class ClassOverridingInterfaceAndMethodInheritance
implements AnnotationOnInterface, AnnotationOnInterfaceMethod {
@Override
public String method() {
return "ok";
}
}
private static class MethodOverridingInterfaceAndMethodInheritance
implements AnnotationOnInterface, AnnotationOnInterfaceMethod {
@Override
@PreAuthorize("twentysix")
public String method() {
return "ok";
}
}
private static class MultipleMethodInheritance
implements AnnotationOnInterfaceMethod, AlsoAnnotationOnInterfaceMethod {
@Override
public String method() {
return "ok";
}
}
private interface InterfaceInheritingInterfaceAnnotation extends AnnotationOnInterface {
}
private static class ClassInheritingGrandparentInterfaceAnnotation
implements InterfaceInheritingInterfaceAnnotation {
@Override
public String method() {
return "ok";
}
}
private static class MethodInheritingMethodOverridingInterfaceAndMethodInheritance
extends MethodOverridingInterfaceAndMethodInheritance {
}
@PreAuthorize("twentyseven")
private static class ClassOverridingMethodOverridingInterfaceAndMethodInheritance
extends MethodOverridingInterfaceAndMethodInheritance {
}
private static class InterfaceInheritingAnnotationsAtDifferentLevels
implements InterfaceInheritingInterfaceAnnotation, AlsoAnnotationOnInterface {
@Override
public String method() {
return "ok";
}
}
private static class ClassMethodOverridingAnnotationOnMethod implements AnnotationOnInterfaceMethod {
@Override
@PreAuthorize("twentyeight")
public String method() {
return "ok";
}
}
private interface InterfaceInheritingInterfaceMethodAnnotation extends AnnotationOnInterfaceMethod {
}
private static class ClassInheritingInterfaceInheritingInterfaceMethodAnnotation
implements InterfaceInheritingInterfaceMethodAnnotation {
@Override
public String method() {
return "ok";
}
}
}