cancan 1.1.1 → 1.2.0

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.
@@ -1,3 +1,18 @@
1
+ 1.2.0 (July 16, 2010)
2
+
3
+ * Load nested parent resources on collection actions such as "index" (thanks dohzya)
4
+
5
+ * Adding :name option to load_and_authorize_resource if it does not match controller - see issue #65
6
+
7
+ * Fixing issue when using accessible_by with nil can conditions (thanks jrallison) - see issue #66
8
+
9
+ * Pluralize table name for belongs_to associations in can conditions hash (thanks logandk) - see issue #62
10
+
11
+ * Support has_many association or arrays in can conditions hash
12
+
13
+ * Adding joins clause to accessible_by when conditions are across associations
14
+
15
+
1
16
  1.1.1 (April 17, 2010)
2
17
 
3
18
  * Fixing behavior in Rails 3 by properly initializing ResourceAuthorization
@@ -1,4 +1,5 @@
1
1
  require 'cancan/ability'
2
+ require 'cancan/can_definition'
2
3
  require 'cancan/controller_resource'
3
4
  require 'cancan/resource_authorization'
4
5
  require 'cancan/controller_additions'
@@ -1,8 +1,8 @@
1
1
  module CanCan
2
-
2
+
3
3
  # This module is designed to be included into an Ability class. This will
4
4
  # provide the "can" methods for defining and checking abilities.
5
- #
5
+ #
6
6
  # class Ability
7
7
  # include CanCan::Ability
8
8
  #
@@ -14,206 +14,216 @@ module CanCan
14
14
  # end
15
15
  # end
16
16
  # end
17
- #
17
+ #
18
18
  module Ability
19
- # Use to check the user's permission for a given action and object.
20
- #
19
+ # Use to check if the user has permission to perform a given action on an object.
20
+ #
21
21
  # can? :destroy, @project
22
- #
22
+ #
23
23
  # You can also pass the class instead of an instance (if you don't have one handy).
24
- #
24
+ #
25
25
  # can? :create, Project
26
- #
26
+ #
27
27
  # Any additional arguments will be passed into the "can" block definition. This
28
28
  # can be used to pass more information about the user's request for example.
29
- #
29
+ #
30
30
  # can? :create, Project, request.remote_ip
31
- #
31
+ #
32
32
  # can :create Project do |project, remote_ip|
33
33
  # # ...
34
34
  # end
35
- #
36
- # Not only can you use the can? method in the controller and view (see ControllerAdditions),
35
+ #
36
+ # Not only can you use the can? method in the controller and view (see ControllerAdditions),
37
37
  # but you can also call it directly on an ability instance.
38
- #
38
+ #
39
39
  # ability.can? :destroy, @project
40
- #
40
+ #
41
41
  # This makes testing a user's abilities very easy.
42
- #
42
+ #
43
43
  # def test "user can only destroy projects which he owns"
44
44
  # user = User.new
45
45
  # ability = Ability.new(user)
46
46
  # assert ability.can?(:destroy, Project.new(:user => user))
47
47
  # assert ability.cannot?(:destroy, Project.new)
48
48
  # end
49
- #
49
+ #
50
+ # Also see the RSpec Matchers to aid in testing.
50
51
  def can?(action, subject, *extra_args)
51
- matching_can_definition(action, subject) do |base_behavior, defined_actions, defined_subjects, defined_conditions, defined_block|
52
- result = can_perform_action?(action, subject, defined_actions, defined_subjects, defined_conditions, defined_block, extra_args)
53
- return base_behavior ? result : !result
54
- end
55
- false
52
+ raise Error, "Nom nom nom. I eated it." if action == :has && subject == :cheezburger
53
+ can_definition = matching_can_definition(action, subject)
54
+ can_definition && can_definition.can?(action, subject, extra_args)
56
55
  end
57
-
56
+
58
57
  # Convenience method which works the same as "can?" but returns the opposite value.
59
- #
58
+ #
60
59
  # cannot? :destroy, @project
