cancan 1.3.4 → 1.4.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG.rdoc CHANGED
@@ -1,3 +1,22 @@
1
+ 1.4.0 (not yet released)
2
+
3
+ * Adding check_authorization and skip_authorization controller class methods to ensure authorization is performed (thanks justinko) - see issue #135
4
+
5
+ * Setting initial attributes based on ability conditions in new/create actions - see issue #114
6
+
7
+ * Check parent attributes for nested association in index action - see issue #121
8
+
9
+ * Supporting nesting in can? method using hash - see issue #121
10
+
11
+ * Adding I18n support for Access Denied messages (thanks EppO) - see issue #103
12
+
13
+ * Passing no arguments to +can+ definition will pass action, class, and object to block - see issue #129
14
+
15
+ * Don't pass action to block in +can+ definition when using :+manage+ option - see issue #129
16
+
17
+ * No longer calling block in +can+ definition when checking on class - see issue #116
18
+
19
+
1
20
  1.3.4 (August 31, 2010)
2
21
 
3
22
  * Don't stop at +cannot+ with hash conditions when checking class (thanks tamoya) - see issue #131
data/README.rdoc CHANGED
@@ -126,7 +126,7 @@ Notice the +edit+ action is aliased to +update+. If the user is able to update a
126
126
  can :modify, Comment
127
127
  can? :update, Comment # => true
128
128
 
