cancan 1.1.1 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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