passing throw matching rules with not matching conditions

Main goal is to allow:

cannot :manage, :all
can :read, :all
can :manage, User, :id=>user.id
can :manage, User, :manager_id=>user.id

Signed-off-by: Sokolov Yura <funny.falcon@gmail.com>
This commit is contained in:
Sokolov Yura 2010-05-16 22:13:02 +04:00
parent 06296b0a40
commit 7d7d249182
5 changed files with 187 additions and 32 deletions

View File

@ -49,8 +49,12 @@ module CanCan
# #
def can?(action, subject, *extra_args) def can?(action, subject, *extra_args)
raise Error, "Nom nom nom. I eated it." if action == :has && subject == :cheezburger raise Error, "Nom nom nom. I eated it." if action == :has && subject == :cheezburger
can_definition = matching_can_definition(action, subject) matching_can_definition(action, subject) do |can_definition|
can_definition && can_definition.can?(action, subject, extra_args) unless (can = can_definition.can?(action, subject, extra_args)) == :_NOT_MATCHED
return can
end
end
false
end end
# Convenience method which works the same as "can?" but returns the opposite value. # Convenience method which works the same as "can?" but returns the opposite value.
@ -179,11 +183,15 @@ module CanCan
@aliased_actions = {} @aliased_actions = {}
end end
# Returns a hash of conditions which match the given ability. This is useful if you need to generate a database # Returns an array of arrays composing from desired action and hash of conditions which match the given ability.
# query based on the current ability. # This is useful if you need to generate a database query based on the current ability.
# #
# can :read, Article, :visible => true # can :read, Article, :visible => true
# conditions :read, Article # returns { :visible => true } # conditions :read, Article # returns [ [ true, { :visible => true } ] ]
#
# can :read, Article, :visible => true
# cannot :read, Article, :blocked => true
# conditions :read, Article # returns [ [ false, { :blocked => true } ], [ true, { :visible => true } ] ]
# #
# Normally you will not call this method directly, but instead go through ActiveRecordAdditions#accessible_by method. # Normally you will not call this method directly, but instead go through ActiveRecordAdditions#accessible_by method.
# #
@ -191,25 +199,74 @@ module CanCan
# If the ability is defined using a block then this will raise an exception since a hash of conditions cannot be # If the ability is defined using a block then this will raise an exception since a hash of conditions cannot be
# determined from that. # determined from that.
def conditions(action, subject) def conditions(action, subject)
can_definition = matching_can_definition(action, subject) matched = matching_can_definition(action, subject)
if can_definition unless matched.empty?
raise Error, "Cannot determine ability conditions from block for #{action.inspect} #{subject.inspect}" if can_definition.block if matched.any?{|can_definition| can_definition.conditions.nil? && can_definition.block }
can_definition.conditions || {} raise Error, "Cannot determine ability conditions from block for #{action.inspect} #{subject.inspect}"
end
matched.map{|can_definition|
[can_definition.base_behavior, can_definition.conditions]
}
else else
false false
end end
end end
# Returns sql conditions for object, which responds to :sanitize_sql .
# This is useful if you need to generate a database query based on the current ability.
#
# can :manage, User, :id => 1
# can :manage, User, :manager_id => 1
# cannot :manage, User, :self_managed => true
# sql_conditions :manage, User # returns not (self_managed = 't') AND ((manager_id = 1) OR (id = 1))
#
# Normally you will not call this method directly, but instead go through ActiveRecordAdditions#accessible_by method.
#
# If the ability is not defined then false is returned so be sure to take that into consideration.
# If there is just one :can ability, it conditions returned untouched.
# If the ability is defined using a block then this will raise an exception since a hash of conditions cannot be
# determined from that.
def sql_conditions(action, subject)
conds = conditions(action, subject)
return false if conds == false
return (conds[0][1] || {}) if conds.size==1 && conds[0][0] == true # to match previous spec
true_cond = subject.send(:sanitize_sql, ['?=?', true, true])
false_cond = subject.send(:sanitize_sql, ['?=?', true, false])
conds.reverse.inject(nil) do |sql, action|
behavior, condition = action
if condition
condition = "#{subject.send(:sanitize_sql, condition)}"
condition = "not (#{condition})" if behavior == false
else
condition = behavior ? true_cond : false_cond
end
case sql
when nil then condition
when true_cond
behavior ? true_cond : condition
when false_cond
behavior ? condition : false_cond
else
behavior ? "(#{condition}) OR (#{sql})" : "#{condition} AND (#{sql})"
end
end
end
# Returns the associations used in conditions. This is usually used in the :joins option for a search. # Returns the associations used in conditions. This is usually used in the :joins option for a search.
# See ActiveRecordAdditions#accessible_by for use in Active Record. # See ActiveRecordAdditions#accessible_by for use in Active Record.
def association_joins(action, subject) def association_joins(action, subject)
can_definition = matching_can_definition(action, subject) can_definitions = matching_can_definition(action, subject)
if can_definition unless can_definitions.empty?
raise Error, "Cannot determine association joins from block for #{action.inspect} #{subject.inspect}" if can_definition.block if can_definitions.any?{|can_definition| can_definition.conditions.nil? && can_definition.block }
can_definition.association_joins raise Error, "Cannot determine association joins from block for #{action.inspect} #{subject.inspect}"
end
collect_association_joins(can_definitions)
else
nil
end end
end end
private private
def can_definitions def can_definitions
@ -217,9 +274,18 @@ module CanCan
end end
def matching_can_definition(action, subject) def matching_can_definition(action, subject)
can_definitions.reverse.detect do |can_definition| if block_given?
can_definition.expand_actions(aliased_actions) can_definitions.reverse.each do |can_definition|
can_definition.matches? action, subject can_definition.expand_actions(aliased_actions)
if can_definition.matches? action, subject
yield can_definition
break if can_definition.conditions.nil? && can_definition.block.nil?
end
end
else
matched = []
matching_can_definition(action, subject){|can_definition| matched << can_definition}
matched
end end
end end
@ -230,5 +296,32 @@ module CanCan
:update => [:edit], :update => [:edit],
} }
end end
def collect_association_joins(can_definitions)
joins = []
can_definitions.each do |can_definition|
merge_association_joins(joins, can_definition.association_joins || [])
end
clear_association_joins(joins)
end
def merge_association_joins(what, with)
with.each do |join|
name, nested = join.each_pair.first
if at = what.detect{|h| h.has_key?(name) }
at[name] = merge_association_joins(at[name], nested)
else
what << join
end
end
end
def clear_association_joins(joins)
joins.map do |join|
name, nested = join.each_pair.first
nested.empty? ? name : {name => clear_association_joins(nested)}
end
end
end end
end end

