cancan 1.6.0 → 1.6.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -66,11 +66,22 @@ module CanCan
66
66
  return conditions unless conditions.kind_of? Hash
67
67
  conditions.inject({}) do |result_hash, (name, value)|
68
68
  if value.kind_of? Hash
69
+ value = value.dup
69
70
  association_class = model_class.reflect_on_association(name).class_name.constantize
70
- name = model_class.reflect_on_association(name).table_name.to_sym
71
- value = tableized_conditions(value, association_class)
71
+ nested = value.inject({}) do |nested,(k,v)|
72
+ if v.kind_of? Hash
73
+ value.delete(k)
74
+ nested[k] = v
75
+ else
76
+ name = model_class.reflect_on_association(name).table_name.to_sym
77
+ result_hash[name] = value
78
+ end
79
+ nested
80
+ end
81
+ result_hash.merge!(tableized_conditions(nested,association_class))
82
+ else
83
+ result_hash[name] = value
72
84
  end
73
- result_hash[name] = value
74
85
  result_hash
75
86
  end
76
87
  end
@@ -87,9 +98,14 @@ module CanCan
87
98
 
88
99
  def database_records
89
100
  if override_scope
90
- override_scope
101
+ @model_class.scoped.merge(override_scope)
91
102
  elsif @model_class.respond_to?(:where) && @model_class.respond_to?(:joins)
92
- @model_class.where(conditions).joins(joins)
103
+ mergeable_conditions = @rules.select {|rule| rule.unmergeable? }.blank?
104
+ if mergeable_conditions
105
+ @model_class.where(conditions).joins(joins)
106
+ else
107
+ @model_class.where(*(@rules.map(&:conditions))).joins(joins)
108
+ end
93
109
  else
94
110
  @model_class.scoped(:conditions => conditions, :joins => joins)
95
111
  end
@@ -99,7 +115,7 @@ module CanCan
99
115
 
100
116
  def override_scope
101
117
  conditions = @rules.map(&:conditions).compact
102
- if conditions.any? { |c| c.kind_of?(ActiveRecord::Relation) }
118
+ if defined?(ActiveRecord::Relation) && conditions.any? { |c| c.kind_of?(ActiveRecord::Relation) }
103
119
  if conditions.size == 1
104
120
  conditions.first
105
121
  else
@@ -140,8 +156,8 @@ module CanCan
140
156
  # Takes two hashes and does a deep merge.
141
157
  def merge_joins(base, add)
142
158
  add.each do |name, nested|
143
- if base[name].is_a?(Hash) && !nested.empty?
144
- merge_joins(base[name], nested)
159
+ if base[name].is_a?(Hash)
160
+ merge_joins(base[name], nested) unless nested.empty?
145
161
  else
146
162
  base[name] = nested
147
163
  end
@@ -5,29 +5,30 @@ module CanCan
5
5
  model_class <= DataMapper::Resource
6
6
  end
7
7
 
8
+ def self.find(model_class, id)
9
+ model_class.get(id)
10
+ end
11
+
8
12
  def self.override_conditions_hash_matching?(subject, conditions)
9
13
  conditions.any? { |k,v| !k.kind_of?(Symbol) }
10
14
  end
11
15
 
12
16
  def self.matches_conditions_hash?(subject, conditions)
13
- subject.class.all(:conditions => conditions).include?(subject) # TODO don't use a database query here for performance and other instances
17
+ collection = DataMapper::Collection.new(subject.query, [ subject ])
18
+ !!collection.first(conditions)
14
19
  end
15
20
 
16
21
  def database_records
17
- scope = @model_class.all(:conditions => ["0=1"])
18
- conditions.each do |condition|
19
- scope += @model_class.all(:conditions => condition)
20
- end
22
+ scope = @model_class.all(:conditions => ["0 = 1"])
23
+ cans, cannots = @rules.partition { |r| r.base_behavior }
24
+ return scope if cans.empty?
25
+ # apply unions first, then differences. this mean cannot overrides can
26
+ cans.each { |r| scope += @model_class.all(:conditions => r.conditions) }
27
+ cannots.each { |r| scope -= @model_class.all(:conditions => r.conditions) }
21
28
  scope
22
29
  end
30
+ end # class DataMapper
31
+ end # module ModelAdapters
32
+ end # module CanCan
23
33
 
