Authorization of Pundit

認証、認可はアプリケーションで重要な機構です。
Railsでは[Devise](https://github.com/plataformatec/devise)(認証)や[CanCanCan](https://github.com/CanCanCommunity/cancancan)(認可)などのGemが有名ですが、
シンプルな認可のGemの[Pundit](https://github.com/elabs/pundit)を紹介します。

[第64回 Ruby関西 勉強会](https://speakerdeck.com/ogom/ddd-with-rails)の発表で[サンプルコード](https://github.com/dddrb/rails-ddd)を公開しました。

## Pundit

コントローラーのアクション（メソッド）に対する認可のメカニズムが提供されます。
認可のポリシーは`Policy`クラスに許可の処理を自前で実装します。

### Gem

```ruby
gem 'pundit'
```

### ApplicationController

`app/controllers/application_controller.rb`に`include Pundit`を追加します。
（この例では`current_user`が認可の対象のユーザです。）

認可に失敗すると`rescue_from`の`user_not_authorized`メソッドでリダイレクトされます。

```ruby
class ApplicationController < ActionController::Base
  include Pundit
  protect_from_forgery with: :exception
  helper_method :current_user
  before_action :authenticate

  rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized

  def current_user
    @current_user ||= User.find(session[:user_id]) if session[:user_id].present?
  end

  private
    def authenticate
      if current_user.blank?
        session[:user_redirect_url] = request.fullpath if request.get?
        user_not_authenticated
      end
    end

    def user_not_authenticated
      session.delete :user_id
      redirect_to user_sign_in_path
    end

    def user_not_authorized
      flash[:error] = "You are not authorized to perform this action."
      redirect_to(request.referrer || root_path)
    end
end
```

### ApplicationPolicy

許可の処理は、手前みそですが[GrantFront](https://github.com/ogom/grant-front)のGemで実装します。
`app/policies/application_policy.rb`に`include GrantFront`を追加します。

```ruby
class ApplicationPolicy
  include GrantFront
  attr_reader :user, :record

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

  def index?
    false
  end

  def show?
    index?
  end

  def create?
    false
  end

  def new?
    create?
  end

  def update?
    false
  end

  def edit?
    update?
  end

  def destroy?
    false
  end
end
```

### UserPolicy

`app/policies/user_policy.rb`にアクションを許可するロールを`grant`メソッドに追加します。

```ruby
class UserPolicy < ApplicationPolicy
  def index?
    grant :foo, :bar, :baz
  end

  def create?
    grant :foo, :bar
  end

  def update?
    grant :foo, :bar
  end

  def destroy?
    grant :foo
  end
end
```

### UsersController

`before_action :set_user`や`before_action :authorize_user`の`authorize`メソッドで認可の許可を判断します。
（`after_action :verify_authorized`は`Pundit`の処理の忘れを防止します。）

```ruby
class UsersController < ApplicationController
  before_action :set_user, only: [:show, :edit, :update, :destroy]
  before_action :authorize_user, only: [:index, :new, :create]
  after_action :verify_authorized

  def index
    @users = User.all
  end

  def show
  end

  def new
    @user = User.new
  end

  def edit
  end

  def create
    @user = User.new(user_params)
    @user.roles = @user.roles.split(',').map {|role| role.strip.to_sym}
    if @user.save
      redirect_to @user, notice: 'User was successfully created.'
    else
      render :new
    end
  end

  def update
    user = user_params
    user['roles'] = user['roles'].split(',').map {|role| role.strip.to_sym}
    if @user.update(user)
      redirect_to @user, notice: 'User was successfully updated.'
    else
      render :edit
    end
  end

  def destroy
    @user.destroy
    redirect_to users_url, notice: 'User was successfully destroyed.'
  end

  private
    def set_user
      @user = User.find(params[:id])
      authorize @user
    end

    def authorize_user
      authorize User
    end

    def user_params
      params.require(:user).permit(:name, :email, :username, :roles)
    end
end
```

## GrantFront

`Policy`クラスに許可のメカニズムが提供されます。

### Gem

```ruby
gem 'grant-front'
```

### Grant

`user`の`roles`に`grant`メソッドのロールが含まれていると`true`を返します。

```ruby
  private
    def grant(*roles)
      roles.each do |role|
        return true if user.roles.include? role
      end
      return false
    end
```

### Drawing

GrantFrontは`Domain-Driven Design`や`Design-Driven Development`ではない **DDD** の
[Drawing-Driven Development](https://speakerdeck.com/ogom/draw-with-rails)の **見える化 開発** で利用されています。

（開発を進めながら認可の確認ができて便利です。）

![example](http://ogom.github.io/grant-front/assets/img/example.png)

## Draper

モデルにビューで利用するメソッドをデコレータで追加するメカニズムが提供されます。
（Punditと[Draper](https://github.com/drapergem/draper)のGemは組合せて利用できます。）

### Gem

```ruby
gem 'draper'
```

### UsersController

`app/controllers/users_controller.rb`の`User.find`に`.decorate`を追加します。

```ruby
  private
    def set_user
      @user = User.find(params[:id]).decorate
      authorize @user
    end
```

### UserDecorator

`app/decorators/user_decorator.rb`に`update`と`destroy`の許可があればボタンを表示する
`link_to_edit`と`link_to_destroy`メソッドを追加します。

```ruby
class UserDecorator < Draper::Decorator
  delegate_all

  def link_to_edit
    if h.policy(object).update?
      h.link_to h.t('.edit', default: h.t("helpers.links.edit")),
                h.edit_user_path(object), class: 'btn btn-primary'
    end
  end

  def link_to_destroy
    if h.policy(object).destroy?
      h.link_to h.t('.destroy', default: h.t("helpers.links.destroy")),
                h.user_path(object), method: 'delete', class: 'btn btn-danger',
                data: { confirm: h.t('.confirm',
                        default: h.t("helpers.links.confirm", default: 'Are you sure?')) }
    end
  end
end
```

### View

`app/views/users/show.html.erb`に`@user.link_to_edit`と`@user.link_to_destroy`を追加すると
ポリシーの認可とボタンの表示、非表示が連携します。

```html
<%- model_class = User -%>
<%- content_for :sub_title do -%>
  <%=t '.title', :default => model_class.model_name.human %>
<%- end -%>

<dl class="dl-horizontal">
  <dt><strong><%= model_class.human_attribute_name(:state) %>:</strong></dt>
  <dd><%= @user.state %></dd>
  <dt><strong><%= model_class.human_attribute_name(:name) %>:</strong></dt>
  <dd><%= @user.name %></dd>
</dl>

<div class="form-actions">
  <%= @user.link_to_edit %>
  <%= @user.link_to_destroy %>
</div>
```

認可の処理だけでなくポリシーとメカニズムの分離は、Railsのモデル・ビュー・コントローラを小さく保ち
アプリケーションのデザインに良い効果が生まれます。