Rails|CanCanCanの使い方解説

この投稿は、弊社が提供するWESEEK TECH通信の一環です。
WESEEK TECH通信とは、WESEEKのエンジニアがキャッチアップした技術に関する情報を、techブログを通じて定期的に発信していくものです。

はじめに

今回の記事では Rails の権限管理 gem である CanCanCan について、導入手順と機能の解説を行います。導入手順を飛ばして機能の解説を読みたい方はこちらを押してください。

CanCanCan とは

CanCanCan とは、Ruby on Rails の権限管理 gem であり、特定のユーザーがアクセスできるリソースを制限します。

どういうことかと言うと、例えば admin, manager, read_only という3つの権限があったとして、それぞれは以下のことができるように設定を行うことができます。

  • admin という権限を持っているユーザーは管理者用の画面含め全ての画面を閲覧可能で、ページ上で全てのモデルの操作が可能
  • manager という権限を持つユーザーは管理者用の画面を閲覧することができないが、それ以外のページでモデルの操作が可能
  • read_only という権限を持つユーザーはモデルの操作ができず閲覧のみ可能

CanCanCan を用いることでこんな感じの機能を簡単に実装することができます。

CanCanCan を使うための準備

CanCanCan はこの gem 単体で用いることはほとんどなく、多くの場合は devise というログイン機能を実装する gem とセットで使います。

devise により追加された User モデルに今回新しく追加する Role モデルを紐づけて、それを CanCanCan で追加する Ability クラスで管理して使います。
この記事では、devise によってすでに User モデルが存在するという前提で解説をします。

devise に関しては今度機会があれば解説の記事を書こうと思います。

インストール

ではまず、CanCanCan のインストールを行います。
Gemfile に以下の記述を追加し、bundle install を実行します。

gem 'cancancan'
$ bundle install

Gemfile.lock を確認し、CanCanCan がインストールされていることを確認します。

cancancan (3.3.0)

Role モデルの作成

インストールが完了したら、ユーザーに付与する権限を管理する Role を作成していくのですが、今回は1つのユーザーに複数の権限を付与できるようにしたいと思います。

なので Role モデルと、User - Role 間の多対多を実現するための中間テーブルとして機能する UserRole モデルの2つを作成します。

$ rails generate migration CreateRoles
      invoke  active_record
      create    db/migrate/20210904000000_create_roles.rb

マイグレーションファイルが作成されたので、このファイルに以下の記述を追加します。
カラムは権限名を管理する name を必須カラムとして定義します。

class CreateRoles < ActiveRecord::Migration[6.0]
  def change
    create_table :roles do |t|
      t.string :name, null: false
      t.timestamps
    end
  end
end

次に UserRole モデルを作成します。

$ rails generate migration CreateUsersRoles
      invoke  active_record
      create    db/migrate/20210904000000_create_user_roles.rb

UserRole モデルには中間テーブルの役割を持たせるため、references 型を用いて外部キー制約を付けます。
さらに user と role の組み合わせが重複しないように unique 制約も付けます。

class CreateUserRoles < ActiveRecord::Migration[6.0]
  def change
    create_table :user_roles do |t|
      t.references :user, foreign_key: true, null: false
      t.references :role, foreign_key: true, null: false
      t.timestamps

      t.index [:user_id, :role_id], unique: true
    end
  end
end

これが終わったら migrate を実行させてデータベースに反映させます。

$ rails db:migrate
== 20210904000000 CreateRoles: migrating ======================================
-- create_table(:roles)
   -> 0.0144s
== 20210904000000 CreateRoles: migrated (0.0146s) =============================

== 20210904000000 CreateUserRoles: migrating ==================================
-- create_table(:user_roles)
   -> 0.0362s
== 20210904000000 CreateUserRoles: migrated (0.0363s) =========================

そうしたらモデルファイルの作成を行います。

app/models 配下に role.rb を用意して以下の記述を追加します。

class Role < ApplicationRecord
  has_many :users, through: :user_roles
  validates :name, presence: true
end

user_role.rb も用意して記述します。

