diff --git a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/ReactiveAuthorizationManagerMethodSecurityConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/ReactiveAuthorizationManagerMethodSecurityConfiguration.java index 8e777e8bc4..6acd05d2f4 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/ReactiveAuthorizationManagerMethodSecurityConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/ReactiveAuthorizationManagerMethodSecurityConfiguration.java @@ -34,6 +34,7 @@ import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Fallback; import org.springframework.context.annotation.Role; import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler; import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler; @@ -114,6 +115,7 @@ final class ReactiveAuthorizationManagerMethodSecurityConfiguration implements A @Bean @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + @Fallback static DefaultMethodSecurityExpressionHandler methodSecurityExpressionHandler( @Autowired(required = false) GrantedAuthorityDefaults grantedAuthorityDefaults) { DefaultMethodSecurityExpressionHandler handler = new DefaultMethodSecurityExpressionHandler(); diff --git a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/PrePostReactiveMethodSecurityConfigurationTests.java b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/PrePostReactiveMethodSecurityConfigurationTests.java index 5fe335870d..6fb3441ff7 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/PrePostReactiveMethodSecurityConfigurationTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/PrePostReactiveMethodSecurityConfigurationTests.java @@ -16,14 +16,22 @@ package org.springframework.security.config.annotation.method.configuration; +import java.io.Serializable; + import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import reactor.test.StepVerifier; +import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Role; +import org.springframework.security.access.PermissionEvaluator; +import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler; +import org.springframework.security.authorization.AuthorizationDeniedException; import org.springframework.security.config.test.SpringTestContext; import org.springframework.security.config.test.SpringTestContextExtension; +import org.springframework.security.core.Authentication; import org.springframework.security.test.context.annotation.SecurityTestExecutionListeners; import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.context.junit.jupiter.SpringExtension; @@ -201,6 +209,17 @@ public class PrePostReactiveMethodSecurityConfigurationTests { StepVerifier.create(service.preAuthorizeWithMaskAnnotationUsingBean()).expectNext("ok").verifyComplete(); } + @Test + @WithMockUser(roles = "ADMIN") + public void customMethodSecurityExpressionHandler() { + this.spring.register(MethodSecurityServiceEnabledConfig.class, PermissionEvaluatorConfig.class).autowire(); + ReactiveMethodSecurityService service = this.spring.getContext().getBean(ReactiveMethodSecurityService.class); + StepVerifier.create(service.preAuthorizeHasPermission("grant")).expectNext("ok").verifyComplete(); + StepVerifier.create(service.preAuthorizeHasPermission("deny")) + .expectError(AuthorizationDeniedException.class) + .verify(); + } + @Configuration @EnableReactiveMethodSecurity static class MethodSecurityServiceEnabledConfig { @@ -212,4 +231,29 @@ public class PrePostReactiveMethodSecurityConfigurationTests { } + @Configuration + static class PermissionEvaluatorConfig { + + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + static DefaultMethodSecurityExpressionHandler methodSecurityExpressionHandler() { + DefaultMethodSecurityExpressionHandler handler = new DefaultMethodSecurityExpressionHandler(); + handler.setPermissionEvaluator(new PermissionEvaluator() { + @Override + public boolean hasPermission(Authentication authentication, Object targetDomainObject, + Object permission) { + return "grant".equals(targetDomainObject); + } + + @Override + public boolean hasPermission(Authentication authentication, Serializable targetId, String targetType, + Object permission) { + throw new UnsupportedOperationException(); + } + }); + return handler; + } + + } + } 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 000dcb386a..e2c3bef113 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 @@ -101,6 +101,9 @@ public interface ReactiveMethodSecurityService { @HandleAuthorizationDenied(handlerClass = MethodAuthorizationDeniedHandler.class) Mono checkCustomResult(boolean result); + @PreAuthorize("hasPermission(#kgName, 'read')") + Mono preAuthorizeHasPermission(String kgName); + 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 acf50eb113..3787556a87 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 @@ -88,4 +88,9 @@ public class ReactiveMethodSecurityServiceImpl implements ReactiveMethodSecurity return Mono.just("ok"); } + @Override + public Mono preAuthorizeHasPermission(String kgName) { + return Mono.just("ok"); + } + } diff --git a/docs/modules/ROOT/pages/reactive/authorization/method.adoc b/docs/modules/ROOT/pages/reactive/authorization/method.adoc index cfc4a35cad..2f1601db39 100644 --- a/docs/modules/ROOT/pages/reactive/authorization/method.adoc +++ b/docs/modules/ROOT/pages/reactive/authorization/method.adoc @@ -97,7 +97,7 @@ Spring Security's `@PreAuthorize`, `@PostAuthorize`, `@PreFilter`, and `@PostFil Also, for role-based authorization, Spring Security adds a default `ROLE_` prefix, which is uses when evaluating expressions like `hasRole`. You can configure the authorization rules to use a different prefix by exposing a `GrantedAuthorityDefaults` bean, like so: -.Custom MethodSecurityExpressionHandler +.Custom GrantedAuthorityDefaults [tabs] ====== Java:: @@ -118,6 +118,63 @@ We expose `GrantedAuthorityDefaults` using a `static` method to ensure that Spri Since the `GrantedAuthorityDefaults` bean is part of internal workings of Spring Security, we should also expose it as an infrastructural bean effectively avoiding some warnings related to bean post-processing (see https://github.com/spring-projects/spring-security/issues/14751[gh-14751]). ==== +[[customizing-expression-handling]] +=== Customizing Expression Handling + +Or, third, you can customize how each SpEL expression is handled. +To do that, you can expose a custom `MethodSecurityExpressionHandler`, like so: + +.Custom MethodSecurityExpressionHandler +[tabs] +====== +Java:: ++ +[source,java,role="primary"] +---- +@Bean +static MethodSecurityExpressionHandler methodSecurityExpressionHandler(RoleHierarchy roleHierarchy) { + DefaultMethodSecurityExpressionHandler handler = new DefaultMethodSecurityExpressionHandler(); + handler.setRoleHierarchy(roleHierarchy); + return handler; +} +---- + +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +companion object { + @Bean + fun methodSecurityExpressionHandler(val roleHierarchy: RoleHierarchy) : MethodSecurityExpressionHandler { + val handler = DefaultMethodSecurityExpressionHandler() + handler.setRoleHierarchy(roleHierarchy) + return handler + } +} +---- + +Xml:: ++ +[source,xml,role="secondary"] +---- + + + + + + + +---- +====== + +[TIP] +==== +We expose `MethodSecurityExpressionHandler` using a `static` method to ensure that Spring publishes it before it initializes Spring Security's method security `@Configuration` classes +==== + +You can also subclass xref:servlet/authorization/method-security.adoc#subclass-defaultmethodsecurityexpressionhandler[`DefaultMessageSecurityExpressionHandler`] to add your own custom authorization expressions beyond the defaults. + [[jc-reactive-method-security-custom-authorization-manager]] === Custom Authorization Managers