Configure JSR250 role hierarchy in Spring 6

505 views Asked by At

When updating to Spring Security 6, the JSR250 annotation @RolesAllowed on my @RestController doesn't take the defined roleHierarchy into account.

Related to: AccessDecisionVoter Deprecated with Spring Security 6.x

Since Spring Security 6, the AccessDecisionVoter is deprecated and the suggested way, from the thread above, is to "simply expose a expressionHandler". This didn't work for me with JSR250 enabled.

@Bean
public DefaultMethodSecurityExpressionHandler expressionHandler() {
    DefaultMethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler();
    expressionHandler.setRoleHierarchy(roleHierarchy());
    return expressionHandler;
}

@Bean
public RoleHierarchy roleHierarchy() {
    RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
    String hierarchy = "a > b";
    roleHierarchy.setHierarchy(hierarchy);
    return roleHierarchy;
}

It seems like the created AuthorityAuthorizationManager by Jsr250AuthorizationManagerRegistry.resolveManager for RolesAllowed doesn't take the expressionHandler nor DefaultMethodSecurityExpressionHandler into account.

The AuthorityAuthorizationManager does have a field for a roleHierarchy to be set, but I couldn't figure out how or when this is supposed to be called.

I would have commented on the related post above but unfortunately I don't have the required reputation.

1

There are 1 answers

2
Artenes Nogueira On

Unfortunately there is no support to add role hierarchy to JSR250 manager. But there is a workaround that basically clones the library's implementation. At this point it makes more sense to drop JSR250 since you will be just replicating the logic from the libs to your code base, but if you want to do it anyway, just follow these instructions:

  1. Create a custom manager that will deal with the JSR250 annotations:
public class Jsr250CustomAuthorizationManager implements AuthorizationManager<MethodInvocation> {

    static final AuthorizationManager<MethodInvocation> NULL_MANAGER = (a, o) -> null;

    private final RoleHierarchy roleHierarchy;

    public Jsr250CustomAuthorizationManager(RoleHierarchy roleHierarchy) {
        this.roleHierarchy = roleHierarchy;
    }

    @Override
    public AuthorizationDecision check(Supplier<Authentication> authentication, MethodInvocation methodInvocation) {
        var manager = resolveManager(methodInvocation);
        return manager.check(authentication, methodInvocation);
    }

    public AuthorizationManager<MethodInvocation> resolveManager(MethodInvocation methodInvocation) {
        if (hasDenyAll(methodInvocation)) {
            return (a, o) -> new AuthorizationDecision(false);
        }

        if (hasPermitAll(methodInvocation)) {
            return (a, o) -> new AuthorizationDecision(true);
        }

        if (hasRolesAllowed(methodInvocation)) {
            var rolesAllowed = (RolesAllowed) methodInvocation.getMethod().getAnnotation(RolesAllowed.class);
            var manager = AuthorityAuthorizationManager.<MethodInvocation>hasAnyRole(rolesAllowed.value());
            manager.setRoleHierarchy(roleHierarchy);
            return manager;
        }

        return NULL_MANAGER;
    }

    public boolean hasDenyAll(MethodInvocation methodInvocation) {
        return methodInvocation.getMethod().getAnnotation(DenyAll.class) != null;
    }

    public boolean hasPermitAll(MethodInvocation methodInvocation) {
        return methodInvocation.getMethod().getAnnotation(PermitAll.class) != null;
    }

    public boolean hasRolesAllowed(MethodInvocation methodInvocation) {
        return methodInvocation.getMethod().getAnnotation(RolesAllowed.class) != null;
    }

}

You can just copy and paste the code above into a new class in your code, this is a class that I made myself based on the one that exists in the spring security lib.

  1. expose this new instance as a bean of type Advisor:
@EnableMethodSecurity(jsr250Enabled = true)
public class MethodSecuritytConfiguration {

    @Bean
    public Advisor jsr250AuthorizationMethodInterceptor() {
        RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
        roleHierarchy.setHierarchy("ROLE_ADMIN > ROLE_USER");
        var manager = new Jsr250CustomAuthorizationManager(roleHierarchy);
        return AuthorizationManagerBeforeMethodInterceptor.jsr250(manager);
    }

}
  1. In case you are using @EnableMethodSecurity(jsr250Enabled = true), you will also need to add this configuration to your application.properties:
spring.main.allow-bean-definition-overriding=true

This will allow to override a bean that is defined in the security lib that deals with JSR250 annotations. Because this bean's configuration is hardcoded in the lib wihtout exposing a way to change it, we have to override it altogheter to add the behavior we need. Note that the name of your bean needs to be jsr250AuthorizationMethodInterceptor to override the one (with same name) from the security lib. If you remove the jsr250Enabled configuration from EnableMethodSecurity, then you can name your bean anything you want and remove the configuration from application.properties.