pundit でシンプルな認可機能アプリを作成

2021/10/03に公開約6,200字

目的

PunditはRailsにおいて認可システムに必要なヘルパーなどを提供するgemです。
punditよりシンプルな認可機能アプリを作成します

Qiita:Pundit + Railsで認可の仕組みをシンプルに作るより引用
Punditというgemを使ってRailsに認可の仕組みを作ってみます。
認可というとcancancanが有名です。
cancancanはユーザに対して、どんなアクションが許可するかを定義するのに対して、
Punditではリソースに対して誰が許可されるのかを定義します。反対からの目線ですね。
cancancanがコントローラ寄りならば、Punditはモデル寄りの責務です。また、
cancancanがDSLなのに対し、PunditはピュアRubyな書き方になっています。

学習アプリに実装する機能
管理ユーザーのみユーザー詳細(user_show)閲覧可能。
管理ユーザー以外の場合はエラーページを表示(403)。

執筆時の対象Version
Rails:5.2.6
devise:4.8
pundit:2.1.1

実装方法

セットアップ:devise導入

terminal
# 新規アプリを作成
rails new pundit_sample2021
cd pudit_sample2021
Gemfile
# Gemfileに追加する
+ gem 'devise'
terminal
# gemをインストール
bundle install
# deviseをセットアップ
rails g devise:install
# deviseでUserモデルをセットアップ
rails g devise user
db/migrate/****_create_users.rb
# frozen_string_literal: true

class DeviseCreateUsers < ActiveRecord::Migration[5.2]
  def change
    create_table :users do |t|
---省略---
+     # roleカラムを追記
+     t.integer :role, default: 0
      t.timestamps null: false
  end
---省略---
app/models/user.rb
class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable
+  # roleを設定
+  enum role: { general: 0, admin: 1 }
end
db/seeds.rb
+ # ユーザーデータを作成
+ # 管理者ユーザー
+ User.create!(
+   email: "admin@admin.com",
+   password: "password",
+   password_confirmation: "password",
+   role: 1
+ )

+ # 一般ユーザー
+ User.create!(
+   email: "general@general.com",
+   password: "password",
+   password_confirmation: "password",
+   role: 0
+ )
terminal
# DB作成
rails db:create
# マイグレーションを実行
rails db:migrate

セットアップ:User_view・controllerを設定

teminal
# devise導入時にはindexとshowが設定されていないため、作成する。
rails generate controller Users index show
app/controllers/users_controller.rb
class UsersController < ApplicationController
  def index
+   # Userの一覧を表示
+   @users = User.all
  end
 
  def show
+   # 個別のUserの情報を表示
+   @user = User.find(params[:id])
  end
end
app/views/users/index.html.erb
+ <h1>All users</h1>

+ <% if current_user.present? %>
+   <p>現在ログインしているユーザー:<%= current_user.email %></p>
+   <%= link_to 'Sign_out', destroy_user_session_path, method: :delete %>
+ <% else %>
+   <%= link_to 'Sign_in', new_user_session_path %>
+ <% end %>

+ <ul class="users">
+   <% @users.each do |user| %>
+     <li>
+       <%= link_to user.email, user %>
+     </li>
+   <% end %>
+ </ul>
app/views/users/show.html.erb
+ <h1>My page</h1>
+ <p><%= @user.email %></p>
+ <h1>My Role</h1>
+ <p><%= @user.role %></p>

+ <%= link_to 'Back', users_path %>
config/routes.rb
  Rails.application.routes.draw do
    devise_for :users
+   root 'users#index'
+   resources :users, :only => [:index, :show]
  end

Punditの導入

Gemfile
+ gem 'pundit'
terminal
# gemをインストール
bundle install
app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
+   # Punditを適用するcontrollerの継承元でincludeする。
+   include Pundit
end
# app/policies/配下にapplication_policy.rbというファイルが作成されます。
rails g pundit:install
app/policies/application_policy.rb
# frozen_string_literal: true

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
    def initialize(user, scope)
      @user = user
      @scope = scope
    end

    def resolve
      scope.all
    end

    private

    attr_reader :user, :scope
  end
end

PunditをUserに設定

app/policies/user_policy.rb
# userpolicy.rbを下記の内容にて新規に作成
# 基本的に全ての機能を許可。
# ただしユーザー詳細は管理ユーザーのみ許可
class UserPolicy < ApplicationPolicy
    def index?
      true
    end
  
    def show?
      # ユーザー詳細は管理ユーザーのみ許可
      user.admin?
    end
  
    def create?
      true
    end
  
    def new?
      create?
    end
  
    def update?
      true
    end
  
    def edit?
      update?
    end
  
    def destroy?
      true
    end
  end
app/controllers/users_controller.rb
class UsersController < ApplicationController
  def index
    @users = User.all
  end
 
  def show
    @user = User.find(params[:id])
+   # punditにてauthorizeメソッドにリソースオブジェクトを渡して認可状況を確認。
+    authorize @user
  end
end
terminal
# サーバー起動して、管理ユーザー(admin@admin.com)がuser_showを閲覧可能であることを確認
# 一般ユーザー(general@general.com)ではエラーになることを確認
rails s

403を設定

config/environments/development.rb
# 下記をfalseへ変更
# Show full error reports.
+  config.consider_all_requests_local = false
-  config.consider_all_requests_local = true
public/403.html
+ <!DOCTYPE html>
+ <html>
+ <head>
+   <title>権限がありません(401)</title>
+   <meta name="viewport" content="width=device-width,initial-scale=1">
+ </head>

+ <body>
+ <p>権限がありません。</p>
+ </body>
+ </html>
app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  include Pundit
+ rescue_from Pundit::NotAuthorizedError, with: :render_403

+ def render_403
+   render file: Rails.root.join('public/403.html'), status: :forbidden, layout: false, content_type: 'text/html'
+ end
end
terminal
# 一般ユーザー(general@general.com)では403ページへ遷移することを確認
rails s

参考サイト・資料

Github:Pundit_公式ドキュメント
Qiita:Pundit + Railsで認可の仕組みをシンプルに作る
Qiita:Punditをなるべくやさしく解説する
Qiita:Railsの認可Gem「Pundit」で、漏れのない認可設定を上手に作るTips
1434-193:Rails configのconsider_all_requests_localとは
Qiita:Railsで404エラーメッセージを出すために

Discussion

ログインするとコメントできます