Enable Null checking in spring-security-rsocket via JSpecify

Closes gh-16882
This commit is contained in:
Rob Winch 2025-08-30 20:04:32 -05:00
parent a4a4908d71
commit 1216ee598f
No known key found for this signature in database
16 changed files with 176 additions and 21 deletions

View File

@ -1,3 +1,7 @@
plugins {
id 'security-nullability'
}
apply plugin: 'io.spring.convention.spring-module'
dependencies {

View File

@ -0,0 +1,23 @@
/*
* Copyright 2004-present 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.
*/
/**
* Spring Security RSocket APIs.
*/
@NullMarked
package org.springframework.security.rsocket.api;
import org.jspecify.annotations.NullMarked;

View File

@ -24,6 +24,7 @@ import io.netty.buffer.ByteBufAllocator;
import io.rsocket.metadata.AuthMetadataCodec;
import io.rsocket.metadata.WellKnownAuthType;
import io.rsocket.metadata.WellKnownMimeType;
import org.jspecify.annotations.Nullable;
import reactor.core.publisher.Mono;
import org.springframework.core.codec.ByteArrayDecoder;
@ -66,7 +67,7 @@ public class AuthenticationPayloadExchangeConverter implements PayloadExchangeAu
.flatMap((metadata) -> Mono.justOrEmpty(authentication(metadata)));
}
private Authentication authentication(Map<String, Object> metadata) {
private @Nullable Authentication authentication(Map<String, Object> metadata) {
byte[] authenticationMetadata = (byte[]) metadata.get("authentication");
if (authenticationMetadata == null) {
return null;

View File

@ -0,0 +1,23 @@
/*
* Copyright 2004-present 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.
*/
/**
* Spring Security RSocket Authentication integration.
*/
@NullMarked
package org.springframework.security.rsocket.authentication;
import org.jspecify.annotations.NullMarked;

View File

@ -0,0 +1,23 @@
/*
* Copyright 2004-present 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.
*/
/**
* Spring Security RSocket authorization integration.
*/
@NullMarked
package org.springframework.security.rsocket.authorization;
import org.jspecify.annotations.NullMarked;

View File

@ -19,6 +19,7 @@ package org.springframework.security.rsocket.core;
import java.util.List;
import java.util.ListIterator;
import org.jspecify.annotations.Nullable;
import reactor.core.publisher.Mono;
import reactor.util.context.Context;
@ -41,11 +42,11 @@ import org.springframework.security.rsocket.api.PayloadInterceptorChain;
*/
class ContextPayloadInterceptorChain implements PayloadInterceptorChain {
private final PayloadInterceptor currentInterceptor;
private final @Nullable PayloadInterceptor currentInterceptor;
private final ContextPayloadInterceptorChain next;
private final @Nullable ContextPayloadInterceptorChain next;
private Context context;
private @Nullable Context context;
ContextPayloadInterceptorChain(List<PayloadInterceptor> interceptors) {
if (interceptors == null) {
@ -68,18 +69,20 @@ class ContextPayloadInterceptorChain implements PayloadInterceptorChain {
return interceptor;
}
private ContextPayloadInterceptorChain(PayloadInterceptor currentInterceptor, ContextPayloadInterceptorChain next) {
private ContextPayloadInterceptorChain(@Nullable PayloadInterceptor currentInterceptor,
@Nullable ContextPayloadInterceptorChain next) {
this.currentInterceptor = currentInterceptor;
this.next = next;
}
@Override
@SuppressWarnings("NullAway") // Dataflow analysis limitation
public Mono<Void> next(PayloadExchange exchange) {
return Mono.defer(() -> shouldIntercept() ? this.currentInterceptor.intercept(exchange, this.next)
: Mono.deferContextual(Mono::just).cast(Context.class).doOnNext((c) -> this.context = c).then());
}
Context getContext() {
@Nullable Context getContext() {
if (this.next == null) {
return this.context;
}

View File

@ -28,6 +28,7 @@ import reactor.util.context.Context;
import org.springframework.security.rsocket.api.PayloadExchangeType;
import org.springframework.security.rsocket.api.PayloadInterceptor;
import org.springframework.util.Assert;
import org.springframework.util.MimeType;
/**
@ -91,6 +92,7 @@ class PayloadInterceptorRSocket extends RSocketProxy {
public Flux<Payload> requestChannel(Publisher<Payload> payloads) {
return Flux.from(payloads).switchOnFirst((signal, innerFlux) -> {
Payload firstPayload = signal.get();
Assert.notNull(firstPayload, "payload cannot be null");
return intercept(PayloadExchangeType.REQUEST_CHANNEL, firstPayload)
.flatMapMany((context) -> innerFlux.index()
.concatMap((tuple) -> justOrIntercept(tuple.getT1(), tuple.getT2()))

View File

@ -23,10 +23,10 @@ import io.rsocket.Payload;
import io.rsocket.RSocket;
import io.rsocket.SocketAcceptor;
import io.rsocket.metadata.WellKnownMimeType;
import org.jspecify.annotations.Nullable;
import reactor.core.publisher.Mono;
import reactor.util.context.Context;
import org.springframework.lang.Nullable;
import org.springframework.security.rsocket.api.PayloadExchangeType;
import org.springframework.security.rsocket.api.PayloadInterceptor;
import org.springframework.util.Assert;
@ -44,8 +44,7 @@ class PayloadSocketAcceptor implements SocketAcceptor {
private final List<PayloadInterceptor> interceptors;
@Nullable
private MimeType defaultDataMimeType;
private @Nullable MimeType defaultDataMimeType;
private MimeType defaultMetadataMimeType = MimeTypeUtils
.parseMimeType(WellKnownMimeType.MESSAGE_RSOCKET_COMPOSITE_METADATA.getString());
@ -85,7 +84,7 @@ class PayloadSocketAcceptor implements SocketAcceptor {
});
}
private MimeType parseMimeType(String str, MimeType defaultMimeType) {
private @Nullable MimeType parseMimeType(String str, @Nullable MimeType defaultMimeType) {
return StringUtils.hasText(str) ? MimeTypeUtils.parseMimeType(str) : defaultMimeType;
}

View File

@ -0,0 +1,23 @@
/*
* Copyright 2004-present 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.
*/
/**
* Spring Security RSocket core integration.
*/
@NullMarked
package org.springframework.security.rsocket.core;
import org.jspecify.annotations.NullMarked;

View File

@ -18,6 +18,7 @@ package org.springframework.security.rsocket.metadata;
import java.util.Map;
import org.jspecify.annotations.Nullable;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@ -44,7 +45,7 @@ public class BasicAuthenticationDecoder extends AbstractDecoder<UsernamePassword
@Override
public Flux<UsernamePasswordMetadata> decode(Publisher<DataBuffer> input, ResolvableType elementType,
MimeType mimeType, Map<String, Object> hints) {
@Nullable MimeType mimeType, @Nullable Map<String, Object> hints) {
return Flux.from(input).map(DataBuffer::asByteBuffer).map((byteBuffer) -> {
byte[] sizeBytes = new byte[4];
byteBuffer.get(sizeBytes);
@ -61,7 +62,7 @@ public class BasicAuthenticationDecoder extends AbstractDecoder<UsernamePassword
@Override
public Mono<UsernamePasswordMetadata> decodeToMono(Publisher<DataBuffer> input, ResolvableType elementType,
MimeType mimeType, Map<String, Object> hints) {
@Nullable MimeType mimeType, @Nullable Map<String, Object> hints) {
return Mono.from(input).map(DataBuffer::asByteBuffer).map((byteBuffer) -> {
int usernameSize = byteBuffer.getInt();
byte[] usernameBytes = new byte[usernameSize];

View File

@ -20,6 +20,7 @@ import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import org.jspecify.annotations.Nullable;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
@ -47,14 +48,15 @@ public class BasicAuthenticationEncoder extends AbstractEncoder<UsernamePassword
@Override
public Flux<DataBuffer> encode(Publisher<? extends UsernamePasswordMetadata> inputStream,
DataBufferFactory bufferFactory, ResolvableType elementType, MimeType mimeType, Map<String, Object> hints) {
DataBufferFactory bufferFactory, ResolvableType elementType, @Nullable MimeType mimeType,
@Nullable 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) {
ResolvableType valueType, @Nullable MimeType mimeType, @Nullable Map<String, Object> hints) {
String username = credentials.getUsername();
String password = credentials.getPassword();
byte[] usernameBytes = username.getBytes(StandardCharsets.UTF_8);

View File

@ -21,6 +21,7 @@ import java.util.Map;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
import io.rsocket.metadata.AuthMetadataCodec;
import org.jspecify.annotations.Nullable;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
@ -53,14 +54,15 @@ public class BearerTokenAuthenticationEncoder extends AbstractEncoder<BearerToke
@Override
public Flux<DataBuffer> encode(Publisher<? extends BearerTokenMetadata> inputStream,
DataBufferFactory bufferFactory, ResolvableType elementType, MimeType mimeType, Map<String, Object> hints) {
DataBufferFactory bufferFactory, ResolvableType elementType, @Nullable MimeType mimeType,
@Nullable Map<String, Object> hints) {
return Flux.from(inputStream)
.map((credentials) -> encodeValue(credentials, bufferFactory, elementType, mimeType, hints));
}
@Override
public DataBuffer encodeValue(BearerTokenMetadata credentials, DataBufferFactory bufferFactory,
ResolvableType valueType, MimeType mimeType, Map<String, Object> hints) {
ResolvableType valueType, @Nullable MimeType mimeType, @Nullable Map<String, Object> hints) {
String token = credentials.getToken();
NettyDataBufferFactory factory = nettyFactory(bufferFactory);
ByteBufAllocator allocator = factory.getByteBufAllocator();

View File

@ -21,6 +21,7 @@ import java.util.Map;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
import io.rsocket.metadata.AuthMetadataCodec;
import org.jspecify.annotations.Nullable;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
@ -53,14 +54,15 @@ public class SimpleAuthenticationEncoder extends AbstractEncoder<UsernamePasswor
@Override
public Flux<DataBuffer> encode(Publisher<? extends UsernamePasswordMetadata> inputStream,
DataBufferFactory bufferFactory, ResolvableType elementType, MimeType mimeType, Map<String, Object> hints) {
DataBufferFactory bufferFactory, ResolvableType elementType, @Nullable MimeType mimeType,
@Nullable 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) {
ResolvableType valueType, @Nullable MimeType mimeType, @Nullable Map<String, Object> hints) {
String username = credentials.getUsername();
String password = credentials.getPassword();
NettyDataBufferFactory factory = nettyFactory(bufferFactory);

View File

@ -0,0 +1,23 @@
/*
* Copyright 2004-present 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.
*/
/**
* Spring Security RSocket metadata integration.
*/
@NullMarked
package org.springframework.security.rsocket.metadata;
import org.jspecify.annotations.NullMarked;

View File

@ -20,6 +20,7 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import org.jspecify.annotations.Nullable;
import reactor.core.publisher.Mono;
import org.springframework.security.rsocket.api.PayloadExchange;
@ -46,9 +47,9 @@ public interface PayloadExchangeMatcher {
private final boolean match;
private final Map<String, Object> variables;
private final @Nullable Map<String, Object> variables;
private MatchResult(boolean match, Map<String, Object> variables) {
private MatchResult(boolean match, @Nullable Map<String, Object> variables) {
this.match = match;
this.variables = variables;
}
@ -61,7 +62,7 @@ public interface PayloadExchangeMatcher {
* Gets potential variables and their values
* @return
*/
public Map<String, Object> getVariables() {
public @Nullable Map<String, Object> getVariables() {
return this.variables;
}

View File

@ -0,0 +1,23 @@
/*
* Copyright 2004-present 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.
*/
/**
* Spring Security RSocket matching APIs.
*/
@NullMarked
package org.springframework.security.rsocket.util.matcher;
import org.jspecify.annotations.NullMarked;