61
- #
60
+ #
62
61
  def cannot?(*args)
63
62
  !can?(*args)
64
63
  end
65
-
64
+
66
65
  # Defines which abilities are allowed using two arguments. The first one is the action
67
66
  # you're setting the permission for, the second one is the class of object you're setting it on.
68
- #
67
+ #
69
68
  # can :update, Article
70
- #
69
+ #
71
70
  # You can pass an array for either of these parameters to match any one.
72
71
  #
73
72
  # can [:update, :destroy], [Article, Comment]
74
73
  #
75
74
  # In this case the user has the ability to update or destroy both articles and comments.
76
- #
75
+ #
77
76
  # You can pass a hash of conditions as the third argument.
78
77
  #
79
78
  # can :read, Project, :active => true, :user_id => user.id
80
- #
81
- # Here the user can only see active projects which he owns. See ControllerAdditions#conditions for a way to
82
- # use this in database queries.
83
- #
79
+ #
80
+ # Here the user can only see active projects which he owns. See ActiveRecordAdditions#accessible_by
81
+ # for how to use this in database queries.
82
+ #
84
83
  # If the conditions hash does not give you enough control over defining abilities, you can use a block to
85
84
  # write any Ruby code you want.
86
85
  #
87
86
  # can :update, Project do |project|
88
87
  # project && project.groups.include?(user.group)
89
88
  # end
90
- #
89
+ #
91
90
  # If the block returns true then the user has that :update ability for that project, otherwise he
92
91
  # will be denied access. It's possible for the passed in model to be nil if one isn't specified,
93
92
  # so be sure to take that into consideration.
94
- #
93
+ #
95
94
  # The downside to using a block is that it cannot be used to generate conditions for database queries.
96
- #
95
+ #
97
96
  # You can pass :all to reference every type of object. In this case the object type will be passed
98
97
  # into the block as well (just in case object is nil).
99
- #
98
+ #
100
99
  # can :read, :all do |object_class, object|
101
100
  # object_class != Order
102
101
  # end
103
- #
102
+ #
104
103
  # Here the user has permission to read all objects except orders.
105
- #
106
- # You can also pass :manage as the action which will match any action. In this case the action is
104
+ #
105
+ # You can also pass :manage as the action which will match any action. In this case the action is
107
106
  # passed to the block.
108
- #
107
+ #
109
108
  # can :manage, Comment do |action, comment|
110
109
  # action != :destroy
111
110
  # end
112
- #
111
+ #
113
112
  # You can pass custom objects into this "can" method, this is usually done through a symbol
114
113
  # and is useful if a class isn't available to define permissions on.
115
- #
114
+ #
116
115
  # can :read, :stats
117
116
  # can? :read, :stats # => true
118
- #
117
+ #
119
118
  def can(action, subject, conditions = nil, &block)
120
- @can_definitions ||= []
121
- @can_definitions << [true, action, subject, conditions, block]
119
+ can_definitions << CanDefinition.new(true, action, subject, conditions, block)
122
120
  end
123
-
124
- # Define an ability which cannot be done. Accepts the same arguments as "can".
125
- #
121
+
122
+ # Defines an ability which cannot be done. Accepts the same arguments as "can".
123
+ #
126
124
  # can :read, :all
127
125
  # cannot :read, Comment
128
- #
126
+ #
129
127
  # A block can be passed just like "can", however if the logic is complex it is recommended
130
128
  # to use the "can" method.
131
- #
129
+ #
132
130
  # cannot :read, Product do |product|
133
131
  # product.invisible?
134
132
  # end
135
- #
133
+ #
136
134
  def cannot(action, subject, conditions = nil, &block)
137
- @can_definitions ||= []
138
- @can_definitions << [false, action, subject, conditions, block]
135
+ can_definitions << CanDefinition.new(false, action, subject, conditions, block)
139
136
  end
140
-
137
+
141
138
  # Alias one or more actions into another one.
142
- #
139
+ #
143
140
  # alias_action :update, :destroy, :to => :modify
