Ruby
 Computer >> コンピューター >  >> プログラミング >> Ruby

Rubyの認可をマスターする:Pundit vs. CanCanCan 徹底比較

今日の多くのWebアプリケーションでは、ホームページのように誰でも閲覧できる公開ページと、ログインしたユーザーだけがアクセスできるセキュアなページが共存しています。ユーザー登録、ログイン、セッション状態の管理といった一連のプロセスは「認証(authentication)」と呼ばれます。

一方、ログイン済みユーザーに対しては、そのユーザーロールに応じてアクセスできるアクションやリソースを分ける必要があります。たとえば「管理者(admin)」は一般ユーザーよりも広範な権限を持つのが一般的です。このように認証済みユーザーのアクセス範囲を制御するプロセスは「認可(authorization)」と呼ばれます。

本記事では、Rubyエコシステムで最も人気のある2つの認可ライブラリ「Pundit」と「CanCanCan」を徹底的に掘り下げていきます。

それでは始めましょう!

セットアップと前提条件

本記事では、ユーザー(User)と投稿(Post)を扱うシンプルなRails 7アプリケーションを使用します。ユーザーには「editor(編集者)」または「writer(執筆者)」というロールを割り当てます。このシナリオは、認可の仕組みを紹介するのに最適です。

サンプルアプリのコードリポジトリは以下から確認できます。

  • Pundit版
  • CanCanCan版

本記事のテーマは認可ですが、密接に関連する認証の存在も無視できません。

認証のセットアップ手順の詳細は本記事の範囲外となるため割愛します。Devise gemのドキュメントにあるインストール手順に従ってください(認可gemとの組み合わせにはDeviseを使用します)。

もう1点重要なこととして、PunditでもCanCanCanでも、認可gemをインストールしただけでアプリのユーザーロールが自動的に定義されるわけではありません。ロールの設定は手動で行う必要があります。

それでは実際に設定していきましょう。

ユーザーロールの定義

すでにDevise gemをインストールし、Userモデルを作成済みであると仮定します。次のステップは、アプリのユーザーが持つロールを決めることです。今回は以下の2つのロールを用意します。

  • writer(執筆者) — 自分自身の投稿を作成編集更新削除できます。また、他のwriterの投稿を閲覧することも可能です。
  • editor(編集者) — すべてのユーザーの投稿を編集更新閲覧削除できますが、自分自身の投稿を作成することはできません。

まず、ユーザーのロールを保存するカラムを追加します(usersテーブルを変更するマイグレーションを作成)。


続いてマイグレーションを実行します。


そしてUserモデルに、先ほど定義したロールを組み込みます。


ついでに、titlebodyという属性を持ち、投稿者への参照を含むPostモデルも作成しておきましょう。


Postモデルに外部キーuser_idを追加することで、作成されたすべての投稿を特定のユーザーと紐付けられます。Deviseによるユーザー認証はすでに構築済みなので、Postsコントローラーのcreateメソッドを修正し、user_idに現在ログイン中のユーザーが自動的に設定されるようにしましょう。


Userモデル側も、Postモデルとの関連を持つように修正します。


最後に、異なるロールを持つ複数のユーザーをデータベースにシードします。


そしてシードを実行します。


ここまでで、アプリには以下が整いました。

  • Deviseによる認証のセットアップ
  • 「writer」と「editor」という2つのユーザーロールの定義
  • Postモデル

これでPunditを使うための準備がすべて整いました。

PunditによるRubyアプリの認可

Punditは、オブジェクト指向アーキテクチャとプレーンなRubyクラスをベースに構築された認可ライブラリです。アプリの成長に合わせてスケールできる堅牢な認可レイヤーを構築するためのツール群を提供してくれます。

RubyアプリへのPunditのインストール

まず、アプリのGemfileにgemを追加します。


次にターミナルで以下のコマンドを実行します。


あるいは、次のコマンドでも構いません。


認可は主にコントローラーリソースへのアクセス許可・拒否を扱うため、次のステップとしてPunditの認可モジュールをApplicationControllerに追加します。


最後に、他のすべてのポリシーが継承する基底ポリシークラスを生成します。


