Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MergedAnnotations finds duplicate annotations on method in multi-level interface hierarchy #31803

Closed
jzheaux opened this issue Dec 9, 2023 · 3 comments
Assignees
Labels
in: core Issues in core modules (aop, beans, core, context, expression) status: backported An issue that has been backported to maintenance branches type: bug A general bug
Milestone

Comments

@jzheaux
Copy link
Contributor

jzheaux commented Dec 9, 2023

Related to spring-projects/spring-security#13625 (GitHub reproducer)

In a hierarchy like this:

interface Hello {
    @PreAuthorize("...")
    void sayHello();
}

interface SayHello extends Hello {}

class HelloImpl implements SayHello {
    public void sayHello() {}
}

a call to MergedAnnoatations like this:

MergedAnnotations mergedAnnotations = MergedAnnotations.from(HelloImpl.class.getMethod("sayHello"),
	SearchStrategy.TYPE_HIERARCHY);

will return multiple instances of MergedAnnotation for PreAuthorize.class.

It's expected that such an arrangement would only produce one MergedAnnotation instance since there is only one in the hierarchy.

Thanks to @philwebb for helping me find a workaround. Since PreAuthorize is not a repeatable annotation, Spring Security can ignore subsequent MergedAnnotation instances from the same MergedAnnotation#getSource.

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged or decided on label Dec 9, 2023
@sbrannen sbrannen self-assigned this Dec 9, 2023
@sbrannen sbrannen added type: bug A general bug in: core Issues in core modules (aop, beans, core, context, expression) and removed status: waiting-for-triage An issue we've not yet triaged or decided on labels Dec 9, 2023
@sbrannen sbrannen added this to the 6.1.2 milestone Dec 9, 2023
@sbrannen sbrannen changed the title MergedAnnotations should not duplicate annotations MergedAnnotations should not duplicate annotations Dec 9, 2023
@sbrannen
Copy link
Member

Related workaround in Spring Security: spring-projects/spring-security@be11812

@sbrannen
Copy link
Member

sbrannen commented Dec 10, 2023

Thanks for reporting the issue. 👍

I've been able to confirm it with the following standalone test class.

package demo;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Method;

import org.junit.jupiter.api.Test;

import org.springframework.core.annotation.MergedAnnotation;
import org.springframework.core.annotation.MergedAnnotations;

import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.core.annotation.MergedAnnotations.search;
import static org.springframework.core.annotation.MergedAnnotations.SearchStrategy.TYPE_HIERARCHY;

class ReproTests {

	@Test
	void test() throws Exception {
		Method method = HelloImpl.class.getMethod("sayHello");
		Stream<PreAuthorize> stream = search(TYPE_HIERARCHY)
				.from(method)
				.stream(PreAuthorize.class)
				.map(MergedAnnotation::synthesize);

		assertThat(stream).hasSize(1);
	}


	interface Hello {

		@PreAuthorize("demo")
		void sayHello();
	}

	interface SayHello extends Hello {
	}

	static class HelloImpl implements SayHello {

		@Override
		public void sayHello() {
		}
	}

	@Target(ElementType.METHOD)
	@Retention(RetentionPolicy.RUNTIME)
	@interface PreAuthorize {
		String value();
	}

}

The above fails with:

java.lang.AssertionError: 
Expected size: 1 but was: 2 in:
[@demo.ReproTests$PreAuthorize("demo"), @demo.ReproTests$PreAuthorize("demo")]
	at demo.ReproTests.test(ReproTests.java:42)
	at java.base/java.lang.reflect.Method.invoke(Method.java:568)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)

Furthermore, I verified that the test passes if HelloImpl implements Hello directly.

@sbrannen sbrannen changed the title MergedAnnotations should not duplicate annotations MergedAnnotations finds duplicate annotations on methods in multi-level interface hierarchy Dec 10, 2023
@sbrannen sbrannen changed the title MergedAnnotations finds duplicate annotations on methods in multi-level interface hierarchy MergedAnnotations finds duplicate annotations on method in multi-level interface hierarchy Dec 10, 2023
@github-actions github-actions bot added status: backported An issue that has been backported to maintenance branches and removed for: backport-to-6.0.x labels Dec 12, 2023
sbrannen added a commit that referenced this issue Dec 12, 2023
Prior to this commit, the AnnotationsScanner used in the
MergedAnnotations infrastructure found duplicate annotations on methods
within multi-level interface hierarchies.

