Published on

How to implement RBAC with Casbin and nodejs

Authors

When you are building a system that will be access and used by lots of users, you might want restrict users access to resources through their roles, privileges and permissions. This way only users with the right permissions can have access to certain resources or endpoints. Such implication is called Role based access control.

Basically, Role-based access control (RBAC) is a method of restricting resource access based on the roles of individual users within a system. RBAC lets users have access rights only to the information they need to do their jobs and prevents them from accessing information that doesn't pertain to them.

Lets take a good look at one of the basic use case of RBAC. In a simple blog system, A normal user will have read permission. That means he can only view blog posts.
An editor can be given write and update permission, which means that he can only write and edit or update blog posts. Then the blog admin with read, write, update and delete permissions can view, edit, delete an existing post and also create a new post.

Benefits of RBAC

Below are some of the benefits of using RBAC to restrict unnecessary resource access based on users role within a system:

  • Create systematic, repeatable assignment of permissions

  • Easily audit user privileges and correct identified issues

  • Quickly add and change roles, as well as implement them across APIs

  • Cut down on the potential for error when assigning user permissions

  • Integrate third-party users by giving them pre-defined roles

  • More effectively comply with regulatory and statutory requirements for confidentiality and privacy

Roles to follow when implementing RBAC

  • Role assignment: A subject can exercise a permission only if the subject has selected or been assigned a role.
  • Role authorization: A subject's active role must be authorized for the subject. With rule 1 above, this rule ensures that users can take on only roles for which they are authorized.
  • Permission authorization: A subject can exercise a permission only if the permission is authorized for the subject's active role. With rules 1 and 2, this rule ensures that users can exercise only permissions for which they are authorized.

Sometimes implementing a good and effecting RBAC system can be a bit daunting especially when you have lots of other logic to implement in a system. This is where casbin comes in to help ease your job by making you focus on your business logic.

We a going to implement a simple and the most basic of casbin RBAC implementation. It will only have two routes, one will be open for all access and one will have an enforcer to check for permissions.

Prerequisites

  1. Familiarization with javascript and nodejs,
  2. Code Editor (Vscode)
  3. API client (Like Postman or Thunder client). I will be using Thunder client, a vscode extension for making api calls

Scaffolding the Application

First of all, let’s create a directory for the application, head over to a convenient directory on your system and run the following code in your terminal:

//this will create a new directory in your current directory
mkdir casbin-rbac

//change your current directory to your project directory
cd casbin-rbac

4
cd rbac

// Initializes a package.json file
npm init

The commands above creates a new directory that will hold all the files and code and then change your current working directory to the newly created directory and then the last command initializes an npm project in the application directory and creates a package.json file, this file will hold the necessary information regarding the application and also related project dependencies that will be used by the application.

Install the application dependencies

What we will be building requires some dependencies to help set it up and get it ready for implementation. Run the code below in your cli to install them.

npm install fastify casbin fastify-casbin nodemon

More about the packages we just installed

  • Casbin is a powerful and efficient open-source access control library. It provides support for enforcing authorization based on various access control models. learn more here casbin github

  • Fastify is a web framework highly focused on providing the best developer experience with the least overhead and a powerful plugin architecture. It is inspired by Hapi and Express and as far as we know, it is one of the fastest web frameworks in town. Learn more here fastify website

  • Fastify casbin is a plugin for Fastify that adds support for Casbin. it provides an unopinionated approach to use Casbin's Node.js APIs within a Fastify application. Learn more here fastify casbin github.

  • Nodemon is a utility that will monitor for any changes in your source and automatically restart your server. Perfect for development. Nodemon website.

Setting up casbin

To start implementation we need to create two new files which will hold the configurations that will help set up casbin to work. First create a new file called basic_model.conf. This file hold the request definition, policy definition, policy effect and matches. And then add the code below and save the file

//basic_model.conf

[request_definition]
r = sub, obj, act

[policy_definition]
p = sub, obj, act

[policy_effect]
e = some(where (p.eft == allow))

[matchers]
m = r.sub == p.sub && r.obj == p.obj && r.act == p.act

The code above contains the rules that casbin will follow to enforce authorization when a user tries to access a protected resource.

The r in request definition stands for request, sub stands for subject which can be a user. obj stands for Object which can be the resource the user is trying to access. While the act stands for action which is action the user is trying to perform. Here the request definition is the definition for the access request. It means that all request to protected resource must have a subject (User), Object(Resource) and an action example Read/Write/Delete/Update.

Policy definition is the definition for the policy. It defines the meaning of the policy. Each line in a policy is called a policy rule. Each policy rule starts with a policy type, e.g., p, p2. It is used to match the policy definition if there are multiple definitions. Like in the case where we have the following policy definition.

p = sub, obj, act
p2 = sub, act

Policy effect is the definition for the policy effect. It defines whether the access request should be approved if multiple policy rules match the request. For example, one rule permits and the other denies. The above policy effect means if there's any matched policy rule of allow, the final effect is allow (aka allow-override). p.eft is the effect for a policy, it can be allow or deny. It's optional and the default value is allow. So as we didn't specify it above, it uses the default value.

Another example for policy effect is:

[policy_effect]
e = !some(where (p.eft == deny))

It means if there's no matched policy rules of deny, the final effect is allow (aka deny-override). some means: if there exists one matched policy rule. any means: all matched policy rules (not used here).

