認証、認可はアプリケーションで重要な機構です。
RailsではDevise(認証)やCanCanCan(認可)などのGemが有名ですが、
シンプルな認可のGemのPunditを紹介します。
第64回 Ruby関西 勉強会の発表でサンプルコードを公開しました。
Pundit
コントローラーのアクション(メソッド)に対する認可のメカニズムが提供されます。
認可のポリシーはPolicy
クラスに許可の処理を自前で実装します。
Gem
gem 'pundit'
ApplicationController
app/controllers/application_controller.rb
にinclude Pundit
を追加します。
(この例ではcurrent_user
が認可の対象のユーザです。)
認可に失敗するとrescue_from
のuser_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.rb
にinclude 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_user
やbefore_action :authorize_user
のauthorize
メソッドで認可の許可を判断します。
(after_action :verify_authorized
はPundit
の処理の忘れを防止します。)
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
user
のroles
にgrant
メソッドのロールが含まれているとtrue
を返します。
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の 見える化 開発 で利用されています。
(開発を進めながら認可の確認ができて便利です。)
Draper
モデルにビューで利用するメソッドをデコレータで追加するメカニズムが提供されます。
(PunditとDraperのGemは組合せて利用できます。)
Gem
gem 'draper'
UsersController
app/controllers/users_controller.rb
のUser.find
に.decorate
を追加します。
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
メソッドを追加します。
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のモデル・ビュー・コントローラを小さく保ち
アプリケーションのデザインに良い効果が生まれます。