144
141
  # can :modify, Comment
145
- #
142
+ #
146
143
  # Then :modify permission will apply to both :update and :destroy requests.
147
- #
144
+ #
148
145
  # can? :update, Comment # => true
149
146
  # can? :destroy, Comment # => true
150
- #
147
+ #
151
148
  # This only works in one direction. Passing the aliased action into the "can?" call
152
149
  # will not work because aliases are meant to generate more generic actions.
153
- #
150
+ #
154
151
  # alias_action :update, :destroy, :to => :modify
155
152
  # can :update, Comment
156
153
  # can? :modify, Comment # => false
157
- #
154
+ #
158
155
  # Unless that exact alias is used.
159
- #
156
+ #
160
157
  # can :modify, Comment
161
158
  # can? :modify, Comment # => true
162
- #
159
+ #
163
160
  # The following aliases are added by default for conveniently mapping common controller actions.
164
- #
161
+ #
165
162
  # alias_action :index, :show, :to => :read
166
163
  # alias_action :new, :to => :create
167
164
  # alias_action :edit, :to => :update
168
- #
165
+ #
169
166
  # This way one can use params[:action] in the controller to determine the permission.
170
167
  def alias_action(*args)
171
168
  target = args.pop[:to]
172
169
  aliased_actions[target] ||= []
173
170
  aliased_actions[target] += args
174
171
  end
175
-
172
+
176
173
  # Returns a hash of aliased actions. The key is the target and the value is an array of actions aliasing the key.
177
174
  def aliased_actions
178
175
  @aliased_actions ||= default_alias_actions
179
176
  end
180
-
177
+
181
178
  # Removes previously aliased actions including the defaults.
182
179
  def clear_aliased_actions
183
180
  @aliased_actions = {}
184
181
  end
185
-
182
+
186
183
  # Returns a hash of conditions which match the given ability. This is useful if you need to generate a database
187
184
  # query based on the current ability.
188
- #
185
+ #
189
186
  # can :read, Article, :visible => true
190
187
  # conditions :read, Article # returns { :visible => true }
191
- #
188
+ #
192
189
  # Normally you will not call this method directly, but instead go through ActiveRecordAdditions#accessible_by method.
193
- #
190
+ #
194
191
  # If the ability is not defined then false is returned so be sure to take that into consideration.
195
192
  # If the ability is defined using a block then this will raise an exception since a hash of conditions cannot be
196
193
  # determined from that.
197
- def conditions(action, subject)
198
- matching_can_definition(action, subject) do |base_behavior, defined_actions, defined_subjects, defined_conditions, defined_block|
199
- raise Error, "Cannot determine ability conditions from block for #{action.inspect} #{subject.inspect}" if defined_block
200
- return defined_conditions || {}
194
+ def conditions(action, subject, options = {})
195
+ can_definition = matching_can_definition(action, subject)
196
+ if can_definition
197
+ raise Error, "Cannot determine ability conditions from block for #{action.inspect} #{subject.inspect}" if can_definition.block
198
+ can_definition.conditions(options) || {}
199
+ else
200
+ false
201
+ end
202
+ end
203
+
204
+ # Returns the associations used in conditions. This is usually used in the :joins option for a search.
205
+ # See ActiveRecordAdditions#accessible_by for use in Active Record.
206
+ def association_joins(action, subject)
207
+ can_definition = matching_can_definition(action, subject)
208
+ if can_definition
209
+ raise Error, "Cannot determine association joins from block for #{action.inspect} #{subject.inspect}" if can_definition.block
210
+ can_definition.association_joins
201
211
  end
202
- false
203
212
  end
204
-
213
+
205
214
  private
