Ransack 4.0 allowlist changes
In February 2023, Ransack 4.0 was released. One highly significant change in this version is the requirement to define fields and associations that will be visible to Ransack. Without these definitions, exceptions occur in places related to Ransack models, for example:
RuntimeError - Ransack needs ExampleModel attributes explicitly allow listed as searchable. Define a ransackable_attributes class method in your ExampleModel model, watching out for items you DON'T want searchable (for example, encrypted_password, password_reset_token, owner or other sensitive information)
In previous versions, operations could be performed by default across all fields in the model and defined ransackers and associations.
# Previous default behavior of Ransack
def ransackable_attributes(auth_object = nil)
column_names + ransackers.keys
end
def ransackable_associations(auth_object = nil)
reflect_on_all_associations.map { |a| a.name.to_s }
end
As of today (20th July 2023), the official documentation for Ransack still describes the previous flow. Therefore, if you plan to update to version 4.0, I would like to share my idea for solving the problem.
Personally, I was not fond of the vision of having to define methods in each model associated with Ransack. Therefore, I approached the issue differently and defined the following concern:
module Ransackable
extend ActiveSupport::Concern
class_methods do
def ransackable_attributes(auth_object = nil)
return (column_names + ransackers.keys) if auth_object == :admin
const_defined?('RANSACK_ATTRIBUTES') ? self::RANSACK_ATTRIBUTES : []
end
def ransackable_associations(auth_object = nil)
return reflect_on_all_associations.map { |a| a.name.to_s } if auth_object == :admin
const_defined?('RANSACK_ASSOCIATIONS') ? self::RANSACK_ASSOCIATIONS : []
end
end
end
Both for attributes and associations, I wanted access to the "whole" to be restricted to administrators (of course, this can be extended to other permissions). Everyone else, except for administrators, only has access to fields and associations defined in constants called RANSACK_ATTRIBUTES and RANSACK_ASSOCIATIONS. If we don't define constants, then no field or association will be visible to Ransack.
Example usage of the concern:
class ExampleModel < ApplicationRecord
include Ransackable
RANSACK_ATTRIBUTES = %w[example_model_field_1 example_model_field_2].freeze
RANSACK_ASSOCIATIONS = %w[second_example_models].freeze
has_many :second_example_models
end
class SecondExampleModel < ApplicationRecord
include Ransackable
RANSACK_ATTRIBUTES = %w[second_example_model_field].freeze
belongs_to :example_model
end
Sample usage in the controller:
def index
query = ExampleModel.ransack(params[:q], auth_object: :admin)
@result = query.result
end
Another way to define these methods could be an approach like "allow everything except XYZ." However, personally, I believe that this approach carries the risk that for example, someone might add a new field to the model, which we might not necessarily want to expose to Ransack.
If you have just updated to version 4.0 and want to gradually define the list of available attributes and associations in your models, you can always define the required methods in the ApplicationRecord file:
class ApplicationRecord < ActiveRecord::Base
class << self
def ransackable_attributes(auth_object = nil)
if Ransack::SUPPORTS_ATTRIBUTE_ALIAS
column_names + ransackers.keys + ransack_aliases.keys + attribute_aliases.keys
else
column_names + ransackers.keys + ransack_aliases.keys
end.uniq
end
def ransackable_associations(auth_object = nil)
reflect_on_all_associations.map { |a| a.name.to_s }
end
end
end
However, I would not recommend this approach and would suggest adapting the project to the new flow to protect your applications from the issues described in this article: https://younes.codes/posts/how-to-hack-with-ransack
Hope it helps!