これにより、以下のような基底ポリシークラスが生成されます。


これでPunditのセットアップは完了です。認証とユーザーロールの準備もできています。

次は、ポリシーを使って、各ユーザーロールがPostリソースにどうアクセスできるかを定義するルールを実装していきましょう。

Punditポリシーの設定

Punditの用語でいう「ポリシー(policy)」とは、あるユーザーロールがさまざまなリソースとどのように相互作用できるかを定義するルールをすべて記述する、プレーンなRubyクラスのことです。

ポリシーには、以下のような特徴があります。

  • 各ポリシーは既存のモデル名に「Policy」という接尾辞を付けた名前になります。たとえば、Postモデルへのアクセス方法を定義するポリシーはPostPolicyと呼ばれます。
  • attr_reader:2つの引数を受け取ります。第1引数はuser、具体的には現在ログイン中のユーザー(current_user)、第2引数は認可ルールを定義したいモデル(今回の場合はpost)です。
  • クエリメソッド:認可ルールが設定されたリソースのコントローラーメソッドに対応します。整理しやすくするために、すべてのポリシーはapp/policiesフォルダ配下に置くのがおすすめです。

アプリ内の各ユーザーロールに必要なアクセスルールは明確なので、まずはwriterロールから始めましょう。

ロールごとのPunditポリシーの定義

writerロールを例に、具体的な実装方法を見ていきます。まず、writerロールによるPostリソースへのアクセスは以下のように整理できます。

  • 自分自身の投稿を作成する
  • 自分自身の投稿を編集・更新する
  • 自分の投稿と他のユーザーの投稿の両方を閲覧(読み取り)する
  • 自分自身の投稿を削除する

これを踏まえて、投稿へのアクセスを制御する新しいポリシーを生成しましょう。


これにより、先ほど生成した基底ポリシーを継承する、以下のような汎用的なポリシークラスが得られます。


次に、writerロール向けのアクセスルールを追加します(基底ポリシークラスから継承されたルールは上書きされます)。


ここでは、投稿の作成・編集・更新・削除時にwriterが何をできるかを定義しています。これらはpostsコントローラーの対応するアクションに紐付きます。お気づきかもしれませんが、これらのルールは投稿全般に適用されるものであり、必ずしもwriter自身の投稿に限定されるものではありません(この点については後述のスコープのセクションで扱います)。

さて、このポリシーを実際に使う方法を見てみましょう。

Punditでのポリシーの使用

ポリシーを使用するには、アクセスルールをチェックしたいコントローラーのメソッド内で、Punditのauthorizeメソッドを呼び出します。すると、関連するポリシークラスがインスタンス化され、authorizeメソッドが呼び出された場所に基づいて、呼び出すべきアクションが判別されます。

たとえば、postsコントローラーのcreateメソッドでauthorizeを呼び出してみましょう。


動作を確認するために、editorとしてログインして投稿をcreateしてみてください。すると、次のようなエラーが表示されます。

Rubyの認可をマスターする:Pundit vs. CanCanCan 徹底比較

期待どおりの動作ではありますが、このようなエラーページを表示するのはユーザー体験として好ましくありません。次のセクションでは、NotAuthorizedErrorをrescueして、よりユーザーフレンドリーな表示にする方法を学びます。

PunditのNotAuthorizedErrorへの対応

まず、ApplicationControllerを以下のように編集します。


ここでは、NotAuthorizedErrorが発生した際にPunditへuser_not_authorizedメソッドを使用するよう指示しています。このメソッドは、認可されていないユーザーを特定のページへリダイレクトし、何が起きたのかを説明するフラッシュメッセージを表示します。

Rubyの認可をマスターする:Pundit vs. CanCanCan 徹底比較

これで、一般的なパーミッションケースに対応できるシンプルな認可システムが完成しました。

しかし、よりきめ細かい権限管理を行いたい場合はどうすればよいのでしょうか?そのためには、Punditのスコープを活用する必要があります。

Punditのスコープ

