cancat 0.0.1.beta1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. data/CHANGELOG.rdoc +291 -0
  2. data/Gemfile +20 -0
  3. data/LICENSE +20 -0
  4. data/README.rdoc +111 -0
  5. data/Rakefile +18 -0
  6. data/init.rb +1 -0
  7. data/lib/cancan.rb +13 -0
  8. data/lib/cancan/ability.rb +302 -0
  9. data/lib/cancan/controller_additions.rb +389 -0
  10. data/lib/cancan/controller_resource.rb +222 -0
  11. data/lib/cancan/exceptions.rb +50 -0
  12. data/lib/cancan/inherited_resource.rb +19 -0
  13. data/lib/cancan/matchers.rb +14 -0
  14. data/lib/cancan/model_adapters/abstract_adapter.rb +56 -0
  15. data/lib/cancan/model_adapters/active_record_adapter.rb +165 -0
  16. data/lib/cancan/model_adapters/data_mapper_adapter.rb +36 -0
  17. data/lib/cancan/model_adapters/default_adapter.rb +7 -0
  18. data/lib/cancan/model_adapters/mongoid_adapter.rb +53 -0
  19. data/lib/cancan/model_additions.rb +31 -0
  20. data/lib/cancan/rule.rb +142 -0
  21. data/lib/generators/cancan/ability/USAGE +4 -0
  22. data/lib/generators/cancan/ability/ability_generator.rb +11 -0
  23. data/lib/generators/cancan/ability/templates/ability.rb +28 -0
  24. data/spec/README.rdoc +28 -0
  25. data/spec/cancan/ability_spec.rb +419 -0
  26. data/spec/cancan/controller_additions_spec.rb +137 -0
  27. data/spec/cancan/controller_resource_spec.rb +412 -0
  28. data/spec/cancan/exceptions_spec.rb +35 -0
  29. data/spec/cancan/inherited_resource_spec.rb +42 -0
  30. data/spec/cancan/matchers_spec.rb +33 -0
  31. data/spec/cancan/model_adapters/active_record_adapter_spec.rb +278 -0
  32. data/spec/cancan/model_adapters/data_mapper_adapter_spec.rb +119 -0
  33. data/spec/cancan/model_adapters/default_adapter_spec.rb +7 -0
  34. data/spec/cancan/model_adapters/mongoid_adapter_spec.rb +216 -0
  35. data/spec/cancan/rule_spec.rb +39 -0
  36. data/spec/matchers.rb +13 -0
  37. data/spec/spec.opts +2 -0
  38. data/spec/spec_helper.rb +41 -0
  39. metadata +171 -0