View File

@ -20,7 +20,7 @@ module CanCan
# Here only the articles which the user can update are returned. This # Here only the articles which the user can update are returned. This
# internally uses Ability#conditions method, see that for more information. # internally uses Ability#conditions method, see that for more information.
def accessible_by(ability, action = :read) def accessible_by(ability, action = :read)
conditions = ability.conditions(action, self) || {:id => nil} conditions = ability.sql_conditions(action, self) || {:id => nil}
joins = ability.association_joins(action, self) joins = ability.association_joins(action, self)
if respond_to? :where if respond_to? :where
where(conditions).joins(joins) where(conditions).joins(joins)

View File

@ -1,7 +1,7 @@
module CanCan module CanCan
# This class is used internally and should only be called through Ability. # This class is used internally and should only be called through Ability.
class CanDefinition # :nodoc: class CanDefinition # :nodoc:
attr_reader :conditions, :block attr_reader :conditions, :block, :base_behavior, :definitive
def initialize(base_behavior, action, subject, conditions, block) def initialize(base_behavior, action, subject, conditions, block)
@base_behavior = base_behavior @base_behavior = base_behavior
@ -23,18 +23,19 @@ module CanCan
def can?(action, subject, extra_args) def can?(action, subject, extra_args)
result = can_without_base_behavior?(action, subject, extra_args) result = can_without_base_behavior?(action, subject, extra_args)
return :_NOT_MATCHED if result == :_NOT_MATCHED || !result
@base_behavior ? result : !result @base_behavior ? result : !result
end end
def association_joins(conditions = @conditions) def association_joins(conditions = @conditions)
joins = [] joins = []
conditions.each do |name, value| (conditions || []).each do |name, value|
if value.kind_of? Hash if value.kind_of? Hash
nested = association_joins(value) nested = association_joins(value)
if nested if nested
joins << {name => nested} joins << {name => nested}
else else
joins << name joins << {name => []}
end end
end end
end end
@ -54,8 +55,10 @@ module CanCan
def can_without_base_behavior?(action, subject, extra_args) def can_without_base_behavior?(action, subject, extra_args)
if @block if @block
call_block(action, subject, extra_args) call_block(action, subject, extra_args)
elsif @conditions && subject.class != Class elsif @conditions.kind_of?(Hash) && subject.class != Class
matches_conditions? subject matches_conditions?(subject)
elsif [true, false, :_NOT_MATCHES].include? @conditions
@conditions
else else
true true
end end

View File

