Mimic Annotation Fallback Logic

For backward compatibility, this commit changes the annotation traversal
logic to match what is found in PrePostAnnotationSecurityMetadataSource.

This reverts gh-13783 which is a feature that unfortunately regressess
pre-existing behavior like that found in gh-15352. As such, that
functionality has been removed.

Issue gh-15352
This commit is contained in:
Josh Cummings 2024-07-31 13:23:33 -06:00
parent 77bce14462
commit 37a2812d1a
No known key found for this signature in database
GPG Key ID: A306A51F43B8E5A5
15 changed files with 84 additions and 192 deletions

View File

@ -28,6 +28,7 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.function.Supplier; import java.util.function.Supplier;
import jakarta.annotation.security.DenyAll;
import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation; import org.aopalliance.intercept.MethodInvocation;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@ -50,6 +51,7 @@ import org.springframework.security.access.annotation.BusinessService;
import org.springframework.security.access.annotation.BusinessServiceImpl; import org.springframework.security.access.annotation.BusinessServiceImpl;
import org.springframework.security.access.annotation.ExpressionProtectedBusinessServiceImpl; import org.springframework.security.access.annotation.ExpressionProtectedBusinessServiceImpl;
import org.springframework.security.access.annotation.Jsr250BusinessServiceImpl; import org.springframework.security.access.annotation.Jsr250BusinessServiceImpl;
import org.springframework.security.access.annotation.Secured;
import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler; import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler; import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
import org.springframework.security.access.hierarchicalroles.RoleHierarchy; import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
@ -944,6 +946,13 @@ public class PrePostMethodSecurityConfigurationTests {
verify(handler, never()).handleDeniedInvocation(any(), any(Authz.AuthzResult.class)); verify(handler, never()).handleDeniedInvocation(any(), any(Authz.AuthzResult.class));
} }
// gh-15352
@Test
void annotationsInChildClassesDoNotAffectSuperclasses() {
this.spring.register(AbstractClassConfig.class).autowire();
this.spring.getContext().getBean(ClassInheritingAbstractClassWithNoAnnotations.class).method();
}
private static Consumer<ConfigurableWebApplicationContext> disallowBeanOverriding() { private static Consumer<ConfigurableWebApplicationContext> disallowBeanOverriding() {
return (context) -> ((AnnotationConfigWebApplicationContext) context).setAllowBeanDefinitionOverriding(false); return (context) -> ((AnnotationConfigWebApplicationContext) context).setAllowBeanDefinitionOverriding(false);
} }
@ -1480,4 +1489,29 @@ public class PrePostMethodSecurityConfigurationTests {
} }
abstract static class AbstractClassWithNoAnnotations {
String method() {
return "ok";
}
}
@PreAuthorize("denyAll()")
@Secured("DENIED")
@DenyAll
static class ClassInheritingAbstractClassWithNoAnnotations extends AbstractClassWithNoAnnotations {
}
@EnableMethodSecurity(securedEnabled = true, jsr250Enabled = true)
static class AbstractClassConfig {
@Bean
ClassInheritingAbstractClassWithNoAnnotations inheriting() {
return new ClassInheritingAbstractClassWithNoAnnotations();
}
}
} }

View File

