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!