From b864be92d8d98f1b207e32b3a26d310ec295fa98 Mon Sep 17 00:00:00 2001 From: Rob Winch <362503+rwinch@users.noreply.github.com> Date: Mon, 13 Oct 2025 10:48:10 -0500 Subject: [PATCH] Update to Reactor 2025.0.0-SNAPSHOT To prepare for the release we should update to Reactor 2025.0.0-SNAPSHOT to fix any issues that are present. Closes gh-18041 --- .../reactive/InMemoryReactiveOneTimeTokenService.java | 1 + .../ObservationReactiveAuthorizationManager.java | 3 ++- .../method/AuthorizationAdvisorProxyFactory.java | 6 ++++-- ...orizationManagerAfterReactiveMethodInterceptor.java | 10 +++++++++- .../method/ReactiveAuthenticationUtils.java | 3 ++- .../method/ReactiveMethodInvocationUtils.java | 2 +- .../core/session/InMemoryReactiveSessionRegistry.java | 3 ++- gradle/libs.versions.toml | 2 +- .../AuthenticationPrincipalArgumentResolver.java | 3 ++- .../authorization/AuthorizationPayloadInterceptor.java | 4 ++-- ...oadExchangeMatcherReactiveAuthorizationManager.java | 3 ++- .../AuthenticationPrincipalArgumentResolver.java | 3 ++- .../web/server/authentication/SwitchUserWebFilter.java | 6 ++++-- ...faultServerGenerateOneTimeTokenRequestResolver.java | 1 + .../server/authorization/AuthorizationWebFilter.java | 3 ++- .../context/SecurityContextServerWebExchange.java | 4 ++-- .../csrf/ServerCsrfTokenRequestAttributeHandler.java | 3 ++- .../csrf/WebSessionServerCsrfTokenRepository.java | 3 ++- 18 files changed, 43 insertions(+), 20 deletions(-) diff --git a/core/src/main/java/org/springframework/security/authentication/ott/reactive/InMemoryReactiveOneTimeTokenService.java b/core/src/main/java/org/springframework/security/authentication/ott/reactive/InMemoryReactiveOneTimeTokenService.java index bfd26083e1..a465cb03b1 100644 --- a/core/src/main/java/org/springframework/security/authentication/ott/reactive/InMemoryReactiveOneTimeTokenService.java +++ b/core/src/main/java/org/springframework/security/authentication/ott/reactive/InMemoryReactiveOneTimeTokenService.java @@ -43,6 +43,7 @@ public final class InMemoryReactiveOneTimeTokenService implements ReactiveOneTim } @Override + @SuppressWarnings("NullAway") // https://github.com/uber/NullAway/issues/1290 public Mono consume(OneTimeTokenAuthenticationToken authenticationToken) { return Mono.just(authenticationToken).mapNotNull(this.oneTimeTokenService::consume); } diff --git a/core/src/main/java/org/springframework/security/authorization/ObservationReactiveAuthorizationManager.java b/core/src/main/java/org/springframework/security/authorization/ObservationReactiveAuthorizationManager.java index 5d11015964..0470726e1a 100644 --- a/core/src/main/java/org/springframework/security/authorization/ObservationReactiveAuthorizationManager.java +++ b/core/src/main/java/org/springframework/security/authorization/ObservationReactiveAuthorizationManager.java @@ -58,9 +58,10 @@ public final class ObservationReactiveAuthorizationManager } @Override + @SuppressWarnings("NullAway") // https://github.com/uber/NullAway/issues/1290 public Mono authorize(Mono authentication, T object) { AuthorizationObservationContext context = new AuthorizationObservationContext<>(object); - Mono wrapped = authentication.map((auth) -> { + Mono wrapped = authentication.mapNotNull((auth) -> { context.setAuthentication(auth); return context.getAuthentication(); }); diff --git a/core/src/main/java/org/springframework/security/authorization/method/AuthorizationAdvisorProxyFactory.java b/core/src/main/java/org/springframework/security/authorization/method/AuthorizationAdvisorProxyFactory.java index 418c6507d7..248e687421 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/AuthorizationAdvisorProxyFactory.java +++ b/core/src/main/java/org/springframework/security/authorization/method/AuthorizationAdvisorProxyFactory.java @@ -588,12 +588,14 @@ public final class AuthorizationAdvisorProxyFactory implements AuthorizationProx return null; } + @SuppressWarnings("NullAway") // https://github.com/uber/NullAway/issues/1290 private Mono proxyMono(AuthorizationProxyFactory proxyFactory, Mono mono) { - return mono.map(proxyFactory::proxy); + return mono.mapNotNull(proxyFactory::proxy); } + @SuppressWarnings("NullAway") // https://github.com/uber/NullAway/issues/1290 private Flux proxyFlux(AuthorizationProxyFactory proxyFactory, Flux flux) { - return flux.map(proxyFactory::proxy); + return flux.mapNotNull(proxyFactory::proxy); } } diff --git a/core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerAfterReactiveMethodInterceptor.java b/core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerAfterReactiveMethodInterceptor.java index c1d23a4d46..fe5e2613b8 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerAfterReactiveMethodInterceptor.java +++ b/core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerAfterReactiveMethodInterceptor.java @@ -120,6 +120,7 @@ public final class AuthorizationManagerAfterReactiveMethodInterceptor implements + "(for example, a Mono or Flux) or the function must be a Kotlin coroutine " + "in order to support Reactor Context"); Mono authentication = ReactiveAuthenticationUtils.getAuthentication(); + @SuppressWarnings("NullAway") // Dataflow analysis limitation Function, Mono> postAuthorize = (signal) -> { if (signal.isOnComplete()) { return Mono.empty(); @@ -130,12 +131,16 @@ public final class AuthorizationManagerAfterReactiveMethodInterceptor implements if (signal.getThrowable() instanceof AuthorizationDeniedException denied) { return postProcess(denied, mi); } + // getThrowable must be non-null because hasError() is true return Mono.error(signal.getThrowable()); }; ReactiveAdapter adapter = ReactiveAdapterRegistry.getSharedInstance().getAdapter(type); if (hasFlowReturnType) { if (isSuspendingFunction) { Publisher publisher = ReactiveMethodInvocationUtils.proceed(mi); + if (publisher == null) { + return Flux.empty(); + } return Flux.from(publisher).materialize().flatMap(postAuthorize); } else { @@ -148,6 +153,9 @@ public final class AuthorizationManagerAfterReactiveMethodInterceptor implements } } Publisher publisher = ReactiveMethodInvocationUtils.proceed(mi); + if (publisher == null) { + return Flux.empty(); + } if (isMultiValue(type, adapter)) { Flux flux = Flux.from(publisher).materialize().flatMap(postAuthorize); return (adapter != null) ? adapter.fromPublisher(flux) : flux; @@ -173,7 +181,7 @@ public final class AuthorizationManagerAfterReactiveMethodInterceptor implements private Mono postProcess(AuthorizationResult decision, MethodInvocationResult methodInvocationResult) { if (decision.isGranted()) { - return Mono.just(methodInvocationResult.getResult()); + return Mono.justOrEmpty(methodInvocationResult.getResult()); } return Mono.fromSupplier(() -> { if (this.authorizationManager instanceof MethodAuthorizationDeniedHandler handler) { diff --git a/core/src/main/java/org/springframework/security/authorization/method/ReactiveAuthenticationUtils.java b/core/src/main/java/org/springframework/security/authorization/method/ReactiveAuthenticationUtils.java index 84e372d578..ef5332b737 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/ReactiveAuthenticationUtils.java +++ b/core/src/main/java/org/springframework/security/authorization/method/ReactiveAuthenticationUtils.java @@ -35,9 +35,10 @@ final class ReactiveAuthenticationUtils { private static final Authentication ANONYMOUS = new AnonymousAuthenticationToken("key", "anonymous", AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS")); + @SuppressWarnings("NullAway") // https://github.com/uber/NullAway/issues/1290 static Mono getAuthentication() { return ReactiveSecurityContextHolder.getContext() - .map(SecurityContext::getAuthentication) + .mapNotNull(SecurityContext::getAuthentication) .defaultIfEmpty(ANONYMOUS); } diff --git a/core/src/main/java/org/springframework/security/authorization/method/ReactiveMethodInvocationUtils.java b/core/src/main/java/org/springframework/security/authorization/method/ReactiveMethodInvocationUtils.java index 310cb365d0..1bbf8a199d 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/ReactiveMethodInvocationUtils.java +++ b/core/src/main/java/org/springframework/security/authorization/method/ReactiveMethodInvocationUtils.java @@ -28,7 +28,7 @@ import reactor.core.Exceptions; */ final class ReactiveMethodInvocationUtils { - static @Nullable T proceed(MethodInvocation mi) { + static @Nullable T proceed(MethodInvocation mi) { try { return (T) mi.proceed(); } diff --git a/core/src/main/java/org/springframework/security/core/session/InMemoryReactiveSessionRegistry.java b/core/src/main/java/org/springframework/security/core/session/InMemoryReactiveSessionRegistry.java index 0781ab156c..3827e0fcf4 100644 --- a/core/src/main/java/org/springframework/security/core/session/InMemoryReactiveSessionRegistry.java +++ b/core/src/main/java/org/springframework/security/core/session/InMemoryReactiveSessionRegistry.java @@ -50,9 +50,10 @@ public class InMemoryReactiveSessionRegistry implements ReactiveSessionRegistry } @Override + @SuppressWarnings("NullAway") // https://github.com/uber/NullAway/issues/1290 public Flux getAllSessions(Object principal) { return Flux.fromIterable(this.sessionIdsByPrincipal.getOrDefault(principal, Collections.emptySet())) - .map(this.sessionById::get); + .mapNotNull(this.sessionById::get); } @Override diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f8bb325a8b..91b4d7ab84 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -30,7 +30,7 @@ commons-collections = "commons-collections:commons-collections:3.2.2" io-micrometer-context-propagation = "io.micrometer:context-propagation:1.1.3" io-micrometer-micrometer-observation = "io.micrometer:micrometer-observation:1.14.11" io-mockk = "io.mockk:mockk:1.14.6" -io-projectreactor-reactor-bom = "io.projectreactor:reactor-bom:2025.0.0-M7" +io-projectreactor-reactor-bom = "io.projectreactor:reactor-bom:2025.0.0-SNAPSHOT" io-rsocket-rsocket-bom = { module = "io.rsocket:rsocket-bom", version.ref = "io-rsocket" } io-spring-javaformat-spring-javaformat-checkstyle = { module = "io.spring.javaformat:spring-javaformat-checkstyle", version.ref = "io-spring-javaformat" } io-spring-javaformat-spring-javaformat-gradle-plugin = { module = "io.spring.javaformat:spring-javaformat-gradle-plugin", version.ref = "io-spring-javaformat" } diff --git a/messaging/src/main/java/org/springframework/security/messaging/handler/invocation/reactive/AuthenticationPrincipalArgumentResolver.java b/messaging/src/main/java/org/springframework/security/messaging/handler/invocation/reactive/AuthenticationPrincipalArgumentResolver.java index 634c13c97c..9017b2d5bf 100644 --- a/messaging/src/main/java/org/springframework/security/messaging/handler/invocation/reactive/AuthenticationPrincipalArgumentResolver.java +++ b/messaging/src/main/java/org/springframework/security/messaging/handler/invocation/reactive/AuthenticationPrincipalArgumentResolver.java @@ -138,11 +138,12 @@ public class AuthenticationPrincipalArgumentResolver implements HandlerMethodArg } @Override + @SuppressWarnings("NullAway") // https://github.com/uber/NullAway/issues/1290 public Mono resolveArgument(MethodParameter parameter, Message message) { ReactiveAdapter adapter = this.adapterRegistry.getAdapter(parameter.getParameterType()); // @formatter:off return ReactiveSecurityContextHolder.getContext() - .map(SecurityContext::getAuthentication) + .mapNotNull(SecurityContext::getAuthentication) .flatMap((a) -> { Object p = resolvePrincipal(parameter, a.getPrincipal()); Mono principal = Mono.justOrEmpty(p); diff --git a/rsocket/src/main/java/org/springframework/security/rsocket/authorization/AuthorizationPayloadInterceptor.java b/rsocket/src/main/java/org/springframework/security/rsocket/authorization/AuthorizationPayloadInterceptor.java index f90115aaa4..f0361d1bd4 100644 --- a/rsocket/src/main/java/org/springframework/security/rsocket/authorization/AuthorizationPayloadInterceptor.java +++ b/rsocket/src/main/java/org/springframework/security/rsocket/authorization/AuthorizationPayloadInterceptor.java @@ -55,10 +55,10 @@ public class AuthorizationPayloadInterceptor implements PayloadInterceptor, Orde } @Override + @SuppressWarnings("NullAway") // https://github.com/uber/NullAway/issues/1290 public Mono intercept(PayloadExchange exchange, PayloadInterceptorChain chain) { return ReactiveSecurityContextHolder.getContext() - .filter((c) -> c.getAuthentication() != null) - .map(SecurityContext::getAuthentication) + .mapNotNull(SecurityContext::getAuthentication) .switchIfEmpty(Mono.error(() -> new AuthenticationCredentialsNotFoundException( "An Authentication (possibly AnonymousAuthenticationToken) is required."))) .as((authentication) -> this.authorizationManager.verify(authentication, exchange)) diff --git a/rsocket/src/main/java/org/springframework/security/rsocket/authorization/PayloadExchangeMatcherReactiveAuthorizationManager.java b/rsocket/src/main/java/org/springframework/security/rsocket/authorization/PayloadExchangeMatcherReactiveAuthorizationManager.java index 8ff5a4396b..60ecb63031 100644 --- a/rsocket/src/main/java/org/springframework/security/rsocket/authorization/PayloadExchangeMatcherReactiveAuthorizationManager.java +++ b/rsocket/src/main/java/org/springframework/security/rsocket/authorization/PayloadExchangeMatcherReactiveAuthorizationManager.java @@ -52,12 +52,13 @@ public final class PayloadExchangeMatcherReactiveAuthorizationManager } @Override + @SuppressWarnings("NullAway") // https://github.com/uber/NullAway/issues/1290 public Mono authorize(Mono authentication, PayloadExchange exchange) { return Flux.fromIterable(this.mappings) .concatMap((mapping) -> mapping.getMatcher() .matches(exchange) .filter(PayloadExchangeMatcher.MatchResult::isMatch) - .map(MatchResult::getVariables) + .mapNotNull(MatchResult::getVariables) .flatMap((variables) -> mapping.getEntry() .authorize(authentication, new PayloadExchangeAuthorizationContext(exchange, variables)))) .next() diff --git a/web/src/main/java/org/springframework/security/web/reactive/result/method/annotation/AuthenticationPrincipalArgumentResolver.java b/web/src/main/java/org/springframework/security/web/reactive/result/method/annotation/AuthenticationPrincipalArgumentResolver.java index 799aa8ece0..1ef38b32ea 100644 --- a/web/src/main/java/org/springframework/security/web/reactive/result/method/annotation/AuthenticationPrincipalArgumentResolver.java +++ b/web/src/main/java/org/springframework/security/web/reactive/result/method/annotation/AuthenticationPrincipalArgumentResolver.java @@ -83,11 +83,12 @@ public class AuthenticationPrincipalArgumentResolver extends HandlerMethodArgume } @Override + @SuppressWarnings("NullAway") // https://github.com/uber/NullAway/issues/1290 public Mono resolveArgument(MethodParameter parameter, BindingContext bindingContext, ServerWebExchange exchange) { ReactiveAdapter adapter = getAdapterRegistry().getAdapter(parameter.getParameterType()); return ReactiveSecurityContextHolder.getContext() - .map(SecurityContext::getAuthentication) + .mapNotNull(SecurityContext::getAuthentication) .flatMap((authentication) -> { Mono principal = Mono.justOrEmpty(resolvePrincipal(parameter, authentication.getPrincipal())); return (adapter != null) ? Mono.just(adapter.fromPublisher(principal)) : principal; diff --git a/web/src/main/java/org/springframework/security/web/server/authentication/SwitchUserWebFilter.java b/web/src/main/java/org/springframework/security/web/server/authentication/SwitchUserWebFilter.java index c80ea7f83d..8a9554d083 100644 --- a/web/src/main/java/org/springframework/security/web/server/authentication/SwitchUserWebFilter.java +++ b/web/src/main/java/org/springframework/security/web/server/authentication/SwitchUserWebFilter.java @@ -176,11 +176,12 @@ public class SwitchUserWebFilter implements WebFilter { * @throws AuthenticationCredentialsNotFoundException If the target user can not be * found by username */ + @SuppressWarnings("NullAway") // https://github.com/uber/NullAway/issues/1290 protected Mono switchUser(WebFilterExchange webFilterExchange) { return this.switchUserMatcher.matches(webFilterExchange.getExchange()) .filter(ServerWebExchangeMatcher.MatchResult::isMatch) .flatMap((matchResult) -> ReactiveSecurityContextHolder.getContext()) - .map(SecurityContext::getAuthentication) + .mapNotNull(SecurityContext::getAuthentication) .flatMap((currentAuthentication) -> { String username = getUsername(webFilterExchange.getExchange()); return attemptSwitchUser(currentAuthentication, username); @@ -197,11 +198,12 @@ public class SwitchUserWebFilter implements WebFilter { * Authentication associated with this request or the user is not * switched. */ + @SuppressWarnings("NullAway") // https://github.com/uber/NullAway/issues/1290 protected Mono exitSwitchUser(WebFilterExchange webFilterExchange) { return this.exitUserMatcher.matches(webFilterExchange.getExchange()) .filter(ServerWebExchangeMatcher.MatchResult::isMatch) .flatMap((matchResult) -> ReactiveSecurityContextHolder.getContext() - .map(SecurityContext::getAuthentication) + .mapNotNull(SecurityContext::getAuthentication) .switchIfEmpty(Mono.error(this::noCurrentUserException))) .map(this::attemptExitUser); } diff --git a/web/src/main/java/org/springframework/security/web/server/authentication/ott/DefaultServerGenerateOneTimeTokenRequestResolver.java b/web/src/main/java/org/springframework/security/web/server/authentication/ott/DefaultServerGenerateOneTimeTokenRequestResolver.java index ebf6397178..fac32f4540 100644 --- a/web/src/main/java/org/springframework/security/web/server/authentication/ott/DefaultServerGenerateOneTimeTokenRequestResolver.java +++ b/web/src/main/java/org/springframework/security/web/server/authentication/ott/DefaultServerGenerateOneTimeTokenRequestResolver.java @@ -41,6 +41,7 @@ public final class DefaultServerGenerateOneTimeTokenRequestResolver private Duration expiresIn = DEFAULT_EXPIRES_IN; @Override + @SuppressWarnings("NullAway") // https://github.com/uber/NullAway/issues/1290 public Mono resolve(ServerWebExchange exchange) { // @formatter:off return exchange.getFormData() diff --git a/web/src/main/java/org/springframework/security/web/server/authorization/AuthorizationWebFilter.java b/web/src/main/java/org/springframework/security/web/server/authorization/AuthorizationWebFilter.java index e50fb0b499..5736a04cd6 100644 --- a/web/src/main/java/org/springframework/security/web/server/authorization/AuthorizationWebFilter.java +++ b/web/src/main/java/org/springframework/security/web/server/authorization/AuthorizationWebFilter.java @@ -45,10 +45,11 @@ public class AuthorizationWebFilter implements WebFilter { } @Override + @SuppressWarnings("NullAway") // https://github.com/uber/NullAway/issues/1290 public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { return ReactiveSecurityContextHolder.getContext() .filter((c) -> c.getAuthentication() != null) - .map(SecurityContext::getAuthentication) + .mapNotNull(SecurityContext::getAuthentication) .as((authentication) -> this.authorizationManager.verify(authentication, exchange)) .doOnSuccess((it) -> logger.debug("Authorization successful")) .doOnError(AccessDeniedException.class, diff --git a/web/src/main/java/org/springframework/security/web/server/context/SecurityContextServerWebExchange.java b/web/src/main/java/org/springframework/security/web/server/context/SecurityContextServerWebExchange.java index 4a43caf7de..7a04020e4b 100644 --- a/web/src/main/java/org/springframework/security/web/server/context/SecurityContextServerWebExchange.java +++ b/web/src/main/java/org/springframework/security/web/server/context/SecurityContextServerWebExchange.java @@ -42,9 +42,9 @@ public class SecurityContextServerWebExchange extends ServerWebExchangeDecorator } @Override - @SuppressWarnings("unchecked") + @SuppressWarnings({ "unchecked", "NullAway" }) // https://github.com/uber/NullAway/issues/1290 public Mono getPrincipal() { - return this.context.map((context) -> (T) context.getAuthentication()); + return this.context.mapNotNull((context) -> (T) context.getAuthentication()); } } diff --git a/web/src/main/java/org/springframework/security/web/server/csrf/ServerCsrfTokenRequestAttributeHandler.java b/web/src/main/java/org/springframework/security/web/server/csrf/ServerCsrfTokenRequestAttributeHandler.java index e699aa0499..7c67ee5733 100644 --- a/web/src/main/java/org/springframework/security/web/server/csrf/ServerCsrfTokenRequestAttributeHandler.java +++ b/web/src/main/java/org/springframework/security/web/server/csrf/ServerCsrfTokenRequestAttributeHandler.java @@ -67,6 +67,7 @@ public class ServerCsrfTokenRequestAttributeHandler implements ServerCsrfTokenRe this.isTokenFromMultipartDataEnabled = tokenFromMultipartDataEnabled; } + @SuppressWarnings("NullAway") // https://github.com/uber/NullAway/issues/1290 private Mono tokenFromMultipartData(ServerWebExchange exchange, CsrfToken expected) { if (!this.isTokenFromMultipartDataEnabled) { return Mono.empty(); @@ -78,7 +79,7 @@ public class ServerCsrfTokenRequestAttributeHandler implements ServerCsrfTokenRe return Mono.empty(); } return exchange.getMultipartData() - .map((d) -> d.getFirst(expected.getParameterName())) + .mapNotNull((d) -> d.getFirst(expected.getParameterName())) .cast(FormFieldPart.class) .map(FormFieldPart::value); } diff --git a/web/src/main/java/org/springframework/security/web/server/csrf/WebSessionServerCsrfTokenRepository.java b/web/src/main/java/org/springframework/security/web/server/csrf/WebSessionServerCsrfTokenRepository.java index 5466267035..6cc0f6147f 100644 --- a/web/src/main/java/org/springframework/security/web/server/csrf/WebSessionServerCsrfTokenRepository.java +++ b/web/src/main/java/org/springframework/security/web/server/csrf/WebSessionServerCsrfTokenRepository.java @@ -71,10 +71,11 @@ public class WebSessionServerCsrfTokenRepository implements ServerCsrfTokenRepos } @Override + @SuppressWarnings("NullAway") // https://github.com/uber/NullAway/issues/1290 public Mono loadToken(ServerWebExchange exchange) { return exchange.getSession() .filter((session) -> session.getAttributes().containsKey(this.sessionAttributeName)) - .map((session) -> session.getAttribute(this.sessionAttributeName)); + .mapNotNull((session) -> session.getAttribute(this.sessionAttributeName)); } /**