@ -29,7 +29,6 @@ import jakarta.annotation.security.PermitAll;
import jakarta.annotation.security.RolesAllowed; import jakarta.annotation.security.RolesAllowed;
import org.aopalliance.intercept.MethodInvocation; import org.aopalliance.intercept.MethodInvocation;
import org.springframework.aop.support.AopUtils;
import org.springframework.lang.NonNull; import org.springframework.lang.NonNull;
import org.springframework.security.authorization.AuthoritiesAuthorizationManager; import org.springframework.security.authorization.AuthoritiesAuthorizationManager;
import org.springframework.security.authorization.AuthorizationDecision; import org.springframework.security.authorization.AuthorizationDecision;
@ -117,9 +116,8 @@ public final class Jsr250AuthorizationManager implements AuthorizationManager<Me
} }
private Annotation findJsr250Annotation(Method method, Class<?> targetClass) { private Annotation findJsr250Annotation(Method method, Class<?> targetClass) {
Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass); Class<?> targetClassToUse = (targetClass != null) ? targetClass : method.getDeclaringClass();
Class<?> targetClassToUse = (targetClass != null) ? targetClass : specificMethod.getDeclaringClass(); return this.synthesizer.synthesize(method, targetClassToUse);
return this.synthesizer.synthesize(specificMethod, targetClassToUse);
} }
private Set<String> getAllowedRolesWithPrefix(RolesAllowed rolesAllowed) { private Set<String> getAllowedRolesWithPrefix(RolesAllowed rolesAllowed) {

View File

@ -22,7 +22,6 @@ import java.util.function.Function;
import reactor.util.annotation.NonNull; import reactor.util.annotation.NonNull;
import org.springframework.aop.support.AopUtils;
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContext;
import org.springframework.expression.Expression; import org.springframework.expression.Expression;
import org.springframework.security.access.prepost.PostAuthorize; import org.springframework.security.access.prepost.PostAuthorize;
@ -56,8 +55,7 @@ final class PostAuthorizeExpressionAttributeRegistry extends AbstractExpressionA
@NonNull @NonNull
@Override @Override
ExpressionAttribute resolveAttribute(Method method, Class<?> targetClass) { ExpressionAttribute resolveAttribute(Method method, Class<?> targetClass) {
Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass); PostAuthorize postAuthorize = findPostAuthorizeAnnotation(method, targetClass);
PostAuthorize postAuthorize = findPostAuthorizeAnnotation(specificMethod, targetClass);
if (postAuthorize == null) { if (postAuthorize == null) {
return ExpressionAttribute.NULL_ATTRIBUTE; return ExpressionAttribute.NULL_ATTRIBUTE;
} }

View File

@ -18,7 +18,6 @@ package org.springframework.security.authorization.method;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import org.springframework.aop.support.AopUtils;
import org.springframework.expression.Expression; import org.springframework.expression.Expression;
import org.springframework.lang.NonNull; import org.springframework.lang.NonNull;
import org.springframework.security.access.prepost.PostFilter; import org.springframework.security.access.prepost.PostFilter;
@ -39,8 +38,7 @@ final class PostFilterExpressionAttributeRegistry extends AbstractExpressionAttr
@NonNull @NonNull
@Override @Override
ExpressionAttribute resolveAttribute(Method method, Class<?> targetClass) { ExpressionAttribute resolveAttribute(Method method, Class<?> targetClass) {
Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass); PostFilter postFilter = findPostFilterAnnotation(method, targetClass);
PostFilter postFilter = findPostFilterAnnotation(specificMethod, targetClass);
if (postFilter == null) { if (postFilter == null) {
return ExpressionAttribute.NULL_ATTRIBUTE; return ExpressionAttribute.NULL_ATTRIBUTE;
} }

View File

@ -22,7 +22,6 @@ import java.util.function.Function;
import reactor.util.annotation.NonNull; import reactor.util.annotation.NonNull;
import org.springframework.aop.support.AopUtils;
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContext;
import org.springframework.expression.Expression; import org.springframework.expression.Expression;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
@ -56,8 +55,7 @@ final class PreAuthorizeExpressionAttributeRegistry extends AbstractExpressionAt
@NonNull @NonNull
@Override @Override
ExpressionAttribute resolveAttribute(Method method, Class<?> targetClass) { ExpressionAttribute resolveAttribute(Method method, Class<?> targetClass) {
Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass); PreAuthorize preAuthorize = findPreAuthorizeAnnotation(method, targetClass);
PreAuthorize preAuthorize = findPreAuthorizeAnnotation(specificMethod, targetClass);
if (preAuthorize == null) { if (preAuthorize == null) {
return ExpressionAttribute.NULL_ATTRIBUTE; return ExpressionAttribute.NULL_ATTRIBUTE;
} }

View File

@ -18,7 +18,6 @@ package org.springframework.security.authorization.method;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import org.springframework.aop.support.AopUtils;
import org.springframework.expression.Expression; import org.springframework.expression.Expression;
import org.springframework.lang.NonNull; import org.springframework.lang.NonNull;
import org.springframework.security.access.prepost.PreFilter; import org.springframework.security.access.prepost.PreFilter;
@ -40,8 +39,7 @@ final class PreFilterExpressionAttributeRegistry
@NonNull @NonNull
@Override @Override
PreFilterExpressionAttribute resolveAttribute(Method method, Class<?> targetClass) { PreFilterExpressionAttribute resolveAttribute(Method method, Class<?> targetClass) {
Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass); PreFilter preFilter = findPreFilterAnnotation(method, targetClass);
PreFilter preFilter = findPreFilterAnnotation(specificMethod, targetClass);
if (preFilter == null) { if (preFilter == null) {
return PreFilterExpressionAttribute.NULL_ATTRIBUTE; return PreFilterExpressionAttribute.NULL_ATTRIBUTE;
} }

View File

@ -26,7 +26,6 @@ import java.util.function.Supplier;
import org.aopalliance.intercept.MethodInvocation; import org.aopalliance.intercept.MethodInvocation;
import org.springframework.aop.support.AopUtils;
import org.springframework.core.MethodClassKey; import org.springframework.core.MethodClassKey;
import org.springframework.security.access.annotation.Secured; import org.springframework.security.access.annotation.Secured;
import org.springframework.security.authorization.AuthoritiesAuthorizationManager; import org.springframework.security.authorization.AuthoritiesAuthorizationManager;
@ -90,8 +89,7 @@ public final class SecuredAuthorizationManager implements AuthorizationManager<M
} }
private Set<String> resolveAuthorities(Method method, Class<?> targetClass) { private Set<String> resolveAuthorities(Method method, Class<?> targetClass) {
Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass); Secured secured = findSecuredAnnotation(method, targetClass);
Secured secured = findSecuredAnnotation(specificMethod, targetClass);
return (secured != null) ? Set.of(secured.value()) : Collections.emptySet(); return (secured != null) ? Set.of(secured.value()) : Collections.emptySet();
} }

View File

@ -31,6 +31,7 @@ import org.springframework.core.annotation.MergedAnnotation;
import org.springframework.core.annotation.MergedAnnotations; import org.springframework.core.annotation.MergedAnnotations;
import org.springframework.core.annotation.RepeatableContainers; import org.springframework.core.annotation.RepeatableContainers;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
/** /**
* A strategy for synthesizing an annotation from an {@link AnnotatedElement} that * A strategy for synthesizing an annotation from an {@link AnnotatedElement} that
@ -124,11 +125,29 @@ final class UniqueMergedAnnotationSynthesizer<A extends Annotation> implements A
} }
private List<MergedAnnotation<A>> findMethodAnnotations(Method method, Class<?> targetClass) { private List<MergedAnnotation<A>> findMethodAnnotations(Method method, Class<?> targetClass) {
List<MergedAnnotation<A>> annotations = findClosestMethodAnnotations(method, targetClass, new HashSet<>()); // The method may be on an interface, but we need attributes from the target
// class.
// If the target class is null, the method will be unchanged.
Method specificMethod = ClassUtils.getMostSpecificMethod(method, targetClass);
List<MergedAnnotation<A>> annotations = findClosestMethodAnnotations(specificMethod,
specificMethod.getDeclaringClass(), new HashSet<>());
if (!annotations.isEmpty()) { if (!annotations.isEmpty()) {
return annotations; return annotations;
} }
return findClosestClassAnnotations(targetClass, new HashSet<>()); // Check the original (e.g. interface) method
if (specificMethod != method) {
annotations = findClosestMethodAnnotations(method, method.getDeclaringClass(), new HashSet<>());
if (!annotations.isEmpty()) {
return annotations;
}
}
// Check the class-level (note declaringClass, not targetClass, which may not
// actually implement the method)
annotations = findClosestClassAnnotations(specificMethod.getDeclaringClass(), new HashSet<>());
if (!annotations.isEmpty()) {
return annotations;
}
return Collections.emptyList();
} }
private List<MergedAnnotation<A>> findClosestMethodAnnotations(Method method, Class<?> targetClass, private List<MergedAnnotation<A>> findClosestMethodAnnotations(Method method, Class<?> targetClass,

View File

@ -215,56 +215,6 @@ public class Jsr250AuthorizationManagerTests {
.isThrownBy(() -> manager.check(authentication, methodInvocation)); .isThrownBy(() -> manager.check(authentication, methodInvocation));
} }
@Test
public void checkRequiresUserWhenMethodsFromInheritThenApplies() throws Exception {
MockMethodInvocation methodInvocation = new MockMethodInvocation(new RolesAllowedClass(),
RolesAllowedClass.class, "securedUser");
Jsr250AuthorizationManager manager = new Jsr250AuthorizationManager();
AuthorizationDecision decision = manager.check(TestAuthentication::authenticatedUser, methodInvocation);
assertThat(decision.isGranted()).isTrue();
}
@Test
public void checkPermitAllWhenMethodsFromInheritThenApplies() throws Exception {
MockMethodInvocation methodInvocation = new MockMethodInvocation(new PermitAllClass(), PermitAllClass.class,
"securedUser");
Jsr250AuthorizationManager manager = new Jsr250AuthorizationManager();
AuthorizationDecision decision = manager.check(TestAuthentication::authenticatedUser, methodInvocation);
assertThat(decision.isGranted()).isTrue();
}
@Test
public void checkDenyAllWhenMethodsFromInheritThenApplies() throws Exception {
MockMethodInvocation methodInvocation = new MockMethodInvocation(new DenyAllClass(), DenyAllClass.class,
"securedUser");
Jsr250AuthorizationManager manager = new Jsr250AuthorizationManager();
AuthorizationDecision decision = manager.check(TestAuthentication::authenticatedUser, methodInvocation);
assertThat(decision.isGranted()).isFalse();
}
@RolesAllowed("USER")
public static class RolesAllowedClass extends ParentClass {
}
@PermitAll
public static class PermitAllClass extends ParentClass {
}
@DenyAll
public static class DenyAllClass extends ParentClass {
}
public static class ParentClass {
public void securedUser() {
}
}
public static class TestClass implements InterfaceAnnotationsOne, InterfaceAnnotationsTwo { public static class TestClass implements InterfaceAnnotationsOne, InterfaceAnnotationsTwo {
public void doSomething() { public void doSomething() {

View File

@ -156,29 +156,6 @@ public class PostAuthorizeAuthorizationManagerTests {
.isThrownBy(() -> manager.check(authentication, result)); .isThrownBy(() -> manager.check(authentication, result));
} }
@Test
public void checkRequiresUserWhenMethodsFromInheritThenApplies() throws Exception {
MockMethodInvocation methodInvocation = new MockMethodInvocation(new PostAuthorizeClass(),
PostAuthorizeClass.class, "securedUser");
MethodInvocationResult result = new MethodInvocationResult(methodInvocation, null);
PostAuthorizeAuthorizationManager manager = new PostAuthorizeAuthorizationManager();
AuthorizationDecision decision = manager.check(TestAuthentication::authenticatedUser, result);
assertThat(decision.isGranted()).isTrue();
}
@PostAuthorize("hasRole('USER')")
public static class PostAuthorizeClass extends ParentClass {
}
public static class ParentClass {
public void securedUser() {
}
}
public static class TestClass implements InterfaceAnnotationsOne, InterfaceAnnotationsTwo { public static class TestClass implements InterfaceAnnotationsOne, InterfaceAnnotationsTwo {
public void doSomething() { public void doSomething() {

View File

@ -161,34 +161,6 @@ public class PostFilterAuthorizationMethodInterceptorTests {
SecurityContextHolder.setContextHolderStrategy(saved); SecurityContextHolder.setContextHolderStrategy(saved);
} }
@Test
public void checkPostFilterWhenMethodsFromInheritThenApplies() throws Throwable {
String[] array = { "john", "bob" };
MockMethodInvocation methodInvocation = new MockMethodInvocation(new PostFilterClass(), PostFilterClass.class,
"inheritMethod", new Class[] { String[].class }, new Object[] { array }) {
@Override
public Object proceed() {
return array;
}
};
PostFilterAuthorizationMethodInterceptor advice = new PostFilterAuthorizationMethodInterceptor();
Object result = advice.invoke(methodInvocation);
assertThat(result).asInstanceOf(InstanceOfAssertFactories.array(String[].class)).containsOnly("john");
}
@PostFilter("filterObject == 'john'")
public static class PostFilterClass extends ParentClass {
}
public static class ParentClass {
public String[] inheritMethod(String[] array) {
return array;
}
}
@PostFilter("filterObject == 'john'") @PostFilter("filterObject == 'john'")
public static class TestClass implements InterfaceAnnotationsOne, InterfaceAnnotationsTwo { public static class TestClass implements InterfaceAnnotationsOne, InterfaceAnnotationsTwo {

View File

@ -137,28 +137,6 @@ public class PreAuthorizeAuthorizationManagerTests {
assertThat(decision.isGranted()).isTrue(); assertThat(decision.isGranted()).isTrue();
} }
@Test
public void checkRequiresUserWhenMethodsFromInheritThenApplies() throws Exception {
MockMethodInvocation methodInvocation = new MockMethodInvocation(new PreAuthorizeClass(),
PreAuthorizeClass.class, "securedUser");
PreAuthorizeAuthorizationManager manager = new PreAuthorizeAuthorizationManager();
AuthorizationDecision decision = manager.check(TestAuthentication::authenticatedUser, methodInvocation);
assertThat(decision.isGranted()).isTrue();
}
@PreAuthorize("hasRole('USER')")
public static class PreAuthorizeClass extends ParentClass {
}
public static class ParentClass {
public void securedUser() {
}
}
public static class TestClass implements InterfaceAnnotationsOne, InterfaceAnnotationsTwo { public static class TestClass implements InterfaceAnnotationsOne, InterfaceAnnotationsTwo {
public void doSomething() { public void doSomething() {

View File

@ -215,32 +215,6 @@ public class PreFilterAuthorizationMethodInterceptorTests {
SecurityContextHolder.setContextHolderStrategy(saved); SecurityContextHolder.setContextHolderStrategy(saved);
} }
@Test
public void checkPreFilterWhenMethodsFromInheritThenApplies() throws Throwable {
List<String> list = new ArrayList<>();
list.add("john");
list.add("bob");
MockMethodInvocation invocation = new MockMethodInvocation(new PreFilterClass(), PreFilterClass.class,
"inheritMethod", new Class[] { List.class }, new Object[] { list });
PreFilterAuthorizationMethodInterceptor advice = new PreFilterAuthorizationMethodInterceptor();
advice.invoke(invocation);
assertThat(list).hasSize(1);
assertThat(list.get(0)).isEqualTo("john");
}
@PreFilter("filterObject == 'john'")
public static class PreFilterClass extends ParentClass {
}
public static class ParentClass {
public void inheritMethod(List<String> list) {
}
}
@PreFilter("filterObject == 'john'") @PreFilter("filterObject == 'john'")
public static class TestClass implements InterfaceAnnotationsOne, InterfaceAnnotationsTwo { public static class TestClass implements InterfaceAnnotationsOne, InterfaceAnnotationsTwo {

View File

@ -167,28 +167,6 @@ public class SecuredAuthorizationManagerTests {
assertThat(decision.isGranted()).isTrue(); assertThat(decision.isGranted()).isTrue();
} }
@Test
public void checkRequiresUserWhenMethodsFromInheritThenApplies() throws Exception {
MockMethodInvocation methodInvocation = new MockMethodInvocation(new SecuredSonClass(), SecuredSonClass.class,
"securedUser");
SecuredAuthorizationManager manager = new SecuredAuthorizationManager();
AuthorizationDecision decision = manager.check(TestAuthentication::authenticatedUser, methodInvocation);
assertThat(decision.isGranted()).isTrue();
}
@Secured("ROLE_USER")
public static class SecuredSonClass extends ParentClass {
}
public static class ParentClass {
public void securedUser() {
}
}
public static class TestClass implements InterfaceAnnotationsOne, InterfaceAnnotationsTwo { public static class TestClass implements InterfaceAnnotationsOne, InterfaceAnnotationsTwo {
public void doSomething() { public void doSomething() {

View File

@ -242,6 +242,15 @@ public class UniqueMergedAnnotationSynthesizerTests {
assertThat(preAuthorize.value()).isEqualTo("three"); assertThat(preAuthorize.value()).isEqualTo("three");
} }
// gh-15352
@Test
void synthesizeWhenClassInheritingAbstractClassNoAnnotationsThenNoAnnotation() throws Exception {
Method method = ClassInheritingAbstractClassNoAnnotations.class.getMethod("otherMethod");
Class<?> targetClass = ClassInheritingAbstractClassNoAnnotations.class;
PreAuthorize preAuthorize = this.synthesizer.synthesize(method, targetClass);
assertThat(preAuthorize).isNull();
}
@PreAuthorize("one") @PreAuthorize("one")
private interface AnnotationOnInterface { private interface AnnotationOnInterface {
@ -555,4 +564,17 @@ public class UniqueMergedAnnotationSynthesizerTests {
} }
public abstract static class AbstractClassNoAnnotations {
public String otherMethod() {
return "ok";
}
}
@PreAuthorize("twentynine")
private static class ClassInheritingAbstractClassNoAnnotations extends AbstractClassNoAnnotations {
}
} }