Browse Source

Add Default RSocketSecurity

Fixes gh-7361
Rob Winch 6 years ago
parent
commit
96d44cd4b7

+ 1 - 1
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 { }

+ 58 - 0
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<PayloadSocketAcceptorInterceptor> rsocketInterceptor, ObjectProvider<RSocketSecurity> rsocketSecurity) {
+		PayloadSocketAcceptorInterceptor delegate = rsocketInterceptor
+				.getIfAvailable(() -> defaultInterceptor(rsocketSecurity));
+		return new SecuritySocketAcceptorInterceptor(delegate);
+	}
+
+	private PayloadSocketAcceptorInterceptor defaultInterceptor(
+			ObjectProvider<RSocketSecurity> 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();
+	}
+}

+ 174 - 0
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<String> payloads = new ArrayList<>();
+
+		@MessageMapping("**")
+		String retrieveMono(String payload) {
+			add(payload);
+			return "Hi " + payload;
+		}
+
+		private void add(String p) {
+			this.payloads.add(p);
+		}
+	}
+
+}

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

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

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

+ 42 - 0
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);
+	}
+}