Role-Based Access Control (RBAC) in Node.js
Role-based access control (RBAC) is so ubiquitous that Oso provides syntax for modeling RBAC.
This syntax makes it easy to create a role-based authorization policy with roles and permissions – for example, declaring that the reader
role on a repository allows a user to read
from that repository.
In this guide, we’ll walk through building an RBAC policy for GitCloud (opens in a new tab), a sample version-control application.
You can follow along with the steps in this guide by creating a free Oso Cloud account (opens in a new tab).
- Declare application types as actors and resources
- Declare roles and permissions
- Grant permissions to roles
- Grant roles to other roles
- Storing facts in Oso Cloud
- Authorizing users with Oso Cloud
- Inherit a role on a child resource from the parent
- Baby Got RBAC
Declare application types as actors and resources
Oso makes authorization decisions by determining if an actor can perform an action on a resource:
- Actor: who is performing the action?
User("Bill")
- Action: what are they trying to do?
"read"
- Resource: what are they doing it to?
Repository("tps-reports")
The first step of building an RBAC policy is telling Oso which application types are actors and which are resources.
Our example app has a pair of resource types that we want to control access to: Organization
and Repository
.
We declare both as resources in the Oso Cloud Rules Editor (opens in a new tab) as follows:
resource Organization {}resource Repository {}
Our app also has a User
type that will be our lone type of actor
:
actor User {}
This piece of syntax is called a resource block, and it performs two functions: it identifies the type as an actor
or a resource
, and it provides a centralized place to declare roles and permissions for that particular type.
Declare roles and permissions
In GitCloud, users can be granted permission to read jobs and manage (e.g. create, cancel) jobs for a repository.
Users can also be assigned roles on organizations and repositories, such as the admin
role for an organization or the maintainer
role for a repository.
Inside the curly braces of each resource block, we declare the roles and permissions for that resource:
actor User { }resource Organization { roles = ["admin", "member"];}resource Repository { permissions = [ "read", "manage_jobs" ]; roles = ["reader", "maintainer"];}
Grant permissions to roles
Next, we’re going to write shorthand rules that grant permissions to roles.
For example, if we grant the manage_jobs
permission to the maintainer
role in the Repository
resource block, then a user who’s been assigned the maintainer
role for a particular repository can manage jobs on that repository.
Here’s our Repository resource block with a few shorthand rules added:
resource Repository { permissions = [ "read", "manage_jobs" ]; roles = ["reader", "maintainer"]; # reader permissions "read" if "reader"; # maintainer permissions "manage_jobs" if "maintainer";}
Shorthand rules expand to regular Polar
rules when a policy is loaded.
The "manage_jobs" if "maintainer"
shorthand rule above expands to:
has_permission(actor: Actor, "manage_jobs", repository: Repository) if has_role(actor, "maintainer", repository);
Note:
Instances of our application's User
type will match the Actor
parameter
because of our actor User {}
resource block
declaration.
Assign roles to other roles
All of the shorthand rules we’ve written so far have been in the <permission> if <role>
form, but we can also write <role1> if <role2>
rules. This type of rule is great for situations where you want to express that <role2>
should be granted every permission you’ve granted to <role1>
.
In the previous snippet, the permissions granted to the maintainer
role are a superset of those granted to the reader
role. If we replace the existing "read" if "maintainer"
rule with "reader" if "maintainer"
, the "maintainer"
role still grants the "read"
permission:
resource Repository { permissions = ["read", "manage_jobs"]; roles = ["reader", "maintainer"]; # An actor has the "read" permission if they have the "reader" role. "read" if "reader"; # An actor has the "manage_jobs" permission if they have the "maintainer" role. "manage_jobs" if "maintainer"; # An actor has the "reader" role if they have the "maintainer" role. "reader" if "maintainer";}
In addition, any permissions we grant the reader
role in the future will automatically propagate to the "maintainer"
role.
Storing facts in Oso Cloud
An Oso policy contains authorization logic, but the application remains in control of all authorization data.
For example, the logic that the maintainer
role on a repository grants the manage_jobs
permission lives in the policy, but the policy doesn’t manage the data that defines which users have been assigned the maintainer
role on Repository("Acme App")
.
That data is represented as facts in Oso Cloud.
To create facts in Oso Cloud from your node.js application, first install the oso-cloud npm package (opens in a new tab).
npm install oso-cloud
Next, get an API key (opens in a new tab) from Oso Cloud.
The oso-cloud npm package exports an Oso
class that you can use to instantiate a new Oso Cloud client as follows.
const { Oso } = require("oso-cloud");const assert = require("assert");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);
Now, use the oso.tell()
function to create a new fact.
The following function call tells Oso Cloud that the user "Bill"
has role "reader"
on repository "tps-reports"
.
await oso.tell( "has_role", // Fact name { type: "User", id: "Bill" }, // Actor "reader", // Role { type: "Repository", id: "tps-reports" } // Resource);
Authorizing users with Oso Cloud
Once you have added the above fact, you can use the oso.authorize()
function to check whether a user can take certain actions based on their roles as follows.
let authorized = await oso.authorize({ type: "User", id: "Bill" }, "read", { type: "Repository", id: "tps-reports",});console.log(authorized); // trueauthorized = await oso.authorize({ type: "User", id: "Bill" }, "manage_jobs", { type: "Repository", id: "tps-reports",});console.log(authorized); // false
Inherit a role on a child resource from the parent
If you’ve used GitHub GitCloud before, you know that having a role on an organization grants certain roles and permissions on that organization’s repositories.
For example, a user is granted the "reader"
role on a repository if they’re assigned the "member"
role on the repository’s parent organization.
This is how you write that rule with Oso:
resource Organization { roles = ["admin", "member"];}resource Repository { permissions = [ "read", "manage_jobs" ]; roles = ["reader", "maintainer"]; relations = { organization: Organization }; # ... "reader" if "member" on "organization";}
First, we declare that every Repository
has an organization
relation that references an Organization
:
relations = { organization: Organization };
This is a dictionary where each key is the name of the relation and each value is the relation’s type.
Next, we add a has_relation
fact to Oso Cloud.
await oso.tell( "has_relation", { type: "Repository", id: "tps-reports" }, "organization", { type: "Organization", id: "initech" });
This says that the tps-reports
repository is related to the initech
organization.
Next, we add a shorthand rule that involves the "reader" repository role, the "member" organization role, and the "organization" relation between the two resource types:
resource Repository { permissions = ["read", "push"]; roles = ["reader", "maintainer"]; relations = { organization: Organization }; # ... # An actor has the "reader" role on a Repository if they have the "member" role on its parent Organization. "reader" if "member" on "organization";}
Finally, we add a fact telling Oso Cloud that a user has a role on a given organizaton:
await oso.tell("has_role", { type: "User", id: "Michael" }, "member", { type: "Organization", id: "initech",});
This says that the user Michael
has the member
role on the initech
organization (which we defined as the parent of the tps-reports
repository with the has_relation
fact above).
Once the has_relation
and has_role
facts are set up, Oso Cloud can resolve that user "Michael"
should be able to read
the tps-reports
repository, but not manage_jobs
.
let authorized = await oso.authorize({ type: "User", id: "Michael" }, "read", { type: "Repository", id: "tps-reports",});console.log(authorized); // trueauthorized = await oso.authorize( { type: "User", id: "Michael" }, "manage_jobs", { type: "Repository", id: "tps-reports" });console.log(authorized); // true
Baby got RBAC
Our complete policy looks like this:
actor User { }resource Organization { roles = ["admin", "member"];}resource Repository { permissions = [ "read", "manage_jobs" ]; roles = ["reader", "maintainer"]; relations = { organization: Organization }; "reader" if "member" on "organization"; "maintainer" if "admin" on "organization"; "reader" if "maintainer"; "read" if "reader"; "manage_jobs" if "maintainer";}
It's easy to add authorization to your node.js application with Oso Cloud (opens in a new tab).
You can model common application authorization patterns like RBAC in the Rules Editor (opens in a new tab) with concise rules that are easy to read.
Then, you use the oso.tell()
function to send authorization data from your application to Oso Cloud.
With the policy and data in place, you implement enforcement by calling oso.authorize()
to confirm that the given user has permission to perform the requested action.
If you'd like to learn more, including how to integrate Oso Cloud with other languages, check out our docs. You can also join us on Slack (opens in a new tab) to ask any questions, or if you'd like to share what you're building with the Oso Community.