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

Railsアプリでユーザー権限を管理する完全ガイド〜Policy Objectパターンの実践〜

Webアプリケーションにおいて、特定のロール(役割)や権限をユーザーに割り当てる機能は、非常によくある要件です。

多くのWebアプリでは、管理者と一般ユーザーでアクセスできる範囲を分けています。これを実現する最もシンプルな方法は、「そのユーザーが管理者かどうか」を判定する真偽値(boolean)を持たせることです。しかし、実際のアプリケーションでは、ロールや権限の要件はもっと複雑になることが少なくありません。

アプリケーションの価値は、特定のデータや操作へのアクセスを制限することにあります。この部分の実装は絶対に失敗したくないところです。本記事では、Ruby on Railsアプリケーションにおけるロールと権限の実装方法について詳しく解説します。

権限管理にgemは必要なのか?

結論から言えば、必ずしもgemは必要ありません。特に小規模なアプリケーションで、依存関係をこれ以上増やしたくない場合には、自前で実装するのも十分現実的な選択肢です。

ただし、既存のライブラリを検討したい方向けに、ロール・権限管理で人気のある主要なgemを紹介します。

  • Devise
    認証とロール管理を提供する、非常に高機能で堅牢なソリューションです。GitHubで21.7k以上のスターを獲得しており、本記事で紹介する中で最も人気があります。ただしDeviseは認証基盤としての側面が強いため、堅牢なライブラリが必要な場合に採用を検討するとよいでしょう。
  • Pundit
    シンプルなRubyオブジェクトをベースとした認可gemで、おそらく本記事で紹介する中で最も扱いやすいものです。純粋なRubyに近い感覚で使え、GitHubでは7.3kスターを持ち、現在 最も人気のあるポリシー系gemといえます。
  • CanCan
    特定のユーザーがアクセスできるリソースを制限する認可ライブラリです。しかし、CanCanは長年メンテナンスされておらず、Rails 3以前でしか動作しないため、新規プロジェクトでの利用は推奨されません。
  • CanCanCan
    RubyおよびRuby on Rails向けの認可ライブラリで、CanCanの後継となるプロジェクトです。現在も活発にメンテナンスされており、GitHubでは4.9kスターと紹介した中では最も少ないものの、安定して動作し保守状況も良好です。

いずれも優れたgemですが、プレーンなRubyだけでも権限管理の仕組みを作ることはそれほど難しくありません。ここからは、Policy Objectパターンと呼ばれる手法を使って、gemに頼らず権限を管理する方法を紹介します。

Policy Objectパターンとは

Policy Objectは、権限やロールの扱いに使われるデザインパターンです。「何か(または誰か)がある操作を行ってよいか」を判定したい場面のすべてで利用できます。複雑なビジネスルールをオブジェクト内部にカプセル化でき、異なるルールを持つ別のポリシーオブジェクトへ簡単に差し替えられるのが特徴です。外部依存はすべてポリシーオブジェクトに注入されるため、権限チェックのロジックが分離され、コントローラやモデルをクリーンな状態に保てます。Pundit、CanCan、CanCanCanといったgemも、このパターンを実装したものです。

純粋なポリシーオブジェクトのルール

良いポリシーオブジェクトには、次のようなルールがあります。

  • 戻り値は必ず真偽値(boolean)にする
  • ロジックはシンプルに保つ
  • メソッド内では、渡されたオブジェクトに対する呼び出しのみ行う

実装