24
- def conditions
25
- @rules.map(&:conditions)
26
- end
27
- end
28
- end
29
- end
30
-
31
- DataMapper::Model.class_eval do
32
- include CanCan::ModelAdditions::ClassMethods
33
- end
34
+ DataMapper::Model.append_extensions(CanCan::ModelAdditions::ClassMethods)
@@ -6,7 +6,14 @@ module CanCan
6
6
  end
7
7
 
8
8
  def self.override_conditions_hash_matching?(subject, conditions)
9
- conditions.any? { |k,v| !k.kind_of?(Symbol) }
9
+ conditions.any? do |k,v|
10
+ key_is_not_symbol = lambda { !k.kind_of?(Symbol) }
11
+ subject_value_is_array = lambda do
12
+ subject.respond_to?(k) && subject.send(k).is_a?(Array)
13
+ end
14
+
15
+ key_is_not_symbol.call || subject_value_is_array.call
16
+ end
10
17
  end
11
18
 
12
19
  def self.matches_conditions_hash?(subject, conditions)
@@ -16,14 +23,23 @@ module CanCan
16
23
  end
17
24
 
18
25
  def database_records
19
- if @rules.size == 0
26
+ if @rules.size == 0
20
27
  @model_class.where(:_id => {'$exists' => false, '$type' => 7}) # return no records in Mongoid
28
+ elsif @rules.size == 1 && @rules[0].conditions.is_a?(Mongoid::Criteria)
29
+ @rules[0].conditions
21
30
  else
22
- @rules.inject(@model_class.all) do |records, rule|
23
- if rule.base_behavior
24
- records.or(rule.conditions)
31
+ # we only need to process can rules if
32
+ # there are no rules with empty conditions
33
+ rules = @rules.reject { |rule| rule.conditions.empty? && rule.base_behavior }
34
+ process_can_rules = @rules.count == rules.count
35
+
36
+ rules.inject(@model_class.all) do |records, rule|
37
+ if process_can_rules && rule.base_behavior
38
+ records.or rule.conditions
39
+ elsif !rule.base_behavior
40
+ records.excludes rule.conditions
25
41
  else
26
- records.excludes(rule.conditions)
42
+ records
27
43
  end
28
44
  end
29
45
  end
@@ -35,4 +51,4 @@ end
35
51
  # simplest way to add `accessible_by` to all Mongoid Documents
36
52
  module Mongoid::Document::ClassMethods
37
53
  include CanCan::ModelAdditions::ClassMethods
38
- end
54
+ end
@@ -4,7 +4,7 @@ module CanCan
4
4
  module ModelAdditions
5
5
  module ClassMethods
6
6
  # Returns a scope which fetches only the records that the passed ability
7
- # can perform a given action on. The action defaults to :read. This
7
+ # can perform a given action on. The action defaults to :index. This
8
8
  # is usually called from a controller and passed the +current_ability+.
9
9
  #
10
10
  # @articles = Article.accessible_by(current_ability)
@@ -19,7 +19,7 @@ module CanCan
19
19
  # @articles = Article.accessible_by(current_ability, :update)
20
20
  #
21
21
  # Here only the articles which the user can update are returned.
22
- def accessible_by(ability, action = :read)
22
+ def accessible_by(ability, action = :index)
23
23
  ability.model_adapter(self, action).database_records
24
24
  end
25
25
  end
data/lib/cancan/rule.rb CHANGED
@@ -54,6 +54,11 @@ module CanCan
54
54
  @conditions == {} || @conditions.nil?
55
55
  end
56
56
 
57
+ def unmergeable?
58
+ @conditions.respond_to?(:keys) && @conditions.present? &&
59
+ (!@conditions.keys.first.kind_of? Symbol)
60
+ end
61
+
57
62
  def associations_hash(conditions = @conditions)
58
63
  hash = {}
59
64
  conditions.map do |name, value|
@@ -109,9 +114,9 @@ module CanCan
109
114
  if attribute.kind_of? Array
110
115
  attribute.any? { |element| matches_conditions_hash? element, value }
111
116
  else
112
- matches_conditions_hash? attribute, value
117
+ !attribute.nil? && matches_conditions_hash?(attribute, value)
113
118
  end
114
- elsif value.kind_of?(Array) || value.kind_of?(Range)
119
+ elsif !value.is_a?(String) && value.kind_of?(Enumerable)
115
120
  value.include? attribute
116
121
  else
117
122
  attribute == value
@@ -123,7 +128,7 @@ module CanCan
123
128
  end
