Published on

〜権限管理切り出せていないの厳しいって〜Railsで権限管理やアクセス制御をするためのPolicyオブジェクトパターンを実例付きで解説❗️

Authors

この記事は ex-crowdworks Advent Calendar 2024の18日目の記事です。

はじめに

今年、株式会社クラウドワークスを退職した@nisyuuです。今年、食べて正解だった海鮮丼はじもの亭の海鮮丼(華)です。 エンジニアとしてクラウドワークステック(旧クラウドテック)というフリーランスと企業をマッチングするエージェントサービスを開発していました。

Policyオブジェクトパターンは、ソフトウェア設計において権限管理やアクセス制御を明確にするためのデザインパターンです。 特に、Webアプリケーションでは、ユーザーが特定の操作を実行できるかどうかを判定する場面で頻繁に利用されます。

本記事では、Policyオブジェクトパターンの基本的な概念と利点・欠点、さらにRuby on Railsでの実装例について解説します。

Policyオブジェクトパターンとは

Policyオブジェクトパターンは、アプリケーション内のビジネスルールを1つのクラスにカプセル化して管理する設計手法です。このパターンは以下のような特徴があります:

  • 役割の分離: 権限管理のロジックをモデルやコントローラーから切り離して、専用のクラスにまとめます
  • 単一責任原則(SRP): 各Policyオブジェクトは特定のリソースと操作に対するルールのみを担当します
  • テストの容易さ: 権限ロジックを独立したクラスとして実装するため、テストがしやすくなります

基本的な構成

Policyオブジェクトは、通常以下のように構成されます:

  • 対象リソース: 許可または拒否を判断するリソース(例:Postモデル)
  • メソッド: 各操作(例:create?update?)の許可/拒否を判定するメソッド
  • コンテキスト: 現在のユーザーやその他の関連情報(例:ログインユーザー)

Policyオブジェクトパターンのメリットとデメリット

メリット

  • コードの可読性向上: 権限ロジックが明確に分離されるため、コードベースが読みやすくなります
  • 再利用性の向上: Policyオブジェクトを異なるコントローラーやモデル間で再利用可能
  • テストの容易化: テストが個別のクラス単位で実施でき、ユニットテストが簡単
  • 保守性の向上: 権限ロジックが1か所に集約されているため、変更が容易

デメリット

  • クラスの増加: 小規模なアプリケーションでは、ファイルやクラスの数が増えすぎて煩雑になる可能性がある
  • 学習コスト: 初心者にとって、別途Policyオブジェクトを作成する設計思想に慣れる必要がある
  • パフォーマンス: 設計の複雑さにより、パフォーマンスに若干の影響が出ることもある

Ruby on Railsでの実装例

RailsでのPolicyオブジェクトパターンの実装には、PunditというGemが広く利用されています。以下は、Punditを使った基本的な実装例です。

Gemのインストール

まず、PunditをGemfileに追加します。

# Gemfile
gem 'pundit'

インストール後、以下のコマンドでセットアップを行います:

bundle install
rails g pundit:install

Policyクラスの作成

例えば、Postモデルに対するPolicyオブジェクトを作成します。

rails g pundit:policy post

以下のようなファイルが生成されます:

# app/policies/post_policy.rb
class PostPolicy < ApplicationPolicy
  def show?
    true # 全てのユーザーが閲覧可能
  end

  def update?
    user.admin? || record.user_id == user.id # 管理者または投稿者本人のみ更新可能
  end

  def destroy?
    user.admin? # 管理者のみ削除可能
  end
end

コントローラーでの利用

PostPolicyを利用してアクセス制御を行います。

# app/controllers/posts_controller.rb
class PostsController < ApplicationController
  before_action :authenticate_user!
  before_action :set_post, only: [:show, :update, :destroy]

  def show
    authorize @post # Punditのauthorizeメソッドを使用
  end

  def update
    authorize @post
    if @post.update(post_params)
      redirect_to @post, notice: '投稿を更新しました。'
    else
      render :edit
    end
  end

  def destroy
    authorize @post
    @post.destroy
    redirect_to posts_path, notice: '投稿を削除しました。'
  end

  private

  def set_post
    @post = Post.find(params[:id])
  end

  def post_params
    params.require(:post).permit(:title, :content)
  end
end

Policy Specの作成

PolicyオブジェクトのテストはRSpecを使うと簡単です。

# spec/policies/post_policy_spec.rb
require 'rails_helper'

describe PostPolicy do
  subject { described_class.new(user, post) }

  let(:user) { User.create(role: 'admin') }
  let(:post) { Post.create(user: user) }

  context '管理者ユーザーの場合' do
    it { is_expected.to permit_action(:update) }
    it { is_expected.to permit_action(:destroy) }
  end

  context '通常ユーザーの場合' do
    let(:user) { User.create(role: 'user') }

    it { is_expected.to forbid_action(:destroy) }
  end
end

おわりに

Policyオブジェクトパターンは、アプリケーションの権限管理を分離し、コードの保守性と再利用性を向上させるために有効です。 Railsでは、Punditのようなツールを利用することで、簡単かつ効率的に実装が可能です。 小規模なプロジェクトでは冗長に感じるかもしれませんが、中〜大規模なアプリケーションではそのメリットを感じられるでしょう。