Heap Blog

How To Structure Permissions In A SaaS App

So you’re building a SaaS product and you want to serve real customers and start making those fat enterprise bucks. Great! Now you need to support weird stuff you’ve never heard of before like LDAP, SAML, SSO, and… RBAC.

What is RBAC?

Role Based Access Control is a system for organizing permissions and specifying who is allowed to do what. Your product almost certainly has features that not all users are allowed to do, like resetting other users’ passwords or viewing coworkers’ salaries. As your product becomes more complex and serves more customers, and admins for those customers need granular rules, you eventually need a system that’s a bit more solid than hundreds of ad hoc gates in your product.

There are three basic concepts:

  • Role: a job function or title, which defines a person’s authority level
  • Permission: the ability to view or modify a class of functionality
  • Operation: a specific action in your system, which may require multiple permissions to be usable

By way of example, let’s assume you’re a Product Manager at Heap. PMs at Heap dogfood Heap extensively, so we’ll use our own product as the example. PMs at Heap are assigned the Analyst role. The Analyst role has many permissions, one of which is the ability to modify reports. There are a few different places in the interface where you can modify reports, so each of those different places would be an operation.

These concepts have the following relationships:

  • A user has many roles (Mallory has the Analyst role)
  • A role has many permissions (the Analyst role has the Edit Report permission)
  • A permission has many operations (Edit Event Definition permission enables the Edit Report Name, and Edit Report Criteria operations)

This means you give a user one or more roles, which grants them a set of permissions, which in turn grants them some set of operations in your product.

Why do you want RBAC?

RBAC allows you to create a permissions system that follows human organizational structures well, allowing it to stay more up-to-date over time than other models. RBAC works the way administrators think, which makes it easy to configure correctly.

One alternative model is an ACL (Access Control List), in which you explicitly list all the operations a particular user is allowed to do. Under ACL, a user might start in marketing and be given permits to marketing systems. Then, a few months later they transfer departments and need access to Salesforce. It’s hard for an IT department to know which permissions are still needed, so in the interest of not breaking the user’s account, they will simply add the new permissions in addition to the old ones.

In RBAC, though, if the user’s role was changed from “Marketer” To “Salesperson”, their access would be correctly updated across the board.

In the other direction, if marketing added a new tool and all users needed access to it, in an ACL world you’d need to add a permit to each marketer, but in RBAC you can add it to the role and be good to go.

How To Implement It Sanely

Here are a few simple guidelines on how to implement RBAC in a way that will scale as your product matures.

Check operations on permissions, not roles.

This is the most important thing. You’re going to need to add permissions checks to each RBAC operation in your codebase – potentially hundreds of places. Make sure to do it in terms of permissions, which should exist in a first class form in your codebase.

For example, you never want to have code that looks like this:

if (User.role in ['admin', 'marketer']) { ... }

You always want your code to look like:

if (User.can(Permissions.EDIT_REPORT, report) { ... }

Because a role allows you to glob together as many permissions as you need, the permissions themselves can be extremely granular. This allows you to move permissions between roles fluidly, or create new roles easily without needing to change your front-end code. You can even make custom roles for different customers, based on however they want the product to be accessed, without changing any front-end code!

Positive, never negative.

There is likely some baseline functionality that every single user of your application can do. (For some sensitive apps, this might be extremely limited, e.g. logging in and nothing else.) This is what all users should start with, and nothing more.

Beyond that baseline, permissions should grant access additively, never negatively. Negative permissions cause conflicts if a subject has more than one role, since it’s unclear which role “wins”, and resolving that conflict creates many problems.

Extensions

As your product matures, you may want to incorporate some additional concepts into your permissions system. If you’ve followed the above rules, these will be straightforward to add.

Custom Roles

It’s possible that each of your customers would like to have a different role definitions. For example, by default at Heap, Analysts can modify and delete reports in any way. But perhaps BigEnterpriseCo would not like to allow Analysts to delete or recategorize reports. We support this in Heap by allowing our solutions team to create custom roles. The way we implement this at a technical level is by storing each custom role in our database, as such:

Table "public.role"
                   Column                   |      Type       
--------------------------------------------+---------------- 
 id                                         | bigint                  
 customer_id                                | bigint                  
 name                                       | text  
 description                                | text                    
 create_report                              | boolean                 
 modify_report                              | boolean                 
 modify_report_name                         | boolean                 
 modify_report_category                     | boolean                 
 modify_report_note                         | boolean                 
 modify_report_report                       | boolean                 
 delete_report                              | boolean                 

By adding rows to this table, our solutions team can create fully customized roles for each customer. You’ll need a shim to load the custom roles, but not much else. If you’ve written your code to always check operations against permissions, instead of roles, you can now support this custom analyst role with no change to your frontend!

These custom roles come with some maintenance overhead. When we add new permissions, we need to think through whether they should be enabled for each custom role. A careful backfill is required to ensure that these roles get the appropriate permissions. Sometimes this requires product-level decision-making. As a general rule, we err on the side of restricting access when configuring roles for enterprise customers.

Groups & Resources

It’s possible that you may wish for a group of users to all have the same set of roles. For example, if you have Researchers across different departments, they might all be part of a Researchers group that gives them the Analyst and Consumer roles.

Many of these permissions are ideally applied on a per-resource basis. For example, if your company has 10 different projects, some users may need read access to certain projects but not others. There are many ways to express this but the cleanest is to use groups, so that you don’t need to create a mapping between each individual user and the resources they’re permitted to access.

For example, Marketing Researchers should be able to read/write marketing projects, and the Product Researchers team can read/write within their own projects. Then the base Researchers group would grant some limited base permissions, and the Marketing Researchers group would be associated with a set of resources. Then the set of available resources can be changed atomically.

If you’ve written your permission sets to be positive, never negative, a group is an easy indirection to introduce: it’s just a union of some other permission sets.

Our code architecture also makes it easy to add per-resource restrictions. Consider the permission check code from above:

if (User.can(Permissions.EDIT_REPORT, report) { ... }

Note that we’re passing in the resource – the report the user is attempting to edit – rather than just the edit permission. This allows the permissions machinery to check between the user, the permission, and the resource in question. All of this logic lives in that permissions code, and the rest of our product code didn’t need to change when we added the cross-cutting new concept of per-resource restrictions.

If this sounds interesting to you, we are hiring! Or, if you have any questions, feel free to reach out to me via email: dorian@heapanalytics.com.

Prior Work / Helpful Links

Dorian Johnson

Subscribe to our mailing list