Authorization of Pundit

  • 33
    いいね
  • 0
    コメント

認証、認可はアプリケーションで重要な機構です。
RailsではDevise(認証)やCanCanCan(認可)などのGemが有名ですが、
シンプルな認可のGemのPunditを紹介します。

第64回 Ruby関西 勉強会の発表でサンプルコードを公開しました。

Pundit

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

Gem

gem 'pundit'

ApplicationController

app/controllers/application_controller.rbinclude Punditを追加します。
(この例ではcurrent_userが認可の対象のユーザです。)

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

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のGemで実装します。
app/policies/application_policy.rbinclude GrantFrontを追加します。

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メソッドに追加します。

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_userbefore_action :authorize_userauthorizeメソッドで認可の許可を判断します。
after_action :verify_authorizedPunditの処理の忘れを防止します。)

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

gem 'grant-front'

Grant

userrolesgrantメソッドのロールが含まれているとtrueを返します。

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

Drawing

GrantFrontはDomain-Driven DesignDesign-Driven Developmentではない DDD
Drawing-Driven Development見える化 開発 で利用されています。

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

example

Draper

モデルにビューで利用するメソッドをデコレータで追加するメカニズムが提供されます。
(PunditとDraperのGemは組合せて利用できます。)

Gem

gem 'draper'

UsersController

app/controllers/users_controller.rbUser.find.decorateを追加します。

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

UserDecorator

app/decorators/user_decorator.rbupdatedestroyの許可があればボタンを表示する
link_to_editlink_to_destroyメソッドを追加します。

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を追加すると
ポリシーの認可とボタンの表示、非表示が連携します。

<%- 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のモデル・ビュー・コントローラを小さく保ち
アプリケーションのデザインに良い効果が生まれます。

この投稿は Ruby on Rails Advent Calendar 201410日目の記事です。