cancancan で API ベースの制御を行う方法

# 概要
Web システムを構築するにあたり、「ユーザーごとに権限管理を行いたい」という要望は比較的多いと思います。

Ruby on Rails では [cancancan](https://github.com/CanCanCommunity/cancancan) という Gem を使って権限管理を行うことができますが、この Gem は Model への実行権限（read/write）を管理するのが基本的な使い方になっています。


今回、「API の実行権限を管理したい」という要件があり、その方法を調べたのでまとめました。

# cancancan の一般的な使い方（モデルベースの制御）
本題に入る前に、cancancan の一般的な使い方（モデルベースの制御）を簡単に紹介します。
（ほぼ Gem の Readme の日本語訳です）

## Gem のインストール
Gemfile に下記を追加します。

```
gem 'cancancan'
```

## Ability の定義
ユーザーに付与する権限は `Ability` クラスで定義します。
まず、下記コマンドで `Ability` クラスを作成します。

```
rails g cancan:ability
```

例として、モデル Post の実行権限を制御します。

```
class Ability
  include CanCan::Ability

  def initialize(user)
    can :read, Post, public: true  # public=true のレコードは誰でも参照可

    if user.present?  # ログインユーザーの権限を追加で定義
      can :read, Post, user_id: user.id  # user_id が自分自身のレコードも参照可

      if user.admin?  # admin ユーザーの権限を追加で定義
        can :read, Post  # すべてのレコードを参照可
      end
    end
  end
end
```
cancancan は wiki が充実しており、詳細な定義方法もそちらにまとまっています。
[Defining Abilities - cancancan
](https://github.com/CanCanCommunity/cancancan/wiki/defining-abilities)

## 権限の確認
### ビューで権限を確認する場合
```
<% if can? :read, @post %>
  <%= link_to "View", @post %>
<% end %>
```
`can? :read, @post` で、ユーザーに変数 `@post` の参照権限があるかを確認できます。
上記例の場合は、`@post` の参照権限がある場合のみリンクを表示するようにしています。
ビューで使用できる helper の詳細については、wiki の下記ページを参照してください。
[Checking Abilities - cancancan
](https://github.com/CanCanCommunity/cancancan/wiki/checking-abilities)

### コントローラーで権限を確認する場合
前提として、メソッド `current_user` でログインユーザーを参照できる必要があります。
[Devise](https://github.com/heartcombo/devise) や [Authlogic](https://github.com/binarylogic/authlogic) といった認証用の Gem を事前に導入してください。

```
def show
  @post = Post.find(params[:id])
  authorize! :read, @post  # current_user が @post を参照不可の場合はエラー
end
```
`load_and_authorize_resource` を記述すると、コントローラー名に応じたリソースの読み込みと権限の確認を行う `before_action` が追加されます。
`load_and_authorize_resource` を使えば、「後から追加したアクションに `authorize!` を書き忘れて権限チェックから漏れていた」という事態を防ぐことが出来ます。

```
class PostsController < ApplicationController
  load_and_authorize_resource

  def show  # GET /posts/:post_id にアクセスした際に呼び出される
    # before_action で以下を実行
    # @post = Post.find(params[:post_id])
    # authorize! :show, @post
  end
end
```

#### 補足事項
cancancan では内部的に以下のエイリアスが切られています。

```
alias_action :index, :show, :to => :read
alias_action :new, :to => :create
alias_action :edit, :to => :update
```
このため、たとえば `authorize! :show, @post` と `authorize! :read, @post` では同じ結果が得られます。
cf. [Action Aliases - cancancan
](https://github.com/CanCanCommunity/cancancan/wiki/Action-Aliases)


`load_and_authorize_resource` は `load_resource` と `authorize_resource` の 2 つに分けることができます。
上記例だと、`load_resource` で `@post = Post.find(params[:post_id])` の部分の `before_action` が、`authorize_resource` で `authorize! :show, @post` の部分の `before_action` が追加されます。
cf. [Authorizing controller actions - cancancan](https://github.com/CanCanCommunity/cancancan/wiki/Authorizing-controller-actions)

# API ベースの制御
今回の本題です。
例として、`Post` コントローラーに `show` と `update` の2つのアクションを定義している場合を考えます。

```
class PostsController < ApplicationController

  def show  # GET /posts/:post_id で呼び出す API
  end

  def update  # PUT /posts/:post_id で呼び出す API
  end
end
```
admin ユーザーは `GET /posts/:post_id` と `PUT /posts/:post_id` の両方を、それ以外のユーザーは `GET /posts/:post_id` だけを実行できるようにします。

## Ability の定義
```
class Ability
  include CanCan::Ability

  def initialize(user)
    can :show, :post  # GET /posts/:post_id は誰でも実行可

    if user.admin?  # admin ユーザーの権限を追加で定義
      can :update, :post  # PUT /posts/:post_id は admin ユーザーだけが実行可
    end
  end
end
```
## 権限の確認
API の実行を制御するのが目的なので、ビューではなくコントローラーで確認します。

```
class PostsController < ApplicationController

  authorize_resource class: false

  def show  # GET /posts/:post_id で実行する API
    # before_action で以下を実行
    # authorize! :show, :post
  end

  def update  # PUT /posts/:post_id で実行する API
    # before_action で以下を実行
    # authorize! :update, :post
  end
end
```

`load_and_authorize_resource` ではなく `authorize_resource class: false` を使うのが鍵になります。

### `authorize_resource class: false` を使う理由
例として `PostsController` の `show` の場合で考えると、`authorize_resource` は以下のようなロジックで動作する `before_action` を追加するようです。
（ソースをきちんと追ったわけではないので、厳密には違うかもしれません）

```
if @post にリソースが代入されている？
  authorize! :show, @post
elsif 'class: false' が指定されていない？
  authorize! :show, Post
else
  authorize! :show, :post
end
```

`load_and_authorize_resource` を使用すると `@post` にリソースが入ってしまうため、`@post` の権限チェックが走ってしまいます。
また、`class: false` を指定しないとモデル Post の権限チェックが走ってしまいます。
以上が `authorize_resource class: false` を使う理由です。

# おわりに
API の実行権限を管理する方法をネットで調べてみましたが、以外にもなかなか見つからなかったので今回まとめてみました。
もし間違い等ありましたらお気軽にコメントください。


マイクロサービスの流行に伴い、こういった要件が増えていくかと思います。
もし同じような状況に陥った方がいたら、参考にしていただければ幸いです。