Skip to content

Dashboard

Vue Js Role-Based Access Control with CASL Library

Created by Admin

Role-based access control (RBAC) là một phương thức kiểm soát truy cập hệ thống dựa trên role của từng user trong một tập thể. RBAC cho phép user chỉ có quyền truy cập tới các thông tin mà họ cần tới để làm việc và ngăn chặn truy cập vào các thông tin không liên quan gì tới họ. RBAC đã trở thành một chức năng vô cùng cần thiết trong hầu hết mọi ứng dụng kể cả web app hay mobile app.

Trong quá trình xây dựng một project sử dụng VueJs và được giao một số task liên quan tới việc triển khai RBAC cho một web app, tôi đã tham khảo một số thư viện và phương pháp khác nhau để quản lý phân quyền user như CASL Js, Vue-kindergarten,... Tôi đã thử so sánh các ưu nhược điểm giữa chúng và cuối cùng thì tôi đã lựa chọn CASL để giúp phân quyền user một cách dễ dàng.

CASL là gì ?

CASL là một thư viện Javascript cho phép quản lý phân quyền một cách dễ dàng và được lấy cảm hứng từ cancan. Tất cả quyền được định nghĩa tại một địa điểm (class Ability) và không trùng lặp xuyên suốt các UI components, API services và database queries.

Hãy xem CASL CHANGELOG để biết thêm chi tiết.

Bắt đầu

Class Ability là nơi định nghĩa tất cả các quyền của các user. Bạn có thể tạo nó dựa vào AbilityBuilder hoặc Ability constructor.

AbilityBuilder cho phép định nghĩa các quy tắc theo kiểu DSL với các hàm cancannot.

import { AbilityBuilder } from '@casl/ability'
function defineAbilitiesFor(user) {
  return AbilityBuilder.define((can, cannot) => {
    if (user.role ==='Admin') {
      can('manage', 'all')
    } else {
      can('read', 'all')
    }
  })
}
  • Tại đây, chúng ta định nghĩa một hàm tên là defineAbilitiesFor, tên này có thể đặt tùy ý bạn.
  • Object user hiện tại được truyền vào defineAbilitiesFor nên các quyền có thể được chỉnh sửa dựa trên bất kì một thuộc tính nào của user, ví dụ như dùng user.id hoặc user.email,... để định nghĩa các hàm.
  • Các quy tắc ability cần phải được cập nhật sau khi đã khai báo bằng Ability constructor.
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')  //quy tắc cho mỗi user
    {
      can('manage', 'all')    
    } else {
      can('read', 'all', ['title', 'description'])  //quy tắc cho mỗi trường
    }
  })
}
//cho phép sử dụng ability trong tất cả các component
Vue.use('abilityPlugins', 'ability')    
....
//logic xác thực
...
//user hiện tại từ Vuex store sau khi xác thực: authenticationability.update(defineAbilitiesFor(this.$store.state.user))
// check ability
ability.can('read','Post')  //true

Trong app, chúng ta sẽ lấy user hiện tại sau khi xác thực từ Vuex store.

Thêm vào plugin abilities để cho phép chúng ta có thể test trong một component như this.$can(...).

Note:

  • Luôn luôn sử dụng abilityPlugin trước ability trong Vue.use()
  • Không có một quy định nào buộc phải định nghĩa ability trong một folder hay gì cả, nó phụ thuộc vào yêu cầu của app.
  • Hãy định nghĩa các hàm defineAbilityFor(), Vue.use(), ability.update() ngay khi xác thực đăng nhập xong, khi đó ta có thể lấy được object user hiệ ntaij một cách dễ dàng và chính xác.

Ở trên, ta có thể thấy một user được phép edit hoặc delete một article nếu user này đã đăng nhập, nếu không thì người này sẽ chỉ có thể xem article thôi.

Sử dụng ability.can( ) trong Vue component

  • Tất cả Vue components đều có method $can
  • Khi cần ẩn một thẻ trong UI thì ta có thể dùng v-if với method $can
  • Ta có thể dùng $can với bất kì một directive, component hay filter nào.

Ưu điểm lớn nhất của việc cho phép kiểm tra ability thông qua method $can là có thể kết hợp logic của các quyền với các cách kiểm tra sử dụng boolean và truyền nó vào directives và components dưới dạng một parameter.

<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)) {   //kiểm tra quy tắc của ability
...
} else {
this.$emit('err', 'Only the owner of a post can delete it!');
}
}
}
}
</script>

SubjectName trong CASL Js

AbilityBuilder.define cho phép bạn có thể truyền một số tùy chọn ability như subjectName:

function subjectName(item) {
  // logic để bóc tách subject name từ một instances của subject
  // Cần xử lí các trường hợp khi `subject` là undefined hoặc string! 
if (!item || typeof item === 'string') {
return item
}
return item.constructor.name  // trả về object name, ví dụ: object project trả về Project
}
}
const ability = AbilityBuilder.define({ subjectName }, can => {
  can('read', 'all')
})

Ta dùng subject name để cho phép kiểm tra thêm các điều kiện bổ sung khác với subject name

// Quy tắc của ability để kiểm tra nếu active là true và ownerId giống user.id thì chúng ta có thể đọc project
can('read', 'Project', { active: true, ownerId: user.id })
// Chúng ta có thể kiểm tra điều kiện nếu truyền một object trong khi ability kiểm tra xem project có phải là một object với key là active và ownerId không.
$can('read',project)
// Các lệnh này sẽ được truyền qua hàm subjectName và trả về Project, đồng thời được dùng để kiểm tra ability can('read', 'Project', conditions) nếu một điều kiện thỏa mãn. Khi đó ta có thể đọc project 

Tổng kết

Như vậy, ta đã có một cách để quản lý phân quyền user một cách đơn giản trong app dùng VueJs.

Sử dụng this.$can('delete', post) sẽ hay hơn là

if (user.id === post.user && post.type === 'Post') {
...
}

Nó không chỉ làm khó đọc mà còn có một quy tắc ngầm hiểu ở đây. như là một post có thể bị delete bởi một user. Quy tắc này sẽ được sử dụng ở các nơi khác trong app và nó cần phải được trừu tượng hóa. CASL có thể làm điều này cho chúng ta.

Cảm ơn các bạn đã đọc!

Source: https://viblo.asia/p/vue-js-role-based-access-control-with-casl-library-Qpmley1olrd