cancan 1.3.4 → 1.4.0.beta1

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.
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