diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/reactive/EnableWebFluxSecurityTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/reactive/EnableWebFluxSecurityTests.java index 890af2bfa07..a2c0a7de2ff 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/reactive/EnableWebFluxSecurityTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/reactive/EnableWebFluxSecurityTests.java @@ -16,6 +16,10 @@ package org.springframework.security.config.annotation.web.reactive; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; import java.nio.charset.StandardCharsets; import org.junit.jupiter.api.Test; @@ -28,6 +32,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.core.Ordered; +import org.springframework.core.annotation.AliasFor; import org.springframework.core.annotation.Order; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DefaultDataBufferFactory; @@ -404,11 +409,28 @@ public String username(UserDetails user) { } + @Target({ ElementType.PARAMETER }) + @Retention(RetentionPolicy.RUNTIME) + @AuthenticationPrincipal + @interface Property { + + @AliasFor(attribute = "expression", annotation = AuthenticationPrincipal.class) + String value() default "id"; + + } + + interface UsernameResolver { + + String username(@Property("@principalBean.username(#this)") String username); + + } + @RestController - static class AuthenticationPrincipalResolver { + static class AuthenticationPrincipalResolver implements UsernameResolver { + @Override @GetMapping("/spel") - String username(@AuthenticationPrincipal(expression = "@principalBean.username(#this)") String username) { + public String username(String username) { return username; } diff --git a/messaging/src/test/java/org/springframework/security/messaging/context/AuthenticationPrincipalArgumentResolverTests.java b/messaging/src/test/java/org/springframework/security/messaging/context/AuthenticationPrincipalArgumentResolverTests.java index 0074657e222..a2b7a0603e1 100644 --- a/messaging/src/test/java/org/springframework/security/messaging/context/AuthenticationPrincipalArgumentResolverTests.java +++ b/messaging/src/test/java/org/springframework/security/messaging/context/AuthenticationPrincipalArgumentResolverTests.java @@ -28,6 +28,7 @@ import org.springframework.core.MethodParameter; import org.springframework.core.annotation.AliasFor; +import org.springframework.core.annotation.AnnotatedMethod; import org.springframework.security.authentication.TestingAuthenticationToken; import org.springframework.security.core.annotation.AnnotationTemplateExpressionDefaults; import org.springframework.security.core.annotation.AuthenticationPrincipal; @@ -186,10 +187,21 @@ public void resolveArgumentCustomMetaAnnotationTpl() throws Exception { assertThat(this.resolver.resolveArgument(showUserCustomMetaAnnotationTpl(), null)).isEqualTo(principal.id); } + @Test + public void resolveArgumentWhenAliasForOnInterfaceThenInherits() { + CustomUserPrincipal principal = new CustomUserPrincipal(); + setAuthenticationPrincipal(principal); + assertThat(this.resolver.resolveArgument(showUserNoConcreteAnnotation(), null)).isEqualTo(principal.property); + } + private MethodParameter showUserNoAnnotation() { return getMethodParameter("showUserNoAnnotation", String.class); } + private MethodParameter showUserNoConcreteAnnotation() { + return getMethodParameter("showUserNoConcreteAnnotation", String.class); + } + private MethodParameter showUserAnnotationString() { return getMethodParameter("showUserAnnotation", String.class); } @@ -240,7 +252,7 @@ private MethodParameter showUserAnnotationObject() { private MethodParameter getMethodParameter(String methodName, Class... paramTypes) { Method method = ReflectionUtils.findMethod(TestController.class, methodName, paramTypes); - return new MethodParameter(method, 0); + return new AnnotatedMethod(method).getMethodParameters()[0]; } private void setAuthenticationPrincipal(Object principal) { @@ -280,11 +292,32 @@ private void setAuthenticationPrincipal(Object principal) { } - public static class TestController { + @Target({ ElementType.PARAMETER }) + @Retention(RetentionPolicy.RUNTIME) + @AuthenticationPrincipal + @interface Property { + + @AliasFor(attribute = "expression", annotation = AuthenticationPrincipal.class) + String value() default "id"; + + } + + private interface TestInterface { + + void showUserNoConcreteAnnotation(@Property("property") String property); + + } + + public static class TestController implements TestInterface { public void showUserNoAnnotation(String user) { } + @Override + public void showUserNoConcreteAnnotation(String user) { + + } + public void showUserAnnotation(@AuthenticationPrincipal String user) { } diff --git a/messaging/src/test/java/org/springframework/security/messaging/handler/invocation/reactive/AuthenticationPrincipalArgumentResolverTests.java b/messaging/src/test/java/org/springframework/security/messaging/handler/invocation/reactive/AuthenticationPrincipalArgumentResolverTests.java index 67c77123b86..3d1b6440367 100644 --- a/messaging/src/test/java/org/springframework/security/messaging/handler/invocation/reactive/AuthenticationPrincipalArgumentResolverTests.java +++ b/messaging/src/test/java/org/springframework/security/messaging/handler/invocation/reactive/AuthenticationPrincipalArgumentResolverTests.java @@ -16,14 +16,17 @@ package org.springframework.security.messaging.handler.invocation.reactive; +import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; import org.junit.jupiter.api.Test; import reactor.core.publisher.Mono; import org.springframework.core.MethodParameter; import org.springframework.core.annotation.AliasFor; +import org.springframework.core.annotation.AnnotatedMethod; import org.springframework.core.annotation.SynthesizingMethodParameter; import org.springframework.security.authentication.TestAuthentication; import org.springframework.security.authentication.TestingAuthenticationToken; @@ -128,6 +131,19 @@ public void supportsParameterWhenNotAnnotatedThenFalse() { assertThat(this.resolver.supportsParameter(arg0("monoUserDetails"))).isFalse(); } + @Test + public void resolveArgumentWhenAliasForOnInterfaceThenInherits() { + CustomUserPrincipal principal = new CustomUserPrincipal(); + Authentication authentication = new TestingAuthenticationToken(principal, "password", "ROLE_USER"); + ResolvableMethod method = ResolvableMethod.on(TestController.class) + .named("showUserNoConcreteAnnotation") + .method(); + MethodParameter parameter = new AnnotatedMethod(method.method()).getMethodParameters()[0]; + Mono result = this.resolver.resolveArgument(parameter, null) + .contextWrite(ReactiveSecurityContextHolder.withAuthentication(authentication)); + assertThat(result.block()).isEqualTo(principal.property); + } + @SuppressWarnings("unused") private void monoUserDetails(Mono user) { } @@ -172,6 +188,8 @@ static class CustomUserPrincipal { public final int id = 1; + public final String property = "property"; + public Object getPrincipal() { return this; } @@ -195,4 +213,29 @@ public Object getPrincipal() { } + @Target({ ElementType.PARAMETER }) + @Retention(RetentionPolicy.RUNTIME) + @AuthenticationPrincipal + @interface Property { + + @AliasFor(attribute = "expression", annotation = AuthenticationPrincipal.class) + String value() default "id"; + + } + + private interface TestInterface { + + void showUserNoConcreteAnnotation(@Property("property") String property); + + } + + private static class TestController implements TestInterface { + + @Override + public void showUserNoConcreteAnnotation(String user) { + + } + + } + } diff --git a/web/src/main/java/org/springframework/security/web/method/annotation/AuthenticationPrincipalArgumentResolver.java b/web/src/main/java/org/springframework/security/web/method/annotation/AuthenticationPrincipalArgumentResolver.java index 63a660012d7..94471bb3476 100644 --- a/web/src/main/java/org/springframework/security/web/method/annotation/AuthenticationPrincipalArgumentResolver.java +++ b/web/src/main/java/org/springframework/security/web/method/annotation/AuthenticationPrincipalArgumentResolver.java @@ -19,6 +19,8 @@ import java.lang.annotation.Annotation; import org.springframework.core.MethodParameter; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.core.annotation.MergedAnnotations; import org.springframework.expression.BeanResolver; import org.springframework.expression.Expression; import org.springframework.expression.ExpressionParser; @@ -93,6 +95,8 @@ */ public final class AuthenticationPrincipalArgumentResolver implements HandlerMethodArgumentResolver { + private final Class annotationType = AuthenticationPrincipal.class; + private SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder .getContextHolderStrategy(); @@ -101,6 +105,8 @@ public final class AuthenticationPrincipalArgumentResolver implements HandlerMet private SecurityAnnotationScanner scanner = SecurityAnnotationScanners .requireUnique(AuthenticationPrincipal.class); + private boolean useAnnotationTemplate = false; + private BeanResolver beanResolver; @Override @@ -165,6 +171,7 @@ public void setSecurityContextHolderStrategy(SecurityContextHolderStrategy secur */ public void setTemplateDefaults(AnnotationTemplateExpressionDefaults templateDefaults) { this.scanner = SecurityAnnotationScanners.requireUnique(AuthenticationPrincipal.class, templateDefaults); + this.useAnnotationTemplate = templateDefaults != null; } /** @@ -174,8 +181,22 @@ public void setTemplateDefaults(AnnotationTemplateExpressionDefaults templateDef * @return the {@link Annotation} that was found or null. */ @SuppressWarnings("unchecked") - private T findMethodAnnotation(MethodParameter parameter) { - return (T) this.scanner.scan(parameter.getParameter()); + private AuthenticationPrincipal findMethodAnnotation(MethodParameter parameter) { + if (this.useAnnotationTemplate) { + return this.scanner.scan(parameter.getParameter()); + } + AuthenticationPrincipal annotation = parameter.getParameterAnnotation(this.annotationType); + if (annotation != null) { + return annotation; + } + Annotation[] annotationsToSearch = parameter.getParameterAnnotations(); + for (Annotation toSearch : annotationsToSearch) { + annotation = AnnotationUtils.findAnnotation(toSearch.annotationType(), this.annotationType); + if (annotation != null) { + return MergedAnnotations.from(toSearch).get(this.annotationType).synthesize(); + } + } + return null; } } diff --git a/web/src/test/java/org/springframework/security/web/method/annotation/AuthenticationPrincipalArgumentResolverTests.java b/web/src/test/java/org/springframework/security/web/method/annotation/AuthenticationPrincipalArgumentResolverTests.java index eda4249315b..fcc1e05088c 100644 --- a/web/src/test/java/org/springframework/security/web/method/annotation/AuthenticationPrincipalArgumentResolverTests.java +++ b/web/src/test/java/org/springframework/security/web/method/annotation/AuthenticationPrincipalArgumentResolverTests.java @@ -28,6 +28,7 @@ import org.springframework.core.MethodParameter; import org.springframework.core.annotation.AliasFor; +import org.springframework.core.annotation.AnnotatedMethod; import org.springframework.expression.BeanResolver; import org.springframework.security.authentication.TestingAuthenticationToken; import org.springframework.security.core.annotation.AnnotationTemplateExpressionDefaults; @@ -214,10 +215,22 @@ public void resolveArgumentCustomMetaAnnotationTpl() throws Exception { .isEqualTo(this.expectedPrincipal); } + @Test + public void resolveArgumentWhenAliasForOnInterfaceThenInherits() throws Exception { + CustomUserPrincipal principal = new CustomUserPrincipal(); + setAuthenticationPrincipal(principal); + assertThat(this.resolver.resolveArgument(showUserNoConcreteAnnotation(), null, null, null)) + .isEqualTo(principal.property); + } + private MethodParameter showUserNoAnnotation() { return getMethodParameter("showUserNoAnnotation", String.class); } + private MethodParameter showUserNoConcreteAnnotation() { + return getMethodParameter("showUserNoConcreteAnnotation", String.class); + } + private MethodParameter showUserAnnotationString() { return getMethodParameter("showUserAnnotation", String.class); } @@ -272,7 +285,7 @@ private MethodParameter showUserCustomMetaAnnotationTpl() { private MethodParameter getMethodParameter(String methodName, Class... paramTypes) { Method method = ReflectionUtils.findMethod(TestController.class, methodName, paramTypes); - return new MethodParameter(method, 0); + return new AnnotatedMethod(method).getMethodParameters()[0]; } private void setAuthenticationPrincipal(Object principal) { @@ -295,6 +308,16 @@ private void setAuthenticationPrincipal(Object principal) { } + @Target({ ElementType.PARAMETER }) + @Retention(RetentionPolicy.RUNTIME) + @AuthenticationPrincipal + @interface Property { + + @AliasFor(attribute = "expression", annotation = AuthenticationPrincipal.class) + String value() default "id"; + + } + @Retention(RetentionPolicy.RUNTIME) @AuthenticationPrincipal public @interface CurrentUser2 { @@ -312,11 +335,22 @@ private void setAuthenticationPrincipal(Object principal) { } - public static class TestController { + public interface TestInterface { + + void showUserNoConcreteAnnotation(@Property("property") String property); + + } + + public static class TestController implements TestInterface { public void showUserNoAnnotation(String user) { } + @Override + public void showUserNoConcreteAnnotation(String user) { + + } + public void showUserAnnotation(@AuthenticationPrincipal String user) { } diff --git a/web/src/test/java/org/springframework/security/web/reactive/result/method/annotation/AuthenticationPrincipalArgumentResolverTests.java b/web/src/test/java/org/springframework/security/web/reactive/result/method/annotation/AuthenticationPrincipalArgumentResolverTests.java index 507175a9df9..63d6deea0aa 100644 --- a/web/src/test/java/org/springframework/security/web/reactive/result/method/annotation/AuthenticationPrincipalArgumentResolverTests.java +++ b/web/src/test/java/org/springframework/security/web/reactive/result/method/annotation/AuthenticationPrincipalArgumentResolverTests.java @@ -32,6 +32,7 @@ import org.springframework.core.MethodParameter; import org.springframework.core.ReactiveAdapterRegistry; import org.springframework.core.annotation.AliasFor; +import org.springframework.core.annotation.AnnotatedMethod; import org.springframework.core.annotation.SynthesizingMethodParameter; import org.springframework.expression.BeanResolver; import org.springframework.security.core.Authentication; @@ -230,6 +231,19 @@ public void resolveArgumentCustomMetaAnnotationTpl() { assertThat(result.block()).isEqualTo(principal.id); } + @Test + public void resolveArgumentWhenAliasForOnInterfaceThenInherits() { + CustomUserPrincipal principal = new CustomUserPrincipal(); + given(this.authentication.getPrincipal()).willReturn(principal); + ResolvableMethod method = ResolvableMethod.on(TestController.class) + .named("showUserNoConcreteAnnotation") + .build(); + MethodParameter parameter = new AnnotatedMethod(method.method()).getMethodParameters()[0]; + Mono result = this.resolver.resolveArgument(parameter, this.bindingContext, this.exchange) + .contextWrite(ReactiveSecurityContextHolder.withAuthentication(this.authentication)); + assertThat(result.block()).isEqualTo(principal.property); + } + private MethodParameter arg0(String methodName) { ResolvableMethod method = ResolvableMethod.on(getClass()).named(methodName).build(); return new SynthesizingMethodParameter(method.method(), 0); @@ -317,6 +331,8 @@ static class CustomUserPrincipal { public final int id = 1; + public final String property = "property"; + public Object getPrincipal() { return this; } @@ -340,4 +356,29 @@ public Object getPrincipal() { } + @Target({ ElementType.PARAMETER }) + @Retention(RetentionPolicy.RUNTIME) + @AuthenticationPrincipal + @interface Property { + + @AliasFor(attribute = "expression", annotation = AuthenticationPrincipal.class) + String value() default "id"; + + } + + private interface TestInterface { + + void showUserNoConcreteAnnotation(@Property("property") String property); + + } + + private static class TestController implements TestInterface { + + @Override + public void showUserNoConcreteAnnotation(String user) { + + } + + } + }