Skip to content
Advertisement

A better way to implement a large amount of cascading roles and authorities in Spring Security?

So i’m currently refactoring the backend code for my organization to prep for future upgrades. It currently runs fine, its just that the code is getting quite messy because of the sheer amount of roles and authorizations that exist in this org.

So our backend stack here is a simple springboot Rest API, we use a third party Oauth authentication provider as our security provider.

So for every request that comes in, we have a spring security filter that gets the users roles from the authenthication header JWT and assigns authorizations based on that.

    List<GrantedAuthority> authorities = new ArrayList<>();
    for (Map.Entry<String,Object> role : user.getCustomClaims().entrySet()) {
        authorities.add(new SimpleGrantedAuthority((String) user.getCustomClaims().get("role")));
    }
    SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(user.getEmail(), user.getUid(), authorities));
    filterChain.doFilter(request, response);

Now the “problem” exists because of the number of roles and authenthications that exist, when this was first made the company was a simple startup 8 years ago with 10 employees, now it’s a middle sized company with 500~ employees. So the number of roles and authorities has grown exponentially.

For example just for the finance department, we currently have 5 levels of authority, and they all cascade one another.

  • Finance Head
  • Finance Admin
  • Finance 3
  • Finance 2
  • Finance 1

Wherein Finance Admin has all the permissions of Finance 3 + its own exclusive permissions and so on.

And each endpoint in the rest API end up looking like this:

@PreAuthorize("hasAuthority('FinanceHead') or hasAuthority('FinanceAdmin') or hasAuthority('Finance3') or hasAuthority('Finance2') or hasAuthority('Finance1')")

with many endpoints that are multi-department having 20+ hasAuthorities. Dont get me wrong this currently works fine, but its becoming a pain to change one role as then id have to search all endpoints and change each one. Is there a better way to do this?

I was thingking it might be possible to Override the GrantedAuthority object into something like this:

public class GrantedAuthority {
    String role; ("Finance","Logistics", etc)
    int authLevel; (1,2,3,4,5)
}

That way i can do hasRole(“Finance”) at for each main endpoint and then use hasAuth(>3) for each method in in any further endpoints, something like this:

@PreAuthorize("hasRole('Finance')
@CrossOrigin
@RestController
@RequestMapping("/test")
public class test {

@PreAuthorize("hasAuthority('authLevel > 3')
@GetMapping
    public ResponseEntity<String> ping() {
        return new ResponseEntity<>("test", HttpStatus.OK);
    }
}

Is there a better way to do this?

Advertisement

Answer

Here’s how I ended up going about this, thanks to M. Deinum for showing me that Hierarchial Roles exist in spring security.

So what I did was create Role Hiearchies like so:

@Bean
    public RoleHierarchyImpl roleHierarchy() {
        RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
        roleHierarchy.setHierarchy(
                "ROLE_BOSS > ROLE_FINANCE_HEAD > ROLE_FINANCE_ADMIN > ROLE_FINANCE_3 > ROLE_FINANCE_2 > ROLE_FINANCE_1" 
        return roleHierarchy;
    }

private SecurityExpressionHandler<FilterInvocation> webExpressionHandler() {
        DefaultWebSecurityExpressionHandler defaultWebSecurityExpressionHandler = new DefaultWebSecurityExpressionHandler();
        defaultWebSecurityExpressionHandler.setRoleHierarchy(roleHierarchy());
        return defaultWebSecurityExpressionHandler;
    }

and then add them to my security configuration like so:

http                   
    .authorizeRequests().anyRequest().authenticated()
    .expressionHandler(webExpressionHandler())

so now instead of

   @PreAuthorize("hasAuthority('FinanceHead') or hasAuthority('FinanceAdmin') or hasAuthority('Finance3') or hasAuthority('Finance2') or hasAuthority('Finance1')")
   @GetMapping("/test")
    public ResponseEntity<String> test1() {
        return new ResponseEntity<>("Test", HttpStatus.OK);
    }

I can simply go

@PreAuthorize("hasRole('ROLE_ADMIN_3')")
@GetMapping("/test")
public ResponseEntity<String> test1() {
    return new ResponseEntity<>("Test", HttpStatus.OK);
}

And if the request has any of the following roles:

ROLE_BOSS, ROLE_FINANCE_HEAD, ROLE_FINANCE_ADMIN, ROLE_FINANCE_3 

The request will be done, else a 403 is thrown.

User contributions licensed under: CC BY-SA
10 People found this is helpful
Advertisement