Skip to content

Commit

Permalink
Support custom MethodSecurityExpressionHandler
Browse files Browse the repository at this point in the history
Closes gh-15715
  • Loading branch information
kse-music authored and jzheaux committed Sep 10, 2024
1 parent 2424e76 commit 60cd8fd
Show file tree
Hide file tree
Showing 5 changed files with 112 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Expand All @@ -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;
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,9 @@ public interface ReactiveMethodSecurityService {
@HandleAuthorizationDenied(handlerClass = MethodAuthorizationDeniedHandler.class)
Mono<String> checkCustomResult(boolean result);

@PreAuthorize("hasPermission(#kgName, 'read')")
Mono<String> preAuthorizeHasPermission(String kgName);

class StarMaskingHandler implements MethodAuthorizationDeniedHandler {

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,4 +88,9 @@ public Mono<String> checkCustomResult(boolean result) {
return Mono.just("ok");
}

@Override
public Mono<String> preAuthorizeHasPermission(String kgName) {
return Mono.just("ok");
}

}
59 changes: 58 additions & 1 deletion docs/modules/ROOT/pages/reactive/authorization/method.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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::
Expand All @@ -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"]
----
<sec:method-security>
<sec:expression-handler ref="myExpressionHandler"/>
</sec:method-security>
<bean id="myExpressionHandler"
class="org.springframework.security.messaging.access.expression.DefaultMessageSecurityExpressionHandler">
<property name="roleHierarchy" ref="roleHierarchy"/>
</bean>
----
======

[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

Expand Down

0 comments on commit 60cd8fd

Please sign in to comment.