Relationship-based access control (ReBAC) is an authorization pattern where permissions are derived from relationships between resources. The most common example of ReBAC is the concept of ownership. For example, the user that created a blog post can edit the blog post because the user is the "owner" of the blog post. However, ReBAC also includes resource hierarchies, user groups, and any other situation where permissions are based on relationships. In this blog post, you will learn how to implement ReBAC in Node.js with Oso Cloud using Oso's GitCloud sample app as an example.
If you haven't already, create a free Oso Cloud account and set up an API key.
Oso Cloud lets you define authorization rules that determine which actors have permissions on a given resource.
For the purposes of this tutorial, we will have one type of actor, a User
; and two types of resources, a Repository
and an Issue
.
Below is how you can represent these entities in Polar, Oso's declarative authorization language.
actor User {}
resource Repository {}
resource Issue {}
Add the above policy to Oso Cloud from the Oso Cloud UI's "Rules" tab.
For this blog post, you'll implement two ReBAC patterns.
First, the user that originally created an issue should be able to edit the issue.
Second, admin users should be able to edit issues that belong to their repositories.
You can represent the ability to edit an issue by adding an edit
permission on the Issue
resource as follows.
resource Issue {
# UPDATED: added permissions
permissions = ["edit"];
# UPDATED: added roles
roles = ["editor"];
# UPDATED: any user with the "editor" role can "edit"
"edit" if "editor";
}
To connect to Oso Cloud from Node.js, install the oso-cloud npm package.
npm install oso-cloud
Next, create an Oso Cloud client using your Oso Cloud API key as follows.
const { Oso } = require('oso-cloud');
const assert = require('assert');
// Put your Oso Cloud API key in the OSO_CLOUD_API_KEY environment variable
const apiKey = process.env.OSO_CLOUD_API_KEY;
assert.ok(apiKey, 'Must set OSO_CLOUD_API_KEY environment variable');
const oso = new Oso('https://cloud.osohq.com', apiKey);
The Oso Cloud client has an authorize()
function that you can use to check whether a given user has a certain permission on a resource.
For example, below is how you can check whether the user Bill
has the edit
permission on the issue tps-reports-99
.
const authorized = await oso.authorize(
{ type: 'User', id: 'Bill' },
'edit',
{ type: 'Issue', id: 'tps-reports-99' }
);
console.log(authorized); // false
Oso policies contain authorization logic, but you still need authorization data so Oso can know that Bill
is an editor
on the issue tps-reports-99
.
In Oso Cloud, authorization data is represented by facts.
Below is how you can create a fact using the oso.tell()
function.
await oso.tell(
'has_role', // Fact type
{ type: 'User', id: 'Bill' }, // Actor
'editor', // Role
{ type: 'Issue', id: 'tps-reports-99' } // Resource
);
Now the oso.authorize()
call will return true
for User Bill
and Issue tps-reports-99
.
Next, let's update our Oso policy so that users can edit issues that they've created.
First, you need to create a relationship between issues and users as follows.
The creator
relation will store which user created a given issue; and the user who created the issue will automatically get the editor
role.
resource Issue {
# Permissions
permissions = ["edit"];
# Roles
roles = ["editor"];
# UPDATED: added relationships
relations = { creator: User };
# Any user with the "editor" role can "edit"
"edit" if "editor";
# UPDATED: the user who created this issue has "editor" role
"editor" if "creator";
}
To make this example more concrete, let's say the user Peter
created the issue tps-reports-99
.
Before you set up the relationship that connects the user Peter
to the issue tps-reports-99
, the oso.authorize()
function will return false
:
const authorized = await oso.authorize(
{ type: 'User', id: 'Peter' },
'edit',
{ type: 'Issue', id: 'tps-reports-99' }
);
console.log(authorized); // false
To create the relationship between the user Peter
and the issue tps-reports-99
, you can call oso.tell()
with the has_relation
fact type.
await oso.tell(
'has_relation', // Fact type
{ type: 'Issue', id: 'tps-reports-99' }, // Resource
'creator', // Relation
{ type: 'User', id: 'Peter' } // Actor
);
With the above fact, Oso Cloud can now resolve that the user Peter
can edit
the issue tps-reports-99
.
const authorized = await oso.authorize(
{ type: 'User', id: 'Peter' },
'edit',
{ type: 'Issue', id: 'tps-reports-99' }
);
console.log(authorized); // true
Finally, let's update our Oso policy so users that have admin permissions on a particular repository can also edit issues.
To do that, first you need to add an admin
role to the Repository
resource as follows.
resource Repository {
# UPDATED: added roles to Repository
roles = ["admin"];
}
Next, you need to add a new relation to issues to track which Repository
the issue belongs to.
Below is the updated Issue
resource definition, with a new repository
relation and a new rule that admin
role on a repository
also grants the editor
role on that repository's issues.
resource Issue {
# Permissions
permissions = ["edit"];
# Roles
roles = ["editor"];
# UPDATED: added repository Relation
relations = { creator: User, repository: Repository };
# Any user with the "editor" role can "edit"
"edit" if "editor";
# User who created this issue has "editor" role
"editor" if "creator";
# UPDATED: admins on repository have "editor" role
"editor" if "admin" on "repository";
}
In order to show how the new Oso policy works, you need to add two facts to Oso Cloud.
First, you need to add a relation that connects the issue tps-reports-99
to the repository tps-reports
.
Then, you need to grant the user Bill
the admin
role on the repository tps-reports
as follows.
// Issue `tps-reports-99` belongs to repository `tps-reports`
await oso.tell(
'has_relation', // Fact type
{ type: 'Issue', id: 'tps-reports-99' }, // Resource
'repository', // Relation
{ type: 'Repository', id: 'tps-reports' } // Actor
);
// User `Bill` has role `admin` on repository `tps-reports`
await oso.tell(
'has_role', // Fact type
{ type: 'User', id: 'Bill' }, // User
'admin', // Role
{ type: 'Repository', id: 'tps-reports' } // Resource
);
With these two facts, Oso Cloud can now deduce that Bill
can edit
the issue tps-reports-99
, because he has the admin
role on the repository tps-reports
.
authorized = await oso.authorize(
{ type: 'User', id: 'Bill' },
'edit',
{ type: 'Issue', id: 'tps-reports-99' }
);
console.log(authorized); // true
ReBAC is an authorization pattern that almost every application implements in some way: checking that users can't edit arbitrary data, but can edit their own data is an example of ReBAC. While ownership is the most common ReBAC pattern, there are many other common ReBAC patterns. And ReBAC can be tricky to implement when you have deeply nested data hierarchies. For example, imagine extending this blog post to include making repositories belong to organizations, and allowing organization admins to edit issues. Oso Cloud makes implementing ReBAC much easier, so next time you find yourself implementing a ReBAC pattern from data ownership to data hierarchies, try Oso Cloud first!