206
-
207
- def matching_can_definition(action, subject, &block)
208
- (@can_definitions || []).reverse.each do |base_behavior, defined_action, defined_subject, defined_conditions, defined_block|
209
- defined_actions = expand_actions(defined_action)
210
- defined_subjects = [defined_subject].flatten
211
- if includes_action?(defined_actions, action) && includes_subject?(defined_subjects, subject)
212
- return block.call(base_behavior, defined_actions, defined_subjects, defined_conditions, defined_block)
213
- end
215
+
216
+ def can_definitions
217
+ @can_definitions ||= []
218
+ end
219
+
220
+ def matching_can_definition(action, subject)
221
+ can_definitions.reverse.detect do |can_definition|
222
+ can_definition.expand_actions(aliased_actions)
223
+ can_definition.matches? action, subject
214
224
  end
215
225
  end
216
-
226
+
217
227
  def default_alias_actions
218
228
  {
219
229
  :read => [:index, :show],
@@ -221,55 +231,5 @@ module CanCan
221
231
  :update => [:edit],
222
232
  }
223
233
  end
224
-
225
- def expand_actions(actions)
226
- [actions].flatten.map do |action|
227
- if aliased_actions[action]
228
- [action, *aliased_actions[action]]
229
- else
230
- action
231
- end
232
- end.flatten
233
- end
234
-
235
- def can_perform_action?(action, subject, defined_actions, defined_subjects, defined_conditions, defined_block, extra_args)
236
- if defined_block
237
- block_args = []
238
- block_args << action if defined_actions.include?(:manage)
239
- block_args << (subject.class == Class ? subject : subject.class) if defined_subjects.include?(:all)
240
- block_args << (subject.class == Class ? nil : subject)
241
- block_args += extra_args
242
- defined_block.call(*block_args)
243
- elsif defined_conditions
244
- if subject.class == Class
245
- true
246
- else
247
- matches_conditions? subject, defined_conditions
248
- end
249
- else
250
- true
251
- end
252
- end
253
-
254
- def matches_conditions?(subject, defined_conditions)
255
- defined_conditions.all? do |name, value|
256
- attribute = subject.send(name)
257
- if value.kind_of?(Hash)
258
- matches_conditions? attribute, value
259
- elsif value.kind_of?(Array) || value.kind_of?(Range)
260
- value.include? attribute
261
- else
262
- attribute == value
263
- end
264
- end
265
- end
266
-
267
- def includes_action?(actions, action)
268
- actions.include?(:manage) || actions.include?(action)
269
- end
270
-
271
- def includes_subject?(subjects, subject)
272
- subjects.include?(:all) || subjects.include?(subject) || subjects.any? { |c| c.kind_of?(Class) && subject.kind_of?(c) }
273
- end
274
234
  end
275
235
  end
@@ -1,5 +1,5 @@
1
1
  module CanCan
2
- # This module is automatically included into all Active Record.
2
+ # This module is automatically included into all Active Record models.
3
3
  module ActiveRecordAdditions
4
4
  module ClassMethods
5
5
  # Returns a scope which fetches only the records that the passed ability
@@ -7,28 +7,29 @@ module CanCan
7
7
  # is usually called from a controller and passed the +current_ability+.
8
8
  #
9
9
  # @articles = Article.accessible_by(current_ability)
10
- #
10
+ #
11
11
  # Here only the articles which the user is able to read will be returned.
12
12
  # If the user does not have permission to read any articles then an empty
13
13
  # result is returned. Since this is a scope it can be combined with any
14
14
  # other scopes or pagination.
15
- #
15
+ #
16
16
  # An alternative action can optionally be passed as a second argument.
17
- #
17
+ #
18
18
  # @articles = Article.accessible_by(current_ability, :update)
19
- #
19
+ #
20
20
  # Here only the articles which the user can update are returned. This
21
21
  # internally uses Ability#conditions method, see that for more information.
22
22
  def accessible_by(ability, action = :read)
23
- conditions = ability.conditions(action, self) || {:id => nil}
23
+ conditions = ability.conditions(action, self, :tableize => true) || {:id => nil}
24
+ joins = ability.association_joins(action, self)
24
25
  if respond_to? :where
25
- where(conditions)
26
+ where(conditions).joins(joins)
26
27
  else
