Punditで権限管理
ユーザーによってページの表示を許可、拒否したり、アクションを制限したりしたいことがあると思います。
今回は認可の仕組みを提供してくれるgem Pundit
を使って実装してみたいと思います。
単純なuserのCRUD機能とログイン機能を持ったアプリを作りました。
userはadmin(管理者)とgeneral(一般ユーザー)の2種類います。
adminがログインした時には制限はありませんが、generalがログインした時はユーザーの編集、削除ができないようにしてみます。
adminであるtanakaでログインすると
こんな感じです。
generalのyamadaでログインしても今は同じ画面になります。
Punditを導入してgeneralができることを制限してみましょう。
Punditの導入
Gemfileに
gem 'pundit'
としてbundle install
して、コントローラでincludeします。
class ApplicationController < ActionController::Base include Pundit end
下記のコマンドで
rails g pundit:install
app/policies/配下にapplication_policy.rbが作成されます。
class ApplicationPolicy attr_reader :user, :record def initialize(user, record) @user = user @record = record end def index? false end def show? false end def create? false end def new? create? end def update? false end def edit? update? end def destroy? false end class Scope attr_reader :user, :scope def initialize(user, scope) @user = user @scope = scope end def resolve scope.all end end end
Policyの作成
userに関するpolicy(方針)を記述するファイルuser_policy.rb
を作成し、
class UserPolicy < ApplicationPolicy def index? true end def edit? user.admin? end end
ApplicationPolicyを継承させ、アクションを定義します。
admin?
はenumで
class User < ApplicationRecord enum role: { general: 0, admin: 1 } end
このように定義して使えるようになったメソッドです。
def アクション名? end
で認可ルールを記述します。
def index? true end def edit? user.admin? end
index?
はtrueが返るので制限はかかりません。
edit?
にはuserがadminの場合に許可しtrueが返ります。
user
にはデフォルトでcurrent_user
が入るようになっているみたいです。
adminでなければPundit::NotAuthorizedError
が発生します。
コントローラでPunditを呼び出し
コントローラ側で
class UsersController < ApplicationController before_action :set_user, only: %i[ show edit update destroy ] def index authorize(User) @users = User.all end def edit authorize(@user) # 編集は特定のユーザー情報が必要なので「@user」で特定のユーザーを引数に指定 end private def set_user @user = User.find(params[:id]) end end
authorizeメソッド
を記述することで該当するPolicy(この場合UserPolicy)を確認してくれます。
indexアクションからはindex?
アクションを確認し、index?
アクションはtrueを返すのでadminでもgeneralでもユーザー一覧が表示されます。
editアクションからはedit?
アクションを確認し、edit?
アクションはuser.admin?
となっているのでadminの場合だけtrueが返り、編集ページへアクセスできます。
Viewで表示を制限
View側でもpolicyメソッド
を使って表示自体を制限できます。
<% @users.each do |user| %> <tr> <td><%= user.name %></td> <td><%= user.role %></td> <td><%= link_to 'Show', user %></td> <% if policy(user).edit? %> <td><%= link_to 'Edit', edit_user_path(user) %></td> <% end %> <% if policy(user).destroy? %> <td><%= link_to 'Destroy', user, method: :delete, data: { confirm: 'Are you sure?' } %></td> <% end %> </tr> <% end %>
このようにすると「Edit」「Destroy」のリンク自体、adminでログインしている場合でないと表示されなくなります。
もしusers/1/edit
のように直接URLからアクセスしようとしても
def edit? user.admin? end
これにより弾くことができます。
Pundit::NotAuthorizedError
権限がないgeneralで編集ページにアクセスしようとするとPundit::NotAuthorizedError
が発生します。
エラーページを用意してそちらを表示させることができます。
public配下に403.html
ファイルを作成します。
<!DOCTYPE html> <html> <head> <title>権限がありません</title> <meta name="viewport" content="width=device-width,initial-scale=1"> </head> <body> <p>権限がありません。</p> </body> </html>
そしてapplication_controller.rb
を
class ApplicationController < ActionController::Base include Pundit rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized # これと private def user_not_authorized #これを追加 render file: 'public/403.html', status: :forbidden, layout: false end end
このようにします。
わざとエラーを発生させると
エラーページに遷移しました。
また、config/application.rbにconfig.action_dispatch.rescue_responses["Pundit::NotAuthorizedError"] = :forbidden
の記述をし、
config/environments/development.rbのconfig.consider_all_requests_local
をtrueからfalseに変更してサーバーを再起動することでもエラーページに遷移できました。
最後に
動作確認しながら書きましたが、最低限だけ書こうとして端折ったところもあるのでもしかしたら矛盾があるかもしれません。
あったらごめんなさい。
今回は以上です。ありがとうございました。
参考サイト
GitHub - varvet/pundit: Minimal authorization through OO design and pure Ruby classes