129
- See {Custom Actions}[http://wiki.github.com/ryanb/cancan/custom-actions] for information on adding other actions.
129
+ The +alias_action+ method is an instance method and usually called in +initialize+. See {Custom Actions}[http://wiki.github.com/ryanb/cancan/custom-actions] for information on adding other actions.
130
130
 
131
131
 
132
132
  == Fetching Records
data/Rakefile CHANGED
@@ -10,4 +10,4 @@ Spec::Rake::SpecTask.new do |t|
10
10
  t.spec_opts = ["-c"]
11
11
  end
12
12
 
13
- task :default => :spec
13
+ task :default => :spec
@@ -16,7 +16,7 @@ module CanCan
16
16
  # end
17
17
  #
18
18
  module Ability
19
- # Use to check if the user has permission to perform a given action on an object.
19
+ # Check if the user has permission to perform a given action on an object.
20
20
  #
21
21
  # can? :destroy, @project
22
22
  #
@@ -24,6 +24,11 @@ module CanCan
24
24
  #
25
25
  # can? :create, Project
26
26
  #
27
+ # Nested resources can be passed through a hash, this way conditions which are
28
+ # dependent upon the association will work when using a class.
29
+ #
30
+ # can? :create, @category => Project
31
+ #
27
32
  # Any additional arguments will be passed into the "can" block definition. This
28
33
  # can be used to pass more information about the user's request for example.
29
34
  #
@@ -49,7 +54,6 @@ module CanCan
49
54
  #
50
55
  # Also see the RSpec Matchers to aid in testing.
51
56
  def can?(action, subject, *extra_args)
52
- raise Error, "Nom nom nom. I eated it." if action == :has && subject == :cheezburger
53
57
  match = relevant_can_definitions(action, subject).detect do |can_definition|
54
58
  can_definition.matches_conditions?(action, subject, extra_args)
55
59
  end
@@ -70,54 +74,54 @@ module CanCan
70
74
  # can :update, Article
71
75
  #
72
76
  # You can pass an array for either of these parameters to match any one.
77
+ # Here the user has the ability to update or destroy both articles and comments.
73
78
  #
74
79
  # can [:update, :destroy], [Article, Comment]
75
80
  #
76
- # In this case the user has the ability to update or destroy both articles and comments.
81
+ # You can pass :all to match any object and :manage to match any action. Here are some examples.
77
82
  #
78
- # You can pass a hash of conditions as the third argument.
83
+ # can :manage, :all
84
+ # can :update, :all
85
+ # can :manage, Project
86
+ #
87
+ # You can pass a hash of conditions as the third argument. Here the user can only see active projects which he owns.
79
88
  #
80
89
  # can :read, Project, :active => true, :user_id => user.id
81
90
  #
82
- # Here the user can only see active projects which he owns. See ActiveRecordAdditions#accessible_by
83
- # for how to use this in database queries.
91
+ # See ActiveRecordAdditions#accessible_by for how to use this in database queries. These conditions
92
+ # are also used for initial attributes when building a record in ControllerAdditions#load_resource.
84
93
  #
85
- # If the conditions hash does not give you enough control over defining abilities, you can use a block to
86
- # write any Ruby code you want.
94
+ # If the conditions hash does not give you enough control over defining abilities, you can use a block
95
+ # along with any Ruby code you want.
87
96
  #
88
97
  # can :update, Project do |project|
89
- # project && project.groups.include?(user.group)
98
+ # project.groups.include?(user.group)
90
99
  # end
91
100
  #
92
101
  # If the block returns true then the user has that :update ability for that project, otherwise he
93
- # will be denied access. It's possible for the passed in model to be nil if one isn't specified,
94
- # so be sure to take that into consideration.
102
+ # will be denied access. The downside to using a block is that it cannot be used to generate
103
+ # conditions for database queries.
95
104
  #
96
- # The downside to using a block is that it cannot be used to generate conditions for database queries.
105
+ # You can pass custom objects into this "can" method, this is usually done with a symbol
106
+ # and is useful if a class isn't available to define permissions on.
97
107
  #
98
- # You can pass :all to reference every type of object. In this case the object type will be passed
99
- # into the block as well (just in case object is nil).
108
+ # can :read, :stats
109
+ # can? :read, :stats # => true
100
110
  #
101
- # can :read, :all do |object_class, object|
102
- # object_class != Order
103
- # end
111
+ # IMPORTANT: Neither a hash of conditions or a block will be used when checking permission on a class.
104
112
  #
105
- # Here the user has permission to read all objects except orders.
113
+ # can :update, Project, :priority => 3
114
+ # can? :update, Project # => true
106
115
  #
107
- # You can also pass :manage as the action which will match any action. In this case the action is
108
- # passed to the block.
116
+ # If you pass no arguments to +can+, the action, class, and object will be passed to the block and the
117
+ # block will always be executed. This allows you to override the full behavior if the permissions are
118
+ # defined in an external source such as the database.
109
119
  #
110
- # can :manage, Comment do |action, comment|
111
- # action != :destroy
120
+ # can do |action, object_class, object|
121
+ # # check the database and return true/false
112
122
  # end
113
123
  #
114
- # You can pass custom objects into this "can" method, this is usually done through a symbol
115
- # and is useful if a class isn't available to define permissions on.
116
- #
117
- # can :read, :stats
118
- # can? :read, :stats # => true
119
- #
120
- def can(action, subject, conditions = nil, &block)
124
+ def can(action = nil, subject = nil, conditions = nil, &block)
121
125
  can_definitions << CanDefinition.new(true, action, subject, conditions, block)
122
126
  end
123
127
 
@@ -133,7 +137,7 @@ module CanCan
133
137
  # product.invisible?
134
138
  # end
135
139
  #
136
- def cannot(action, subject, conditions = nil, &block)
140
+ def cannot(action = nil, subject = nil, conditions = nil, &block)
137
141
  can_definitions << CanDefinition.new(false, action, subject, conditions, block)
138
142
  end
139
143
 
@@ -189,9 +193,44 @@ module CanCan
189
193
  Query.new(subject, relevant_can_definitions_for_query(action, subject))
190
194
  end
191
195
 
196
+ # See ControllerAdditions#authorize! for documentation.
197
+ def authorize!(action, subject, *args)
198
+ message = nil
199
+ if args.last.kind_of?(Hash) && args.last.has_key?(:message)
200
+ message = args.pop[:message]
201
+ end
202
+ if cannot?(action, subject, *args)
203
+ message ||= unauthorized_message(action, subject)
204
+ raise AccessDenied.new(message, action, subject)
205
+ end
206
+ end
207
+
208
+ def unauthorized_message(action, subject)
209
+ keys = unauthorized_message_keys(action, subject)
210
+ message = I18n.translate(nil, :scope => :unauthorized, :default => keys + [""])
211
+ message.blank? ? nil : message
212
+ end
213
+
214
+ def attributes_for(action, subject)
215
+ attributes = {}
216
+ relevant_can_definitions(action, subject).map do |can_definition|
217
+ attributes.merge!(can_definition.attributes_from_conditions) if can_definition.base_behavior
218
+ end
219
+ attributes
220
+ end
221
+
192
222
  private
193
223
 
194
- # Accepts a hash of aliased actions and returns an array of actions which match.
224
+ def unauthorized_message_keys(action, subject)
225
+ subject = (subject.class == Class ? subject : subject.class).name.underscore unless subject.kind_of? Symbol
226
+ [subject, :all].map do |try_subject|
227
+ [aliases_for_action(action), :manage].flatten.map do |try_action|
228
+ :"#{try_action}.#{try_subject}"
229
+ end
230
+ end.flatten
231
+ end
232
+
233
+ # Accepts an array of actions and returns an array of actions which match.
195
234
  # This should be called before "matches?" and other checking methods since they
196
235
  # rely on the actions to be expanded.
197
236
  def expand_actions(actions)
@@ -200,6 +239,16 @@ module CanCan
200
239
  end.flatten
201
240
  end
202
241
 
242
+ # Given an action, it will try to find all of the actions which are aliased to it.
243
+ # This does the opposite kind of lookup as expand_actions.
244
+ def aliases_for_action(action)
245
+ results = [action]
246
+ aliased_actions.each do |aliased_action, actions|
247
+ results += aliases_for_action(aliased_action) if actions.include? action
248
+ end
249
+ results
250
+ end
251
+
203
252
  def can_definitions
204
253
  @can_definitions ||= []
205
254
  end
@@ -11,6 +11,7 @@ module CanCan
11
11
  # and subject respectively (such as :read, @project). The third argument is a hash
12
12
  # of conditions and the last one is the block passed to the "can" call.
13
13
  def initialize(base_behavior, action, subject, conditions, block)
14
+ @match_all = action.nil? && subject.nil?
14
15
  @base_behavior = base_behavior
15
16
  @actions = [action].flatten
16
17
  @subjects = [subject].flatten
@@ -20,14 +21,19 @@ module CanCan
20
21
 
21
22
  # Matches both the subject and action, not necessarily the conditions
22
23
  def relevant?(action, subject)
23
- matches_action?(action) && matches_subject?(subject)
24
+ subject = subject.values.first if subject.kind_of? Hash
25
+ @match_all || (matches_action?(action) && matches_subject?(subject))
24
26
  end
25
27
 
26
28
  # Matches the block or conditions hash
27
29
  def matches_conditions?(action, subject, extra_args)
28
- if @block
29
- call_block(action, subject, extra_args)
30
- elsif @conditions.kind_of?(Hash) && subject.class != Class
30
+ if @match_all
31
+ call_block_with_all(action, subject, extra_args)
32
+ elsif @block && !subject_class?(subject)
33
+ @block.call(subject, *extra_args)
34
+ elsif @conditions.kind_of?(Hash) && subject.kind_of?(Hash)
35
+ nested_subject_matches_conditions?(subject)
36
+ elsif @conditions.kind_of?(Hash) && !subject_class?(subject)
31
37
  matches_conditions_hash?(subject)
32
38
  else
33
39
  @base_behavior
@@ -61,8 +67,20 @@ module CanCan
61
67
  hash
62
68
  end
63
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
75
+ attributes
76
+ end
77
+
64
78
  private
65
79
 
80
+ def subject_class?(subject)
81
+ (subject.kind_of?(Hash) ? subject.values.first : subject).class == Class
82
+ end
83
+
66
84
  def matches_action?(action)
67
85
  @expanded_actions.include?(:manage) || @expanded_actions.include?(action)
68
86
  end
@@ -92,13 +110,17 @@ module CanCan
92
110
  end
93
111
  end
94
112
 
95
- def call_block(action, subject, extra_args)
96
- block_args = []
97
- block_args << action if @expanded_actions.include?(:manage)
98
- block_args << (subject.class == Class ? subject : subject.class) if @subjects.include?(:all)
99
- block_args << (subject.class == Class ? nil : subject)
100
- block_args += extra_args
101
- @block.call(*block_args)
113
+ def nested_subject_matches_conditions?(subject_hash)
114
+ parent, child = subject_hash.shift
115
+ matches_conditions_hash?(parent, @conditions[parent.class.name.downcase.to_sym] || {})
116
+ end
117
+
118
+ def call_block_with_all(action, subject, extra_args)
119
+ if subject.class == Class
120
+ @block.call(action, subject, nil, *extra_args)
121
+ else
122
+ @block.call(action, subject.class, subject, *extra_args)
123
+ end
102
124
  end
103
125
  end
104
126
  end
@@ -21,6 +21,10 @@ module CanCan
21
21
  # Article.new(params[:article]) depending upon the action. It does nothing for the "index"
22
22
  # action.
23
23
  #
24
+ # If a conditions hash is used in the Ability, the +new+ and +create+ actions will set
25
+ # the initial attributes based on these conditions. This way these actions will satisfy
26
+ # the ability restrictions.
27
+ #
24
28
  # Call this method directly on the controller class.
25
29
  #
26
30
  # class BooksController < ApplicationController
@@ -148,9 +152,44 @@ module CanCan
148
152
  # [:+instance_name+]
149
153
  # The name of the instance variable for this resource.
150
154
  #
155
+ # [:+through+]
156
+ # Authorize conditions on this parent resource when instance isn't available.
157
+ #
151
158
  def authorize_resource(*args)
152
159
  ControllerResource.add_before_filter(self, :authorize_resource, *args)
153
160
  end
161
+
162
+ # Add this to a controller to ensure it performs authorization through +authorized+! or +authorize_resource+ call.
163
+ # If neither of these authorization methods are called, a CanCan::AuthorizationNotPerformed exception will be raised.
164
+ # This is normally added to the ApplicationController to ensure all controller actions do authorization.
165
+ #
166
+ # class ApplicationController < ActionController::Base
167
+ # check_authorization
168
+ # end
169
+ #
170
+ # Any arguments are passed to the +after_filter+ it triggers.
171
+ #
172
+ # See skip_authorization to bypass this check on specific controller actions.
173
+ def check_authorization(*args)
174
+ self.after_filter(*args) do |controller|
175
+ unless controller.instance_variable_defined?(:@_authorized)
176
+ raise AuthorizationNotPerformed, "This action failed the check_authorization because it does not authorize_resource. Add skip_authorization to bypass this check."
177
+ end
178
+ end
179
+ end
180
+
181
+ # Call this in the class of a controller to skip the check_authorization behavior on the actions.
182
+ #
183
+ # class HomeController < ApplicationController
184
+ # skip_authorization :only => :index
185
+ # end
186
+ #
187
+ # Any arguments are passed to the +before_filter+ it triggers.
188
+ def skip_authorization(*args)
189
+ self.before_filter(*args) do |controller|
190
+ controller.instance_variable_set(:@_authorized, true)
191
+ end
192
+ end
154
193
  end
155
194
 
156
195
  def self.included(base)
@@ -171,6 +210,16 @@ module CanCan
171
210
  #
172
211
  # authorize! :read, @article, :message => "Not authorized to read #{@article.name}"
173
212
  #
213
+ # You can also use I18n to customize the message. Action aliases defined in Ability work here.
214
+ #
215
+ # en:
216
+ # unauthorized:
217
+ # manage:
218
+ # all: "Not authorized to perform that action."
219
+ # user: "Not allowed to manage other user accounts."
220
+ # update:
221
+ # project: "Not allowed to update this project."
222
+ #
174
223
  # You can rescue from the exception in the controller to customize how unauthorized
175
224
  # access is displayed to the user.
176
225
  #
@@ -185,12 +234,9 @@ module CanCan
185
234
  #
186
235
  # See the load_and_authorize_resource method to automatically add the authorize! behavior
187
236
  # to the default RESTful actions.
188
- def authorize!(action, subject, *args)
189
- message = nil
190
- if args.last.kind_of?(Hash) && args.last.has_key?(:message)
191
- message = args.pop[:message]
192
- end
193
- raise AccessDenied.new(message, action, subject) if cannot?(action, subject, *args)
237
+ def authorize!(*args)
238
+ @_authorized = true
239
+ current_ability.authorize!(*args)
194
240
  end
195
241
 
196
242
  def unauthorized!(message = nil)
@@ -223,6 +269,13 @@ module CanCan
223
269
  # <%= link_to "New Project", new_project_path %>
224
270
  # <% end %>
225
271
  #
272
+ # If it's a nested resource, you can pass the parent instance in a hash. This way it will
273
+ # check conditions which reach through that association.
274
+ #
275
+ # <% if can? :create, @category => Project %>
276
+ # <%= link_to "New Project", new_project_path %>
277
+ # <% end %>
278
+ #
226
279
  # This simply calls "can?" on the current_ability. See Ability#can?.
227
280
  def can?(*args)
228
281
  current_ability.can?(*args)
@@ -32,7 +32,7 @@ module CanCan
32
32
  end
33
33
 
34
34
  def authorize_resource
35
- @controller.authorize!(authorization_action, resource_instance || resource_class)
35
+ @controller.authorize!(authorization_action, resource_instance || resource_class_with_parent)
36
36
  end
37
37
 
38
38
  def parent?
@@ -50,8 +50,16 @@ module CanCan
50
50
  end
51
51
 
52
52
  def build_resource
53
- method_name = @options[:singleton] ? "build_#{name}" : "new"
54
- resource_base.send(*[method_name, @params[name]].compact)
53
+ resource = resource_base.send(@options[:singleton] ? "build_#{name}" : "new")
54
+ initial_attributes.each do |name, value|
55
+ resource.send("#{name}=", value)
56
+ end
57
+ resource.attributes = @params[name] if @params[name]
58
+ resource
59
+ end
60
+
61
+ def initial_attributes
62
+ @controller.current_ability.attributes_for(@params[:action].to_sym, resource_class)
55
63
  end
56
64
 
57
65
  def find_resource
@@ -86,6 +94,10 @@ module CanCan
86
94
  end
87
95
  end
88
96
 
97
+ def resource_class_with_parent
98
+ parent_resource ? {parent_resource => resource_class} : resource_class
99
+ end
100
+
89
101
  def resource_instance
90
102
  @controller.instance_variable_get("@#{instance_name}")
91
103
  end
@@ -94,15 +106,15 @@ module CanCan
94
106
  # If the :through option is passed it will go through an association on that instance.
95
107
  # If the :singleton option is passed it won't use the association because it needs to be handled later.
96
108
  def resource_base
97
- if through_resource
98
- @options[:singleton] ? through_resource : through_resource.send(name.to_s.pluralize)
109
+ if parent_resource
110
+ @options[:singleton] ? parent_resource : parent_resource.send(name.to_s.pluralize)
99
111
  else
100
112
  resource_class
101
113
  end
102
114
  end
103
115
 
104
116
  # The object to load this resource through.
105
- def through_resource
117
+ def parent_resource
106
118
  @options[:through] && [@options[:through]].flatten.map { |i| @controller.instance_variable_get("@#{i}") }.compact.first
107
119
  end
108
120