Role Hierarchy

Why a Hierarchy?

An admin should be able to do everything a seller can, and a seller everything a customer can. Without a hierarchy you must write hasAnyRole("CUSTOMER", "SELLER", "ADMIN") everywhere. A RoleHierarchy bean says it once:

ROLE_ADMIN
   |  implies
   v
ROLE_SELLER
   |  implies
   v
ROLE_CUSTOMER

Configuring It

import org.springframework.context.annotation.Bean;
import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl;

@Configuration
public class RoleConfig {

    // static: lets method security pick it up early
    @Bean
    static RoleHierarchy roleHierarchy() {
        return RoleHierarchyImpl.withDefaultRolePrefix()
                .role("ADMIN").implies("SELLER")
                .role("SELLER").implies("CUSTOMER")
                .build();
    }
}

Since Spring Security 6.3, a RoleHierarchy bean is used automatically by both authorizeHttpRequests and @PreAuthorize. Now .hasRole("CUSTOMER") also lets sellers and admins in.

How the Expansion Works

Example

Spring expands the user's authorities with everything they imply, then checks the rule. Here is the same idea in plain Java:

import java.util.*;

public class Main {
    static final Map<String, String> IMPLIES = Map.of(
        "ROLE_ADMIN", "ROLE_SELLER",
        "ROLE_SELLER", "ROLE_CUSTOMER");

    static Set<String> reachable(Collection<String> granted) {
        Set<String> all = new TreeSet<>(granted);
        Deque<String> todo = new ArrayDeque<>(granted);
        while (!todo.isEmpty()) {
            String next = IMPLIES.get(todo.pop());
            if (next != null && all.add(next)) todo.push(next);
        }
        return all;
    }

    public static void main(String[] args) {
        System.out.println("asha  -> " + reachable(List.of("ROLE_CUSTOMER")));
        System.out.println("ravi  -> " + reachable(List.of("ROLE_SELLER", "product:write")));
        System.out.println("meera -> " + reachable(List.of("ROLE_ADMIN", "order:refund")));
        System.out.println("meera hasRole(CUSTOMER)? " + reachable(List.of("ROLE_ADMIN")).contains("ROLE_CUSTOMER"));
    }
}

Output

asha  -> [ROLE_CUSTOMER]
ravi  -> [ROLE_CUSTOMER, ROLE_SELLER, product:write]
meera -> [ROLE_ADMIN, ROLE_CUSTOMER, ROLE_SELLER, order:refund]
meera hasRole(CUSTOMER)? true