diff --git a/spring-boot-project/spring-boot-autoconfigure/pom.xml b/spring-boot-project/spring-boot-autoconfigure/pom.xml index d9cda412802..70f9e1349ac 100755 --- a/spring-boot-project/spring-boot-autoconfigure/pom.xml +++ b/spring-boot-project/spring-boot-autoconfigure/pom.xml @@ -682,6 +682,11 @@ spring-security-oauth2-resource-server true + + org.springframework.security + spring-security-rsocket + true + org.springframework.security spring-security-saml2-service-provider diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/reactive/ReactiveUserDetailsServiceAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/reactive/ReactiveUserDetailsServiceAutoConfiguration.java index 1ea12528950..2a555467f03 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/reactive/ReactiveUserDetailsServiceAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/reactive/ReactiveUserDetailsServiceAutoConfiguration.java @@ -23,12 +23,19 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.condition.AnyNestedCondition; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.boot.autoconfigure.rsocket.RSocketMessagingAutoConfiguration; import org.springframework.boot.autoconfigure.security.SecurityProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; +import org.springframework.messaging.rsocket.annotation.support.RSocketMessageHandler; import org.springframework.security.authentication.ReactiveAuthenticationManager; import org.springframework.security.core.userdetails.MapReactiveUserDetailsService; import org.springframework.security.core.userdetails.ReactiveUserDetailsService; @@ -51,7 +58,9 @@ import org.springframework.util.StringUtils; @ConditionalOnMissingBean(value = { ReactiveAuthenticationManager.class, ReactiveUserDetailsService.class }, type = { "org.springframework.security.oauth2.jwt.ReactiveJwtDecoder", "org.springframework.security.oauth2.server.resource.introspection.ReactiveOpaqueTokenIntrospector" }) -@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE) +@Conditional(ReactiveUserDetailsServiceAutoConfiguration.ReactiveUserDetailsServiceCondition.class) +@EnableConfigurationProperties(SecurityProperties.class) +@AutoConfigureAfter(RSocketMessagingAutoConfiguration.class) public class ReactiveUserDetailsServiceAutoConfiguration { private static final String NOOP_PASSWORD_PREFIX = "{noop}"; @@ -84,4 +93,22 @@ public class ReactiveUserDetailsServiceAutoConfiguration { return NOOP_PASSWORD_PREFIX + password; } + static class ReactiveUserDetailsServiceCondition extends AnyNestedCondition { + + ReactiveUserDetailsServiceCondition() { + super(ConfigurationPhase.REGISTER_BEAN); + } + + @ConditionalOnBean(RSocketMessageHandler.class) + static class RSocketSecurityEnabledCondition { + + } + + @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE) + static class ReactiveWebApplicationCondition { + + } + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/rsocket/RSocketSecurityAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/rsocket/RSocketSecurityAutoConfiguration.java new file mode 100644 index 00000000000..f95790be6bf --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/rsocket/RSocketSecurityAutoConfiguration.java @@ -0,0 +1,44 @@ +/* + * Copyright 2012-2019 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 + * + * https://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.boot.autoconfigure.security.rsocket; + +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.rsocket.server.ServerRSocketFactoryProcessor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.rsocket.EnableRSocketSecurity; +import org.springframework.security.rsocket.core.SecuritySocketAcceptorInterceptor; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for Spring Security for an RSocket + * server. + * + * @author Madhura Bhave + * @since 2.2.0 + */ +@Configuration(proxyBeanMethods = false) +@EnableRSocketSecurity +@ConditionalOnClass(SecuritySocketAcceptorInterceptor.class) +public class RSocketSecurityAutoConfiguration { + + @Bean + ServerRSocketFactoryProcessor springSecurityRSocketSecurity(SecuritySocketAcceptorInterceptor interceptor) { + return (factory) -> factory.addSocketAcceptorPlugin(interceptor); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories b/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories index 55a14896416..c7392ff2b06 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories @@ -109,6 +109,7 @@ org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoCo org.springframework.boot.autoconfigure.security.servlet.SecurityFilterAutoConfiguration,\ org.springframework.boot.autoconfigure.security.reactive.ReactiveSecurityAutoConfiguration,\ org.springframework.boot.autoconfigure.security.reactive.ReactiveUserDetailsServiceAutoConfiguration,\ +org.springframework.boot.autoconfigure.security.rsocket.RSocketSecurityAutoConfiguration,\ org.springframework.boot.autoconfigure.security.saml2.Saml2RelyingPartyAutoConfiguration,\ org.springframework.boot.autoconfigure.sendgrid.SendGridAutoConfiguration,\ org.springframework.boot.autoconfigure.session.SessionAutoConfiguration,\ diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/reactive/ReactiveUserDetailsServiceAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/reactive/ReactiveUserDetailsServiceAutoConfigurationTests.java index c9d4077771e..50eea54462d 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/reactive/ReactiveUserDetailsServiceAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/reactive/ReactiveUserDetailsServiceAutoConfigurationTests.java @@ -21,13 +21,17 @@ import java.time.Duration; import org.junit.jupiter.api.Test; import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.rsocket.RSocketMessagingAutoConfiguration; +import org.springframework.boot.autoconfigure.rsocket.RSocketStrategiesAutoConfiguration; import org.springframework.boot.autoconfigure.security.SecurityProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.security.authentication.ReactiveAuthenticationManager; +import org.springframework.security.config.annotation.rsocket.EnableRSocketSecurity; import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; import org.springframework.security.core.userdetails.MapReactiveUserDetailsService; import org.springframework.security.core.userdetails.ReactiveUserDetailsService; @@ -59,6 +63,17 @@ class ReactiveUserDetailsServiceAutoConfigurationTests { }); } + @Test + void userDetailsServiceWhenRSocketConfigured() { + new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(ReactiveUserDetailsServiceAutoConfiguration.class, + RSocketMessagingAutoConfiguration.class, RSocketStrategiesAutoConfiguration.class)) + .withUserConfiguration(TestRSocketSecurityConfiguration.class).run((context) -> { + ReactiveUserDetailsService userDetailsService = context.getBean(ReactiveUserDetailsService.class); + assertThat(userDetailsService.findByUsername("user").block(Duration.ofSeconds(30))).isNotNull(); + }); + } + @Test void doesNotConfigureDefaultUserIfUserDetailsServiceAvailable() { this.contextRunner.withUserConfiguration(UserConfig.class, TestSecurityConfiguration.class).run((context) -> { @@ -135,6 +150,13 @@ class ReactiveUserDetailsServiceAutoConfigurationTests { } + @Configuration(proxyBeanMethods = false) + @EnableRSocketSecurity + @EnableConfigurationProperties(SecurityProperties.class) + static class TestRSocketSecurityConfiguration { + + } + @Configuration(proxyBeanMethods = false) static class UserConfig { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/rsocket/RSocketSecurityAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/rsocket/RSocketSecurityAutoConfigurationTests.java new file mode 100644 index 00000000000..cd52963df25 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/rsocket/RSocketSecurityAutoConfigurationTests.java @@ -0,0 +1,74 @@ +/* + * Copyright 2012-2019 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 + * + * https://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.boot.autoconfigure.security.rsocket; + +import java.util.List; + +import io.rsocket.RSocketFactory; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.rsocket.RSocketMessagingAutoConfiguration; +import org.springframework.boot.autoconfigure.rsocket.RSocketStrategiesAutoConfiguration; +import org.springframework.boot.autoconfigure.security.reactive.ReactiveUserDetailsServiceAutoConfiguration; +import org.springframework.boot.rsocket.server.ServerRSocketFactoryProcessor; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.security.config.annotation.rsocket.RSocketSecurity; +import org.springframework.security.rsocket.core.SecuritySocketAcceptorInterceptor; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link RSocketSecurityAutoConfiguration}. + * + * @author Madhura Bhave + */ +class RSocketSecurityAutoConfigurationTests { + + private ApplicationContextRunner contextRunner = new ApplicationContextRunner().withConfiguration(AutoConfigurations + .of(ReactiveUserDetailsServiceAutoConfiguration.class, RSocketSecurityAutoConfiguration.class, + RSocketMessagingAutoConfiguration.class, RSocketStrategiesAutoConfiguration.class)); + + @Test + void autoConfigurationEnablesRSocketSecurity() { + this.contextRunner.run((context) -> assertThat(context.getBean(RSocketSecurity.class)).isNotNull()); + } + + @Test + void autoConfigurationIsConditionalOnSecuritySocketAcceptorInterceptorClass() { + this.contextRunner.withClassLoader(new FilteredClassLoader(SecuritySocketAcceptorInterceptor.class)) + .run((context) -> assertThat(context).doesNotHaveBean(RSocketSecurity.class)); + } + + @Test + void autoConfigurationAddsCustomizerForServerRSocketFactory() { + RSocketFactory.ServerRSocketFactory factory = Mockito.mock(RSocketFactory.ServerRSocketFactory.class); + ArgumentCaptor captor = ArgumentCaptor + .forClass(SecuritySocketAcceptorInterceptor.class); + this.contextRunner.run((context) -> { + ServerRSocketFactoryProcessor customizer = context.getBean(ServerRSocketFactoryProcessor.class); + customizer.process(factory); + Mockito.verify(factory).addSocketAcceptorPlugin(captor.capture()); + List values = captor.getAllValues(); + assertThat(values.get(0)).isInstanceOf(SecuritySocketAcceptorInterceptor.class); + }); + } + +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-rsocket/pom.xml b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-rsocket/pom.xml index c231b86eb8d..99bfae491f9 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-rsocket/pom.xml +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-rsocket/pom.xml @@ -36,6 +36,14 @@ org.springframework.boot spring-boot-starter-rsocket + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.security + spring-security-rsocket + org.springframework.boot diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-rsocket/src/main/resources/application.properties b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-rsocket/src/main/resources/application.properties index 5e4aaaf7f7e..ce9e4d6a90c 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-rsocket/src/main/resources/application.properties +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-rsocket/src/main/resources/application.properties @@ -1 +1,2 @@ spring.rsocket.server.port=0 +spring.security.user.password=password diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-rsocket/src/test/java/smoketest/rsocket/SampleRSocketApplicationTests.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-rsocket/src/test/java/smoketest/rsocket/SampleRSocketApplicationTests.java index 55ab627902b..baefcc90904 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-rsocket/src/test/java/smoketest/rsocket/SampleRSocketApplicationTests.java +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-rsocket/src/test/java/smoketest/rsocket/SampleRSocketApplicationTests.java @@ -27,6 +27,8 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.rsocket.context.LocalRSocketServerPort; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.messaging.rsocket.RSocketRequester; +import org.springframework.security.rsocket.metadata.BasicAuthenticationEncoder; +import org.springframework.security.rsocket.metadata.UsernamePasswordMetadata; @SpringBootTest(properties = "spring.rsocket.server.port=0") public class SampleRSocketApplicationTests { @@ -38,9 +40,20 @@ public class SampleRSocketApplicationTests { private RSocketRequester.Builder builder; @Test - void testRSocketEndpoint() { + void unauthenticatedAccessToRSocketEndpoint() { RSocketRequester requester = this.builder.connectTcp("localhost", this.port).block(Duration.ofSeconds(5)); Mono result = requester.route("find.project.spring-boot").retrieveMono(Project.class); + StepVerifier.create(result).expectErrorMessage("Access Denied").verify(); + } + + @Test + void rSocketEndpoint() { + RSocketRequester requester = this.builder + .rsocketStrategies((builder) -> builder.encoder(new BasicAuthenticationEncoder())) + .setupMetadata(new UsernamePasswordMetadata("user", "password"), + UsernamePasswordMetadata.BASIC_AUTHENTICATION_MIME_TYPE) + .connectTcp("localhost", this.port).block(Duration.ofSeconds(5)); + Mono result = requester.route("find.project.spring-boot").retrieveMono(Project.class); StepVerifier.create(result) .assertNext((project) -> Assertions.assertThat(project.getName()).isEqualTo("spring-boot")) .verifyComplete();