We are using Searchkick to run our elasticsearch and Pundit to authorize users for specific actions. Below is our module SearchkickScope that does it's best to scope results to those found via the policy_scope supplied by Pundit. Since this is a security related module, I would love some outside opinions on this implementation.
# frozen_string_literal: true
# This allows searchkick to be easily implemented in a controller action
# while scoping the search to respect the current user's permissions via
# the pundit policy. It does so by grabbing all of the resource ids from the
# policy scope and then adding a where clause to the search query.
module SearchKickScope
extend ActiveSupport::Concern
# Creates a search query that respects a policy scope.
# @example
# def index
# search(Lead)
# end
# @param klass [Class] the class to search.
def search(klass)
@klass = klass
@search = klass.search(search_query, **scoped_options)
render json: @search.results, meta: search_meta, status: :ok
end
private
# Return a query, if the q param is not present, return * for a wildcard
# search. This is to have consistent indexing and searching behavior more
# similar to a traditional index action.
# @return [String] the query.
def search_query
params[:q].presence || '*'
end
# Return the hash of the search options. These can be present in the
# opts param. This method is safe to use with strong parameters.
# @return [Hash] the search options.
def search_options
params[:opts] ? params[:opts].to_unsafe_h : {}
end
# Merges all other options with the scoped ids, ensuring that the search
# will only return records that are within the punditp policy scope.
# @return [Hash] the search options.
def scoped_options
opts = search_options
if opts[:where]&.[](:id).present?
opts[:where][:id] = scope_ids & opts[:where][:id]
else
opts[:where] ||= {}
opts[:where][:id] = scope_ids
end
opts.deep_symbolize_keys
end
# Return a meta object for searchkick queries that can be serialized
# and returned with the search results.
# @param search [Searchkick::Relation] the search results.
# @return [Hash] the meta object.
def search_meta
{
total: @search.size,
page: @search.current_page,
per_page: @search.per_page,
total_pages: @search.total_pages,
aggs: @search.aggs
}
end
# Returns the ids of the records that are in the current user's scope. Memoized.
# @return [Array<String>] the ids of the records in the current user's scope.
def scope_ids
@_pundit_policy_authorized = true # Override the authorization check because this is outside normal usage.
@scope_ids ||= policy_scope(@klass).pluck(:id)
end
end
Example Usage
class LeadsController < ApplicationController
include Pundit::Authorization
include SearchkickScope
def index
search(Lead)
end
end