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 9a9ff57da49..145f344d126 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 @@ -18,6 +18,8 @@ import reactor.core.publisher.Mono; +import org.springframework.security.authorization.AuthorizationDecision; +import org.springframework.security.authorization.AuthorizationResult; import org.springframework.security.core.Authentication; import org.springframework.stereotype.Component; @@ -45,4 +47,20 @@ public boolean check(Authentication authentication, String message) { return message != null && message.contains(authentication.getName()); } + public AuthorizationResult checkResult(boolean result) { + return new AuthzResult(result); + } + + public Mono checkReactiveResult(boolean result) { + return Mono.just(checkResult(result)); + } + + public static class AuthzResult extends AuthorizationDecision { + + public AuthzResult(boolean granted) { + super(granted); + } + + } + } 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 c3162debb3c..f8e1889c38d 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 @@ -173,6 +173,11 @@ public interface MethodSecurityService { @PreAuthorize(value = "hasRole('ADMIN')", handlerClass = UserFallbackDeniedHandler.class) UserRecordWithEmailProtected getUserWithFallbackWhenUnauthorized(); + @PreAuthorize(value = "@authz.checkResult(#result)", handlerClass = MethodAuthorizationDeniedHandler.class) + @PostAuthorize(value = "@authz.checkResult(!#result)", + postProcessorClass = MethodAuthorizationDeniedPostProcessor.class) + String checkCustomResult(boolean result); + class StarMaskingHandler implements MethodAuthorizationDeniedHandler { @Override diff --git a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/MethodSecurityServiceConfig.java b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/MethodSecurityServiceConfig.java index ee664f5a45e..a5c78f6d962 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/MethodSecurityServiceConfig.java +++ b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/MethodSecurityServiceConfig.java @@ -28,4 +28,14 @@ MethodSecurityService service() { return new MethodSecurityServiceImpl(); } + @Bean + ReactiveMethodSecurityService reactiveService() { + return new ReactiveMethodSecurityServiceImpl(); + } + + @Bean + Authz authz() { + return new Authz(); + } + } 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 e44e9e048c6..4fd5b7fe1ad 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 @@ -197,4 +197,9 @@ public UserRecordWithEmailProtected getUserWithFallbackWhenUnauthorized() { return new UserRecordWithEmailProtected("username", "useremail@example.com"); } + @Override + public String checkCustomResult(boolean result) { + return "ok"; + } + } 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 72981bbd83c..f9b16350b60 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 @@ -66,6 +66,8 @@ import org.springframework.security.authorization.method.AuthorizationInterceptorsOrder; import org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor; import org.springframework.security.authorization.method.AuthorizeReturnObject; +import org.springframework.security.authorization.method.MethodAuthorizationDeniedHandler; +import org.springframework.security.authorization.method.MethodAuthorizationDeniedPostProcessor; import org.springframework.security.authorization.method.MethodInvocationResult; import org.springframework.security.authorization.method.PrePostTemplateDefaults; import org.springframework.security.config.Customizer; @@ -92,6 +94,8 @@ import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.verifyNoMoreInteractions; /** * Tests for {@link PrePostMethodSecurityConfiguration}. @@ -925,6 +929,23 @@ void getUserWhenNotAuthorizedAndHandlerFallbackValueThenReturnFallbackValue() { assertThat(user.name()).isEqualTo("Protected"); } + @Test + @WithMockUser + void getUserWhenNotAuthorizedThenHandlerUsesCustomAuthorizationDecision() { + this.spring.register(MethodSecurityServiceConfig.class, CustomResultConfig.class).autowire(); + MethodSecurityService service = this.spring.getContext().getBean(MethodSecurityService.class); + MethodAuthorizationDeniedHandler handler = this.spring.getContext() + .getBean(MethodAuthorizationDeniedHandler.class); + MethodAuthorizationDeniedPostProcessor postProcessor = this.spring.getContext() + .getBean(MethodAuthorizationDeniedPostProcessor.class); + assertThat(service.checkCustomResult(false)).isNull(); + verify(handler).handle(any(), any(Authz.AuthzResult.class)); + verifyNoInteractions(postProcessor); + assertThat(service.checkCustomResult(true)).isNull(); + verify(postProcessor).postProcessResult(any(), any(Authz.AuthzResult.class)); + verifyNoMoreInteractions(handler); + } + private static Consumer disallowBeanOverriding() { return (context) -> ((AnnotationConfigWebApplicationContext) context).setAllowBeanDefinitionOverriding(false); } @@ -1449,4 +1470,23 @@ public String getName() { } + @EnableMethodSecurity + static class CustomResultConfig { + + MethodAuthorizationDeniedHandler handler = mock(MethodAuthorizationDeniedHandler.class); + + MethodAuthorizationDeniedPostProcessor postProcessor = mock(MethodAuthorizationDeniedPostProcessor.class); + + @Bean + MethodAuthorizationDeniedHandler methodAuthorizationDeniedHandler() { + return this.handler; + } + + @Bean + MethodAuthorizationDeniedPostProcessor methodAuthorizationDeniedPostProcessor() { + return this.postProcessor; + } + + } + } 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 60c54195bf8..1a2fc446a7c 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 @@ -47,6 +47,8 @@ import org.springframework.security.authorization.method.AuthorizationAdvisorProxyFactory; import org.springframework.security.authorization.method.AuthorizationAdvisorProxyFactory.TargetVisitor; import org.springframework.security.authorization.method.AuthorizeReturnObject; +import org.springframework.security.authorization.method.MethodAuthorizationDeniedHandler; +import org.springframework.security.authorization.method.MethodAuthorizationDeniedPostProcessor; import org.springframework.security.config.Customizer; import org.springframework.security.config.core.GrantedAuthorityDefaults; import org.springframework.security.config.test.SpringTestContext; @@ -54,8 +56,14 @@ import org.springframework.security.core.Authentication; import org.springframework.security.core.context.ReactiveSecurityContextHolder; 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.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.verifyNoMoreInteractions; /** * @author Tadaya Tsuyukubo @@ -65,7 +73,7 @@ public class ReactiveMethodSecurityConfigurationTests { public final SpringTestContext spring = new SpringTestContext(this); - @Autowired + @Autowired(required = false) DefaultMethodSecurityExpressionHandler methodSecurityExpressionHandler; @Test @@ -212,6 +220,23 @@ public void findAllWhenNestedPreAuthorizeThenAuthorizes() { .verifyError(AccessDeniedException.class); } + @Test + @WithMockUser + void getUserWhenNotAuthorizedThenHandlerUsesCustomAuthorizationDecision() { + this.spring.register(MethodSecurityServiceConfig.class, CustomResultConfig.class).autowire(); + ReactiveMethodSecurityService service = this.spring.getContext().getBean(ReactiveMethodSecurityService.class); + MethodAuthorizationDeniedHandler handler = this.spring.getContext() + .getBean(MethodAuthorizationDeniedHandler.class); + MethodAuthorizationDeniedPostProcessor postProcessor = this.spring.getContext() + .getBean(MethodAuthorizationDeniedPostProcessor.class); + assertThat(service.checkCustomResult(false).block()).isNull(); + verify(handler).handle(any(), any(Authz.AuthzResult.class)); + verifyNoInteractions(postProcessor); + assertThat(service.checkCustomResult(true).block()).isNull(); + verify(postProcessor).postProcessResult(any(), any(Authz.AuthzResult.class)); + verifyNoMoreInteractions(handler); + } + private static Consumer authorities(String... authorities) { return (builder) -> builder.authorities(authorities); } @@ -353,4 +378,23 @@ public Mono getName() { } + @EnableReactiveMethodSecurity + static class CustomResultConfig { + + MethodAuthorizationDeniedHandler handler = mock(MethodAuthorizationDeniedHandler.class); + + MethodAuthorizationDeniedPostProcessor postProcessor = mock(MethodAuthorizationDeniedPostProcessor.class); + + @Bean + MethodAuthorizationDeniedHandler methodAuthorizationDeniedHandler() { + return this.handler; + } + + @Bean + MethodAuthorizationDeniedPostProcessor methodAuthorizationDeniedPostProcessor() { + return this.postProcessor; + } + + } + } 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 836340a7eec..66c83348cb2 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 @@ -85,6 +85,11 @@ public interface ReactiveMethodSecurityService { @Mask(expression = "@myMasker.getMask(returnObject)") Mono postAuthorizeWithMaskAnnotationUsingBean(); + @PreAuthorize(value = "@authz.checkReactiveResult(#result)", handlerClass = MethodAuthorizationDeniedHandler.class) + @PostAuthorize(value = "@authz.checkReactiveResult(!#result)", + postProcessorClass = MethodAuthorizationDeniedPostProcessor.class) + Mono checkCustomResult(boolean result); + class StarMaskingHandler implements MethodAuthorizationDeniedHandler { @Override 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 7fb421585aa..ce6a0204b64 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 @@ -82,4 +82,9 @@ public Mono postAuthorizeWithMaskAnnotationUsingBean() { return Mono.just("ok"); } + @Override + public Mono checkCustomResult(boolean result) { + return Mono.just("ok"); + } + } diff --git a/core/src/main/java/org/springframework/security/access/expression/ExpressionUtils.java b/core/src/main/java/org/springframework/security/access/expression/ExpressionUtils.java index feed5bd543d..1dff67ec985 100644 --- a/core/src/main/java/org/springframework/security/access/expression/ExpressionUtils.java +++ b/core/src/main/java/org/springframework/security/access/expression/ExpressionUtils.java @@ -20,6 +20,7 @@ import org.springframework.expression.EvaluationException; import org.springframework.expression.Expression; import org.springframework.security.authorization.AuthorizationDecision; +import org.springframework.security.authorization.AuthorizationResult; import org.springframework.security.authorization.ExpressionAuthorizationDecision; public final class ExpressionUtils { @@ -34,10 +35,10 @@ private ExpressionUtils() { * @return the resulting {@link AuthorizationDecision} * @since 6.3 */ - public static AuthorizationDecision evaluate(Expression expr, EvaluationContext ctx) { + public static AuthorizationResult evaluate(Expression expr, EvaluationContext ctx) { try { Object result = expr.getValue(ctx); - if (result instanceof AuthorizationDecision decision) { + if (result instanceof AuthorizationResult decision) { return decision; } if (result instanceof Boolean granted) { 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 0121c2478ac..02022d2ccc3 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 @@ -93,7 +93,7 @@ public AuthorizationDecision check(Supplier authentication, Meth MethodSecurityExpressionHandler expressionHandler = this.registry.getExpressionHandler(); EvaluationContext ctx = expressionHandler.createEvaluationContext(authentication, mi.getMethodInvocation()); expressionHandler.setReturnObject(mi.getResult(), ctx); - return ExpressionUtils.evaluate(attribute.getExpression(), ctx); + return (AuthorizationDecision) ExpressionUtils.evaluate(attribute.getExpression(), ctx); } @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 08d166fdd6c..4c988c05933 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 @@ -89,7 +89,8 @@ public Mono check(Mono authentication, Me 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)) + .cast(AuthorizationDecision.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 b022cbbdb19..fdab49b3402 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 @@ -83,7 +83,7 @@ public AuthorizationDecision check(Supplier authentication, Meth return null; } EvaluationContext ctx = this.registry.getExpressionHandler().createEvaluationContext(authentication, mi); - return ExpressionUtils.evaluate(attribute.getExpression(), ctx); + return (AuthorizationDecision) ExpressionUtils.evaluate(attribute.getExpression(), ctx); } @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 a2b1674cc65..b9b9b2cc6e8 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 @@ -84,7 +84,8 @@ public Mono check(Mono authentication, Me // @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)) + .cast(AuthorizationDecision.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 9de15886d95..6c80c0d364e 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 @@ -21,7 +21,7 @@ import org.springframework.expression.EvaluationContext; import org.springframework.expression.EvaluationException; import org.springframework.expression.Expression; -import org.springframework.security.authorization.AuthorizationDecision; +import org.springframework.security.authorization.AuthorizationResult; import org.springframework.security.authorization.ExpressionAuthorizationDecision; /** @@ -32,7 +32,7 @@ */ final class ReactiveExpressionUtils { - static Mono evaluate(Expression expr, EvaluationContext ctx) { + static Mono evaluate(Expression expr, EvaluationContext ctx) { return Mono.defer(() -> { Object value; try { @@ -49,11 +49,11 @@ static Mono evaluate(Expression expr, EvaluationContext c }); } - private static Mono adapt(Expression expr, Object value) { + private static Mono adapt(Expression expr, Object value) { if (value instanceof Boolean granted) { return Mono.just(new ExpressionAuthorizationDecision(granted, expr)); } - if (value instanceof AuthorizationDecision decision) { + if (value instanceof AuthorizationResult decision) { return Mono.just(decision); } return createInvalidReturnTypeMono(expr); @@ -86,8 +86,8 @@ static Mono evaluateAsBoolean(Expression expr, EvaluationContext ctx) { } private static Mono createInvalidReturnTypeMono(Expression expr) { - return Mono.error(() -> new IllegalStateException( - "Expression: '" + expr.getExpressionString() + "' must return boolean or Mono")); + return Mono.error(() -> new IllegalStateException("Expression: '" + expr.getExpressionString() + + "' must return boolean, Mono, AuthorizationResult, or Mono")); } private ReactiveExpressionUtils() { diff --git a/docs/modules/ROOT/pages/servlet/authorization/method-security.adoc b/docs/modules/ROOT/pages/servlet/authorization/method-security.adoc index 9380b5cb28f..2a6be2454df 100644 --- a/docs/modules/ROOT/pages/servlet/authorization/method-security.adoc +++ b/docs/modules/ROOT/pages/servlet/authorization/method-security.adoc @@ -1218,7 +1218,7 @@ It also has access to the full Java language. [TIP] In addition to returning a `Boolean`, you can also return `null` to indicate that the code abstains from making a decision. -If you want to include more information about the nature of the decision, you can instead return a a custom `AuthorizationDecision` like this: +If you want to include more information about the nature of the decision, you can instead return a custom `AuthorizationDecision` like this: [tabs] ====== @@ -1249,7 +1249,7 @@ open class AuthorizationLogic { ---- ====== - +Then, you can access the custom details when you <>. [[custom-authorization-managers]] === Using a Custom Authorization Manager