From aaa9708b952409269f2829e60126344a1288019e Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Tue, 18 Oct 2016 19:31:30 -0500 Subject: [PATCH] Add BeanResolver to AuthenticationPrincipalArgumentResolver Previously @AuthenticationPrincipal's expression attribute didn't support bean references because the BeanResolver was not set on the SpEL context. This commit adds a BeanResolver and ensures that the configuration sets a BeanResolver. Fixes gh-3949 --- .../WebMvcSecurityConfiguration.java | 21 +++- ...icationPrincipalArgumentResolverTests.java | 105 ++++++++++++++++++ docs/manual/src/docs/asciidoc/index.adoc | 20 ++++ ...thenticationPrincipalArgumentResolver.java | 12 ++ 4 files changed, 154 insertions(+), 4 deletions(-) create mode 100644 config/src/test/java/org/springframework/security/config/annotation/web/configuration/AuthenticationPrincipalArgumentResolverTests.java diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebMvcSecurityConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebMvcSecurityConfiguration.java index 817f8204c9..e27e0e0904 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebMvcSecurityConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebMvcSecurityConfiguration.java @@ -15,7 +15,14 @@ */ package org.springframework.security.config.annotation.web.configuration; +import java.util.List; + +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; import org.springframework.context.annotation.Bean; +import org.springframework.context.expression.BeanFactoryResolver; +import org.springframework.expression.BeanResolver; import org.springframework.security.web.method.annotation.AuthenticationPrincipalArgumentResolver; import org.springframework.security.web.method.annotation.CsrfTokenArgumentResolver; import org.springframework.security.web.servlet.support.csrf.CsrfRequestDataValueProcessor; @@ -24,8 +31,6 @@ import org.springframework.web.servlet.config.annotation.EnableWebMvc; import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; import org.springframework.web.servlet.support.RequestDataValueProcessor; -import java.util.List; - /** * Used to add a {@link RequestDataValueProcessor} for Spring MVC and Spring Security CSRF * integration. This configuration is added whenever {@link EnableWebMvc} is added by @@ -36,12 +41,15 @@ import java.util.List; * @author Rob Winch * @since 3.2 */ -class WebMvcSecurityConfiguration extends WebMvcConfigurerAdapter { +class WebMvcSecurityConfiguration extends WebMvcConfigurerAdapter implements ApplicationContextAware { + private BeanResolver beanResolver; @Override @SuppressWarnings("deprecation") public void addArgumentResolvers(List argumentResolvers) { - argumentResolvers.add(new AuthenticationPrincipalArgumentResolver()); + AuthenticationPrincipalArgumentResolver authenticationPrincipalResolver = new AuthenticationPrincipalArgumentResolver(); + authenticationPrincipalResolver.setBeanResolver(beanResolver); + argumentResolvers.add(authenticationPrincipalResolver); argumentResolvers .add(new org.springframework.security.web.bind.support.AuthenticationPrincipalArgumentResolver()); argumentResolvers.add(new CsrfTokenArgumentResolver()); @@ -51,4 +59,9 @@ class WebMvcSecurityConfiguration extends WebMvcConfigurerAdapter { public RequestDataValueProcessor requestDataValueProcessor() { return new CsrfRequestDataValueProcessor(); } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + this.beanResolver = new BeanFactoryResolver(applicationContext.getAutowireCapableBeanFactory()); + } } \ No newline at end of file diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configuration/AuthenticationPrincipalArgumentResolverTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configuration/AuthenticationPrincipalArgumentResolverTests.java new file mode 100644 index 0000000000..7931f51b87 --- /dev/null +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configuration/AuthenticationPrincipalArgumentResolverTests.java @@ -0,0 +1,105 @@ +/* + * Copyright 2002-2016 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.config.annotation.web.configuration; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import org.junit.After; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.User; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; + +/** + * + * @author Rob Winch + * + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration +@WebAppConfiguration +public class AuthenticationPrincipalArgumentResolverTests { + @Autowired + WebApplicationContext wac; + + @After + public void cleanup() { + SecurityContextHolder.clearContext(); + } + + @Test + public void authenticationPrincipalExpressionWhenBeanExpressionSuppliedThenBeanUsed() throws Exception { + User user = new User("user", "password", AuthorityUtils.createAuthorityList("ROLE_USER")); + SecurityContext context = SecurityContextHolder.createEmptyContext(); + context.setAuthentication(new UsernamePasswordAuthenticationToken(user, user.getPassword(), user.getAuthorities())); + SecurityContextHolder.setContext(context); + + MockMvc mockMvc = MockMvcBuilders + .webAppContextSetup(wac) + .build(); + + mockMvc.perform(get("/users/self")) + .andExpect(status().isOk()) + .andExpect(content().string("extracted-user")); + } + + @EnableWebSecurity + @EnableWebMvc + static class Config { + @Autowired + public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { + auth + .inMemoryAuthentication(); + } + + @Bean + public UsernameExtractor usernameExtractor() { + return new UsernameExtractor(); + } + + @RestController + static class UserController { + @GetMapping("/users/self") + public String usersSelf(@AuthenticationPrincipal(expression = "@usernameExtractor.extract(#this)") String userName) { + return userName; + } + } + } + + static class UsernameExtractor { + public String extract(User u) { + return "extracted-" + u.getUsername(); + } + } +} diff --git a/docs/manual/src/docs/asciidoc/index.adoc b/docs/manual/src/docs/asciidoc/index.adoc index 941a8c73ed..e385a55481 100644 --- a/docs/manual/src/docs/asciidoc/index.adoc +++ b/docs/manual/src/docs/asciidoc/index.adoc @@ -6864,6 +6864,26 @@ public ModelAndView findMessagesForUser(@AuthenticationPrincipal(expression = "c } ---- +We can also refer to Beans in our SpEL expressions. +For example, the following could be used if we were using JPA to manage our Users and we wanted to modify and save a propoerty on the current user. + +[source,java] +---- +import org.springframework.security.core.annotation.AuthenticationPrincipal; + +// ... + +@PutMapping("/users/self") +public ModelAndView updateName(@AuthenticationPrincipal(expression = "@jpaEntityManager.merge(#this)") CustomUser attachedCustomUser + @RequestParam String firstName) { + + // change the firstName on an atached instance which will be persisted to the database + attachedCustomUser.setFirstName(firstName); + + // ... +} +---- + We can further remove our dependency on Spring Security by making `@AuthenticationPrincipal` a meta annotation on our own annotation. Below we demonstrate how we could do this on an annotation named `@CurrentUser`. NOTE: It is important to realize that in order to remove the dependency on Spring Security, it is the consuming application that would create `@CurrentUser`. This step is not strictly required, but assists in isolating your dependency to Spring Security to a more central location. 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 b18513d7e7..da23e0edf9 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,7 @@ import java.lang.annotation.Annotation; import org.springframework.core.MethodParameter; import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.expression.BeanResolver; import org.springframework.expression.Expression; import org.springframework.expression.ExpressionParser; import org.springframework.expression.spel.standard.SpelExpressionParser; @@ -88,6 +89,8 @@ public final class AuthenticationPrincipalArgumentResolver private ExpressionParser parser = new SpelExpressionParser(); + private BeanResolver beanResolver; + /* * (non-Javadoc) * @@ -125,6 +128,7 @@ public final class AuthenticationPrincipalArgumentResolver StandardEvaluationContext context = new StandardEvaluationContext(); context.setRootObject(principal); context.setVariable("this", principal); + context.setBeanResolver(beanResolver); Expression expression = this.parser.parseExpression(expressionToParse); principal = expression.getValue(context); @@ -144,6 +148,14 @@ public final class AuthenticationPrincipalArgumentResolver return principal; } + /** + * Sets the {@link BeanResolver} to be used on the expressions + * @param beanResolver the {@link BeanResolver} to use + */ + public void setBeanResolver(BeanResolver beanResolver) { + this.beanResolver = beanResolver; + } + /** * Obtains the specified {@link Annotation} on the specified {@link MethodParameter}. *