From cef6c21232aafdfd43c188c00fc0b4f3f274288a Mon Sep 17 00:00:00 2001 From: Ryan Bates Date: Tue, 4 Jan 2011 11:43:41 -0800 Subject: [PATCH] allow model adapter to override condition hash matching in Rule, also clean up Mongoid adapter and specs --- lib/cancan/model_adapters/abstract_adapter.rb | 11 ++ lib/cancan/model_adapters/mongoid_adapter.rb | 35 ++--- lib/cancan/rule.rb | 38 ++++-- .../model_adapters/mongoid_adapter_spec.rb | 121 ++++++++---------- 4 files changed, 96 insertions(+), 109 deletions(-) diff --git a/lib/cancan/model_adapters/abstract_adapter.rb b/lib/cancan/model_adapters/abstract_adapter.rb index 4d99233..a765da4 100644 --- a/lib/cancan/model_adapters/abstract_adapter.rb +++ b/lib/cancan/model_adapters/abstract_adapter.rb @@ -15,6 +15,17 @@ module CanCan false # override in subclass 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 + def self.matches_conditions_hash?(subject, conditions) + raise NotImplemented, "This model adapter does not support matching on a conditions hash." + end + def initialize(model_class, rules) @model_class = model_class @rules = rules diff --git a/lib/cancan/model_adapters/mongoid_adapter.rb b/lib/cancan/model_adapters/mongoid_adapter.rb index 1ec6556..ced2a41 100644 --- a/lib/cancan/model_adapters/mongoid_adapter.rb +++ b/lib/cancan/model_adapters/mongoid_adapter.rb @@ -5,6 +5,14 @@ module CanCan model_class <= Mongoid::Document end + def self.override_conditions_hash_matching?(subject, conditions) + conditions.any? { |k,v| !k.kind_of?(Symbol) } + end + + def self.matches_conditions_hash?(subject, conditions) + subject.class.where(conditions).include?(subject) # just use Mongoid's where function + end + def database_records @model_class.where(conditions) end @@ -23,33 +31,6 @@ module CanCan end end end - - # customize to handle Mongoid queries in ability definitions conditions - # Mongoid Criteria are simpler to check than normal conditions hashes - # When no conditions are given, true should be returned. - # The default CanCan behavior relies on the fact that conditions.all? will return true when conditions is empty - # The way ruby handles all? for empty hashes can be unexpected: - # {}.all?{|a| a == 5} - # => true - # {}.all?{|a| a != 5} - # => true - class Rule - def matches_conditions_hash_with_mongoid_subject?(subject, conditions = @conditions) - if defined?(::Mongoid) && subject.class.include?(::Mongoid::Document) && conditions.any?{|k,v| !k.kind_of?(Symbol)} - if conditions.empty? - true - else - subject.class.where(conditions).include?(subject) # just use Mongoid's where function - end - else - matches_conditions_hash_without_mongoid_subject? subject, conditions - end - end - - # could use alias_method_chain, but it's not worth adding activesupport as a gem dependency - alias_method :matches_conditions_hash_without_mongoid_subject?, :matches_conditions_hash? - alias_method :matches_conditions_hash?, :matches_conditions_hash_with_mongoid_subject? - end end # simplest way to add `accessible_by` to all Mongoid Documents diff --git a/lib/cancan/rule.rb b/lib/cancan/rule.rb index 3a1d02c..7da0075 100644 --- a/lib/cancan/rule.rb +++ b/lib/cancan/rule.rb @@ -88,19 +88,31 @@ module CanCan @subjects.any? { |sub| sub.kind_of?(Module) && (subject.kind_of?(sub) || subject.class.to_s == sub.to_s || subject.kind_of?(Module) && subject.ancestors.include?(sub)) } end + # Checks if the given subject matches the given conditions hash. + # This behavior can be overriden by a model adapter by defining two class methods: + # override_matching_for_conditions?(subject, conditions) and + # matches_conditions_hash?(subject, conditions) def matches_conditions_hash?(subject, conditions = @conditions) - conditions.all? do |name, value| - attribute = subject.send(name) - if value.kind_of?(Hash) - if attribute.kind_of? Array - attribute.any? { |element| matches_conditions_hash? element, value } - else - matches_conditions_hash? attribute, value - end - elsif value.kind_of?(Array) || value.kind_of?(Range) - value.include? attribute + if conditions.empty? + true + else + if model_adapter(subject).override_conditions_hash_matching? subject, conditions + model_adapter(subject).matches_conditions_hash? subject, conditions else - attribute == value + conditions.all? do |name, value| + attribute = subject.send(name) + if value.kind_of?(Hash) + if attribute.kind_of? Array + attribute.any? { |element| matches_conditions_hash? element, value } + else + matches_conditions_hash? attribute, value + end + elsif value.kind_of?(Array) || value.kind_of?(Range) + value.include? attribute + else + attribute == value + end + end end end end @@ -117,5 +129,9 @@ module CanCan @block.call(action, subject.class, subject, *extra_args) end end + + def model_adapter(subject) + ModelAdapters::AbstractAdapter.adapter_class(subject_class?(subject) ? subject : subject.class) + end end end diff --git a/spec/cancan/model_adapters/mongoid_adapter_spec.rb b/spec/cancan/model_adapters/mongoid_adapter_spec.rb index 4ea0441..61b96ef 100644 --- a/spec/cancan/model_adapters/mongoid_adapter_spec.rb +++ b/spec/cancan/model_adapters/mongoid_adapter_spec.rb @@ -18,31 +18,8 @@ if ENV["MODEL_ADAPTER"] == "mongoid" end describe CanCan::ModelAdapters::MongoidAdapter do - context "Mongoid not defined" do - before(:all) do - @mongoid_class = Object.send(:remove_const, :Mongoid) - end - - after(:all) do - Object.const_set(:Mongoid, @mongoid_class) - end - - it "should not raise an error for ActiveRecord models" do - @model_class = Class.new(Project) - stub(@model_class).scoped { :scoped_stub } - @ability = Object.new - @ability.extend(CanCan::Ability) - - @ability.can :read, @model_class - lambda { - @ability.can? :read, @model_class.new - }.should_not raise_error - end - end - context "Mongoid defined" do before(:each) do - @model_class = MongoidProject @ability = Object.new @ability.extend(CanCan::Ability) end @@ -55,121 +32,123 @@ if ENV["MODEL_ADAPTER"] == "mongoid" it "should be for only Mongoid classes" do CanCan::ModelAdapters::MongoidAdapter.should_not be_for_class(Object) - CanCan::ModelAdapters::MongoidAdapter.should be_for_class(@model_class) - CanCan::ModelAdapters::AbstractAdapter.adapter_class(@model_class).should == CanCan::ModelAdapters::MongoidAdapter + CanCan::ModelAdapters::MongoidAdapter.should be_for_class(MongoidProject) + CanCan::ModelAdapters::AbstractAdapter.adapter_class(MongoidProject).should == CanCan::ModelAdapters::MongoidAdapter end it "should compare properties on mongoid documents with the conditions hash" do - model = @model_class.new - @ability.can :read, @model_class, :id => model.id - @ability.should be_able_to :read, model + model = MongoidProject.new + @ability.can :read, MongoidProject, :id => model.id + @ability.should be_able_to(:read, model) end it "should return [] when no ability is defined so no records are found" do - @model_class.create :title => 'Sir' - @model_class.create :title => 'Lord' - @model_class.create :title => 'Dude' + MongoidProject.create(:title => 'Sir') + MongoidProject.create(:title => 'Lord') + MongoidProject.create(:title => 'Dude') - @model_class.accessible_by(@ability, :read).entries.should == [] + MongoidProject.accessible_by(@ability, :read).entries.should == [] end it "should return the correct records based on the defined ability" do - @ability.can :read, @model_class, :title => "Sir" - sir = @model_class.create :title => 'Sir' - lord = @model_class.create :title => 'Lord' - dude = @model_class.create :title => 'Dude' + @ability.can :read, MongoidProject, :title => "Sir" + sir = MongoidProject.create(:title => 'Sir') + lord = MongoidProject.create(:title => 'Lord') + dude = MongoidProject.create(:title => 'Dude') - @model_class.accessible_by(@ability, :read).should == [sir] + MongoidProject.accessible_by(@ability, :read).should == [sir] end it "should return everything when the defined ability is manage all" do @ability.can :manage, :all - sir = @model_class.create :title => 'Sir' - lord = @model_class.create :title => 'Lord' - dude = @model_class.create :title => 'Dude' + sir = MongoidProject.create(:title => 'Sir') + lord = MongoidProject.create(:title => 'Lord') + dude = MongoidProject.create(:title => 'Dude') - @model_class.accessible_by(@ability, :read).entries.should == [sir, lord, dude] + MongoidProject.accessible_by(@ability, :read).entries.should == [sir, lord, dude] end describe "Mongoid::Criteria where clause Symbol extensions using MongoDB expressions" do it "should handle :field.in" do - obj = @model_class.create :title => 'Sir' - @ability.can :read, @model_class, :title.in => ["Sir", "Madam"] + obj = MongoidProject.create(:title => 'Sir') + @ability.can :read, MongoidProject, :title.in => ["Sir", "Madam"] @ability.can?(:read, obj).should == true - @model_class.accessible_by(@ability, :read).should == [obj] + MongoidProject.accessible_by(@ability, :read).should == [obj] - obj2 = @model_class.create :title => 'Lord' + obj2 = MongoidProject.create(:title => 'Lord') @ability.can?(:read, obj2).should == false end describe "activates only when there are Criteria in the hash" do it "Calls where on the model class when there are criteria" do - obj = @model_class.create :title => 'Bird' + obj = MongoidProject.create(:title => 'Bird') @conditions = {:title.nin => ["Fork", "Spoon"]} - mock(@model_class).where(@conditions) {[obj]} - @ability.can :read, @model_class, @conditions + mock(MongoidProject).where(@conditions) {[obj]} + @ability.can :read, MongoidProject, @conditions @ability.should be_able_to(:read, obj) end it "Calls the base version if there are no mongoid criteria" do - obj = @model_class.new :title => 'Bird' + obj = MongoidProject.new(:title => 'Bird') @conditions = {:id => obj.id} - @ability.can :read, @model_class, @conditions + @ability.can :read, MongoidProject, @conditions @ability.should be_able_to(:read, obj) end end it "should handle :field.nin" do - obj = @model_class.create :title => 'Sir' - @ability.can :read, @model_class, :title.nin => ["Lord", "Madam"] + obj = MongoidProject.create(:title => 'Sir') + @ability.can :read, MongoidProject, :title.nin => ["Lord", "Madam"] @ability.can?(:read, obj).should == true - @model_class.accessible_by(@ability, :read).should == [obj] + MongoidProject.accessible_by(@ability, :read).should == [obj] - obj2 = @model_class.create :title => 'Lord' + obj2 = MongoidProject.create(:title => 'Lord') @ability.can?(:read, obj2).should == false end it "should handle :field.size" do - obj = @model_class.create :titles => ['Palatin', 'Margrave'] - @ability.can :read, @model_class, :titles.size => 2 + obj = MongoidProject.create(:titles => ['Palatin', 'Margrave']) + @ability.can :read, MongoidProject, :titles.size => 2 @ability.can?(:read, obj).should == true - @model_class.accessible_by(@ability, :read).should == [obj] + MongoidProject.accessible_by(@ability, :read).should == [obj] - obj2 = @model_class.create :titles => ['Palatin', 'Margrave', 'Marquis'] + obj2 = MongoidProject.create(:titles => ['Palatin', 'Margrave', 'Marquis']) @ability.can?(:read, obj2).should == false end it "should handle :field.exists" do - obj = @model_class.create :titles => ['Palatin', 'Margrave'] - @ability.can :read, @model_class, :titles.exists => true + obj = MongoidProject.create(:titles => ['Palatin', 'Margrave']) + @ability.can :read, MongoidProject, :titles.exists => true @ability.can?(:read, obj).should == true - @model_class.accessible_by(@ability, :read).should == [obj] + MongoidProject.accessible_by(@ability, :read).should == [obj] - obj2 = @model_class.create + obj2 = MongoidProject.create @ability.can?(:read, obj2).should == false end it "should handle :field.gt" do - obj = @model_class.create :age => 50 - @ability.can :read, @model_class, :age.gt => 45 + obj = MongoidProject.create(:age => 50) + @ability.can :read, MongoidProject, :age.gt => 45 @ability.can?(:read, obj).should == true - @model_class.accessible_by(@ability, :read).should == [obj] + MongoidProject.accessible_by(@ability, :read).should == [obj] - obj2 = @model_class.create :age => 40 + obj2 = MongoidProject.create(:age => 40) @ability.can?(:read, obj2).should == false end end it "should call where with matching ability conditions" do - obj = @model_class.create :foo => {:bar => 1} - @ability.can :read, @model_class, :foo => {:bar => 1} - @model_class.accessible_by(@ability, :read).entries.first.should == obj + obj = MongoidProject.create(:foo => {:bar => 1}) + @ability.can :read, MongoidProject, :foo => {:bar => 1} + MongoidProject.accessible_by(@ability, :read).entries.first.should == obj end it "should not allow to fetch records when ability with just block present" do - @ability.can :read, @model_class do false end + @ability.can :read, MongoidProject do + false + end lambda { - @model_class.accessible_by(@ability) + MongoidProject.accessible_by(@ability) }.should raise_error(CanCan::Error) end end