Skip to main content

Multi-Tenant SaaS

In this guide, we'll go over how you can build a flexible authorization scheme for a multi-tenant B2B SaaS application using Warrant.

What is Multi-Tenant SaaS?

Multi-tenant SaaS has become the default architecture for most B2B SaaS apps today. By B2B SaaS apps, we mean any cloud-based software used by multiple users or teams within an organization.

In a multi-tenant setup, all tenants (or accounts) are hosted on a single instance of the application with data separation primarily enforced in the application layer. This is in contrast to a single-tenant setup where each tenant receives its own instance of the application, including separate data stores and web services.

While a multi-tenant architecture is cheaper, easier to deploy and maintain (just 1 instance!) and more commonly accepted by enterprise customers today, data security becomes a pretty important application-level concern. Let's take Mixpanel as an example. If I'm a user of Mixpanel, I want assurance that my product analytics data (raw events, reports etc.) is only accessible by my team and me in Mixpanel. Furthermore, I might also want to have more guardrails in my account and restrict access to certain data and reports to a handful of people (ex. a group of admins).

In the remainder of this guide, we'll go over how you can use Warrant to model a B2B multi-tenant app like Mixpanel and ensure proper authorization and access control to data based on tenant and role/permissions.

Prerequisites

This guide exclusively uses the Warrant API and thus assumes you have a Warrant account with API keys. If you don't, please first follow the Getting Started guide to set up your account.

Creating Object Types

Let's define our app's resources and relationships. Like Mixpanel, our app supports multiple users per tenant. Each user can only belong to 1 tenant but can also belong to 1 or more 'groups' within that tenant (ex. the admin group).

In addition to tenant and group, our app has a resource type called 'report'. A report is an instance of data and visualizations that can be created, edited and viewed by users in a tenant based on their roles and permissions. Reports cannot be accessed by users outside of their tenant.

To summarize then, these are the following relationships that we need to be able to resolve in our app:

  • Which tenant(s) does a given user belong to?
  • Which group(s) does a given user belong to?
  • Who can edit and/or view a given report?

We need 3 object types in order to define these relationships: tenant, group, and report. Let's create these types in Warrant.

Tenant

Let's start by creating the tenant object type. A tenant can have an owner (likely the user who created the account or an account/IT admin). In addition to an owner, tenants have members, or all the other users that are part of the tenant. This gives us the following object type definition:

{
"type": "tenant",
"relations": {
"owner": {},
"member": {
"type": "anyOf",
"rules": [
{
"type": "userset",
"relation": "owner"
}
]
}
}
}

Group

Groups can help us categorize users within a tenant. As an example, we might have 'admin' and 'support' groups in one of our tenants. Let's say that all users in the 'admin' group can 'create reports' and all users in the 'support' group can 'view all reports'. By granting these permissions to the appropriate group, we can implement basic role based access control (RBAC).

Given that RBAC is so common, Warrant includes a pre-defined group object type that we can use. The type comes with a member relationship which can grant group membership to users.

If this type didn't already exist, we could create it as follows:

{
"type": "group",
"relations": {
"member": {}
}
}

Report

Defining tenant and group ensures that we can properly categorize our users into their respective tenants and groups/roles. Now we need to define the concept of a report, the main resource in our system.

Like in Mixpanel, reports in our system can be created, edited and viewed. But the set of users that can take each of these actions depends. For one, each report has an owner, usually the user that created it. Otherwise, a report can have multiple editors (including the owner) and multiple viewers (including the editors). Putting this all together, we can create the report object type as follows:

{
"type": "report",
"relations": {
"owner": {},
"editor": {
"type": "anyOf",
"rules": [
{
"type": "userset",
"relation": "owner"
}
]
},
"viewer": {
"type": "anyOf",
"rules": [
{
"type": "userset",
"relation": "editor"
}
]
}
}
}

Creating & Managing Warrants

Now that our object types are defined, we need to add some logic in our code that will create warrants to establish the specific relationships between our tenants, groups, reports and users. Let's review these scenarios and see what logic we need to add in our app.

Associating Users with Tenants

One of the first things we need to do is add logic to associate new users with their tenant. We can do this by creating a warrant that associates a new user with their tenant on creation. The easiest way to hook this up is by adding the following code to our app's newUser() creation flow:

// 'org' is the tenant and 'userId' is the id of the new user
client
.createWarrant("tenant", org, "member", { userId: userId })
.then((newWarrant) => console.log(newWarrant))
.catch((error) => console.log(error));

Associating Users with Groups

In addition to associating users with tenants, we need to add logic that lets us add users to groups within their tenants. For example, we might want to add a user to an 'admin' group in order to grant elevated access. Assuming we have logic in our app to manage group membership, we also need to add the following code:

// 'group' is the group, 'userId' is the id of the user to add to the group
client
.createWarrant("group", group, "member", { userId: userId })
.then((newWarrant) => console.log(newWarrant))
.catch((error) => console.log(error));

Managing Reports

In addition to creating warrants for tenant and group membership, we need to create warrants that help us manage the reports in our app. For example, when a report is created, we should create a warrant that associates the user who created the report as being the owner of that report. We might also want to grant a particular group (ex. admin group) access to 'edit' a report. In this case, we would need to add the following code in our app:

// Grant all members of the 'admin' group editor access to 'reportA'
client
.createWarrant("report", "reportA", "editor", { objectType: "group", objectId: "admin", relation: "member" })
.then((newWarrant) => console.log(newWarrant))
.catch((error) => console.log(error));

Checking User Access

Now that our access model and warrants are set up, we can add access checks in our app that will help us enforce our multi-tenant setup. For example, we might want to check if a particular user (user1) has the ability to edit a report (reportA). The answer to this query will depend on multiple factors including user1's tenant and whether they are part of the 'admin' group which has the ability to edit reportA. We can query Warrant for this check from our code as follows:

client
.isAuthorized("report", "reportA", "editor", "user1")
.then((isAuthorized) => {
if (isAuthorized) {
// Allow access to 'analytics' for this user
}
})
.catch((error) => console.log(error));

Summary

In this guide, we created a flexible authorization scheme for a multi-tenant B2B SaaS application using Warrant. Although not completely exhaustive, this scheme is extendable and at least allows us to manage tenant and group membership dynamically and ensure that objects in the app (ex. reports) are only accessible to users in the appropriate tenant and group.