権限管理のgemをbankenに乗り換えてみた

この記事をみて、さっそくbankenの乗り換えてみた。

- [権限管理のgemで良いのはどれ?](http://blog.willnet.in/entry/2016/01/20/100606)

## 準備
bankenのREADME.mdにある通りGemfileに追記して、

```rb:Gemfile
gem 'banken'
```

gemとApplicationLoyaltyをインストールをする。

```
$ bundle install
$ bundle exec rails g banken:install
```

## 乗り換えるにあたっての手抜き
乗り換えるにあたり、なるべくコード量を減らしたかったので、手抜きのためのコードを書いた。

### `ApplicationLoyalty#allow_all?`

「この権限ならこのコントローラー全部アクセスできるでしょ！」っていうのがあると思うので、`ApplicationLoyalty`に`allow_all?`というメソッドを追加して、各メソッドはその値を参照するようにした。

```rb:app/loyalties/application_loyalty.rb
class ApplicationLoyalty
  attr_reader :user, :record

  def initialize(user, record)
    @user = user
    @record = record
  end

  def index?
    allow_all?
  end

  def show?
    allow_all?
  end

  def create?
    allow_all?
  end

  def new?
    create?
  end

  def update?
    allow_all?
  end

  def edit?
    update?
  end

  def destroy?
    allow_all?
  end

  private

  def allow_all?
    false
  end
end
```

使うときはこんな感じ。「この権限なら」の権限毎に`allow_all?`を定義したモジュールを用意してインクルードしてる。

```rb
# app/loyalties/concerns/allow_no_signed_in_loyal.rb
module AllowNotSignedInLoyal
  def allow_all?
    true
  end
end

# app/loyalties/concerns/allow_singed_in_loyal.rb
module AllowSignedInLoyal
  def allow_all?
    user.present?
  end
end

# app/loyalties/welcome_loyalty.rb
class WelcomeLoyalty < ApplicationLoyalty
  include AllowNotSignedInLoyal
end

# app/loyalties/posts_loyalty.rb
class PostsLoyalty < ApplicationLoyalty
  include AllowSignedInLoyal

  # 条件個別に定義することもできる。
  def index?
    true
  end
end
```
「ログインしていればOK」「管理者権限があればOK」とかそういう大味な権限管理の場合に使う。

### アクションのメソッドを定義するのをやめる

- いちいちアクションを定義するのがだるい[^1]
- 水際で食い止めるのは`allow_all?`でいいじゃん


[^1]: アクセス制御にあるまじき発想だけど、、、許して、、

という気持ちがあったので、`method_missing`で`Banken#banken_query_name`（実際は`?`で終わるメソッド）に自動で応答するようにした。

```rb:app/loyalties/concerns/dynamic_loyal.rb
module DynamicLoyal
  def method_missing(method_name, *argments, &block)
    if method_name.to_s.end_with?('?')
      allow_all?
    else
      super
    end
  end

  def respond_to_missing?(method_name, include_private = false)
    method_name.to_s.end_with?('?') || super
  end
end
```

やんちゃなので`ApplicationLoyalty`でincludeしちゃう。

```rb:app/loyalties/application_loyalty.rb
class ApplicationLoyalty
  include DynamicLoyal

  # snip ...
end
```

### 権限は専用のオブジェクトにまとめて移譲する。

bankenは「権限とコントローラー、ビューの紐付けをするgem」で「権限自体を定義するgemではない」って認識なので、`HogeLoyalty`に具体的な権限の定義を書くのはやめて`Ability`のようなクラスにまとめて定義した。[^2]

[^2]: `Ability`の実装には[cancancan](https://github.com/CanCanCommunity/cancancan)や[six](https://github.com/randx/six)(gitlabの権限管理gem)のような権限を定義するgemが使える。

```rb:app/models/ability.rb
class Ability
  attr_reader :user

  def initialize(user)
    @user = user
  end

  def not_singed_in?
    true
  end

  def signed_in?
    user.present?
  end

  def admin?
    user.try(:admin?)
  end

  def update_post?(post)
    admin? || post.unpublished?
  end
end
```

確認は`Ability`に委譲する。

```rb
class ApplicationLoyalty
  def ability
    @ability ||= Ability(user)
  end
end

module AllowSignedInLoyal
  def allow_all?
    ability.signed_in?
  end
end

class PostsLoyalty < ApplicationLoyalty
  include AllowSignedInLoyal

  def index?
    ability.not_singed_in?
  end

  def update?
    ability.update_post?(record)
  end
end
```

### テストをかく

愚直に書くと`controller_name`と`action_name`が分離してしまってわかりにくいので

```rb
describe 'loyalty' do
  context 'user' do
    let(:user) { create(:user) }
    it { expect(Banken.loyalty!(:posts, user, create(:post))).to be_update }
  end
end
```

ヘルパークラス書いた。

```rb
# spec/support/banken_permission.rb
class BankenPermission
  attr_reader :user

  def initialize(user)
    @user = user
  end

  def allow?(controller_name, action_name, record = nil)
    Banken.loyalty!(controller_name, user, record).public_send("#{action_name}?")
  end
end

describe 'loyalty' do
  context 'user' do
    subject { BankenPermission.new(create(:user)) }
    it { should be_allow(:posts, :update, create(:post)) }
  end
end
```

## 乗り換えてみて

乗り換え前は[#386 Authorization from Scratch Part 2](http://railscasts.com/episodes/386-Authorization-from-scratch-part-2)の[コード](https://github.com/railscasts/386-authorization-from-scratch-part-2)を使っていた。これと比べると、

Pros.

- 権限周りの修正の影響範囲が狭くなった。
  - コントローラーごとに別クラス、別ファイルなので影響しにくい
  - コンフリクトに強そう。diffが見やすそう。
- 定義順に依存とかそういうのを気にする必要が無くなった。

Cons.

- コントローラーの数だけ`Loyalty`を用意するし記述が冗長。
  - 冗長だけど、3行で済むLoyaltyも多いので何とかなりそう。