cancan 1.1.1 → 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.rdoc +15 -0
- data/lib/cancan.rb +1 -0
- data/lib/cancan/ability.rb +95 -135
- data/lib/cancan/active_record_additions.rb +10 -9
- data/lib/cancan/can_definition.rb +114 -0
- data/lib/cancan/controller_additions.rb +61 -51
- data/lib/cancan/controller_resource.rb +16 -9
- data/lib/cancan/exceptions.rb +11 -11
- data/lib/cancan/resource_authorization.rb +21 -19
- data/spec/cancan/ability_spec.rb +40 -28
- data/spec/cancan/active_record_additions_spec.rb +8 -8
- data/spec/cancan/can_definition_spec.rb +44 -0
- data/spec/cancan/controller_additions_spec.rb +9 -9
- data/spec/cancan/controller_resource_spec.rb +9 -9
- data/spec/cancan/exceptions_spec.rb +5 -5
- data/spec/cancan/resource_authorization_spec.rb +35 -17
- data/spec/spec_helper.rb +1 -1
- metadata +6 -4
data/CHANGELOG.rdoc
CHANGED
@@ -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
|
data/lib/cancan.rb
CHANGED
data/lib/cancan/ability.rb
CHANGED
@@ -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
|
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
|
-
|
52
|
-
|
53
|
-
|
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
|
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
|
-
|
121
|
-
@can_definitions << [true, action, subject, conditions, block]
|
119
|
+
can_definitions << CanDefinition.new(true, action, subject, conditions, block)
|
122
120
|
end
|
123
|
-
|
124
|
-
#
|
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
|
-
|
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)
|
199
|
-
|
200
|
-
|
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
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
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
|