27
- scoped(:conditions => conditions)
28
+ scoped(:conditions => conditions, :joins => joins)
28
29
  end
29
30
  end
30
31
  end
31
-
32
+
32
33
  def self.included(base)
33
34
  base.extend ClassMethods
34
35
  end
@@ -0,0 +1,114 @@
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 CanDefinition # :nodoc:
6
+ include ActiveSupport::Inflector
7
+ attr_reader :block
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
+ @base_behavior = base_behavior
15
+ @actions = [action].flatten
16
+ @subjects = [subject].flatten
17
+ @conditions = conditions || {}
18
+ @block = block
19
+ end
20
+
21
+ # Accepts a hash of aliased actions and returns an array of actions which match.
22
+ # This should be called before "matches?" and other checking methods since they
23
+ # rely on the actions to be expanded.
24
+ def expand_actions(aliased_actions)
25
+ @expanded_actions = @actions.map do |action|
26
+ aliased_actions[action] ? [action, *aliased_actions[action]] : action
27
+ end.flatten
28
+ end
29
+
30
+ def matches?(action, subject)
31
+ matches_action?(action) && matches_subject?(subject)
32
+ end
33
+
34
+ def can?(action, subject, extra_args)
35
+ result = can_without_base_behavior?(action, subject, extra_args)
36
+ @base_behavior ? result : !result
37
+ end
38
+
39
+ # Returns a hash of conditions. If the ":tableize => true" option is passed
40
+ # it will pluralize the association conditions to match the table name.
41
+ def conditions(options = {})
42
+ if options[:tableize] && @conditions.kind_of?(Hash)
43
+ @conditions.inject({}) do |tableized_conditions, (name, value)|
44
+ name = tableize(name).to_sym if value.kind_of? Hash
45
+ tableized_conditions[name] = value
46
+ tableized_conditions
47
+ end
48
+ else
49
+ @conditions
50
+ end
51
+ end
52
+
53
+ def association_joins(conditions = @conditions)
54
+ joins = []
55
+ conditions.each do |name, value|
56
+ if value.kind_of? Hash
57
+ nested = association_joins(value)
58
+ if nested
59
+ joins << {name => nested}
60
+ else
61
+ joins << name
62
+ end
63
+ end
64
+ end
65
+ joins unless joins.empty?
66
+ end
67
+
68
+ private
69
+
70
+ def matches_action?(action)
71
+ @expanded_actions.include?(:manage) || @expanded_actions.include?(action)
72
+ end
73
+
74
+ def matches_subject?(subject)
75
+ @subjects.include?(:all) || @subjects.include?(subject) || @subjects.any? { |sub| sub.kind_of?(Class) && subject.kind_of?(sub) }
76
+ end
77
+
78
+ def can_without_base_behavior?(action, subject, extra_args)
79
+ if @block
80
+ call_block(action, subject, extra_args)
81
+ elsif @conditions && subject.class != Class
82
+ matches_conditions? subject
83
+ else
84
+ true
85
+ end
86
+ end
87
+
88
+ def matches_conditions?(subject, conditions = @conditions)
89
+ conditions.all? do |name, value|
90
+ attribute = subject.send(name)
91
+ if value.kind_of?(Hash)
92
+ if attribute.kind_of? Array
93
+ attribute.any? { |element| matches_conditions? element, value }
94
+ else
95
+ matches_conditions? attribute, value
96
+ end
97
+ elsif value.kind_of?(Array) || value.kind_of?(Range)
98
+ value.include? attribute
99
+ else
100
+ attribute == value
101
+ end
102
+ end
103
+ end
104
+
105
+ def call_block(action, subject, extra_args)
106
+ block_args = []
107
+ block_args << action if @expanded_actions.include?(:manage)
108
+ block_args << (subject.class == Class ? subject : subject.class) if @subjects.include?(:all)
109
+ block_args << (subject.class == Class ? nil : subject)
110
+ block_args += extra_args
111
+ @block.call(*block_args)
112
+ end
113
+ end
114
+ end