Implementing Custom Roles in your SaaS Application
It seems like forever ago, but when you built v1 of your SaaS application, your authorization system was pretty simple. Your permissions were grouped into a fixed set of roles, and the admins of each tenant assigned their users into these roles.
Then came enterprise customers. Sure, they pay more, but they bring with them some gnarly requirements.
First, they want single sign-on using their identity provider. So you created an OpenID Connect (OIDC) / SAML integration.
Next, they wanted their users and groups mapped from their identity provider into your system. Fine, you built a SCIM integration.
Now they want to remix your permissions into their own set of roles. This authorization business is getting to be a lot of work!
Guess what? It’s time to build a real authorization system. And fortunately, there are now a set of tools that help you do exactly that. In this post, we’ll walk through how to use one of these tools, the Topaz authorization engine, to allow each one of your tenant admins to define their own custom roles that can extend from yours.
Relationship-Based Access Control
First, let’s talk about the architectural model that’s taking the authorization world by storm - relationship-based access control, or ReBAC. With ReBAC, you design your authorization model by defining your object types, relationships, and permissions. Permissions (like can_read) are granted through relationships (viewer) between object instances (such as a tenant) and subjects (typically a user or a group).
In the example below, Alice has the can_read, can_write, and can_delete permissions on the Acmecorp-tenant because she’s a member of the Acmecorp-admins group, and that group has the admin relationship to the Acmecorp-tenant.
Similarly, Bob has the can_read permission on the Fabrikam-tenant because he’s a member of Fabrikam-viewers and this group has a viewer relationship to the Fabrikam-tenant.
RBAC as a simple case of ReBAC
You’re probably familiar with role-based access control (RBAC) systems. These are just a simple case of ReBAC. An RBAC model will typically define users, groups, roles, and permissions. Discrete permissions are aggregated into roles, which can be assigned to users or groups.
RBAC is typically coarse-grained, meaning that permissions are granted at the tenant level as opposed to individual resources. Later we’ll look at fine-grained examples, but for now, let’s assume that your model has three roles - viewer, member, and admin - which grant the can_read, can_write, and can_delete permissions on the tenant.
In an authorization system like Topaz, this is expressed through the following manifest:
types:
tenant:
relations:
admin: user
member: user | group#member
viewer: user | group#member
permissions:
can_delete: admin
can_write: member | admin
can_read: viewer | member | admin
Adding new permissions and roles
As your application evolves, you can easily add new permissions (such as can_add_billing_info), and indicate which relationships (roles) grant these permissions. Given the three existing roles, the can_add_billing_info permission is likely granted through the admin role. However, we may want to add a specific role (such as billing_admin) that can only add billing info, without the permission to delete the whole tenant.
All we need to do is add the billing_admin relation to the manifest below, and specify that the can_add_billing_info permission is granted either through the admin role or this new billing_admin role.
types:
tenant:
relations:
admin: user
member: user | group#member
viewer: user | group#member
billing_admin: user | group#member
permissions:
can_delete: admin
can_write: member | admin
can_read: viewer | member | admin
can_add_billing_info: billing-admin | admin
So far, it’s easy to see how to add new system permissions, and new system roles that grant these permissions. But what if we wanted to allow each tenant admin to configure new roles that are only applicable to their tenant?
Adding custom roles
There are two ways of doing this, and we’ll cover both of them. The first way is appropriate for simple RBAC systems that want to use a single manifest to describe their model. We treat roles and permissions as explicitly modeled object types.
The second way is appropriate for fine-grained ReBAC systems that utilize more of the ReBAC model, and require each tenant to have its own custom manifest.
Custom roles for simple RBAC systems
If we have a simple role-based model, where roles are monolithic (for example, a viewer of a tenant gets to view every object in that tenant), we can design a single manifest that models roles and permissions as explicit object instances. In this model, roles and permissions are data. Adding a role simply means adding an instance of a role object type, and connecting it to instances of permission objects.
Here’s an example Topaz manifest that defines users, groups, roles, and permissions. Permissions are assigned to roles, each role has a corresponding group, and groups can contain users (or members of other groups).
types:
user: {}
group:
relations:
member: user | group#member
role:
relations:
member: group#member
permissions:
has_permission: member
permission:
relations:
role: role
permissions:
has_permission: role | role->has_permission
Following this convention, each discrete permission has a singleton instance in the system. Each tenant has a set of system role objects, which have relationships to the permissions that it grants. For example, the can_delete permission is granted through the acmecorp-admin role, and the can_read permission is granted through the acmecorp-admin, acmecorp-member, and acmecorp-viewer roles.
Each role has a corresponding group. For example, the acmecorp-admin role is granted through membership in the acmecorp-admin group:
And each of these groups may be assigned a user or group that comes from the identity provider, like LDAP, Okta, or Azure AD. In the example below, Rick Sanchez is assigned to be an admin for the acmecorp tenant:
In this way, new tenant-specific roles (and their corresponding groups) can be added that “remix” these permissions. It’s all data within the system - objects and relations.
Adding a billing-admin role to the acmecorp tenant
In the example below, we’ve added an acmecorp-billing-admin role and group which grants ONLY the can_add_billing_info permission to the members of that group.
A full example of this approach can be found in this repo.
But there’s another way of skinning this cat - instead of treating custom roles as data, you could treat each tenant as having its own schema.
Custom roles for fine-grained ReBAC systems
ReBAC really shines when you have a fine-grained authorization model - where you allow each object instance to have its own access control list (ACL). This can sound like a management headache, but many systems you’re familiar with employ exactly this model. These systems tend to use hierarchy to simplify permission assignment - for example, child objects inherit the ACLs from their parents.
For example, in Google Drive, you have documents, folders, groups, and users. Documents and folders have parents, and permissions are inherited through a graph of relationships between objects and subjects (users).
In an authorization system like Topaz, a Google Drive-like model is expressed through the following manifest:
types:
folder:
relations:
parent: folder
owner: user
editor: user | group#member
viewer: user | group#member
permissions:
can_delete: owner | parent->can_delete
can_write: editor | can_delete | parent->can_write
can_read: viewer | can_write | parent->can_read
doc:
relations:
parent: folder
owner: user
editor: user | group#member
viewer: user | group#member
permissions:
can_delete: owner | parent->can_write
can_write: editor | can_delete | parent->can_write
can_read: viewer | can_write | parent->can_read
Let’s look at a specific set of relationships between documents, folders, groups, and users.
Determining whether a user has a permission on an object involves using the object-relationship graph above in conjunction with the manifest, which tells us how a permission is granted.
In the manifest above, we specified that the can_read permission is granted through the viewer relationship on a document or its parent folder. If there’s a path through this graph which connects an object to a subject (user) through a set of relationships that carry this permission, then that user has the permission on the object.
In this example, Alice has the can_read relationship on the Handbook document because that document is in the General folder, the Engineering group is a viewer on that folder, and Alice is a member of the Engineering group.
Custom roles as a schema change
Let's say that one of your customers wants to add an admin role on a folder, which grants the can_delete permission for that folder, but cannot read or write the items in that folder. This can be done by adding an admin relation in the manifest for that tenant:
types:
folder:
relations:
parent: folder
owner: user
editor: user | group#member
viewer: user | group#member
## new relation
admin: user | group#member
permissions:
can_write: editor | owner | parent->can_write
can_read: viewer | can_write | parent->can_read
## can_delete comes from either owner or admin
can_delete: owner | admin | parent->can_delete
The can_delete permission can then be granted through either the owner or the admin relation. The owner can also still read and write, but the admin can only delete.
You’ll notice that we needed to modify the manifest in order to do this. It follows that allowing per-tenant custom roles for fine-grained ReBAC models means that each of your tenants will have a custom manifest. You would need to run a separate Topaz instance for each of your tenants, each with its own manifest.
If this sounds like a lot of overhead, fortunately there’s a better way! Aserto offers a multi-tenant directory which lets you have a different manifest for each tenant. You can create a UI that allows your customers to “remix” your permissions into custom roles, and you can add these new relations into your manifest, alongside the existing “system” relations that are common to every tenant.
If you’re interested in exploring this further, just set up some time with one of our engineers, and we’d be glad to talk you through it.
Happy hacking!
Related Content
An “easy button” for API Authorization
Scaling a fine-grained authorization model for APIs can be tricky, especially when you have hundreds or thousands of them. Fortunately, Topaz makes it easy!
Jul 8th, 2024
Building Permission-Aware Enterprise Chatbots
Every enterprise wants a chatbot that combines the power of LLMs and your corporate data. But how does the chatbot respect permissions on documents across your enterprise?
Nov 6th, 2024
How ReBAC helps solve data filtering
Data filtering based on roles or permissions is an important use-case for application developers. Find out how your authorization system can help!
Apr 12th, 2024