Browse Source

Support BeanResolver for Reactive AuthenticationPrincipal

Fixes: gh-4326
Rob Winch 7 years ago
parent
commit
a2073b2b91

+ 12 - 2
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)

+ 61 - 9
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;
+import reactor.core.publisher.Mono;
 
 /**
  * @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));

+ 1 - 0
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`
 * <<jackson>> - added support for `BadCredentialsException`
+* <<mvc-authentication-principal>> - Supports resolving beans in WebFlux (was already supported in Spring MVC).
 
 
 [[samples]]

+ 9 - 1
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;

+ 29 - 1
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<Object> 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;