まずは命名規則から始めましょう。ファイル名には _policy という接尾辞を付け、クラス名も末尾に「Policy」を付けます。また、判定用のメソッド名は常に ? で終わるようにします(例:UsersPolicy#allowed?)。

以下はサンプルコードです。

class UsersPolicy
  def initialize(user)
    @user = user
  end

  def allowed?
    admin? || editor?
  end

  def editor?
    @user.where(editor: true)
  end

  def admin?
    @user.where(admin: true)
  end
end

どんな場面で使うべきか?

アプリに制限付きアクセスや制限付き操作が複数種類存在する場合に、ポリシーオブジェクトが威力を発揮します。例えば、次のような条件で投稿を作成できるケースを考えてみましょう。

  • タグが1つ以上設定されていること
  • 作成できるのは管理者と編集者のみであること
  • 編集者の場合はメールアドレスの確認済みである必要があること

まずは、ポリシーオブジェクトを使わないコントローラの例を見てください。

class PostsController < ApplicationController
  def create
    if @post.tag_ids.size > 0 &&
       (current_user.role == 'admin' ||
        (current_user.role == 'editor' && current_user.verified_email))
      # 投稿を作成
    end
  end
end

上記のように条件式が長くなると、見た目が悪いうえに可読性も大きく損なわれます。こうした場面こそ、Policy Objectパターンを適用すべきタイミングです。

それでは、PostsCreationPolicy を作成してみましょう。

class PostsCreationPolicy
  attr_reader :user, :post

  def initialize(user, post)
    @user = user
    @post = post
  end

  def self.create?(user, post)
    new(user, post).create?
  end

  def create?
    with_tags? && author_is_allowed?
  end

  private

  def with_tags?
    post.tag_ids.size > 0
  end

  def author_is_allowed?
    is_admin? || editor_is_verified?
  end

  def is_admin?
    user.role == 'admin'
  end

  def editor_is_verified?
    user.role == 'editor' && user.verified_email
  end
end

ポリシーオブジェクトを導入したコントローラは、次のように非常にすっきりします。

class PostsController < ApplicationController
  def create
    if PostsCreationPolicy.create?(current_user, @post)
      # 投稿を作成
    end
  end
end

Railsでポリシーオブジェクトを使う方法

app/policies ディレクトリを作成し、すべてのポリシークラスをそこに配置しましょう。コントローラから呼び出す際は、アクション内で直接呼んでもよいですし、before_action を使って共通化することもできます。

class PostsController < ApplicationController
  before_action :authorized?, only: [:edit, :create, :update, :destroy]

  def authorized?
    unless ::PostsCreationPolicy.create?(current_user, @post)
      render file: "public/404.html", status: :unauthorized
    end
  end
end

ポリシーオブジェクトのテスト方法

コントローラレベルの振る舞いのテストはシンプルです。

require 'rails_helper'

RSpec.describe "/posts", type: :request do
  describe "when user is not allowed" do
    let(:user_not_allowed) { create(:user, admin: false, editor: false) }
    let(:tag) { create(:tag) }
    let(:valid_attributes) { attributes_for(:post, tag_id: tag.id) }

    before do
      sign_in user_not_allowed
    end

    describe "GET /posts/:id/edit" do
      it "returns 401" do
        post = Post.create! valid_attributes
        get edit_post_url(post)
        expect(response).to have_http_status(401)
      end
    end
  end
end

ポリシー自体のテストも簡単です。各メソッドが単一の責務しか持たない小さな単位になっているためです。

require 'rails_helper'

RSpec.describe PostsCreationPolicy do
  describe "when checking permissions" do
    let(:user) { create(:user, editor: false, admin: false) }
    let(:verified_editor) { create(:user, editor: true, verified_email: true) }
    let(:tag) { create(:tag) }
    let(:post) { create(:post, tag_id: tag.id) }

    describe ".create?" do
      context "when user is allowed" do
        it "returns true" do
          expect(described_class.create?(verified_editor, post)).to eq(true)
        end
      end

      context "when user is not allowed" do
        it "returns false" do
          expect(described_class.create?(user, post)).to eq(false)
        end
      end
    end

    # ...その他のテストケース
  end
end

このように、あらゆるシナリオにおいて「そのオブジェクトの作成が許可されるかどうか」をテストできます。

まとめ

Policy Objectパターンの概念はとても小さなものですが、大きな効果をもたらします。シンプルな権限でも複雑な権限でも、権限チェックが必要になった場面では、ぜひポリシーオブジェクトの適用を検討してください。また、RSpecでのテストについても、データベースのレコードに依存せず、ポリシーが純粋なRubyオブジェクトであるため、テストはシンプルかつ高速に書けます。

  1. Fitbit Pay完全ガイド:設定方法からお支払い・セキュリティ対策まで徹底解説

    Fitbitは、最も優れたスマートウェアラブルデバイスのひとつです。多彩なモデルと豊富な機能を備えており、その中でも人気の高い機能が「Fitbit Pay」です。Fitbit IonicやFitbit Versaに搭載されたNFC(近距離無線通信)チップにより、非接触決済に対応したあらゆる店舗で、腕時計をつけたまま支払いが可能になります。 この記事では、Fitbit Payの使い方をマスターできるよう、設定方法からセキュリティ対策まで完全ガイドとして詳しく解説します。 Fitbit Payとは? Fitbit Payは、デビットカードやクレジットカードをFitbit IonicおよびVersa

  2. Androidのウイルス除去完全ガイド|感染原因・症状・対処法を徹底解説

    あらゆる場面において、ウイルスへの感染は最大の悪夢です! テクノロジーの世界では、ウイルスやマルウェアといえばデスクトップPCやノートPCを連想しがちですが、実は最も感染リスクが高いOSのひとつがAndroidスマートフォンです。ここ数ヶ月だけでも数多くの脆弱性が悪用されており、研究者の調査によると、全Android端末の87%が少なくとも1つの重大な脆弱性にさらされ、95%の端末はたった1通のSMSメッセージでハッキングされる可能性があることが判明しています。 では、もしスマホがウイルスに感染してしまったらどうすればよいのでしょうか?多くの方が真っ先に思い浮かべるのは「初期化(ファクトリーリ