Punditのスコープは、ActiveRecordのスコープに似ています。ActiveRecordでは、特定の条件に基づいてレコードを取得するためにスコープを使用しますが、Punditのスコープでは、独自に設定したルールに従って特定リソースへのアクセスを管理します。

たとえば、editorには「draft(下書き)」ステータスの投稿を閲覧・編集できるようにしつつ、writerには自分自身の投稿のみを作成・閲覧・編集・更新・削除できるようにしたいとしましょう。

まず、投稿ポリシーを以下のように編集します。


次に、postsコントローラーでリソースへのアクセスを認可します。


もちろん、Punditによるスコーピングはここで紹介した内容よりはるかに深い使い方が可能ですが、本記事ではこのあたりにとどめておきます。より高度なスコープの活用方法については、Punditの公式ドキュメントを参照してください。

続いて、Railsのストロングパラメータと組み合わせてこのライブラリを使う方法を見ていきます。

PunditとRailsのストロングパラメータの併用

Punditの認可ルールとRailsのストロングパラメータを組み合わせれば、リソースの属性へのアクセスを厳格にロックダウンできます。たとえば、Postモデルのexcerptフィールドにアクセスできるのはeditorだけにしたい場合は、どうすればよいでしょうか?

まず、該当するポリシーに適切な名前のブロックを追加します。


次に、コントローラー内のpermitted paramsブロックを修正します。


これで、excerpt属性はeditorだけが利用できるようになりました。

ここからは視点を変えて、もう一方の認可ライブラリ「CanCanCan」を見ていきましょう。

RubyアプリへのCanCanCanの導入

CanCanCanは、「ability(アビリティ)」クラスを使用して、Railsアプリ内で誰が何にアクセスできるかを定義する認可ライブラリです。実際のアクセス制御は、認可モジュールといくつかのビューヘルパーによって実現されます。

CanCanCanのインストール

インストールは、以下のコマンドを実行するだけと非常に簡単です。


Punditと同様に、CanCanCanでも「abilityクラス」と呼ばれるプレーンなRubyクラスオブジェクト内ですべてのアクセスルールを定義できます。次のジェネレーターコマンドで作成してみましょう。


これにより、以下のクラスオブジェクトが生成されます。


次に、このabilityクラスを使って、サンプルRailsアプリのアクセスルールを定義する方法を詳しく見ていきます。

CanCanCanアビリティの定義とチェック

Punditの例と同じく、writerとeditorという2つのユーザーロールを使用します。writerは自分の投稿の作成・編集・更新・削除と、他のwriterの投稿の閲覧が可能。editorは自分の投稿の作成以外、すべての操作が可能です。

CanCanCanを使うには、まずabilityクラス内で、各ユーザーやロールが何にアクセスできるかを以下の形式で定義します。


具体例は以下のとおりです。


次に、コントローラー内で、特定のアクションに対するアクセスルールが存在するかをチェックします。ここでは例としてeditアクションをチェックしてみましょう。


この状態で、別のwriterとして投稿の編集ビューにアクセスすると、以下のエラーが表示されます。

Rubyの認可をマスターする:Pundit vs. CanCanCan 徹底比較

Punditのときと同じように、このエラーをrescueして、ユーザーにより良いエラーページを表示させましょう。

CanCanCanの「Access Denied」エラーの処理

リソースへのアクセスが認可されなかった場合、CanCanCanはCanCan::AccessDeniedエラーを発生させます。この例外をキャッチする最も簡単な方法は、ApplicationControllerを以下のように修正することです。


こうすることで、優れたユーザー体験を実現できます。下のスクリーンショットでは、認可されていないユーザーがホームページへリダイレクトされ、適切なフラッシュメッセージが表示されています。

Rubyの認可をマスターする:Pundit vs. CanCanCan 徹底比較

さらに、ユーザーに表示されるエラーメッセージをカスタマイズすることも可能です。


アプリがXMLレスポンスを返す場合や、CanCanCan::AccessDenied例外の処理をさらに深く知りたい場合は、CanCanCanのドキュメントを参照してください。ここからは、CanCanCanの複数のアビリティを組み合わせて、より堅牢な認可レイヤーを構築する方法を見ていきます。