124
129
 
125
130
  def nested_subject_matches_conditions?(subject_hash)
126
- parent, child = subject_hash.shift
131
+ parent, child = subject_hash.first
127
132
  matches_conditions_hash?(parent, @conditions[parent.class.name.downcase.to_sym] || {})
128
133
  end
129
134
 
@@ -136,7 +141,7 @@ module CanCan
136
141
  end
137
142
 
138
143
  def model_adapter(subject)
139
- ModelAdapters::AbstractAdapter.adapter_class(subject_class?(subject) ? subject : subject.class)
144
+ CanCan::ModelAdapters::AbstractAdapter.adapter_class(subject_class?(subject) ? subject : subject.class)
140
145
  end
141
146
  end
142
147
  end
data/lib/cancan.rb CHANGED
@@ -10,4 +10,4 @@ require 'cancan/model_adapters/abstract_adapter'
10
10
  require 'cancan/model_adapters/default_adapter'
11
11
  require 'cancan/model_adapters/active_record_adapter' if defined? ActiveRecord
12
12
  require 'cancan/model_adapters/data_mapper_adapter' if defined? DataMapper
13
- require 'cancan/model_adapters/mongoid_adapter' if defined? Mongoid
13
+ require 'cancan/model_adapters/mongoid_adapter' if defined?(Mongoid) && defined?(Mongoid::Document)
@@ -11,18 +11,22 @@ class Ability
11
11
  # can :read, :all
12
12
  # end
13
13
  #
14
- # The first argument to `can` is the action you are giving the user permission to do.
15
- # If you pass :manage it will apply to every action. Other common actions here are
16
- # :read, :create, :update and :destroy.
14
+ # The first argument to `can` is the action you are giving the user
15
+ # permission to do.
16
+ # If you pass :manage it will apply to every action. Other common actions
17
+ # here are :read, :create, :update and :destroy.
17
18
  #
18
- # The second argument is the resource the user can perform the action on. If you pass
19
- # :all it will apply to every resource. Otherwise pass a Ruby class of the resource.
19
+ # The second argument is the resource the user can perform the action on.
20
+ # If you pass :all it will apply to every resource. Otherwise pass a Ruby
21
+ # class of the resource.
20
22
  #
21
- # The third argument is an optional hash of conditions to further filter the objects.
23
+ # The third argument is an optional hash of conditions to further filter the
24
+ # objects.
22
25
  # For example, here the user can only update published articles.
23
26
  #
24
27
  # can :update, Article, :published => true
25
28
  #
26
- # See the wiki for details: https://github.com/ryanb/cancan/wiki/Defining-Abilities
29
+ # See the wiki for details:
30
+ # https://github.com/ryanb/cancan/wiki/Defining-Abilities
27
31
  end
28
32
  end
@@ -87,6 +87,10 @@ describe CanCan::Ability do
87
87
  @ability.can?(:increment, 123).should be_true
88
88
  end
89
89
 
90
+ it "should raise an Error if alias target is an exist action" do
91
+ lambda{ @ability.alias_action :show, :to => :show }.should raise_error(CanCan::Error, "You can't specify target (show) as alias because it is real action name")
92
+ end
93
+
90
94
  it "should always call block with arguments when passing no arguments to can" do
91
95
  @ability.can do |action, object_class, object|
92
96
  action.should == :foo
@@ -250,6 +254,27 @@ describe CanCan::Ability do
250
254
  @ability.can?(:read, 4..6).should be_false
251
255
  end
252
256
 
257
+ it "should accept a set as a condition value" do
258
+ mock(object_with_foo_2 = Object.new).foo { 2 }
259
+ mock(object_with_foo_3 = Object.new).foo { 3 }
260
+ @ability.can :read, Object, :foo => [1, 2, 5].to_set
261
+ @ability.can?(:read, object_with_foo_2).should be_true
262
+ @ability.can?(:read, object_with_foo_3).should be_false
263
+ end
264
+
265
+ it "should not match subjects return nil for methods that must match nested a nested conditions hash" do
266
+ mock(object_with_foo = Object.new).foo { :bar }
267
+ @ability.can :read, Array, :first => { :foo => :bar }
268
+ @ability.can?(:read, [object_with_foo]).should be_true
269
+ @ability.can?(:read, []).should be_false
270
+ end
271
+
272
+ it "should match strings but not substrings specified in a conditions hash" do
273
+ @ability.can :read, String, :presence => "declassified"
274
+ @ability.can?(:read, "declassified").should be_true
275
+ @ability.can?(:read, "classified").should be_false
276
+ end
277
+
253
278
  it "should not stop at cannot definition when comparing class" do