@@ -0,0 +1,36 @@
1
+ module CanCan
2
+ module ModelAdapters
3
+ class DataMapperAdapter < AbstractAdapter
4
+ def self.for_class?(model_class)
5
+ model_class <= DataMapper::Resource
6
+ end
7
+
8
+ def self.find(model_class, id)
9
+ model_class.get(id)
10
+ end
11
+
12
+ def self.override_conditions_hash_matching?(subject, conditions)
13
+ conditions.any? { |k,v| !k.kind_of?(Symbol) }
14
+ end
15
+
16
+ def self.matches_conditions_hash?(subject, conditions)
17
+ collection = DataMapper::Collection.new(subject.query, [ subject ])
18
+ !!collection.first(conditions)
19
+ end
20
+
21
+ def database_records
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) }
28
+ scope
29
+ end
30
+ end # class DataMapper
31
+ end # module ModelAdapters
32
+ end # module CanCan
33
+
34
+ DataMapper::Model.class_eval do
35
+ include CanCan::ModelAdditions::ClassMethods
36
+ end
@@ -0,0 +1,7 @@
1
+ module CanCan
2
+ module ModelAdapters
3
+ class DefaultAdapter < AbstractAdapter
4
+ # This adapter is used when no matching adapter is found
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,53 @@
1
+ module CanCan
2
+ module ModelAdapters
3
+ class MongoidAdapter < AbstractAdapter
4
+ def self.for_class?(model_class)
5
+ model_class <= Mongoid::Document
6
+ end
7
+
8
+ def self.override_conditions_hash_matching?(subject, conditions)
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
17
+ end
18
+
19
+ def self.matches_conditions_hash?(subject, conditions)
20
+ # To avoid hitting the db, retrieve the raw Mongo selector from
21
+ # the Mongoid Criteria and use Mongoid::Matchers#matches?
22
+ subject.matches?( subject.class.where(conditions).selector )
23
+ end
24
+
25
+ def database_records
26
+ if @rules.size == 0
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
30
+ else
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? }
34
+ process_can_rules = @rules.count == rules.count
35
+ rules.inject(@model_class.all) do |records, rule|
36
+ if process_can_rules && rule.base_behavior
37
+ records.or rule.conditions
38
+ elsif !rule.base_behavior
39
+ records.excludes rule.conditions
40
+ else
41
+ records
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+
50
+ # simplest way to add `accessible_by` to all Mongoid Documents
51
+ module Mongoid::Document::ClassMethods
52
+ include CanCan::ModelAdditions::ClassMethods
53
+ end
@@ -0,0 +1,31 @@
1
+ module CanCan
2
+
3
+ # This module adds the accessible_by class method to a model. It is included in the model adapters.
4
+ module ModelAdditions
5
+ module ClassMethods
6
+ # Returns a scope which fetches only the records that the passed ability
7
+ # can perform a given action on. The action defaults to :index. This
8
+ # is usually called from a controller and passed the +current_ability+.
9
+ #
10
+ # @articles = Article.accessible_by(current_ability)
11
+ #
12
+ # Here only the articles which the user is able to read will be returned.
13
+ # If the user does not have permission to read any articles then an empty
14
+ # result is returned. Since this is a scope it can be combined with any
15
+ # other scopes or pagination.
16
+ #
17
+ # An alternative action can optionally be passed as a second argument.
18
+ #
19
+ # @articles = Article.accessible_by(current_ability, :update)
20
+ #
21
+ # Here only the articles which the user can update are returned.
22
+ def accessible_by(ability, action = :index)
23
+ ability.model_adapter(self, action).database_records
24
+ end
25
+ end
26
+
27
+ def self.included(base)
28
+ base.extend ClassMethods
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,142 @@
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 associations_hash(conditions = @conditions)
58
+ hash = {}
59
+ conditions.map do |name, value|
60
+ hash[name] = associations_hash(value) if value.kind_of? Hash
61
+ end if conditions.kind_of? Hash
62
+ hash
63
+ end
64
+
65
+ def attributes_from_conditions
66
+ attributes = {}
67
+ @conditions.each do |key, value|
68
+ attributes[key] = value unless [Array, Range, Hash].include? value.class
69
+ end if @conditions.kind_of? Hash
70
+ attributes
71
+ end
72
+
73
+ private
74
+
75
+ def subject_class?(subject)
76
+ klass = (subject.kind_of?(Hash) ? subject.values.first : subject).class
77
+ klass == Class || klass == Module
78
+ end
79
+
80
+ def matches_action?(action)
81
+ @expanded_actions.include?(:manage) || @expanded_actions.include?(action)
82
+ end
83
+
84
+ def matches_subject?(subject)
85
+ @subjects.include?(:all) || @subjects.include?(subject) || matches_subject_class?(subject)
86
+ end
87
+
88
+ def matches_subject_class?(subject)
89
+ @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)) }
90
+ end
91
+
92
+ # Checks if the given subject matches the given conditions hash.
93
+ # This behavior can be overriden by a model adapter by defining two class methods:
94
+ # override_matching_for_conditions?(subject, conditions) and
95
+ # matches_conditions_hash?(subject, conditions)
96
+ def matches_conditions_hash?(subject, conditions = @conditions)
97
+ if conditions.empty?
98
+ true
99
+ else
100
+ if model_adapter(subject).override_conditions_hash_matching? subject, conditions
101
+ model_adapter(subject).matches_conditions_hash? subject, conditions
102
+ else
103
+ conditions.all? do |name, value|
104
+ if model_adapter(subject).override_condition_matching? subject, name, value
105
+ model_adapter(subject).matches_condition? subject, name, value
106
+ else
107
+ attribute = subject.send(name)
108
+ if value.kind_of?(Hash)
109
+ if attribute.kind_of? Array
110
+ attribute.any? { |element| matches_conditions_hash? element, value }
111
+ else
112
+ !attribute.nil? && matches_conditions_hash?(attribute, value)
113
+ end
114
+ elsif value.kind_of?(Array) || value.kind_of?(Range)
115
+ value.include? attribute
116
+ else
117
+ attribute == value
118
+ end
119
+ end
120
+ end
121
+ end
122
+ end
123
+ end
124
+
125
+ def nested_subject_matches_conditions?(subject_hash)
126
+ parent, child = subject_hash.shift
127
+ matches_conditions_hash?(parent, @conditions[parent.class.name.downcase.to_sym] || {})
128
+ end
129
+
130
+ def call_block_with_all(action, subject, extra_args)
131
+ if subject.class == Class
132
+ @block.call(action, subject, nil, *extra_args)
133
+ else
134
+ @block.call(action, subject.class, subject, *extra_args)
135
+ end
136
+ end
137
+
138
+ def model_adapter(subject)
139
+ ModelAdapters::AbstractAdapter.adapter_class(subject_class?(subject) ? subject : subject.class)
140
+ end
141
+ end
142
+ end
@@ -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,28 @@
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 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.
17
+ #
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.
20
+ #
21
+ # The third argument is an optional hash of conditions to further filter the objects.
22
+ # For example, here the user can only update published articles.
23
+ #
24
+ # can :update, Article, :published => true
25
+ #
26
+ # See the wiki for details: https://github.com/ryanb/cancan/wiki/Defining-Abilities
27
+ end
28
+ end
data/spec/README.rdoc ADDED
@@ -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,419 @@
1
+ require "spec_helper"
2
+
3
+ describe CanCan::Ability do
4
+ before(:each) do
5
+ @ability = Object.new
6
+ @ability.extend(CanCan::Ability)
7
+ end
8
+
9
+ it "should be able to :read anything" do
10
+ @ability.can :read, :all
11
+ @ability.can?(:read, String).should be_true
12
+ @ability.can?(:read, 123).should be_true
13
+ end
14
+
15
+ it "should not have permission to do something it doesn't know about" do
16
+ @ability.can?(:foodfight, String).should be_false
17
+ end
18
+
19
+ it "should pass true to `can?` when non false/nil is returned in block" do
20
+ @ability.can :read, :all
21
+ @ability.can :read, Symbol do |sym|
22
+ "foo" # TODO test that sym is nil when no instance is passed
23
+ end
24
+ @ability.can?(:read, :some_symbol).should == true
25
+ end
26
+
27
+ it "should pass nil to a block when no instance is passed" do
28
+ @ability.can :read, Symbol do |sym|
29
+ sym.should be_nil
30
+ true
31
+ end
32
+ @ability.can?(:read, Symbol).should be_true
33
+ end
34
+
35
+ it "should pass to previous rule, if block returns false or nil" do
36
+ @ability.can :read, Symbol
37
+ @ability.can :read, Integer do |i|
38
+ i < 5
39
+ end
40
+ @ability.can :read, Integer do |i|
41
+ i > 10
42
+ end
43
+ @ability.can?(:read, Symbol).should be_true
44
+ @ability.can?(:read, 11).should be_true
45
+ @ability.can?(:read, 1).should be_true
46
+ @ability.can?(:read, 6).should be_false
47
+ end
48
+
49
+ it "should not pass class with object if :all objects are accepted" do
50
+ @ability.can :preview, :all do |object|
51
+ object.should == 123
52
+ @block_called = true
53
+ end
54
+ @ability.can?(:preview, 123)
55
+ @block_called.should be_true
56
+ end
57
+
58
+ it "should not call block when only class is passed, only return true" do
59
+ @block_called = false
60
+ @ability.can :preview, :all do |object|
61
+ @block_called = true
62
+ end
63
+ @ability.can?(:preview, Hash).should be_true
64
+ @block_called.should be_false
65
+ end
66
+
67
+ it "should pass only object for global manage actions" do
68
+ @ability.can :manage, String do |object|
69
+ object.should == "foo"
70
+ @block_called = true
71
+ end
72
+ @ability.can?(:stuff, "foo").should
73
+ @block_called.should be_true
74
+ end
75
+
76
+ it "should alias update or destroy actions to modify action" do
77
+ @ability.alias_action :update, :destroy, :to => :modify
78
+ @ability.can :modify, :all
79
+ @ability.can?(:update, 123).should be_true
80
+ @ability.can?(:destroy, 123).should be_true
81
+ end
82
+
83
+ it "should allow deeply nested aliased actions" do
84
+ @ability.alias_action :increment, :to => :sort
85
+ @ability.alias_action :sort, :to => :modify
86
+ @ability.can :modify, :all
87
+ @ability.can?(:increment, 123).should be_true
88
+ end
89
+
90
+ it "should always call block with arguments when passing no arguments to can" do
91
+ @ability.can do |action, object_class, object|
92
+ action.should == :foo
93
+ object_class.should == 123.class
94
+ object.should == 123
95
+ @block_called = true
96
+ end
97
+ @ability.can?(:foo, 123)
98
+ @block_called.should be_true
99
+ end
100
+
101
+ it "should pass nil to object when comparing class with can check" do
102
+ @ability.can do |action, object_class, object|
103
+ action.should == :foo
104
+ object_class.should == Hash
105
+ object.should be_nil
106
+ @block_called = true
107
+ end
108
+ @ability.can?(:foo, Hash)
109
+ @block_called.should be_true
110
+ end
111
+
112
+ it "should automatically alias index and show into read calls" do
113
+ @ability.can :read, :all
114
+ @ability.can?(:index, 123).should be_true
115
+ @ability.can?(:show, 123).should be_true
116
+ end
117
+
118
+ it "should automatically alias new and edit into create and update respectively" do
119
+ @ability.can :create, :all
120
+ @ability.can :update, :all
121
+ @ability.can?(:new, 123).should be_true
122
+ @ability.can?(:edit, 123).should be_true
123
+ end
124
+
125
+ it "should not respond to prepare (now using initialize)" do
126
+ @ability.should_not respond_to(:prepare)
127
+ end
128
+
129
+ it "should offer cannot? method which is simply invert of can?" do
130
+ @ability.cannot?(:tie, String).should be_true
131
+ end
132
+
133
+ it "should be able to specify multiple actions and match any" do
134
+ @ability.can [:read, :update], :all
135
+ @ability.can?(:read, 123).should be_true
136
+ @ability.can?(:update, 123).should be_true
137
+ @ability.can?(:count, 123).should be_false
138
+ end
139
+
140
+ it "should be able to specify multiple classes and match any" do
141
+ @ability.can :update, [String, Range]
142
+ @ability.can?(:update, "foo").should be_true
143
+ @ability.can?(:update, 1..3).should be_true
144
+ @ability.can?(:update, 123).should be_false
145
+ end
146
+
147
+ it "should support custom objects in the rule" do
148
+ @ability.can :read, :stats
149
+ @ability.can?(:read, :stats).should be_true
150
+ @ability.can?(:update, :stats).should be_false
151
+ @ability.can?(:read, :nonstats).should be_false
152
+ end
153
+
154
+ it "should check ancestors of class" do
155
+ @ability.can :read, Numeric
156
+ @ability.can?(:read, Integer).should be_true
157
+ @ability.can?(:read, 1.23).should be_true
158
+ @ability.can?(:read, "foo").should be_false
159
+ end
160
+
161
+ it "should support 'cannot' method to define what user cannot do" do
162
+ @ability.can :read, :all
163
+ @ability.cannot :read, Integer
164
+ @ability.can?(:read, "foo").should be_true
165
+ @ability.can?(:read, 123).should be_false
166
+ end
167
+
168
+ it "should pass to previous rule, if block returns false or nil" do
169
+ @ability.can :read, :all
170
+ @ability.cannot :read, Integer do |int|
171
+ int > 10 ? nil : ( int > 5 )
172
+ end
173
+ @ability.can?(:read, "foo").should be_true
174
+ @ability.can?(:read, 3).should be_true
175
+ @ability.can?(:read, 8).should be_false
176
+ @ability.can?(:read, 123).should be_true
177
+ end
178
+
179
+ it "should always return `false` for single cannot definition" do
180
+ @ability.cannot :read, Integer do |int|
181
+ int > 10 ? nil : ( int > 5 )
182
+ end
183
+ @ability.can?(:read, "foo").should be_false
184
+ @ability.can?(:read, 3).should be_false
185
+ @ability.can?(:read, 8).should be_false
186
+ @ability.can?(:read, 123).should be_false
187
+ end
188
+
189
+ it "should pass to previous cannot definition, if block returns false or nil" do
190
+ @ability.cannot :read, :all
191
+ @ability.can :read, Integer do |int|
192
+ int > 10 ? nil : ( int > 5 )
193
+ end
194
+ @ability.can?(:read, "foo").should be_false
195
+ @ability.can?(:read, 3).should be_false
196
+ @ability.can?(:read, 10).should be_true
197
+ @ability.can?(:read, 123).should be_false
198
+ end
199
+
200
+ it "should append aliased actions" do
201
+ @ability.alias_action :update, :to => :modify
202
+ @ability.alias_action :destroy, :to => :modify
203
+ @ability.aliased_actions[:modify].should == [:update, :destroy]
204
+ end
205
+
206
+ it "should clear aliased actions" do
207
+ @ability.alias_action :update, :to => :modify
208
+ @ability.clear_aliased_actions
209
+ @ability.aliased_actions[:modify].should be_nil
210
+ end
211
+
212
+ it "should pass additional arguments to block from can?" do
213
+ @ability.can :read, Integer do |int, x|
214
+ int > x
215
+ end
216
+ @ability.can?(:read, 2, 1).should be_true
217
+ @ability.can?(:read, 2, 3).should be_false
218
+ end
219
+
220
+ it "should use conditions as third parameter and determine abilities from it" do
221
+ @ability.can :read, Range, :begin => 1, :end => 3
222
+ @ability.can?(:read, 1..3).should be_true
223
+ @ability.can?(:read, 1..4).should be_false
224
+ @ability.can?(:read, Range).should be_true
225
+ end
226
+
227
+ it "should allow an array of options in conditions hash" do
228
+ @ability.can :read, Range, :begin => [1, 3, 5]
229
+ @ability.can?(:read, 1..3).should be_true
230
+ @ability.can?(:read, 2..4).should be_false
231
+ @ability.can?(:read, 3..5).should be_true
232
+ end
233
+
234
+ it "should allow a range of options in conditions hash" do
235
+ @ability.can :read, Range, :begin => 1..3
236
+ @ability.can?(:read, 1..10).should be_true
237
+ @ability.can?(:read, 3..30).should be_true
238
+ @ability.can?(:read, 4..40).should be_false
239
+ end
240
+
241
+ it "should allow nested hashes in conditions hash" do
242
+ @ability.can :read, Range, :begin => { :to_i => 5 }
243
+ @ability.can?(:read, 5..7).should be_true
244
+ @ability.can?(:read, 6..8).should be_false
245
+ end
246
+
247
+ it "should match any element passed in to nesting if it's an array (for has_many associations)" do
248
+ @ability.can :read, Range, :to_a => { :to_i => 3 }
249
+ @ability.can?(:read, 1..5).should be_true
250
+ @ability.can?(:read, 4..6).should be_false
251
+ end
252
+
253
+ it "should not match subjects return nil for methods that must match nested a nested conditions hash" do
254
+ mock(object_with_foo = Object.new).foo { :bar }
255
+ @ability.can :read, Array, :first => { :foo => :bar }
256
+ @ability.can?(:read, [object_with_foo]).should be_true
257
+ @ability.can?(:read, []).should be_false
258
+ end
259
+
260
+ it "should not stop at cannot definition when comparing class" do
261
+ @ability.can :read, Range
262
+ @ability.cannot :read, Range, :begin => 1
263
+ @ability.can?(:read, 2..5).should be_true
264
+ @ability.can?(:read, 1..5).should be_false
265
+ @ability.can?(:read, Range).should be_true
266
+ end
267
+
268
+ it "should stop at cannot definition when no hash is present" do
269
+ @ability.can :read, :all
270
+ @ability.cannot :read, Range
271
+ @ability.can?(:read, 1..5).should be_false
272
+ @ability.can?(:read, Range).should be_false
273
+ end
274
+
275
+ it "should allow to check ability for Module" do
276
+ module B; end
277
+ class A; include B; end
278
+ @ability.can :read, B
279
+ @ability.can?(:read, A).should be_true
280
+ @ability.can?(:read, A.new).should be_true
281
+ end
282
+
283
+ it "should pass nil to a block for ability on Module when no instance is passed" do
284
+ module B; end
285
+ class A; include B; end
286
+ @ability.can :read, B do |sym|
287
+ sym.should be_nil
288
+ true
289
+ end
290
+ @ability.can?(:read, B).should be_true
291
+ @ability.can?(:read, A).should be_true
292
+ end
293
+
294
+ it "passing a hash of subjects should check permissions through association" do
295
+ @ability.can :read, Range, :string => {:length => 3}
296
+ @ability.can?(:read, "foo" => Range).should be_true
297
+ @ability.can?(:read, "foobar" => Range).should be_false
298
+ @ability.can?(:read, 123 => Range).should be_true
299
+ end
300
+
301
+ it "should allow to check ability on Hash-like object" do
302
+ class Container < Hash; end
303
+ @ability.can :read, Container
304
+ @ability.can?(:read, Container.new).should be_true
305
+ end
306
+
307
+ it "should have initial attributes based on hash conditions of 'new' action" do
308
+ @ability.can :manage, Range, :foo => "foo", :hash => {:skip => "hashes"}
309
+ @ability.can :create, Range, :bar => 123, :array => %w[skip arrays]
310
+ @ability.can :new, Range, :baz => "baz", :range => 1..3
311
+ @ability.cannot :new, Range, :ignore => "me"
312
+ @ability.attributes_for(:new, Range).should == {:foo => "foo", :bar => 123, :baz => "baz"}
313
+ end
314
+
315
+ it "should raise access denied exception if ability us unauthorized to perform a certain action" do
316
+ begin
317
+ @ability.authorize! :read, :foo, 1, 2, 3, :message => "Access denied!"
318
+ rescue CanCan::AccessDenied => e
319
+ e.message.should == "Access denied!"
320
+ e.action.should == :read
321
+ e.subject.should == :foo
322
+ else
323
+ fail "Expected CanCan::AccessDenied exception to be raised"
324
+ end
325
+ end
326
+
327
+ it "should not raise access denied exception if ability is authorized to perform an action and return subject" do
328
+ @ability.can :read, :foo
329
+ lambda {
330
+ @ability.authorize!(:read, :foo).should == :foo
331
+ }.should_not raise_error
332
+ end
333
+
334
+ it "should know when block is used in conditions" do
335
+ @ability.can :read, :foo
336
+ @ability.should_not have_block(:read, :foo)
337
+ @ability.can :read, :foo do |foo|
338
+ false
339
+ end
340
+ @ability.should have_block(:read, :foo)
341
+ end
342
+
343
+ it "should know when raw sql is used in conditions" do
344
+ @ability.can :read, :foo
345
+ @ability.should_not have_raw_sql(:read, :foo)
346
+ @ability.can :read, :foo, 'false'
347
+ @ability.should have_raw_sql(:read, :foo)
348
+ end
349
+
350
+ it "should raise access denied exception with default message if not specified" do
351
+ begin
352
+ @ability.authorize! :read, :foo
353
+ rescue CanCan::AccessDenied => e
354
+ e.default_message = "Access denied!"
355
+ e.message.should == "Access denied!"
356
+ else
357
+ fail "Expected CanCan::AccessDenied exception to be raised"
358
+ end
359
+ end
360
+
361
+ it "should determine model adapter class by asking AbstractAdapter" do
362
+ model_class = Object.new
363
+ adapter_class = Object.new
364
+ stub(CanCan::ModelAdapters::AbstractAdapter).adapter_class(model_class) { adapter_class }
365
+ stub(adapter_class).new(model_class, []) { :adapter_instance }
366
+ @ability.model_adapter(model_class, :read).should == :adapter_instance
367
+ end
368
+
369
+ it "should raise an error when attempting to use a block with a hash condition since it's not likely what they want" do
370
+ lambda {
371
+ @ability.can :read, Array, :published => true do
372
+ false
373
+ end
374
+ }.should raise_error(CanCan::Error, "You are not able to supply a block with a hash of conditions in read Array ability. Use either one.")
375
+ end
376
+
377
+ describe "unauthorized message" do
378
+ after(:each) do
379
+ I18n.backend = nil
380
+ end
381
+
382
+ it "should use action/subject in i18n" do
383
+ I18n.backend.store_translations :en, :unauthorized => {:update => {:array => "foo"}}
384
+ @ability.unauthorized_message(:update, Array).should == "foo"
385
+ @ability.unauthorized_message(:update, [1, 2, 3]).should == "foo"
386
+ @ability.unauthorized_message(:update, :missing).should be_nil
387
+ end
388
+
389
+ it "should use symbol as subject directly" do
390
+ I18n.backend.store_translations :en, :unauthorized => {:has => {:cheezburger => "Nom nom nom. I eated it."}}
391
+ @ability.unauthorized_message(:has, :cheezburger).should == "Nom nom nom. I eated it."
392
+ end
393
+
394
+ it "should fall back to 'manage' and 'all'" do
395
+ I18n.backend.store_translations :en, :unauthorized => {
396
+ :manage => {:all => "manage all", :array => "manage array"},
397
+ :update => {:all => "update all", :array => "update array"}
398
+ }
399
+ @ability.unauthorized_message(:update, Array).should == "update array"
400
+ @ability.unauthorized_message(:update, Hash).should == "update all"
401
+ @ability.unauthorized_message(:foo, Array).should == "manage array"
402
+ @ability.unauthorized_message(:foo, Hash).should == "manage all"
403
+ end
404
+
405
+ it "should follow aliased actions" do
406
+ I18n.backend.store_translations :en, :unauthorized => {:modify => {:array => "modify array"}}
407
+ @ability.alias_action :update, :to => :modify
408
+ @ability.unauthorized_message(:update, Array).should == "modify array"
409
+ @ability.unauthorized_message(:edit, Array).should == "modify array"
410
+ end
411
+
412
+ it "should have variables for action and subject" do
413
+ I18n.backend.store_translations :en, :unauthorized => {:manage => {:all => "%{action} %{subject}"}} # old syntax for now in case testing with old I18n
414
+ @ability.unauthorized_message(:update, Array).should == "update array"
415
+ @ability.unauthorized_message(:update, ArgumentError).should == "update argument error"
416
+ @ability.unauthorized_message(:edit, 1..3).should == "edit range"
417
+ end
418
+ end
419
+ end