CanCanCommunity / cancancan

The authorization Gem for Ruby on Rails.
MIT License
5.54k stars 633 forks source link

Add own SQL strategy #844

Open 23tux opened 2 months ago

23tux commented 2 months ago

Steps to reproduce

I want to implement my own SQL strategy to fight some performance issues regarding subqueries and distinct keywords in the left_join strategy. My approach is, to skip the distinct as along as the all joins are belongs_to or has_one. This way, the query will run in 1/30 of the time on my system.

However, adding a custom strategy requires some monkey patching, which is quiet ugly:

# config/initializers/cancancan.rb
CanCan.module_eval do
  class << self
    alias _valid_accessible_by_strategies valid_accessible_by_strategies
    def valid_accessible_by_strategies
      _valid_accessible_by_strategies + [CanCanLeftJoinOptimizedWithFallback::STRATEGY_NAME]
    end
  end
end

Rails.configuration.to_prepare { CanCanLeftJoinOptimizedWithFallback.install }
# app/lib/can_can_left_join_optimized_with_fallback.rb
class CanCanLeftJoinOptimizedWithFallback < CanCan::ModelAdapters::Strategies::Base
  STRATEGY_NAME = name.delete_prefix("CanCan").underscore.to_sym
  class << self
    def install
      const = STRATEGY_NAME.to_s.camelize.to_sym
      if CanCan::ModelAdapters::Strategies.const_defined?(const)
        CanCan::ModelAdapters::Strategies.send(:remove_const, const)
      end
      CanCan::ModelAdapters::Strategies.const_set(const, self)
      CanCan.accessible_by_strategy = STRATEGY_NAME
    end
  end

  def execute!
    # ...
  end
end

Expected behavior

It would be nice to add a custom strategy like this:

Rails.configuration.to_prepare do
  CanCan.accessible_by_strategy = CanCanLeftJoinOptimizedWithFallback
end

The setter could recognize if the argument is a class or a symbol and add it to the allowed strategies. It would also have to be compatible with Zeitwerks, so when the to_prepare hook from Rails is triggered during development, the constant is removed and added again like I do in my .install method.

I'll be happy to try for a PR, but I would need some guidance on how this could be implemented.

Actual behavior

> CanCan.accessible_by_strategy = CanCanLeftJoinOptimizedWithFallback
ArgumentError: accessible_by_strategy must be one of left_join, joined_alias_exists_subquery, joined_alias_each_rule_as_exists_subquery, subquery

System configuration

Rails version: 7.0.8.1

Ruby version: 3.2.1

CanCanCan version 3.5.0

coorasse commented 1 month ago

I am very happy to accept a PR for this feature 👍 I don't have much guidance to give, but I am happy to review the PR