複数のCanCanCanアビリティの組み合わせ

abilityクラスでは、1つのリソースに対して複数のアクセスルールを定義できます。writerロールとeditorロールを例にすると、以下のように記述できます。


疑問に思うのは、「なぜこんなことをする必要があるのか?」という点でしょう。

CanCanCanでは、すべてのアクセスルールを1つのabilityファイルにまとめて定義できます。これにはメリットとデメリットの両方があります。

すべてのルールが1箇所に集約されているため、アクセスルールの管理は非常に便利です。

しかし、アプリが多数のユーザーロールを扱っていたり、認可が必要なリソースが複数あったりする場合、abilityクラスはたちまち巨大化して複雑になり、扱いにくくなります。これへの対処法のひとつが、abilityクラスをメソッド定義を使って再構成する方法です。


CanCanCanには、本記事で網羅しきれないほど多くの機能があります。詳細については、充実したCanCanCanの公式ドキュメントをぜひ参照してください。

締めくくりに、それぞれのライブラリの特徴を簡単に整理し、どちらを選ぶべきかの判断材料を提供します。

機能比較:Pundit vs. CanCanCan

  • ファイル構成 — Punditなら、アプリの認可ロジックを複数のポリシーファイルに分散して整理できます。一方CanCanCanでは、認可ルールは1つのabilityファイルに集約されます。複数のabilityファイルを使うことも可能ですが、これは標準的な実装スタイルではありません。
  • テスト — CanCanCanのパーミッションコードは単一クラス内にほぼ収まるのに対し、Punditは複数のポリシークラスに分かれるため、CanCanCanの方がテストを書きやすい傾向があります。
  • ヘルパー — どちらのライブラリも、ビュー層でパーミッションをチェックするためのビューヘルパーを多数提供しています。CanCanCanではcan?メソッドをビューで使用でき、Punditではpolicyヘルパーでほぼ同等の機能を実現できます。
  • Deviseとの統合 — 本記事の実装例からもわかるように、どちらのライブラリもDeviseときわめてスムーズに統合できます。

まとめ

本記事では、RubyおよびRailsエコシステムで最も人気のある2つの認可gem「Pundit」と「CanCanCan」を紹介しました。

どちらのライブラリも、Railsアプリでのパーミッション管理に豊富な機能を提供しています。そのため、どちらを選ぶべきかを断言するのはほぼ不可能です。どちらも、最も複雑なパーミッション設定にも十分対応できるからです。

ぜひあなたのアプリでPunditとCanCanCanの両方を試してみて、ニーズに最も合う方を見つけてください。

Happy coding!

P.S. Ruby Magicの最新記事をいち早く読みたい方は、Ruby Magicニュースレターを購読してください。記事を見逃すことは一切ありません!

  1. Rubyの文字列フォーマット入門:%演算子と書式指定子の使い方

    この記事では、Rubyで文字列をフォーマットする方法について詳しく解説します。 なぜ文字列のフォーマットが必要なのでしょうか?例えば、10未満の数値にも先頭にゼロを付けて表示したい場合(01、02、03など)、あるいはコンソール出力を見やすいように桁を揃えて整形したい場合などが挙げられます。 他の言語ではprintf関数を使って文字列をフォーマットできます。C言語を使ったことがある人なら馴染み深いでしょう。printfでは、書式指定子(フォーマット指定子)のリストと、変数や値のリストを定義して使用します。 Rubyでの文字列フォーマットの基本 Rubyでもsprintfメソッドが利用可能ですが

  2. Rubyのcaseステートメントの多彩な活用法と仕組みを徹底解説

    Rubyでif / elsifを使おうとしている場面では、代わりにcaseステートメントを使うことを検討してみてください。この記事では、caseステートメントのさまざまな活用例と、その内部で実際にどのような仕組みで動作しているのかを解説します。 補足:他のプログラミング言語では、これはswitch文として知られています。 Rubyにおけるcaseステートメントの構成要素は以下の通りです。 キーワード 説明 case caseステートメントの定義を開始します。処理対象となる変数を受け取ります。 when マッチ可能な各条件が、1つのwhen句に相当します。 else どの