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 a9d77c641b..0ef9b3a4be 100644 --- a/core/src/main/java/org/springframework/security/authorization/ObservationReactiveAuthorizationManager.java +++ b/core/src/main/java/org/springframework/security/authorization/ObservationReactiveAuthorizationManager.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 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. @@ -62,6 +62,17 @@ public final class ObservationReactiveAuthorizationManager @Deprecated @Override public Mono check(Mono authentication, T object) { + return authorize(authentication, object).flatMap((result) -> { + if (result instanceof AuthorizationDecision decision) { + return Mono.just(decision); + } + return Mono.error(new IllegalArgumentException( + "Please call #authorize or ensure that the returned result is of type Mono")); + }); + } + + @Override + public Mono authorize(Mono authentication, T object) { AuthorizationObservationContext context = new AuthorizationObservationContext<>(object); Mono wrapped = authentication.map((auth) -> { context.setAuthentication(auth); @@ -71,9 +82,9 @@ public final class ObservationReactiveAuthorizationManager Observation observation = Observation.createNotStarted(this.convention, () -> context, this.registry) .parentObservation(contextView.getOrDefault(ObservationThreadLocalAccessor.KEY, null)) .start(); - return this.delegate.check(wrapped, object).doOnSuccess((decision) -> { - context.setAuthorizationResult(decision); - if (decision == null || !decision.isGranted()) { + return this.delegate.authorize(wrapped, object).doOnSuccess((result) -> { + context.setAuthorizationResult(result); + if (result == null || !result.isGranted()) { observation.error(new AccessDeniedException("Access Denied")); } observation.stop(); diff --git a/core/src/main/java/org/springframework/security/authorization/ReactiveAuthorizationManager.java b/core/src/main/java/org/springframework/security/authorization/ReactiveAuthorizationManager.java index 05662737d1..860208c6f5 100644 --- a/core/src/main/java/org/springframework/security/authorization/ReactiveAuthorizationManager.java +++ b/core/src/main/java/org/springframework/security/authorization/ReactiveAuthorizationManager.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 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. @@ -50,8 +50,8 @@ public interface ReactiveAuthorizationManager { */ default Mono verify(Mono authentication, T object) { // @formatter:off - return check(authentication, object) - .filter(AuthorizationDecision::isGranted) + return authorize(authentication, object) + .filter(AuthorizationResult::isGranted) .switchIfEmpty(Mono.defer(() -> Mono.error(new AccessDeniedException("Access Denied")))) .flatMap((decision) -> Mono.empty()); // @formatter:on diff --git a/core/src/test/java/org/springframework/security/authorization/ObservationReactiveAuthorizationManagerTests.java b/core/src/test/java/org/springframework/security/authorization/ObservationReactiveAuthorizationManagerTests.java index 34b0533a26..d3eb935cc7 100644 --- a/core/src/test/java/org/springframework/security/authorization/ObservationReactiveAuthorizationManagerTests.java +++ b/core/src/test/java/org/springframework/security/authorization/ObservationReactiveAuthorizationManagerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2025 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. @@ -70,6 +70,7 @@ public class ObservationReactiveAuthorizationManagerTests { void verifyWhenDefaultsThenObserves() { given(this.handler.supportsContext(any())).willReturn(true); given(this.authorizationManager.check(any(), any())).willReturn(Mono.just(this.grant)); + given(this.authorizationManager.authorize(any(), any())).willCallRealMethod(); this.tested.verify(this.token, this.object).block(); ArgumentCaptor captor = ArgumentCaptor.forClass(Observation.Context.class); verify(this.handler).onStart(captor.capture()); @@ -86,6 +87,7 @@ public class ObservationReactiveAuthorizationManagerTests { void verifyWhenErrorsThenObserves() { given(this.handler.supportsContext(any())).willReturn(true); given(this.authorizationManager.check(any(), any())).willReturn(Mono.just(this.deny)); + given(this.authorizationManager.authorize(any(), any())).willCallRealMethod(); assertThatExceptionOfType(AccessDeniedException.class) .isThrownBy(() -> this.tested.verify(this.token, this.object).block()); ArgumentCaptor captor = ArgumentCaptor.forClass(Observation.Context.class); @@ -106,6 +108,7 @@ public class ObservationReactiveAuthorizationManagerTests { ((Mono) invocation.getArgument(0)).block(); return Mono.just(this.grant); }); + given(this.authorizationManager.authorize(any(), any())).willCallRealMethod(); this.tested.verify(this.token, this.object).block(); ArgumentCaptor captor = ArgumentCaptor.forClass(Observation.Context.class); verify(this.handler).onStart(captor.capture()); 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 fc7862f51e..9004633f9e 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 @@ -1,5 +1,5 @@ /* - * Copyright 2019 the original author or authors. + * Copyright 2002-2025 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. @@ -23,6 +23,7 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import org.springframework.security.authorization.AuthorizationDecision; +import org.springframework.security.authorization.AuthorizationResult; import org.springframework.security.authorization.ReactiveAuthorizationManager; import org.springframework.security.core.Authentication; import org.springframework.security.rsocket.api.PayloadExchange; @@ -51,18 +52,29 @@ public final class PayloadExchangeMatcherReactiveAuthorizationManager } /** - * @deprecated please use {@link #authorize(Mono, Object)} instead + * @deprecated please use {@link #authorize(Mono, PayloadExchange)} instead */ @Deprecated @Override public Mono check(Mono authentication, PayloadExchange exchange) { + return authorize(authentication, exchange).flatMap((result) -> { + if (result instanceof AuthorizationDecision decision) { + return Mono.just(decision); + } + return Mono.error(new IllegalArgumentException( + "Please call #authorize or ensure that the returned result is of type Mono")); + }); + } + + @Override + 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) .flatMap((variables) -> mapping.getEntry() - .check(authentication, new PayloadExchangeAuthorizationContext(exchange, variables)))) + .authorize(authentication, new PayloadExchangeAuthorizationContext(exchange, variables)))) .next() .switchIfEmpty(Mono.fromCallable(() -> new AuthorizationDecision(false))); } diff --git a/rsocket/src/test/java/org/springframework/security/rsocket/authorization/PayloadExchangeMatcherReactiveAuthorizationManagerTests.java b/rsocket/src/test/java/org/springframework/security/rsocket/authorization/PayloadExchangeMatcherReactiveAuthorizationManagerTests.java index 4f8041aeb4..c0b14cb835 100644 --- a/rsocket/src/test/java/org/springframework/security/rsocket/authorization/PayloadExchangeMatcherReactiveAuthorizationManagerTests.java +++ b/rsocket/src/test/java/org/springframework/security/rsocket/authorization/PayloadExchangeMatcherReactiveAuthorizationManagerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2019 the original author or authors. + * Copyright 2002-2025 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. @@ -53,6 +53,7 @@ public class PayloadExchangeMatcherReactiveAuthorizationManagerTests { public void checkWhenGrantedThenGranted() { AuthorizationDecision expected = new AuthorizationDecision(true); given(this.authz.check(any(), any())).willReturn(Mono.just(expected)); + given(this.authz.authorize(any(), any())).willCallRealMethod(); PayloadExchangeMatcherReactiveAuthorizationManager manager = PayloadExchangeMatcherReactiveAuthorizationManager .builder() .add(new PayloadExchangeMatcherEntry<>(PayloadExchangeMatchers.anyExchange(), this.authz)) @@ -64,6 +65,7 @@ public class PayloadExchangeMatcherReactiveAuthorizationManagerTests { public void checkWhenDeniedThenDenied() { AuthorizationDecision expected = new AuthorizationDecision(false); given(this.authz.check(any(), any())).willReturn(Mono.just(expected)); + given(this.authz.authorize(any(), any())).willCallRealMethod(); PayloadExchangeMatcherReactiveAuthorizationManager manager = PayloadExchangeMatcherReactiveAuthorizationManager .builder() .add(new PayloadExchangeMatcherEntry<>(PayloadExchangeMatchers.anyExchange(), this.authz)) @@ -75,6 +77,7 @@ public class PayloadExchangeMatcherReactiveAuthorizationManagerTests { public void checkWhenFirstMatchThenSecondUsed() { AuthorizationDecision expected = new AuthorizationDecision(true); given(this.authz.check(any(), any())).willReturn(Mono.just(expected)); + given(this.authz.authorize(any(), any())).willCallRealMethod(); PayloadExchangeMatcherReactiveAuthorizationManager manager = PayloadExchangeMatcherReactiveAuthorizationManager .builder() .add(new PayloadExchangeMatcherEntry<>(PayloadExchangeMatchers.anyExchange(), this.authz)) @@ -87,6 +90,7 @@ public class PayloadExchangeMatcherReactiveAuthorizationManagerTests { public void checkWhenSecondMatchThenSecondUsed() { AuthorizationDecision expected = new AuthorizationDecision(true); given(this.authz2.check(any(), any())).willReturn(Mono.just(expected)); + given(this.authz2.authorize(any(), any())).willCallRealMethod(); PayloadExchangeMatcherReactiveAuthorizationManager manager = PayloadExchangeMatcherReactiveAuthorizationManager .builder() .add(new PayloadExchangeMatcherEntry<>((e) -> PayloadExchangeMatcher.MatchResult.notMatch(), this.authz)) diff --git a/web/src/main/java/org/springframework/security/web/server/authorization/DelegatingReactiveAuthorizationManager.java b/web/src/main/java/org/springframework/security/web/server/authorization/DelegatingReactiveAuthorizationManager.java index ebdb2b8ccb..e245ce0466 100644 --- a/web/src/main/java/org/springframework/security/web/server/authorization/DelegatingReactiveAuthorizationManager.java +++ b/web/src/main/java/org/springframework/security/web/server/authorization/DelegatingReactiveAuthorizationManager.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2025 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. @@ -26,6 +26,7 @@ import reactor.core.publisher.Mono; import org.springframework.core.log.LogMessage; import org.springframework.security.authorization.AuthorizationDecision; +import org.springframework.security.authorization.AuthorizationResult; import org.springframework.security.authorization.ReactiveAuthorizationManager; import org.springframework.security.core.Authentication; import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher.MatchResult; @@ -35,6 +36,7 @@ import org.springframework.web.server.ServerWebExchange; /** * @author Rob Winch * @author Mathieu Ouellet + * @author Evgeniy Cheban * @since 5.0 */ public final class DelegatingReactiveAuthorizationManager implements ReactiveAuthorizationManager { @@ -49,11 +51,22 @@ public final class DelegatingReactiveAuthorizationManager implements ReactiveAut } /** - * @deprecated please use {@link #authorize(Mono, Object)} instead + * @deprecated please use {@link #authorize(Mono, ServerWebExchange)} instead */ @Deprecated @Override public Mono check(Mono authentication, ServerWebExchange exchange) { + return authorize(authentication, exchange).flatMap((result) -> { + if (result instanceof AuthorizationDecision decision) { + return Mono.just(decision); + } + return Mono.error(new IllegalArgumentException( + "Please call #authorize or ensure that the returned result is of type Mono")); + }); + } + + @Override + public Mono authorize(Mono authentication, ServerWebExchange exchange) { return Flux.fromIterable(this.mappings) .concatMap((mapping) -> mapping.getMatcher() .matches(exchange) @@ -63,10 +76,10 @@ public final class DelegatingReactiveAuthorizationManager implements ReactiveAut logger.debug(LogMessage.of(() -> "Checking authorization on '" + exchange.getRequest().getPath().pathWithinApplication() + "' using " + mapping.getEntry())); - return mapping.getEntry().check(authentication, new AuthorizationContext(exchange, variables)); + return mapping.getEntry().authorize(authentication, new AuthorizationContext(exchange, variables)); })) .next() - .defaultIfEmpty(new AuthorizationDecision(false)); + .switchIfEmpty(Mono.fromCallable(() -> new AuthorizationDecision(false))); } public static DelegatingReactiveAuthorizationManager.Builder builder() { diff --git a/web/src/test/java/org/springframework/security/web/server/authorization/DelegatingReactiveAuthorizationManagerTests.java b/web/src/test/java/org/springframework/security/web/server/authorization/DelegatingReactiveAuthorizationManagerTests.java index 8d916142d2..adab9bb717 100644 --- a/web/src/test/java/org/springframework/security/web/server/authorization/DelegatingReactiveAuthorizationManagerTests.java +++ b/web/src/test/java/org/springframework/security/web/server/authorization/DelegatingReactiveAuthorizationManagerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2025 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. @@ -81,6 +81,7 @@ public class DelegatingReactiveAuthorizationManagerTests { given(this.match1.matches(any())).willReturn(ServerWebExchangeMatcher.MatchResult.match()); given(this.delegate1.check(eq(this.authentication), any(AuthorizationContext.class))) .willReturn(Mono.just(this.decision)); + given(this.delegate1.authorize(eq(this.authentication), any(AuthorizationContext.class))).willCallRealMethod(); assertThat(this.manager.check(this.authentication, this.exchange).block()).isEqualTo(this.decision); verifyNoMoreInteractions(this.match2, this.delegate2); } @@ -91,6 +92,7 @@ public class DelegatingReactiveAuthorizationManagerTests { given(this.match2.matches(any())).willReturn(ServerWebExchangeMatcher.MatchResult.match()); given(this.delegate2.check(eq(this.authentication), any(AuthorizationContext.class))) .willReturn(Mono.just(this.decision)); + given(this.delegate2.authorize(eq(this.authentication), any(AuthorizationContext.class))).willCallRealMethod(); assertThat(this.manager.check(this.authentication, this.exchange).block()).isEqualTo(this.decision); verifyNoMoreInteractions(this.delegate1); }