254
279
  @ability.can :read, Range
255
280
  @ability.cannot :read, Range, :begin => 1
@@ -290,7 +315,15 @@ describe CanCan::Ability do
290
315
  @ability.can?(:read, "foobar" => Range).should be_false
291
316
  @ability.can?(:read, 123 => Range).should be_true
292
317
  end
293
-
318
+
319
+ it "passing a hash of subjects with multiple definitions should check permissions correctly" do
320
+ @ability.can :read, Range, :string => {:length => 4}
321
+ @ability.can [:create, :read], Range, :string => {:upcase => 'FOO'}
322
+ @ability.can?(:read, "foo" => Range).should be_true
323
+ @ability.can?(:read, "foobar" => Range).should be_false
324
+ @ability.can?(:read, 1234 => Range).should be_true
325
+ end
326
+
294
327
  it "should allow to check ability on Hash-like object" do
295
328
  class Container < Hash; end
296
329
  @ability.can :read, Container
@@ -317,9 +350,11 @@ describe CanCan::Ability do
317
350
  end
318
351
  end
319
352
 
320
- it "should not raise access denied exception if ability is authorized to perform an action" do
353
+ it "should not raise access denied exception if ability is authorized to perform an action and return subject" do
321
354
  @ability.can :read, :foo
322
- lambda { @ability.authorize!(:read, :foo) }.should_not raise_error
355
+ lambda {
356
+ @ability.authorize!(:read, :foo).should == :foo
357
+ }.should_not raise_error
323
358
  end
324
359
 
325
360
  it "should know when block is used in conditions" do
@@ -407,4 +442,17 @@ describe CanCan::Ability do
407
442
  @ability.unauthorized_message(:edit, 1..3).should == "edit range"
408
443
  end
409
444
  end
445
+
446
+ describe "#merge" do
447
+ it "should add the rules from the given ability" do
448
+ @ability.can :use, :tools
449
+ another_ability = Object.new
450
+ another_ability.extend(CanCan::Ability)
451
+ another_ability.can :use, :search
452
+
453
+ @ability.merge(another_ability)
454
+ @ability.can?(:use, :search).should be_true
455
+ @ability.send(:rules).size.should == 2
456
+ end
457
+ end
410
458
  end
@@ -6,7 +6,7 @@ describe CanCan::ControllerAdditions do
6
6
  @controller = @controller_class.new
7
7
  stub(@controller).params { {} }
8
8
  stub(@controller).current_user { :current_user }
9
- mock(@controller_class).helper_method(:can?, :cannot?)
9
+ mock(@controller_class).helper_method(:can?, :cannot?, :current_ability)
10
10
  @controller_class.send(:include, CanCan::ControllerAdditions)
11
11
  end
12
12
 
@@ -49,14 +49,14 @@ describe CanCan::ControllerAdditions do
49
49
 
50
50
  it "authorize_resource should setup a before filter which passes call to ControllerResource" do
51
51
  stub(CanCan::ControllerResource).new(@controller, nil, :foo => :bar).mock!.authorize_resource
52
- mock(@controller_class).before_filter(:except => :show) { |options, block| block.call(@controller) }
53
- @controller_class.authorize_resource :foo => :bar, :except => :show
52
+ mock(@controller_class).before_filter(:except => :show, :if => true) { |options, block| block.call(@controller) }
53
+ @controller_class.authorize_resource :foo => :bar, :except => :show, :if => true
54
54
  end
55
55
 
56
56
  it "load_resource should setup a before filter which passes call to ControllerResource" do
57
57
  stub(CanCan::ControllerResource).new(@controller, nil, :foo => :bar).mock!.load_resource
58
- mock(@controller_class).before_filter(:only => [:show, :index]) { |options, block| block.call(@controller) }
59
- @controller_class.load_resource :foo => :bar, :only => [:show, :index]
58
+ mock(@controller_class).before_filter(:only => [:show, :index], :unless => false) { |options, block| block.call(@controller) }
59
+ @controller_class.load_resource :foo => :bar, :only => [:show, :index], :unless => false
60
60
  end
61
61
 
62
62
  it "skip_authorization_check should set up a before filter which sets @_authorized to true" do