Add RSocket Support
Fixes gh-7360
This commit is contained in:
+96
@@ -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 + "]";
|
||||
}
|
||||
}
|
||||
+70
@@ -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;
|
||||
}
|
||||
}
|
||||
+36
@@ -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();
|
||||
}
|
||||
+80
@@ -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;
|
||||
}
|
||||
}
|
||||
+38
@@ -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);
|
||||
}
|
||||
+34
@@ -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);
|
||||
}
|
||||
+140
@@ -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 + "]";
|
||||
}
|
||||
}
|
||||
+99
@@ -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;
|
||||
}
|
||||
}
|
||||
+66
@@ -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;
|
||||
}
|
||||
}
|
||||
+83
@@ -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));
|
||||
}
|
||||
}
|
||||
+74
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
+60
@@ -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;
|
||||
}
|
||||
}
|
||||
+54
@@ -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();
|
||||
}
|
||||
}
|
||||
+30
@@ -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);
|
||||
}
|
||||
+53
@@ -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));
|
||||
}
|
||||
}
|
||||
+82
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+76
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
+76
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+47
@@ -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;
|
||||
}
|
||||
}
|
||||
+55
@@ -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;
|
||||
}
|
||||
}
|
||||
+48
@@ -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);
|
||||
}
|
||||
}
|
||||
+89
@@ -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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
+38
@@ -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;
|
||||
}
|
||||
}
|
||||
+57
@@ -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() {}
|
||||
}
|
||||
+61
@@ -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());
|
||||
}
|
||||
}
|
||||
+108
@@ -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);
|
||||
}
|
||||
}
|
||||
+45
@@ -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;
|
||||
}
|
||||
}
|
||||
+148
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
+118
@@ -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();
|
||||
}
|
||||
}
|
||||
+509
@@ -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);
|
||||
};
|
||||
}
|
||||
}
|
||||
+121
@@ -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();
|
||||
}
|
||||
}
|
||||
+160
@@ -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();
|
||||
}
|
||||
}
|
||||
+108
@@ -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);
|
||||
}
|
||||
}
|
||||
+54
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
+116
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user