Skip to content

Instantly share code, notes, and snippets.

@tomasc
Created December 30, 2020 09:00
Show Gist options
  • Save tomasc/52652bd6fcacdf0d03ecc5ad3c6c298d to your computer and use it in GitHub Desktop.
Save tomasc/52652bd6fcacdf0d03ecc5ad3c6c298d to your computer and use it in GitHub Desktop.
CanCan custom ActiveRecord Adapter
# frozen_string_literal: true
module CanCan
module ModelAdapters
class CustomActiveRecordAdapter < AbstractAdapter
def self.inherited(subclass)
@subclasses ||= []
@subclasses.insert(0, subclass)
end
def self.adapter_class(model_class)
@subclasses.detect { |subclass| subclass.for_class?(model_class) } || DefaultAdapter
end
# Used to determine if the given adapter should be used for the passed in class.
def self.for_class?(model_class)
model_class <= ActiveRecord::Base
end
# Override if you need custom find behavior
def self.find(model_class, id)
model_class.find(id)
end
# Used to determine if this model adapter will override the matching behavior for a hash of conditions.
# If this returns true then matches_conditions_hash? will be called. See Rule#matches_conditions_hash
def self.override_conditions_hash_matching?(subject, conditions)
false
end
# Override if override_conditions_hash_matching? returns true
# TODO: override for conditions in locks (do not need that at the moment)
def self.matches_conditions_hash?(subject, conditions)
raise NotImplemented, "This model adapter does not support matching on a conditions hash."
end
# Used to determine if this model adapter will override the matching behavior for a specific condition.
# If this returns true then matches_condition? will be called. See Rule#matches_conditions_hash
def self.override_condition_matching?(subject, name, value)
false
end
# Override if override_condition_matching? returns true
def self.matches_condition?(subject, name, value)
raise NotImplemented, "This model adapter does not support matching on a specific condition."
end
def initialize(model_class, rules)
@model_class = model_class
@rules = rules
end
def database_records
or_conditions = [subject_type_conditions, *open_conditions].compact.reduce(:or)
and_conditions = [or_conditions, *closed_conditions].compact.reduce(:and)
if sti_class?
if and_conditions.present?
# the and_conditions already include subject_type_conditions controlling
# whether class is accessible or not
@model_class.unscoped.where(and_conditions)
else
# the records are not accessible unless specified otherwise
@model_class.none
end
else
if and_conditions.present?
@model_class.unscoped.where(and_conditions)
else
open_subject_types.include?(@model_class) ? @model_class.unscoped : @model_class.none
end
end
end
private
def has_any_conditions?
subject_type_conditions.present? ||
open_conditions.present? ||
closed_conditions.present?
end
def subject_types
@subject_types ||= begin
root_cls = @model_class.base_class
[root_cls, *root_cls.descendants].compact
end
end
def open_subject_types
@open_subject_types ||= begin
subject_types.inject(Set[]) do |res, cls|
subject_type_rules_for(cls).each do |rule|
cls_list = [cls, *cls.descendants].compact
rule.base_behavior ? res += cls_list : res -= cls_list
end
res.to_a
end
end
end
def closed_subject_types
@closed_subject_types ||= subject_types - open_subject_types
end
def subject_type_conditions
return unless sti_class?
@model_class.arel_table[type_key].in(open_subject_types)
end
def open_conditions
@open_conditions ||= begin
condition_rules.select(&:base_behavior).each_with_object({}) do |rule, res|
rule.conditions.each do |k, v|
res[k] ||= []
res[k] << v
end
end.map { |key, values| @model_class.arel_table[key].in(values) }
end
end
def closed_conditions
@closed_conditions ||= begin
condition_rules.reject(&:base_behavior).each_with_object({}) do |rule, res|
rule.conditions.each do |k, v|
res[k] ||= []
res[k] << v
end
end.map { |key, values| @model_class.arel_table[key].not_in(values) }
end
end
def subject_type_rules_for(subject_type)
subject_type_rules.select do |rule|
rule.subjects.include?(subject_type)
end
end
def subject_type_rules
@rules.reject { |rule| rule.conditions.present? }
end
def condition_rules
@rules.select { |rule| rule.conditions.present? }
end
def id_key
@model_class.primary_key
end
def type_key
@model_class.inheritance_column
end
def sti_class?
@model_class.column_names.include?(type_key)
end
end
end
end
ActiveSupport.on_load(:active_record) do
send :include, CanCan::ModelAdditions
end
@tomasc
Copy link
Author

tomasc commented Dec 30, 2020

In Ability class override the model adapter method:

    def model_adapter(model_class, action)
      adapter_class = CanCan::ModelAdapters::AbstractAdapter.adapter_class(model_class)

      # include all rules that apply for descendants as well
      # so the adapter can exclude include subclasses from critieria
      rules = ([model_class] + model_class.descendants).inject([]) do |res, cls|
        res += relevant_rules_for_query(action, cls)
        res.uniq
      end

      adapter_class.new(model_class, rules)
    end

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment