cancancan 1.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. checksums.yaml +15 -0
  2. data/CHANGELOG.rdoc +427 -0
  3. data/CONTRIBUTING.md +11 -0
  4. data/Gemfile +23 -0
  5. data/LICENSE +20 -0
  6. data/README.rdoc +161 -0
  7. data/Rakefile +18 -0
  8. data/init.rb +1 -0
  9. data/lib/cancan.rb +13 -0
  10. data/lib/cancan/ability.rb +324 -0
  11. data/lib/cancan/controller_additions.rb +397 -0
  12. data/lib/cancan/controller_resource.rb +286 -0
  13. data/lib/cancan/exceptions.rb +50 -0
  14. data/lib/cancan/inherited_resource.rb +20 -0
  15. data/lib/cancan/matchers.rb +14 -0
  16. data/lib/cancan/model_adapters/abstract_adapter.rb +56 -0
  17. data/lib/cancan/model_adapters/active_record_adapter.rb +180 -0
  18. data/lib/cancan/model_adapters/data_mapper_adapter.rb +34 -0
  19. data/lib/cancan/model_adapters/default_adapter.rb +7 -0
  20. data/lib/cancan/model_adapters/mongoid_adapter.rb +54 -0
  21. data/lib/cancan/model_additions.rb +31 -0
  22. data/lib/cancan/rule.rb +147 -0
  23. data/lib/cancancan.rb +1 -0
  24. data/lib/generators/cancan/ability/USAGE +4 -0
  25. data/lib/generators/cancan/ability/ability_generator.rb +11 -0
  26. data/lib/generators/cancan/ability/templates/ability.rb +32 -0
  27. data/spec/README.rdoc +28 -0
  28. data/spec/cancan/ability_spec.rb +455 -0
  29. data/spec/cancan/controller_additions_spec.rb +141 -0
  30. data/spec/cancan/controller_resource_spec.rb +553 -0
  31. data/spec/cancan/exceptions_spec.rb +58 -0
  32. data/spec/cancan/inherited_resource_spec.rb +60 -0
  33. data/spec/cancan/matchers_spec.rb +29 -0
  34. data/spec/cancan/model_adapters/active_record_adapter_spec.rb +358 -0
  35. data/spec/cancan/model_adapters/data_mapper_adapter_spec.rb +118 -0
  36. data/spec/cancan/model_adapters/default_adapter_spec.rb +7 -0
  37. data/spec/cancan/model_adapters/mongoid_adapter_spec.rb +226 -0
  38. data/spec/cancan/rule_spec.rb +52 -0
  39. data/spec/matchers.rb +13 -0
  40. data/spec/spec.opts +2 -0
  41. data/spec/spec_helper.rb +77 -0
  42. metadata +126 -0
