diff --git a/build.gradle b/build.gradle index 65d5d625b9..330fb1f6ba 100644 --- a/build.gradle +++ b/build.gradle @@ -25,7 +25,7 @@ allprojects { ext.releaseBuild = version.endsWith('RELEASE') ext.snapshotBuild = version.endsWith('SNAPSHOT') - ext.springVersion = '4.1.0.RC2' + ext.springVersion = '4.1.1.BUILD-SNAPSHOT' ext.springLdapVersion = '2.0.1.RELEASE' group = 'org.springframework.security' diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/servlet/configuration/WebMvcSecurityConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/web/servlet/configuration/WebMvcSecurityConfiguration.java index 984d6611bb..e13ac62dd2 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/servlet/configuration/WebMvcSecurityConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/servlet/configuration/WebMvcSecurityConfiguration.java @@ -20,7 +20,7 @@ import java.util.List; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; -import org.springframework.security.web.bind.support.AuthenticationPrincipalArgumentResolver; +import org.springframework.security.web.method.annotation.AuthenticationPrincipalArgumentResolver; import org.springframework.security.web.servlet.support.csrf.CsrfRequestDataValueProcessor; import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.servlet.config.annotation.EnableWebMvc; @@ -43,9 +43,11 @@ import org.springframework.web.servlet.support.RequestDataValueProcessor; public class WebMvcSecurityConfiguration extends WebMvcConfigurerAdapter { @Override + @SuppressWarnings("deprecation") public void addArgumentResolvers( List argumentResolvers) { argumentResolvers.add(new AuthenticationPrincipalArgumentResolver()); + argumentResolvers.add(new org.springframework.security.web.bind.support.AuthenticationPrincipalArgumentResolver()); } @Bean diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/socket/AbstractSecurityWebSocketMessageBrokerConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/socket/AbstractSecurityWebSocketMessageBrokerConfigurer.java index 75430a4171..8223e1b735 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/socket/AbstractSecurityWebSocketMessageBrokerConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/socket/AbstractSecurityWebSocketMessageBrokerConfigurer.java @@ -18,6 +18,7 @@ package org.springframework.security.config.annotation.web.socket; import org.springframework.context.annotation.Bean; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; +import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver; import org.springframework.messaging.simp.config.ChannelRegistration; import org.springframework.security.access.AccessDecisionVoter; import org.springframework.security.access.vote.AffirmativeBased; @@ -25,6 +26,7 @@ import org.springframework.security.config.annotation.web.messaging.MessageSecur import org.springframework.security.messaging.access.expression.MessageExpressionVoter; import org.springframework.security.messaging.access.intercept.ChannelSecurityInterceptor; import org.springframework.security.messaging.access.intercept.MessageSecurityMetadataSource; +import org.springframework.security.messaging.context.AuthenticationPrincipalArgumentResolver; import org.springframework.security.messaging.context.SecurityContextChannelInterceptor; import org.springframework.web.socket.config.annotation.AbstractWebSocketMessageBrokerConfigurer; import org.springframework.web.socket.config.annotation.StompEndpointRegistry; @@ -62,6 +64,13 @@ public abstract class AbstractSecurityWebSocketMessageBrokerConfigurer extends A public final void registerStompEndpoints(StompEndpointRegistry registry) {} + @Override + public void addArgumentResolvers( + List argumentResolvers) { + argumentResolvers.add(new AuthenticationPrincipalArgumentResolver()); + } + + @Override public final void configureClientInboundChannel(ChannelRegistration registration) { ChannelSecurityInterceptor inboundChannelSecurity = inboundChannelSecurity(); diff --git a/core/src/main/java/org/springframework/security/core/SpringSecurityCoreVersion.java b/core/src/main/java/org/springframework/security/core/SpringSecurityCoreVersion.java index 89eca48c54..98a48bc455 100644 --- a/core/src/main/java/org/springframework/security/core/SpringSecurityCoreVersion.java +++ b/core/src/main/java/org/springframework/security/core/SpringSecurityCoreVersion.java @@ -23,7 +23,7 @@ public class SpringSecurityCoreVersion { */ public static final long SERIAL_VERSION_UID = 400L; - static final String MIN_SPRING_VERSION = "4.1.0.RC2"; + static final String MIN_SPRING_VERSION = "4.1.1.BUILD-SNAPSHOT"; static { performVersionChecks(); diff --git a/core/src/main/java/org/springframework/security/core/annotation/AuthenticationPrincipal.java b/core/src/main/java/org/springframework/security/core/annotation/AuthenticationPrincipal.java new file mode 100644 index 0000000000..53b2319bd2 --- /dev/null +++ b/core/src/main/java/org/springframework/security/core/annotation/AuthenticationPrincipal.java @@ -0,0 +1,49 @@ +/* + * Copyright 2002-2013 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 + * + * http://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.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.security.core.Authentication; + +/** + * Annotation that is used to resolve + * {@link Authentication#getPrincipal()} to a method argument. + * + * @author Rob Winch + * @since 4.0 + * + * @see AuthenticationPrincipalArgumentResolver + */ +@Target({ ElementType.PARAMETER, ElementType.ANNOTATION_TYPE }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface AuthenticationPrincipal { + + /** + * True if a {@link ClassCastException} should be thrown + * when the current {@link Authentication#getPrincipal()} is the incorrect + * type. Default is false. + * + * @return + */ + boolean errorOnInvalidType() default false; +} + diff --git a/messaging/src/main/java/org/springframework/security/messaging/context/AuthenticationPrincipalArgumentResolver.java b/messaging/src/main/java/org/springframework/security/messaging/context/AuthenticationPrincipalArgumentResolver.java new file mode 100644 index 0000000000..27285df1c6 --- /dev/null +++ b/messaging/src/main/java/org/springframework/security/messaging/context/AuthenticationPrincipalArgumentResolver.java @@ -0,0 +1,134 @@ +/* + * Copyright 2002-2013 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 + * + * http://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.messaging.context; + +import java.lang.annotation.Annotation; + +import org.springframework.core.MethodParameter; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.messaging.Message; +import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Controller; + + +/** + * Allows resolving the {@link Authentication#getPrincipal()} using the + * {@link AuthenticationPrincipal} annotation. For example, the following + * {@link Controller}: + * + *
+ * @Controller
+ * public class MyController {
+ *     @MessageMapping("/im")
+ *     public void im(@AuthenticationPrincipal CustomUser customUser) {
+ *         // do something with CustomUser
+ *     }
+ * 
+ * + *

+ * Will resolve the CustomUser argument using + * {@link Authentication#getPrincipal()} from the {@link SecurityContextHolder}. + * If the {@link Authentication} or {@link Authentication#getPrincipal()} is + * null, it will return null. If the types do not match, null will be returned + * unless {@link AuthenticationPrincipal#errorOnInvalidType()} is true in which + * case a {@link ClassCastException} will be thrown. + *

+ * + *

+ * Alternatively, users can create a custom meta annotation as shown below: + *

+ * + *
+ * @Target({ ElementType.PARAMETER })
+ * @Retention(RetentionPolicy.RUNTIME)
+ * @AuthenticationPrincipal
+ * public @interface CurrentUser {
+ * }
+ * 
+ * + *

+ * The custom annotation can then be used instead. For example: + *

+ * + *
+ * @Controller
+ * public class MyController {
+ *     @MessageMapping("/im")
+ *     public void im(@CurrentUser CustomUser customUser) {
+ *         // do something with CustomUser
+ *     }
+ * 
+ * + * @author Rob Winch + * @since 4.0 + */ +public final class AuthenticationPrincipalArgumentResolver implements + HandlerMethodArgumentResolver { + + /* + * (non-Javadoc) + * @see org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver#supportsParameter(org.springframework.core.MethodParameter) + */ + public boolean supportsParameter(MethodParameter parameter) { + return findMethodAnnotation(AuthenticationPrincipal.class, parameter) != null; + } + + /* + * (non-Javadoc) + * @see org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver#resolveArgument(org.springframework.core.MethodParameter, org.springframework.messaging.Message) + */ + public Object resolveArgument(MethodParameter parameter, Message message) throws Exception { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if(authentication == null) { + return null; + } + Object principal = authentication.getPrincipal(); + if(principal != null && !parameter.getParameterType().isAssignableFrom(principal.getClass())) { + AuthenticationPrincipal authPrincipal = findMethodAnnotation(AuthenticationPrincipal.class, parameter); + if(authPrincipal.errorOnInvalidType()) { + throw new ClassCastException(principal + " is not assignable to " + parameter.getParameterType()); + } else { + return null; + } + } + return principal; + } + + /** + * Obtains the specified {@link Annotation} on the specified {@link MethodParameter}. + * + * @param annotationClass the class of the {@link Annotation} to find on the {@link MethodParameter} + * @param parameter the {@link MethodParameter} to search for an {@link Annotation} + * @return the {@link Annotation} that was found or null. + */ + private T findMethodAnnotation(Class annotationClass, MethodParameter parameter) { + T annotation = parameter.getParameterAnnotation(annotationClass); + if(annotation != null) { + return annotation; + } + Annotation[] annotationsToSearch = parameter.getParameterAnnotations(); + for(Annotation toSearch : annotationsToSearch) { + annotation = AnnotationUtils.findAnnotation(toSearch.annotationType(), annotationClass); + if(annotation != null) { + return annotation; + } + } + return null; + } +} \ No newline at end of file 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 new file mode 100644 index 0000000000..2181bc14c3 --- /dev/null +++ b/messaging/src/test/java/org/springframework/security/messaging/context/AuthenticationPrincipalArgumentResolverTests.java @@ -0,0 +1,202 @@ +/* + * Copyright 2002-2013 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 + * + * http://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.messaging.context; + + +import static org.fest.assertions.Assertions.assertThat; + +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.After; +import org.junit.Before; +import org.junit.Test; +import org.springframework.core.MethodParameter; +import org.springframework.security.authentication.TestingAuthenticationToken; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.util.ReflectionUtils; + +/** + * @author Rob Winch + * + */ +public class AuthenticationPrincipalArgumentResolverTests { + private Object expectedPrincipal; + private AuthenticationPrincipalArgumentResolver resolver; + + @Before + public void setup() { + resolver = new AuthenticationPrincipalArgumentResolver(); + } + + @After + public void cleanup() { + SecurityContextHolder.clearContext(); + } + + @Test + public void supportsParameterNoAnnotation() throws Exception { + assertThat(resolver.supportsParameter(showUserNoAnnotation())).isFalse(); + } + + @Test + public void supportsParameterAnnotation() throws Exception { + assertThat(resolver.supportsParameter(showUserAnnotationObject())).isTrue(); + } + + @Test + public void supportsParameterCustomAnnotation() throws Exception { + assertThat(resolver.supportsParameter(showUserCustomAnnotation())).isTrue(); + } + + @Test + public void resolveArgumentNullAuthentication() throws Exception { + assertThat(resolver.resolveArgument(showUserAnnotationString(), null)).isNull(); + } + + @Test + public void resolveArgumentNullPrincipal() throws Exception { + setAuthenticationPrincipal(null); + assertThat(resolver.resolveArgument(showUserAnnotationString(), null)).isNull(); + } + + @Test + public void resolveArgumentString() throws Exception { + setAuthenticationPrincipal("john"); + assertThat(resolver.resolveArgument(showUserAnnotationString(), null)).isEqualTo(expectedPrincipal); + } + + @Test + public void resolveArgumentPrincipalStringOnObject() throws Exception { + setAuthenticationPrincipal("john"); + assertThat(resolver.resolveArgument(showUserAnnotationObject(), null)).isEqualTo(expectedPrincipal); + } + + @Test + public void resolveArgumentUserDetails() throws Exception { + setAuthenticationPrincipal(new User("user", "password", AuthorityUtils.createAuthorityList("ROLE_USER"))); + assertThat(resolver.resolveArgument(showUserAnnotationUserDetails(), null)).isEqualTo(expectedPrincipal); + } + + @Test + public void resolveArgumentCustomUserPrincipal() throws Exception { + setAuthenticationPrincipal(new CustomUserPrincipal()); + assertThat(resolver.resolveArgument(showUserAnnotationCustomUserPrincipal(), null)).isEqualTo(expectedPrincipal); + } + + @Test + public void resolveArgumentCustomAnnotation() throws Exception { + setAuthenticationPrincipal(new CustomUserPrincipal()); + assertThat(resolver.resolveArgument(showUserCustomAnnotation(), null)).isEqualTo(expectedPrincipal); + } + + @Test + public void resolveArgumentNullOnInvalidType() throws Exception { + setAuthenticationPrincipal(new CustomUserPrincipal()); + assertThat(resolver.resolveArgument(showUserAnnotationString(), null)).isNull(); + } + + @Test(expected = ClassCastException.class) + public void resolveArgumentErrorOnInvalidType() throws Exception { + setAuthenticationPrincipal(new CustomUserPrincipal()); + resolver.resolveArgument(showUserAnnotationErrorOnInvalidType(), null); + } + + + @Test(expected = ClassCastException.class) + public void resolveArgumentCustomserErrorOnInvalidType() throws Exception { + setAuthenticationPrincipal(new CustomUserPrincipal()); + resolver.resolveArgument(showUserAnnotationCurrentUserErrorOnInvalidType(), null); + } + + @Test + public void resolveArgumentObject() throws Exception { + setAuthenticationPrincipal(new Object()); + assertThat(resolver.resolveArgument(showUserAnnotationObject(), null)).isEqualTo(expectedPrincipal); + } + + private MethodParameter showUserNoAnnotation() { + return getMethodParameter("showUserNoAnnotation", String.class); + } + + private MethodParameter showUserAnnotationString() { + return getMethodParameter("showUserAnnotation", String.class); + } + + private MethodParameter showUserAnnotationErrorOnInvalidType() { + return getMethodParameter("showUserAnnotationErrorOnInvalidType", String.class); + } + + private MethodParameter showUserAnnotationCurrentUserErrorOnInvalidType() { + return getMethodParameter("showUserAnnotationCurrentUserErrorOnInvalidType", String.class); + } + + private MethodParameter showUserAnnotationUserDetails() { + return getMethodParameter("showUserAnnotation", UserDetails.class); + } + + private MethodParameter showUserAnnotationCustomUserPrincipal() { + return getMethodParameter("showUserAnnotation", CustomUserPrincipal.class); + } + + private MethodParameter showUserCustomAnnotation() { + return getMethodParameter("showUserCustomAnnotation", CustomUserPrincipal.class); + } + + private MethodParameter showUserAnnotationObject() { + return getMethodParameter("showUserAnnotation", Object.class); + } + + private MethodParameter getMethodParameter(String methodName, Class... paramTypes) { + Method method = ReflectionUtils.findMethod(TestController.class, methodName,paramTypes); + return new MethodParameter(method,0); + } + + @Target({ ElementType.PARAMETER}) + @Retention(RetentionPolicy.RUNTIME) + @AuthenticationPrincipal + static @interface CurrentUser { } + + @Target({ ElementType.PARAMETER}) + @Retention(RetentionPolicy.RUNTIME) + @AuthenticationPrincipal(errorOnInvalidType = true) + static @interface CurrentUserErrorOnInvalidType { } + + public static class TestController { + public void showUserNoAnnotation(String user) {} + public void showUserAnnotation(@AuthenticationPrincipal String user) {} + public void showUserAnnotationErrorOnInvalidType(@AuthenticationPrincipal(errorOnInvalidType=true) String user) {} + public void showUserAnnotationCurrentUserErrorOnInvalidType(@CurrentUserErrorOnInvalidType String user) {} + public void showUserAnnotation(@AuthenticationPrincipal UserDetails user) {} + public void showUserAnnotation(@AuthenticationPrincipal CustomUserPrincipal user) {} + public void showUserCustomAnnotation(@CurrentUser CustomUserPrincipal user) {} + public void showUserAnnotation(@AuthenticationPrincipal Object user) {} + } + + private static class CustomUserPrincipal {} + + private void setAuthenticationPrincipal(Object principal) { + this.expectedPrincipal = principal; + SecurityContextHolder.getContext().setAuthentication(new TestingAuthenticationToken(expectedPrincipal, "password", "ROLE_USER")); + } +} diff --git a/test/src/main/java/org/springframework/security/test/web/servlet/setup/SecurityMockMvcConfigurer.java b/test/src/main/java/org/springframework/security/test/web/servlet/setup/SecurityMockMvcConfigurer.java index 0345be746c..6f5ec2908a 100644 --- a/test/src/main/java/org/springframework/security/test/web/servlet/setup/SecurityMockMvcConfigurer.java +++ b/test/src/main/java/org/springframework/security/test/web/servlet/setup/SecurityMockMvcConfigurer.java @@ -19,7 +19,6 @@ import org.springframework.security.config.BeanIds; import org.springframework.test.web.servlet.request.RequestPostProcessor; import org.springframework.test.web.servlet.setup.ConfigurableMockMvcBuilder; import org.springframework.test.web.servlet.setup.MockMvcConfigurerAdapter; -import org.springframework.util.Assert; import org.springframework.web.context.WebApplicationContext; import javax.servlet.Filter; diff --git a/web/src/main/java/org/springframework/security/web/bind/annotation/AuthenticationPrincipal.java b/web/src/main/java/org/springframework/security/web/bind/annotation/AuthenticationPrincipal.java index d9fc7a494c..0c0ab9535f 100644 --- a/web/src/main/java/org/springframework/security/web/bind/annotation/AuthenticationPrincipal.java +++ b/web/src/main/java/org/springframework/security/web/bind/annotation/AuthenticationPrincipal.java @@ -29,6 +29,8 @@ import org.springframework.security.core.Authentication; * argument should be resolved to the current user rather than a user that might * be edited on a form. * + * @deprecated Use org.springframework.security.core.annotation.AuthenticationPrincipal instead + * * @author Rob Winch * @since 3.2 */ diff --git a/web/src/main/java/org/springframework/security/web/bind/support/AuthenticationPrincipalArgumentResolver.java b/web/src/main/java/org/springframework/security/web/bind/support/AuthenticationPrincipalArgumentResolver.java index f64addd54c..c215843fa4 100644 --- a/web/src/main/java/org/springframework/security/web/bind/support/AuthenticationPrincipalArgumentResolver.java +++ b/web/src/main/java/org/springframework/security/web/bind/support/AuthenticationPrincipalArgumentResolver.java @@ -79,9 +79,12 @@ import org.springframework.web.method.support.ModelAndViewContainer; * } * * + * @deprecated use org.springframework.security.web.method.annotation.AuthenticationPrincipalArgumentResolver + * * @author Rob Winch * @since 3.2 */ +@Deprecated public final class AuthenticationPrincipalArgumentResolver implements HandlerMethodArgumentResolver { 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 new file mode 100644 index 0000000000..31e020ec4d --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/method/annotation/AuthenticationPrincipalArgumentResolver.java @@ -0,0 +1,137 @@ +/* + * Copyright 2002-2013 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 + * + * http://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.web.method.annotation; + +import java.lang.annotation.Annotation; + +import org.springframework.core.MethodParameter; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; + + +/** + * Allows resolving the {@link Authentication#getPrincipal()} using the + * {@link AuthenticationPrincipal} annotation. For example, the following + * {@link Controller}: + * + *
+ * @Controller
+ * public class MyController {
+ *     @MessageMapping("/im")
+ *     public void im(@AuthenticationPrincipal CustomUser customUser) {
+ *         // do something with CustomUser
+ *     }
+ * 
+ * + *

+ * Will resolve the CustomUser argument using + * {@link Authentication#getPrincipal()} from the {@link SecurityContextHolder}. + * If the {@link Authentication} or {@link Authentication#getPrincipal()} is + * null, it will return null. If the types do not match, null will be returned + * unless {@link AuthenticationPrincipal#errorOnInvalidType()} is true in which + * case a {@link ClassCastException} will be thrown. + *

+ * + *

+ * Alternatively, users can create a custom meta annotation as shown below: + *

+ * + *
+ * @Target({ ElementType.PARAMETER })
+ * @Retention(RetentionPolicy.RUNTIME)
+ * @AuthenticationPrincipal
+ * public @interface CurrentUser {
+ * }
+ * 
+ * + *

+ * The custom annotation can then be used instead. For example: + *

+ * + *
+ * @Controller
+ * public class MyController {
+ *     @MessageMapping("/im")
+ *     public void im(@CurrentUser CustomUser customUser) {
+ *         // do something with CustomUser
+ *     }
+ * 
+ * + * @author Rob Winch + * @since 4.0 + */ +public final class AuthenticationPrincipalArgumentResolver implements + HandlerMethodArgumentResolver { + + + /* (non-Javadoc) + * @see org.springframework.web.method.support.HandlerMethodArgumentResolver#supportsParameter(org.springframework.core.MethodParameter) + */ + public boolean supportsParameter(MethodParameter parameter) { + return findMethodAnnotation(AuthenticationPrincipal.class, parameter) != null; + } + + /* (non-Javadoc) + * @see org.springframework.web.method.support.HandlerMethodArgumentResolver#resolveArgument(org.springframework.core.MethodParameter, org.springframework.web.method.support.ModelAndViewContainer, org.springframework.web.context.request.NativeWebRequest, org.springframework.web.bind.support.WebDataBinderFactory) + */ + public Object resolveArgument(MethodParameter parameter, + ModelAndViewContainer mavContainer, NativeWebRequest webRequest, + WebDataBinderFactory binderFactory) throws Exception { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if(authentication == null) { + return null; + } + Object principal = authentication.getPrincipal(); + if(principal != null && !parameter.getParameterType().isAssignableFrom(principal.getClass())) { + AuthenticationPrincipal authPrincipal = findMethodAnnotation(AuthenticationPrincipal.class, parameter); + if(authPrincipal.errorOnInvalidType()) { + throw new ClassCastException(principal + " is not assignable to " + parameter.getParameterType()); + } else { + return null; + } + } + return principal; + } + + /** + * Obtains the specified {@link Annotation} on the specified {@link MethodParameter}. + * + * @param annotationClass the class of the {@link Annotation} to find on the {@link MethodParameter} + * @param parameter the {@link MethodParameter} to search for an {@link Annotation} + * @return the {@link Annotation} that was found or null. + */ + private T findMethodAnnotation(Class annotationClass, MethodParameter parameter) { + T annotation = parameter.getParameterAnnotation(annotationClass); + if(annotation != null) { + return annotation; + } + Annotation[] annotationsToSearch = parameter.getParameterAnnotations(); + for(Annotation toSearch : annotationsToSearch) { + annotation = AnnotationUtils.findAnnotation(toSearch.annotationType(), annotationClass); + if(annotation != null) { + return annotation; + } + } + return null; + } +} \ No newline at end of file diff --git a/web/src/test/java/org/springframework/security/web/bind/support/AuthenticationPrincipalArgumentResolverTests.java b/web/src/test/java/org/springframework/security/web/bind/support/AuthenticationPrincipalArgumentResolverTests.java index ab280c7771..46a0dcfaa7 100644 --- a/web/src/test/java/org/springframework/security/web/bind/support/AuthenticationPrincipalArgumentResolverTests.java +++ b/web/src/test/java/org/springframework/security/web/bind/support/AuthenticationPrincipalArgumentResolverTests.java @@ -40,6 +40,7 @@ import org.springframework.util.ReflectionUtils; * @author Rob Winch * */ +@SuppressWarnings("deprecation") public class AuthenticationPrincipalArgumentResolverTests { private Object expectedPrincipal; private AuthenticationPrincipalArgumentResolver resolver; 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 new file mode 100644 index 0000000000..cd9d262071 --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/method/annotation/AuthenticationPrincipalArgumentResolverTests.java @@ -0,0 +1,202 @@ +/* + * Copyright 2002-2013 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 + * + * http://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.web.method.annotation; + + +import static org.fest.assertions.Assertions.assertThat; + +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.After; +import org.junit.Before; +import org.junit.Test; +import org.springframework.core.MethodParameter; +import org.springframework.security.authentication.TestingAuthenticationToken; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.util.ReflectionUtils; + +/** + * @author Rob Winch + * + */ +public class AuthenticationPrincipalArgumentResolverTests { + private Object expectedPrincipal; + private AuthenticationPrincipalArgumentResolver resolver; + + @Before + public void setup() { + resolver = new AuthenticationPrincipalArgumentResolver(); + } + + @After + public void cleanup() { + SecurityContextHolder.clearContext(); + } + + @Test + public void supportsParameterNoAnnotation() throws Exception { + assertThat(resolver.supportsParameter(showUserNoAnnotation())).isFalse(); + } + + @Test + public void supportsParameterAnnotation() throws Exception { + assertThat(resolver.supportsParameter(showUserAnnotationObject())).isTrue(); + } + + @Test + public void supportsParameterCustomAnnotation() throws Exception { + assertThat(resolver.supportsParameter(showUserCustomAnnotation())).isTrue(); + } + + @Test + public void resolveArgumentNullAuthentication() throws Exception { + assertThat(resolver.resolveArgument(showUserAnnotationString(), null, null, null)).isNull(); + } + + @Test + public void resolveArgumentNullPrincipal() throws Exception { + setAuthenticationPrincipal(null); + assertThat(resolver.resolveArgument(showUserAnnotationString(), null, null, null)).isNull(); + } + + @Test + public void resolveArgumentString() throws Exception { + setAuthenticationPrincipal("john"); + assertThat(resolver.resolveArgument(showUserAnnotationString(), null, null, null)).isEqualTo(expectedPrincipal); + } + + @Test + public void resolveArgumentPrincipalStringOnObject() throws Exception { + setAuthenticationPrincipal("john"); + assertThat(resolver.resolveArgument(showUserAnnotationObject(), null, null, null)).isEqualTo(expectedPrincipal); + } + + @Test + public void resolveArgumentUserDetails() throws Exception { + setAuthenticationPrincipal(new User("user", "password", AuthorityUtils.createAuthorityList("ROLE_USER"))); + assertThat(resolver.resolveArgument(showUserAnnotationUserDetails(), null, null, null)).isEqualTo(expectedPrincipal); + } + + @Test + public void resolveArgumentCustomUserPrincipal() throws Exception { + setAuthenticationPrincipal(new CustomUserPrincipal()); + assertThat(resolver.resolveArgument(showUserAnnotationCustomUserPrincipal(), null, null, null)).isEqualTo(expectedPrincipal); + } + + @Test + public void resolveArgumentCustomAnnotation() throws Exception { + setAuthenticationPrincipal(new CustomUserPrincipal()); + assertThat(resolver.resolveArgument(showUserCustomAnnotation(), null, null, null)).isEqualTo(expectedPrincipal); + } + + @Test + public void resolveArgumentNullOnInvalidType() throws Exception { + setAuthenticationPrincipal(new CustomUserPrincipal()); + assertThat(resolver.resolveArgument(showUserAnnotationString(), null, null, null)).isNull(); + } + + @Test(expected = ClassCastException.class) + public void resolveArgumentErrorOnInvalidType() throws Exception { + setAuthenticationPrincipal(new CustomUserPrincipal()); + resolver.resolveArgument(showUserAnnotationErrorOnInvalidType(), null, null, null); + } + + + @Test(expected = ClassCastException.class) + public void resolveArgumentCustomserErrorOnInvalidType() throws Exception { + setAuthenticationPrincipal(new CustomUserPrincipal()); + resolver.resolveArgument(showUserAnnotationCurrentUserErrorOnInvalidType(), null, null, null); + } + + @Test + public void resolveArgumentObject() throws Exception { + setAuthenticationPrincipal(new Object()); + assertThat(resolver.resolveArgument(showUserAnnotationObject(), null, null, null)).isEqualTo(expectedPrincipal); + } + + private MethodParameter showUserNoAnnotation() { + return getMethodParameter("showUserNoAnnotation", String.class); + } + + private MethodParameter showUserAnnotationString() { + return getMethodParameter("showUserAnnotation", String.class); + } + + private MethodParameter showUserAnnotationErrorOnInvalidType() { + return getMethodParameter("showUserAnnotationErrorOnInvalidType", String.class); + } + + private MethodParameter showUserAnnotationCurrentUserErrorOnInvalidType() { + return getMethodParameter("showUserAnnotationCurrentUserErrorOnInvalidType", String.class); + } + + private MethodParameter showUserAnnotationUserDetails() { + return getMethodParameter("showUserAnnotation", UserDetails.class); + } + + private MethodParameter showUserAnnotationCustomUserPrincipal() { + return getMethodParameter("showUserAnnotation", CustomUserPrincipal.class); + } + + private MethodParameter showUserCustomAnnotation() { + return getMethodParameter("showUserCustomAnnotation", CustomUserPrincipal.class); + } + + private MethodParameter showUserAnnotationObject() { + return getMethodParameter("showUserAnnotation", Object.class); + } + + private MethodParameter getMethodParameter(String methodName, Class... paramTypes) { + Method method = ReflectionUtils.findMethod(TestController.class, methodName,paramTypes); + return new MethodParameter(method,0); + } + + @Target({ ElementType.PARAMETER}) + @Retention(RetentionPolicy.RUNTIME) + @AuthenticationPrincipal + static @interface CurrentUser { } + + @Target({ ElementType.PARAMETER}) + @Retention(RetentionPolicy.RUNTIME) + @AuthenticationPrincipal(errorOnInvalidType = true) + static @interface CurrentUserErrorOnInvalidType { } + + public static class TestController { + public void showUserNoAnnotation(String user) {} + public void showUserAnnotation(@AuthenticationPrincipal String user) {} + public void showUserAnnotationErrorOnInvalidType(@AuthenticationPrincipal(errorOnInvalidType=true) String user) {} + public void showUserAnnotationCurrentUserErrorOnInvalidType(@CurrentUserErrorOnInvalidType String user) {} + public void showUserAnnotation(@AuthenticationPrincipal UserDetails user) {} + public void showUserAnnotation(@AuthenticationPrincipal CustomUserPrincipal user) {} + public void showUserCustomAnnotation(@CurrentUser CustomUserPrincipal user) {} + public void showUserAnnotation(@AuthenticationPrincipal Object user) {} + } + + private static class CustomUserPrincipal {} + + private void setAuthenticationPrincipal(Object principal) { + this.expectedPrincipal = principal; + SecurityContextHolder.getContext().setAuthentication(new TestingAuthenticationToken(expectedPrincipal, "password", "ROLE_USER")); + } +}