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オブジェクトであるため、テストはシンプルかつ高速に書けます。
-
Fitbit Pay完全ガイド:設定方法からお支払い・セキュリティ対策まで徹底解説
Fitbitは、最も優れたスマートウェアラブルデバイスのひとつです。多彩なモデルと豊富な機能を備えており、その中でも人気の高い機能が「Fitbit Pay」です。Fitbit IonicやFitbit Versaに搭載されたNFC(近距離無線通信)チップにより、非接触決済に対応したあらゆる店舗で、腕時計をつけたまま支払いが可能になります。 この記事では、Fitbit Payの使い方をマスターできるよう、設定方法からセキュリティ対策まで完全ガイドとして詳しく解説します。 Fitbit Payとは? Fitbit Payは、デビットカードやクレジットカードをFitbit IonicおよびVersa
-
Androidのウイルス除去完全ガイド|感染原因・症状・対処法を徹底解説
あらゆる場面において、ウイルスへの感染は最大の悪夢です! テクノロジーの世界では、ウイルスやマルウェアといえばデスクトップPCやノートPCを連想しがちですが、実は最も感染リスクが高いOSのひとつがAndroidスマートフォンです。ここ数ヶ月だけでも数多くの脆弱性が悪用されており、研究者の調査によると、全Android端末の87%が少なくとも1つの重大な脆弱性にさらされ、95%の端末はたった1通のSMSメッセージでハッキングされる可能性があることが判明しています。 では、もしスマホがウイルスに感染してしまったらどうすればよいのでしょうか?多くの方が真っ先に思い浮かべるのは「初期化(ファクトリーリ