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

Ruby on RailsでAction Policyをマスターする:安全な認可のための実践ガイド(前編)

アプリケーションを安全に保つためには、誰が・何に対してアクセスできるのかを制御する必要があります。アクセス制御は、「誰を許可するか」を扱う認証(Authentication)と、「何にアクセスできるか」を扱う認可(Authorization)の2つに分類できます。

認証の詳細は別の機会に譲るとして、ユーザーの認可については、一般的に「ロールベース」と「リソースベース」の2つのアプローチがあります。

本記事は2部構成のシリーズで、Ruby on Railsのブログアプリケーションを題材に、Action Policy gemの使い方を深く掘り下げて解説します。この前編では、Action Policyの基礎を学びましょう。

それでは始めましょう!

前提条件

  • Ruby(本記事ではバージョン3.2.2を使用)
  • Rails(バージョン7.0.7を使用)
  • Rubyの基本的な使用経験

まずはリソースベース認可の定義から見ていきましょう。

リソースベース認可とは?

ロールベース認可が事前に定義されたユーザーロールに基づいて権限を設定するのに対し、リソースベース認可は、アプリケーション内の実際のリソースに対してルールを設定することでアクセスを制御します。各リソースにはポリシーが関連付けられ、そのリソースに対してユーザーが何を行えるのかが明示的に定義されます。

本記事ではリソースベース認可に焦点を当てますが、両者の違いを理解しておくことで、それぞれのアプローチがどんな場面で活きるのかを判断できるようになります。

ロールベース認可を使うべきケース

ロールベース認可が適しているのは以下のような場合です。

  • シンプルなアプリケーション:権限システムが単純で、ユーザーロールの種類が少ないアプリなら、ロールベース認可で十分対応できます。
  • 明確に定義されたユーザーグループがある場合:「管理者」「編集者」「ライター」など、明確なユーザーグループが存在するアプリでは、ロールレベルでアクセス制御を扱えるロールベース認可が有効です。

リソースベース認可を使うべきケース

一方、リソースベース認可が輝くのは次のようなシーンです。

  • 動的または複雑なアクセス制御:認可要件が頻繁に変化したり、動的な条件によって決まったりする場合は、リソースベース認可の方が適しています。
  • きめ細かな制御:複数の条件に基づいてリソースへのアクセスを許可・拒否したい場合に便利です。たとえば、Rails製ヘルプデスクアプリで、ユーザーが動的なカテゴリ一覧からサポートチケットを提出し、スタッフがカテゴリごとにチケットを担当するようなケースでは、リソースベース認可が真価を発揮します。
  • オブジェクト指向の制御:リソースベース認可はオブジェクトレベルで動作するため、複雑なオブジェクト指向のルールを定義しやすくなります。

とはいえ、ロールベースとリソースベースのどちらを選ぶかは、アプリケーション固有の特性によって決めるべきです。

それでは、Action Policy gemについて見ていきましょう。

Ruby/Rails向けのAction Policy gem

Action Policyは、RubyおよびRailsアプリ向けの柔軟で拡張性が高く、パフォーマンスに優れた認可フレームワークです。複数のキャッシュ戦略を標準で備えているため、認可ルールにデータベースクエリが必要な場合でも非常に高速に動作します。

また、このgemがリソースベースのルール構築に最適なもう一つの理由は、高いカスタマイズ性です。複数のRubyクラスやモジュールが提供されており、それらをさまざまな方法で組み合わせられます。Railsのコントローラーにとどまらず、アプリ内のほぼどこでもきめ細かなアクセス制御ルールを設定できます。

詳細はAction Policyのプロジェクトホームページをご覧ください。

続いて、今回構築するRailsアプリの概要を確認しましょう。

Railsアプリを作成する

以降では、ユーザーが投稿(CRUD:作成・読み取り・更新・削除)できるRails 7のブログアプリケーションを例に進めます。

Postモデルに対して、リソースベースのアクセス制御を実現するアクションポリシーを段階的に定義していきます。Action Policyを使ってこのアクセス制御戦略をどう実現するのかを学びましょう。

まずは新しいRailsアプリケーションを生成します。

rails new blog
cd blog

注:サンプルアプリのスタイリングにはPico CSSを使用しますが、お好みのものを使って問題ありません。

次に、チュートリアル全体で使用するPostリソースをscaffoldで素早く生成します。

rails generate scaffold Post title:string body:text
rails db:migrate

これでマイグレーションが完了しました。

