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.
- data/CHANGELOG.rdoc +157 -0
- data/CONTRIBUTING.md +11 -0
- data/Gemfile +2 -2
- data/README.rdoc +10 -1
- data/lib/cancan/ability.rb +14 -0
- data/lib/cancan/controller_additions.rb +16 -8
- data/lib/cancan/controller_resource.rb +73 -18
- data/lib/cancan/exceptions.rb +1 -1
- data/lib/cancan/inherited_resource.rb +3 -2
- data/lib/cancan/matchers.rb +2 -2
- data/lib/cancan/model_adapters/abstract_adapter.rb +5 -0
- data/lib/cancan/model_adapters/active_record_adapter.rb +24 -8
- data/lib/cancan/model_adapters/data_mapper_adapter.rb +16 -15
- data/lib/cancan/model_adapters/mongoid_adapter.rb +23 -7
- data/lib/cancan/model_additions.rb +2 -2
- data/lib/cancan/rule.rb +9 -4
- data/lib/cancan.rb +1 -1
- data/lib/generators/cancan/ability/templates/ability.rb +11 -7
- data/spec/cancan/ability_spec.rb +51 -3
- data/spec/cancan/controller_additions_spec.rb +5 -5
- data/spec/cancan/controller_resource_spec.rb +116 -26
- data/spec/cancan/exceptions_spec.rb +23 -0
- data/spec/cancan/inherited_resource_spec.rb +20 -2
- data/spec/cancan/model_adapters/active_record_adapter_spec.rb +63 -6
- data/spec/cancan/model_adapters/data_mapper_adapter_spec.rb +5 -1
- data/spec/cancan/model_adapters/mongoid_adapter_spec.rb +44 -2
- data/spec/cancan/rule_spec.rb +13 -0
- data/spec/spec_helper.rb +40 -0
- metadata +12 -11
|
@@ -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
|
-
|
|
71
|
-
|
|
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
|
-
@
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
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?
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
|
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 :
|
|
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 = :
|
|
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?
|
|
117
|
+
!attribute.nil? && matches_conditions_hash?(attribute, value)
|
|
113
118
|
end
|
|
114
|
-
elsif value.
|
|
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.
|
|
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
|
|
15
|
-
#
|
|
16
|
-
#
|
|
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.
|
|
19
|
-
# :all it will apply to every resource. Otherwise pass a Ruby
|
|
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
|
|
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:
|
|
29
|
+
# See the wiki for details:
|
|
30
|
+
# https://github.com/ryanb/cancan/wiki/Defining-Abilities
|
|
27
31
|
end
|
|
28
32
|
end
|
data/spec/cancan/ability_spec.rb
CHANGED
|
@@ -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 {
|
|
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
|