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

# 目的
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:terminal
# 新規アプリを作成
rails new pundit_sample2021
cd pudit_sample2021
```
```diff ruby:Gemfile
# Gemfileに追加する
+ gem 'devise'
```
```terminal:terminal
# gemをインストール
bundle install
# deviseをセットアップ
rails g devise:install
# deviseでUserモデルをセットアップ
rails g devise user
```
```diff ruby: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
---省略---
```
```diff ruby: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
```
```diff ruby: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:terminal
# DB作成
rails db:create
# マイグレーションを実行
rails db:migrate
```
## セットアップ：User_view・controllerを設定
```terminal:teminal
# devise導入時にはindexとshowが設定されていないため、作成する。
rails generate controller Users index show
```
```diff ruby: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
```
    
```diff ruby: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>
```  
```diff ruby: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 %>
``` 
```diff ruby:config/routes.rb 
  Rails.application.routes.draw do
    devise_for :users
+   root 'users#index'
+   resources :users, :only => [:index, :show]
  end
```
## Punditの導入
```diff ruby:Gemfile
+ gem 'pundit'
```
```terminal:terminal
# gemをインストール
bundle install
``` 
```diff ruby:app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
+   # Punditを適用するcontrollerの継承元でincludeする。
+   include Pundit
end
```
```terminal
# app/policies/配下にapplication_policy.rbというファイルが作成されます。
rails g pundit:install
``` 
```ruby: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に設定
```ruby: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
```
```diff ruby: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:terminal
# サーバー起動して、管理ユーザー(admin@admin.com)がuser_showを閲覧可能であることを確認
# 一般ユーザー（general@general.com）ではエラーになることを確認
rails s
``` 
## 403を設定 
```diff ruby:config/environments/development.rb 
# 下記をfalseへ変更
# Show full error reports.
+  config.consider_all_requests_local = false
-  config.consider_all_requests_local = true
```
```diff html: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>
```
```diff ruby: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:terminal
# 一般ユーザー（general@general.com）では403ページへ遷移することを確認
rails s
```
# 参考サイト・資料
[Github:Pundit_公式ドキュメント](https://github.com/varvet/pundit)
[Qiita:Pundit + Railsで認可の仕組みをシンプルに作る](https://qiita.com/zaru/items/8bf7b41b33f3f55bd27d)
[Qiita:Punditをなるべくやさしく解説する](https://qiita.com/yutaro50/items/52484b7ae4ca87aa99a2)
[Qiita:Railsの認可Gem「Pundit」で、漏れのない認可設定を上手に作るTips](https://qiita.com/Coolucky/items/9dfdebf920c2b31f13c9)
[1434-193:Rails configのconsider_all_requests_localとは](https://zashii.hatenablog.com/entry/2017/05/12/131034)
[Qiita:Railsで404エラーメッセージを出すために](https://qiita.com/suzuki-koya/items/b2a7039b08917e2171df)