Implementing access control is a common requirement for most applications, but a lack of agreed standard practices and tooling means engineering teams often have to come up with custom implementations.

One good reason why implementations end up being completely custom is that access control often depends on the application, and there are many different ways that it could work. These different ways are commonly called access control models.
To illustrate this, let’s involve two popular actors: Alice and Bob.

In a simple system, Alice could have read access to a resource called data1 and Bob write access to another resource, data2. Specific rules like these are commonly called Access Control Lists ( ACL).

In another, more sophisticated system, Alice could have an admin role in the system and Bob a user role. Admins may perform operations not permitted to users. This is commonly referred to as Role Based Access Control (RBAC).

Finally, a user in another system might be allowed to perform certain tasks only on the resources she owns, where ownership is characterised by attributes of the resources themselves. For instance, Alice might be allowed to edit a blog post only if she is the author. This is commonly called Attribute Based Access Control (ABAC) because it relies on attributes stored on the data itself.

The above examples are by no means exhaustive, and a mix of models may be necessary for even a moderately complex system.

To extend the blog post example to a more realistic scenario:

  • An admin of the blogging platform may perform any action on any blog post (RBAC).
  • The author of a blog post may edit it (ABAC).
  • Specific selected users may review and make modifications to specific blog posts (ACL).

The need for a flexible access control system

Considering the many different ways that an access control system may work, the only real alternative to a custom implementation is a reusable system that is flexible enough to support different model types.

At NearForm, we have built and released an open-source project called Udaru, an extremely flexible access control system inspired by Amazon Web Services’ IAM. Udaru is an implementation of yet another access control model called Policy Based Access Control (PBAC), where everything is modelled via policies, exactly as it happens in IAM.

Unfortunately, over time we came to realise that in some projects the level of flexibility provided by Udaru is overkill, and the effort required to create and maintain policies is hard to justify for a system that doesn’t need such flexibility.

In this post, we are going to talk about another open-source project for access control called Casbin.

Introduction to Casbin

Casbin is a relatively new player in the access control world, at least when it comes to its Node.js implementation.
The way Casbin works is by defining some fundamental concepts, which are implemented in its core libraries, and then externalising specific extensions as plugins.
One of Casbin’s core concepts is the PERM metamodel (Policy, Effect, Request, Matchers), which is used to define a Casbin model.

Using this metamodel, multiple models can be implemented. A Casbin model is a configuration file containing the definitions for the elements of the metamodel.

Let’s take a basic model file for an ACL system as an example:

[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

In the model configuration file, some conventional names are used to identify the entities on which the model operates:

  • sub refers to the subject performing an action — for example, Alice
  • obj refers to the object on which the action is performed — for example, data1
  • act refers to the action that is performed by the subject on the object — for example, read

Furthermore, the model file also contains other “variables” and expressions. For example, in the [matchers] section:

r refers to the request, as defined in the [request_definition] section
p refers to the policy, as defined in the [policy_definition] section

But what does the above model do?

  1. [request_definition] defines the shape of the access control request sent by client code when checking whether an action is allowed or not.
  2. [policy_definition] defines the structure of the policy itself, which we’ll take a look at next.
  3. [policy_effect] defines a sort of folding function that, given a set of policies that apply to the request, decides whether the operation is allowed or not. In the case above, the operation is allowed as long as at least one of the policies allows it. There are alternative ways to implement this logic.
  4. [matchers] defines a condition that, provided a policy and the request, defines whether the policy allows the operation for that request. In the example model, a policy allows a request if the sub, obj and act of the request match those of the policy itself. This is a simple matcher; more complex matchers can be implemented via expressions and functions.

A practical example

Given the model file shown earlier, let’s assume that we have these two policies, where each line is a distinct policy:

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

Based on the policy definition in the model, we have that:

  • In the first policy, alice is the sub, data1 the obj and read is the act
  • In the second policy, bob is the sub, data2 is the obj and write is the act

When sending a request to Casbin with data (sub = Alice, obj = data1, act = read), it will be evaluated by the matcher and it will be found that for that request, the first policy allows the operation, whereas the second doesn’t.

Because the policy effect is defined in the [policy_effect] section of the model so that at least one policy must allow the operation, this operation will be allowed.

On the other hand, if we sent a request with data (sub = Alice, obj = data2, act = read), none of the policies would allow the operation, so the operation would be denied.

Using Casbin in Node.js

Casbin has bindings for several programming languages, and Node.js is very well supported via the casbin NPM package.

To use Casbin in a Node.js project, the package needs to be installed first:

> npm install casbin

Then, Casbin’s Node.js API can be used to instantiate anEnforcer, which is the main object through which the access requests are executed.

The simplest way to create an Enforcer is by providing the path to a model configuration file and a policy file. Then, in order to check whether an access request is allowed or not, the Enforcer’s enforce method is called, providing a set of arguments matching the definition of the request in the model.

import { newEnforcer } from 'casbin';
const enforcer = await newEnforcer('basic_model.conf', 
'basic_policy.csv');

const sub = 'alice'; // the user that wants to access a resource.
const obj = 'data1'; // the resource that is going to be accessed.
const act = 'read'; // the operation that the user performs on the 
resource.

const res = await enforcer.enforce(sub, obj, act);
if (res) {
  // permit alice to read data1
} else {
  // deny the request, show an error
}

Policy storage

In the simple example above, the policies are stored on the file system. In a more complex system, the policies may need to be more dynamic. For instance, a user may gain more permissions, new users may be added to the system and granted privileges and so on. All these circumstances lead to new policies being created and existing policies being modified or even removed.

Therefore, more flexible storage mechanisms may be necessary — a database, for example. Casbin allows this level of flexibility via adapters. Many adapters are already available for Node.js and include support for storing policies in PostgreSQL, MongoDB, Redis and others.

Besides, in a complex system, the application carrying out access request checks may run in multiple processes and the policies would need to be in sync in order to apply them consistently. Casbin supports this via watchers, which are a mechanism to notify other nodes that policies have changed. As with adapters, several implementations of watchers are available already for Node.js.

Using Casbin in Fastify via fastify-casbin

At NearForm, we use Fastify as our main web application framework for Node.js. Among other things, Fastify has a great plugin architecture, which makes building and using plugins easy and effective.

We have started using Casbin extensively in our projects, so, instead of duplicating the same code to set up Casbin every time, we created a Fastify plugin for it: fastify-casbin. The plugin must be installed alongside casbin NPM package:

> npm install casbin fastify-casbin

The plugin helps by encapsulating the initialisation and shutdown logic and exposes a decorator that can be used in the application’s code to carry out access requests, as shown in the example below.

const fastify = require('fastify')()

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

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

  return `You're in!`
})

