Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

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>
  • Loading branch information...
commit 7d7d24918204df0e55dd8a3fc87b01036adaa244 1 parent 06296b0
@funny-falcon funny-falcon authored
View
127 lib/cancan/ability.rb
@@ -49,8 +49,12 @@ module Ability
#
def can?(action, subject, *extra_args)
raise Error, "Nom nom nom. I eated it." if action == :has && subject == :cheezburger
- can_definition = matching_can_definition(action, subject)
- can_definition && can_definition.can?(action, subject, extra_args)
+ matching_can_definition(action, subject) do |can_definition|
+ unless (can = can_definition.can?(action, subject, extra_args)) == :_NOT_MATCHED
+ return can
+ end
+ end
+ false
end
# Convenience method which works the same as "can?" but returns the opposite value.
@@ -179,11 +183,15 @@ def clear_aliased_actions
@aliased_actions = {}
end
- # Returns a hash of conditions which match the given ability. This is useful if you need to generate a database
- # query based on the current ability.
+ # Returns an array of arrays composing from desired action and hash of conditions which match the given ability.
+ # This is useful if you need to generate a database query based on the current ability.
#
# 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.
#
@@ -191,25 +199,74 @@ def clear_aliased_actions
# 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 conditions(action, subject)
- can_definition = matching_can_definition(action, subject)
- if can_definition
- raise Error, "Cannot determine ability conditions from block for #{action.inspect} #{subject.inspect}" if can_definition.block
- can_definition.conditions || {}
+ matched = matching_can_definition(action, subject)
+ unless matched.empty?
+ if matched.any?{|can_definition| can_definition.conditions.nil? && can_definition.block }
+ 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
false
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.
# See ActiveRecordAdditions#accessible_by for use in Active Record.
def association_joins(action, subject)
- can_definition = matching_can_definition(action, subject)
- if can_definition
- raise Error, "Cannot determine association joins from block for #{action.inspect} #{subject.inspect}" if can_definition.block
- can_definition.association_joins
+ can_definitions = matching_can_definition(action, subject)
+ unless can_definitions.empty?
+ if can_definitions.any?{|can_definition| can_definition.conditions.nil? && can_definition.block }
+ raise Error, "Cannot determine association joins from block for #{action.inspect} #{subject.inspect}"
+ end
+ collect_association_joins(can_definitions)
+ else
+ nil
end
end
-
+
private
def can_definitions
@@ -217,9 +274,18 @@ def can_definitions
end
def matching_can_definition(action, subject)
- can_definitions.reverse.detect do |can_definition|
- can_definition.expand_actions(aliased_actions)
- can_definition.matches? action, subject
+ if block_given?
+ can_definitions.reverse.each do |can_definition|
+ 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
@@ -230,5 +296,32 @@ def default_alias_actions
:update => [:edit],
}
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
View
2  lib/cancan/active_record_additions.rb
@@ -20,7 +20,7 @@ module ClassMethods
# Here only the articles which the user can update are returned. This
# internally uses Ability#conditions method, see that for more information.
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)
if respond_to? :where
where(conditions).joins(joins)
View
13 lib/cancan/can_definition.rb
@@ -1,7 +1,7 @@
module CanCan
# This class is used internally and should only be called through Ability.
class CanDefinition # :nodoc:
- attr_reader :conditions, :block
+ attr_reader :conditions, :block, :base_behavior, :definitive
def initialize(base_behavior, action, subject, conditions, block)
@base_behavior = base_behavior
@@ -23,18 +23,19 @@ def matches?(action, subject)
def can?(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
end
def association_joins(conditions = @conditions)
joins = []
- conditions.each do |name, value|
+ (conditions || []).each do |name, value|
if value.kind_of? Hash
nested = association_joins(value)
if nested
joins << {name => nested}
else
- joins << name
+ joins << {name => []}
end
end
end
@@ -54,8 +55,10 @@ def matches_subject?(subject)
def can_without_base_behavior?(action, subject, extra_args)
if @block
call_block(action, subject, extra_args)
- elsif @conditions && subject.class != Class
- matches_conditions? subject
+ elsif @conditions.kind_of?(Hash) && subject.class != Class
+ matches_conditions?(subject)
+ elsif [true, false, :_NOT_MATCHES].include? @conditions
+ @conditions
else
true
end
View
73 spec/cancan/ability_spec.rb
@@ -16,13 +16,27 @@
@ability.can?(:foodfight, String).should be_false
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, Symbol do |sym|
sym
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, 1).should be_true
+ @ability.can?(:read, -1).should be_true
end
it "should pass class with object if :all objects are accepted" do
@@ -121,6 +135,30 @@
@ability.can?(:read, 123).should be_false
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
@ability.alias_action :update, :to => :modify
@ability.alias_action :destroy, :to => :modify
@@ -174,27 +212,48 @@
@ability.can?(:read, [[4, 5, 6]]).should be_false
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.conditions(:show, Array).should == {:first => 1, :last => 3}
+ @ability.conditions(:show, Array).should == [[true, {:first => 1, :last => 3}]]
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|
true
end
lambda { @ability.conditions(:show, Array) }.should raise_error(CanCan::Error, "Cannot determine ability conditions from block for :show Array")
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.conditions(:show, Array).should == {}
+ @ability.conditions(:show, Array).should == [ [true, nil] ]
end
it "should return false when performed on an action which isn't defined" do
@ability.conditions(:foo, Array).should == false
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
lambda {
@ability.can? :has, :cheezburger
View
4 spec/cancan/can_definition_spec.rb
@@ -17,7 +17,7 @@
it "should return single association for joins" do
@conditions[:foo] = {:bar => 1}
- @can.association_joins.should == [:foo]
+ @can.association_joins.should == [{:foo=>[]}]
end
it "should return multiple associations for joins" do
@@ -28,6 +28,6 @@
it "should return nested associations for joins" do
@conditions[:foo] = {:bar => {1 => 2}}
- @can.association_joins.should == [{:foo => [:bar]}]
+ @can.association_joins.should == [{:foo => [{:bar=>[]}]}]
end
end
Please sign in to comment.
Something went wrong with that request. Please try again.