diff --git a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/Authz.java b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/Authz.java index dfb8e43ac8..489bbc9479 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/Authz.java +++ b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/Authz.java @@ -16,10 +16,13 @@ package org.springframework.security.config.annotation.method.configuration; +import org.aopalliance.intercept.MethodInvocation; import reactor.core.publisher.Mono; import org.springframework.security.authorization.AuthorizationDecision; +import org.springframework.security.authorization.AuthorizationManager; import org.springframework.security.authorization.AuthorizationResult; +import org.springframework.security.authorization.ReactiveAuthorizationManager; import org.springframework.security.core.Authentication; import org.springframework.stereotype.Component; @@ -55,6 +58,14 @@ public class Authz { return Mono.just(checkResult(result)); } + public AuthorizationManager checkManager(long id) { + return (authentication, context) -> new AuthorizationDecision(check(id)); + } + + public ReactiveAuthorizationManager checkReactiveManager(long id) { + return (authentication, context) -> checkReactive(id).map(AuthorizationDecision::new); + } + @SuppressWarnings("serial") public static class AuthzResult extends AuthorizationDecision { diff --git a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/MethodSecurityService.java b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/MethodSecurityService.java index 1906da92d4..e46a0ace32 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/MethodSecurityService.java +++ b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/MethodSecurityService.java @@ -196,6 +196,9 @@ public interface MethodSecurityService { @HandleAuthorizationDenied(handlerClass = MethodAuthorizationDeniedHandler.class) String checkCustomResult(boolean result); + @PreAuthorize("@authz.checkManager(#id)") + String checkCustomManager(long id); + class StarMaskingHandler implements MethodAuthorizationDeniedHandler { @Override diff --git a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/MethodSecurityServiceImpl.java b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/MethodSecurityServiceImpl.java index 1dd92a6b5b..dc14439190 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/MethodSecurityServiceImpl.java +++ b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/MethodSecurityServiceImpl.java @@ -203,6 +203,11 @@ public class MethodSecurityServiceImpl implements MethodSecurityService { return "ok"; } + @Override + public String checkCustomManager(long id) { + return "ok"; + } + @Override public void hasAllRolesUserAdmin() { } diff --git a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/PrePostMethodSecurityConfigurationTests.java b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/PrePostMethodSecurityConfigurationTests.java index 1b9124e2e8..d48047980e 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/PrePostMethodSecurityConfigurationTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/PrePostMethodSecurityConfigurationTests.java @@ -92,6 +92,7 @@ import org.springframework.security.access.prepost.PostFilter; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreFilter; import org.springframework.security.authorization.AuthorizationDecision; +import org.springframework.security.authorization.AuthorizationDeniedException; import org.springframework.security.authorization.AuthorizationEventPublisher; import org.springframework.security.authorization.AuthorizationManager; import org.springframework.security.authorization.SpringAuthorizationEventPublisher; @@ -1410,6 +1411,14 @@ public class PrePostMethodSecurityConfigurationTests { this.mvc.perform(requestWithUser).andExpect(status().isForbidden()); } + @Test + void checkCustomManagerWhenInvokedThenUsesBeanToAuthorize() { + this.spring.register(MethodSecurityServiceConfig.class).autowire(); + MethodSecurityService service = this.spring.getContext().getBean(MethodSecurityService.class); + service.checkCustomManager(2); + assertThatExceptionOfType(AuthorizationDeniedException.class).isThrownBy(() -> service.checkCustomManager(1)); + } + private static Consumer disallowBeanOverriding() { return (context) -> ((AnnotationConfigWebApplicationContext) context).setAllowBeanDefinitionOverriding(false); } diff --git a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecurityConfigurationTests.java b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecurityConfigurationTests.java index 5b70096a85..f462e04393 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecurityConfigurationTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecurityConfigurationTests.java @@ -51,6 +51,7 @@ import org.springframework.security.access.prepost.PostFilter; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreFilter; import org.springframework.security.authentication.TestAuthentication; +import org.springframework.security.authorization.AuthorizationDeniedException; import org.springframework.security.authorization.method.AuthorizationAdvisorProxyFactory; import org.springframework.security.authorization.method.AuthorizationAdvisorProxyFactory.TargetVisitor; import org.springframework.security.authorization.method.AuthorizeReturnObject; @@ -66,6 +67,7 @@ import org.springframework.security.core.userdetails.User; import org.springframework.security.test.context.support.WithMockUser; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.mock; @@ -285,6 +287,15 @@ public class ReactiveMethodSecurityConfigurationTests { verifyNoInteractions(handler); } + @Test + void checkCustomManagerWhenInvokedThenUsesBeanToAuthorize() { + this.spring.register(WithRolePrefixConfiguration.class, MethodSecurityServiceConfig.class).autowire(); + ReactiveMethodSecurityService service = this.spring.getContext().getBean(ReactiveMethodSecurityService.class); + service.checkCustomManager(2).block(); + assertThatExceptionOfType(AuthorizationDeniedException.class) + .isThrownBy(() -> service.checkCustomManager(1).block()); + } + private static Consumer authorities(String... authorities) { return (builder) -> builder.authorities(authorities); } diff --git a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecurityService.java b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecurityService.java index 7e6bd9836d..ce4a137753 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecurityService.java +++ b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecurityService.java @@ -110,6 +110,9 @@ public interface ReactiveMethodSecurityService { @HandleAuthorizationDenied(handlerClass = MethodAuthorizationDeniedHandler.class) Mono checkCustomResult(boolean result); + @PreAuthorize("@authz.checkReactiveManager(#id)") + Mono checkCustomManager(long id); + @PreAuthorize("hasPermission(#kgName, 'read')") Mono preAuthorizeHasPermission(String kgName); diff --git a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecurityServiceImpl.java b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecurityServiceImpl.java index 413d485296..16dbf6446b 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecurityServiceImpl.java +++ b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecurityServiceImpl.java @@ -100,6 +100,11 @@ public class ReactiveMethodSecurityServiceImpl implements ReactiveMethodSecurity return Mono.just("ok"); } + @Override + public Mono checkCustomManager(long id) { + return Mono.just("ok"); + } + @Override public Mono preAuthorizeHasPermission(String kgName) { return Mono.just("ok"); diff --git a/core/src/main/java/org/springframework/security/authorization/method/ExpressionUtils.java b/core/src/main/java/org/springframework/security/authorization/method/ExpressionUtils.java index 7f3029c1b3..b11b986174 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/ExpressionUtils.java +++ b/core/src/main/java/org/springframework/security/authorization/method/ExpressionUtils.java @@ -16,14 +16,19 @@ package org.springframework.security.authorization.method; +import java.util.function.Supplier; + import org.jspecify.annotations.Nullable; import org.springframework.expression.EvaluationContext; import org.springframework.expression.EvaluationException; import org.springframework.expression.Expression; import org.springframework.security.authorization.AuthorizationDeniedException; +import org.springframework.security.authorization.AuthorizationManager; import org.springframework.security.authorization.AuthorizationResult; import org.springframework.security.authorization.ExpressionAuthorizationDecision; +import org.springframework.security.core.Authentication; +import org.springframework.util.Assert; final class ExpressionUtils { @@ -31,8 +36,18 @@ final class ExpressionUtils { } static @Nullable AuthorizationResult evaluate(Expression expr, EvaluationContext ctx) { + return evaluate(expr, ctx, () -> null, null); + } + + static @Nullable AuthorizationResult evaluate(Expression expr, EvaluationContext ctx, + Supplier authentication, @Nullable T context) { try { Object result = expr.getValue(ctx); + if (result instanceof AuthorizationManager manager) { + Assert.notNull(authentication, "authentication supplier cannot be null"); + Assert.notNull(context, "context cannot be null"); + return ((AuthorizationManager) manager).authorize(authentication, context); + } if (result instanceof AuthorizationResult decision) { return decision; } diff --git a/core/src/main/java/org/springframework/security/authorization/method/PostAuthorizeAuthorizationManager.java b/core/src/main/java/org/springframework/security/authorization/method/PostAuthorizeAuthorizationManager.java index 968c0a6587..26423e87cb 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/PostAuthorizeAuthorizationManager.java +++ b/core/src/main/java/org/springframework/security/authorization/method/PostAuthorizeAuthorizationManager.java @@ -95,7 +95,7 @@ public final class PostAuthorizeAuthorizationManager MethodSecurityExpressionHandler expressionHandler = this.registry.getExpressionHandler(); EvaluationContext ctx = expressionHandler.createEvaluationContext(authentication, mi.getMethodInvocation()); expressionHandler.setReturnObject(mi.getResult(), ctx); - return ExpressionUtils.evaluate(attribute.getExpression(), ctx); + return ExpressionUtils.evaluate(attribute.getExpression(), ctx, authentication, mi); } @Override diff --git a/core/src/main/java/org/springframework/security/authorization/method/PostAuthorizeReactiveAuthorizationManager.java b/core/src/main/java/org/springframework/security/authorization/method/PostAuthorizeReactiveAuthorizationManager.java index c857eaa51a..d1e57d050d 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/PostAuthorizeReactiveAuthorizationManager.java +++ b/core/src/main/java/org/springframework/security/authorization/method/PostAuthorizeReactiveAuthorizationManager.java @@ -91,7 +91,7 @@ public final class PostAuthorizeReactiveAuthorizationManager return authentication .map((auth) -> expressionHandler.createEvaluationContext(auth, mi)) .doOnNext((ctx) -> expressionHandler.setReturnObject(result.getResult(), ctx)) - .flatMap((ctx) -> ReactiveExpressionUtils.evaluate(attribute.getExpression(), ctx)) + .flatMap((ctx) -> ReactiveExpressionUtils.evaluate(attribute.getExpression(), ctx, authentication, result)) .cast(AuthorizationResult.class); // @formatter:on } diff --git a/core/src/main/java/org/springframework/security/authorization/method/PreAuthorizeAuthorizationManager.java b/core/src/main/java/org/springframework/security/authorization/method/PreAuthorizeAuthorizationManager.java index 4075448081..c37f62a2a9 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/PreAuthorizeAuthorizationManager.java +++ b/core/src/main/java/org/springframework/security/authorization/method/PreAuthorizeAuthorizationManager.java @@ -85,7 +85,7 @@ public final class PreAuthorizeAuthorizationManager return null; } EvaluationContext ctx = this.registry.getExpressionHandler().createEvaluationContext(authentication, mi); - return ExpressionUtils.evaluate(attribute.getExpression(), ctx); + return ExpressionUtils.evaluate(attribute.getExpression(), ctx, authentication, mi); } @Override diff --git a/core/src/main/java/org/springframework/security/authorization/method/PreAuthorizeReactiveAuthorizationManager.java b/core/src/main/java/org/springframework/security/authorization/method/PreAuthorizeReactiveAuthorizationManager.java index c181837f58..35bfb6cd40 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/PreAuthorizeReactiveAuthorizationManager.java +++ b/core/src/main/java/org/springframework/security/authorization/method/PreAuthorizeReactiveAuthorizationManager.java @@ -85,7 +85,7 @@ public final class PreAuthorizeReactiveAuthorizationManager // @formatter:off return authentication .map((auth) -> this.registry.getExpressionHandler().createEvaluationContext(auth, mi)) - .flatMap((ctx) -> ReactiveExpressionUtils.evaluate(attribute.getExpression(), ctx)) + .flatMap((ctx) -> ReactiveExpressionUtils.evaluate(attribute.getExpression(), ctx, authentication, mi)) .cast(AuthorizationResult.class); // @formatter:on } diff --git a/core/src/main/java/org/springframework/security/authorization/method/ReactiveExpressionUtils.java b/core/src/main/java/org/springframework/security/authorization/method/ReactiveExpressionUtils.java index bae15b1701..a47bd4fe8f 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/ReactiveExpressionUtils.java +++ b/core/src/main/java/org/springframework/security/authorization/method/ReactiveExpressionUtils.java @@ -24,6 +24,9 @@ import org.springframework.expression.EvaluationException; import org.springframework.expression.Expression; import org.springframework.security.authorization.AuthorizationResult; import org.springframework.security.authorization.ExpressionAuthorizationDecision; +import org.springframework.security.authorization.ReactiveAuthorizationManager; +import org.springframework.security.core.Authentication; +import org.springframework.util.Assert; /** * For internal use only, as this contract is likely to change. @@ -34,6 +37,11 @@ import org.springframework.security.authorization.ExpressionAuthorizationDecisio final class ReactiveExpressionUtils { static Mono evaluate(Expression expr, EvaluationContext ctx) { + return evaluate(expr, ctx, Mono.empty(), null); + } + + static Mono evaluate(Expression expr, EvaluationContext ctx, + Mono authentication, @Nullable T context) { return Mono.defer(() -> { Object value; try { @@ -43,6 +51,10 @@ final class ReactiveExpressionUtils { return Mono.error(() -> new IllegalArgumentException( "Failed to evaluate expression '" + expr.getExpressionString() + "'", ex)); } + if (value instanceof ReactiveAuthorizationManager manager) { + Assert.notNull(context, "context cannot be null"); + return ((ReactiveAuthorizationManager) manager).authorize(authentication, context); + } if (value instanceof Mono mono) { return mono.flatMap((data) -> adapt(expr, data)); } diff --git a/docs/modules/ROOT/pages/servlet/authorization/method-security.adoc b/docs/modules/ROOT/pages/servlet/authorization/method-security.adoc index 397e3d4b75..28f0009e1b 100644 --- a/docs/modules/ROOT/pages/servlet/authorization/method-security.adoc +++ b/docs/modules/ROOT/pages/servlet/authorization/method-security.adoc @@ -1362,6 +1362,12 @@ Note, though, that returning an object is preferred as this doesn't incur the ex Then, you can access the custom details when you <>. +[TIP] +==== +Further, you can return an `AuthorizationManager` itself. +This is helpful when unifying custom web authorization rules with method security ones since web security by default requires specifying an `AuthorizationManager` instance. +==== + [[custom-authorization-managers]] === Using a Custom Authorization Manager