The model and policy files used in the example can be found among the examples provided on Casbin’s website.

In the basic example, the plugin is registered on the Fastify application and configured to use a file system–based model and policy file.
Then, the casbin decorator exposed by the plugin on the Fastify instance is used within a route to imperatively check for access.

A more realistic example could use PostgreSQL via casbin-pg-adapter and casbin-pg-watcher, as shown in the following example:

const fastify = require('fastify')()
const { newAdapter } = require('casbin-pg-adapter').default
const { newWatcher } = require('casbin-pg-watcher')

const pgOptions = {
  connectionString: 'postgres://localhost'
  migrate: true
}

fastify.register(require('fastify-casbin'), {
  modelPath: 'basic_model.conf', // the model configuration
  adapter: await newAdapter(pgOptions), // the adapter
  watcher: await newWatcher(pgOptions) // the watcher
})

// add some policies at application startup
fastify.addHook('onReady', async function () {
  await fastify.casbin.addPolicy('alice', 'data1', 'read')
})

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

  return `You're in!`
})

In the example above, the fastify-casbin plugin is registered and configured to provide the modelPath as in the earlier example. The adapter option, instead of being a string and indirectly using a file system–based adapter to store policies, is provided as an instance of the casbin-pg-adapter, which stores policies in a PostgreSQL database.

For the sake of completeness, a watcher is also provided, which ensures that the policies are kept in sync in case the application is running in multiple processes. For simplicity, the watcher being used is a PostgreSQL-based implementation, but other implementations are also available.

The example also assumes that the database table used by the adapter to store policies will be empty initially, thereby a policy is added programmatically at application startup using Casbin’s APIs.

Overall, fastify-casbin provides low-level and non-opinionated APIs to integrate Casbin with Fastify and invoke them imperatively in your application’s code.

A declarative way to check access

In some scenarios, access control could be carried out in a more declarative way — for example, by making assumptions about where sub, obj and act come from.
In a RESTful API built with Fastify, for instance, we could assume that:

  • sub is stored on the Fastify request object as request.user
  • obj is the path of the HTTP request
  • act is the method of the HTTP request (GET/POST/PUT/DELETE/…)

These assumptions make it unnecessary to imperatively invoke the enforce method because this could be done automatically via Fastify Hooks or Route Options.

For this reason, we have built fastify-casbin-rest, which adopts this approach, while still allowing some customisation options.

fastify-casbin-rest

This plugin is an extension of the basic fastify-casbin plugin (which must be installed and registered in the Fastify application) and provides a declarative way to apply access control to routes of a RESTful HTTP application.
npm i casbin fastify-casbin fastify-casbin-rest

Because of the higher abstraction level provided by this plugin, access checks can be performed automatically by enabling the plugin on a Fastify route, as shown in the following example:

fastify.route({
  // ... other route options
  casbin: {
    rest: true
  }
})

The plugin uses some defaults to extract sub, obj and act from the request and to define what’s returned to the client in case an authorisation check fails, but all options can be customised.
sub, obj and act are a conventional way to define entities participating in the access control logic. In a multi-tenant system, for instance, an additional entity may be taking part in such logic, and their names may differ based on how the model is configured.
The source code for fastify-casbin-rest is available on GitHub and provides more thorough documentation and a fully working example of using it in a Fastify application.

Summary

Access control is a typical requirement of web applications, and Casbin provides an excellent foundation to implement a robust and flexible authorisation system.
The extensible nature of the Fastify web framework makes it a perfect choice for abstracting away some of the complexity of carrying out access control via its plugin system.

A low-level plugin like fastify-casbin can provide the foundation to create a more specialised and declarative authorisation system that doesn’t require routes to interact with its API directly, as we did in fastify-casbin-rest.

In the same vein as fastify-casbin-rest, an application could model its custom authorisation system and abstract away its implementation details in a fastify plugin, thereby leaving the application routes free of authorisation logic, which is centralised and applied automatically throughout the application.

View all posts  |  Technology  |  Business  |  Culture  |  Opinion  |  Design
Follow us for more information on this and other topics.