Matchers is the definition for policy matchers. The matchers are expressions. It defines how the policy rules are evaluated against the request. The matcher in the basic_model.conf means that the subject, object and action in a request should match the ones in a policy rule.

You can learn more about casbin model here on casbin website

The second file we have to create is a policy file. It will hold our policy which is the name of the subject, the resource itself and actions the subject is allowed to perform. Now create a new file and save it as basic_policy.csv and the insert the code below.

//basic_policy.csv

p, alice, data1, read
p, bob, data2, write

If you take a close look at the policy file above, you will see that it contains two policies. The subject as alice and bob, the resource as data1 and data2 and actions as read and write. Which means that alice as a subject can only have read access to data1, while bob only has write access to data2. so if you want to give more rights to alice, you can add more policies like the one below.

p, alice, data1, read
p, alice, data1, write
p, alice, data1, update
p, bob, data2, write

In the code above, alice can never have access to data2, but she can read, write and update data1. Same as bob with only write access to data2. You can keep adding more policy to suit your system design.

Server set up and implementation

Now that we have finished configuring casbin with our basic policy and model, we can now set up our web server and implement our basic RBAC system. Create a new file and call it server.js and then add the code below.

//server.js file

const fastify = require('fastify')()

fastify.register(require('fastify-casbin'), {
  model: 'basic_model.conf', // the model configuration
  adapter: 'basic_policy.csv', // the adapter
})

fastify.get('/', async () => {
  return `You're in!`
})

fastify.get('/protected', async () => {
  if (!(await fastify.casbin.enforce('alice', 'data1', 'read'))) {
    throw new Error('Not Authorized')
  }

  return `You're in!`
})

const start = async () => {
  try {
    await fastify.listen(3000)
  } catch (err) {
    fastify.log.error(err)
    process.exit(1)
  }
}

start()

In our server.js file, we imported fastify, and registered the fastify-casbin plugin, and then passed in our basic_model.conf we created earlier as a value of the fastify casbin model property. we also passed in the basic_policy.csv file we created as the adapter value.

//casbin registration
fastify.register(require('fastify-casbin'), {
  model: 'basic_model.conf', // the model configuration
  adapter: 'basic_policy.csv', // the adapter
})

With this, we are set to start authorization enforcement.

We created our first route which is an open route that does not require authorization.

//Open route
fastify.get('/', async () => {
  return `You're in!`
})

Any one can access the route above because it is not equipped with the enforcer to check for permissions.

In our second route which is the protected route, we brought it casbin enforcer to check if the user that is trying to access the route and resource has the appropraite permissions.

fastify.get('/protected', async () => {
  if (!(await fastify.casbin.enforce('alice', 'data1', 'read'))) {
    throw new Error('Not Authorized')
  }

  return `You're in!`
})

As you can see, the casbin enforcer accepts three parameters, the first is the subject which is the hard coded alice. Lets assume alice is a signed in user and her name is alice. Also the enforcer accepts data1 as the resource alice is trying to access and the third parameter as read which is the action alice is trying to perform.

Now the enforcer will take a look into the basic_policy.csv file to check if alice has read access to data1, if she does the route will return you are in, if not it will throw a Not Authorized error.

Now lets test our both routes to see which we have access to. We will assume that alice is a logged in user and she is trying to access data1 which can a database model.

Before we test our endpoints, lets make sure we can get our sever up by adding a script that will help us run our server. Add the following code to the script object. "dev": "nodemon server.js",

"scripts": {
    "dev": "nodemon server.js"
  },

After adding the script above, just run npm run dev in your command line and then Open your http client and send a send request to both endpoints http://localhost:3000 and http://localhost:3000/protected

RBAC with Casbin

In the image above you can see that the unprotected routes returns you are in because there is no permission enforcer there. SO it has to return you are in.

RBAC with Casbin
Here since the subject (alice), the object (data1) and the action (read) is the same as the one defined in the `basic_policy.csv` file, it will also return `you are in`. Now if you alter those enforcer parameter to not be the same as those in the policy file, it will throw an error.
//this will throw Not Authorized error
fastify.get('/protected', async () => {
  if (!(await fastify.casbin.enforce('alice', 'data2', 'read'))) {
    throw new Error('Not Authorized')
  }

  return `You're in!`
})

//this will throw Not Authorized error
fastify.get('/protected', async () => {
  if (!(await fastify.casbin.enforce('bob', 'data1', 'read'))) {
    throw new Error('Not Authorized')
  }

  return `You're in!`
})
//this will throw Not Authorized error
fastify.get('/protected', async () => {
  if (!(await fastify.casbin.enforce('alice', 'data1', 'delete'))) {
    throw new Error('Not Authorized')
  }

  return `You're in!`
})
//this will throw Not Authorized error
fastify.get('/protected', async () => {
  if (!(await fastify.casbin.enforce('bob', 'data1', 'delete'))) {
    throw new Error('Not Authorized')
  }

  return `You're in!`
})

If you try any of the above code and test try to access it in your API client, it will throw a Not Authorized error like in the image below.

RBAC with Casbin

Conclusion

You can see how easy it is to implement a basic RBAC system with casbin and fastify. Here our config data are stored in a file and enforcer parameter is hard coded. If you want to see a more advanced version of this implementation, where we have an actual logged in user and the policies are loaded from the database, take a look at this repo i created on github RBAC with express.