allow model adapter to override condition hash matching in Rule, also clean up Mongoid adapter and specs
This commit is contained in:
		
							parent
							
								
									685e926d96
								
							
						
					
					
						commit
						cef6c21232
					
				| @ -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 | ||||
|  | ||||
| @ -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 | ||||
|  | ||||
| @ -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 | ||||
|  | ||||
| @ -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 | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 Ryan Bates
						Ryan Bates