- アプリ内の特定のデータに対して、**「このユーザーからは見れるようにしたいけど、あのユーザーには見せたくない」**といったケース、結構ありますよね。
- このように**「特定のユーザーにだけ、特定の行動を許可すること」を「認可」**といいます。
- 今回はそんな「認可設定」をしたい時に便利なGem、Punditについて色々と上手に使う方法を書いていこうと思います。
-
注意点:Punditでは、
current_user
(ログイン中のユーザーを取得する)メソッドを使うので、前もってDevise Gemなどの「認証の仕組み」を入れておく必要があります。
Punditの概要
- まずは
pundit
をGemインストールした上で、ApplicationController
にPundit
をinclude
して、Punditを使う準備をします。(リポジトリ参照)
class ApplicationController < ActionController::Base
include Pundit
end
- 次に、例えば「飼い主」に対して紐づいている、**「飼い犬のデータ」**に対して、以下のような「認可設定」をしたいとします。
- 名前が記載されているにだけアクセスできる。
- ログイン中のユーザーが飼い主か、飼い主の知り合いの場合だけアクセスできる。
- この「アクセス」というのは、色々な方法があるのですが、今回は**「Dogの詳細ページにアクセスする」**場合のみ考えます。
- するとPunditではこう書けます。
# app/policies/dog_policy.rb
class DogPolicy < ApplicationPolicy
# 必ずtrueかfalseを返すメソッドにする
# recordにはデータ(対象となるDogインスタンス)が自動で入る
# userはcurrent_userが自動で入る
def show?
dog_owner = record.owner
record.name? && (dog_owner == user || dog_owner.friend?(user))
end
end
- これすごくよくわかりやすくないですか?
- あとはこれを必要な箇所(主にコントローラー)から、
authorize
で呼び出します。
class DogsController < ApplicationController
def show
# ログインユーザーがアクセスできるのかチェック
@dog = authorize Dog.find(params[:id])
end
end
- このGemの賢いのは、
authorize
で呼び出したアクション名とモデル名から自動的にポリシーメソッドを選び出して適用してくれるのです。 - 今回は「
Dog
モデルに対してshow
アクション内で呼び出している」ので、DogPolicy#show?
が自動で選ばれるわけです。 -
注意点としては、
show?
がfalse
を返した場合、authorizeがPundit::NotAuthorizedError
をraiseするので、ApplicationController
などで、エラーを拾って403を返す仕組みを作っておく必要があることです。
# app/controllers/application_controller.rb
unless Rails.env.development?
rescue_from Pundit::NotAuthorizedError, with: :render_403
end
def render_403
# 中身は各自に任せる
render template: 'pages/403', status: 403
end
Punditを使う時に意識すること
- ここからが本題です。
- 先ほどのコードに戻ります。
# app/policies/dog_policy.rb
class DogPolicy < ApplicationPolicy
# 必ずtrueかfalseを返すメソッドにする
def show?
dog_owner = record.owner
record.name? && (dog_owner == user || dog_owner.friend?(user))
end
end
-
これ、分析してみると二つのセクションに分かれてるんです
- データの状態に関する条件文
- ログインユーザーに関する条件文
-
当たり前と思われるかもしれませんが、認可のコードを書く時にこれらの二つをごちゃごちゃにしてしまって、「認可の漏れや誤作動」が発生することがあります。
-
そのような事態を防ぐためにも、まずは
- それぞれの認可設定で、どちら(もしくは両方)の条件を考える必要があるのか
- 条件が複数ある場合、「最も強い制約」がかかっているのはどの条件か
をしっかりと認識する必要があります。
例題
-
ちょっとややこしい例を考えてみます。
- 名前が記載されているにだけアクセスできる。
- それぞれのに対して**「非公開設定」ができて、非公開の場合には、飼い主以外そのにアクセスできないようにする**。
- ただし、飼い主から「ライセンス」が与えられている場合には、「非公開」でもアクセスできる。
-
この場合まず押さえるべきは、**「どの条件が一番強いのか」**です。
-
例えば今回の仕様で、「の名前がないとそもそもページが表示できない」場合、「最も強い制約」は「に名前が記載されていること」です。
-
なぜなら、たとえ飼い主であっても、の名前が無いのであればそもそもページが見れない(アプリに支障をきたす)からです。
-
なので、**「の名前があること」**をトップにもってきます。
class DogPolicy < ApplicationPolicy
def show?
record.name?
end
end
- 次は何かと考えると、次は**「飼い主であること」か「飼い主のDog閲覧ライセンスをもっていること」**がくると思います。
- なぜなら「犬の名前が無い」ことを除いて、この二つの制約を否定できる条件が一つもないからです。(非公開設定に関わらず、アクセスできる)
- よって次はこうなります
class DogPolicy < ApplicationPolicy
def show?
return false unless record.name?
dog_owner = record.owner
dog_owner == user || dog_owner.gave_license_to?(user)
end
end
- 最後に、残った「公開か非公開か」の条件文が入ってフィニッシュです。
class DogPolicy < ApplicationPolicy
def show?
return false unless record.name?
dog_owner = record.owner
return true if dog_owner == user || dog_owner.gave_license_to?(user)
record.is_public?
end
end
まとめ
- Punditを使うと、「認可設定」を楽に実装できる。
- ただ認可設定をする時には、**「強さの順番」や「データの条件かログインユーザーの条件か」をしっかりと認識して「認可チェックをする時の流れをストーリーにすること」**が大事になる。