diff --git a/lib/cancan/ability.rb b/lib/cancan/ability.rb index c4874de..0d953bc 100644 --- a/lib/cancan/ability.rb +++ b/lib/cancan/ability.rb @@ -53,9 +53,9 @@ module CanCan # end # # Also see the RSpec Matchers to aid in testing. - def can?(action, subject, *extra_args) - match = relevant_rules_for_match(action, subject).detect do |rule| - rule.matches_conditions?(action, subject, extra_args) + def can?(action, subject, attribute = nil) + match = relevant_rules_for_match(action, subject, attribute).detect do |rule| + rule.matches_conditions?(action, subject, attribute) end match ? match.base_behavior : false end @@ -121,8 +121,8 @@ module CanCan # # check the database and return true/false # end # - def can(action = nil, subject = nil, conditions = nil, &block) - rules << Rule.new(true, action, subject, conditions, block) + def can(*args, &block) + rules << Rule.new(true, *args, &block) end # Defines an ability which cannot be done. Accepts the same arguments as "can". @@ -137,8 +137,8 @@ module CanCan # product.invisible? # end # - def cannot(action = nil, subject = nil, conditions = nil, &block) - rules << Rule.new(false, action, subject, conditions, block) + def cannot(*args, &block) + rules << Rule.new(false, *args, &block) end # Alias one or more actions into another one. @@ -282,16 +282,16 @@ module CanCan # Returns an array of Rule instances which match the action and subject # This does not take into consideration any hash conditions or block statements - def relevant_rules(action, subject) + def relevant_rules(action, subject, attribute = nil) rules.reverse.select do |rule| rule.expanded_actions = expand_aliases(:actions, rule.actions) rule.expanded_subjects = expand_aliases(:subjects, rule.subjects) - rule.relevant? action, subject + rule.relevant? action, subject, attribute end end - def relevant_rules_for_match(action, subject) - relevant_rules(action, subject).each do |rule| + def relevant_rules_for_match(action, subject, attribute) + relevant_rules(action, subject, attribute).each do |rule| if rule.only_raw_sql? raise Error, "The can? and cannot? call cannot be used with a raw sql 'can' definition. The checking code cannot be determined for #{action.inspect} #{subject.inspect}" end @@ -299,7 +299,7 @@ module CanCan end def relevant_rules_for_query(action, subject) - relevant_rules(action, subject).each do |rule| + relevant_rules(action, subject, nil).each do |rule| if rule.only_block? raise Error, "The accessible_by call cannot be used with a block 'can' definition. The SQL cannot be determined for #{action.inspect} #{subject.inspect}" end diff --git a/lib/cancan/rule.rb b/lib/cancan/rule.rb index 769f006..9c22b27 100644 --- a/lib/cancan/rule.rb +++ b/lib/cancan/rule.rb @@ -10,28 +10,29 @@ module CanCan # value. True for "can" and false for "cannot". The next two arguments are the action # and subject respectively (such as :read, @project). The third argument is a hash # of conditions and the last one is the block passed to the "can" call. - def initialize(base_behavior, action, subject, conditions, block) - raise Error, "You are not able to supply a block with a hash of conditions in #{action} #{subject} ability. Use either one." if conditions.kind_of?(Hash) && !block.nil? + def initialize(base_behavior, action = nil, subject = nil, *extra_args, &block) @match_all = action.nil? && subject.nil? @base_behavior = base_behavior - @actions = [action].flatten.compact.map(&:to_sym) - @subjects = [subject].flatten.compact.map(&:to_sym) - @conditions = conditions || {} + @actions = [action].flatten + @subjects = [subject].flatten + @attributes = [extra_args.shift].flatten if extra_args.first.kind_of?(Symbol) || extra_args.first.kind_of?(Array) && extra_args.first.first.kind_of?(Symbol) + raise Error, "You are not able to supply a block with a hash of conditions in #{action} #{subject} ability. Use either one." if extra_args.first && !block.nil? + @conditions = extra_args.first || {} @block = block end - # Matches both the subject and action, not necessarily the conditions - def relevant?(action, subject) + # Matches the subject, action, and given attribute. Conditions are not checked here. + def relevant?(action, subject, attribute) subject = subject.values.first if subject.class == Hash - @match_all || (matches_action?(action) && matches_subject?(subject)) + @match_all || (matches_action?(action) && matches_subject?(subject) && matches_attribute?(attribute)) end # Matches the block or conditions hash - def matches_conditions?(action, subject, extra_args) + def matches_conditions?(action, subject, attribute) if @match_all - call_block_with_all(action, subject, extra_args) + call_block_with_all(action, subject, attribute) elsif @block && subject_object?(subject) - @block.call(subject, *extra_args) + @block.arity == 1 ? @block.call(subject) : @block.call(subject, attribute) elsif @conditions.kind_of?(Hash) && subject.class == Hash nested_subject_matches_conditions?(subject) elsif @conditions.kind_of?(Hash) && subject_object?(subject) @@ -87,6 +88,10 @@ module CanCan @expanded_subjects.include?(:all) || @expanded_subjects.include?(subject.to_sym) # || matches_subject_class?(subject) end + def matches_attribute?(attribute) + @attributes.nil? || attribute.nil? || @attributes.include?(attribute.to_sym) + end + # TODO deperecate this def matches_subject_class?(subject) @expanded_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)) } @@ -130,11 +135,11 @@ module CanCan matches_conditions_hash?(parent, @conditions[parent.class.name.downcase.to_sym] || {}) end - def call_block_with_all(action, subject, extra_args) + def call_block_with_all(action, subject, attribute) if subject_object? subject - @block.call(action, subject_name(subject), subject, *extra_args) + @block.call(action, subject_name(subject), subject, attribute) else - @block.call(action, subject, nil, *extra_args) + @block.call(action, subject, nil, attribute) end end diff --git a/spec/cancan/ability_spec.rb b/spec/cancan/ability_spec.rb index 68c3753..35a97fd 100644 --- a/spec/cancan/ability_spec.rb +++ b/spec/cancan/ability_spec.rb @@ -35,11 +35,9 @@ describe CanCan::Ability do @ability.can?(:paint, :cars).should be_false end - it "allows strings instead of symbols" do - @ability.can "paint", "fences" + it "allows strings instead of symbols in ability check" do + @ability.can :paint, :fences @ability.can?("paint", "fences").should be_true - @ability.can "access", "all" - @ability.can?("wax", "cars").should be_true end @@ -182,7 +180,7 @@ describe CanCan::Ability do @block_called.should be_true end - it "should raise an error when attempting to use a block with a hash condition since it's not likely what they want" do + it "raises an error when attempting to use a block with a hash condition since it's not likely what they want" do lambda { @ability.can :read, :ranges, :published => true do false @@ -191,6 +189,52 @@ describe CanCan::Ability do end + # Attributes + + it "allows permission on attributes" do + @ability.can :update, :users, :name + @ability.can :update, :users, [:email, :age] + @ability.can?(:update, :users, :name).should be_true + @ability.can?(:update, :users, :email).should be_true + @ability.can?(:update, :users, :password).should be_false + end + + it "allows permission on all attributes when none are given" do + @ability.can :update, :users + @ability.can?(:update, :users, :password).should be_true + end + + it "allows strings when chekcing attributes" do + @ability.can :update, :users, :name + @ability.can?(:update, :users, "name").should be_true + end + + it "combines attribute check with conditions hash" do + @ability.can :update, :ranges, :begin => 1 + @ability.can :update, :ranges, :name, :begin => 2 + @ability.can?(:update, 1..3, :foobar).should be_true + @ability.can?(:update, 2..4, :foobar).should be_false + @ability.can?(:update, 2..4, :name).should be_true + @ability.can?(:update, 3..5, :name).should be_false + end + + it "passes attribute to block and nil if no attribute checked" do + @ability.can :update, :ranges do |range, attribute| + attribute == :name + end + @ability.can?(:update, 1..3, :name).should be_true + @ability.can?(:update, 2..4).should be_false + end + + it "passes attribute to block for global can definition" do + @ability.can do |action, subject, object, attribute| + attribute == :name + end + @ability.can?(:update, 1..3, :name).should be_true + @ability.can?(:update, 2..4).should be_false + end + + # Cannot it "offers cannot? method which inverts can?" do @@ -259,7 +303,7 @@ describe CanCan::Ability do end end - it "should raise access denied exception with default message if not specified" do + it "raises access denied exception with default message if not specified" do begin @ability.authorize! :read, :books rescue CanCan::AccessDenied => e diff --git a/spec/cancan/rule_spec.rb b/spec/cancan/rule_spec.rb index c679eff..fddf588 100644 --- a/spec/cancan/rule_spec.rb +++ b/spec/cancan/rule_spec.rb @@ -4,7 +4,7 @@ require "spec_helper" describe CanCan::Rule do before(:each) do @conditions = {} - @rule = CanCan::Rule.new(true, :read, :integers, @conditions, nil) + @rule = CanCan::Rule.new(true, :read, :integers, @conditions) end it "should return no association joins if none exist" do @@ -33,7 +33,7 @@ describe CanCan::Rule do end it "should return no association joins if conditions is nil" do - rule = CanCan::Rule.new(true, :read, :integers, nil, nil) + rule = CanCan::Rule.new(true, :read, :integers) rule.associations_hash.should == {} end end