はじめに
Ransack 4.0.0では「検索可能な列」や「検索可能な関連モデル」をモデル内に予め定義しておかないと、以下のようなエラーが発生します。
RuntimeError in Users#index
Ransack needs User attributes explicitly allowlisted as
searchable. Define a `ransackable_attributes` class method in your `User`
model, watching out for items you DON'T want searchable (for
example, `encrypted_password`, `password_reset_token`, `owner` or
other sensitive information). You can use the following as a base:
これは悪意のあるユーザーが任意の検索条件を実行して、内部データを推測するセキュリティ問題を回避するためです。
この記事では既存のRailsアプリケーションをRansack 4に移行する手順を説明します。
動作確認バージョン
- ransack 4.0.0
- rails 6.1.7.4
- ruby 3.1.3
この記事で想定しているRailsアプリケーション
この記事では以下のような構造のRailsアプリケーションを想定しています。
- 管理者はAdministratorクラス、一般ユーザーはUserクラスとモデルが別れている
- ログイン画面も別々
- Userクラスが
admin
フラグを持つ、というような設計ではない
- 管理者は管理者専用の画面とコントローラを持つ
- 管理者用画面のコントローラはすべて
Admins::
というネームスペースが切られている - 1つのerbファイル内で「管理者かどうか」で条件分岐するようなことはない
- 管理者用画面のコントローラはすべて
この構成と異なる場合は適宜本文の内容を読み替えてください。
管理者向け画面(自由に検索させたい画面)の場合
「管理者向け画面は管理者しかアクセスできないはずだから、基本的にどんな検索条件でも構わない」という場合は、ApplicationRecordクラスに以下のようなコードを入れておくと便利です。
class ApplicationRecord < ActiveRecord::Base
# ...
def self.ransackable_attributes(auth_object = nil)
# NOTE: ifの条件式は自分のプロジェクトに応じて変更すること
if Administrator === auth_object
# すべての列を検索OKとする
authorizable_ransackable_attributes
else
raise "Please implement ransackable_attributes: #{self}"
end
end
def self.ransackable_associations(auth_object = nil)
# NOTE: ifの条件式は自分のプロジェクトに応じて変更すること
if Administrator === auth_object
# すべての関連モデルを検索OKとする
authorizable_ransackable_associations
else
raise "Please implement ransackable_associations: #{self}"
end
end
end
そして、管理者画面のコントローラではransack
メソッドでauth_object:
に「ログイン中の管理者(current_administrator
)」を引数として渡すようにします。
class Admins::UsersController < Admins::ApplicationController
def index
# NOTE: auth_objectに渡すオブジェクトは自分のプロジェクトに応じて変更すること
- @q = User.ransack(params[:q])
+ @q = User.ransack(params[:q], auth_object: current_administrator)
@users = @q.result.order(:id).page(params[:page])
end
end
こうすると「ログイン中の管理者」がauth_object
としてransackable_attributes
やransackable_associations
に渡されます。
ransackable_attributes
やransackable_associations
の内部では「管理者なら自由に検索しても良い」と判断して、authorizable_ransackable_attributes
やauthorizable_ransackable_associations
を返すようにします。
注意点
- 上のコード内コメントにも書いたように、「管理者がアクセスしていると判断する条件」はプロジェクトによって異なるので適宜変更してください。
- 「管理者だからといって自由に検索させるのはNG」というプロジェクトの場合は、ここで説明した内容は適用しないでください。
検索条件の制限が必要な画面の場合
既存のコードですでにRansackを使っている場合かつ、検索条件を制限したい場合はちょっと対応が面倒です。まず、以下のようなキーワードでプロジェクト内をgrep検索してみてください。
-
.ransack
(コントローラ内にあるはず) -
search_form_for
(erb内にあるはず) -
search_field
(erb内にあるはず) -
sort_link
(erb内にあるはず)
上記のキーワードに引っかかったコードはRansackを利用している画面です。検索結果を参考にして、対象となる画面にアクセスしてみてください。おそらく以下のようなエラーが発生すると思います。
Please implement ransackable_attributes: User
ここではUserモデルを検索しようとしているようなので、erb内で使われているsearch_field
やsort_link
の引数を参考にしながら、検索やソートを許可させたいUserモデルの列を確認してください。
たとえば以下の画面ではname
とnickname
を検索しようとしていることがわかります。
<%= search_form_for @q do |f| %>
<%= f.label :name_cont %>
<%= f.search_field :name_cont %>
<%= f.label :nickname_cont %>
<%= f.search_field :nickname_cont %>
<%= f.submit %>
<% end %>
そこでUserクラスにransackable_attributes
とransackable_associations
を定義します。
class User < ApplicationRecord
# ...
def self.ransackable_attributes(auth_object = nil)
# nameとnicknameは検索OKとする(ただし管理者は自由に検索可)
auth_object ? super : %w(name nickname)
end
def self.ransackable_associations(auth_object = nil)
# 関連先のモデルを検索する必要がなければ空の配列を返す
auth_object ? super : []
end
end
上のコード例ではauth_object
に何かしら値がセットされていれば、それは管理者によるアクセスであろう、ということで親クラスに処理を委譲しています(super
)。それ以外は一般ユーザーによるアクセスと見なし、name
とnickname
だけを検索可能としています。
実際に画面を動かし、Ransackを利用した検索機能やソート機能が意図通りに動いていることを確認してください。
関連モデルの列を検索する場合
たとえばUserモデルがCompanyクラスと関連を持っていたとします。
class User < ApplicationRecord
belongs_to :company
end
このときRansackで「ユーザーが所属している企業の企業名」で検索したい場合は、
<%= f.search_field :company_name_cont %>
というように、search_field
に"(関連モデル名)_(関連モデルの列名)_(検索条件)"を渡し、UserモデルとCompanyモデルにそれぞれ以下の設定を追加します。
class User < ApplicationRecord
belongs_to :company
# ...
def self.ransackable_associations(auth_object = nil)
# company_nameのcompnay(関連モデル名)を設定
auth_object ? super : %w(company)
end
end
class Company < ApplicationRecord
# ...
def self.ransackable_attributes(auth_object = nil)
# company_nameのname(列名)を設定
auth_object ? super : %w(name)
end
# ...
end
間違ってUserモデルのransackable_attributes
に%w(company_name)
と書いてしまわないように注意してください。
class User < ApplicationRecord
# ...
def self.ransackable_attributes(auth_object = nil)
# ここにcompany_nameと書くのは間違い!
auth_object ? super : %w(name nickname company_name)
end
def self.ransackable_associations(auth_object = nil)
auth_object ? super : %w(company)
end
end
並び替えは見過ごされやすいので注意
ransackable_attributes
に定義し忘れた列をsearch_field
メソッドに渡すと、画面を開いたタイミングで
NoMethodError: undefined method `nickname_cont' for Ransack::Search...
というようなエラーが発生するので比較的すぐに気づけます。
ところが、sort_link
の場合は定義し忘れた列を渡してもエラーが発生しません。ソート用のリンクをクリックしてもソートが機能しないだけでエラーは発生しません。
よって「ついうっかり並び替えで使う列をransackable_attributes
に定義し忘れた」という問題が起きやすいので十分注意してください。
なお、Ransackには「想定外の検索条件を渡されたら例外を発生させるオプション」が用意されているのですが、なぜかこのオプションは並び替えには適用されません(例外が発生しない。ransack!
メソッドを使った場合も同様)。この問題についてissueが作られていますが、2023年7月現在ではまだ何も対応されていないようです。
その他の注意点
- 上のコード例では単純に「管理者か、一般ユーザーか」という2つの選択肢がありませんが、プロジェクトによってはもっと細かく検索可能な列を切り替える必要があるかもしれません。
-
ransackable_attributes
やransackable_associations
は要素が文字列の配列を返す必要があります。%i()
のようにシンボルの配列を返すとうまく動かないので注意してください。 -
ransackable_attributes
やransackable_associations
の詳細な設定方法はRansackの公式ドキュメントを参考にしてください。
テストを実行する
修正が終わったらテストコードを実行して問題なくテストがパスすることを確認してください。これはもちろん、Ransackを使っている画面のシステムテスト(or システムスペック)が事前に用意されていることが前提です。
可能であればsimplecovのようなカバレッジツールを使ってransack
メソッドを使っているコントローラがすべてテストコードによってテストされているかどうかを確認してください。もしテストが書かれていなければ手動テストが必要です(そしてこのタイミングでテストコードも追加しておきましょう!!)。
おまけ
<%= f.search_field :name_or_nickname_cont %>
や<%= f.search_field :name_or_nickname_eq %>
のようなsearch_field
の引数があった場合、以下のようなメソッドで列名(name
とnickname
)を抽出できます(irbやpryで実行してください)。
def extract(str)
str.sub(/_(cont|eq)$/, '').split('_or_').join(' ')
end
extract 'name_or_nickname_cont' #=> "name nickname"
_cont
や_eq
以外の条件を使っている場合は(cont|eq|lteq)
のようにOR条件を増やしてください。