This commit addresses this issue by scanning methods at a given level
in the interface hierarchy using ReflectionUtils#getDeclaredMethods
instead of Class#getMethods, since the latter includes public methods
declared in super-interfaces which will anyway be scanned when
processing super-interfaces recursively.

Closes gh-31803

(cherry picked from commit 75da9c3)
sbrannen added a commit that referenced this issue Dec 13, 2023
Prior to this commit, the AnnotationsScanner used in the
MergedAnnotations infrastructure found duplicate annotations on methods
within multi-level interface hierarchies.

This commit addresses this issue by scanning methods at a given level
in the interface hierarchy using ReflectionUtils#getDeclaredMethods
instead of Class#getMethods, since the latter includes public methods
declared in super-interfaces which will anyway be scanned when
processing super-interfaces recursively.

Closes gh-31803

(cherry picked from commit 75da9c3)
(cherry picked from commit 1e742aa)
@sbrannen
Copy link
Member

This has been fixed in 75da9c3 for inclusion in the upcoming Spring Framework 6.1.2 release.

Because this bug has existed since 5.2 when the MergedAnnotations API was introduced, the fix has also been backported to 6.0.15 and 5.3.32.

sbrannen added a commit to sbrannen/spring-security that referenced this issue Jan 5, 2024
This commit revises AuthorizationAnnotationUtils as follows.

- Removes code duplication by treating both Class and Method as
  AnnotatedElement.

- Avoids duplicated annotation searches by processing merged
  annotations in a single Stream instead of first using the
  MergedAnnotations API to find possible duplicates and then again
  searching for a single annotation via AnnotationUtils (which
  effectively performs the same search using the MergedAnnotations API
  internally).

- Uses `.distinct()` within the Stream to avoid the need for the
  workaround introduced in spring-projectsgh-13625. Note that the semantics here
  result in duplicate "equivalent" annotations being ignored. In other
  words, if @⁠PreAuthorize("hasRole('someRole')") is present multiple
  times as a meta-annotation, no exception will be thrown and the first
  such annotation found will be used.

- Improves the error message when competing annotations are found by
  including the competing annotations in the error message.

- Updates AuthorizationAnnotationUtilsTests to cover all known,
  supported use cases.

- Configures correct role in @⁠RequireUserRole.

Please note this commit uses
`.map(MergedAnnotation::withNonMergedAttributes)` to retain backward
compatibility with previous versions of Spring Security. However, that
line can be deleted if the Spring Security team decides that it wishes
to support merged annotation attributes via custom composed
annotations. If that decision is made, the
composedMergedAnnotationsAreNotSupported() test should be renamed and
updated as explained in the comment in that method.

See spring-projectsgh-13625
See spring-projects/spring-framework#31803
jzheaux pushed a commit to spring-projects/spring-security that referenced this issue Jan 18, 2024
This commit revises AuthorizationAnnotationUtils as follows.

- Removes code duplication by treating both Class and Method as
  AnnotatedElement.

- Avoids duplicated annotation searches by processing merged
  annotations in a single Stream instead of first using the
  MergedAnnotations API to find possible duplicates and then again
  searching for a single annotation via AnnotationUtils (which
  effectively performs the same search using the MergedAnnotations API
  internally).

- Uses `.distinct()` within the Stream to avoid the need for the
  workaround introduced in gh-13625. Note that the semantics here
  result in duplicate "equivalent" annotations being ignored. In other
  words, if @⁠PreAuthorize("hasRole('someRole')") is present multiple
  times as a meta-annotation, no exception will be thrown and the first
  such annotation found will be used.

- Improves the error message when competing annotations are found by
  including the competing annotations in the error message.

- Updates AuthorizationAnnotationUtilsTests to cover all known,
  supported use cases.

- Configures correct role in @⁠RequireUserRole.

Please note this commit uses
`.map(MergedAnnotation::withNonMergedAttributes)` to retain backward
compatibility with previous versions of Spring Security. However, that
line can be deleted if the Spring Security team decides that it wishes
to support merged annotation attributes via custom composed
annotations. If that decision is made, the
composedMergedAnnotationsAreNotSupported() test should be renamed and
updated as explained in the comment in that method.

See gh-13625
See spring-projects/spring-framework#31803
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
in: core Issues in core modules (aop, beans, core, context, expression) status: backported An issue that has been backported to maintenance branches type: bug A general bug
Projects
None yet
Development

No branches or pull requests

3 participants