ユーザー認証のセットアップ

本記事のテーマはユーザー認可ですが、その前に重要な点があります。それはユーザー認証です。認証がなければ、後ほど定義する認可ポリシーは無意味になってしまいます。ただし、ゼロから認証を実装する必要はありません。ここでは定番のDeviseを使いましょう。

Deviseのインストール

まずGemfileにDevise gemを追加します。

gem 'devise'

インストールしてUserモデルを生成します。

bundle install
rails generate devise:install
rails generate devise User
rails db:migrate

また、開発環境の設定ファイル(config/environments/development.rb)に以下を追加しておくことを忘れないでください。

config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }

次に、Postリソースに対するさまざまなユーザーアクセスシナリオをテストできるよう、いくつかの基本的なユーザーロールを実装します。

基本的なユーザーロールの定義

まず、usersテーブルにroleカラムを追加します。以下のコマンドでマイグレーションを生成します。

rails generate migration AddRoleToUsers role:integer

注:roleカラムに整数型を使用するのは、enumを使えるようにするためです。enumを使えば、ロールを手軽かつ簡潔に実装できます。

マイグレーションを実行したら、Userモデルを開いて編集します。

rails db:migrate
class User < ApplicationRecord
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable

  enum role: { reader: 0, author: 1 }
end

これで準備が整いました。次はAction Policyのインストールに移りましょう。

Ruby/Rails向けAction Policyのセットアップ

アプリのGemfileを開き、以下の行を追加します。

gem 'action_policy'

そしてbundle installコマンドでAction Policyをインストールします。

最後に、以下のコマンドでインストールを完了させます。

rails generate action_policy:install

これにより、app/policies配下に基底クラスとなるApplicationPolicyが生成されます。

# app/policies/application_policy.rb
class ApplicationPolicy
  include ActionPolicy::Base
end

Action Policyの基本的な使い方

Action Policyの中核となるのはApplicationPolicyというポリシークラスです。ここでグローバルな設定を定義すると、すべてのポリシーがそれを継承できます。すべてのポリシーはapp/policies配下に配置し、アプリ内のリソースごとに分けて管理するのがおすすめです。たとえばPostリソースがあれば対応するポリシーはPostPolicyCommentリソースがあればCommentPolicy、という具合です。

Action Policyを使う上でもう一つ注意すべき点があります。それは、ルールの定義はポリシークラスのpublicメソッド内で行わなければならないということです。privateメソッド内で定義するとエラーが発生します。

それでは、この知識を活かしてPostPolicyを作成し、段階的にさまざまなレベルのユーザーアクセスを構築していきましょう。

最初のポリシーを作成する

app/policies配下にpost_policy.rbという新しいファイルを作成します。

# app/policies/post_policy.rb
class PostPolicy < ApplicationPolicy
  def index?
    true
  end
end

ここでは、Postリソースに対して「誰でもPostにアクセスできる」というシンプルなルールを定義しています。

投稿をユーザーに関連付ける

この新しい投稿ポリシーを実際に機能させるには、先ほど生成したPostリソースを修正し、作成時にログイン中のユーザーと紐づくようにする必要があります(postsテーブルにuser_idカラムを追加します)。

rails generate migration AddUserIdToPosts user:references
rails db:migrate

次に、この変更に合わせてpostsコントローラーを修正します。まず、許可するpost_paramsuser_idを追加します。

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

続いて、createアクションを修正します。

def create
  @post = current_user.posts.build(post_params)

  respond_to do |format|
    if @post.save
      format.html { redirect_to post_url(@post), notice: "Post was successfully created." }
      format.json { render :show, status: :created, location: @post }
    else
      format.html { render :new, status: :unprocessable_entity }
      format.json { render json: @post.errors, status: :unprocessable_entity }
    end
  end
end

さらに、UserモデルとPostモデルに関連付けを追加しておきましょう。

# app/models/user.rb
class User < ApplicationRecord
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable

  enum role: { reader: 0, author: 1 }
  has_many :posts
end
# app/models/post.rb
class Post < ApplicationRecord
  belongs_to :user
end

次は、PostsControllerを保護していきます。

コントローラーへの認可の実装

PostsControllerにコールバックを追加し、ログイン済みユーザーのみがアクセスできるようにします。

class PostsController < ApplicationController
  before_action :authenticate_user!
  # ...
end

続いて、新しいPostポリシーを使って、基本的なユーザーアクセス制御を追加します。