@ -16,13 +16,27 @@ describe CanCan::Ability do
@ability.can?(:foodfight, String).should be_false @ability.can?(:foodfight, String).should be_false
end end
it "should return what block returns on a can call" do it "should return what block returns on a can call, except for nil and false" do
@ability.can :read, :all
@ability.can :read, Symbol do |sym|
[ sym ]
end
@ability.can?(:read, Symbol).should == [ nil ]
@ability.can?(:read, :some_symbol).should == [ :some_symbol ]
end
it "should pass to previous can definition, if block returns false or nil" do
@ability.can :read, :all @ability.can :read, :all
@ability.can :read, Symbol do |sym| @ability.can :read, Symbol do |sym|
sym sym
end end
@ability.can?(:read, Symbol).should be_nil @ability.can :read, Integer do |i|
i > 0
end
@ability.can?(:read, Symbol).should be_true
@ability.can?(:read, :some_symbol).should == :some_symbol @ability.can?(:read, :some_symbol).should == :some_symbol
@ability.can?(:read, 1).should be_true
@ability.can?(:read, -1).should be_true
end end
it "should pass class with object if :all objects are accepted" do it "should pass class with object if :all objects are accepted" do
@ -121,6 +135,30 @@ describe CanCan::Ability do
@ability.can?(:read, 123).should be_false @ability.can?(:read, 123).should be_false
end end
it "should pass to previous can definition, if block returns false or nil" do
#same as previous
@ability.can :read, :all
@ability.cannot :read, Integer do |int|
int > 10 ? nil : ( int > 5 )
end
@ability.can?(:read, "foo").should be_true
@ability.can?(:read, 3).should be_true
@ability.can?(:read, 8).should be_false
@ability.can?(:read, 123).should be_true
end
it "should pass to previous cannot definition, if block returns false or nil" do
@ability.cannot :read, :all
@ability.can :read, Integer do |int|
int > 10 ? nil : ( int > 5 )
end
@ability.can?(:read, "foo").should be_false
@ability.can?(:read, 3).should be_false
@ability.can?(:read, 10).should be_true
@ability.can?(:read, 123).should be_false
end
it "should append aliased actions" do it "should append aliased actions" do
@ability.alias_action :update, :to => :modify @ability.alias_action :update, :to => :modify
@ability.alias_action :destroy, :to => :modify @ability.alias_action :destroy, :to => :modify
@ -174,27 +212,48 @@ describe CanCan::Ability do
@ability.can?(:read, [[4, 5, 6]]).should be_false @ability.can?(:read, [[4, 5, 6]]).should be_false
end end
it "should return conditions for a given ability" do it "should return array of behavior and conditions for a given ability" do
@ability.can :read, Array, :first => 1, :last => 3 @ability.can :read, Array, :first => 1, :last => 3
@ability.conditions(:show, Array).should == {:first => 1, :last => 3} @ability.conditions(:show, Array).should == [[true, {:first => 1, :last => 3}]]
end end
it "should raise an exception when a block is used on condition" do it "should raise an exception when a block is used on condition, and no" do
@ability.can :read, Array do |a| @ability.can :read, Array do |a|
true true
end end
lambda { @ability.conditions(:show, Array) }.should raise_error(CanCan::Error, "Cannot determine ability conditions from block for :show Array") lambda { @ability.conditions(:show, Array) }.should raise_error(CanCan::Error, "Cannot determine ability conditions from block for :show Array")
end end
it "should return an empty hash for conditions when there are no conditions" do it "should return an array with just behavior for conditions when there are no conditions" do
@ability.can :read, Array @ability.can :read, Array
@ability.conditions(:show, Array).should == {} @ability.conditions(:show, Array).should == [ [true, nil] ]
end end
it "should return false when performed on an action which isn't defined" do it "should return false when performed on an action which isn't defined" do
@ability.conditions(:foo, Array).should == false @ability.conditions(:foo, Array).should == false
end end
it "should return appropriate sql conditions" do
obj = Class.new do
def self.sanitize_sql(hash_cond)
case hash_cond
when Hash then hash_cond.map{|name, value| "#{name}=#{value}"}
when Array
hash_cond.shift.gsub('?'){"#{hash_cond.shift.inspect}"}
when String then hash_cond
end
end
end
@ability.can :read, obj
@ability.can :manage, obj, :id => 1
@ability.can :update, obj, :manager_id => 1
@ability.cannot :update, obj, :self_managed => true
@ability.sql_conditions(:update, obj).should == 'not (self_managed=true) AND ((manager_id=1) OR (id=1))'
@ability.sql_conditions(:manage, obj).should == {:id=>1}
@ability.sql_conditions(:read, obj).should == 'true=true'
end
it "should has eated cheezburger" do it "should has eated cheezburger" do
lambda { lambda {
@ability.can? :has, :cheezburger @ability.can? :has, :cheezburger

View File

@ -17,7 +17,7 @@ describe CanCan::CanDefinition do
it "should return single association for joins" do it "should return single association for joins" do
@conditions[:foo] = {:bar => 1} @conditions[:foo] = {:bar => 1}
@can.association_joins.should == [:foo] @can.association_joins.should == [{:foo=>[]}]
end end
it "should return multiple associations for joins" do it "should return multiple associations for joins" do
@ -28,6 +28,6 @@ describe CanCan::CanDefinition do
it "should return nested associations for joins" do it "should return nested associations for joins" do
@conditions[:foo] = {:bar => {1 => 2}} @conditions[:foo] = {:bar => {1 => 2}}
@can.association_joins.should == [{:foo => [:bar]}] @can.association_joins.should == [{:foo => [{:bar=>[]}]}]
end end
end end