From 6e9fb7930b5ff1e6d512719bc06754808713ab89 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Fri, 30 Aug 2013 17:06:40 -0500 Subject: [PATCH] SEC-2298: Add AuthenticationPrincipalArgumentResolver --- .../SpringWebMvcImportSelector.java | 4 +- ....java => WebMvcSecurityConfiguration.java} | 17 +- .../annotation/AuthenticationPrincipal.java | 40 +++++ ...thenticationPrincipalArgumentResolver.java | 114 ++++++++++++ ...icationPrincipalArgumentResolverTests.java | 165 ++++++++++++++++++ web/template.mf | 1 + 6 files changed, 337 insertions(+), 4 deletions(-) rename config/src/main/java/org/springframework/security/config/annotation/web/configuration/{CsrfWebMvcConfiguration.java => WebMvcSecurityConfiguration.java} (67%) create mode 100644 web/src/main/java/org/springframework/security/web/bind/annotation/AuthenticationPrincipal.java create mode 100644 web/src/main/java/org/springframework/security/web/bind/support/AuthenticationPrincipalArgumentResolver.java create mode 100644 web/src/test/java/org/springframework/security/web/bind/support/AuthenticationPrincipalArgumentResolverTests.java diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/SpringWebMvcImportSelector.java b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/SpringWebMvcImportSelector.java index 5cc4073bc0..5d430b7bf6 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/SpringWebMvcImportSelector.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/SpringWebMvcImportSelector.java @@ -21,7 +21,7 @@ import org.springframework.util.ClassUtils; /** * Used by {@link EnableWebSecurity} to conditionaly import - * {@link CsrfWebMvcConfiguration} when the DispatcherServlet is present on the + * {@link WebMvcSecurityConfiguration} when the DispatcherServlet is present on the * classpath. * * @author Rob Winch @@ -34,6 +34,6 @@ class SpringWebMvcImportSelector implements ImportSelector { */ public String[] selectImports(AnnotationMetadata importingClassMetadata) { boolean webmvcPresent = ClassUtils.isPresent("org.springframework.web.servlet.DispatcherServlet", getClass().getClassLoader()); - return webmvcPresent ? new String[] {"org.springframework.security.config.annotation.web.configuration.CsrfWebMvcConfiguration"} : new String[] {}; + return webmvcPresent ? new String[] {"org.springframework.security.config.annotation.web.configuration.WebMvcSecurityConfiguration"} : new String[] {}; } } diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/CsrfWebMvcConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebMvcSecurityConfiguration.java similarity index 67% rename from config/src/main/java/org/springframework/security/config/annotation/web/configuration/CsrfWebMvcConfiguration.java rename to config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebMvcSecurityConfiguration.java index 7b508bdca0..c845bdc953 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/CsrfWebMvcConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebMvcSecurityConfiguration.java @@ -15,23 +15,36 @@ */ package org.springframework.security.config.annotation.web.configuration; +import java.util.List; + import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.security.web.bind.support.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; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; import org.springframework.web.servlet.support.RequestDataValueProcessor; /** * Used to add a {@link RequestDataValueProcessor} for Spring MVC and Spring * Security CSRF integration. This configuration is added whenever * {@link EnableWebMvc} is added by {@link SpringWebMvcImportSelector} and the - * DispatcherServlet is present on the classpath. + * DispatcherServlet is present on the classpath. It also adds the + * {@link AuthenticationPrincipalArgumentResolver} as a + * {@link HandlerMethodArgumentResolver}. * * @author Rob Winch * @since 3.2 */ @Configuration -class CsrfWebMvcConfiguration { +class WebMvcSecurityConfiguration extends WebMvcConfigurerAdapter { + + @Override + public void addArgumentResolvers( + List argumentResolvers) { + argumentResolvers.add(new AuthenticationPrincipalArgumentResolver()); + } @Bean public RequestDataValueProcessor requestDataValueProcessor() { 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 new file mode 100644 index 0000000000..b02ea1340d --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/bind/annotation/AuthenticationPrincipal.java @@ -0,0 +1,40 @@ +/* + * 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.bind.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 binds a method parameter or method return value to the + * {@link Authentication#getPrincipal()}. This is necessary to signal that the + * argument should be resolved to the current user rather than a user that might + * be edited on a form. + * + * @author Rob Winch + * @since 3.2 + */ +@Target({ ElementType.PARAMETER, ElementType.ANNOTATION_TYPE }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface AuthenticationPrincipal { + +} 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 new file mode 100644 index 0000000000..d55b2f1bf5 --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/bind/support/AuthenticationPrincipalArgumentResolver.java @@ -0,0 +1,114 @@ +/* + * 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.bind.support; + +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.context.SecurityContextHolder; +import org.springframework.security.web.bind.annotation.AuthenticationPrincipal; +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 {
+ *     @RequestMapping("/user/current/show")
+ *     public String show(@AuthenticationPrincipal CustomUser customUser) {
+ *         // do something with CustomUser
+ *         return "view";
+ *     }
+ * 
+ * + *

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, 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 {
+ *     @RequestMapping("/user/current/show")
+ *     public String show(@CurrentUser CustomUser customUser) {
+ *         // do something with CustomUser
+ *         return "view";
+ *     }
+ * 
+ * + * @author Rob Winch + * @since 3.2 + */ +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) { + if(parameter.getParameterAnnotation(AuthenticationPrincipal.class) != null) { + return true; + } + Annotation[] annotationsToSearch = parameter.getParameterAnnotations(); + for(Annotation toSearch : annotationsToSearch) { + if(AnnotationUtils.findAnnotation(toSearch.annotationType(), AuthenticationPrincipal.class) != null) { + return true; + } + } + return false; + } + + /* (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 { + if(!supportsParameter(parameter)) { + return null; + } + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if(authentication == null) { + return null; + } + Object principal = authentication.getPrincipal(); + if(principal != null && !parameter.getParameterType().isAssignableFrom(principal.getClass())) { + throw new ClassCastException(principal + " is not assiable to " + parameter.getParameterType()); + } + return principal; + } +} \ 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 new file mode 100644 index 0000000000..e6ef7765f8 --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/bind/support/AuthenticationPrincipalArgumentResolverTests.java @@ -0,0 +1,165 @@ +/* + * 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.bind.support; + + +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.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.security.web.bind.annotation.AuthenticationPrincipal; +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 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 resolveArgumentNoAnnotation() throws Exception { + setAuthenticationPrincipal("john"); + assertThat(resolver.resolveArgument(showUserNoAnnotation(), 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(expected = ClassCastException.class) + public void resolveArgumentClassCastException() throws Exception { + setAuthenticationPrincipal(new CustomUserPrincipal()); + resolver.resolveArgument(showUserAnnotationString(), 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 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 { } + + public static class TestController { + public void showUserNoAnnotation(String user) {} + public void showUserAnnotation(@AuthenticationPrincipal 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/web/template.mf b/web/template.mf index ff69859a6a..a463f39749 100644 --- a/web/template.mf +++ b/web/template.mf @@ -35,6 +35,7 @@ Import-Template: org.springframework.jdbc.*;version="${springRange}";resolution:=optional, org.springframework.mock.web;version="${springRange}";resolution:=optional, org.springframework.web.*;version="${springRange}";resolution:=optional, + org.springframework.core.annotation.*;version="${springRange}";resolution:=optional,, org.springframework.web.context.*;version="${springRange}";resolution:=optional, org.springframework.web.filter.*;version="${springRange}", org.springframework.web.*;version="${springRange}",