class UserRole < ApplicationRecord
  belongs_to :user
  belongs_to :role
end

既存の user.rb には以下の1行を追加します。

has_many :roles, through: :user_roles

これにて Uesr - Role 間で多対多の関連付けが設定できました。

今回は1つのユーザーに複数の権限を付与するために Role と UserRole カラムを作成しましたが、1つのユーザーに1つの権限しか付与しないのであれば、User モデルに role カラムを追加するだけで問題ないです。

Ability クラスの作成

次に権限ごとのルールを設定する Ability クラスを作成します。

$ rails generate cancan:ability
      create  app/models/ability.rb

models 配下に ability.rb が作成されました。

設定を行い CanCanCan を使ってみる

Role モデルの作成と Ability クラスの作成が終わり、ようやく下準備が終わりました。
それでは早速 CanCanCan を使おうと思うのですが、その前に権限を作成しましょう。

権限を作成する

今回は冒頭で話した3つの権限(admin, manager, read_only)を作成しようと思います。

  • admin という権限を持っているユーザーは管理者用の画面含め全ての画面を閲覧可能で、ページ上で全てのモデルの操作が可能
  • manager という権限を持つユーザーは管理者用の画面を閲覧することができないが、それ以外のページでモデルの操作が可能
  • read_only という権限を持つユーザーはモデルの操作ができず閲覧のみ可能

権限の作成自体はとても簡単で、Role モデルにレコードを追加するだけです。
rails コンソールを用いて作成していきます。

$ rails console

コンソールが起動したら、Role モデルに対して create コマンドを実行して作成します。

[1] pry(main)> Role.create(name: 'admin')
#<Role:0x0000559bddf1b9d0> {
                 :id => 1,
            :name => "admin",
    :created_at => Sat, 04 Sep 2021 09:26:22 UTC +00:00,
    :updated_at => Sat, 04 Sep 2021 09:26:22 UTC +00:00
}

続いて manager, read_only も作成します。

[2] pry(main)> Role.create(name: 'manager')
[3] pry(main)> Role.create(name: 'read_only')

これで3つの role が作成されました。

権限ごとにルールを設定する

admin であれば全ての画面を閲覧可能、read_only であれば 閲覧のみ可能というように、それぞれの Role に対して設定を行なっていきます。

先ほどの rails generate cancan:ability で app/models 配下に ablity.rb というファイルが生成されているはずなので、このファイルを開きます。

class Ability
  include CanCan::Ability

  def initialize(user)
    # Define abilities for the passed in user here. For example:
    #
    #   user ||= User.new # guest user (not logged in)
    #   if user.admin?
    #     can :manage, :all
    #   else
    #     can :read, :all
    #   end
    #
    # The first argument to `can` is the action you are giving the user
    # permission to do.
    # If you pass :manage it will apply to every action. Other common actions
    # here are :read, :create, :update and :destroy.
    #
    # The second argument is the resource the user can perform the action on.
    # If you pass :all it will apply to every resource. Otherwise pass a Ruby
    # class of the resource.
    #
    # The third argument is an optional hash of conditions to further filter the
    # objects.
    # For example, here the user can only update published articles.
    #
    #   can :update, Article, :published => true
    #
    # See the wiki for details:
    # https://github.com/CanCanCommunity/cancancan/wiki/Defining-Abilities
  end
end

お、何やら沢山のコメントが書かれているようです。
このコメントに設定方法が記されており、意訳するとこのようになります。

例えばこのようにして ability を定義します。

user ||= User.new
if user.admin?
  can :manage, :all
else
  can :read, :all
end

can の第一引数には :read, :create, :update, :destroy を定義します。
manage を渡すとこれらの4つ全てが有効になります。

can の第二引数には、第一引数で設定したアクションを実行できるモデルを設定します。
:all を渡すと全てのモデルが対象になります。

can の第三引数ではさらに追加の設定を行うことができます。

詳しくは wiki をご覧ください。
https://github.com/CanCanCommunity/cancancan/wiki/Defining-Abilities

