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 +19 -0
- data/README.rdoc +1 -1
- data/Rakefile +1 -1
- data/lib/cancan/ability.rb +80 -31
- data/lib/cancan/can_definition.rb +33 -11
- data/lib/cancan/controller_additions.rb +59 -6
- data/lib/cancan/controller_resource.rb +18 -6
- data/lib/cancan/exceptions.rb +5 -1
- data/spec/cancan/ability_spec.rb +128 -59
- data/spec/cancan/active_record_additions_spec.rb +1 -1
- data/spec/cancan/can_definition_spec.rb +1 -0
- data/spec/cancan/controller_additions_spec.rb +25 -26
- data/spec/cancan/controller_resource_spec.rb +92 -83
- data/spec/cancan/query_spec.rb +48 -48
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +12 -2
- metadata +5 -4
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
data/lib/cancan/ability.rb
CHANGED
@@ -16,7 +16,7 @@ module CanCan
|
|
16
16
|
# end
|
17
17
|
#
|
18
18
|
module Ability
|
19
|
-
#
|
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
|
-
#
|
81
|
+
# You can pass :all to match any object and :manage to match any action. Here are some examples.
|
77
82
|
#
|
78
|
-
#
|
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
|
-
#
|
83
|
-
# for
|
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
|
86
|
-
#
|
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
|
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.
|
94
|
-
#
|
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
|
-
#
|
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
|
-
#
|
99
|
-
#
|
108
|
+
# can :read, :stats
|
109
|
+
# can? :read, :stats # => true
|
100
110
|
#
|
101
|
-
#
|
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
|
-
#
|
113
|
+
# can :update, Project, :priority => 3
|
114
|
+
# can? :update, Project # => true
|
106
115
|
#
|
107
|
-
#
|
108
|
-
#
|
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
|
111
|
-
#
|
120
|
+
# can do |action, object_class, object|
|
121
|
+
# # check the database and return true/false
|
112
122
|
# end
|
113
123
|
#
|
114
|
-
|
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
|
-
|
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
|
-
|
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 @
|
29
|
-
|
30
|
-
elsif @
|
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
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
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!(
|
189
|
-
|
190
|
-
|
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 ||
|
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
|
-
|
54
|
-
|
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
|
98
|
-
@options[:singleton] ?
|
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
|
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
|
|