diff --git a/config/src/main/java/org/springframework/security/config/annotation/rsocket/EnableRSocketSecurity.java b/config/src/main/java/org/springframework/security/config/annotation/rsocket/EnableRSocketSecurity.java index e4dce801f9..440186090f 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/rsocket/EnableRSocketSecurity.java +++ b/config/src/main/java/org/springframework/security/config/annotation/rsocket/EnableRSocketSecurity.java @@ -35,5 +35,5 @@ import java.lang.annotation.Target; @Documented @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) -@Import({ RSocketSecurityConfiguration.class }) +@Import({ RSocketSecurityConfiguration.class, SecuritySocketAcceptorInterceptorConfiguration.class }) public @interface EnableRSocketSecurity { } diff --git a/config/src/main/java/org/springframework/security/config/annotation/rsocket/SecuritySocketAcceptorInterceptorConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/rsocket/SecuritySocketAcceptorInterceptorConfiguration.java new file mode 100644 index 0000000000..a469b29939 --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/rsocket/SecuritySocketAcceptorInterceptorConfiguration.java @@ -0,0 +1,58 @@ +/* + * Copyright 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.security.config.annotation.rsocket; + +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.Customizer; +import org.springframework.security.rsocket.core.PayloadSocketAcceptorInterceptor; +import org.springframework.security.rsocket.core.SecuritySocketAcceptorInterceptor; +import org.springframework.security.rsocket.util.matcher.PayloadExchangeMatcher.MatchResult; + +/** + * @author Rob Winch + * @since 5.2 + */ +@Configuration(proxyBeanMethods = false) +class SecuritySocketAcceptorInterceptorConfiguration { + @Bean + SecuritySocketAcceptorInterceptor securitySocketAcceptorInterceptor( + ObjectProvider rsocketInterceptor, ObjectProvider rsocketSecurity) { + PayloadSocketAcceptorInterceptor delegate = rsocketInterceptor + .getIfAvailable(() -> defaultInterceptor(rsocketSecurity)); + return new SecuritySocketAcceptorInterceptor(delegate); + } + + private PayloadSocketAcceptorInterceptor defaultInterceptor( + ObjectProvider rsocketSecurity) { + RSocketSecurity rsocket = rsocketSecurity.getIfAvailable(); + if (rsocket == null) { + throw new NoSuchBeanDefinitionException("No RSocketSecurity defined"); + } + rsocket + .basicAuthentication(Customizer.withDefaults()) + .authorizePayload(authz -> + authz + .setup().authenticated() + .anyRequest().authenticated() + .matcher(e -> MatchResult.match()).permitAll() + ); + return rsocket.build(); + } +} diff --git a/config/src/test/java/org/springframework/security/config/annotation/rsocket/HelloRSocketITests.java b/config/src/test/java/org/springframework/security/config/annotation/rsocket/HelloRSocketITests.java new file mode 100644 index 0000000000..876e22d2a5 --- /dev/null +++ b/config/src/test/java/org/springframework/security/config/annotation/rsocket/HelloRSocketITests.java @@ -0,0 +1,174 @@ +/* + * Copyright 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.security.config.annotation.rsocket; + +import io.rsocket.RSocketFactory; +import io.rsocket.frame.decoder.PayloadDecoder; +import io.rsocket.transport.netty.server.CloseableChannel; +import io.rsocket.transport.netty.server.TcpServerTransport; +import org.junit.After; +import org.junit.Before; +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.context.annotation.Configuration; +import org.springframework.messaging.handler.annotation.MessageMapping; +import org.springframework.messaging.rsocket.RSocketRequester; +import org.springframework.messaging.rsocket.RSocketStrategies; +import org.springframework.messaging.rsocket.annotation.support.RSocketMessageHandler; +import org.springframework.security.core.userdetails.MapReactiveUserDetailsService; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.rsocket.core.SecuritySocketAcceptorInterceptor; +import org.springframework.security.rsocket.metadata.BasicAuthenticationEncoder; +import org.springframework.security.rsocket.metadata.UsernamePasswordMetadata; +import org.springframework.stereotype.Controller; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringRunner; + +import java.util.ArrayList; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; + +/** + * @author Rob Winch + */ +@ContextConfiguration +@RunWith(SpringRunner.class) +public class HelloRSocketITests { + @Autowired + RSocketMessageHandler handler; + + @Autowired + SecuritySocketAcceptorInterceptor interceptor; + + @Autowired + ServerController controller; + + private CloseableChannel server; + + private RSocketRequester requester; + + @Before + public void setup() { + this.server = RSocketFactory.receive() + .frameDecoder(PayloadDecoder.ZERO_COPY) + .addSocketAcceptorPlugin(this.interceptor) + .acceptor(this.handler.responder()) + .transport(TcpServerTransport.create("localhost", 0)) + .start() + .block(); + } + + @After + public void dispose() { + this.requester.rsocket().dispose(); + this.server.dispose(); + this.controller.payloads.clear(); + } + + @Test + public void retrieveMonoWhenSecureThenDenied() throws Exception { + this.requester = RSocketRequester.builder() + .rsocketStrategies(this.handler.getRSocketStrategies()) + .connectTcp("localhost", this.server.address().getPort()) + .block(); + + String data = "rob"; + assertThatCode(() -> this.requester.route("secure.retrieve-mono") + .data(data) + .retrieveMono(String.class) + .block() + ) + .isNotNull(); + // FIXME: https://github.com/rsocket/rsocket-java/issues/686 + // .isInstanceOf(RejectedSetupException.class); + assertThat(this.controller.payloads).isEmpty(); + } + + @Test + public void retrieveMonoWhenAuthorizedThenGranted() throws Exception { + UsernamePasswordMetadata credentials = new UsernamePasswordMetadata("rob", "password"); + this.requester = RSocketRequester.builder() + .setupMetadata(credentials, UsernamePasswordMetadata.BASIC_AUTHENTICATION_MIME_TYPE) + .rsocketStrategies(this.handler.getRSocketStrategies()) + .connectTcp("localhost", this.server.address().getPort()) + .block(); + String data = "rob"; + String hiRob = this.requester.route("secure.retrieve-mono") + .metadata(credentials, UsernamePasswordMetadata.BASIC_AUTHENTICATION_MIME_TYPE) + .data(data) + .retrieveMono(String.class) + .block(); + + assertThat(hiRob).isEqualTo("Hi rob"); + assertThat(this.controller.payloads).containsOnly(data); + } + + @Configuration + @EnableRSocketSecurity + static class Config { + + @Bean + public ServerController controller() { + return new ServerController(); + } + + @Bean + public RSocketMessageHandler messageHandler() { + RSocketMessageHandler handler = new RSocketMessageHandler(); + handler.setRSocketStrategies(rsocketStrategies()); + return handler; + } + + @Bean + public RSocketStrategies rsocketStrategies() { + return RSocketStrategies.builder() + .encoder(new BasicAuthenticationEncoder()) + .build(); + } + + @Bean + MapReactiveUserDetailsService uds() { + UserDetails rob = User.withDefaultPasswordEncoder() + .username("rob") + .password("password") + .roles("USER", "ADMIN") + .build(); + return new MapReactiveUserDetailsService(rob); + } + } + + @Controller + static class ServerController { + private List payloads = new ArrayList<>(); + + @MessageMapping("**") + String retrieveMono(String payload) { + add(payload); + return "Hi " + payload; + } + + private void add(String p) { + this.payloads.add(p); + } + } + +} diff --git a/config/src/test/java/org/springframework/security/config/annotation/rsocket/JwtITests.java b/config/src/test/java/org/springframework/security/config/annotation/rsocket/JwtITests.java index 4133873ff7..7968bcca8e 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/rsocket/JwtITests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/rsocket/JwtITests.java @@ -35,6 +35,7 @@ import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames; import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder; import org.springframework.security.rsocket.core.PayloadSocketAcceptorInterceptor; +import org.springframework.security.rsocket.core.SecuritySocketAcceptorInterceptor; import org.springframework.security.rsocket.metadata.BasicAuthenticationEncoder; import org.springframework.security.rsocket.metadata.BearerTokenMetadata; import org.springframework.stereotype.Controller; @@ -64,7 +65,7 @@ public class JwtITests { RSocketMessageHandler handler; @Autowired - PayloadSocketAcceptorInterceptor interceptor; + SecuritySocketAcceptorInterceptor interceptor; @Autowired ServerController controller; diff --git a/config/src/test/java/org/springframework/security/config/annotation/rsocket/RSocketMessageHandlerConnectionITests.java b/config/src/test/java/org/springframework/security/config/annotation/rsocket/RSocketMessageHandlerConnectionITests.java index 75a8e8bde2..7641ce9a6a 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/rsocket/RSocketMessageHandlerConnectionITests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/rsocket/RSocketMessageHandlerConnectionITests.java @@ -36,6 +36,7 @@ import org.springframework.security.core.userdetails.MapReactiveUserDetailsServi import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.rsocket.core.PayloadSocketAcceptorInterceptor; +import org.springframework.security.rsocket.core.SecuritySocketAcceptorInterceptor; import org.springframework.security.rsocket.metadata.BasicAuthenticationEncoder; import org.springframework.security.rsocket.metadata.UsernamePasswordMetadata; import org.springframework.stereotype.Controller; @@ -58,7 +59,7 @@ public class RSocketMessageHandlerConnectionITests { RSocketMessageHandler handler; @Autowired - PayloadSocketAcceptorInterceptor interceptor; + SecuritySocketAcceptorInterceptor interceptor; @Autowired ServerController controller; diff --git a/config/src/test/java/org/springframework/security/config/annotation/rsocket/RSocketMessageHandlerITests.java b/config/src/test/java/org/springframework/security/config/annotation/rsocket/RSocketMessageHandlerITests.java index c6d3d51164..ee555db1cd 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/rsocket/RSocketMessageHandlerITests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/rsocket/RSocketMessageHandlerITests.java @@ -37,6 +37,7 @@ import org.springframework.security.core.userdetails.MapReactiveUserDetailsServi import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.rsocket.core.PayloadSocketAcceptorInterceptor; +import org.springframework.security.rsocket.core.SecuritySocketAcceptorInterceptor; import org.springframework.security.rsocket.metadata.BasicAuthenticationEncoder; import org.springframework.security.rsocket.metadata.UsernamePasswordMetadata; import org.springframework.stereotype.Controller; @@ -47,6 +48,7 @@ import reactor.core.publisher.Mono; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.TimeUnit; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatCode; @@ -61,7 +63,7 @@ public class RSocketMessageHandlerITests { RSocketMessageHandler handler; @Autowired - PayloadSocketAcceptorInterceptor interceptor; + SecuritySocketAcceptorInterceptor interceptor; @Autowired ServerController controller; diff --git a/rsocket/src/main/java/org/springframework/security/rsocket/core/SecuritySocketAcceptorInterceptor.java b/rsocket/src/main/java/org/springframework/security/rsocket/core/SecuritySocketAcceptorInterceptor.java new file mode 100644 index 0000000000..f8f9cbaf81 --- /dev/null +++ b/rsocket/src/main/java/org/springframework/security/rsocket/core/SecuritySocketAcceptorInterceptor.java @@ -0,0 +1,42 @@ +/* + * Copyright 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.security.rsocket.core; + +import io.rsocket.SocketAcceptor; +import io.rsocket.plugins.SocketAcceptorInterceptor; +import org.springframework.util.Assert; + +/** + * A SocketAcceptorInterceptor that applies Security through a delegate {@link SocketAcceptorInterceptor}. This allows + * security to be applied lazily to an application. + * + * @author Rob Winch + * @since 5.2 + */ +public class SecuritySocketAcceptorInterceptor implements SocketAcceptorInterceptor { + private final SocketAcceptorInterceptor acceptorInterceptor; + + public SecuritySocketAcceptorInterceptor(SocketAcceptorInterceptor acceptorInterceptor) { + Assert.notNull(acceptorInterceptor, "acceptorInterceptor cannot be null"); + this.acceptorInterceptor = acceptorInterceptor; + } + + @Override + public SocketAcceptor apply(SocketAcceptor socketAcceptor) { + return this.acceptorInterceptor.apply(socketAcceptor); + } +}