@@ -0,0 +1,147 @@
1
+ module CanCan
2
+ # This class is used internally and should only be called through Ability.
3
+ # it holds the information about a "can" call made on Ability and provides
4
+ # helpful methods to determine permission checking and conditions hash generation.
5
+ class Rule # :nodoc:
6
+ attr_reader :base_behavior, :subjects, :actions, :conditions
7
+ attr_writer :expanded_actions
8
+
9
+ # The first argument when initializing is the base_behavior which is a true/false
10
+ # value. True for "can" and false for "cannot". The next two arguments are the action
11
+ # and subject respectively (such as :read, @project). The third argument is a hash
12
+ # of conditions and the last one is the block passed to the "can" call.
13
+ def initialize(base_behavior, action, subject, conditions, block)
14
+ 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?
15
+ @match_all = action.nil? && subject.nil?
16
+ @base_behavior = base_behavior
17
+ @actions = [action].flatten
18
+ @subjects = [subject].flatten
19
+ @conditions = conditions || {}
20
+ @block = block
21
+ end
22
+
23
+ # Matches both the subject and action, not necessarily the conditions
24
+ def relevant?(action, subject)
25
+ subject = subject.values.first if subject.class == Hash
26
+ @match_all || (matches_action?(action) && matches_subject?(subject))
27
+ end
28
+
29
+ # Matches the block or conditions hash
30
+ def matches_conditions?(action, subject, extra_args)
31
+ if @match_all
32
+ call_block_with_all(action, subject, extra_args)
33
+ elsif @block && !subject_class?(subject)
34
+ @block.call(subject, *extra_args)
35
+ elsif @conditions.kind_of?(Hash) && subject.class == Hash
36
+ nested_subject_matches_conditions?(subject)
37
+ elsif @conditions.kind_of?(Hash) && !subject_class?(subject)
38
+ matches_conditions_hash?(subject)
39
+ else
40
+ # Don't stop at "cannot" definitions when there are conditions.
41
+ conditions_empty? ? true : @base_behavior
42
+ end
43
+ end
44
+
45
+ def only_block?
46
+ conditions_empty? && !@block.nil?
47
+ end
48
+
49
+ def only_raw_sql?
50
+ @block.nil? && !conditions_empty? && !@conditions.kind_of?(Hash)
51
+ end
52
+
53
+ def conditions_empty?
54
+ @conditions == {} || @conditions.nil?
55
+ end
56
+
57
+ def unmergeable?
58
+ @conditions.respond_to?(:keys) && @conditions.present? &&
59
+ (!@conditions.keys.first.kind_of? Symbol)
60
+ end
61
+
62
+ def associations_hash(conditions = @conditions)
63
+ hash = {}
64
+ conditions.map do |name, value|
65
+ hash[name] = associations_hash(value) if value.kind_of? Hash
66
+ end if conditions.kind_of? Hash
67
+ hash
68
+ end
69
+
70
+ def attributes_from_conditions
71
+ attributes = {}
72
+ @conditions.each do |key, value|
73
+ attributes[key] = value unless [Array, Range, Hash].include? value.class
74
+ end if @conditions.kind_of? Hash
75
+ attributes
76
+ end
77
+
78
+ private
79
+
80
+ def subject_class?(subject)
81
+ klass = (subject.kind_of?(Hash) ? subject.values.first : subject).class
82
+ klass == Class || klass == Module
83
+ end
84
+
85
+ def matches_action?(action)
86
+ @expanded_actions.include?(:manage) || @expanded_actions.include?(action)
87
+ end
88
+
89
+ def matches_subject?(subject)
90
+ @subjects.include?(:all) || @subjects.include?(subject) || matches_subject_class?(subject)
91
+ end
92
+
93
+ def matches_subject_class?(subject)
94
+ @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)) }
95
+ end
96
+
97
+ # Checks if the given subject matches the given conditions hash.
98
+ # This behavior can be overriden by a model adapter by defining two class methods:
99
+ # override_matching_for_conditions?(subject, conditions) and
100
+ # matches_conditions_hash?(subject, conditions)
101
+ def matches_conditions_hash?(subject, conditions = @conditions)
102
+ if conditions.empty?
103
+ true
104
+ else
105
+ if model_adapter(subject).override_conditions_hash_matching? subject, conditions
106
+ model_adapter(subject).matches_conditions_hash? subject, conditions
107
+ else
108
+ conditions.all? do |name, value|
109
+ if model_adapter(subject).override_condition_matching? subject, name, value
110
+ model_adapter(subject).matches_condition? subject, name, value
111
+ else
112
+ attribute = subject.send(name)
113
+ if value.kind_of?(Hash)
114
+ if attribute.kind_of?(Array) || attribute.kind_of?(ActiveRecord::Relation)
115
+ attribute.any? { |element| matches_conditions_hash? element, value }
116
+ else
117
+ !attribute.nil? && matches_conditions_hash?(attribute, value)
118
+ end
119
+ elsif !value.is_a?(String) && value.kind_of?(Enumerable)
120
+ value.include? attribute
121
+ else
122
+ attribute == value
123
+ end
124
+ end
125
+ end
126
+ end
127
+ end
128
+ end
129
+
130
+ def nested_subject_matches_conditions?(subject_hash)
131
+ parent, child = subject_hash.first
132
+ matches_conditions_hash?(parent, @conditions[parent.class.name.downcase.to_sym] || {})
133
+ end
134
+
135
+ def call_block_with_all(action, subject, extra_args)
136
+ if subject.class == Class
137
+ @block.call(action, subject, nil, *extra_args)
138
+ else
139
+ @block.call(action, subject.class, subject, *extra_args)
140
+ end
141
+ end
142
+
143
+ def model_adapter(subject)
144
+ CanCan::ModelAdapters::AbstractAdapter.adapter_class(subject_class?(subject) ? subject : subject.class)
145
+ end
146
+ end
147
+ end
@@ -0,0 +1 @@
1
+ require 'cancan'
@@ -0,0 +1,4 @@
1
+ Description:
2
+ The cancan:ability generator creates an Ability class in the models
3
+ directory. You can move this file anywhere you want as long as it
4
+ is in the load path.
@@ -0,0 +1,11 @@
1
+ module Cancan
2
+ module Generators
3
+ class AbilityGenerator < Rails::Generators::Base
4
+ source_root File.expand_path('../templates', __FILE__)
5
+
6
+ def generate_ability
7
+ copy_file "ability.rb", "app/models/ability.rb"
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,32 @@
1
+ class Ability
2
+ include CanCan::Ability
3
+
4
+ def initialize(user)
5
+ # Define abilities for the passed in user here. For example:
6
+ #
7
+ # user ||= User.new # guest user (not logged in)
8
+ # if user.admin?
9
+ # can :manage, :all
10
+ # else
11
+ # can :read, :all
12
+ # end
13
+ #
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.
18
+ #
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.
22
+ #
23
+ # The third argument is an optional hash of conditions to further filter the
24
+ # objects.
25
+ # For example, here the user can only update published articles.
26
+ #
27
+ # can :update, Article, :published => true
28
+ #
29
+ # See the wiki for details:
30
+ # https://github.com/bryanrite/cancancan/wiki/Defining-Abilities
31
+ end
32
+ end
@@ -0,0 +1,28 @@
1
+ = CanCan Specs
2
+
3
+ == Running the specs
4
+
5
+ To run the specs first run the +bundle+ command to install the necessary gems and the +rake+ command to run the specs.
6
+
7
+ bundle
8
+ rake
9
+
10
+ The specs currently require Ruby 1.8.7. Ruby 1.9.2 support will be coming soon.
11
+
12
+
13
+ == Model Adapters
14
+
15
+ CanCan offers separate specs for different model adapters (such as Mongoid and Data Mapper). By default it will use Active Record but you can change this by setting the +MODEL_ADAPTER+ environment variable before running. You can run the +bundle+ command with this as well to ensure you have the installed gems.
16
+
17
+ MODEL_ADAPTER=data_mapper bundle
18
+ MODEL_ADAPTER=data_mapper rake
19
+
20
+ The different model adapters you can specify are:
21
+
22
+ * active_record (default)
23
+ * data_mapper
24
+ * mongoid
25
+
26
+ You can also run the +spec_all+ rake task to run specs for each adapter.
27
+
28
+ rake spec_all
@@ -0,0 +1,455 @@
1
+ require "spec_helper"
2
+
3
+ describe CanCan::Ability do
4
+ before(:each) do
5
+ (@ability = double).extend(CanCan::Ability)
6
+ end
7
+
8
+ it "is able to :read anything" do
9
+ @ability.can :read, :all
10
+ expect(@ability.can?(:read, String)).to be_true
11
+ expect(@ability.can?(:read, 123)).to be_true
12
+ end
13
+
14
+ it "does not have permission to do something it doesn't know about" do
15
+ expect(@ability.can?(:foodfight, String)).to be_false
16
+ end
17
+
18
+ it "passes true to `can?` when non false/nil is returned in block" do
19
+ @ability.can :read, :all
20
+ @ability.can :read, Symbol do |sym|
21
+ "foo" # TODO test that sym is nil when no instance is passed
22
+ end
23
+ expect(@ability.can?(:read, :some_symbol)).to be_true
24
+ end
25
+
26
+ it "passes nil to a block when no instance is passed" do
27
+ @ability.can :read, Symbol do |sym|
28
+ expect(sym).to be_nil
29
+ true
30
+ end
31
+ expect(@ability.can?(:read, Symbol)).to be_true
32
+ end
33
+
34
+ it "passes to previous rule, if block returns false or nil" do
35
+ @ability.can :read, Symbol
36
+ @ability.can :read, Integer do |i|
37
+ i < 5
38
+ end
39
+ @ability.can :read, Integer do |i|
40
+ i > 10
41
+ end
42
+ expect(@ability.can?(:read, Symbol)).to be_true
43
+ expect(@ability.can?(:read, 11)).to be_true
44
+ expect(@ability.can?(:read, 1)).to be_true
45
+ expect(@ability.can?(:read, 6)).to be_false
46
+ end
47
+
48
+ it "does not pass class with object if :all objects are accepted" do
49
+ @ability.can :preview, :all do |object|
50
+ expect(object).to eq(123)
51
+ @block_called = true
52
+ end
53
+ @ability.can?(:preview, 123)
54
+ expect(@block_called).to be_true
55
+ end
56
+
57
+ it "does not call block when only class is passed, only return true" do
58
+ @block_called = false
59
+ @ability.can :preview, :all do |object|
60
+ @block_called = true
61
+ end
62
+ expect(@ability.can?(:preview, Hash)).to be_true
63
+ expect(@block_called).to be_false
64
+ end
65
+
66
+ it "passes only object for global manage actions" do
67
+ @ability.can :manage, String do |object|
68
+ expect(object).to eq("foo")
69
+ @block_called = true
70
+ end
71
+ expect(@ability.can?(:stuff, "foo")).to be_true
72
+ expect(@block_called).to be_true
73
+ end
74
+
75
+ it "makes alias for update or destroy actions to modify action" do
76
+ @ability.alias_action :update, :destroy, :to => :modify
77
+ @ability.can :modify, :all
78
+ expect(@ability.can?(:update, 123)).to be_true
79
+ expect(@ability.can?(:destroy, 123)).to be_true
80
+ end
81
+
82
+ it "allows deeply nested aliased actions" do
83
+ @ability.alias_action :increment, :to => :sort
84
+ @ability.alias_action :sort, :to => :modify
85
+ @ability.can :modify, :all
86
+ expect(@ability.can?(:increment, 123)).to be_true
87
+ end
88
+
89
+ it "raises an Error if alias target is an exist action" do
90
+ expect { @ability.alias_action :show, :to => :show }.to raise_error(CanCan::Error, "You can't specify target (show) as alias because it is real action name")
91
+ end
92
+
93
+ it "always calls block with arguments when passing no arguments to can" do
94
+ @ability.can do |action, object_class, object|
95
+ expect(action).to eq(:foo)
96
+ expect(object_class).to eq(123.class)
97
+ expect(object).to eq(123)
98
+ @block_called = true
99
+ end
100
+ @ability.can?(:foo, 123)
101
+ expect(@block_called).to be_true
102
+ end
103
+
104
+ it "passes nil to object when comparing class with can check" do
105
+ @ability.can do |action, object_class, object|
106
+ expect(action).to eq(:foo)
107
+ expect(object_class).to eq(Hash)
108
+ expect(object).to be_nil
109
+ @block_called = true
110
+ end
111
+ @ability.can?(:foo, Hash)
112
+ expect(@block_called).to be_true
113
+ end
114
+
115
+ it "automatically makes alias for index and show into read calls" do
116
+ @ability.can :read, :all
117
+ expect(@ability.can?(:index, 123)).to be_true
118
+ expect(@ability.can?(:show, 123)).to be_true
119
+ end
120
+
121
+ it "automatically makes alias for new and edit into create and update respectively" do
122
+ @ability.can :create, :all
123
+ @ability.can :update, :all
124
+ expect(@ability.can?(:new, 123)).to be_true
125
+ expect(@ability.can?(:edit, 123)).to be_true
126
+ end
127
+
128
+ it "does not respond to prepare (now using initialize)" do
129
+ expect(@ability).to_not respond_to(:prepare)
130
+ end
131
+
132
+ it "offers cannot? method which is simply invert of can?" do
133
+ expect(@ability.cannot?(:tie, String)).to be_true
134
+ end
135
+
136
+ it "is able to specify multiple actions and match any" do
137
+ @ability.can [:read, :update], :all
138
+ expect(@ability.can?(:read, 123)).to be_true
139
+ expect(@ability.can?(:update, 123)).to be_true
140
+ expect(@ability.can?(:count, 123)).to be_false
141
+ end
142
+
143
+ it "is able to specify multiple classes and match any" do
144
+ @ability.can :update, [String, Range]
145
+ expect(@ability.can?(:update, "foo")).to be_true
146
+ expect(@ability.can?(:update, 1..3)).to be_true
147
+ expect(@ability.can?(:update, 123)).to be_false
148
+ end
149
+
150
+ it "supports custom objects in the rule" do
151
+ @ability.can :read, :stats
152
+ expect(@ability.can?(:read, :stats)).to be_true
153
+ expect(@ability.can?(:update, :stats)).to be_false
154
+ expect(@ability.can?(:read, :nonstats)).to be_false
155
+ end
156
+
157
+ it "checks ancestors of class" do
158
+ @ability.can :read, Numeric
159
+ expect(@ability.can?(:read, Integer)).to be_true
160
+ expect(@ability.can?(:read, 1.23)).to be_true
161
+ expect(@ability.can?(:read, "foo")).to be_false
162
+ end
163
+
164
+ it "supports 'cannot' method to define what user cannot do" do
165
+ @ability.can :read, :all
166
+ @ability.cannot :read, Integer
167
+ expect(@ability.can?(:read, "foo")).to be_true
168
+ expect(@ability.can?(:read, 123)).to be_false
169
+ end
170
+
171
+ it "passes to previous rule, if block returns false or nil" do
172
+ @ability.can :read, :all
173
+ @ability.cannot :read, Integer do |int|
174
+ int > 10 ? nil : ( int > 5 )
175
+ end
176
+ expect(@ability.can?(:read, "foo")).to be_true
177
+ expect(@ability.can?(:read, 3)).to be_true
178
+ expect(@ability.can?(:read, 8)).to be_false
179
+ expect(@ability.can?(:read, 123)).to be_true
180
+ end
181
+
182
+ it "always returns `false` for single cannot definition" do
183
+ @ability.cannot :read, Integer do |int|
184
+ int > 10 ? nil : ( int > 5 )
185
+ end
186
+ expect(@ability.can?(:read, "foo")).to be_false
187
+ expect(@ability.can?(:read, 3)).to be_false
188
+ expect(@ability.can?(:read, 8)).to be_false
189
+ expect(@ability.can?(:read, 123)).to be_false
190
+ end
191
+
192
+ it "passes to previous cannot definition, if block returns false or nil" do
193
+ @ability.cannot :read, :all
194
+ @ability.can :read, Integer do |int|
195
+ int > 10 ? nil : ( int > 5 )
196
+ end
197
+ expect(@ability.can?(:read, "foo")).to be_false
198
+ expect(@ability.can?(:read, 3)).to be_false
199
+ expect(@ability.can?(:read, 10)).to be_true
200
+ expect(@ability.can?(:read, 123)).to be_false
201
+ end
202
+
203
+ it "appends aliased actions" do
204
+ @ability.alias_action :update, :to => :modify
205
+ @ability.alias_action :destroy, :to => :modify
206
+ expect(@ability.aliased_actions[:modify]).to eq([:update, :destroy])
207
+ end
208
+
209
+ it "clears aliased actions" do
210
+ @ability.alias_action :update, :to => :modify
211
+ @ability.clear_aliased_actions
212
+ expect(@ability.aliased_actions[:modify]).to be_nil
213
+ end
214
+
215
+ it "passes additional arguments to block from can?" do
216
+ @ability.can :read, Integer do |int, x|
217
+ int > x
218
+ end
219
+ expect(@ability.can?(:read, 2, 1)).to be_true
220
+ expect(@ability.can?(:read, 2, 3)).to be_false
221
+ end
222
+
223
+ it "uses conditions as third parameter and determine abilities from it" do
224
+ @ability.can :read, Range, :begin => 1, :end => 3
225
+ expect(@ability.can?(:read, 1..3)).to be_true
226
+ expect(@ability.can?(:read, 1..4)).to be_false
227
+ expect(@ability.can?(:read, Range)).to be_true
228
+ end
229
+
230
+ it "allows an array of options in conditions hash" do
231
+ @ability.can :read, Range, :begin => [1, 3, 5]
232
+ expect(@ability.can?(:read, 1..3)).to be_true
233
+ expect(@ability.can?(:read, 2..4)).to be_false
234
+ expect(@ability.can?(:read, 3..5)).to be_true
235
+ end
236
+
237
+ it "allows a range of options in conditions hash" do
238
+ @ability.can :read, Range, :begin => 1..3
239
+ expect(@ability.can?(:read, 1..10)).to be_true
240
+ expect(@ability.can?(:read, 3..30)).to be_true
241
+ expect(@ability.can?(:read, 4..40)).to be_false
242
+ end
243
+
244
+ it "allows nested hashes in conditions hash" do
245
+ @ability.can :read, Range, :begin => { :to_i => 5 }
246
+ expect(@ability.can?(:read, 5..7)).to be_true
247
+ expect(@ability.can?(:read, 6..8)).to be_false
248
+ end
249
+
250
+ it "matches any element passed in to nesting if it's an array (for has_many associations)" do
251
+ @ability.can :read, Range, :to_a => { :to_i => 3 }
252
+ expect(@ability.can?(:read, 1..5)).to be_true
253
+ expect(@ability.can?(:read, 4..6)).to be_false
254
+ end
255
+
256
+ it "accepts a set as a condition value" do
257
+ expect(object_with_foo_2 = double(:foo => 2)).to receive(:foo)
258
+ expect(object_with_foo_3 = double(:foo => 3)).to receive(:foo)
259
+ @ability.can :read, Object, :foo => [1, 2, 5].to_set
260
+ expect(@ability.can?(:read, object_with_foo_2)).to be_true
261
+ expect(@ability.can?(:read, object_with_foo_3)).to be_false
262
+ end
263
+
264
+ it "does not match subjects return nil for methods that must match nested a nested conditions hash" do
265
+ expect(object_with_foo = double(:foo => :bar)).to receive(:foo)
266
+ @ability.can :read, Array, :first => { :foo => :bar }
267
+ expect(@ability.can?(:read, [object_with_foo])).to be_true
268
+ expect(@ability.can?(:read, [])).to be_false
269
+ end
270
+
271
+ it "matches strings but not substrings specified in a conditions hash" do
272
+ @ability.can :read, String, :presence => "declassified"
273
+ expect(@ability.can?(:read, "declassified")).to be_true
274
+ expect(@ability.can?(:read, "classified")).to be_false
275
+ end
276
+
277
+ it "does not stop at cannot definition when comparing class" do
278
+ @ability.can :read, Range
279
+ @ability.cannot :read, Range, :begin => 1
280
+ expect(@ability.can?(:read, 2..5)).to be_true
281
+ expect(@ability.can?(:read, 1..5)).to be_false
282
+ expect(@ability.can?(:read, Range)).to be_true
283
+ end
284
+
285
+ it "stops at cannot definition when no hash is present" do
286
+ @ability.can :read, :all
287
+ @ability.cannot :read, Range
288
+ expect(@ability.can?(:read, 1..5)).to be_false
289
+ expect(@ability.can?(:read, Range)).to be_false
290
+ end
291
+
292
+ it "allows to check ability for Module" do
293
+ module B; end
294
+ class A; include B; end
295
+ @ability.can :read, B
296
+ expect(@ability.can?(:read, A)).to be_true
297
+ expect(@ability.can?(:read, A.new)).to be_true
298
+ end
299
+
300
+ it "passes nil to a block for ability on Module when no instance is passed" do
301
+ module B; end
302
+ class A; include B; end
303
+ @ability.can :read, B do |sym|
304
+ expect(sym).to be_nil
305
+ true
306
+ end
307
+ expect(@ability.can?(:read, B)).to be_true
308
+ expect(@ability.can?(:read, A)).to be_true
309
+ end
310
+
311
+ it "checks permissions through association when passing a hash of subjects" do
312
+ @ability.can :read, Range, :string => {:length => 3}
313
+ expect(@ability.can?(:read, "foo" => Range)).to be_true
314
+ expect(@ability.can?(:read, "foobar" => Range)).to be_false
315
+ expect(@ability.can?(:read, 123 => Range)).to be_true
316
+ end
317
+
318
+ it "checks permissions correctly when passing a hash of subjects with multiple definitions" do
319
+ @ability.can :read, Range, :string => {:length => 4}
320
+ @ability.can [:create, :read], Range, :string => {:upcase => 'FOO'}
321
+ expect(@ability.can?(:read, "foo" => Range)).to be_true
322
+ expect(@ability.can?(:read, "foobar" => Range)).to be_false
323
+ expect(@ability.can?(:read, 1234 => Range)).to be_true
324
+ end
325
+
326
+ it "allows to check ability on Hash-like object" do
327
+ class Container < Hash; end
328
+ @ability.can :read, Container
329
+ expect(@ability.can?(:read, Container.new)).to be_true
330
+ end
331
+
332
+ it "has initial attributes based on hash conditions of 'new' action" do
333
+ @ability.can :manage, Range, :foo => "foo", :hash => {:skip => "hashes"}
334
+ @ability.can :create, Range, :bar => 123, :array => %w[skip arrays]
335
+ @ability.can :new, Range, :baz => "baz", :range => 1..3
336
+ @ability.cannot :new, Range, :ignore => "me"
337
+ expect(@ability.attributes_for(:new, Range)).to eq({:foo => "foo", :bar => 123, :baz => "baz"})
338
+ end
339
+
340
+ it "raises access denied exception if ability us unauthorized to perform a certain action" do
341
+ begin
342
+ @ability.authorize! :read, :foo, 1, 2, 3, :message => "Access denied!"
343
+ rescue CanCan::AccessDenied => e
344
+ expect(e.message).to eq("Access denied!")
345
+ expect(e.action).to eq(:read)
346
+ expect(e.subject).to eq(:foo)
347
+ else
348
+ fail "Expected CanCan::AccessDenied exception to be raised"
349
+ end
350
+ end
351
+
352
+ it "does not raise access denied exception if ability is authorized to perform an action and return subject" do
353
+ @ability.can :read, :foo
354
+ expect {
355
+ expect(@ability.authorize!(:read, :foo)).to eq(:foo)
356
+ }.to_not raise_error
357
+ end
358
+
359
+ it "knows when block is used in conditions" do
360
+ @ability.can :read, :foo
361
+ expect(@ability).to_not have_block(:read, :foo)
362
+ @ability.can :read, :foo do |foo|
363
+ false
364
+ end
365
+ expect(@ability).to have_block(:read, :foo)
366
+ end
367
+
368
+ it "knows when raw sql is used in conditions" do
369
+ @ability.can :read, :foo
370
+ expect(@ability).to_not have_raw_sql(:read, :foo)
371
+ @ability.can :read, :foo, 'false'
372
+ expect(@ability).to have_raw_sql(:read, :foo)
373
+ end
374
+
375
+ it "raises access denied exception with default message if not specified" do
376
+ begin
377
+ @ability.authorize! :read, :foo
378
+ rescue CanCan::AccessDenied => e
379
+ e.default_message = "Access denied!"
380
+ expect(e.message).to eq("Access denied!")
381
+ else
382
+ fail "Expected CanCan::AccessDenied exception to be raised"
383
+ end
384
+ end
385
+
386
+ it "determines model adapterO class by asking AbstractAdapter" do
387
+ adapter_class, model_class = double, double
388
+ allow(CanCan::ModelAdapters::AbstractAdapter).to receive(:adapter_class).with(model_class) { adapter_class }
389
+ allow(adapter_class).to receive(:new).with(model_class, []) { :adapter_instance }
390
+ expect(@ability.model_adapter(model_class, :read)).to eq(:adapter_instance)
391
+ end
392
+
393
+ it "raises an error when attempting to use a block with a hash condition since it's not likely what they want" do
394
+ expect {
395
+ @ability.can :read, Array, :published => true do
396
+ false
397
+ end
398
+ }.to raise_error(CanCan::Error, "You are not able to supply a block with a hash of conditions in read Array ability. Use either one.")
399
+ end
400
+
401
+ describe "unauthorized message" do
402
+ after(:each) do
403
+ I18n.backend = nil
404
+ end
405
+
406
+ it "uses action/subject in i18n" do
407
+ I18n.backend.store_translations :en, :unauthorized => {:update => {:array => "foo"}}
408
+ expect(@ability.unauthorized_message(:update, Array)).to eq("foo")
409
+ expect(@ability.unauthorized_message(:update, [1, 2, 3])).to eq("foo")
410
+ expect(@ability.unauthorized_message(:update, :missing)).to be_nil
411
+ end
412
+
413
+ it "uses symbol as subject directly" do
414
+ I18n.backend.store_translations :en, :unauthorized => {:has => {:cheezburger => "Nom nom nom. I eated it."}}
415
+ expect(@ability.unauthorized_message(:has, :cheezburger)).to eq("Nom nom nom. I eated it.")
416
+ end
417
+
418
+ it "falls back to 'manage' and 'all'" do
419
+ I18n.backend.store_translations :en, :unauthorized => {
420
+ :manage => {:all => "manage all", :array => "manage array"},
421
+ :update => {:all => "update all", :array => "update array"}
422
+ }
423
+ expect(@ability.unauthorized_message(:update, Array)).to eq("update array")
424
+ expect(@ability.unauthorized_message(:update, Hash)).to eq("update all")
425
+ expect(@ability.unauthorized_message(:foo, Array)).to eq("manage array")
426
+ expect(@ability.unauthorized_message(:foo, Hash)).to eq("manage all")
427
+ end
428
+
429
+ it "follows aliased actions" do
430
+ I18n.backend.store_translations :en, :unauthorized => {:modify => {:array => "modify array"}}
431
+ @ability.alias_action :update, :to => :modify
432
+ expect(@ability.unauthorized_message(:update, Array)).to eq("modify array")
433
+ expect(@ability.unauthorized_message(:edit, Array)).to eq("modify array")
434
+ end
435
+
436
+ it "has variables for action and subject" do
437
+ I18n.backend.store_translations :en, :unauthorized => {:manage => {:all => "%{action} %{subject}"}} # old syntax for now in case testing with old I18n
438
+ expect(@ability.unauthorized_message(:update, Array)).to eq("update array")
439
+ expect(@ability.unauthorized_message(:update, ArgumentError)).to eq("update argument error")
440
+ expect(@ability.unauthorized_message(:edit, 1..3)).to eq("edit range")
441
+ end
442
+ end
443
+
444
+ describe "#merge" do
445
+ it "adds the rules from the given ability" do
446
+ @ability.can :use, :tools
447
+ (another_ability = double).extend(CanCan::Ability)
448
+ another_ability.can :use, :search
449
+
450
+ @ability.merge(another_ability)
451
+ expect(@ability.can?(:use, :search)).to be_true
452
+ expect(@ability.send(:rules).size).to eq(2)
453
+ end
454
+ end
455
+ end