CanCanCanを使ってみる


deviseを使ってみる」の続きです。
ユーザーの権限付与に便利なCanCanCanを導入してみました。
公式Github→https://github.com/CanCanCommunity/cancancan#readme

Gemfileの更新

Gemfile
+ gem 'cancancan'
bundle install

ちなみにbundle installと、updateの違いがいまいちよく分かっていませんでしたが、簡単に記述すると、

  • install: Gemfile.lockを参照してGemをインストールする。
  • update: Gemfile.lockを参照せずにインストールする。

詳しい解説は、下記の記事にて記載されていますが、基本的にはbundle installを使用する方が良いようです。
bundle install と bundle updateの違いについて

roleカラムの追加

deviseでデフォルト作成されるテーブルを使用していたため、そこにroleという権限を管理するカラムを追加します。
カラムの追加なので、マイグレーションファイルを作成します。

rails g migration add_role_to_admin_users role:integer

そして、生成されたマイグレーションファイルを以下のように修正し、rails db:migrateします。

class AddRoleToAdminUsers < ActiveRecord::Migration[5.0]
  def change
    add_column :admin_users, :role, :integer, null: false, default: 2
  end
end

権限のカラムはINT型でデータを保管するようにしました。
2の定義は後ほど実施していきます。

余談ですが、SQLite、PostgreSQLでは、カラムを指定位置に追加することができないようです。

モデルの修正

新しいカラムを作成したので、モデルを修正します。
enumを使用し、この文字列ならばこの数値をDBに入れるよう定義します。

admin_user.rb
+ enum role: { admin: 1, member: 2 }

Abilityクラスの作成

CanCanCanでキモとなるAbilityクラスを作成します。
とはいえ、rails genereteを使用します。

rails g cancan:ability

これで、app/models/ability.rbが作成されます。
デフォルトではinitializeがあるものの、特に記載がないため、今回の条件に合わせておきます。
なお、今回はadminmemberの2つの権限を作り、adminは全て実行可能、memberはリードオンリーなイメージとします。
2017-05-28 追記
ShoheiNakano様よりご指摘をいただいた項目を修正しました。
(今回はAdminUserモデルを使用しているため、AdminUser.newになります。)
合わせて、引数もadmin_userに修正しました。

app/models/ability.rb
class Ability
  include CanCan::Ability

  def initialize(admin_user)
    admin_user ||= AdminUser.new
    if admin_user.admin?
      can :manage, :all
    end

    if admin_user.member?
      can :read, :all
    end
  end
end

権限には基本5種類あり、adminにはmanageを、memberにはreadを割り当てました。

  • read: 読み込み
  • create: 新規作成
  • update: 更新
  • destroy: 削除
  • manage: 全て

canの後に許可したい権限を指定し、第2引数で許可する場所を指定します。
今回はまだページもないので、allとしました。
こちらの記事が非常に詳しく記載しております。
How to use CanCan / CanCanCan

テスト用にビューを修正

権限付与のテストのため、以下をビューに追記します。

+ <% if can? :update, current_admin_user %>
+   <h1>You can update</h1>
+ <% end %>
+ <% if can? :read, current_admin_user %>
+   <h1>You can read member</h1>
+ <% end %>

can?メソッドで権限を確認し、その権限が第2引数で渡されたユーザーにあれば、trueを返します。

なお、このcan?を利用する際は、暗黙的に以下が呼ばれます。

def current_ability
  @current_ability ||= Ability.new(current_user)
end

私の場合ですが、今回deviseにてAdminUserモデルを作成してしまった関係上、current_userではなく、current_admin_userを引数にとってもらいたいです。

その場合はオーバーライドします。

app/controllers/application_controller.rb
+ def current_ability
+    @current_ability ||= Ability.new(current_admin_user)
+ end

実際に試してみる

まずは、roleの値が2のユーザーで試すと、(member)

のみが表示されます。
ここで、roleの値を1に変えると、(まだ編集機能などはないので、テーブルを直接書き換えて)

updateとreadの両方が表示されます。
1にはmanageにより全権限があるため、readでもupdateでもtrueが返されます。

deviseで新規登録する際にroleも登録する

ここからはdeviseを使用している場合になります。
新しくroleカラムを追加したので、ストロングパラメータに追加してあげる必要があります。

app/controllers/application_controller.rb
+ before_action :configure_permitted_parameters, if: :devise_controller?

# 省略

+ private
+   def configure_permitted_parameters
+       devise_parameter_sanitizer.permit(:sign_up, keys: [:role])
+   end

今回のケースに限らず、カラムを追加したい場合はこのようにするようです。
deviseの公式Github→https://github.com/plataformatec/devise#readme

そして新規登録の際の入力フォームに追記します。(今回は登録のみ修正します。)

app/views/devise/registrations/new.html.erb
+ <div class="field">
+   <%= f.label :role %><br />
+   <%= f.select :role, {'administer' => 'admin', 'member' => 'member'}, {selected: 'member', include_blank: false}, {autofocus: 'true', class: 'form-control'} %>
+ </div>

form_forselectを利用して、以下のようなHTML文を出力しています。
enumにより、adminを渡せば1が、memberを渡せば2がDBに入るようになっています。

<select id="admin_user_role" name="admin_user[role]">
    <option value="admin">administer</option>
    <option value="member" selected>member</option>
</select>