diff --git a/lib/cancan/can_definition.rb b/lib/cancan/can_definition.rb index 6f7a3ef..b81a8bc 100644 --- a/lib/cancan/can_definition.rb +++ b/lib/cancan/can_definition.rb @@ -21,6 +21,7 @@ module CanCan # Matches both the subject and action, not necessarily the conditions def relevant?(action, subject) + subject = subject.values.first if subject.kind_of? Hash @match_all || (matches_action?(action) && matches_subject?(subject)) end @@ -28,9 +29,11 @@ module CanCan def matches_conditions?(action, subject, extra_args) if @match_all call_block_with_all(action, subject, extra_args) - elsif @block && subject.class != Class + elsif @block && !subject_class?(subject) @block.call(subject, *extra_args) - elsif @conditions.kind_of?(Hash) && subject.class != Class + elsif @conditions.kind_of?(Hash) && subject.kind_of?(Hash) + nested_subject_matches_conditions?(subject) + elsif @conditions.kind_of?(Hash) && !subject_class?(subject) matches_conditions_hash?(subject) else @base_behavior @@ -66,6 +69,10 @@ module CanCan private + def subject_class?(subject) + (subject.kind_of?(Hash) ? subject.values.first : subject).class == Class + end + def matches_action?(action) @expanded_actions.include?(:manage) || @expanded_actions.include?(action) end @@ -95,6 +102,11 @@ module CanCan end end + def nested_subject_matches_conditions?(subject_hash) + parent, child = subject_hash.shift + matches_conditions_hash?(parent, @conditions[parent.class.name.downcase.to_sym]) + end + def call_block_with_all(action, subject, extra_args) if subject.class == Class @block.call(action, subject, nil, *extra_args) diff --git a/lib/cancan/controller_resource.rb b/lib/cancan/controller_resource.rb index 59be270..ec5a87a 100644 --- a/lib/cancan/controller_resource.rb +++ b/lib/cancan/controller_resource.rb @@ -32,7 +32,7 @@ module CanCan end def authorize_resource - @controller.authorize!(authorization_action, resource_instance || resource_class) + @controller.authorize!(authorization_action, resource_instance || resource_class_with_parent) end def parent? @@ -86,6 +86,10 @@ module CanCan end end + def resource_class_with_parent + parent_resource ? {parent_resource => resource_class} : resource_class + end + def resource_instance @controller.instance_variable_get("@#{instance_name}") end @@ -94,15 +98,15 @@ module CanCan # If the :through option is passed it will go through an association on that instance. # If the :singleton option is passed it won't use the association because it needs to be handled later. def resource_base - if through_resource - @options[:singleton] ? through_resource : through_resource.send(name.to_s.pluralize) + if parent_resource + @options[:singleton] ? parent_resource : parent_resource.send(name.to_s.pluralize) else resource_class end end # The object to load this resource through. - def through_resource + def parent_resource @options[:through] && [@options[:through]].flatten.map { |i| @controller.instance_variable_get("@#{i}") }.compact.first end diff --git a/spec/cancan/ability_spec.rb b/spec/cancan/ability_spec.rb index 224f724..4dac4d6 100644 --- a/spec/cancan/ability_spec.rb +++ b/spec/cancan/ability_spec.rb @@ -57,11 +57,11 @@ describe CanCan::Ability do end it "should pass only object for global manage actions" do - @ability.can :manage, Array do |object| - object.should == [1, 2] + @ability.can :manage, String do |object| + object.should == "foo" @block_called = true end - @ability.can?(:stuff, [1, 2]).should + @ability.can?(:stuff, "foo").should @block_called.should be_true end @@ -130,9 +130,9 @@ describe CanCan::Ability do end it "should be able to specify multiple classes and match any" do - @ability.can :update, [String, Array] + @ability.can :update, [String, Range] @ability.can?(:update, "foo").should be_true - @ability.can?(:update, []).should be_true + @ability.can?(:update, 1..3).should be_true @ability.can?(:update, 123).should be_false end @@ -210,44 +210,50 @@ describe CanCan::Ability do end it "should use conditions as third parameter and determine abilities from it" do - @ability.can :read, Array, :first => 1, :last => 3 - @ability.can?(:read, [1, 2, 3]).should be_true - @ability.can?(:read, [1, 2, 3, 4]).should be_false - @ability.can?(:read, Array).should be_true + @ability.can :read, Range, :begin => 1, :end => 3 + @ability.can?(:read, 1..3).should be_true + @ability.can?(:read, 1..4).should be_false + @ability.can?(:read, Range).should be_true end it "should allow an array of options in conditions hash" do - @ability.can :read, Array, :first => [1, 3, 5] - @ability.can?(:read, [1, 2, 3]).should be_true - @ability.can?(:read, [2, 3]).should be_false - @ability.can?(:read, [3, 4]).should be_true + @ability.can :read, Range, :begin => [1, 3, 5] + @ability.can?(:read, 1..3).should be_true + @ability.can?(:read, 2..4).should be_false + @ability.can?(:read, 3..5).should be_true end it "should allow a range of options in conditions hash" do - @ability.can :read, Array, :first => 1..3 - @ability.can?(:read, [1, 2, 3]).should be_true - @ability.can?(:read, [3, 4]).should be_true - @ability.can?(:read, [4, 5]).should be_false + @ability.can :read, Range, :begin => 1..3 + @ability.can?(:read, 1..10).should be_true + @ability.can?(:read, 3..30).should be_true + @ability.can?(:read, 4..40).should be_false end it "should allow nested hashes in conditions hash" do - @ability.can :read, Array, :first => { :length => 5 } - @ability.can?(:read, ["foo", "bar"]).should be_false - @ability.can?(:read, ["test1", "foo"]).should be_true + @ability.can :read, Range, :begin => { :to_i => 5 } + @ability.can?(:read, 5..7).should be_true + @ability.can?(:read, 6..8).should be_false end - it "should allow nested hash of arrays and match any element" do - @ability.can :read, Array, :first => { :to_i => 3 } - @ability.can?(:read, [[1, 2, 3]]).should be_true - @ability.can?(:read, [[4, 5, 6]]).should be_false + it "should match any element passed in to nesting if it's an array (for has_many associations)" do + @ability.can :read, Range, :to_a => { :to_i => 3 } + @ability.can?(:read, 1..5).should be_true + @ability.can?(:read, 4..6).should be_false end it "should not stop at cannot definition when comparing class" do - @ability.can :read, Array - @ability.cannot :read, Array, :first => 1 - @ability.can?(:read, [2, 3, 5]).should be_true - @ability.can?(:read, [1, 3, 5]).should be_false - @ability.can?(:read, Array).should be_true + @ability.can :read, Range + @ability.cannot :read, Range, :begin => 1 + @ability.can?(:read, 2..5).should be_true + @ability.can?(:read, 1..5).should be_false + @ability.can?(:read, Range).should be_true + end + + it "passing a hash of subjects should check permissions through association" do + @ability.can :read, Range, :string => {:length => 3} + @ability.can?(:read, "foo" => Range).should be_true + @ability.can?(:read, "foobar" => Range).should be_false end describe "unauthorized message" do diff --git a/spec/cancan/controller_resource_spec.rb b/spec/cancan/controller_resource_spec.rb index bc26c22..1683f70 100644 --- a/spec/cancan/controller_resource_spec.rb +++ b/spec/cancan/controller_resource_spec.rb @@ -161,6 +161,15 @@ describe CanCan::ControllerResource do @controller.instance_variable_get(:@ability).should == :some_ability end + it "should authorize nested resource through parent association on index action" do + @params.merge!(:action => "index") + person = Object.new + @controller.instance_variable_set(:@person, person) + stub(@controller).authorize!(:index, person => Ability) { raise CanCan::AccessDenied } + resource = CanCan::ControllerResource.new(@controller, :through => :person) + lambda { resource.authorize_resource }.should raise_error(CanCan::AccessDenied) + end + it "should load through first matching if multiple are given" do @params.merge!(:action => "show", :id => 123) person = Object.new diff --git a/spec/spec.opts b/spec/spec.opts index 4e1e0d2..a5faa1d 100644 --- a/spec/spec.opts +++ b/spec/spec.opts @@ -1 +1,2 @@ --color +--backtrace