Vue Js Role-Based Access Control with CASL Library
Role-based access control (RBAC) is a method of restricting network access based on the roles of individual users within an enterprise. 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. RBAC has become a much-needed feature in almost every application be it a web application or mobile application.
While working on a Vue project and being assigned with a task of creating layered role-based access control system for a web application I came across a lot of libraries and methods for managing user permissions in a Vue JS App, namely CASL Js, Vue with Laravel, Vue-kindergarten, ACL plugin, etc. I started listing out these JavaScript libraries and attempting to compare them carefully so as to use the most appropriate library for my project considering constraints such as time, compatibility with Vue.js, the community that backs the library and options that the library provides. This introduced me to CASL a neat little library that helps manage user permissions very simply.
What CASL is?
- CASL is an isomorphic authorization JavaScript library which makes permissions management easy. Heavily inspired by cancan.
- All permissions are defined in a single location (the Ability class) and not duplicated across UI components, API services, and database queries.
- An actively maintained library by Sergii Stotskyi, with 11 versions, 1066 stars, 71 forks, rapidly gaining acknowledgment and attention.
- A well-documented library can see the documentation for more details.
- See CASL CHANGELOG for details and migration guide;
Get Started
The Ability
class is where all user permissions are defined. You can create Ability
using AbilityBuilder
or Ability
constructor.
AbilityBuilder
allows defining rules in DSL-like style with the help of can
and cannot
functions:
import { AbilityBuilder } from '@casl/ability'function defineAbilitiesFor(user) {
return AbilityBuilder.define((can, cannot) => {
if (user.role ==='Admin') {
can('manage', 'all')
} else {
can('read', 'all')
}
})
}
- Here we define a function named defineAbilitiesFor, the function name can be anything as per your choice.
- The current user object is passed into the
defineAbilitiesFor
function, so the permissions can be modified based on any user attributes, i.e we can use user.id, or user.email, etc for defining the functions. - The ability rules need to get updated after declaration which can be done via Ability constructor as follows
import { AbilityBuilder, Ability } from '@casl/ability'
import { abilityPlugin } from '@casl/vue'
import store from '@/store'
import Vue from 'vue';const ability = new ability([])
function defineAbilitiesFor(user) {
return AbilityBuilder.define((can, cannot) => {
if (user.role ==='Admin') //rules per user
{
can('manage', 'all')
} else {
can('read', 'all', ['title', 'description']) //rule per field
}
})
}//to allow ability to be used in all components
Vue.use('abilityPlugins', 'ability')
....//logic for authentication
...//current user from Vuex store after authenticationability.update(defineAbilitiesFor(this.$store.state.user))
// check ability
ability.can('read','Post') //true
In our Vue app, we basically get the current user after authentication from the Vuex store. More about Vuex store.
Adding the abilities plugin, allowing us to make tests within a component like this.$can(...)
.
Note:
- Always use abilityPlugin before ability in Vue.use()
- There is no hardcore rule of defining the abilities in one config folder or so, nor any rule for Vue.use() to be in main.js. It depends upon our requirements.
- Recommended: to place the function defineAbilityFor(), Vue.use(), ability.update() once authentication is done, where we can get the current user easily and asynchronously.
- Always update the ability after you get the user, or else the ability.rule() will be null/empty.
We now want to be able to test an object in our front-end app to see what CRUD operations the user is allowed to perform on it. We’ll need to provide access to the CASL rules within our Vue components.
A glimpse of how the role-based authorization will work in a Vue App. Here it shows that a user is allowed to edit or delete an article if he is logged in else he can only read the articles.
Using ability.can( ) inside Vue component
This is how I’d like to see CASL in Vue:
- all Vue components have
$can
method; - in places where it’s required to hide UI element, I’ll use
v-if
directive together with$can
method; - it should be possible to use
$can
method with any directive, component or filter.
The main advantage of exposing abilities via $can
method and not create a separate directive (e.g., v-can
) is a possibility to combine permissions logic with any other boolean checks and pass it as a parameter of directives and components.
<template>
<div class="post">
<div class="content">
{{ post.content }}
<br/><small>posted by {{ username }}</small>
</div>
<button @click="del">Delete</button>
</div>
</template><script>
import axios from 'axios';export default {
props: ['post', 'username'],
methods: {
del() {
if (this.$can('delete', this.post)) { // checks the ability rules
...
} else {
this.$emit('err', 'Only the owner of a post can delete it!');
}
}
}
}
</script>
SubjectName in CASL Js
AbilityBuilder.define has another signature which allows you to pass some ability options like subjectName. For example:
function subjectName(item) {
// logic to extract subject name from subject instances
// It's important to handle case when `subject` is undefined or string!
// Otherwise you will not be able to check abilities on class names (e.g., `ability.can('read', Post)`, where Post is not a string but object)if (!item || typeof item === 'string') {
return item
}
return item.constructor.name // returns object name i.e for project object returns Project
}
}const ability = AbilityBuilder.define({ subjectName }, can => {
can('read', 'all')
})
We use the subject name to allow the use of additional conditional checks apart from the subject name, for example:
// Ability rule where it checks if active is true and ownerId is same is user.id then we can read the projectcan('read', 'Project', { active: true, ownerId: user.id })// we can check the condition if we pass an object in ability check where project is an object with active and ownerId as its keys$can('read',project)// this statements goes through the subjectName function and returns Project and this is used to check the ability rule can('read', 'Project', conditions) if condition matches then we can read the project
Wrap Up
With that, we have a really nice way of managing user permissions in a simple Vue app.
I believe this.$can('delete', post)
is much more elegant than:
if (user.id === post.user && post.type === 'Post') {
...
}
This is not only more difficult to read, but, also, there’s an implicit rule here, i.e. that a post can be deleted by a user. This rule will undoubtedly be used elsewhere in our app, and should really be abstracted. This is what CASL can do for us.
Looking for more?
- See CASL documentation for per field checking and per field rules;
- Follow the creator of CASL on Twitter @sergiy_stotskiy to be up-to-date about CASL changes;
- Join Gitter channel to discuss CASL, request features or ask for help in the integration.
Thank you for reading!!