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 8e777e8bc4c..6acd05d2f4d 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.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 @@ static MethodInterceptor postAuthorizeAuthorizationMethodInterceptor( @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 5fe335870d7..6fb3441ff72 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 @@ void preAuthorizeWhenAllowedAndHandlerWithCustomAnnotationUsingBeanThenInvokeMet 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 @@ ReactiveMethodSecurityService methodSecurityService() { } + @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 000dcb386a0..e2c3bef113d 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 acf50eb1130..3787556a878 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 Mono checkCustomResult(boolean result) { 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 cfc4a35cad3..42a68da6093 100644 --- a/docs/modules/ROOT/pages/reactive/authorization/method.adoc +++ b/docs/modules/ROOT/pages/reactive/authorization/method.adoc @@ -88,39 +88,6 @@ public Function> func() { ---- ====== -=== Customizing Authorization - -Spring Security's `@PreAuthorize`, `@PostAuthorize`, `@PreFilter`, and `@PostFilter` ship with rich expression-based support. - - -[[jc-reactive-method-security-custom-granted-authority-defaults]] -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 -[tabs] -====== -Java:: -+ -[source,java,role="primary"] ----- -@Bean -@Role(BeanDefinition.ROLE_INFRASTRUCTURE) -static GrantedAuthorityDefaults grantedAuthorityDefaults() { - return new GrantedAuthorityDefaults("MYPREFIX_"); -} ----- -====== - -[TIP] -==== -We expose `GrantedAuthorityDefaults` using a `static` method to ensure that Spring publishes it before it initializes Spring Security's method security `@Configuration` classes. -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]). -==== - -[[jc-reactive-method-security-custom-authorization-manager]] -=== Custom Authorization Managers - Method authorization is a combination of before- and after-method authorization. [NOTE] @@ -175,10 +142,17 @@ class MethodSecurityConfig { ====== Notice that Spring Security's method security is built using Spring AOP. -So, interceptors are invoked based on the order specified. -This can be customized by calling `setOrder` on the interceptor instances like so: -.Publish Custom Advisor +=== Customizing Authorization + +Spring Security's `@PreAuthorize`, `@PostAuthorize`, `@PreFilter`, and `@PostFilter` ship with rich expression-based support. + + +[[jc-reactive-method-security-custom-granted-authority-defaults]] +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 GrantedAuthorityDefaults [tabs] ====== Java:: @@ -187,46 +161,101 @@ Java:: ---- @Bean @Role(BeanDefinition.ROLE_INFRASTRUCTURE) -Advisor postFilterAuthorizationMethodInterceptor() { - PostFilterAuthorizationMethodInterceptor interceptor = new PostFilterAuthorizationReactiveMethodInterceptor(); - interceptor.setOrder(AuthorizationInterceptorOrders.POST_AUTHORIZE.getOrder() - 1); - return interceptor; +static GrantedAuthorityDefaults grantedAuthorityDefaults() { + return new GrantedAuthorityDefaults("MYPREFIX_"); } ---- ====== -You may want to only support `@PreAuthorize` in your application, in which case you can do the following: +[TIP] +==== +We expose `GrantedAuthorityDefaults` using a `static` method to ensure that Spring publishes it before it initializes Spring Security's method security `@Configuration` classes. +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]). +==== + +[[use-programmatic-authorization]] +== Authorizing Methods Programmatically + +As you've already seen, there are several ways that you can specify non-trivial authorization rules using xref:servlet/authorization/method-security.adoc#authorization-expressions[Method Security SpEL expressions]. + +There are a number of ways that you can instead allow your logic to be Java-based instead of SpEL-based. +This gives use access the entire Java language for increased testability and flow control. + +=== Using a Custom Bean in SpEL + +The first way to authorize a method programmatically is a two-step process. + +First, declare a bean that has a method that takes a `MethodSecurityExpressionOperations` instance like the following: -.Only @PreAuthorize Configuration [tabs] ====== Java:: + [source,java,role="primary"] ---- -@Configuration -class MethodSecurityConfig { - @Bean - BeanDefinitionRegistryPostProcessor aopConfig() { - return AopConfigUtils::registerAutoProxyCreatorIfNecessary; - } +@Component("authz") +public class AuthorizationLogic { + public decide(MethodSecurityExpressionOperations operations): Mono { + // ... authorization logic + } +} +---- - @Bean - @Role(BeanDefinition.ROLE_INFRASTRUCTURE) - Advisor preAuthorize() { - return AuthorizationManagerBeforeMethodInterceptor.preAuthorize(); - } +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +@Component("authz") +open class AuthorizationLogic { + fun decide(val operations: MethodSecurityExpressionOperations): Mono { + // ... authorization logic + } +} +---- +====== + +Then, reference that bean in your annotations in the following way: + +[tabs] +====== +Java:: ++ +[source,java,role="primary"] +---- +@Controller +public class MyController { + @PreAuthorize("@authz.decide(#root)") + @GetMapping("/endpoint") + public Mono endpoint() { + // ... + } +} +---- + +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +@Controller +open class MyController { + @PreAuthorize("@authz.decide(#root)") + @GetMapping("/endpoint") + fun endpoint(): Mono { + // ... + } } ---- ====== -Or, you may have a custom before-method `ReactiveAuthorizationManager` that you want to add to the list. +Spring Security will invoke the given method on that bean for each method invocation. -In this case, you will need to tell Spring Security both the `ReactiveAuthorizationManager` and to which methods and classes your authorization manager applies. +What's nice about this is all your authorization logic is in a separate class that can be independently unit tested and verified for correctness. +It also has access to the full Java language. -Thus, you can configure Spring Security to invoke your `ReactiveAuthorizationManager` in between `@PreAuthorize` and `@PostAuthorize` like so: +[TIP] +In addition to returning a `Mono`, you can also return `Mono.empty()` to indicate that the code abstains from making a decision. -.Custom Before Advisor +If you want to include more information about the nature of the decision, you can instead return a custom `AuthorizationDecision` like this: [tabs] ====== @@ -234,75 +263,168 @@ Java:: + [source,java,role="primary"] ---- -@EnableReactiveMethodSecurity(useAuthorizationManager=true) -class MethodSecurityConfig { - @Bean - @Role(BeanDefinition.ROLE_INFRASTRUCTURE) - public Advisor customAuthorize() { - JdkRegexpMethodPointcut pattern = new JdkRegexpMethodPointcut(); - pattern.setPattern("org.mycompany.myapp.service.*"); - ReactiveAuthorizationManager rule = AuthorityAuthorizationManager.isAuthenticated(); - AuthorizationManagerBeforeReactiveMethodInterceptor interceptor = new AuthorizationManagerBeforeReactiveMethodInterceptor(pattern, rule); - interceptor.setOrder(AuthorizationInterceptorsOrder.PRE_AUTHORIZE_ADVISOR_ORDER.getOrder() + 1); - return interceptor; +@Component("authz") +public class AuthorizationLogic { + public Mono decide(MethodSecurityExpressionOperations operations) { + // ... authorization logic + return Mono.just(new MyAuthorizationDecision(false, details)); + } +} +---- + +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +@Component("authz") +open class AuthorizationLogic { + fun decide(val operations: MethodSecurityExpressionOperations): Mono { + // ... authorization logic + return Mono.just(MyAuthorizationDecision(false, details)) } } ---- ====== -[TIP] -==== -You can place your interceptor in between Spring Security method interceptors using the order constants specified in `AuthorizationInterceptorsOrder`. -==== +Or throw a custom `AuthorizationDeniedException` instance. +Note, though, that returning an object is preferred as this doesn't incur the expense of generating a stacktrace. + +Then, you can access the custom details when you xref:servlet/authorization/method-security.adoc#fallback-values-authorization-denied[customize how the authorization result is handled]. -The same can be done for after-method authorization. -After-method authorization is generally concerned with analysing the return value to verify access. +[[jc-reactive-method-security-custom-authorization-manager]] +[[custom-authorization-managers]] +=== Using a Custom Authorization Manager -For example, you might have a method that confirms that the account requested actually belongs to the logged-in user like so: +The second way to authorize a method programmatically is to create a custom xref:servlet/authorization/architecture.adoc#_the_authorizationmanager[`AuthorizationManager`]. + +First, declare an authorization manager instance, perhaps like this one: -.@PostAuthorize example [tabs] ====== Java:: + [source,java,role="primary"] ---- -public interface BankService { +@Component +public class MyPreAuthorizeAuthorizationManager implements ReactiveAuthorizationManager { + @Override + public Mono check(Supplier authentication, MethodInvocation invocation) { + // ... authorization logic + } - @PreAuthorize("hasRole('USER')") - @PostAuthorize("returnObject.owner == authentication.name") - Mono readAccount(Long id); } ---- -====== -You can supply your own `AuthorizationMethodInterceptor` to customize how access to the return value is evaluated. +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +@Component +class MyPreAuthorizeAuthorizationManager : ReactiveAuthorizationManager { + override fun check(authentication: Supplier, invocation: MethodInvocation): Mono { + // ... authorization logic + } -For example, if you have your own custom annotation, you can configure it like so: +} +---- +====== +Then, publish the method interceptor with a pointcut that corresponds to when you want that `ReactiveAuthorizationManager` to run. +For example, you could replace how `@PreAuthorize` and `@PostAuthorize` work like so: -.Custom After Advisor +.Only @PreAuthorize and @PostAuthorize Configuration [tabs] ====== Java:: + [source,java,role="primary"] ---- -@EnableReactiveMethodSecurity(useAuthorizationManager=true) +@Configuration +@EnableMethodSecurity(prePostEnabled = false) class MethodSecurityConfig { + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + Advisor preAuthorize(MyPreAuthorizeAuthorizationManager manager) { + return AuthorizationManagerBeforeReactiveMethodInterceptor.preAuthorize(manager); + } + @Bean @Role(BeanDefinition.ROLE_INFRASTRUCTURE) - public Advisor customAuthorize(ReactiveAuthorizationManager rules) { - AnnotationMethodMatcher pattern = new AnnotationMethodMatcher(MySecurityAnnotation.class); - AuthorizationManagerAfterReactiveMethodInterceptor interceptor = new AuthorizationManagerAfterReactiveMethodInterceptor(pattern, rules); - interceptor.setOrder(AuthorizationInterceptorsOrder.POST_AUTHORIZE_ADVISOR_ORDER.getOrder() + 1); - return interceptor; + Advisor postAuthorize(MyPostAuthorizeAuthorizationManager manager) { + return AuthorizationManagerAfterReactiveMethodInterceptor.postAuthorize(manager); + } +} +---- + +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +@Configuration +@EnableMethodSecurity(prePostEnabled = false) +class MethodSecurityConfig { + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + fun preAuthorize(val manager: MyPreAuthorizeAuthorizationManager) : Advisor { + return AuthorizationManagerBeforeReactiveMethodInterceptor.preAuthorize(manager) + } + + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + fun postAuthorize(val manager: MyPostAuthorizeAuthorizationManager) : Advisor { + return AuthorizationManagerAfterReactiveMethodInterceptor.postAuthorize(manager) + } +} +---- +====== + +[TIP] +==== +You can place your interceptor in between Spring Security method interceptors using the order constants specified in `AuthorizationInterceptorsOrder`. +==== + +[[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 } } ---- ====== -and it will be invoked after the `@PostAuthorize` interceptor. +[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. == EnableReactiveMethodSecurity