【Gem】Banken
はじめに
こんにちは!大ちゃんの駆け出し技術ブログです。
2ヶ月ほど前にPunditの記事を書きましたが、今回はそれとよく似たgemのBankenについて書いていきたいと思います。
こちらのgemを作ってくださったのは日本人の方で英語のドキュメントだけでなく日本語のドキュメントも用意されています。
- 日本語のドキュメント
日本人が作った日本語ドキュメントなので本当にわかりやすく導入することができました。
まずはインストール方法を先に紹介します。ただ、これは日本語ドキュメントがありますので、日本語ドキュメントを参照していただいた方が早いと思ってます。
インストール方法
上記ドキュメントに従ってインストールします。
Gemfile
に追記しbundle install
# Gemfile gem "banken" # ターミナル $ bundle Fetching banken 1.0.3
- アプリケーションコントローラでBankenを
include
# app/controllers/application_controller.rb class ApplicationController < ActionController::Base include Banken
- ターミナル上でコマンドを使用してインストール
application_loyalty.rb
というファイルが作成されます。
$ rails g banken:install
create app/loyalties/application_loyalty.rb
作成されたファイル
# app/loyalties/application_loyalty.rb class ApplicationLoyalty 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 end
ここまでで基本インストールはおしまいです。
要件
今回自分がPFに使用した例を紹介します。
自分のPFではログイン後ユーザーがプロフィールを作成していない場合、まずはプロフィール新規作成画面に遷移します。ここでユーザーはプロフィール作成が終わるまで、プロフィール新規作成画からはどこのページにも遷移できないように制御したいです。
また、新規作成後はプロフィール新規作成画面に戻る事ができない仕様にしたいです。ユーザーとプロフィールのアソシエーションはhas_oneの関係(1 : 1)なので、ユーザーが1つのプロフィールを既に作成していれば、新規作成画面に遷移する必要がないからです。
# app/models/profile.rb class Profile < ApplicationRecord belongs_to :user # app/models/user.rb class User < ApplicationRecord has_one :profile, dependent: :destroy
以下2つの項目を制御できるようにBankenを使用してみます。
- 基本プロフィール未作成時にプロフィール一覧に遷移できない
- プロフィール作成後はプロフィール作成画面に遷移できない
使用方法
- Loyaltyクラスの作成
制御したいアクションがあるコントローラの名前と同名のLoyaltyクラスを作成します。以下公式ドキュメントを引用。
公式ドキュメントの例
Bankenではcontrollerに一対一で紐づく同名のloyaltyクラスを作成していきます。 今回はPostsControllerに対して制御を行うのでPostsLoyaltyクラスを作成する必要があります。
> rails g banken:loyalty posts
create app/loyalties/posts_loyalty.rb
今回はProfilesControllerの新規作成画面での遷移(newアクション)と一覧画面への遷移(indexアクション)に対して認可制御を行います。
rails g banken:loyalty profiles
create app/loyalties/profiles_loyalty.rb
新しくProfilesLoyaltyクラスが作成されました。しかし、中身はクラスが定義されているだけでメソッド等のロジックは記載されていません。
# app/loyalties/profiles_loyalty.rb class ProfilesLoyalty < ApplicationLoyalty end
基本プロフィール未作成時にプロフィール一覧に遷移できない
まずは新規作成画面からどこの画面にも遷移できないように制御します。
- プロフィール一覧画面を表示させる
index
アクションに対してauthorize!
メソッドを設置
※ 今回はAPI化を行っているため、特にこちらのコントローラに記載はしておりません。
# app/controllers/profiles_controller.rb class ProfilesController < ApplicationController def index authorize! end
authorize!
メソッドについてですが、実行されたアクション名に紐づいたLoyaltyクラス内のアクション名?
メソッドを実行します。今回はProfilesController
のindex
アクションの中でauthorize!
メソッドが実行されているので、コントローラに紐づいているProfilesLoyalty
のindex?
メソッドが実行されます。
ProfilesLoyalty
のindex?
に対してuser.profile.present?
を記述
index?
メソッドがまだ定義されていませんので定義します。?とあるようにこのLoyalty
クラス内のメソッドは真偽値を返さないといけません。そこで今回は要件に従うようににcurrent_user
がプロフィールを所持しているかどうかに対してboolean
値を返すようにします。
# app/loyalties/profiles_loyalty.rb class ProfilesLoyalty < ApplicationLoyalty def index? user.profile.present? end end
このメソッドの中にあるuser
はどこからくるの?と思うかもしれませんが、実はこのuser
はcurrent_user
と同じです。Bankenの便利なところで上記のauthorize!
メソッドは下記のことと同じことを実行します。
raise "not authorized" unless ProfilesLoyalty.new(current_user).index?
勝手に引数にcurrent_user
を渡してくれていますね。なのでauthorize!
メソッドで引数を渡さずとも、current_user
は勝手に渡されており、それがindex?
アクションの中でuser
として扱われています。
※ 上記のようにcurrent_user
を必ず使用するため、current_user
が使用できないとエラーが発生します。よって、必ずcurrent_user
を使えるようにしておく必要があります。自分の場合、deviseを使用してcurrent_user
を使えるようにしております。
application_controller
に例外処理を記載
上記のuser.profile.present?
でfalseが返ると例外が発生します。例外をハンドリングするためにApplicationController
にエラーハンドリングのロジックを書きます。
# app/controllers/application_controller.rb class ApplicationController < ActionController::Base include Banken # エラーハンドリング ============= rescue_from Banken::NotAuthorizedError, with: :user_not_authorized private def user_not_authorized(exception) redirect_to(request.referer || root_path) end
request.referer
は遷移する前の元の画面のURLを取得します。プロフィール新規作成画面から遷移しようとしBankenで例外が生じると、プロフィール新規作成画面に戻るということになります。そして元の画面のURLがnilの場合はroot_path
に遷移するようにします。
ここで問題なのが、request.referer
はボタンやリンクなどから元の画面のURLを取得するのですが、直接ブラウザにURLを打ち込まれて画面遷移した場合、request.referer
の値はnilになってしまいます。よって、新規作成画面にいる状態でブラウザに/profiles
を打ち込むと、root_path
にリダイレクトされていしまいます。しかし今回の実装の場合、プロフィール新規作成が終了するまでは他の画面に遷移する事はできないようにしたいので、root_path
にも遷移したくありません。
なのでroot_path
のアクションに対して直接ロジックを書きます。
def index if user_signed_in? && current_user.profile.nil? redirect_to new_profile_path end end
ユーザーがuser_signed_in?
していて尚且つプロフィールを持っていないのであれば、新規作成画面にリダイレクトします。少し冗長ですがこれで第一の要件を満たす事はできました。
プロフィール作成後はプロフィール作成画面に遷移できない
次の要件を満たすために先ほどと同じようにauthorize!
メソッドを使用します。今回はnew
アクションですね。
# app/controllers/profiles_controller.rb class ProfilesController < ApplicationController def index authorize! end def new authorize! end
new
アクションに対応するnew?
メソッドをProfilesLoyalty
クラスに定義します。今回は新規作成済みの場合の制御となるのでuser.profile.present?
とは逆のロジックを書けばいいと思います。
# app/loyalties/profiles_loyalty.rb class ProfilesLoyalty < ApplicationLoyalty def index? user.profile.present? end def new? user.profile.nil? end end
これで実装はできているのですが、先ほどと同じように直接ブラウザ上で新規作成画面のURLを打ち込むと、request.referer
がnilになりますので、このままだとトップページにリダイレクトします。
それを制御するためには少々手荒な実装ですが、下記のように記述して解決しました。
def index if user_signed_in? && current_user.profile.nil? redirect_to new_profile_path elsif user_signed_in? redirect_to profiles_path end end
user_signed_in?
をさらに下に書く事で実装できました。これによりプロフィールは持っているけども、サインインしている状態のユーザーがプロフィール一覧画面にリダイレクトします。
※ user_signed_in? && current_user.profile.nil?
の制御を必ず最初の条件に持っていかないとuser_signed_in?
の制御が先に行われます。そのため、新規作成画面からトップページにリダイレクトした時にプロフィール一覧画面に再度また遷移し、そこでまた認可制御されるという無限ループになり画面が白くなります。
これで実装は完了です!
Punditとの違い
BankenはPunditの影響を強く受けているgemです。gemの英語ページでも下記のように説明されています。
Simple and lightweight authorization library for Rails inspired by Pundit
今回Bankenを使用し、また過去にPunditを使用したので2つのgemの違いがざっくりと見えましたので共有します。
コードの記述方法の違いを見ればわかりやすいかと思います。今回のロジックをPunditで行うと以下のようになります。以下はプロフィール一覧画面への制御です。
Punditの場合
def index authorize Profile end
Bankenの場合
def index authorize! end
違いとしては下記2点です。
Bankenと違う点
- authorize!ではなくauthorize(!マークがない)
- 引数がない場合クラスを引数にする(Profile)
1に関しては書き方の違いなので大した問題ではないのですが、2は大きな違いだと思います。Bankenの場合引数がなくても引数なしにauthorize!
メソッドを使用できますが、Punditの場合、対応するモデルのクラス名を記述しないといけません。
こちらの理由ですが、これはPunditとBankenの紐づいている対象がモデルかコントローラかという違いになります。
Punditはコントローラ、モデル、そしてPolicyクラス(BankenのLoyalityクラスと同じ認可のメソッドが定義されるファイル)が1 : 1 : 1で結びついています。そして、authorize
メソッドは引数のインスタンス変数(@profile
など)からその変数のモデルクラスがあると推測し、そのモデルクラスに対応するPolicyクラスがあると判断します。ここがPunditとBankenの大きな違いです。
Punditの場合の認可のフローは下記のようなものです。まさにコントローラ、モデル、そしてPolicyクラスそれぞれが結びついていることがわかると思います。3つのファイルがあって初めて機能します。
def index # indexアクションを実行 authorize @profile # 変数名からProfileクラスがあると推測 end ↓ class Profile # Profileクラスを確認 end ↓ class ProfilePolicy < ApplicationPolicy # モデルのクラスに対応するPolicyを確認 def index? # コントローラで実行されたのはindexアクションなのでindex?メソッドを実行 user.profile.present? end end
そのため、authorize
メソッドで何も引数がない場合、Profileクラスがあると推測することができません。よって、モデルのクラス名を渡してあげることで、Punditにクラスがあると推測させる必要があるわけです。
def index # indexアクションを実行 authorize Profile # クラス名からProfileクラスがあると推測 end
逆にBankenだとコントローラのみでしたね。モデルの有無は関係ありません。
class ProfilesController # コントローラ名からProfilesLoyaltyがあると推測 def index # indexアクションを実行 authorize! end ↓ class ProfilesLoyalty < ApplicationLoyalty # コントローラに対応するProfilesLoyaltyを確認 def index? # コントローラで実行されたのはindexアクションなのでindex?メソッドを実行 user.profile.present? end end
もっと詳しく知りたいという方は下記のドキュメントを参照してください。
The difference between Banken and Pundit (Japanese)
本記事は以上になります。