Implementing Custom Roles in your SaaS Application

Jun 20th, 2024

Omri Gazitt avatar

Omri Gazitt

ReBAC  |  

Topaz  |  

Authorization  |  

RBAC

fancy looking lock with neon

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.

multi-tenant rbac

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.

can-read permission to role assignment

Each role has a corresponding group. For example, the acmecorp-admin role is granted through membership in the acmecorp-admin group:

role-to-group assignment

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:

group-to-user assignment

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.

acmecorp custom billing role

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.

rebac relations

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.

rebac permissions

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!

Omri Gazitt avatar

Omri Gazitt

CEO, Aserto