Gem CanCanCan

1. Giới thiệu gem CanCanCan:

  • CanCanCan là một authorization library giúp ta quy định được user nào có quyền truy cập được resource nào và có thể action gì với resource đó.
  • Tất cả logic về phần quyền có thể được định nghĩa trong một hay nhiều file mà không bị lặp lại giữa nhiều controller, view khác nhau.
  • Điều đó giúp cho logic phân quyền được tập trung tại một chỗ dễ dàng cho maintain và test hơn
  • CanCanCan gồm 2 phần chính
  1. Authorization library cho phép định nghĩa các rule khác nhau để quy định logic phần quyền cho các object khác nhau, cung cấp thêm các helper để check phân quyền.
  2. Rails helper giúp đơn giản hóa code trong Rails Controller bằng loading và checking phân quyền của từng model một cách tự động và hạn chế được lặp code.

2. Installation:

  • Thêm gem cancancan vào Gemfile
    # Gemfile
    gem "cancancan"
    
  • Chạy lệnh bundle install để install gem cancancan vào project Rails.

3. Define Abilities:

  • Chạy lệnh để tạo Ability class quy định logic phân quyền cho user
    rails g cancan:ability
    
  • File app/models/ability.rb được tạo ra
    class Ability
      include CanCan::Ability
    
      def initialize(user)
      end
    end
    
  • Method initialize của class Ability được dùng để định nghĩa logic phân quyền của user được truyền vào.
  • Ví dụ:
    can :read, :all
    
  • Với ví dụ trên user không cần login cũng có permission read tất cả (:all) model.
  • Hoặc
    can :read, :all if user.present?
    
  • Với ví dụ trên user đã login mới có permission read tất cả (:all) model.
  • Theo mặc định thì current_user được truyền vào trong method initialize của class Ability.
  • Ta có các ví dụ khác như sau
    # user không cần logic cũng có permission read với các post có public là true
    can :read, Post, public: true
    
    # user đã login ó permission read với các post có user_id là id của user đã login
    can :read, Post, user_id: user.id if user.present?
    
    # user admin đã login có permission read với các post (bất kể public và user_id của post đó)
    can :read, Post if user.present? && user.admin?
    

4. The can method:

  • Method can được sử dụng để quy định logic phân quyền.
  • Method can nhận hai tham số.
  • Tham số thứ nhất là action mà bạn đang quy đinh logic phân quyền.
  • action nhận 4 giá trị tương đương với CRUD lần lượt là :create, :read, :update, :destroy
  • Tham số thứ hai là class của object mà bạn đang quy định logic phân quyền.
  • Ví dụ:
    can :update, Article
    
  • Ngoài ra khi sử dụng Action AliasesCustom Actions , action có thể nhận thêm các giá trị khác.
  • actionclass cũng có thể nhận giá trị là array.
  • Ví dụ:
    can [:update, :destroy], [Article, Comment]
    
  • action có thể nhận giá trị là :manage tương đương với tất cả action.
  • class có thể nhận giá trị là :all tương đương với tất cả class.
  • Trong trường hơp có các action ngoài các action CRUD, khi action nhân giá trị :manage thì user cũng có permission để thực hiện các action này.
  • Ví dụ:
    can :manage, User
    can :invite, User
    
  • Khi đó user có thể thực hiện những action CRUD và action invite với User.
  • Để user chỉ thực hiện những action CRUD thôi, ta có thể sử dụng AliasAction như sau.
    alias_action :create, :read, :update, :destroy, to: :crud
    can :crud, User
    
  • Bản thân các action :create, :read, :update cũng là các alias action mặc định như sau.
    alias_action :new, to: :create
    alias_action :index, :show, to: :read
    alias_action :edit, to: :update
    

5. Hash of condition:

  • Hash condition có thể được thêm vào method can để quy định logic phân quyền được apply cho record nào.
  • Ví dụ:
    can :read, Project, active: true, user_id: user.id
    can :read, Project, priority: 1..3
    
  • Trong trường hợp logic phân quyền dựa trên association, bạn có thể sử dụng nested hash như sau
    can :read, Post, category: {visible: true}
    can :read, Project, group: {id: user.group_ids}
    
  • Hash condition cũng có thể sử dụng scope như sau
    can :read, Photo.unowned do |photo|
      photo.groups.empty?
    end
    