基本的には第一引数にアクションを定義し、第二引数でそのアクションを実行できるモデルを定義します。

can :read, :all
can :update, Book

例えばこのような記述だと、各モデルの一覧画面と詳細画面を閲覧でき、Book モデルだけは編集画面の閲覧とモデルの更新(update)の操作を行えるようになります。

この説明を元に各権限に対して設定を行うと、以下のようになります。

class Ability
  include CanCan::Ability

  def initialize(user)
    user ||= User.new

    # role が admin のユーザーはモデルの操作を行うことができて管理者画面を閲覧可能
    if user.has_role?(:admin)
      can :manage, :all
      can :access_admin_page, :all

    # role が manager のユーザーはモデルの操作を行うことができるが、管理者画面は閲覧不可能
    elsif user.has_role?(:manager)
      can :manage, :all
      cannot :access_admin_page, :all

    # role が read_only のユーザーはモデルの操作を行うことができず閲覧のみ可能、管理者画面は閲覧不可能
    elsif user.has_role?(:read_only)
      can :read, :all
      cannot :access_admin_page, :all

    # role を持っていないユーザーは全ての画面が閲覧不可能
    else
      cannot :read, :all
    end
  end
end

cannot は can の反対でアクションを実行できなくします。
一部の画面だけアクションを制限させたい場合に、can :manage, :all などの記述と併用することで楽に設定を行えます。

authorize_resource でアビリティをチェックする

ability.rb ファイルに権限ごとに設定を行いましたが、これだけでは設定を行なっただけで実際にアビリティのチェックが行われません。

チェックを行うにはコントローラーファイルに設定を記述する必要があります。
以下の例では、ユーザーが edit メソッドを実行した時に :update のアビリティを持っているかチェックし、持っていれば edit 画面を表示するようにしています。

class BooksController < ApplicationController
  def edit
    @book = Book.find(params[:id])
    authorize! :update, @book
  end
end

これでも全然問題ないですが、全てのアクションに authorize! を記述してチェックを行うのは少し面倒です。
そういう時は authorize_resource を記述することによって、そのコントローラー内の各アクションでチェックを行えます。

class BooksController < ApplicationController
  authorize_resource

  def edit
    @book = Book.find(params[:id])
  end
end

authorize! や authorize_resource でのチェックが実行された時、アクションに対応するアビリティを持っていればそのままメソッドが実行され、持っていなければ AccessDenied エラーが発生します。

管理者画面の閲覧を URL で判定して制限する

さて、基本的な ability は :read, :create, :update, :destroy, :manage の5種類なのですが、先ほどの例では :access_admin_page という ability も使用しています。
これは自分で作成した ability であり、管理者画面の閲覧が可能かどうかをこの ability で判断できるようにしようと思います。

今のままだとどういった画面が管理者画面なのかをアプリケーション側が判断することができないので、次にそれの設定を行います。

access_admin_page という ability を持っている場合にのみ管理者画面の閲覧ができるようにします。

apprication_controller.rb に以下の記述を追加します。

before_action :check_admin_authorization

def check_admin_authorization
  if request.path.start_with?('/admin')
    authorize! :access_admin_page
  end
end

この記述について説明します。
今回の例では、url が /admin で始まる画面を管理者画面とします。

原則として全ての Controller はこの ApplicationController を継承しているため、どの画面を開いたとしても before_action により check_admin_authorization メソッドが実行されることになります。

check_admin_authorization メソッドでは、まず初めに現在のパス(画面を開いた時のパス)が /admin で始まっているかどうかを判定します。

/admin で始まっていた場合、authorize! メソッドが実行されて :access_admin_page の ability を持っているかどうかチェックします。

ability を持っていた場合は何も起きず、持っていなかった場合は AccessDenied というエラーが発生します。

これにより、ability を持っていないユーザーのアクセスを制限することができました。

AccessDenied エラー発生時に別の画面へ遷移させる

しかし、ユーザーがアプリケーションを操作中に AccessDenied のエラー画面が出てきてしまうのはあまり良くないので、次はこのエラーが発生した時に別の画面へ遷移させてあげましょう。

