3
\$\begingroup\$

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
\$\endgroup\$

0

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.