Nodejs RBAC: Why You Shouldn't Write RBAC in Node.js

Role-based access control (RBAC for short) is a common pattern for determining whether a user is authorized to perform a certain action. The idea is that each user has a set of roles, and each action the user can take has a set of roles that are allowed to perform the action. Sounds pretty simple, right? The following is an excerpt of some RBAC code I once wrote to determine what roles are allowed to access certain endpoints in a car rental platform.

There's a mapping from 4 endpoints to the roles required to use each endpoint. And each user in the system has a list of roles, like 'admin' or 'manager'. An admin can perform any action, including modifying managers. A manager can update the vehicle's details, like the license plate. And the user that has reserved the vehicle can unlock the vehicle.

Determining whether a user has access to a given endpoint means finding whether one of their roles is in the requiredRoles mapping for a given endpoint.

The Hidden Complexities of RBAC

While RBAC is simple at first, there are several implicit assumptions in the above code that make RBAC tricky.

  1. While roles are listed out for every endpoint, in practice you would want an admin to have access to every endpoint. And managers should have access to everything that booked-users do. This simple requiredRoles mapping doesn't have a way to express relationships between roles.
  2. "booked-user" and "manager" are scoped to particular vehicles. So getting the user's roles isn't quite as easy as getting a property from your database.
  3. The requiredRoles mapping means you need a separate list of roles for every endpoint. There's no way to indicate that a group of endpoints require the same permissions.

Instead, here's how you might represent this authorization structure in Polar.

This Polar representation has some neat advantages.

  1. There are relationships between roles: "admin" users automatically get "manager" permissions, and "manager" users automatically get "booked-user" permissions.
  2. The "admin" role is global, but the "booked-user" role only applies based on the currentlyBookedUser relationship.
  3. There's a distinct "security" permission that is meant to encapsulate both the /vehicle/lock-vehicle and /vehicle/unlock-vehicle endpoints.

Running Polar Authorization

You can try out Oso on Oso Cloud. In addition to the above policy, you will need to add facts through the Oso Cloud UI. Facts tell Oso about what roles users have, and what relationships exist between vehicles and users.

For example, the following screenshot shows how you can add a fact that the user "brian" has the "manager" role on the vehicle "tesla".

Once the rules and facts are set up, you can then run queries in Oso Cloud's "Explain" tab. The following screenshot shows that "brian" does have the "security" permission on the vehicle "tesla".

You can also try out Oso locally, without signing up for Oso cloud. Below is how you can run the above Polar authorization setup in Node.js using Oso's npm package. First, you need to copy the following Polar logic into a main.polar file. This logic is slightly modified from the Oso Cloud version, because Oso Cloud supports global, but the Oso npm package does not at the time of this writing.

Next, run npm install oso, add the following JavaScript to an index.mjs file, and run node ./index.mjs.


When You Realize You Aren't Implementing RBAC

Sometimes, when you think you're implementing basic RBAC, you are actually implementing Relationship-Based Access Control (ReBAC). For example, when I originally implemented this authorization system, I thought that "booked-user" was a role. I originally implemented that role as a computed role that I passed into the authorization system as follows.

But the "booked-user" role is more accurately represented as a relationship: users can unlock vehicles that they have booked. You still need a conditional role, but Oso can handle the logic of "users have this role if they've booked the vehicle." The following Polar code adds a "driver" role to a user based on the "currentlyBookedBy" relationship.

Relationships are also facts in Oso Cloud. The below screenshot shows adding a currentlyBookedBy relationship between user 1 and vehicle 1 through the Oso Cloud UI.

With the Polar authorization logic and the above fact, Oso Cloud can now tell that user 1 has "security" permissions on vehicle 1.

Oso Cloud can also tell that user 1 does not have "update" permissions on vehicle 1.

You can also run this Polar logic locally by copying the above Polar code into main.polar and adding the following logic.

Moving On

Implementing RBAC yourself in Node.js is trickier than you think. Issues like relationships between roles, scoped roles, and distinct permissions pop up once your authorization model grows beyond an isAdmin flag. And many Node.js applications are already beyond the constraints of RBAC by letting users modify their own data. Rules like "users can modify documents that they've created" and "users can unlock the vehicle that they've booked" are a form of ReBAC, not RBAC.

Ready to level up your app's authorization setup? Try Oso Cloud

Want us to remind you?
We'll email you before the event with a friendly reminder.

Write your first policy