apprication_controller.rb に以下の記述を追加します。

rescue_from CanCan::AccessDenied do |_exception|
  redirect_to root_path, alert: '画面を閲覧する権限がありません。'
end

この例では AccessDenied エラーが発生したときに root パスにリダイレクトさせ、さらにメッセージを表示させるようにしています。

<%= content_tag(:div, flash[:alert], class: "alert alert-danger") if flash[:alert] %>

エラー画面にならず、root パスにリダイレクトされてメッセージが表示されることが確認できました。

その他詳しい解説と便利な機能

アビリティのエイリアス

CanCanCan には :read, :create, :update, :destroy (と :manage) のアビリティが用意されていますが、これに加えて Rails の 7 つの RESTful アクションと同じエイリアスが自動的に設定されています。

  • read: [:index, :show]
  • create: [:new, :create]
  • update: [:edit, :update]
  • destroy: [:destroy]

このエイリアスが存在するおかげで authorize_resource 使用時にアクションに対応するアビリティをチェックできます。

リソースの自動読み込みで Controller ファイルを省略する

CanCanCan にはリソースを自動で読み込む便利な機能が備わっています。

この機能を用いた場合、例えば index メソッドだと

def index
  @books = Book.all
end

show メソッドであれば

def show
  @book = Book.find(params[:id])
end

などといったリソースを読み込む際に用いられる一般的な処理を、Controller ファイルに load_resource を記述することによって自動で行ってくれるようになります。

リソースの自動読み込みを有効にした際、モデル名に準ずる処理が実行されます。
例えば Book モデルの場合、@books や @book のインスタンス変数にリソースが格納されます。

これにより :index, :show, :new, :edit の4つのメソッドを完全に省略することができるため、ただの CRUD 機能を持ったモデルの Controller ファイルは以下のように書き換えられます。

class BooksController < ApplicationController
  load_resource

  def create
    Book.create(book_params)
    redirect_to books_path
  end

  def update
    @book.update(book_params)
    redirect_to books_path
  end

  def destroy
    @book.destroy
    redirect_to books_path
  end

  private

  def book_params
    params.require(:book).permit(:title)
  end
end

read_resource の他に load_and_authorize_resource という機能も存在しており、こちらは名前の通り、read_resource と authorize_resource を合体させた機能になっています。

また、authorize_resource, read_resource, load_and_authorize_resource は :only オプションを用いることで一部のメソッドだけ有効にできます。

# index と show メソッドのみ権限をチェックする
authorize_resource only: [:index, :show]

もっと詳しく知りたい方はこちら

他人の作成したレコードを操作できなくする

自身で作成したレコードしか更新・削除をできないようにしたい場合、can の第三引数を用いることによって設定できます。

class Ability
  include CanCan::Ability

  def initialize(user)
    user ||= User.new

    can [:read, :create], :all
    can [:update, :destroy], :all, user: user
  end
end

例えばこのように設定することで、全てのモデルにおいて閲覧と作成はできるが、更新と削除は自身が作成したものしか行えないようにできます。
ちなみにアビリティは配列 [] を用いることでまとめて設定可能です。

ログインしていないユーザーに実行権限を与える

基本的にログインしていないユーザーは何もできませんが、can の第三引数に published: true を設定することで、ログインしていなくてもアクションを実行できるようになります。

# ログインしていなくても Book モデルの閲覧だけはできる
can :read, Book, published: true

おまけ

自社のプロジェクトでは ability.rb にアビリティの設定を記述するのではなく、Ability モデルを作成してデータベース上でアビリティを管理しています。
通常、権限の設定を変更するには Ability.rb を書き換える必要がありますが、データベース上でアビリティを管理することにより、アプリケーション上から権限の設定を行えるようになる他、より柔軟な設定が可能となります。

終わりに

CanCanCan を用いることで、ユーザーの権限ごとに細かい設定を行うことができました。
非常に便利で使うメリットも大きいので、Rails アプリケーションに devise を採用している場合は是非採用を検討してみてください。