# app/policies/post_policy.rb
class PostPolicy < ApplicationPolicy
  def update?
    user.author? || record.user == user
  end

  def destroy?
    record.user == user
  end
end

ここでは、postsコントローラーのupdateアクションとdestroyアクションに対するアクセスルールを定義しています。投稿の作者(authorロールを持つユーザーを含む)は投稿を更新でき、削除できるのは投稿の作者のみ、という仕様です。

ワンポイント:Action Policyは、Deviseが提供するcurrent_userを参照し、ポリシー内のuserとして自動的に割り当てることができます。

次に、このアクセスルールをpostsコントローラーで次のように使用します。

class PostsController < ApplicationController
  before_action :authenticate_user!
  before_action :set_post, only: %i[show edit update destroy]
  authorize :post, through: :post

  # ...

  def update
    authorize! @post

    respond_to do |format|
      if @post.update(post_params)
        format.html { redirect_to post_url(@post), notice: "Post was successfully updated." }
        format.json { render :show, status: :ok, location: @post }
      else
        format.html { render :edit, status: :unprocessable_entity }
        format.json { render json: @post.errors, status: :unprocessable_entity }
      end
    end
  end

  def destroy
    authorize! @post
    @post.destroy

    respond_to do |format|
      format.html { redirect_to posts_url, notice: "Post was successfully destroyed." }
      format.json { head :no_content }
    end
  end
end

これでコントローラーにアクセスルールが適用されました。次のステップは、ビューも保護することです。

認可によるビューの保護

下のスクリーンショットでは、メールアドレス<user2@example.com>で「reader」ロールを持つユーザーがログインしています。ご覧のとおり、このユーザーは<user@example.com>が作成した投稿を閲覧できるだけでなく、その投稿の編集・削除リンクにもアクセスできてしまっています。これは望ましい状態ではありません。編集・削除リンクは投稿の作者だけに表示されるべきです。

Ruby on RailsでAction Policyをマスターする:安全な認可のための実践ガイド(前編)

最初のタスクは、投稿の作者以外のユーザーからこれらのリンクへのアクセスを取り除くことです。アクセスルールはすでに定義済みなので、Action Policyの便利なallowed_to?メソッドを使って、showビューに適用するだけでOKです。

<% if allowed_to?(:update?, @post) || allowed_to?(:destroy?, @post) %>
  <div>
    <%= link_to "Edit this post", edit_post_path(@post) %> |
    <%= button_to "Destroy this post", @post, method: :delete %>
  </div>
<% end %>

このルールを適用したら、投稿のshowページを更新して結果を確認してみましょう。

Ruby on RailsでAction Policyをマスターする:安全な認可のための実践ガイド(前編)

先ほどまでリンクにアクセスできていた同じユーザーが、投稿ページ上でできることが制限されました。

まとめ

この2部構成シリーズの前編では、認可gem「Action Policy」の基礎について学びました。

後編では、より高度なユースケースを掘り下げていきます。

それでは、Happy Coding!

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

  1. Rubyのラムダ徹底解説――基本から実践的な応用テクニックまで

    Rubyにおいてブロックは非常に重要な存在であり、ブロックなしにこの言語を語ることはできません。では、ラムダはどうでしょうか?ラムダを愛している人はいるのでしょうか?実際のところ、何年もラムダを使わずに過ごすことだって可能かもしれません。まるで昔の時代の遺物のように見えることもあるでしょう。 しかし、それは完全な真実ではありません。少し掘り下げてみると、ラムダには興味深い技が数多く隠されているのです。 この記事では、まずラムダの基本的な使い方から始め、その後、より高度で面白い応用へと進んでいきます。すでに毎日ラムダを使いこなしている方は、ぜひ後半の応用編までスクロールしてください。 ラムダにつ

  2. Rubyistのための環境変数徹底ガイド――仕組みの理解からRailsでの実践管理まで

    Webアプリを開発環境と本番環境の両方で効果的に管理したいなら、環境変数への深い理解は欠かせません。 実は、これは昔から当たり前だったわけではありません。数年前まで、Railsアプリを環境変数で設定する開発者はほとんどいませんでした。しかし、Herokuの登場がその常識を大きく変えたのです。 Herokuは開発者に「12-Factor App(12ファクター・アプリ)」という設計手法を広めました。このマニフェストには、デプロイしやすいアプリを作るためのベストプラクティスが数多くまとめられており、中でも環境変数に関するセクションは特に大きな影響力を持っています。 12ファクター・アプリは設定(c