6. Combining Abilities:

  • Chúng ta có thể định nghĩa nhiều logic phân quyền cho chung một resource.
  • Ví dụ
    can :read, Project, released: true
    can :read, Project, preview: true
    
  • Với ví dụ trên thì user có thể thực hiện action :read với Project có released là true hoặc có preview là true.
  • Bên cạnh can ta còn có cannot để quy định user không thể thực hiện action nào đó
  • Ví dụ
    can :manage, Project
    cannot :destroy, Project
    
  • Với ví dụ trên thì user có thể thực hiện tất cả action trừ action :destroy.

7. Check Abilities:

  • Bên cạnh method cancannot để quy định logic phân quyền của user với từng model, cancancan cung cấp thêm method can?cannot? để kiểm tra user có thể thực hiện action nào đó hay không.
  • Trong view và controller ta có thể thực hiện kiểm tra phân quyền như sau
    can? :create, Project
    
  • Nếu method can nhận hash condition, method can? được gọi với class sẽ bỏ qua hash condition và luôn luôn trả về true
  • Ví dụ
    can :read, Project, priority: 3
    
    can? :read, Project # true
    can? :read, project # true nếu project.priority == 3
    can? :read, project # false nếu project.priority != 3
    
  • Nguyên nhân là với record project thì có thể thực hiên kiểm tra project.priority có bằng 3 hay không
  • Nhưng với class Project thì không thể thự hiện kiểm tra như vậy được.
  • Nguyên nhân cancancan trả về true với case này là để sử dụng cho action index
  • Tuy nhiên đối với action index, thay vì sử dụng scope all ta nên sử dụng Fetching Records với scope accessible_by như sau
  • Ví dụ:
    Project.accessible_by(curent_action, :index)
    
  • Đối với rails console ta có thể thực hiện kiểm rea phần quyền như sau
    user = User.first
    curent_ability = Abilirt.new user
    curent_ability.can? :read, Project
    

8. Controller helpers:

  • CanCanCan cần method curent_user tồn tại trong controller.
  • Do đó cần setup với gem Devise trước khi thực hiện phân quyền với gem CanCanCan.

1. Authorizations:

  • Method authorize! được gọi trong controller sẽ raise exception nếu user không được phân quyền để thực hiện action đang được authorize.
  • Ví dụ:
    def update
      authorize! :update, @post
    end
    
  • CanCanCan sẽ raise CanCan::AccessDenied exception, chúng ra cần thực hiện rescue_from trong controller để tránh bị dừng chương trình hoặc hiển thị trang lỗi với user
    class ApplicationController < ActionController::Base
      rescue_from CanCan::AccessDenied, with: :cancan_access_denied
    
      private
    
      def cancan_access_denied
        flash[:danger] = "You are not authorized to access this page."
        redirect_to root_url
      end
    end
    

2. Loaders:

  • CanCanCan cung cấp method load_and_authorize_resource để tự động load resource và thực hiện kiểm tra quyền của user trước mỗi action.
  • Method này là kết hợp của hai methof load_resourceauthorize_resource
  • Method load_resource sẽ tự động thực hiện load resource trước mỗi action
  • Ví dụ
    class PostsController < ApplicationController
      load_resource
    
      def index
        # @posts được load bởi load_resource
        # @posts = Post.accessible_by(current_ability)
      end
    
      def show
       # @post được load bởi load_resource
       # @post = Post.find id: params[:id]
      end
    
      # action edit, update, destroy tương tự action show
    end
    
  • Method load_resource sẽ raise ActiveRecord::RecordNotFound exception với các action :show, :edit, :update, :destroy khi không tìm thấy resource.
  • Do đó ta cần thực hiện rescue_from ActiveRecord::RecordNotFound tương tự rescue_from CanCan::AccessDenied
    class ApplicationController < ActionController::Base
      rescue_from ActiveRecord::RecordNotFound, with: :active_record_record_not_found
    
      private
    
      def active_record_record_not_found
        flash[:danger] = "Couldn't find resource."
        redirect_to root_url
      end
    end
    
  • Method authorize_resource sẽ tự động thực hiện kiểm tra quyền của user với resource trước mỗi action.
    class PostsController < ApplicationController
      load_resource
      authorize_resource
    
      def index
        # authorize! :read, @posts
      end
    
      def show
       # authorize! :read, @post
      end
    end
    
  • CanCanCan cung cấp các method skip_load_and_authorize_resource, skip_load_resource, skip_authorize_resource để skip các method load_and_authorize_resource, load_resource, authorize_resource
  • Ví dụ:
    class PostsController < ApplicationController
      load_and_authorize_resource
      skil_load_and_authorize_resource only: :index
    
      def index
        # load_and_authorize_resource không được gọi
      end
    
      def show
        # load_and_authorize_resource được gọi
      end
    end
    

9. Source code demo: