Never Use Role Hierarchies

Check capabilities, not role names—a practical intro to RBAC

You've built an app. You have a member and an admin role. Your platform grows, so you create another role for slightly elevated permissions. Your customer, a few weeks later, asks for a role like the member role, but can manage billing. You think:

No problem, I'll create an Accountant role

This continues for a few more weeks, and you're stuck with 6 roles that are all inconvenient in some way — and none of them are technically higher than the others, none flexible enough, and at every call site you check: does the user have this role, or this role, or this role... until you miss a spot and have a security vulnerability somewhere.

At Gumloop, this is exactly what happened to us. As the platform scaled, this pattern didn't scale with us. We realized that, while in our heads we knew what was going on, our code was asking the wrong questions.

§The Wrong Question

While working across a number of codebases, I realized that the most common check of whether a user can do something looks like this:

While this is often the first intuition of how to gate something, it's encoding information in the code while asking Does this user have a hardcoded set of roles we want to allow for this feature? However, across your entire codebase, you've built a mental model of exactly what each role is allowed to do. The question you mean to ask is Is this user allowed to execute this action?

So, why don't you write code to ask the right question?

§The Right Question

Call sites should check capabilities, not role names. We can make this efficient by keeping a dictionary of what permission keys each role has.

With this pattern, adding a permission to a role is one line in one place, reliably propagating throughout the entire codebase, and requires zero changes to application code. Your roles aren't inherently hierarchical, allowing you to create bundles of permissions for the feature families across your product. However, conflicting roles may still exist... and to that, the solution is to allow users to have multiple roles and evaluate their permissions as the union of the permissions all their roles grant them. At scale, in an enterprise product, this has been proven to be the most battle-tested practice. These systems are used by companies like Microsoft, Google, Amazon, Uber, and probably every other enterprise you rely on day-to-day.

There are steps you can take towards having this be exceptionally type safe as well, but something like the simple example above works at scale for single-tenant applications. We call this 'Role-Based Access Control', or 'RBAC'.

§Hierarchy is not always the wrong mental model

Perhaps the title Never Use Role Hierarchies is exaggerated. In fact, there are moments where a mental model of role hierarchy is more effective than the composable, flat set of roles I presented above.

When thinking about how to share a single item with people, or with projects, organizations, or publicly, role hierarchies make sense:

That's fine because the scope is narrow and the permissions are genuinely ordered. The real problem is using role rank as a proxy for permission checks at the ingress points of features. The hierarchy can exist in your data model, but it should never leak into your authorization logic.

§Conclusion

The examples provided above work when every user belongs to one organization, and permissions are global. However, the questions change when we consider per-item sharing mechanisms, and cross-org/cross-project sharing as well. Oftentimes, these are the patterns that emerge in collaborative products, and these are important patterns to get right.

Lucky for you, I'm writing about them below:

  1. Item Sharing—Access Control List
  2. (Coming Soon) Two Permission Systems, One Answer (Layered ACL + RBAC)