1

I'm working on a project that has Cue model that could be accessed and searched by multiple roles like guest and admin. The authorization happens using CanCanCan gem based on some logic written in the Ability class. While the search happens using Pagy/MeiliSearch combination to do the full text search and the pagination. The issue I'm facing now is when I search as a guest, I can see the correct items a guest should see, and when I search as an admin, I can see the correct item an admin should see, BUT Pagy gem counts are incorrect. This is my code:

class SearchesController < ApplicationController
  INCLUDES = %i[medium speakers].freeze

  include Pagy::Backend

  authorize_resource class: false

  def search
    filter = ''
    filter = "speaker_ids IN [#{params[:speaker]}]" if params[:speaker].present?

    search_results = Cue.accessible_by(current_ability).includes(INCLUDES).pagy_search(params[:query], filter:)
    @pagy, @search_results = pagy_meilisearch(search_results)
  end
end

If there are some cues not accessible by a guest, the search will not return them in @search_results, but the @pagy object will count them in. This shows incorrect counts to the user and also shows less than the required Pagy::DEFAULT[:items]. I tried a lot of options, but with no success. The only thing I can think about (And I don't want to go this path TBH) is to add the filtering logic itself into the filter string and add the required parameters to the index. But this will be complicated and it will increase the index size. Do you have any thoughts?

AliOsm
  • 541
  • 2
  • 6
  • 20
  • 1
    I am not familiar with `meilisearch` but are you sure that adding user provided parameters with string interpolation to the query doesn't make your application vulnerable to SQL injection attacks? Because if you did that with the Rails query language, then that would allow SQL injection. – spickermann Apr 28 '23 at 15:52
  • Thanks for your note, I will take a look into it, but I don't think it will make any issue as this filter will be used to request the MeiliSearch index only, and it will not be sent to the application DB itself. Any idea about the main issue mentioned in the question? – AliOsm Apr 28 '23 at 16:01

2 Answers2

0
just hint

when search in meilisearch not read direct what current ability in CanCan , can handle in application_record to get all CanCan Ability and overwrite in https://github.com/meilisearch/meilisearch-rails/blob/5d4ac6871a8e89db06fdd1c6d765361b86c47eb6/playground/config/initializers/pagy.rb#L10

this as you see we don't call can can

can in app/models/application_recored.rb

def ms_search(query, params = {})
   # cancan_params should here merge cancan_params and get params
   # to search with cancan params
    params.merge!(cancan_params)
    super(query, params)
end
    
0

Thanks @abdelrahman-haider for the hint, it was useful.

Basically, we can't solve this issue unless we implement the filtration logic inside the MeiliSearch index itself. So, what I did is extracting the filtration conditions using the CanCan ability and convert it to MeiliSearch filtering string manually.

To get the CanCan ability conditions for a specific model on a specific controller action, we can do the following:

cue_conditions = current_ability.model_adapter(Cue, :show).conditions

Then, we can pass the cue_conditions to another method to convert them to MeiliSearch filtering string like the following method:

def ability_conditions_to_meilisearch_filter(condition)
  condition.map do |key, value|
    case value
    when Array then "#{key} IN [#{value.join(',')}]"
    when NilClass then "#{key} NOT EXISTS"
    when TrueClass, FalseClass then "#{key} = #{value}"
    when Hash then hash_condition_to_filter(key, value)
    else "#{key} = '#{value}'"
    end
  end.join(' AND ')
end

def hash_condition_to_filter(key, value)
  case value.keys[0]
  when :gt then "#{key} > #{value[:gt]}"
  when :gte then "#{key} >= #{value[:gte]}"
  when :lt then "#{key} < #{value[:lt]}"
  when :lte then "#{key} <= #{value[:lte]}"
  when :to then "#{key} #{value[:to].first} TO #{key} #{value[:to].last}"
  end
end

The ability_conditions_to_meilisearch_filter method implementation is not the best implementation, but it fulfills my requirements for now, and I will try to improve it later.

Note that you need to add the required attributes to your MeiliSearch index.

I hope this is helpful, as I didn't find anything related on the internet :) If you want to support, I submitted a feature request to meilisearch-rails GitHub repo: https://github.com/meilisearch/meilisearch-rails/issues/255.

Finally, thanks @abdelrahman-haider and poe.com for the help :3

AliOsm
  • 541
  • 2
  • 6
  • 20