4
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Railsの認可Gem「Pundit」で、漏れのない認可設定を上手に作るTips

Last updated at Posted at 2019-09-30
  • アプリ内の特定のデータに対して、**「このユーザーからは見れるようにしたいけど、あのユーザーには見せたくない」**といったケース、結構ありますよね。:frowning2:
  • このように**「特定のユーザーにだけ、特定の行動を許可すること」「認可」**といいます。
  • 今回はそんな「認可設定」をしたい時に便利なGem、Punditについて色々と上手に使う方法を書いていこうと思います。
  • :warning:注意点:Punditでは、current_user(ログイン中のユーザーを取得する)メソッドを使うので、前もってDevise Gemなどの「認証の仕組み」を入れておく必要があります

Punditの概要

  • まずはpunditをGemインストールした上で、ApplicationControllerPunditincludeして、Punditを使う準備をします。(リポジトリ参照
class ApplicationController < ActionController::Base
  include Pundit
end
  • 次に、例えば「飼い主」に対して紐づいている、**「飼い犬のデータ:dog:」**に対して、以下のような「認可設定」をしたいとします。
    • 名前が記載されている:dog:にだけアクセスできる。
    • ログイン中のユーザーが飼い主か、飼い主の知り合いの場合だけアクセスできる。
  • この「アクセス」というのは、色々な方法があるのですが、今回は**「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
  • これすごくよくわかりやすくないですか?:sunny:
  • あとはこれを必要な箇所(主にコントローラー)から、authorizeで呼び出します。
class DogsController < ApplicationController
  def show
    # ログインユーザーがアクセスできるのかチェック
    @dog = authorize Dog.find(params[:id])
  end
end
  • このGemの賢いのは、authorizeで呼び出したアクション名とモデル名から自動的にポリシーメソッドを選び出して適用してくれるのです。
  • 今回は「Dogモデルに対してshowアクション内で呼び出している」ので、DogPolicy#show?が自動で選ばれるわけです。
  • :warning:注意点としては、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を使う時に意識すること

  • ここからが本題です。
  • 先ほどのコードに戻ります。:point_down:
# 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
  • これ、分析してみると二つのセクションに分かれてるんです

    • データ:dog:の状態に関する条件文
    • ログインユーザーに関する条件文
  • 当たり前と思われるかもしれませんが、認可のコードを書く時にこれらの二つをごちゃごちゃにしてしまって、「認可の漏れや誤作動」が発生することがあります

  • そのような事態を防ぐためにも、まずは

    • それぞれの認可設定で、どちら(もしくは両方)の条件を考える必要があるのか
    • 条件が複数ある場合、「最も強い制約」がかかっているのはどの条件か

をしっかりと認識する必要があります。

例題

  • ちょっとややこしい例を考えてみます。

    • 名前が記載されている:dog:にだけアクセスできる。
    • それぞれの:dog:に対して**「非公開設定」ができて、非公開の場合には、飼い主以外その:dog:にアクセスできないようにする**。
    • ただし、飼い主から「ライセンス」が与えられている場合には、「非公開」でもアクセスできる
  • この場合まず押さえるべきは、**「どの条件が一番強いのか」**です。

  • 例えば今回の仕様で、:dog:の名前がないとそもそもページが表示できない」場合、「最も強い制約」は「:dog:に名前が記載されていること」です

  • なぜなら、たとえ飼い主であっても、:dog:の名前が無いのであればそもそもページが見れない(アプリに支障をきたす)からです

  • なので、**「:dog:の名前があること」**をトップにもってきます。:point_down:

class DogPolicy < ApplicationPolicy
  def show?
    record.name?
  end
end
  • 次は何かと考えると、次は**「飼い主であること」か「飼い主のDog閲覧ライセンスをもっていること」**がくると思います。
  • なぜなら「犬の名前が無い」ことを除いて、この二つの制約を否定できる条件が一つもないからです。(非公開設定に関わらず、アクセスできる)
  • よって次はこうなります:point_down:
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
  • 最後に、残った「公開か非公開か」の条件文が入ってフィニッシュです。:point_down:
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を使うと、「認可設定」を楽に実装できる。
  • ただ認可設定をする時には、**「強さの順番」や「データの条件かログインユーザーの条件か」をしっかりと認識して「認可チェックをする時の流れをストーリーにすること」**が大事になる。
4
4
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?