How do I choose which user can access what resources?

TL;DR — Check out Casl, which supports both approach. Read on if you are curious about terminology/which one to implement.

Quick Recap: RBAC

Role-Based Access Control is a way of controlling what resources a user can create/read/update/delete given their role(s).

Example:

Any user view any post.

Any user can edit their own post.

Admin user can edit anyone’s post.

Admin user can create groups.

Admin user can attach/remove any user to/from a group.

When working with a resource in the server, we check that user’s role attribute and a mapping to decide if they can perform actions or not.

Quick Recap: ABAC

Attribute-Based Access Control is a way of controlling what resources a user can create/read/update/delete, given attributes from the user/resource/execution context.

Example:

Any user can view any user’s public post. (Check post’s visibility attributes)

Any admin user can view any post, whether it is public or private.

Any user can view their own private post. (Check post’s visibility attributes)

Any user can share their private posts to other users.

The difference between the two is that ABAC can consider more contextual information to decide whether to grant access or not.

If we only look at Resource Type and user role, we will have reduced ABAC back into RBAC.

Scenario: Just released the MVP to production

The version 0.0.1 alpha of your minimum viable product is complete. It can host content and has a user auth/signup system built-in. You are seeing traffic coming into the site. All is going well, then you realize:

Anyone can delete anyone else’s replies in the chat box.

Even though you do not display links to each member’s private post, if someone has the url link, they are able to view other member’s private post.

We would like to fix this, such that:

User can only delete replies they own.

Prevent user from accessing the private post information by knowing or guessing the url.

Without Ability frameworks

It is possible to implement the above without the use of frameworks:

router.delete('replies/:replyId', async (req, res, next) => {

const { replyId } = req.params;

const reply = await dbLookupReplyById(replyId);

if (reply === undefined) {

// respond with 404

}



if (reply.userId !== req.user.id) {

// respond with 401 - access denied

}

// otherwise remove the reply, return 200

}

And

router.get('private/:postId', async (req, res, next) => {

const { postId } = req.params;

const post = await dbLookupPostById(postId);

if (post === undefined) {

// respond with 404

} if (post.userId !== req.user.id) {

// respond with 401 - access denied

}

// otherwise return the content - 200

}

With one-off cases like this, it is easy enough to get by without using frameworks. However, when you have different types of users (viewer/editor/admin/reviewer), the logic to check permission and grant access would become duplicated and harder to test.

Ideally, we want the following signature on a function to check for access:

const userReplyAbility = getUserReplyAbility(user, reply)

userReplyAbility.throwUnlessCan(ReplyAction.delete, reply) const userPostAbility = getUserPostAbility(user, post)

userPostAbility.throwUnlessCan(PostAction.readPrivate, post)

And that the above would carry things out different base on both user role type, and reply/post ownership.

With a function that constructs a unique ability object in each context, we have very fine grained control, and can avoid having duplicate permission code sprinkled in the controller code.

Furthermore, we can define unit tests to test those ability objects! It makes code coverage around access control simpler (you do not have to test the entire controller method to validate that the correct access is given).

Casl

CASL is an isomorphic authorization JavaScript library which restricts what resources a given user is allowed to access.

From https://stalniy.github.io/casl/abilities/2017/07/20/define-abilities.html

import { AbilityBuilder, Ability } from '@casl/ability'



function defineAbilitiesFor(user) {

const { rules, can, cannot } = AbilityBuilder.extract()



if (user.isAdmin()) {

can('manage', 'all')

} else {

can('read', 'all')

}



return new Ability(rules)

}

Here we have a function that creates ability for a user to access all resources.

Once the ability object is created, you can use it like this:

ability.can('manage', 'Post') // true

ability.can('read', 'Post') // true // Since we never declared the delete action above, it is not allowed. ability.can('delete', 'Post') // false

ability.throwUnlessCan('delete', 'Post')

// Throws an exception, which we can catch to turn into 401 returns.

To fit into the earlier example, we simply pass more context information into the factory:

import { AbilityBuilder, Ability } from '@casl/ability'



function getUserReplyAbility (user, reply) {

const { rules, can, cannot } = AbilityBuilder.extract()



if (user.isAdmin()) {

can(['read', 'delete', 'update'], reply)

} else {

can('read', reply)

if (reply.userId === user.id) {

can('delete', reply)

}

}

return new Ability(rules)

}

And

import { AbilityBuilder, Ability } from '@casl/ability'



function getUserPostAbility (user, post) {

const { rules, can, cannot } = AbilityBuilder.extract()



if (user.isAdmin()) {

can(['read', 'read_private', 'delete', 'update'], post)

} else {

can('read', post)

if (post.userId === user.id) {

can('read_private', post)

}

// Here we can define additional rules of private posts based on user/post attributes

}

return new Ability(rules)

}

And there you have it!

ABAC in Node.JS with as much contextual attribute information as you desire. Putting those logic inside their own factories means we also get separation of concern at the controller level.

If you know of simpler ways to define access control, please do let me know.

Other Node.JS access control libraries

https://github.com/casbin/node-casbin — comes with their own DSL for resource definition.

https://github.com/onury/accesscontrol — Use chaining to define create/read/update/delete on resources, can use attribute list, but hard to insert additional context information.

https://github.com/seeden/rbac#readme — Role based only, does not look at resource/context attributes.

https://github.com/ZenitechSoftware/visa-js#readme — Policy definition DSL includes database lookup, and is more complex than CASL. Takes the middleware approach.

P.S. I have worked on projects where CASL is integrated into Express and NestJs. Please contact jack@teamzerolabs.com if you need pointers!

Thanks for reading!