Skip to content

Instantly share code, notes, and snippets.

@vkarpov15
Last active January 17, 2024 22:23
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save vkarpov15/8f7f5920a3c0138a0d1ffc5cb3b94b01 to your computer and use it in GitHub Desktop.
Save vkarpov15/8f7f5920a3c0138a0d1ffc5cb3b94b01 to your computer and use it in GitHub Desktop.
Relationship-Based Access Control (ReBAC) in Node.js With Oso Cloud

Relationship-Based Access Control (ReBAC) in Node.js With Oso Cloud

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.

Getting Started With Oso Cloud and Node.js

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.

Users Can Edit Their Own Issues

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

Repository Admins Can Edit Issues

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 to the Future

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!

@gsarjeant
Copy link

This is great. I have two small suggestions:

Nitpick in this line:

Now the oso.authorize() call will return true for User bill and Issue tps-reports-99.

bill should be Bill

For the policy examples, it might be helpful to mark the changes somehow so they're more obvious to the reader. Maybe something like:

Might be worth tagging the changes in examples just to make it blindingly obvious. something like:
 resource Issue {
  # Permissions
  permissions = ["edit"];

  # Roles
  roles = ["editor"];

  # UPDATED: Relations
  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";

  # NEW: Admins on repository have "editor" role
  "editor" if "admin" on "repository";
}

@francium-lupe
Copy link

<script src="https://gist.github.com/francium-lupe/b6aca15c24c605e929fdc069fc3ca939.js"></script>

@francium-lupe
Copy link

<script src="https://gist.github.com/francium-lupe/13290572110d49064f4a2ae6231ebece.js"></script>

@francium-lupe
Copy link

<script src="https://gist.github.com/francium-lupe/b99d10869ee435eb3c017e6428df3e65.js"></script>

@francium-lupe
Copy link

<script src="https://gist.github.com/francium-lupe/90e28598d03b05d28a81be91e943aa14.js"></script>

@francium-lupe
Copy link

<script src="https://gist.github.com/francium-lupe/e91389f11e6acde2e7c70b66c437724b.js"></script>

@francium-lupe
Copy link

<script src="https://gist.github.com/francium-lupe/2a48a83be56c8e3ebf351896213d65bf.js"></script>

@francium-lupe
Copy link

<script src="https://gist.github.com/francium-lupe/f0866de1492f06bb50035830137b113d.js"></script>

@francium-lupe
Copy link

<script src="https://gist.github.com/francium-lupe/81dab1669a7485fd0473216ebddb663f.js"></script>

@francium-lupe
Copy link

<script src="https://gist.github.com/francium-lupe/aa7f9be58ae88b1f7ec091fe8900970a.js"></script>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment