From a2073b2b91f438c7f4d95cb49ac468b2e4b1ef1e Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Fri, 9 Mar 2018 10:14:17 -0600 Subject: [PATCH] Support BeanResolver for Reactive AuthenticationPrincipal Fixes: gh-4326 --- .../ServerHttpSecurityConfiguration.java | 14 +++- .../reactive/EnableWebFluxSecurityTests.java | 70 ++++++++++++++++--- docs/manual/src/docs/asciidoc/index.adoc | 1 + ...thenticationPrincipalArgumentResolver.java | 10 ++- ...icationPrincipalArgumentResolverTests.java | 30 +++++++- 5 files changed, 112 insertions(+), 13 deletions(-) diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/reactive/ServerHttpSecurityConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/web/reactive/ServerHttpSecurityConfiguration.java index 380e4d4e66..357b43372e 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/reactive/ServerHttpSecurityConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/reactive/ServerHttpSecurityConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2018 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. @@ -16,9 +16,11 @@ package org.springframework.security.config.annotation.web.reactive; +import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Scope; +import org.springframework.context.expression.BeanFactoryResolver; import org.springframework.core.ReactiveAdapterRegistry; import org.springframework.security.authentication.ReactiveAuthenticationManager; import org.springframework.security.authentication.UserDetailsRepositoryReactiveAuthenticationManager; @@ -51,6 +53,9 @@ class ServerHttpSecurityConfiguration implements WebFluxConfigurer { @Autowired(required = false) private PasswordEncoder passwordEncoder; + @Autowired(required = false) + private BeanFactory beanFactory; + @Override public void configureArgumentResolvers(ArgumentResolverConfigurer configurer) { configurer.addCustomResolver(authenticationPrincipalArgumentResolver()); @@ -58,7 +63,12 @@ class ServerHttpSecurityConfiguration implements WebFluxConfigurer { @Bean public AuthenticationPrincipalArgumentResolver authenticationPrincipalArgumentResolver() { - return new AuthenticationPrincipalArgumentResolver(this.adapterRegistry); + AuthenticationPrincipalArgumentResolver resolver = new AuthenticationPrincipalArgumentResolver( + this.adapterRegistry); + if(this.beanFactory != null) { + resolver.setBeanResolver(new BeanFactoryResolver(this.beanFactory)); + } + return resolver; } @Bean(HTTPSECURITY_BEAN_NAME) 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 a67821ec85..71b7297aa6 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2018 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. @@ -16,8 +16,17 @@ package org.springframework.security.config.annotation.web.reactive; +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.csrf; +import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication; +import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.Credentials.basicAuthenticationCredentials; + +import java.nio.charset.StandardCharsets; +import java.security.Principal; + import org.junit.Rule; import org.junit.Test; +import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Bean; @@ -32,40 +41,43 @@ import org.springframework.security.config.test.SpringTestRule; import org.springframework.security.config.users.ReactiveAuthenticationTestConfiguration; import org.springframework.security.config.web.server.ServerHttpSecurity; import org.springframework.security.core.Authentication; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.core.context.ReactiveSecurityContextHolder; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextImpl; import org.springframework.security.core.userdetails.MapReactiveUserDetailsService; import org.springframework.security.core.userdetails.ReactiveUserDetailsService; import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.test.context.annotation.SecurityTestExecutionListeners; +import org.springframework.security.test.context.support.WithMockUser; import org.springframework.security.test.web.reactive.server.WebTestClientBuilder; import org.springframework.security.web.reactive.result.view.CsrfRequestDataValueProcessor; import org.springframework.security.web.server.SecurityWebFilterChain; import org.springframework.security.web.server.WebFilterChainProxy; import org.springframework.security.web.server.context.WebSessionServerSecurityContextRepository; import org.springframework.security.web.server.util.matcher.PathPatternParserServerWebExchangeMatcher; +import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.reactive.server.FluxExchangeResult; import org.springframework.test.web.reactive.server.WebTestClient; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.reactive.config.EnableWebFlux; import org.springframework.web.reactive.function.BodyInserters; import org.springframework.web.reactive.result.view.AbstractView; + import reactor.core.publisher.Mono; -import java.nio.charset.StandardCharsets; -import java.security.Principal; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.csrf; -import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.Credentials.basicAuthenticationCredentials; -import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication; - /** * @author Rob Winch * @since 5.0 */ +@RunWith(SpringRunner.class) +@SecurityTestExecutionListeners public class EnableWebFluxSecurityTests { @Rule public final SpringTestRule spring = new SpringTestRule(); @@ -288,6 +300,46 @@ public class EnableWebFluxSecurityTests { } } + @Test + @WithMockUser + public void authenticationPrincipalArgumentResolverWhenSpelThenWorks() { + this.spring.register(AuthenticationPrincipalConfig.class).autowire(); + + WebTestClient client = WebTestClient.bindToApplicationContext(this.spring.getContext()).build(); + + client.get() + .uri("/spel") + .exchange() + .expectStatus().isOk() + .expectBody(String.class).isEqualTo("user"); + } + + + @EnableWebFluxSecurity + @EnableWebFlux + @Import(ReactiveAuthenticationTestConfiguration.class) + static class AuthenticationPrincipalConfig { + + @Bean + public PrincipalBean principalBean() { + return new PrincipalBean(); + } + + static class PrincipalBean { + public String username(UserDetails user) { + return user.getUsername(); + } + } + + @RestController + public static class AuthenticationPrincipalResolver { + @GetMapping("/spel") + String username(@AuthenticationPrincipal(expression = "@principalBean.username(#this)") String username) { + return username; + } + } + } + private static DataBuffer toDataBuffer(String body) { DataBuffer buffer = new DefaultDataBufferFactory().allocateBuffer(); buffer.write(body.getBytes(StandardCharsets.UTF_8)); diff --git a/docs/manual/src/docs/asciidoc/index.adoc b/docs/manual/src/docs/asciidoc/index.adoc index 99e1cd7e2b..e7cdde1a44 100644 --- a/docs/manual/src/docs/asciidoc/index.adoc +++ b/docs/manual/src/docs/asciidoc/index.adoc @@ -412,6 +412,7 @@ Below are the highlights of the release. For example, `@WithMockUser(setupBefore = TestExecutionEvent.TEST_EXECUTION)` will setup a user after JUnit's `@Before` and before the test executes. ** `@WithUserDetails` now works with `ReactiveUserDetailsService` * <> - added support for `BadCredentialsException` +* <> - Supports resolving beans in WebFlux (was already supported in Spring MVC). [[samples]] diff --git a/web/src/main/java/org/springframework/security/web/reactive/result/method/annotation/AuthenticationPrincipalArgumentResolver.java b/web/src/main/java/org/springframework/security/web/reactive/result/method/annotation/AuthenticationPrincipalArgumentResolver.java index d27cf6ac3f..b2a311b869 100644 --- a/web/src/main/java/org/springframework/security/web/reactive/result/method/annotation/AuthenticationPrincipalArgumentResolver.java +++ b/web/src/main/java/org/springframework/security/web/reactive/result/method/annotation/AuthenticationPrincipalArgumentResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2018 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. @@ -49,6 +49,14 @@ public class AuthenticationPrincipalArgumentResolver extends HandlerMethodArgume super(adapterRegistry); } + /** + * 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; + } + @Override public boolean supportsParameter(MethodParameter parameter) { return findMethodAnnotation(AuthenticationPrincipal.class, parameter) != null; 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 0d3c1eafa1..865a5dbdc8 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2018 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. @@ -23,6 +23,7 @@ import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import org.springframework.core.MethodParameter; import org.springframework.core.ReactiveAdapterRegistry; +import org.springframework.expression.BeanResolver; import org.springframework.security.core.Authentication; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.web.method.ResolvableMethod; @@ -33,6 +34,8 @@ import reactor.core.publisher.Mono; import java.lang.annotation.*; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.when; @@ -48,16 +51,20 @@ public class AuthenticationPrincipalArgumentResolverTests { BindingContext bindingContext; @Mock Authentication authentication; + @Mock + BeanResolver beanResolver; ResolvableMethod authenticationPrincipal = ResolvableMethod.on(getClass()).named("authenticationPrincipal").build(); ResolvableMethod spel = ResolvableMethod.on(getClass()).named("spel").build(); ResolvableMethod meta = ResolvableMethod.on(getClass()).named("meta").build(); + ResolvableMethod bean = ResolvableMethod.on(getClass()).named("bean").build(); AuthenticationPrincipalArgumentResolver resolver; @Before public void setup() { resolver = new AuthenticationPrincipalArgumentResolver(new ReactiveAdapterRegistry()); + this.resolver.setBeanResolver(this.beanResolver); } @Test @@ -126,6 +133,19 @@ public class AuthenticationPrincipalArgumentResolverTests { assertThat(argument.block()).isEqualTo(user.getId()); } + @Test + public void resolveArgumentWhenBeanThenObtainsPrincipal() throws Exception { + MyUser user = new MyUser(3L); + MethodParameter parameter = this.bean.arg(Long.class); + when(authentication.getPrincipal()).thenReturn(user); + when(exchange.getPrincipal()).thenReturn(Mono.just(authentication)); + when(this.beanResolver.resolve(any(), eq("beanName"))).thenReturn(new Bean()); + + Mono argument = resolver.resolveArgument(parameter, bindingContext, exchange); + + assertThat(argument.block()).isEqualTo(user.getId()); + } + @Test public void resolveArgumentWhenMetaThenObtainsPrincipal() throws Exception { MethodParameter parameter = this.meta.arg(String.class); @@ -142,8 +162,16 @@ public class AuthenticationPrincipalArgumentResolverTests { void spel(@AuthenticationPrincipal(expression = "id") Long id) {} + void bean(@AuthenticationPrincipal(expression = "@beanName.methodName(#this)") Long id) {} + void meta(@CurrentUser String principal) {} + static class Bean { + public Long methodName(MyUser user) { + return user.getId(); + } + } + static class MyUser { private final Long id;