[Rails5]cancancanってなんぞ？

# はじめに

Railsの勉強としてgem周りを触ってみる企画です。
今回は・・・

[cancancan](https://github.com/CanCanCommunity/cancancan)

です。

先日別記事で[gemのdeviseは試した](https://qiita.com/betti/items/12c0d8a47a08bd28c64d)のですが、
このdeviseはあくまで認証機能のgemだったので、
今回はこのcancancanを使って権限周りを実装してみたいと思います。

## どういうgemなの？

大体どのシステムにおいても、ユーザーに「権限」ってありますよね。
Aさんはこの画面見えるけど、Bさんは見せられないよ！とか。
そういった権限を管理するためのgem。それがcancancanです。

余談としては、元々はcancanという名前のgemで開発されていたが、
プロジェクトの開発が止まってしまったため、新たにcancancanとして、
メンテナス・保守をしているというgemとのことです。

## 検証環境

以下の環境で実施しました。

```
[client]
・MacOS Mojave(10.14.2)
・Vagrant 2.2.2
・VBoxManage 6.0.0

[virtual]
・CentOS 7.6
・Rails 5.2.2
・ruby 2.3.1
```

ご参考までに。

# cancancan導入

**1.gemのインストール**

事前にRails勉強用に作成したプロジェクトで進めます。
各MVCは適当に掲示板アプリを想定してPostsコントローラーなどを生成済みです。
(index,show,editの空ページが見れる程度)

```
$ rails g model post
$ rails g controller posts index show edit
```

その前提のもと、Gemfileに以下を追記。

```ruby:Gemfile
# cancancan
gem 'cancancan'
```

追記したらインストール。

```
$ bundle install

-----------
Fetching cancancan 2.3.0
Installing cancancan 2.3.0
-----------
→無事に完了。
```

**2.事前準備**

ではインストールが完了したので、
実際に権限周りを進めていきましょう。

が、その前に事前準備からです。
gemのdevise時にusersテーブルが作られており、

```
mysql> show columns from users;
+------------------------+--------------+------+-----+---------+----------------+
| Field                  | Type         | Null | Key | Default | Extra          |
+------------------------+--------------+------+-----+---------+----------------+
| id                     | bigint(20)   | NO   | PRI | NULL    | auto_increment |
| email                  | varchar(255) | NO   | UNI |         |                |
| encrypted_password     | varchar(255) | NO   |     |         |                |
| reset_password_token   | varchar(255) | YES  | UNI | NULL    |                |
| reset_password_sent_at | datetime     | YES  |     | NULL    |                |
| remember_created_at    | datetime     | YES  |     | NULL    |                |
| created_at             | datetime     | NO   |     | NULL    |                |
| updated_at             | datetime     | NO   |     | NULL    |                |
+------------------------+--------------+------+-----+---------+----------------+
```

このような構成になっています。
ここに、新しく権限という概念を追加するために、
カラムを追加します。名前は直球で`role`でいきます。

```
$ rails g migration add_column_role_to_users
    invoke  active_record
    create    db/migrate/20190117081130_add_column_role_to_users.rb
```

で、マイグレーションにカラム追加処理を書きます。

```ruby:20190117081130_add_column_role_to_users
class AddColumnRoleToUsers < ActiveRecord::Migration[5.2]
  def change
    add_column :users, :role, :integer, null: false, default: 1
  end
end
```

型はintegerにして、ユーザーには何かしらの権限が絶対必要なので、NULLはNGとしました。
デフォルトは1で一般、99がシステム管理者のような想定でいきます。

```
$ bundle exec rake db:migrate

== 20190117081130 AddColumnRoleToUsers: migrating =============================
-- add_column(:users, :role, :integer, {:null=>false, :default=>1})
   -> 0.1007s
== 20190117081130 AddColumnRoleToUsers: migrated (0.1009s) ====================

Annotated (3): app/models/user.rb, test/models/user_test.rb, test/fixtures/users.yml
```

```
mysql> show columns from users;
+------------------------+--------------+------+-----+---------+----------------+
| Field                  | Type         | Null | Key | Default | Extra          |
+------------------------+--------------+------+-----+---------+----------------+
| id                     | bigint(20)   | NO   | PRI | NULL    | auto_increment |
| email                  | varchar(255) | NO   | UNI |         |                |
| encrypted_password     | varchar(255) | NO   |     |         |                |
| reset_password_token   | varchar(255) | YES  | UNI | NULL    |                |
| reset_password_sent_at | datetime     | YES  |     | NULL    |                |
| remember_created_at    | datetime     | YES  |     | NULL    |                |
| created_at             | datetime     | NO   |     | NULL    |                |
| updated_at             | datetime     | NO   |     | NULL    |                |
| role                   | int(11)      | NO   |     | 1       |                |
+------------------------+--------------+------+-----+---------+----------------+
9 rows in set (0.00 sec)
```

カラムも追加されたので、準備は完了。

**3.cancancanを使う**

ようやく本題へ。
まずはAbilityクラスを生成します。

```
$ rails g cancan:ability
    create  app/models/ability.rb
```

コマンド自体は`cancancan`ではなく`cancan`。

```ruby:ability.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
```

initializeだけが準備されたものが生成されました。
また、自動で付いていたコメントにサンプルがありますね。
ありがてぇ・・・。
参考にして、実際に処理を書いていきます。

```ruby:ability.rb
class Ability
  include CanCan::Ability

  def initialize(user)
    user ||= User.new
    if user.admin?
      can :manage, :all
    else
      can :read, :all
    end
  end
end
```

とりあえずサンプルの書き方を見よう見まね。
ついでに、

```ruby:user.rb
class User < ApplicationRecord
  enum role: { general: 1, admin: 99 }

  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable
end
```

`enum`でuserモデルを調整。
(ちゃんとするなら別テーブルでロールの種類を管理しようね！)

続いて、画面の閲覧制限を入れてみます。
アプリ全体に関わるので、基底クラスの`application_controller`で対応。

```ruby:application_controller.rb
class ApplicationController < ActionController::Base
  # 各アクションで権限をチェック
  authorize_resource

  # 権限が無いページへアクセス時の例外処理
  rescue_from CanCan::AccessDenied do |exception|
    # root_urlにかっ飛ばす。
    redirect_to root_url
  end
end
```

権限がないページにアクセスされたら、
今回はindexページにリダイレクトさせる方法でやってみます。

## 動作確認

では`role`は1に設定した状態で、
早速当該ユーザーでログインしてみましょうかね。
`/users/sign_in`にアクセスして・・・

![cap1.png](https://qiita-image-store.s3.amazonaws.com/0/335844/69f42ca7-1c86-034e-fd5b-6b693a9a767c.png)

あれ！？

### エラー解決

原因は
先ほど追加したこの２つ。

```ruby:application_controller.rb
class ApplicationController < ActionController::Base
  # 各アクションで権限をチェック
  authorize_resource

  # 権限が無いページへアクセス時の例外処理
  rescue_from CanCan::AccessDenied do |exception|
    # root_urlにかっ飛ばす。
    redirect_to root_url
  end
end
```

```ruby:ability.rb
class Ability
  include CanCan::Ability

  def initialize(user)
    user ||= User.new
    if user.admin?
      can :manage, :all
    else
      can :read, :all
    end
  end
end
```

設定が不足してるから。
なのでこうしました。

```ruby:application_controller.rb
class ApplicationController < ActionController::Base
  # 各アクションで権限をチェック。オプションでモデル依存をfalseに。
  authorize_resource :class => false

  # 権限が無いページへアクセス時の例外処理
  rescue_from CanCan::AccessDenied do |exception|
    # root_urlにかっ飛ばす。
    redirect_to root_url
  end
end

```

```ruby:ability.rb
class Ability
  include CanCan::Ability

  def initialize(user)
    # ログイン/アウトはみんな許可
    can :manage, :session

    user ||= User.new

    if user.admin?
      can :manage, :all
    else
      can :read, :all
    end
  end
end
```

`authorize_resource`にオプションを追加して、
Abilityのほうには`can :manage, :session`を追加で設定。

### 再動作確認

![cap2.png](https://qiita-image-store.s3.amazonaws.com/0/335844/28acd4cd-28e2-68f3-7270-af116d87d752.png)

ログインページ見れました。
良かった〜。

そして`role=1`の状態なので、
`/posts/edit`にアクセスすると、indexにリダイレクトされました！

続いて`role=99`に調整後、
再度`/posts/edit`にアクセスすると、、、

![cap3.png](https://qiita-image-store.s3.amazonaws.com/0/335844/50561c1e-ff3c-c7a6-2f39-cddee5093da2.png)

表示できた！
ミッションコンプリート！

# 所感

試しとして２種類の`role`でやりましたが、
もっと多くの種類を取り扱っていくことになると、
設定・管理自体は結構大変そうだなと感じた。(小並感)

けどそこさえ乗り越えちゃえば、
わざわざアクセス制限の処理をガリガリと組まなくても良いし、
gem様様ですね。

2019/01/25 追記：
[続き書きました。](https://qiita.com/betti/items/203708284353b90fe193)

# おわりに

何かお気づきの点がありましたら、
ご指摘やアドバイス等頂けると大変助かります！