Add RSocket Support

Fixes gh-7360
This commit is contained in:
Rob Winch 2019-09-04 19:16:56 -05:00
parent 099d49aa40
commit 5a4eded696
46 changed files with 4366 additions and 4 deletions

View File

@ -15,10 +15,12 @@ dependencies {
optional project(':spring-security-oauth2-jose')
optional project(':spring-security-oauth2-resource-server')
optional project(':spring-security-openid')
optional project(':spring-security-rsocket')
optional project(':spring-security-web')
optional 'io.projectreactor:reactor-core'
optional 'org.aspectj:aspectjweaver'
optional 'org.springframework:spring-jdbc'
optional 'org.springframework:spring-messaging'
optional 'org.springframework:spring-tx'
optional 'org.springframework:spring-webmvc'
optional'org.springframework:spring-web'
@ -39,6 +41,7 @@ dependencies {
testCompile 'com.squareup.okhttp3:mockwebserver'
testCompile 'ch.qos.logback:logback-classic'
testCompile 'io.projectreactor.netty:reactor-netty'
testCompile 'io.rsocket:rsocket-transport-netty'
testCompile 'javax.annotation:jsr250-api:1.0'
testCompile 'javax.xml.bind:jaxb-api'
testCompile 'ldapsdk:ldapsdk:4.1'

View File

@ -0,0 +1,39 @@
/*
* 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.context.annotation.Import;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Add this annotation to a {@code Configuration} class to have Spring Security
* {@link RSocketSecurity} support added.
*
* @author Rob Winch
* @since 5.2
* @see RSocketSecurity
*/
@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import({ RSocketSecurityConfiguration.class })
public @interface EnableRSocketSecurity { }

View File

@ -0,0 +1,313 @@
/*
* 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.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.core.ResolvableType;
import org.springframework.messaging.rsocket.annotation.support.RSocketMessageHandler;
import org.springframework.security.authentication.ReactiveAuthenticationManager;
import org.springframework.security.authorization.AuthenticatedReactiveAuthorizationManager;
import org.springframework.security.authorization.AuthorityReactiveAuthorizationManager;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.ReactiveAuthorizationManager;
import org.springframework.security.config.Customizer;
import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder;
import org.springframework.security.oauth2.server.resource.authentication.JwtReactiveAuthenticationManager;
import org.springframework.security.rsocket.interceptor.PayloadInterceptor;
import org.springframework.security.rsocket.interceptor.PayloadSocketAcceptorInterceptor;
import org.springframework.security.rsocket.interceptor.authentication.AnonymousPayloadInterceptor;
import org.springframework.security.rsocket.interceptor.authentication.AuthenticationPayloadInterceptor;
import org.springframework.security.rsocket.interceptor.authentication.BearerPayloadExchangeConverter;
import org.springframework.security.rsocket.interceptor.authorization.AuthorizationPayloadInterceptor;
import org.springframework.security.rsocket.interceptor.authorization.PayloadExchangeMatcherReactiveAuthorizationManager;
import org.springframework.security.rsocket.util.PayloadExchangeAuthorizationContext;
import org.springframework.security.rsocket.util.PayloadExchangeMatcher;
import org.springframework.security.rsocket.util.PayloadExchangeMatcherEntry;
import org.springframework.security.rsocket.util.PayloadExchangeMatchers;
import org.springframework.security.rsocket.util.RoutePayloadExchangeMatcher;
import reactor.core.publisher.Mono;
import java.util.ArrayList;
import java.util.List;
/**
* Allows configuring RSocket based security.
*
* A minimal example can be found below:
*
* <pre class="code">
* &#064;EnableRSocketSecurity
* public class SecurityConfig {
* // @formatter:off
* &#064;Bean
* PayloadSocketAcceptorInterceptor rsocketInterceptor(RSocketSecurity rsocket) {
* rsocket
* .authorizePayload(authorize ->
* authorize
* .anyRequest().authenticated()
* );
* return rsocket.build();
* }
* // @formatter:on
*
* // @formatter:off
* &#064;Bean
* public MapReactiveUserDetailsService userDetailsService() {
* UserDetails user = User.withDefaultPasswordEncoder()
* .username("user")
* .password("password")
* .roles("USER")
* .build();
* return new MapReactiveUserDetailsService(user);
* }
* // @formatter:on
* }
* </pre>
*
* A more advanced configuration can be seen below:
*
* <pre class="code">
* &#064;EnableRSocketSecurity
* public class SecurityConfig {
* // @formatter:off
* &#064;Bean
* PayloadSocketAcceptorInterceptor rsocketInterceptor(RSocketSecurity rsocket) {
* rsocket
* .authorizePayload(authorize ->
* authorize
* // must have ROLE_SETUP to make connection
* .setup().hasRole("SETUP")
* // must have ROLE_ADMIN for routes starting with "admin."
* .route("admin.*").hasRole("ADMIN")
* // any other request must be authenticated for
* .anyRequest().authenticated()
* );
* return rsocket.build();
* }
* // @formatter:on
* }
* </pre>
* @author Rob Winch
* @since 5.2
*/
public class RSocketSecurity {
private BasicAuthenticationSpec basicAuthSpec;
private JwtSpec jwtSpec;
private AuthorizePayloadsSpec authorizePayload;
private ApplicationContext context;
private ReactiveAuthenticationManager authenticationManager;
public RSocketSecurity authenticationManager(ReactiveAuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
return this;
}
public RSocketSecurity basicAuthentication(Customizer<BasicAuthenticationSpec> basic) {
if (this.basicAuthSpec == null) {
this.basicAuthSpec = new BasicAuthenticationSpec();
}
basic.customize(this.basicAuthSpec);
return this;
}
public class BasicAuthenticationSpec {
private ReactiveAuthenticationManager authenticationManager;
public BasicAuthenticationSpec authenticationManager(ReactiveAuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
return this;
}
private ReactiveAuthenticationManager getAuthenticationManager() {
if (this.authenticationManager == null) {
return RSocketSecurity.this.authenticationManager;
}
return this.authenticationManager;
}
protected AuthenticationPayloadInterceptor build() {
ReactiveAuthenticationManager manager = getAuthenticationManager();
return new AuthenticationPayloadInterceptor(manager);
}
private BasicAuthenticationSpec() {}
}
public RSocketSecurity jwt(Customizer<JwtSpec> jwt) {
if (this.jwtSpec == null) {
this.jwtSpec = new JwtSpec();
}
jwt.customize(this.jwtSpec);
return this;
}
public class JwtSpec {
private ReactiveAuthenticationManager authenticationManager;
public JwtSpec authenticationManager(ReactiveAuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
return this;
}
private ReactiveAuthenticationManager getAuthenticationManager() {
if (this.authenticationManager != null) {
return this.authenticationManager;
}
ReactiveJwtDecoder jwtDecoder = getBeanOrNull(ReactiveJwtDecoder.class);
if (jwtDecoder != null) {
this.authenticationManager = new JwtReactiveAuthenticationManager(jwtDecoder);
return this.authenticationManager;
}
return RSocketSecurity.this.authenticationManager;
}
protected AuthenticationPayloadInterceptor build() {
ReactiveAuthenticationManager manager = getAuthenticationManager();
AuthenticationPayloadInterceptor result = new AuthenticationPayloadInterceptor(manager);
result.setAuthenticationConverter(new BearerPayloadExchangeConverter());
return result;
}
private JwtSpec() {}
}
public RSocketSecurity authorizePayload(Customizer<AuthorizePayloadsSpec> authorize) {
if (this.authorizePayload == null) {
this.authorizePayload = new AuthorizePayloadsSpec();
}
authorize.customize(this.authorizePayload);
return this;
}
public PayloadSocketAcceptorInterceptor build() {
PayloadSocketAcceptorInterceptor interceptor = new PayloadSocketAcceptorInterceptor(
payloadInterceptors());
RSocketMessageHandler handler = getBean(RSocketMessageHandler.class);
interceptor.setDefaultDataMimeType(handler.getDefaultDataMimeType());
interceptor.setDefaultMetadataMimeType(handler.getDefaultMetadataMimeType());
return interceptor;
}
private List<PayloadInterceptor> payloadInterceptors() {
List<PayloadInterceptor> payloadInterceptors = new ArrayList<>();
if (this.basicAuthSpec != null) {
payloadInterceptors.add(this.basicAuthSpec.build());
}
if (this.jwtSpec != null) {
payloadInterceptors.add(this.jwtSpec.build());
}
payloadInterceptors.add(new AnonymousPayloadInterceptor("anonymousUser"));
if (this.authorizePayload != null) {
payloadInterceptors.add(this.authorizePayload.build());
}
return payloadInterceptors;
}
public class AuthorizePayloadsSpec {
private PayloadExchangeMatcherReactiveAuthorizationManager.Builder authzBuilder =
PayloadExchangeMatcherReactiveAuthorizationManager.builder();
public Access setup() {
return matcher(PayloadExchangeMatchers.setup());
}
public Access anyRequest() {
return matcher(PayloadExchangeMatchers.anyExchange());
}
protected AuthorizationPayloadInterceptor build() {
return new AuthorizationPayloadInterceptor(this.authzBuilder.build());
}
public Access route(String pattern) {
RSocketMessageHandler handler = getBean(RSocketMessageHandler.class);
PayloadExchangeMatcher matcher = new RoutePayloadExchangeMatcher(
handler.getMetadataExtractor(),
handler.getRouteMatcher(),
pattern);
return matcher(matcher);
}
public Access matcher(PayloadExchangeMatcher matcher) {
return new Access(matcher);
}
public class Access {
private final PayloadExchangeMatcher matcher;
private Access(PayloadExchangeMatcher matcher) {
this.matcher = matcher;
}
public AuthorizePayloadsSpec authenticated() {
return access(AuthenticatedReactiveAuthorizationManager.authenticated());
}
public AuthorizePayloadsSpec hasRole(String role) {
return access(AuthorityReactiveAuthorizationManager.hasRole(role));
}
public AuthorizePayloadsSpec permitAll() {
return access((a, ctx) -> Mono
.just(new AuthorizationDecision(true)));
}
public AuthorizePayloadsSpec access(
ReactiveAuthorizationManager<PayloadExchangeAuthorizationContext> authorization) {
AuthorizePayloadsSpec.this.authzBuilder.add(new PayloadExchangeMatcherEntry<>(this.matcher, authorization));
return AuthorizePayloadsSpec.this;
}
}
}
private <T> T getBean(Class<T> beanClass) {
if (this.context == null) {
return null;
}
return this.context.getBean(beanClass);
}
private <T> T getBeanOrNull(Class<T> beanClass) {
return getBeanOrNull(ResolvableType.forClass(beanClass));
}
private <T> T getBeanOrNull(ResolvableType type) {
if (this.context == null) {
return null;
}
String[] names = this.context.getBeanNamesForType(type);
if (names.length == 1) {
return (T) this.context.getBean(names[0]);
}
return null;
}
protected void setApplicationContext(ApplicationContext applicationContext)
throws BeansException {
this.context = applicationContext;
}
}

View File

@ -0,0 +1,84 @@
/*
* 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.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;
import org.springframework.security.authentication.ReactiveAuthenticationManager;
import org.springframework.security.authentication.UserDetailsRepositoryReactiveAuthenticationManager;
import org.springframework.security.core.userdetails.ReactiveUserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
/**
* @author Rob Winch
* @since 5.2
*/
@Configuration(proxyBeanMethods = false)
class RSocketSecurityConfiguration {
private static final String BEAN_NAME_PREFIX = "org.springframework.security.config.annotation.rsocket.RSocketSecurityConfiguration.";
private static final String RSOCKET_SECURITY_BEAN_NAME = BEAN_NAME_PREFIX + "rsocketSecurity";
private ReactiveAuthenticationManager authenticationManager;
private ReactiveUserDetailsService reactiveUserDetailsService;
private PasswordEncoder passwordEncoder;
@Autowired(required = false)
void setAuthenticationManager(
ReactiveAuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
@Autowired(required = false)
void setUserDetailsService(ReactiveUserDetailsService userDetailsService) {
this.reactiveUserDetailsService = userDetailsService;
}
@Autowired(required = false)
void setPasswordEncoder(PasswordEncoder passwordEncoder) {
this.passwordEncoder = passwordEncoder;
}
@Bean(name = RSOCKET_SECURITY_BEAN_NAME)
@Scope("prototype")
public RSocketSecurity rsocketSecurity(ApplicationContext context) {
RSocketSecurity security = new RSocketSecurity()
.authenticationManager(authenticationManager());
security.setApplicationContext(context);
return security;
}
private ReactiveAuthenticationManager authenticationManager() {
if (this.authenticationManager != null) {
return this.authenticationManager;
}
if (this.reactiveUserDetailsService != null) {
UserDetailsRepositoryReactiveAuthenticationManager manager =
new UserDetailsRepositoryReactiveAuthenticationManager(this.reactiveUserDetailsService);
if (this.passwordEncoder != null) {
manager.setPasswordEncoder(this.passwordEncoder);
}
return manager;
}
return null;
}
}

View File

@ -0,0 +1,41 @@
/*
* 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.AbstractRSocket;
import io.rsocket.ConnectionSetupPayload;
import io.rsocket.Payload;
import io.rsocket.RSocket;
import io.rsocket.SocketAcceptor;
import io.rsocket.util.ByteBufPayload;
import reactor.core.publisher.Mono;
public class HelloHandler implements SocketAcceptor {
@Override
public Mono<RSocket> accept(ConnectionSetupPayload setup, RSocket sendingSocket) {
return Mono.just(
new AbstractRSocket() {
@Override
public Mono<Payload> requestResponse(Payload payload) {
String data = payload.getDataUtf8();
payload.release();
System.out.println("Got " + data);
return Mono.just(ByteBufPayload.create("Hello " + data));
}
});
}
}

View File

@ -0,0 +1,182 @@
/*
* Copyright 2002-2013 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.config.Customizer;
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.interceptor.PayloadSocketAcceptorInterceptor;
import org.springframework.security.rsocket.metadata.BasicAuthenticationEncoder;
import org.springframework.security.rsocket.metadata.BearerTokenMetadata;
import org.springframework.stereotype.Controller;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
import reactor.core.publisher.Mono;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
/**
* @author Rob Winch
*/
@ContextConfiguration
@RunWith(SpringRunner.class)
public class JwtITests {
@Autowired
RSocketMessageHandler handler;
@Autowired
PayloadSocketAcceptorInterceptor interceptor;
@Autowired
ServerController controller;
@Autowired
ReactiveJwtDecoder decoder;
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", 7000))
.start()
.block();
}
@After
public void dispose() {
this.requester.rsocket().dispose();
this.server.dispose();
this.controller.payloads.clear();
}
@Test
public void routeWhenAuthorized() {
BearerTokenMetadata credentials =
new BearerTokenMetadata("token");
when(this.decoder.decode(any())).thenReturn(Mono.just(jwt()));
this.requester = requester()
.setupMetadata(credentials.getToken(), BearerTokenMetadata.BEARER_AUTHENTICATION_MIME_TYPE)
.connectTcp(this.server.address().getHostName(), this.server.address().getPort())
.block();
String hiRob = this.requester.route("secure.retrieve-mono")
.data("rob")
.retrieveMono(String.class)
.block();
assertThat(hiRob).isEqualTo("Hi rob");
}
private Jwt jwt() {
Map<String, Object> claims = new HashMap<>();
claims.put(IdTokenClaimNames.ISS, "https://issuer.example.com");
claims.put(IdTokenClaimNames.SUB, "rob");
claims.put(IdTokenClaimNames.AUD, Arrays.asList("client-id"));
Instant issuedAt = Instant.now();
Instant expiresAt = Instant.from(issuedAt).plusSeconds(3600);
return new Jwt("token", issuedAt, expiresAt, claims, claims);
}
private RSocketRequester.Builder requester() {
return RSocketRequester.builder()
.rsocketStrategies(this.handler.getRSocketStrategies());
}
@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
PayloadSocketAcceptorInterceptor rsocketInterceptor(RSocketSecurity rsocket) {
rsocket
.authorizePayload(authorize ->
authorize
.route("secure.admin.*").authenticated()
.anyRequest().permitAll()
)
.jwt(Customizer.withDefaults());
return rsocket.build();
}
@Bean
ReactiveJwtDecoder jwtDecoder() {
return mock(ReactiveJwtDecoder.class);
}
}
@Controller
static class ServerController {
private List<String> payloads = new ArrayList<>();
@MessageMapping("**")
String connect(String payload) {
return "Hi " + payload;
}
}
}

View File

@ -0,0 +1,246 @@
/*
* Copyright 2002-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.exceptions.ApplicationErrorException;
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.config.Customizer;
import org.springframework.security.config.annotation.rsocket.EnableRSocketSecurity;
import org.springframework.security.config.annotation.rsocket.RSocketSecurity;
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.interceptor.PayloadSocketAcceptorInterceptor;
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 RSocketMessageHandlerConnectionITests {
@Autowired
RSocketMessageHandler handler;
@Autowired
PayloadSocketAcceptorInterceptor 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", 7000))
.start()
.block();
}
@After
public void dispose() {
this.requester.rsocket().dispose();
this.server.dispose();
this.controller.payloads.clear();
}
@Test
public void routeWhenAuthorized() {
UsernamePasswordMetadata credentials =
new UsernamePasswordMetadata("user", "password");
this.requester = requester()
.setupMetadata(credentials, UsernamePasswordMetadata.BASIC_AUTHENTICATION_MIME_TYPE)
.connectTcp(this.server.address().getHostName(), this.server.address().getPort())
.block();
String hiRob = this.requester.route("secure.retrieve-mono")
.data("rob")
.retrieveMono(String.class)
.block();
assertThat(hiRob).isEqualTo("Hi rob");
}
@Test
public void routeWhenNotAuthorized() {
UsernamePasswordMetadata credentials = new UsernamePasswordMetadata("user", "password");
this.requester = requester()
.setupMetadata(credentials, UsernamePasswordMetadata.BASIC_AUTHENTICATION_MIME_TYPE)
.connectTcp(this.server.address().getHostName(), this.server.address().getPort())
.block();
assertThatCode(() -> this.requester.route("secure.admin.retrieve-mono")
.data("data")
.retrieveMono(String.class)
.block())
.isInstanceOf(ApplicationErrorException.class);
}
@Test
public void routeWhenStreamCredentialsAuthorized() {
UsernamePasswordMetadata connectCredentials = new UsernamePasswordMetadata("user", "password");
this.requester = requester()
.setupMetadata(connectCredentials, UsernamePasswordMetadata.BASIC_AUTHENTICATION_MIME_TYPE)
.connectTcp(this.server.address().getHostName(), this.server.address().getPort())
.block();
String hiRob = this.requester.route("secure.admin.retrieve-mono")
.metadata(new UsernamePasswordMetadata("admin", "password"), UsernamePasswordMetadata.BASIC_AUTHENTICATION_MIME_TYPE)
.data("rob")
.retrieveMono(String.class)
.block();
assertThat(hiRob).isEqualTo("Hi rob");
}
@Test
public void connectWhenNotAuthenticated() {
this.requester = requester()
.connectTcp(this.server.address().getHostName(), this.server.address().getPort())
.block();
assertThatCode(() -> this.requester.route("retrieve-mono")
.data("data")
.retrieveMono(String.class)
.block())
.isNotNull();
// FIXME: https://github.com/rsocket/rsocket-java/issues/686
// .isInstanceOf(RejectedSetupException.class);
}
@Test
public void connectWhenNotAuthorized() {
UsernamePasswordMetadata credentials = new UsernamePasswordMetadata("evil", "password");
this.requester = requester()
.setupMetadata(credentials, UsernamePasswordMetadata.BASIC_AUTHENTICATION_MIME_TYPE)
.connectTcp(this.server.address().getHostName(), this.server.address().getPort())
.block();
assertThatCode(() -> this.requester.route("retrieve-mono")
.data("data")
.retrieveMono(String.class)
.block())
.isNotNull();
// FIXME: https://github.com/rsocket/rsocket-java/issues/686
// .isInstanceOf(RejectedSetupException.class);
}
private RSocketRequester.Builder requester() {
return RSocketRequester.builder()
.rsocketStrategies(this.handler.getRSocketStrategies());
}
@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 admin = User.withDefaultPasswordEncoder()
.username("admin")
.password("password")
.roles("USER", "ADMIN", "SETUP")
.build();
UserDetails user = User.withDefaultPasswordEncoder()
.username("user")
.password("password")
.roles("USER", "SETUP")
.build();
UserDetails evil = User.withDefaultPasswordEncoder()
.username("evil")
.password("password")
.roles("EVIL")
.build();
return new MapReactiveUserDetailsService(admin, user, evil);
}
@Bean
PayloadSocketAcceptorInterceptor rsocketInterceptor(RSocketSecurity rsocket) {
rsocket
.authorizePayload(authorize ->
authorize
.setup().hasRole("SETUP")
.route("secure.admin.*").hasRole("ADMIN")
.route("secure.**").hasRole("USER")
.anyRequest().permitAll()
)
.basicAuthentication(Customizer.withDefaults());
return rsocket.build();
}
}
@Controller
static class ServerController {
private List<String> payloads = new ArrayList<>();
@MessageMapping("**")
String connect(String payload) {
return "Hi " + payload;
}
}
}

View File

@ -0,0 +1,312 @@
/*
* 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.exceptions.ApplicationErrorException;
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.config.Customizer;
import org.springframework.security.config.annotation.rsocket.EnableRSocketSecurity;
import org.springframework.security.config.annotation.rsocket.RSocketSecurity;
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.interceptor.PayloadSocketAcceptorInterceptor;
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 reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
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 RSocketMessageHandlerITests {
@Autowired
RSocketMessageHandler handler;
@Autowired
PayloadSocketAcceptorInterceptor 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", 7000))
.start()
.block();
this.requester = RSocketRequester.builder()
// .rsocketFactory(factory -> factory.addRequesterPlugin(payloadInterceptor))
.rsocketStrategies(this.handler.getRSocketStrategies())
.connectTcp("localhost", 7000)
.block();
}
@After
public void dispose() {
this.requester.rsocket().dispose();
this.server.dispose();
this.controller.payloads.clear();
}
@Test
public void retrieveMonoWhenSecureThenDenied() throws Exception {
String data = "rob";
assertThatCode(() -> this.requester.route("secure.retrieve-mono")
.data(data)
.retrieveMono(String.class)
.block()
).isInstanceOf(ApplicationErrorException.class);
assertThat(this.controller.payloads).isEmpty();
}
@Test
public void retrieveMonoWhenAuthenticationFailedThenException() throws Exception {
String data = "rob";
UsernamePasswordMetadata credentials = new UsernamePasswordMetadata("invalid", "password");
assertThatCode(() -> this.requester.route("secure.retrieve-mono")
.metadata(credentials, UsernamePasswordMetadata.BASIC_AUTHENTICATION_MIME_TYPE)
.data(data)
.retrieveMono(String.class)
.block()
).isInstanceOf(ApplicationErrorException.class);
assertThat(this.controller.payloads).isEmpty();
}
@Test
public void retrieveMonoWhenAuthorizedThenGranted() throws Exception {
String data = "rob";
UsernamePasswordMetadata credentials = new UsernamePasswordMetadata("rob", "password");
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);
}
@Test
public void retrieveMonoWhenPublicThenGranted() throws Exception {
String data = "rob";
String hiRob = this.requester.route("retrieve-mono")
.data(data)
.retrieveMono(String.class)
.block();
assertThat(hiRob).isEqualTo("Hi rob");
assertThat(this.controller.payloads).containsOnly(data);
}
@Test
public void retrieveFluxWhenDataFluxAndSecureThenDenied() throws Exception {
Flux<String> data = Flux.just("a", "b", "c");
assertThatCode(() -> this.requester.route("secure.secure.retrieve-flux")
.data(data, String.class)
.retrieveFlux(String.class)
.collectList()
.block()).isInstanceOf(
ApplicationErrorException.class);
assertThat(this.controller.payloads).isEmpty();
}
@Test
public void retrieveFluxWhenDataFluxAndPublicThenGranted() throws Exception {
Flux<String> data = Flux.just("a", "b", "c");
List<String> hi = this.requester.route("retrieve-flux")
.data(data, String.class)
.retrieveFlux(String.class)
.collectList()
.block();
assertThat(hi).containsOnly("hello a", "hello b", "hello c");
assertThat(this.controller.payloads).containsOnlyElementsOf(data.collectList().block());
}
@Test
public void retrieveFluxWhenDataStringAndSecureThenDenied() throws Exception {
String data = "a";
assertThatCode(() -> this.requester.route("secure.hello")
.data(data)
.retrieveFlux(String.class)
.collectList()
.block()).isInstanceOf(
ApplicationErrorException.class);
assertThat(this.controller.payloads).isEmpty();
}
@Test
public void retrieveFluxWhenDataStringAndPublicThenGranted() throws Exception {
String data = "a";
List<String> hi = this.requester.route("retrieve-flux")
.data(data)
.retrieveFlux(String.class)
.collectList()
.block();
assertThat(hi).contains("hello a");
assertThat(this.controller.payloads).containsOnly(data);
}
@Test
public void sendWhenSecureThenDenied() throws Exception {
String data = "hi";
this.requester.route("secure.send")
.data(data)
.send()
.block();
assertThat(this.controller.payloads).isEmpty();
}
@Test
public void sendWhenPublicThenGranted() throws Exception {
String data = "hi";
this.requester.route("send")
.data(data)
.send()
.block();
assertThat(this.controller.awaitPayloads()).containsOnly("hi");
}
@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();
UserDetails rossen = User.withDefaultPasswordEncoder()
.username("rossen")
.password("password")
.roles("USER")
.build();
return new MapReactiveUserDetailsService(rob, rossen);
}
@Bean
PayloadSocketAcceptorInterceptor rsocketInterceptor(RSocketSecurity rsocket) {
rsocket
.authorizePayload(authorize -> {
authorize
.route("secure.*").authenticated()
.anyRequest().permitAll();
})
.basicAuthentication(Customizer.withDefaults());
return rsocket.build();
}
}
@Controller
static class ServerController {
private List<String> payloads = new ArrayList<>();
@MessageMapping({"secure.retrieve-mono", "retrieve-mono"})
String retrieveMono(String payload) {
add(payload);
return "Hi " + payload;
}
@MessageMapping({"secure.retrieve-flux", "retrieve-flux"})
Flux<String> retrieveFlux(Flux<String> payload) {
return payload.doOnNext(this::add)
.map(p -> "hello " + p);
}
@MessageMapping({"secure.send", "send"})
Mono<Void> send(Flux<String> payload) {
return payload
.doOnNext(this::add)
.then(Mono.fromRunnable(() -> {
doNotifyAll();
}));
}
private synchronized void doNotifyAll() {
this.notifyAll();
}
private synchronized List<String> awaitPayloads() throws InterruptedException {
this.wait();
return this.payloads;
}
private void add(String p) {
this.payloads.add(p);
}
}
}

View File

@ -1,5 +1,5 @@
^\Q/*\E$
^\Q * Copyright\E (\d{4}\-\d{4} the original author or authors\.|(\d{4}, )*(\d{4}) Acegi Technology Pty Limited)$
^\Q * Copyright\E (\d{4}(\-\d{4})? the original author or authors\.|(\d{4}, )*(\d{4}) Acegi Technology Pty Limited)$
^\Q *\E$
^\Q * Licensed under the Apache License, Version 2.0 (the "License");\E$
^\Q * you may not use this file except in compliance with the License.\E$

View File

@ -1,15 +1,17 @@
if (!project.hasProperty('reactorVersion')) {
ext.reactorVersion = 'Dysprosium-M3'
ext.reactorVersion = 'Dysprosium-RC1'
}
if (!project.hasProperty('springVersion')) {
ext.springVersion = '5.2.0.RC1'
ext.springVersion = '5.2.0.BUILD-SNAPSHOT'
}
if (!project.hasProperty('springDataVersion')) {
ext.springDataVersion = 'Moore-RC2'
}
ext.rsocketVersion = '1.0.0-RC3'
dependencyManagement {
imports {
mavenBom "io.projectreactor:reactor-bom:${reactorVersion}"
@ -71,6 +73,8 @@ dependencyManagement {
dependency 'commons-logging:commons-logging:1.2'
dependency 'dom4j:dom4j:1.6.1'
dependency 'io.projectreactor.tools:blockhound:1.0.0.M4'
dependency "io.rsocket:rsocket-core:${rsocketVersion}"
dependency "io.rsocket:rsocket-transport-netty:${rsocketVersion}"
dependency 'javax.activation:activation:1.1.1'
dependency 'javax.annotation:jsr250-api:1.0'
dependency 'javax.inject:javax.inject:1'

View File

@ -0,0 +1,9 @@
apply plugin: 'io.spring.convention.spring-module'
dependencies {
compile project(':spring-security-core')
compile 'io.rsocket:rsocket-core'
optional project(':spring-security-oauth2-resource-server')
optional 'org.springframework:spring-messaging'
testCompile 'io.projectreactor:reactor-test'
}

View File

@ -0,0 +1,96 @@
/*
* 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.interceptor;
import reactor.core.publisher.Mono;
import reactor.util.context.Context;
import java.util.List;
import java.util.ListIterator;
/**
* A {@link PayloadInterceptorChain} which exposes the Reactor {@link Context} via a member variable.
* This class is not Thread safe, so a new instance must be created for each Thread.
*
* Internally {@code ContextPayloadInterceptorChain} is used to ensure that the Reactor
* {@code Context} is captured so it can be transferred to subscribers outside of this
* {@code Context} in {@code PayloadSocketAcceptor}.
*
* @author Rob Winch
* @since 5.2
* @see PayloadSocketAcceptor
*/
class ContextPayloadInterceptorChain implements PayloadInterceptorChain {
private final PayloadInterceptor currentInterceptor;
private final ContextPayloadInterceptorChain next;
private Context context;
ContextPayloadInterceptorChain(List<PayloadInterceptor> interceptors) {
if (interceptors == null) {
throw new IllegalArgumentException("interceptors cannot be null");
}
if (interceptors.isEmpty()) {
throw new IllegalArgumentException("interceptors cannot be empty");
}
ContextPayloadInterceptorChain interceptor = init(interceptors);
this.currentInterceptor = interceptor.currentInterceptor;
this.next = interceptor.next;
}
private static ContextPayloadInterceptorChain init(List<PayloadInterceptor> interceptors) {
ContextPayloadInterceptorChain interceptor = new ContextPayloadInterceptorChain(null, null);
ListIterator<? extends PayloadInterceptor> iterator = interceptors.listIterator(interceptors.size());
while (iterator.hasPrevious()) {
interceptor = new ContextPayloadInterceptorChain(iterator.previous(), interceptor);
}
return interceptor;
}
private ContextPayloadInterceptorChain(PayloadInterceptor currentInterceptor, ContextPayloadInterceptorChain next) {
this.currentInterceptor = currentInterceptor;
this.next = next;
}
public Mono<Void> next(PayloadExchange exchange) {
return Mono.defer(() ->
shouldIntercept() ?
this.currentInterceptor.intercept(exchange, this.next) :
Mono.subscriberContext()
.doOnNext(c -> this.context = c)
.then()
);
}
Context getContext() {
if (this.next == null) {
return this.context;
}
return this.next.getContext();
}
private boolean shouldIntercept() {
return this.currentInterceptor != null && this.next != null;
}
@Override
public String toString() {
return getClass().getSimpleName() + "[currentInterceptor=" + this.currentInterceptor + "]";
}
}

View File

@ -0,0 +1,70 @@
/*
* 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.interceptor;
import io.rsocket.Payload;
import org.springframework.util.Assert;
import org.springframework.util.MimeType;
/**
* Default implementation of {@link PayloadExchange}
*
* @author Rob Winch
* @since 5.2
*/
public class DefaultPayloadExchange implements PayloadExchange {
private final PayloadExchangeType type;
private final Payload payload;
private final MimeType metadataMimeType;
private final MimeType dataMimeType;
public DefaultPayloadExchange(PayloadExchangeType type, Payload payload, MimeType metadataMimeType,
MimeType dataMimeType) {
Assert.notNull(type, "type cannot be null");
Assert.notNull(payload, "payload cannot be null");
Assert.notNull(metadataMimeType, "metadataMimeType cannot be null");
Assert.notNull(dataMimeType, "dataMimeType cannot be null");
this.type = type;
this.payload = payload;
this.metadataMimeType = metadataMimeType;
this.dataMimeType = dataMimeType;
}
@Override
public PayloadExchangeType getType() {
return this.type;
}
@Override
public Payload getPayload() {
return this.payload;
}
@Override
public MimeType getMetadataMimeType() {
return this.metadataMimeType;
}
@Override
public MimeType getDataMimeType() {
return this.dataMimeType;
}
}

View File

@ -0,0 +1,36 @@
/*
* 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.interceptor;
import io.rsocket.Payload;
import org.springframework.util.MimeType;
/**
* Contract for a Payload interaction.
*
* @author Rob Winch
* @since 5.2
*/
public interface PayloadExchange {
PayloadExchangeType getType();
Payload getPayload();
MimeType getDataMimeType();
MimeType getMetadataMimeType();
}

View File

@ -0,0 +1,80 @@
/*
* 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.interceptor;
/**
* The {@link PayloadExchange} type
*
* @author Rob Winch
* @since 5.2
*/
public enum PayloadExchangeType {
/**
* The <a href="https://rsocket.io/docs/Protocol#setup-frame-0x01">Setup</a>. Can
* be used to determine if a Payload is part of the connection
*/
SETUP(false),
/**
* A <a href="https://rsocket.io/docs/Protocol#frame-fnf">Fire and Forget</a> exchange.
*/
FIRE_AND_FORGET(true),
/**
* A <a href="https://rsocket.io/docs/Protocol#frame-request-response">Request
* Response</a> exchange.
*/
REQUEST_RESPONSE(true),
/**
* A <a href="https://rsocket.io/docs/Protocol#request-stream-frame">Request Stream</a>
* exchange. This is only represents the request portion. The {@link #PAYLOAD} type
* represents the data that submitted.
*/
REQUEST_STREAM(true),
/**
* A <a href="https://rsocket.io/docs/Protocol#request-channel-frame">Request
* Channel</a> exchange.
*/
REQUEST_CHANNEL(true),
/**
* A <a href="https://rsocket.io/docs/Protocol#payload-frame">Payload</a> exchange.
*/
PAYLOAD(false),
/**
* A <a href="https://rsocket.io/docs/Protocol#frame-metadata-push">Metadata Push</a>
* exchange.
*/
METADATA_PUSH(true);
private final boolean isRequest;
PayloadExchangeType(boolean isRequest) {
this.isRequest = isRequest;
}
/**
* Determines if this exchange is a type of request (i.e. the initial frame).
* @return true if it is a request, else false
*/
public boolean isRequest() {
return this.isRequest;
}
}

View File

@ -0,0 +1,38 @@
/*
* 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.interceptor;
import reactor.core.publisher.Mono;
/**
* Contract for interception-style, chained processing of Payloads that may
* be used to implement cross-cutting, application-agnostic requirements such
* as security, timeouts, and others.
*
* @author Rob Winch
* @since 5.2
*/
public interface PayloadInterceptor {
/**
* Process the Web request and (optionally) delegate to the next
* {@code PayloadInterceptor} through the given {@link PayloadInterceptorChain}.
* @param exchange the current payload exchange
* @param chain provides a way to delegate to the next interceptor
* @return {@code Mono<Void>} to indicate when payload processing is complete
*/
Mono<Void> intercept(PayloadExchange exchange, PayloadInterceptorChain chain);
}

View File

@ -0,0 +1,34 @@
/*
* 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.interceptor;
import reactor.core.publisher.Mono;
/**
* Contract to allow a {@link PayloadInterceptor} to delegate to the next in the chain.
* *
* @author Rob Winch
* @since 5.2
*/
public interface PayloadInterceptorChain {
/**
* Process the payload exchange.
* @param exchange the current server exchange
* @return {@code Mono<Void>} to indicate when request processing is complete
*/
Mono<Void> next(PayloadExchange exchange);
}

View File

@ -0,0 +1,140 @@
/*
* 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.interceptor;
import io.rsocket.Payload;
import io.rsocket.RSocket;
import io.rsocket.ResponderRSocket;
import io.rsocket.util.RSocketProxy;
import org.reactivestreams.Publisher;
import org.springframework.util.MimeType;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.util.context.Context;
import java.util.List;
/**
* Combines the {@link PayloadInterceptor} with a {@link ResponderRSocket}
* @author Rob Winch
* @since 5.2
*/
class PayloadInterceptorRSocket extends RSocketProxy implements ResponderRSocket {
private final List<PayloadInterceptor> interceptors;
private final MimeType metadataMimeType;
private final MimeType dataMimeType;
private final Context context;
PayloadInterceptorRSocket(RSocket delegate,
List<PayloadInterceptor> interceptors, MimeType metadataMimeType,
MimeType dataMimeType) {
this(delegate, interceptors, metadataMimeType, dataMimeType, Context.empty());
}
PayloadInterceptorRSocket(RSocket delegate,
List<PayloadInterceptor> interceptors, MimeType metadataMimeType,
MimeType dataMimeType, Context context) {
super(delegate);
this.metadataMimeType = metadataMimeType;
this.dataMimeType = dataMimeType;
if (delegate == null) {
throw new IllegalArgumentException("delegate cannot be null");
}
if (interceptors == null) {
throw new IllegalArgumentException("interceptors cannot be null");
}
if (interceptors.isEmpty()) {
throw new IllegalArgumentException("interceptors cannot be empty");
}
this.interceptors = interceptors;
this.context = context;
}
@Override
public Mono<Void> fireAndForget(Payload payload) {
return intercept(PayloadExchangeType.FIRE_AND_FORGET, payload)
.flatMap(context ->
this.source.fireAndForget(payload)
.subscriberContext(context)
);
}
@Override
public Mono<Payload> requestResponse(Payload payload) {
return intercept(PayloadExchangeType.REQUEST_RESPONSE, payload)
.flatMap(context ->
this.source.requestResponse(payload)
.subscriberContext(context)
);
}
@Override
public Flux<Payload> requestStream(Payload payload) {
return intercept(PayloadExchangeType.REQUEST_STREAM, payload)
.flatMapMany(context ->
this.source.requestStream(payload)
.subscriberContext(context)
);
}
@Override
public Flux<Payload> requestChannel(Publisher<Payload> payloads) {
return Flux.from(payloads)
.switchOnFirst((signal, innerFlux) -> {
Payload firstPayload = signal.get();
return intercept(PayloadExchangeType.REQUEST_CHANNEL, firstPayload)
.flatMapMany(context ->
innerFlux
.skip(1)
.flatMap(p -> intercept(PayloadExchangeType.PAYLOAD, p).thenReturn(p))
.transform(securedPayloads -> Flux.concat(Flux.just(firstPayload), securedPayloads))
.transform(securedPayloads -> this.source.requestChannel(securedPayloads))
.subscriberContext(context)
);
});
}
@Override
public Mono<Void> metadataPush(Payload payload) {
return intercept(PayloadExchangeType.METADATA_PUSH, payload)
.flatMap(c -> this.source
.metadataPush(payload)
.subscriberContext(c)
);
}
private Mono<Context> intercept(PayloadExchangeType type, Payload payload) {
return Mono.defer(() -> {
ContextPayloadInterceptorChain chain = new ContextPayloadInterceptorChain(this.interceptors);
DefaultPayloadExchange exchange = new DefaultPayloadExchange(type, payload,
this.metadataMimeType, this.dataMimeType);
return chain.next(exchange)
.then(Mono.fromCallable(() -> chain.getContext()))
.defaultIfEmpty(Context.empty())
.subscriberContext(this.context);
});
}
@Override
public String toString() {
return getClass().getSimpleName() + "[source=" + this.source + ",interceptors="
+ this.interceptors + "]";
}
}

View File

@ -0,0 +1,99 @@
/*
* 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.interceptor;
import io.rsocket.ConnectionSetupPayload;
import io.rsocket.Payload;
import io.rsocket.RSocket;
import io.rsocket.SocketAcceptor;
import io.rsocket.metadata.WellKnownMimeType;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.MimeType;
import org.springframework.util.MimeTypeUtils;
import org.springframework.util.StringUtils;
import reactor.core.publisher.Mono;
import reactor.util.context.Context;
import java.util.List;
/**
* @author Rob Winch
* @since 5.2
*/
class PayloadSocketAcceptor implements SocketAcceptor {
private final SocketAcceptor delegate;
private final List<PayloadInterceptor> interceptors;
@Nullable
private MimeType defaultDataMimeType;
private MimeType defaultMetadataMimeType =
MimeTypeUtils.parseMimeType(WellKnownMimeType.MESSAGE_RSOCKET_COMPOSITE_METADATA.getString());
PayloadSocketAcceptor(SocketAcceptor delegate, List<PayloadInterceptor> interceptors) {
Assert.notNull(delegate, "delegate cannot be null");
if (interceptors == null) {
throw new IllegalArgumentException("interceptors cannot be null");
}
if (interceptors.isEmpty()) {
throw new IllegalArgumentException("interceptors cannot be empty");
}
this.delegate = delegate;
this.interceptors = interceptors;
}
@Override
public Mono<RSocket> accept(ConnectionSetupPayload setup, RSocket sendingSocket) {
MimeType dataMimeType = parseMimeType(setup.dataMimeType(), this.defaultDataMimeType);
Assert.notNull(dataMimeType, "No `dataMimeType` in ConnectionSetupPayload and no default value");
MimeType metadataMimeType = parseMimeType(setup.metadataMimeType(), this.defaultMetadataMimeType);
Assert.notNull(metadataMimeType, "No `metadataMimeType` in ConnectionSetupPayload and no default value");
// FIXME do we want to make the sendingSocket available in the PayloadExchange
return intercept(setup, dataMimeType, metadataMimeType)
.flatMap(ctx -> this.delegate.accept(setup, sendingSocket)
.map(acceptingSocket -> new PayloadInterceptorRSocket(acceptingSocket, this.interceptors, metadataMimeType, dataMimeType, ctx))
);
}
private Mono<Context> intercept(Payload payload, MimeType dataMimeType, MimeType metadataMimeType) {
return Mono.defer(() -> {
ContextPayloadInterceptorChain chain = new ContextPayloadInterceptorChain(this.interceptors);
DefaultPayloadExchange exchange = new DefaultPayloadExchange(PayloadExchangeType.SETUP, payload,
metadataMimeType, dataMimeType);
return chain.next(exchange)
.then(Mono.fromCallable(() -> chain.getContext()))
.defaultIfEmpty(Context.empty());
});
}
private MimeType parseMimeType(String str, MimeType defaultMimeType) {
return StringUtils.hasText(str) ? MimeTypeUtils.parseMimeType(str) : defaultMimeType;
}
public void setDefaultDataMimeType(@Nullable MimeType defaultDataMimeType) {
this.defaultDataMimeType = defaultDataMimeType;
}
public void setDefaultMetadataMimeType(MimeType defaultMetadataMimeType) {
Assert.notNull(defaultMetadataMimeType, "defaultMetadataMimeType cannot be null");
this.defaultMetadataMimeType = defaultMetadataMimeType;
}
}

View File

@ -0,0 +1,66 @@
/*
* 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.interceptor;
import io.rsocket.SocketAcceptor;
import io.rsocket.metadata.WellKnownMimeType;
import io.rsocket.plugins.SocketAcceptorInterceptor;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.MimeType;
import org.springframework.util.MimeTypeUtils;
import java.util.List;
/**
* A {@link SocketAcceptorInterceptor} that applies the {@link PayloadInterceptor}s
*
* @author Rob Winch
* @since 5.2
*/
public class PayloadSocketAcceptorInterceptor implements SocketAcceptorInterceptor {
private final List<PayloadInterceptor> interceptors;
@Nullable
private MimeType defaultDataMimeType;
private MimeType defaultMetadataMimeType =
MimeTypeUtils.parseMimeType(WellKnownMimeType.MESSAGE_RSOCKET_COMPOSITE_METADATA.getString());
public PayloadSocketAcceptorInterceptor(List<PayloadInterceptor> interceptors) {
this.interceptors = interceptors;
}
@Override
public SocketAcceptor apply(SocketAcceptor socketAcceptor) {
PayloadSocketAcceptor acceptor = new PayloadSocketAcceptor(
socketAcceptor, this.interceptors);
acceptor.setDefaultDataMimeType(this.defaultDataMimeType);
acceptor.setDefaultMetadataMimeType(this.defaultMetadataMimeType);
return acceptor;
}
public void setDefaultDataMimeType(@Nullable MimeType defaultDataMimeType) {
this.defaultDataMimeType = defaultDataMimeType;
}
public void setDefaultMetadataMimeType(MimeType defaultMetadataMimeType) {
Assert.notNull(defaultMetadataMimeType, "defaultMetadataMimeType cannot be null");
this.defaultMetadataMimeType = defaultMetadataMimeType;
}
}

View File

@ -0,0 +1,83 @@
/*
* 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.interceptor.authentication;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
import org.springframework.util.Assert;
import reactor.core.publisher.Mono;
import org.springframework.security.rsocket.interceptor.PayloadInterceptorChain;
import org.springframework.security.rsocket.interceptor.PayloadExchange;
import org.springframework.security.rsocket.interceptor.PayloadInterceptor;
import java.util.List;
/**
* If {@link ReactiveSecurityContextHolder} is empty populates an
* {@code AnonymousAuthenticationToken}
*
* @author Rob Winch
* @since 5.2
*/
public class AnonymousPayloadInterceptor implements PayloadInterceptor {
private String key;
private Object principal;
private List<GrantedAuthority> authorities;
/**
* Creates a filter with a principal named "anonymousUser" and the single authority
* "ROLE_ANONYMOUS".
*
* @param key the key to identify tokens created by this filter
*/
public AnonymousPayloadInterceptor(String key) {
this(key, "anonymousUser", AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS"));
}
/**
* @param key key the key to identify tokens created by this filter
* @param principal the principal which will be used to represent anonymous users
* @param authorities the authority list for anonymous users
*/
public AnonymousPayloadInterceptor(String key, Object principal,
List<GrantedAuthority> authorities) {
Assert.hasLength(key, "key cannot be null or empty");
Assert.notNull(principal, "Anonymous authentication principal must be set");
Assert.notNull(authorities, "Anonymous authorities must be set");
this.key = key;
this.principal = principal;
this.authorities = authorities;
}
@Override
public Mono<Void> intercept(PayloadExchange exchange, PayloadInterceptorChain chain) {
return ReactiveSecurityContextHolder.getContext()
.switchIfEmpty(Mono.defer(() -> {
AnonymousAuthenticationToken authentication = new AnonymousAuthenticationToken(
this.key, this.principal, this.authorities);
return chain.next(exchange)
.subscriberContext(ReactiveSecurityContextHolder.withAuthentication(authentication))
.then(Mono.empty());
}))
.flatMap(securityContext -> chain.next(exchange));
}
}

View File

@ -0,0 +1,74 @@
/*
* 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.interceptor.authentication;
import org.springframework.security.authentication.ReactiveAuthenticationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
import org.springframework.security.rsocket.interceptor.PayloadExchange;
import org.springframework.security.rsocket.interceptor.PayloadInterceptor;
import org.springframework.security.rsocket.interceptor.PayloadInterceptorChain;
import org.springframework.util.Assert;
import reactor.core.publisher.Mono;
/**
* Uses the provided {@code ReactiveAuthenticationManager} to authenticate a Payload. If
* authentication is successful, then the result is added to
* {@link ReactiveSecurityContextHolder}.
*
* @author Rob Winch
* @since 5.2
*/
public class AuthenticationPayloadInterceptor implements PayloadInterceptor {
private final ReactiveAuthenticationManager authenticationManager;
private PayloadExchangeAuthenticationConverter authenticationConverter =
new BasicAuthenticationPayloadExchangeConverter();
/**
* Creates a new instance
* @param authenticationManager the manager to use. Cannot be null
*/
public AuthenticationPayloadInterceptor(ReactiveAuthenticationManager authenticationManager) {
Assert.notNull(authenticationManager, "authenticationManager cannot be null");
this.authenticationManager = authenticationManager;
}
/**
* Sets the convert to be used
* @param authenticationConverter
*/
public void setAuthenticationConverter(
PayloadExchangeAuthenticationConverter authenticationConverter) {
Assert.notNull(authenticationConverter, "authenticationConverter cannot be null");
this.authenticationConverter = authenticationConverter;
}
public Mono<Void> intercept(PayloadExchange exchange, PayloadInterceptorChain chain) {
return this.authenticationConverter.convert(exchange)
.switchIfEmpty(chain.next(exchange).then(Mono.empty()))
.flatMap(a -> this.authenticationManager.authenticate(a))
.flatMap(a -> onAuthenticationSuccess(chain.next(exchange), a));
}
private Mono<Void> onAuthenticationSuccess(Mono<Void> payload, Authentication authentication) {
return payload
.subscriberContext(ReactiveSecurityContextHolder.withAuthentication(authentication));
}
}

View File

@ -0,0 +1,60 @@
/*
* 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.interceptor.authentication;
import io.rsocket.metadata.WellKnownMimeType;
import org.springframework.messaging.rsocket.DefaultMetadataExtractor;
import org.springframework.messaging.rsocket.MetadataExtractor;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.rsocket.interceptor.PayloadExchange;
import org.springframework.security.rsocket.metadata.BasicAuthenticationDecoder;
import org.springframework.security.rsocket.metadata.UsernamePasswordMetadata;
import org.springframework.util.MimeType;
import org.springframework.util.MimeTypeUtils;
import reactor.core.publisher.Mono;
/**
* Converts from the {@link PayloadExchange} to a
* {@link UsernamePasswordAuthenticationToken} by extracting
* {@link UsernamePasswordMetadata#BASIC_AUTHENTICATION_MIME_TYPE} from the metadata.
*
* @author Rob Winch
* @since 5.2
*/
public class BasicAuthenticationPayloadExchangeConverter implements PayloadExchangeAuthenticationConverter {
private MimeType metadataMimetype = MimeTypeUtils.parseMimeType(
WellKnownMimeType.MESSAGE_RSOCKET_COMPOSITE_METADATA.getString());
private MetadataExtractor metadataExtractor = createDefaultExtractor();
@Override
public Mono<Authentication> convert(PayloadExchange exchange) {
return Mono.fromCallable(() -> this.metadataExtractor
.extract(exchange.getPayload(), this.metadataMimetype))
.flatMap(metadata -> Mono.justOrEmpty(metadata.get(UsernamePasswordMetadata.BASIC_AUTHENTICATION_MIME_TYPE.toString())))
.cast(UsernamePasswordMetadata.class)
.map(credentials -> new UsernamePasswordAuthenticationToken(credentials.getUsername(), credentials.getPassword()));
}
private static MetadataExtractor createDefaultExtractor() {
DefaultMetadataExtractor result = new DefaultMetadataExtractor(new BasicAuthenticationDecoder());
result.metadataToExtract(UsernamePasswordMetadata.BASIC_AUTHENTICATION_MIME_TYPE, UsernamePasswordMetadata.class, (String) null);
return result;
}
}

View File

@ -0,0 +1,54 @@
/*
* 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.interceptor.authentication;
import io.netty.buffer.ByteBuf;
import io.rsocket.metadata.CompositeMetadata;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken;
import org.springframework.security.rsocket.interceptor.PayloadExchange;
import org.springframework.security.rsocket.metadata.BearerTokenMetadata;
import reactor.core.publisher.Mono;
import java.nio.charset.StandardCharsets;
/**
* Converts from the {@link PayloadExchange} to a
* {@link BearerTokenAuthenticationToken} by extracting
* {@link BearerTokenMetadata#BEARER_AUTHENTICATION_MIME_TYPE} from the metadata.
* @author Rob Winch
* @since 5.2
*/
public class BearerPayloadExchangeConverter implements PayloadExchangeAuthenticationConverter {
private static final String BEARER_MIME_TYPE_VALUE =
BearerTokenMetadata.BEARER_AUTHENTICATION_MIME_TYPE.toString();
@Override
public Mono<Authentication> convert(PayloadExchange exchange) {
ByteBuf metadata = exchange.getPayload().metadata();
CompositeMetadata compositeMetadata = new CompositeMetadata(metadata, false);
for (CompositeMetadata.Entry entry : compositeMetadata) {
if (BEARER_MIME_TYPE_VALUE.equals(entry.getMimeType())) {
ByteBuf content = entry.getContent();
String token = content.toString(StandardCharsets.UTF_8);
return Mono.just(new BearerTokenAuthenticationToken(token));
}
}
return Mono.empty();
}
}

View File

@ -0,0 +1,30 @@
/*
* 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.interceptor.authentication;
import org.springframework.security.core.Authentication;
import org.springframework.security.rsocket.interceptor.PayloadExchange;
import reactor.core.publisher.Mono;
/**
* Converts from a {@link PayloadExchange} to an {@link Authentication}
* @author Rob Winch
* @since 5.2
*/
public interface PayloadExchangeAuthenticationConverter {
Mono<Authentication> convert(PayloadExchange exchange);
}

View File

@ -0,0 +1,53 @@
/*
* 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.interceptor.authorization;
import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException;
import org.springframework.security.authorization.ReactiveAuthorizationManager;
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.util.Assert;
import reactor.core.publisher.Mono;
import org.springframework.security.rsocket.interceptor.PayloadInterceptorChain;
import org.springframework.security.rsocket.interceptor.PayloadExchange;
import org.springframework.security.rsocket.interceptor.PayloadInterceptor;
/**
* Provides authorization of the {@link PayloadExchange}.
*
* @author Rob Winch
* @since 5.2
*/
public class AuthorizationPayloadInterceptor implements PayloadInterceptor {
private final ReactiveAuthorizationManager<PayloadExchange> authorizationManager;
public AuthorizationPayloadInterceptor(
ReactiveAuthorizationManager<PayloadExchange> authorizationManager) {
Assert.notNull(authorizationManager, "authorizationManager cannot be null");
this.authorizationManager = authorizationManager;
}
@Override
public Mono<Void> intercept(PayloadExchange exchange, PayloadInterceptorChain chain) {
return ReactiveSecurityContextHolder.getContext()
.filter(c -> c.getAuthentication() != null)
.map(SecurityContext::getAuthentication)
.switchIfEmpty(Mono.error(() -> new AuthenticationCredentialsNotFoundException("An Authentication (possibly AnonymousAuthenticationToken) is required.")))
.as(authentication -> this.authorizationManager.verify(authentication, exchange))
.then(chain.next(exchange));
}
}

View File

@ -0,0 +1,82 @@
/*
* 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.interceptor.authorization;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.ReactiveAuthorizationManager;
import org.springframework.security.core.Authentication;
import org.springframework.util.Assert;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.security.rsocket.interceptor.PayloadExchange;
import org.springframework.security.rsocket.util.PayloadExchangeAuthorizationContext;
import org.springframework.security.rsocket.util.PayloadExchangeMatcher;
import org.springframework.security.rsocket.util.PayloadExchangeMatcherEntry;
import java.util.ArrayList;
import java.util.List;
/**
* Maps a @{code List} of {@link PayloadExchangeMatcher} instances to
* @{code ReactiveAuthorizationManager} instances.
*
* @author Rob Winch
* @since 5.2
*/
public class PayloadExchangeMatcherReactiveAuthorizationManager implements ReactiveAuthorizationManager<PayloadExchange> {
private final List<PayloadExchangeMatcherEntry<ReactiveAuthorizationManager<PayloadExchangeAuthorizationContext>>> mappings;
private PayloadExchangeMatcherReactiveAuthorizationManager(List<PayloadExchangeMatcherEntry<ReactiveAuthorizationManager<PayloadExchangeAuthorizationContext>>> mappings) {
Assert.notEmpty(mappings, "mappings cannot be null");
this.mappings = mappings;
}
@Override
public Mono<AuthorizationDecision> check(Mono<Authentication> authentication, PayloadExchange exchange) {
return Flux.fromIterable(this.mappings)
.concatMap(mapping -> mapping.getMatcher().matches(exchange)
.filter(PayloadExchangeMatcher.MatchResult::isMatch)
.map(r -> r.getVariables())
.flatMap(variables -> mapping.getEntry()
.check(authentication, new PayloadExchangeAuthorizationContext(exchange, variables))
)
)
.next()
.switchIfEmpty(Mono.fromCallable(() -> new AuthorizationDecision(false)));
}
public static PayloadExchangeMatcherReactiveAuthorizationManager.Builder builder() {
return new PayloadExchangeMatcherReactiveAuthorizationManager.Builder();
}
public static class Builder {
private final List<PayloadExchangeMatcherEntry<ReactiveAuthorizationManager<PayloadExchangeAuthorizationContext>>> mappings = new ArrayList<>();
private Builder() {
}
public PayloadExchangeMatcherReactiveAuthorizationManager.Builder add(
PayloadExchangeMatcherEntry<ReactiveAuthorizationManager<PayloadExchangeAuthorizationContext>> entry) {
this.mappings.add(entry);
return this;
}
public PayloadExchangeMatcherReactiveAuthorizationManager build() {
return new PayloadExchangeMatcherReactiveAuthorizationManager(this.mappings);
}
}
}

View File

@ -0,0 +1,76 @@
/*
* 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.metadata;
import org.reactivestreams.Publisher;
import org.springframework.core.ResolvableType;
import org.springframework.core.codec.AbstractDecoder;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.util.MimeType;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.Map;
/**
* Decodes {@link UsernamePasswordMetadata#BASIC_AUTHENTICATION_MIME_TYPE}
*
* @author Rob Winch
* @since 5.2
*/
public class BasicAuthenticationDecoder extends AbstractDecoder<UsernamePasswordMetadata> {
public BasicAuthenticationDecoder() {
super(UsernamePasswordMetadata.BASIC_AUTHENTICATION_MIME_TYPE);
}
@Override
public Flux<UsernamePasswordMetadata> decode(Publisher<DataBuffer> input,
ResolvableType elementType, MimeType mimeType, Map<String, Object> hints) {
return Flux.from(input)
.map(DataBuffer::asByteBuffer)
.map(byteBuffer -> {
byte[] sizeBytes = new byte[4];
byteBuffer.get(sizeBytes);
int usernameSize = 4;
byte[] usernameBytes = new byte[usernameSize];
byteBuffer.get(usernameBytes);
byte[] passwordBytes = new byte[byteBuffer.remaining()];
byteBuffer.get(passwordBytes);
String username = new String(usernameBytes);
String password = new String(passwordBytes);
return new UsernamePasswordMetadata(username, password);
});
}
@Override
public Mono<UsernamePasswordMetadata> decodeToMono(Publisher<DataBuffer> input,
ResolvableType elementType, MimeType mimeType, Map<String, Object> hints) {
return Mono.from(input)
.map(DataBuffer::asByteBuffer)
.map(byteBuffer -> {
int usernameSize = byteBuffer.getInt();
byte[] usernameBytes = new byte[usernameSize];
byteBuffer.get(usernameBytes);
byte[] passwordBytes = new byte[byteBuffer.remaining()];
byteBuffer.get(passwordBytes);
String username = new String(usernameBytes);
String password = new String(passwordBytes);
return new UsernamePasswordMetadata(username, password);
});
}
}

View File

@ -0,0 +1,76 @@
/*
* 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.metadata;
import org.reactivestreams.Publisher;
import org.springframework.core.ResolvableType;
import org.springframework.core.codec.AbstractEncoder;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.util.MimeType;
import reactor.core.publisher.Flux;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.Map;
/**
* Encodes {@link UsernamePasswordMetadata#BASIC_AUTHENTICATION_MIME_TYPE}
*
* @author Rob Winch
* @since 5.2
*/
public class BasicAuthenticationEncoder extends
AbstractEncoder<UsernamePasswordMetadata> {
public BasicAuthenticationEncoder() {
super(UsernamePasswordMetadata.BASIC_AUTHENTICATION_MIME_TYPE);
}
@Override
public Flux<DataBuffer> encode(
Publisher<? extends UsernamePasswordMetadata> inputStream,
DataBufferFactory bufferFactory, ResolvableType elementType,
MimeType mimeType, Map<String, Object> hints) {
return Flux.from(inputStream).map(credentials ->
encodeValue(credentials, bufferFactory, elementType, mimeType, hints));
}
@Override
public DataBuffer encodeValue(UsernamePasswordMetadata credentials,
DataBufferFactory bufferFactory, ResolvableType valueType, MimeType mimeType,
Map<String, Object> hints) {
String username = credentials.getUsername();
String password = credentials.getPassword();
byte[] usernameBytes = username.getBytes(StandardCharsets.UTF_8);
byte[] usernameBytesLengthBytes = ByteBuffer.allocate(4).putInt(usernameBytes.length).array();
DataBuffer metadata = bufferFactory.allocateBuffer();
boolean release = true;
try {
metadata.write(usernameBytesLengthBytes);
metadata.write(usernameBytes);
metadata.write(password.getBytes(StandardCharsets.UTF_8));
release = false;
return metadata;
} finally {
if (release) {
DataBufferUtils.release(metadata);
}
}
}
}

View File

@ -0,0 +1,47 @@
/*
* 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.metadata;
import org.springframework.http.MediaType;
import org.springframework.util.MimeType;
/**
* Represents a bearer token that has been encoded into a
* {@link Payload#metadata()}.
*
* @author Rob Winch
* @since 5.2
*/
public class BearerTokenMetadata {
/**
* Represents a bearer token which is encoded as a String.
*
* See <a href="https://github.com/rsocket/rsocket/issues/272">rsocket/rsocket#272</a>
*/
public static final MimeType BEARER_AUTHENTICATION_MIME_TYPE = new MediaType("message", "x.rsocket.authentication.bearer.v0");
private final String token;
public BearerTokenMetadata(String token) {
this.token = token;
}
public String getToken() {
return this.token;
}
}

View File

@ -0,0 +1,55 @@
/*
* 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.metadata;
import io.rsocket.Payload;
import org.springframework.http.MediaType;
import org.springframework.util.MimeType;
/**
* Represents a username and password that have been encoded into a
* {@link Payload#metadata()}.
*
* @author Rob Winch
* @since 5.2
*/
public final class UsernamePasswordMetadata {
/**
* Represents a username password which is encoded as
* {@code ${username-bytes-length}${username-bytes}${password-bytes}}.
*
* See <a href="https://github.com/rsocket/rsocket/issues/272">rsocket/rsocket#272</a>
*/
public static final MimeType BASIC_AUTHENTICATION_MIME_TYPE = new MediaType("message", "x.rsocket.authentication.basic.v0");
private final String username;
private final String password;
public UsernamePasswordMetadata(String username, String password) {
this.username = username;
this.password = password;
}
public String getUsername() {
return this.username;
}
public String getPassword() {
return this.password;
}
}

View File

@ -0,0 +1,48 @@
/*
* 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.util;
import org.springframework.security.rsocket.interceptor.PayloadExchange;
import java.util.Collections;
import java.util.Map;
/**
* @author Rob Winch
* @since 5.2
*/
public class PayloadExchangeAuthorizationContext {
private final PayloadExchange exchange;
private final Map<String, Object> variables;
public PayloadExchangeAuthorizationContext(PayloadExchange exchange) {
this(exchange, Collections.emptyMap());
}
public PayloadExchangeAuthorizationContext(PayloadExchange exchange, Map<String, Object> variables) {
this.exchange = exchange;
this.variables = variables;
}
public PayloadExchange getExchange() {
return this.exchange;
}
public Map<String, Object> getVariables() {
return Collections.unmodifiableMap(this.variables);
}
}

View File

@ -0,0 +1,89 @@
/*
* 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.util;
import org.springframework.security.rsocket.interceptor.PayloadExchange;
import reactor.core.publisher.Mono;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
/**
* An interface for determining if a {@link PayloadExchangeMatcher} matches.
* @author Rob Winch
* @since 5.2
*/
public interface PayloadExchangeMatcher {
/**
* Determines if a request matches or not
* @param exchange
* @return
*/
Mono<MatchResult> matches(PayloadExchange exchange);
/**
* The result of matching
*/
class MatchResult {
private final boolean match;
private final Map<String, Object> variables;
private MatchResult(boolean match, Map<String, Object> variables) {
this.match = match;
this.variables = variables;
}
public boolean isMatch() {
return match;
}
/**
* Gets potential variables and their values
* @return
*/
public Map<String, Object> getVariables() {
return variables;
}
/**
* Creates an instance of {@link MatchResult} that is a match with no variables
* @return
*/
public static Mono<MatchResult> match() {
return match(Collections.emptyMap());
}
/**
*
* Creates an instance of {@link MatchResult} that is a match with the specified variables
* @param variables
* @return
*/
public static Mono<MatchResult> match(Map<String, ? extends Object> variables) {
return Mono.just(new MatchResult(true, variables == null ? null : new HashMap<String, Object>(variables)));
}
/**
* Creates an instance of {@link MatchResult} that is not a match.
* @return
*/
public static Mono<MatchResult> notMatch() {
return Mono.just(new MatchResult(false, Collections.emptyMap()));
}
}
}

View File

@ -0,0 +1,38 @@
/*
* 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.util;
/**
* @author Rob Winch
*/
public class PayloadExchangeMatcherEntry<T> {
private final PayloadExchangeMatcher matcher;
private final T entry;
public PayloadExchangeMatcherEntry(PayloadExchangeMatcher matcher, T entry) {
this.matcher = matcher;
this.entry = entry;
}
public PayloadExchangeMatcher getMatcher() {
return this.matcher;
}
public T getEntry() {
return this.entry;
}
}

View File

@ -0,0 +1,57 @@
/*
* 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.util;
import org.springframework.security.rsocket.interceptor.PayloadExchange;
import org.springframework.security.rsocket.interceptor.PayloadExchangeType;
import reactor.core.publisher.Mono;
/**
* @author Rob Winch
*/
public abstract class PayloadExchangeMatchers {
public static PayloadExchangeMatcher setup() {
return new PayloadExchangeMatcher() {
public Mono<MatchResult> matches(PayloadExchange exchange) {
return PayloadExchangeType.SETUP.equals(exchange.getType()) ?
MatchResult.match() :
MatchResult.notMatch();
}
};
}
public static PayloadExchangeMatcher anyRequest() {
return new PayloadExchangeMatcher() {
public Mono<MatchResult> matches(PayloadExchange exchange) {
return exchange.getType().isRequest() ?
MatchResult.match() :
MatchResult.notMatch();
}
};
}
public static PayloadExchangeMatcher anyExchange() {
return new PayloadExchangeMatcher() {
public Mono<MatchResult> matches(PayloadExchange exchange) {
return MatchResult.match();
}
};
}
private PayloadExchangeMatchers() {}
}

View File

@ -0,0 +1,61 @@
/*
* 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.util;
import org.springframework.messaging.rsocket.MetadataExtractor;
import org.springframework.security.rsocket.interceptor.PayloadExchange;
import org.springframework.util.Assert;
import org.springframework.util.RouteMatcher;
import reactor.core.publisher.Mono;
import java.util.Map;
import java.util.Optional;
/**
* FIXME: Pay attention to the package this goes into. It requires spring-messaging for
* the MetadataExtractor.
*
* @author Rob Winch
* @since 5.2
*/
public class RoutePayloadExchangeMatcher implements PayloadExchangeMatcher {
private final String pattern;
private final MetadataExtractor metadataExtractor;
private final RouteMatcher routeMatcher;
public RoutePayloadExchangeMatcher(MetadataExtractor metadataExtractor,
RouteMatcher routeMatcher, String pattern) {
Assert.notNull(pattern, "pattern cannot be null");
this.metadataExtractor = metadataExtractor;
this.routeMatcher = routeMatcher;
this.pattern = pattern;
}
@Override
public Mono<MatchResult> matches(PayloadExchange exchange) {
Map<String, Object> metadata = this.metadataExtractor
.extract(exchange.getPayload(), exchange.getMetadataMimeType());
return Optional.ofNullable((String) metadata.get(MetadataExtractor.ROUTE_KEY))
.map(routeValue -> this.routeMatcher.parseRoute(routeValue))
.map(route -> this.routeMatcher.matchAndExtract(this.pattern, route))
.map(v -> MatchResult.match(v))
.orElse(MatchResult.notMatch());
}
}

View File

@ -0,0 +1,108 @@
/*
* 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.authentication;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
import org.springframework.security.rsocket.interceptor.PayloadExchange;
import org.springframework.security.rsocket.interceptor.authentication.AnonymousPayloadInterceptor;
import java.util.List;
import static org.assertj.core.api.Assertions.*;
/**
* @author Rob Winch
*/
@RunWith(MockitoJUnitRunner.class)
public class AnonymousPayloadInterceptorTests {
@Mock
private PayloadExchange exchange;
private AnonymousPayloadInterceptor interceptor;
@Before
public void setup() {
this.interceptor = new AnonymousPayloadInterceptor("key");
}
@Test
public void constructorKeyWhenKeyNullThenException() {
String key = null;
assertThatCode(() -> new AnonymousPayloadInterceptor(key))
.isInstanceOf(IllegalArgumentException.class);
}
@Test
public void constructorKeyPrincipalAuthoritiesWhenKeyNullThenException() {
String key = null;
assertThatCode(() -> new AnonymousPayloadInterceptor(key, "principal",
AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS")))
.isInstanceOf(IllegalArgumentException.class);
}
@Test
public void constructorKeyPrincipalAuthoritiesWhenPrincipalNullThenException() {
Object principal = null;
assertThatCode(() -> new AnonymousPayloadInterceptor("key", principal,
AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS")))
.isInstanceOf(IllegalArgumentException.class);
}
@Test
public void constructorKeyPrincipalAuthoritiesWhenAuthoritiesNullThenException() {
List<GrantedAuthority> authorities = null;
assertThatCode(() -> new AnonymousPayloadInterceptor("key", "principal",
authorities))
.isInstanceOf(IllegalArgumentException.class);
}
@Test
public void interceptWhenNoAuthenticationThenAnonymousAuthentication() {
AuthenticationPayloadInterceptorChain chain = new AuthenticationPayloadInterceptorChain();
this.interceptor.intercept(this.exchange, chain).block();
Authentication authentication = chain.getAuthentication();
assertThat(authentication).isInstanceOf(AnonymousAuthenticationToken.class);
}
@Test
public void interceptWhenAuthenticationThenOriginalAuthentication() {
AuthenticationPayloadInterceptorChain chain = new AuthenticationPayloadInterceptorChain();
TestingAuthenticationToken expected =
new TestingAuthenticationToken("test", "password");
this.interceptor.intercept(this.exchange, chain)
.subscriberContext(ReactiveSecurityContextHolder.withAuthentication(expected))
.block();
Authentication authentication = chain.getAuthentication();
assertThat(authentication).isEqualTo(expected);
}
}

View File

@ -0,0 +1,45 @@
/*
* 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.authentication;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
import org.springframework.security.core.context.SecurityContext;
import reactor.core.publisher.Mono;
import org.springframework.security.rsocket.interceptor.PayloadInterceptorChain;
import org.springframework.security.rsocket.interceptor.PayloadExchange;
/**
* @author Rob Winch
*/
class AuthenticationPayloadInterceptorChain implements PayloadInterceptorChain {
private Authentication authentication;
@Override
public Mono<Void> next(PayloadExchange exchange) {
return ReactiveSecurityContextHolder.getContext().map(SecurityContext::getAuthentication)
.doOnNext(a -> this.setAuthentication(a)).then();
}
public Authentication getAuthentication() {
return this.authentication;
}
public void setAuthentication(Authentication authentication) {
this.authentication = authentication;
}
}

View File

@ -0,0 +1,148 @@
/*
* 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.authentication;
import io.netty.buffer.ByteBufAllocator;
import io.netty.buffer.CompositeByteBuf;
import io.rsocket.Payload;
import io.rsocket.metadata.CompositeMetadataFlyweight;
import io.rsocket.metadata.WellKnownMimeType;
import io.rsocket.util.DefaultPayload;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.core.ResolvableType;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
import org.springframework.core.io.buffer.NettyDataBufferFactory;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.ReactiveAuthenticationManager;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.rsocket.interceptor.PayloadExchangeType;
import org.springframework.security.rsocket.interceptor.authentication.AuthenticationPayloadInterceptor;
import org.springframework.security.rsocket.metadata.BasicAuthenticationEncoder;
import org.springframework.security.rsocket.metadata.UsernamePasswordMetadata;
import org.springframework.util.MimeType;
import org.springframework.util.MimeTypeUtils;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import reactor.test.publisher.PublisherProbe;
import org.springframework.security.rsocket.interceptor.DefaultPayloadExchange;
import org.springframework.security.rsocket.interceptor.PayloadInterceptorChain;
import org.springframework.security.rsocket.interceptor.PayloadExchange;
import java.util.Map;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* @author Rob Winch
*/
@RunWith(MockitoJUnitRunner.class)
public class AuthenticationPayloadInterceptorTests {
static final MimeType COMPOSITE_METADATA = MimeTypeUtils.parseMimeType(
WellKnownMimeType.MESSAGE_RSOCKET_COMPOSITE_METADATA.getString());
@Mock
ReactiveAuthenticationManager authenticationManager;
@Captor
ArgumentCaptor<Authentication> authenticationArg;
@Test
public void constructorWhenAuthenticationManagerNullThenException() {
assertThatCode(() -> new AuthenticationPayloadInterceptor(null))
.isInstanceOf(IllegalArgumentException.class);
}
@Test
public void interceptWhenBasicCredentialsThenAuthenticates() {
AuthenticationPayloadInterceptor interceptor = new AuthenticationPayloadInterceptor(
this.authenticationManager);
PayloadExchange exchange = createExchange();
TestingAuthenticationToken expectedAuthentication =
new TestingAuthenticationToken("user", "password");
when(this.authenticationManager.authenticate(any())).thenReturn(Mono.just(
expectedAuthentication));
AuthenticationPayloadInterceptorChain authenticationPayloadChain = new AuthenticationPayloadInterceptorChain();
interceptor.intercept(exchange, authenticationPayloadChain)
.block();
Authentication authentication = authenticationPayloadChain.getAuthentication();
verify(this.authenticationManager).authenticate(this.authenticationArg.capture());
assertThat(this.authenticationArg.getValue()).isEqualToComparingFieldByField(new UsernamePasswordAuthenticationToken("user", "password"));
assertThat(authentication).isEqualTo(expectedAuthentication);
}
@Test
public void interceptWhenAuthenticationSuccessThenChainSubscribedOnce() {
AuthenticationPayloadInterceptor interceptor = new AuthenticationPayloadInterceptor(
this.authenticationManager);
PayloadExchange exchange = createExchange();
TestingAuthenticationToken expectedAuthentication =
new TestingAuthenticationToken("user", "password");
when(this.authenticationManager.authenticate(any())).thenReturn(Mono.just(
expectedAuthentication));
PublisherProbe<Void> voidResult = PublisherProbe.empty();
PayloadInterceptorChain chain = mock(PayloadInterceptorChain.class);
when(chain.next(any())).thenReturn(voidResult.mono());
StepVerifier.create(interceptor.intercept(exchange, chain))
.then(() -> assertThat(voidResult.subscribeCount()).isEqualTo(1))
.verifyComplete();
}
private Payload createRequestPayload() {
UsernamePasswordMetadata credentials = new UsernamePasswordMetadata("user", "password");
BasicAuthenticationEncoder encoder = new BasicAuthenticationEncoder();
DefaultDataBufferFactory factory = new DefaultDataBufferFactory();
ResolvableType elementType = ResolvableType
.forClass(UsernamePasswordMetadata.class);
MimeType mimeType = UsernamePasswordMetadata.BASIC_AUTHENTICATION_MIME_TYPE;
Map<String, Object> hints = null;
DataBuffer dataBuffer = encoder.encodeValue(credentials, factory,
elementType, mimeType, hints);
ByteBufAllocator allocator = ByteBufAllocator.DEFAULT;
CompositeByteBuf metadata = allocator.compositeBuffer();
CompositeMetadataFlyweight.encodeAndAddMetadata(
metadata, allocator, mimeType.toString(), NettyDataBufferFactory.toByteBuf(dataBuffer));
return DefaultPayload.create(allocator.buffer(),
metadata);
}
private PayloadExchange createExchange() {
return new DefaultPayloadExchange(PayloadExchangeType.REQUEST_RESPONSE, createRequestPayload(), COMPOSITE_METADATA,
MediaType.APPLICATION_JSON);
}
}

View File

@ -0,0 +1,118 @@
/*
* 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.authorization;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.authorization.ReactiveAuthorizationManager;
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
import org.springframework.security.rsocket.interceptor.authorization.AuthorizationPayloadInterceptor;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import reactor.test.publisher.PublisherProbe;
import reactor.util.context.Context;
import org.springframework.security.rsocket.interceptor.PayloadInterceptorChain;
import org.springframework.security.rsocket.interceptor.PayloadExchange;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.when;
import static org.springframework.security.authorization.AuthenticatedReactiveAuthorizationManager.authenticated;
import static org.springframework.security.authorization.AuthorityReactiveAuthorizationManager.hasRole;
/**
* @author Rob Winch
*/
@RunWith(MockitoJUnitRunner.class)
public class AuthorizationPayloadInterceptorTests {
@Mock
private ReactiveAuthorizationManager<PayloadExchange> authorizationManager;
@Mock
private PayloadExchange exchange;
@Mock
private PayloadInterceptorChain chain;
private PublisherProbe<Void> managerResult = PublisherProbe.empty();
private PublisherProbe<Void> chainResult = PublisherProbe.empty();
@Test
public void interceptWhenAuthenticationEmptyAndSubscribedThenException() {
when(this.chain.next(any())).thenReturn(this.chainResult.mono());
AuthorizationPayloadInterceptor interceptor =
new AuthorizationPayloadInterceptor(authenticated());
StepVerifier.create(interceptor.intercept(this.exchange, this.chain))
.then(() -> this.chainResult.assertWasNotSubscribed())
.verifyError(AuthenticationCredentialsNotFoundException.class);
}
@Test
public void interceptWhenAuthenticationNotSubscribedAndEmptyThenCompletes() {
when(this.chain.next(any())).thenReturn(this.chainResult.mono());
when(this.authorizationManager.verify(any(), any()))
.thenReturn(this.managerResult.mono());
AuthorizationPayloadInterceptor interceptor =
new AuthorizationPayloadInterceptor(this.authorizationManager);
StepVerifier.create(interceptor.intercept(this.exchange, this.chain))
.then(() -> this.chainResult.assertWasSubscribed())
.verifyComplete();
}
@Test
public void interceptWhenNotAuthorizedThenException() {
when(this.chain.next(any())).thenReturn(this.chainResult.mono());
AuthorizationPayloadInterceptor interceptor =
new AuthorizationPayloadInterceptor(hasRole("USER"));
Context userContext = ReactiveSecurityContextHolder
.withAuthentication(new TestingAuthenticationToken("user", "password"));
Mono<Void> intercept = interceptor.intercept(this.exchange, this.chain)
.subscriberContext(userContext);
StepVerifier.create(intercept)
.then(() -> this.chainResult.assertWasNotSubscribed())
.verifyError(AccessDeniedException.class);
}
@Test
public void interceptWhenAuthorizedThenContinues() {
when(this.chain.next(any())).thenReturn(this.chainResult.mono());
AuthorizationPayloadInterceptor interceptor =
new AuthorizationPayloadInterceptor(authenticated());
Context userContext = ReactiveSecurityContextHolder
.withAuthentication(new TestingAuthenticationToken("user", "password", "ROLE_USER"));
Mono<Void> intercept = interceptor.intercept(this.exchange, this.chain)
.subscriberContext(userContext);
StepVerifier.create(intercept)
.then(() -> this.chainResult.assertWasSubscribed())
.verifyComplete();
}
}

View File

@ -0,0 +1,509 @@
/*
* 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.interceptor;
import io.rsocket.Payload;
import io.rsocket.RSocket;
import io.rsocket.metadata.WellKnownMimeType;
import io.rsocket.util.RSocketProxy;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import org.mockito.stubbing.Answer;
import org.reactivestreams.Publisher;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.util.MimeType;
import org.springframework.util.MimeTypeUtils;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import reactor.test.publisher.PublisherProbe;
import reactor.test.publisher.TestPublisher;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when;
/**
* @author Rob Winch
*/
@RunWith(MockitoJUnitRunner.class)
public class PayloadInterceptorRSocketTests {
static final MimeType COMPOSITE_METADATA = MimeTypeUtils.parseMimeType(
WellKnownMimeType.MESSAGE_RSOCKET_COMPOSITE_METADATA.getString());
@Mock
RSocket delegate;
@Mock
PayloadInterceptor interceptor;
@Mock
PayloadInterceptor interceptor2;
@Mock
Payload payload;
@Captor
private ArgumentCaptor<PayloadExchange> exchange;
PublisherProbe<Void> voidResult = PublisherProbe.empty();
TestPublisher<Payload> payloadResult = TestPublisher.createCold();
private MimeType metadataMimeType = COMPOSITE_METADATA;
private MimeType dataMimeType = MediaType.APPLICATION_JSON;
@Test
public void constructorWhenNullDelegateThenException() {
this.delegate = null;
List<PayloadInterceptor> interceptors = Arrays.asList(this.interceptor);
assertThatCode(() -> {
new PayloadInterceptorRSocket(this.delegate, interceptors,
metadataMimeType, dataMimeType);
})
.isInstanceOf(IllegalArgumentException.class);
}
@Test
public void constructorWhenNullInterceptorsThenException() {
List<PayloadInterceptor> interceptors = null;
assertThatCode(() -> new PayloadInterceptorRSocket(this.delegate, interceptors,
metadataMimeType, dataMimeType))
.isInstanceOf(IllegalArgumentException.class);
}
@Test
public void constructorWhenEmptyInterceptorsThenException() {
List<PayloadInterceptor> interceptors = Collections.emptyList();
assertThatCode(() -> new PayloadInterceptorRSocket(this.delegate, interceptors,
metadataMimeType, dataMimeType))
.isInstanceOf(IllegalArgumentException.class);
}
// single interceptor
@Test
public void fireAndForgetWhenInterceptorCompletesThenDelegateSubscribed() {
when(this.interceptor.intercept(any(), any())).thenAnswer(withChainNext());
when(this.delegate.fireAndForget(any())).thenReturn(this.voidResult.mono());
PayloadInterceptorRSocket interceptor = new PayloadInterceptorRSocket(this.delegate,
Arrays.asList(this.interceptor), metadataMimeType, dataMimeType);
StepVerifier.create(interceptor.fireAndForget(this.payload))
.then(() -> this.voidResult.assertWasSubscribed())
.verifyComplete();
verify(this.interceptor).intercept(this.exchange.capture(), any());
assertThat(this.exchange.getValue().getPayload()).isEqualTo(this.payload);
}
@Test
public void fireAndForgetWhenInterceptorErrorsThenDelegateNotSubscribed() {
RuntimeException expected = new RuntimeException("Oops");
when(this.interceptor.intercept(any(), any())).thenReturn(Mono.error(expected));
PayloadInterceptorRSocket interceptor = new PayloadInterceptorRSocket(this.delegate,
Arrays.asList(this.interceptor), metadataMimeType, dataMimeType);
StepVerifier.create(interceptor.fireAndForget(this.payload))
.then(() -> this.voidResult.assertWasNotSubscribed())
.verifyErrorSatisfies(e -> assertThat(e).isEqualTo(expected));
verify(this.interceptor).intercept(this.exchange.capture(), any());
assertThat(this.exchange.getValue().getPayload()).isEqualTo(this.payload);
}
@Test
public void fireAndForgetWhenSecurityContextThenDelegateContext() {
TestingAuthenticationToken authentication = new TestingAuthenticationToken("user", "password");
when(this.interceptor.intercept(any(), any())).thenAnswer(withAuthenticated(authentication));
when(this.delegate.fireAndForget(any())).thenReturn(Mono.empty());
RSocket assertAuthentication = new RSocketProxy(this.delegate) {
@Override
public Mono<Void> fireAndForget(Payload payload) {
return assertAuthentication(authentication)
.flatMap(a -> super.fireAndForget(payload));
}
};
PayloadInterceptorRSocket interceptor = new PayloadInterceptorRSocket(assertAuthentication,
Arrays.asList(this.interceptor), metadataMimeType, dataMimeType);
interceptor.fireAndForget(this.payload).block();
verify(this.interceptor).intercept(this.exchange.capture(), any());
assertThat(this.exchange.getValue().getPayload()).isEqualTo(this.payload);
verify(this.delegate).fireAndForget(this.payload);
}
@Test
public void requestResponseWhenInterceptorCompletesThenDelegateSubscribed() {
when(this.interceptor.intercept(any(), any())).thenReturn(Mono.empty());
when(this.delegate.requestResponse(any())).thenReturn(this.payloadResult.mono());
PayloadInterceptorRSocket interceptor = new PayloadInterceptorRSocket(this.delegate,
Arrays.asList(this.interceptor), metadataMimeType, dataMimeType);
StepVerifier.create(interceptor.requestResponse(this.payload))
.then(() -> this.payloadResult.assertSubscribers())
.then(() -> this.payloadResult.emit(this.payload))
.expectNext(this.payload)
.verifyComplete();
verify(this.interceptor).intercept(this.exchange.capture(), any());
assertThat(this.exchange.getValue().getPayload()).isEqualTo(this.payload);
verify(this.delegate).requestResponse(this.payload);
}
@Test
public void requestResponseWhenInterceptorErrorsThenDelegateNotInvoked() {
RuntimeException expected = new RuntimeException("Oops");
when(this.interceptor.intercept(any(), any())).thenReturn(Mono.error(expected));
PayloadInterceptorRSocket interceptor = new PayloadInterceptorRSocket(this.delegate,
Arrays.asList(this.interceptor), metadataMimeType, dataMimeType);
assertThatCode(() -> interceptor.requestResponse(this.payload).block()).isEqualTo(expected);
verify(this.interceptor).intercept(this.exchange.capture(), any());
assertThat(this.exchange.getValue().getPayload()).isEqualTo(this.payload);
verifyZeroInteractions(this.delegate);
}
@Test
public void requestResponseWhenSecurityContextThenDelegateContext() {
TestingAuthenticationToken authentication = new TestingAuthenticationToken("user", "password");
when(this.interceptor.intercept(any(), any())).thenAnswer(withAuthenticated(authentication));
when(this.delegate.requestResponse(any())).thenReturn(this.payloadResult.mono());
RSocket assertAuthentication = new RSocketProxy(this.delegate) {
@Override
public Mono<Payload> requestResponse(Payload payload) {
return assertAuthentication(authentication)
.flatMap(a -> super.requestResponse(payload));
}
};
PayloadInterceptorRSocket interceptor = new PayloadInterceptorRSocket(assertAuthentication,
Arrays.asList(this.interceptor), metadataMimeType, dataMimeType);
StepVerifier.create(interceptor.requestResponse(this.payload))
.then(() -> this.payloadResult.assertSubscribers())
.then(() -> this.payloadResult.emit(this.payload))
.expectNext(this.payload)
.verifyComplete();
verify(this.interceptor).intercept(this.exchange.capture(), any());
assertThat(this.exchange.getValue().getPayload()).isEqualTo(this.payload);
verify(this.delegate).requestResponse(this.payload);
}
@Test
public void requestStreamWhenInterceptorCompletesThenDelegateSubscribed() {
when(this.interceptor.intercept(any(), any())).thenReturn(Mono.empty());
when(this.delegate.requestStream(any())).thenReturn(this.payloadResult.flux());
PayloadInterceptorRSocket interceptor = new PayloadInterceptorRSocket(this.delegate,
Arrays.asList(this.interceptor), metadataMimeType, dataMimeType);
StepVerifier.create(interceptor.requestStream(this.payload))
.then(() -> this.payloadResult.assertSubscribers())
.then(() -> this.payloadResult.emit(this.payload))
.expectNext(this.payload)
.verifyComplete();
verify(this.interceptor).intercept(this.exchange.capture(), any());
assertThat(this.exchange.getValue().getPayload()).isEqualTo(this.payload);
}
@Test
public void requestStreamWhenInterceptorErrorsThenDelegateNotSubscribed() {
RuntimeException expected = new RuntimeException("Oops");
when(this.interceptor.intercept(any(), any())).thenReturn(Mono.error(expected));
PayloadInterceptorRSocket interceptor = new PayloadInterceptorRSocket(this.delegate,
Arrays.asList(this.interceptor), metadataMimeType, dataMimeType);
StepVerifier.create(interceptor.requestStream(this.payload))
.then(() -> this.payloadResult.assertNoSubscribers())
.verifyErrorSatisfies(e -> assertThat(e).isEqualTo(expected));
verify(this.interceptor).intercept(this.exchange.capture(), any());
assertThat(this.exchange.getValue().getPayload()).isEqualTo(this.payload);
}
@Test
public void requestStreamWhenSecurityContextThenDelegateContext() {
TestingAuthenticationToken authentication = new TestingAuthenticationToken("user", "password");
when(this.interceptor.intercept(any(), any())).thenAnswer(withAuthenticated(authentication));
when(this.delegate.requestStream(any())).thenReturn(this.payloadResult.flux());
RSocket assertAuthentication = new RSocketProxy(this.delegate) {
@Override
public Flux<Payload> requestStream(Payload payload) {
return assertAuthentication(authentication)
.flatMapMany(a -> super.requestStream(payload));
}
};
PayloadInterceptorRSocket interceptor = new PayloadInterceptorRSocket(assertAuthentication,
Arrays.asList(this.interceptor), metadataMimeType, dataMimeType);
StepVerifier.create(interceptor.requestStream(this.payload))
.then(() -> this.payloadResult.assertSubscribers())
.then(() -> this.payloadResult.emit(this.payload))
.expectNext(this.payload)
.verifyComplete();
verify(this.interceptor).intercept(this.exchange.capture(), any());
assertThat(this.exchange.getValue().getPayload()).isEqualTo(this.payload);
verify(this.delegate).requestStream(this.payload);
}
@Test
public void requestChannelWhenInterceptorCompletesThenDelegateSubscribed() {
when(this.interceptor.intercept(any(), any())).thenReturn(Mono.empty());
when(this.delegate.requestChannel(any())).thenReturn(this.payloadResult.flux());
PayloadInterceptorRSocket interceptor = new PayloadInterceptorRSocket(this.delegate,
Arrays.asList(this.interceptor), metadataMimeType, dataMimeType);
StepVerifier.create(interceptor.requestChannel(Flux.just(this.payload)))
.then(() -> this.payloadResult.assertSubscribers())
.then(() -> this.payloadResult.emit(this.payload))
.expectNext(this.payload)
.verifyComplete();
verify(this.interceptor).intercept(this.exchange.capture(), any());
assertThat(this.exchange.getValue().getPayload()).isEqualTo(this.payload);
verify(this.delegate).requestChannel(any());
}
@Test
public void requestChannelWhenInterceptorErrorsThenDelegateNotSubscribed() {
RuntimeException expected = new RuntimeException("Oops");
when(this.interceptor.intercept(any(), any())).thenReturn(Mono.error(expected));
PayloadInterceptorRSocket interceptor = new PayloadInterceptorRSocket(this.delegate,
Arrays.asList(this.interceptor), this.metadataMimeType, this.dataMimeType);
StepVerifier.create(interceptor.requestChannel(Flux.just(this.payload)))
.then(() -> this.payloadResult.assertNoSubscribers())
.verifyErrorSatisfies(e -> assertThat(e).isEqualTo(expected));
verify(this.interceptor).intercept(this.exchange.capture(), any());
assertThat(this.exchange.getValue().getPayload()).isEqualTo(this.payload);
}
@Test
public void requestChannelWhenSecurityContextThenDelegateContext() {
Mono<Payload> payload = Mono.just(this.payload);
TestingAuthenticationToken authentication = new TestingAuthenticationToken("user", "password");
when(this.interceptor.intercept(any(), any())).thenAnswer(withAuthenticated(authentication));
when(this.delegate.requestChannel(any())).thenReturn(this.payloadResult.flux());
RSocket assertAuthentication = new RSocketProxy(this.delegate) {
@Override
public Flux<Payload> requestChannel(Publisher<Payload> payload) {
return assertAuthentication(authentication)
.flatMapMany(a -> super.requestChannel(payload));
}
};
PayloadInterceptorRSocket interceptor = new PayloadInterceptorRSocket(assertAuthentication,
Arrays.asList(this.interceptor), metadataMimeType, dataMimeType);
StepVerifier.create(interceptor.requestChannel(payload))
.then(() -> this.payloadResult.assertSubscribers())
.then(() -> this.payloadResult.emit(this.payload))
.expectNext(this.payload)
.verifyComplete();
verify(this.interceptor).intercept(this.exchange.capture(), any());
assertThat(this.exchange.getValue().getPayload()).isEqualTo(this.payload);
verify(this.delegate).requestChannel(any());
}
@Test
public void metadataPushWhenInterceptorCompletesThenDelegateSubscribed() {
when(this.interceptor.intercept(any(), any())).thenReturn(Mono.empty());
when(this.delegate.metadataPush(any())).thenReturn(this.voidResult.mono());
PayloadInterceptorRSocket interceptor = new PayloadInterceptorRSocket(this.delegate,
Arrays.asList(this.interceptor), metadataMimeType, dataMimeType);
StepVerifier.create(interceptor.metadataPush(this.payload))
.then(() -> this.voidResult.assertWasSubscribed())
.verifyComplete();
verify(this.interceptor).intercept(this.exchange.capture(), any());
assertThat(this.exchange.getValue().getPayload()).isEqualTo(this.payload);
}
@Test
public void metadataPushWhenInterceptorErrorsThenDelegateNotSubscribed() {
RuntimeException expected = new RuntimeException("Oops");
when(this.interceptor.intercept(any(), any())).thenReturn(Mono.error(expected));
PayloadInterceptorRSocket interceptor = new PayloadInterceptorRSocket(this.delegate,
Arrays.asList(this.interceptor), metadataMimeType, dataMimeType);
StepVerifier.create(interceptor.metadataPush(this.payload))
.then(() -> this.voidResult.assertWasNotSubscribed())
.verifyErrorSatisfies(e -> assertThat(e).isEqualTo(expected));
verify(this.interceptor).intercept(this.exchange.capture(), any());
assertThat(this.exchange.getValue().getPayload()).isEqualTo(this.payload);
}
@Test
public void metadataPushWhenSecurityContextThenDelegateContext() {
TestingAuthenticationToken authentication = new TestingAuthenticationToken("user", "password");
when(this.interceptor.intercept(any(), any())).thenAnswer(withAuthenticated(authentication));
when(this.delegate.metadataPush(any())).thenReturn(this.voidResult.mono());
RSocket assertAuthentication = new RSocketProxy(this.delegate) {
@Override
public Mono<Void> metadataPush(Payload payload) {
return assertAuthentication(authentication)
.flatMap(a -> super.metadataPush(payload));
}
};
PayloadInterceptorRSocket interceptor = new PayloadInterceptorRSocket(assertAuthentication,
Arrays.asList(this.interceptor), metadataMimeType, dataMimeType);
StepVerifier.create(interceptor.metadataPush(this.payload))
.verifyComplete();
verify(this.interceptor).intercept(this.exchange.capture(), any());
assertThat(this.exchange.getValue().getPayload()).isEqualTo(this.payload);
verify(this.delegate).metadataPush(this.payload);
this.voidResult.assertWasSubscribed();
}
// multiple interceptors
@Test
public void fireAndForgetWhenInterceptorsCompleteThenDelegateInvoked() {
when(this.interceptor.intercept(any(), any())).thenAnswer(withChainNext());
when(this.interceptor2.intercept(any(), any())).thenAnswer(withChainNext());
when(this.delegate.fireAndForget(any())).thenReturn(this.voidResult.mono());
PayloadInterceptorRSocket interceptor = new PayloadInterceptorRSocket(this.delegate,
Arrays.asList(this.interceptor, this.interceptor2), metadataMimeType,
dataMimeType);
interceptor.fireAndForget(this.payload).block();
verify(this.interceptor).intercept(this.exchange.capture(), any());
assertThat(this.exchange.getValue().getPayload()).isEqualTo(this.payload);
this.voidResult.assertWasSubscribed();
}
@Test
public void fireAndForgetWhenInterceptorsMutatesPayloadThenDelegateInvoked() {
when(this.interceptor.intercept(any(), any())).thenAnswer(withChainNext());
when(this.interceptor2.intercept(any(), any())).thenAnswer(withChainNext());
when(this.delegate.fireAndForget(any())).thenReturn(this.voidResult.mono());
PayloadInterceptorRSocket interceptor = new PayloadInterceptorRSocket(this.delegate,
Arrays.asList(this.interceptor, this.interceptor2), metadataMimeType,
dataMimeType);
interceptor.fireAndForget(this.payload).block();
verify(this.interceptor).intercept(this.exchange.capture(), any());
assertThat(this.exchange.getValue().getPayload()).isEqualTo(this.payload);
verify(this.interceptor2).intercept(any(), any());
verify(this.delegate).fireAndForget(eq(this.payload));
this.voidResult.assertWasSubscribed();
}
@Test
public void fireAndForgetWhenInterceptor1ErrorsThenInterceptor2AndDelegateNotInvoked() {
RuntimeException expected = new RuntimeException("Oops");
when(this.interceptor.intercept(any(), any())).thenReturn(Mono.error(expected));
PayloadInterceptorRSocket interceptor = new PayloadInterceptorRSocket(this.delegate,
Arrays.asList(this.interceptor, this.interceptor2), metadataMimeType,
dataMimeType);
assertThatCode(() -> interceptor.fireAndForget(this.payload).block()).isEqualTo(expected);
verify(this.interceptor).intercept(this.exchange.capture(), any());
assertThat(this.exchange.getValue().getPayload()).isEqualTo(this.payload);
verifyZeroInteractions(this.interceptor2);
this.voidResult.assertWasNotSubscribed();
}
@Test
public void fireAndForgetWhenInterceptor2ErrorsThenInterceptor2AndDelegateNotInvoked() {
RuntimeException expected = new RuntimeException("Oops");
when(this.interceptor.intercept(any(), any())).thenAnswer(withChainNext());
when(this.interceptor2.intercept(any(), any())).thenReturn(Mono.error(expected));
PayloadInterceptorRSocket interceptor = new PayloadInterceptorRSocket(this.delegate,
Arrays.asList(this.interceptor, this.interceptor2), metadataMimeType,
dataMimeType);
assertThatCode(() -> interceptor.fireAndForget(this.payload).block()).isEqualTo(expected);
verify(this.interceptor).intercept(this.exchange.capture(), any());
assertThat(this.exchange.getValue().getPayload()).isEqualTo(this.payload);
verify(this.interceptor2).intercept(any(), any());
this.voidResult.assertWasNotSubscribed();
}
private Mono<Authentication> assertAuthentication(Authentication authentication) {
return ReactiveSecurityContextHolder.getContext()
.map(SecurityContext::getAuthentication)
.doOnNext(a -> assertThat(a).isEqualTo(authentication));
}
private Answer<Object> withAuthenticated(Authentication authentication) {
return invocation -> {
PayloadInterceptorChain c = (PayloadInterceptorChain) invocation.getArguments()[1];
return c.next(new DefaultPayloadExchange(PayloadExchangeType.REQUEST_CHANNEL, this.payload, this.metadataMimeType,
this.dataMimeType))
.subscriberContext(ReactiveSecurityContextHolder.withAuthentication(authentication));
};
}
private static Answer<Mono<Void>> withChainNext() {
return invocation -> {
PayloadExchange exchange = (PayloadExchange) invocation.getArguments()[0];
PayloadInterceptorChain chain = (PayloadInterceptorChain) invocation.getArguments()[1];
return chain.next(exchange);
};
}
}

View File

@ -0,0 +1,121 @@
/*
* 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.interceptor;
import io.rsocket.ConnectionSetupPayload;
import io.rsocket.Payload;
import io.rsocket.RSocket;
import io.rsocket.SocketAcceptor;
import io.rsocket.metadata.WellKnownMimeType;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import org.springframework.http.MediaType;
import reactor.core.publisher.Mono;
import java.util.Arrays;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* @author Rob Winch
*/
@RunWith(MockitoJUnitRunner.class)
public class PayloadSocketAcceptorInterceptorTests {
@Mock
private PayloadInterceptor interceptor;
@Mock
private SocketAcceptor socketAcceptor;
@Mock
private ConnectionSetupPayload setupPayload;
@Mock
private RSocket rSocket;
@Mock
private Payload payload;
private List<PayloadInterceptor> interceptors;
private PayloadSocketAcceptorInterceptor acceptorInterceptor;
@Before
public void setup() {
this.interceptors = Arrays.asList(this.interceptor);
this.acceptorInterceptor = new PayloadSocketAcceptorInterceptor(this.interceptors);
}
@Test
public void applyWhenDefaultMetadataMimeTypeThenDefaulted() {
when(this.setupPayload.dataMimeType()).thenReturn(MediaType.APPLICATION_JSON_VALUE);
PayloadExchange exchange = captureExchange();
assertThat(exchange.getMetadataMimeType().toString()).isEqualTo(WellKnownMimeType.MESSAGE_RSOCKET_COMPOSITE_METADATA.getString());
assertThat(exchange.getDataMimeType()).isEqualTo(MediaType.APPLICATION_JSON);
}
@Test
public void acceptWhenDefaultMetadataMimeTypeOverrideThenDefaulted() {
this.acceptorInterceptor.setDefaultMetadataMimeType(MediaType.APPLICATION_JSON);
when(this.setupPayload.dataMimeType()).thenReturn(MediaType.APPLICATION_JSON_VALUE);
PayloadExchange exchange = captureExchange();
assertThat(exchange.getMetadataMimeType()).isEqualTo(MediaType.APPLICATION_JSON);
assertThat(exchange.getDataMimeType()).isEqualTo(MediaType.APPLICATION_JSON);
}
@Test
public void acceptWhenDefaultDataMimeTypeThenDefaulted() {
this.acceptorInterceptor.setDefaultDataMimeType(MediaType.APPLICATION_JSON);
PayloadExchange exchange = captureExchange();
assertThat(exchange.getMetadataMimeType().toString()).isEqualTo(WellKnownMimeType.MESSAGE_RSOCKET_COMPOSITE_METADATA.getString());
assertThat(exchange.getDataMimeType()).isEqualTo(MediaType.APPLICATION_JSON);
}
private PayloadExchange captureExchange() {
when(this.socketAcceptor.accept(any(), any())).thenReturn(Mono.just(this.rSocket));
when(this.interceptor.intercept(any(), any())).thenReturn(Mono.empty());
SocketAcceptor wrappedAcceptor = this.acceptorInterceptor.apply(this.socketAcceptor);
RSocket result = wrappedAcceptor.accept(this.setupPayload, this.rSocket).block();
assertThat(result).isInstanceOf(PayloadInterceptorRSocket.class);
when(this.rSocket.fireAndForget(any())).thenReturn(Mono.empty());
result.fireAndForget(this.payload).block();
ArgumentCaptor<PayloadExchange> exchangeArg =
ArgumentCaptor.forClass(PayloadExchange.class);
verify(this.interceptor, times(2)).intercept(exchangeArg.capture(), any());
return exchangeArg.getValue();
}
}

View File

@ -0,0 +1,160 @@
/*
* 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.interceptor;
import io.rsocket.ConnectionSetupPayload;
import io.rsocket.Payload;
import io.rsocket.RSocket;
import io.rsocket.SocketAcceptor;
import io.rsocket.metadata.WellKnownMimeType;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import org.springframework.http.MediaType;
import reactor.core.publisher.Mono;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatCode;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* @author Rob Winch
*/
@RunWith(MockitoJUnitRunner.class)
public class PayloadSocketAcceptorTests {
private PayloadSocketAcceptor acceptor;
private List<PayloadInterceptor> interceptors;
@Mock
private SocketAcceptor delegate;
@Mock
private PayloadInterceptor interceptor;
@Mock
private ConnectionSetupPayload setupPayload;
@Mock
private RSocket rSocket;
@Mock
private Payload payload;
@Before
public void setup() {
this.interceptors = Arrays.asList(this.interceptor);
this.acceptor = new PayloadSocketAcceptor(this.delegate, this.interceptors);
}
@Test
public void constructorWhenNullDelegateThenException() {
this.delegate = null;
assertThatCode(() -> new PayloadSocketAcceptor(this.delegate, this.interceptors));
}
@Test
public void constructorWhenNullInterceptorsThenException() {
this.interceptors = null;
assertThatCode(() -> new PayloadSocketAcceptor(this.delegate, this.interceptors));
}
@Test
public void constructorWhenEmptyInterceptorsThenException() {
this.interceptors = Collections.emptyList();
assertThatCode(() -> new PayloadSocketAcceptor(this.delegate, this.interceptors));
}
@Test
public void acceptWhenDataMimeTypeNullThenException() {
assertThatCode(() -> this.acceptor.accept(this.setupPayload, this.rSocket)
.block()).isInstanceOf(IllegalArgumentException.class);
}
@Test
public void acceptWhenDefaultMetadataMimeTypeThenDefaulted() {
when(this.setupPayload.dataMimeType()).thenReturn(MediaType.APPLICATION_JSON_VALUE);
PayloadExchange exchange = captureExchange();
assertThat(exchange.getMetadataMimeType().toString())
.isEqualTo(WellKnownMimeType.MESSAGE_RSOCKET_COMPOSITE_METADATA.getString());
assertThat(exchange.getDataMimeType()).isEqualTo(MediaType.APPLICATION_JSON);
}
@Test
public void acceptWhenDefaultMetadataMimeTypeOverrideThenDefaulted() {
this.acceptor.setDefaultMetadataMimeType(MediaType.APPLICATION_JSON);
when(this.setupPayload.dataMimeType()).thenReturn(MediaType.APPLICATION_JSON_VALUE);
PayloadExchange exchange = captureExchange();
assertThat(exchange.getMetadataMimeType()).isEqualTo(MediaType.APPLICATION_JSON);
assertThat(exchange.getDataMimeType()).isEqualTo(MediaType.APPLICATION_JSON);
}
@Test
public void acceptWhenDefaultDataMimeTypeThenDefaulted() {
this.acceptor.setDefaultDataMimeType(MediaType.APPLICATION_JSON);
PayloadExchange exchange = captureExchange();
assertThat(exchange.getMetadataMimeType()
.toString()).isEqualTo(WellKnownMimeType.MESSAGE_RSOCKET_COMPOSITE_METADATA.getString());
assertThat(exchange.getDataMimeType()).isEqualTo(MediaType.APPLICATION_JSON);
}
@Test
public void acceptWhenExplicitMimeTypeThenThenOverrideDefault() {
when(this.setupPayload.metadataMimeType()).thenReturn(MediaType.TEXT_PLAIN_VALUE);
when(this.setupPayload.dataMimeType()).thenReturn(MediaType.APPLICATION_JSON_VALUE);
PayloadExchange exchange = captureExchange();
assertThat(exchange.getMetadataMimeType()).isEqualTo(MediaType.TEXT_PLAIN);
assertThat(exchange.getDataMimeType()).isEqualTo(MediaType.APPLICATION_JSON);
}
private PayloadExchange captureExchange() {
when(this.delegate.accept(any(), any())).thenReturn(Mono.just(this.rSocket));
when(this.interceptor.intercept(any(), any())).thenReturn(Mono.empty());
RSocket result = this.acceptor.accept(this.setupPayload, this.rSocket).block();
assertThat(result).isInstanceOf(PayloadInterceptorRSocket.class);
when(this.rSocket.fireAndForget(any())).thenReturn(Mono.empty());
result.fireAndForget(this.payload).block();
ArgumentCaptor<PayloadExchange> exchangeArg =
ArgumentCaptor.forClass(PayloadExchange.class);
verify(this.interceptor, times(2)).intercept(exchangeArg.capture(), any());
return exchangeArg.getValue();
}
}

View File

@ -0,0 +1,108 @@
/*
* 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.interceptor.authorization;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.ReactiveAuthorizationManager;
import org.springframework.security.rsocket.interceptor.PayloadExchange;
import org.springframework.security.rsocket.util.PayloadExchangeAuthorizationContext;
import org.springframework.security.rsocket.util.PayloadExchangeMatcher;
import org.springframework.security.rsocket.util.PayloadExchangeMatcherEntry;
import org.springframework.security.rsocket.util.PayloadExchangeMatchers;
import reactor.core.publisher.Mono;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
/**
* @author Rob Winch
*/
@RunWith(MockitoJUnitRunner.class)
public class PayloadExchangeMatcherReactiveAuthorizationManagerTest {
@Mock
private ReactiveAuthorizationManager<PayloadExchangeAuthorizationContext> authz;
@Mock
private ReactiveAuthorizationManager<PayloadExchangeAuthorizationContext> authz2;
@Mock
private PayloadExchange exchange;
@Test
public void checkWhenGrantedThenGranted() {
AuthorizationDecision expected = new AuthorizationDecision(true);
when(this.authz.check(any(), any())).thenReturn(Mono.just(
expected));
PayloadExchangeMatcherReactiveAuthorizationManager manager =
PayloadExchangeMatcherReactiveAuthorizationManager.builder()
.add(new PayloadExchangeMatcherEntry<>(PayloadExchangeMatchers.anyExchange(), this.authz))
.build();
assertThat(manager.check(Mono.empty(), this.exchange).block())
.isEqualTo(expected);
}
@Test
public void checkWhenDeniedThenDenied() {
AuthorizationDecision expected = new AuthorizationDecision(false);
when(this.authz.check(any(), any())).thenReturn(Mono.just(
expected));
PayloadExchangeMatcherReactiveAuthorizationManager manager =
PayloadExchangeMatcherReactiveAuthorizationManager.builder()
.add(new PayloadExchangeMatcherEntry<>(PayloadExchangeMatchers.anyExchange(), this.authz))
.build();
assertThat(manager.check(Mono.empty(), this.exchange).block())
.isEqualTo(expected);
}
@Test
public void checkWhenFirstMatchThenSecondUsed() {
AuthorizationDecision expected = new AuthorizationDecision(true);
when(this.authz.check(any(), any())).thenReturn(Mono.just(
expected));
PayloadExchangeMatcherReactiveAuthorizationManager manager =
PayloadExchangeMatcherReactiveAuthorizationManager.builder()
.add(new PayloadExchangeMatcherEntry<>(PayloadExchangeMatchers.anyExchange(), this.authz))
.add(new PayloadExchangeMatcherEntry<>(e -> PayloadExchangeMatcher.MatchResult.notMatch(), this.authz2))
.build();
assertThat(manager.check(Mono.empty(), this.exchange).block())
.isEqualTo(expected);
}
@Test
public void checkWhenSecondMatchThenSecondUsed() {
AuthorizationDecision expected = new AuthorizationDecision(true);
when(this.authz2.check(any(), any())).thenReturn(Mono.just(
expected));
PayloadExchangeMatcherReactiveAuthorizationManager manager =
PayloadExchangeMatcherReactiveAuthorizationManager.builder()
.add(new PayloadExchangeMatcherEntry<>(e -> PayloadExchangeMatcher.MatchResult.notMatch(), this.authz))
.add(new PayloadExchangeMatcherEntry<>(PayloadExchangeMatchers.anyExchange(), this.authz2))
.build();
assertThat(manager.check(Mono.empty(), this.exchange).block())
.isEqualTo(expected);
}
}

View File

@ -0,0 +1,54 @@
/*
* 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.metadata;
import org.junit.Test;
import org.springframework.core.ResolvableType;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
import org.springframework.util.MimeType;
import reactor.core.publisher.Mono;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Rob Winch
*/
public class BasicAuthenticationDecoderTests {
@Test
public void basicAuthenticationWhenEncodedThenDecodes() {
BasicAuthenticationEncoder encoder = new BasicAuthenticationEncoder();
BasicAuthenticationDecoder decoder = new BasicAuthenticationDecoder();
UsernamePasswordMetadata expectedCredentials =
new UsernamePasswordMetadata("rob", "password");
DefaultDataBufferFactory factory = new DefaultDataBufferFactory();
ResolvableType elementType = ResolvableType
.forClass(UsernamePasswordMetadata.class);
MimeType mimeType = UsernamePasswordMetadata.BASIC_AUTHENTICATION_MIME_TYPE;
Map<String, Object> hints = null;
DataBuffer dataBuffer = encoder.encodeValue(expectedCredentials, factory,
elementType, mimeType, hints);
UsernamePasswordMetadata actualCredentials = decoder
.decodeToMono(Mono.just(dataBuffer), elementType, mimeType, hints).block();
assertThat(actualCredentials).isEqualToComparingFieldByField(expectedCredentials);
}
}

View File

@ -0,0 +1,116 @@
/*
* 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.util;
import io.rsocket.Payload;
import io.rsocket.metadata.WellKnownMimeType;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import org.springframework.http.MediaType;
import org.springframework.messaging.rsocket.MetadataExtractor;
import org.springframework.security.rsocket.interceptor.DefaultPayloadExchange;
import org.springframework.security.rsocket.interceptor.PayloadExchange;
import org.springframework.security.rsocket.interceptor.PayloadExchangeType;
import org.springframework.util.MimeType;
import org.springframework.util.MimeTypeUtils;
import org.springframework.util.RouteMatcher;
import java.util.Collections;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.when;
/**
* @author Rob Winch
*/
@RunWith(MockitoJUnitRunner.class)
public class RoutePayloadExchangeMatcherTests {
static final MimeType COMPOSITE_METADATA = MimeTypeUtils.parseMimeType(
WellKnownMimeType.MESSAGE_RSOCKET_COMPOSITE_METADATA.getString());
@Mock
private MetadataExtractor metadataExtractor;
@Mock
private RouteMatcher routeMatcher;
private PayloadExchange exchange;
@Mock
private Payload payload;
@Mock
private RouteMatcher.Route route;
private String pattern;
private RoutePayloadExchangeMatcher matcher;
@Before
public void setup() {
this.pattern = "a.b";
this.matcher = new RoutePayloadExchangeMatcher(this.metadataExtractor, this.routeMatcher, this.pattern);
this.exchange = new DefaultPayloadExchange(PayloadExchangeType.REQUEST_CHANNEL, this.payload, COMPOSITE_METADATA,
MediaType.APPLICATION_JSON);
}
@Test
public void matchesWhenNoRouteThenNotMatch() {
when(this.metadataExtractor.extract(any(), any()))
.thenReturn(Collections.emptyMap());
PayloadExchangeMatcher.MatchResult result = this.matcher.matches(this.exchange).block();
assertThat(result.isMatch()).isFalse();
}
@Test
public void matchesWhenNotMatchThenNotMatch() {
String route = "route";
when(this.metadataExtractor.extract(any(), any()))
.thenReturn(Collections.singletonMap(MetadataExtractor.ROUTE_KEY, route));
PayloadExchangeMatcher.MatchResult result = this.matcher.matches(this.exchange).block();
assertThat(result.isMatch()).isFalse();
}
@Test
public void matchesWhenMatchAndNoVariablesThenMatch() {
String route = "route";
when(this.metadataExtractor.extract(any(), any()))
.thenReturn(Collections.singletonMap(MetadataExtractor.ROUTE_KEY, route));
when(this.routeMatcher.parseRoute(any())).thenReturn(this.route);
when(this.routeMatcher.matchAndExtract(any(), any())).thenReturn(Collections.emptyMap());
PayloadExchangeMatcher.MatchResult result = this.matcher.matches(this.exchange).block();
assertThat(result.isMatch()).isTrue();
}
@Test
public void matchesWhenMatchAndVariablesThenMatchAndVariables() {
String route = "route";
Map<String, String> variables = Collections.singletonMap("a", "b");
when(this.metadataExtractor.extract(any(), any()))
.thenReturn(Collections.singletonMap(MetadataExtractor.ROUTE_KEY, route));
when(this.routeMatcher.parseRoute(any())).thenReturn(this.route);
when(this.routeMatcher.matchAndExtract(any(), any())).thenReturn(variables);
PayloadExchangeMatcher.MatchResult result = this.matcher.matches(this.exchange).block();
assertThat(result.isMatch()).isTrue();
assertThat(result.getVariables()).containsAllEntriesOf(variables);
}
}