Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Newer
Older
100644 328 lines (307 sloc) 12.328 kb
0cfb8c7 Ryan Bates adding basic ability module
authored
1 module CanCan
dfd84a1 Ryan Bates improving inline documentation
authored
2
b9227eb Ryan Bates adding a lot of inline documentation to code for rdocs
authored
3 # This module is designed to be included into an Ability class. This will
4 # provide the "can" methods for defining and checking abilities.
dfd84a1 Ryan Bates improving inline documentation
authored
5 #
b9227eb Ryan Bates adding a lot of inline documentation to code for rdocs
authored
6 # class Ability
7 # include CanCan::Ability
8 #
9 # def initialize(user)
10 # if user.admin?
11 # can :manage, :all
12 # else
13 # can :read, :all
14 # end
15 # end
16 # end
dfd84a1 Ryan Bates improving inline documentation
authored
17 #
0cfb8c7 Ryan Bates adding basic ability module
authored
18 module Ability
dfd84a1 Ryan Bates improving inline documentation
authored
19 # Use to check if the user has permission to perform a given action on an object.
20 #
5bd1a85 Ryan Bates little fixes to inline documentation (rdocs)
authored
21 # can? :destroy, @project
dfd84a1 Ryan Bates improving inline documentation
authored
22 #
5bd1a85 Ryan Bates little fixes to inline documentation (rdocs)
authored
23 # You can also pass the class instead of an instance (if you don't have one handy).
dfd84a1 Ryan Bates improving inline documentation
authored
24 #
5bd1a85 Ryan Bates little fixes to inline documentation (rdocs)
authored
25 # can? :create, Project
dfd84a1 Ryan Bates improving inline documentation
authored
26 #
510cf50 Ryan Bates adding documentation for passing additional arguments to can?
authored
27 # Any additional arguments will be passed into the "can" block definition. This
28 # can be used to pass more information about the user's request for example.
dfd84a1 Ryan Bates improving inline documentation
authored
29 #
510cf50 Ryan Bates adding documentation for passing additional arguments to can?
authored
30 # can? :create, Project, request.remote_ip
dfd84a1 Ryan Bates improving inline documentation
authored
31 #
510cf50 Ryan Bates adding documentation for passing additional arguments to can?
authored
32 # can :create Project do |project, remote_ip|
33 # # ...
34 # end
dfd84a1 Ryan Bates improving inline documentation
authored
35 #
36 # Not only can you use the can? method in the controller and view (see ControllerAdditions),
b9227eb Ryan Bates adding a lot of inline documentation to code for rdocs
authored
37 # but you can also call it directly on an ability instance.
dfd84a1 Ryan Bates improving inline documentation
authored
38 #
b9227eb Ryan Bates adding a lot of inline documentation to code for rdocs
authored
39 # ability.can? :destroy, @project
dfd84a1 Ryan Bates improving inline documentation
authored
40 #
b9227eb Ryan Bates adding a lot of inline documentation to code for rdocs
authored
41 # This makes testing a user's abilities very easy.
dfd84a1 Ryan Bates improving inline documentation
authored
42 #
b9227eb Ryan Bates adding a lot of inline documentation to code for rdocs
authored
43 # def test "user can only destroy projects which he owns"
44 # user = User.new
45 # ability = Ability.new(user)
46 # assert ability.can?(:destroy, Project.new(:user => user))
47 # assert ability.cannot?(:destroy, Project.new)
48 # end
dfd84a1 Ryan Bates improving inline documentation
authored
49 #
50 # Also see the RSpec Matchers to aid in testing.
d9f3c8b Ryan Bates renaming noun to subject internally
authored
51 def can?(action, subject, *extra_args)
4da31c0 Ryan Bates can has cheezburger? (thanks Seivan)
authored
52 raise Error, "Nom nom nom. I eated it." if action == :has && subject == :cheezburger
6084814 Ryan Bates refactoring can definition matching behavior
authored
53 match = relevant_can_definitions(action, subject).detect do |can_definition|
54 can_definition.matches_conditions?(action, subject, extra_args)
7d7d249 Sokolov Yura passing throw matching rules with not matching conditions
funny-falcon authored
55 end
6084814 Ryan Bates refactoring can definition matching behavior
authored
56 match ? match.base_behavior : false
0cfb8c7 Ryan Bates adding basic ability module
authored
57 end
dfd84a1 Ryan Bates improving inline documentation
authored
58
b9227eb Ryan Bates adding a lot of inline documentation to code for rdocs
authored
59 # Convenience method which works the same as "can?" but returns the opposite value.
dfd84a1 Ryan Bates improving inline documentation
authored
60 #
b9227eb Ryan Bates adding a lot of inline documentation to code for rdocs
authored
61 # cannot? :destroy, @project
dfd84a1 Ryan Bates improving inline documentation
authored
62 #
0f49b54 Ryan Bates adding 'cannot?' method which performs opposite check of 'can?' - closes...
authored
63 def cannot?(*args)
64 !can?(*args)
65 end
dfd84a1 Ryan Bates improving inline documentation
authored
66
b9227eb Ryan Bates adding a lot of inline documentation to code for rdocs
authored
67 # Defines which abilities are allowed using two arguments. The first one is the action
68 # you're setting the permission for, the second one is the class of object you're setting it on.
dfd84a1 Ryan Bates improving inline documentation
authored
69 #
b9227eb Ryan Bates adding a lot of inline documentation to code for rdocs
authored
70 # can :update, Article
dfd84a1 Ryan Bates improving inline documentation
authored
71 #
b9227eb Ryan Bates adding a lot of inline documentation to code for rdocs
authored
72 # You can pass an array for either of these parameters to match any one.
73 #
74 # can [:update, :destroy], [Article, Comment]
75 #
76 # In this case the user has the ability to update or destroy both articles and comments.
dfd84a1 Ryan Bates improving inline documentation
authored
77 #
baeef0b Ryan Bates adding conditions behavior to Ability#can and fetch with Ability#conditi...
authored
78 # You can pass a hash of conditions as the third argument.
b9227eb Ryan Bates adding a lot of inline documentation to code for rdocs
authored
79 #
baeef0b Ryan Bates adding conditions behavior to Ability#can and fetch with Ability#conditi...
authored
80 # can :read, Project, :active => true, :user_id => user.id
dfd84a1 Ryan Bates improving inline documentation
authored
81 #
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.
84 #
baeef0b Ryan Bates adding conditions behavior to Ability#can and fetch with Ability#conditi...
authored
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.
b9227eb Ryan Bates adding a lot of inline documentation to code for rdocs
authored
87 #
baeef0b Ryan Bates adding conditions behavior to Ability#can and fetch with Ability#conditi...
authored
88 # can :update, Project do |project|
89 # project && project.groups.include?(user.group)
b9227eb Ryan Bates adding a lot of inline documentation to code for rdocs
authored
90 # end
dfd84a1 Ryan Bates improving inline documentation
authored
91 #
baeef0b Ryan Bates adding conditions behavior to Ability#can and fetch with Ability#conditi...
authored
92 # If the block returns true then the user has that :update ability for that project, otherwise he
b9227eb Ryan Bates adding a lot of inline documentation to code for rdocs
authored
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.
dfd84a1 Ryan Bates improving inline documentation
authored
95 #
baeef0b Ryan Bates adding conditions behavior to Ability#can and fetch with Ability#conditi...
authored
96 # The downside to using a block is that it cannot be used to generate conditions for database queries.
dfd84a1 Ryan Bates improving inline documentation
authored
97 #
b9227eb Ryan Bates adding a lot of inline documentation to code for rdocs
authored
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).
dfd84a1 Ryan Bates improving inline documentation
authored
100 #
b9227eb Ryan Bates adding a lot of inline documentation to code for rdocs
authored
101 # can :read, :all do |object_class, object|
102 # object_class != Order
103 # end
dfd84a1 Ryan Bates improving inline documentation
authored
104 #
b9227eb Ryan Bates adding a lot of inline documentation to code for rdocs
authored
105 # Here the user has permission to read all objects except orders.
dfd84a1 Ryan Bates improving inline documentation
authored
106 #
107 # You can also pass :manage as the action which will match any action. In this case the action is
b9227eb Ryan Bates adding a lot of inline documentation to code for rdocs
authored
108 # passed to the block.
dfd84a1 Ryan Bates improving inline documentation
authored
109 #
b9227eb Ryan Bates adding a lot of inline documentation to code for rdocs
authored
110 # can :manage, Comment do |action, comment|
111 # action != :destroy
112 # end
dfd84a1 Ryan Bates improving inline documentation
authored
113 #
e603655 Ryan Bates support custom objects (usually symbols) in can definition - closes #8
authored
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.
dfd84a1 Ryan Bates improving inline documentation
authored
116 #
e603655 Ryan Bates support custom objects (usually symbols) in can definition - closes #8
authored
117 # can :read, :stats
118 # can? :read, :stats # => true
dfd84a1 Ryan Bates improving inline documentation
authored
119 #
d9f3c8b Ryan Bates renaming noun to subject internally
authored
120 def can(action, subject, conditions = nil, &block)
bbbc8a6 Ryan Bates refactoring much of Ability class into separate CanDefinition class
authored
121 can_definitions << CanDefinition.new(true, action, subject, conditions, block)
d4405e6 Ryan Bates adding cannot method to define which abilities cannot be done - closes #...
authored
122 end
dfd84a1 Ryan Bates improving inline documentation
authored
123
124 # Defines an ability which cannot be done. Accepts the same arguments as "can".
125 #
d4405e6 Ryan Bates adding cannot method to define which abilities cannot be done - closes #...
authored
126 # can :read, :all
127 # cannot :read, Comment
dfd84a1 Ryan Bates improving inline documentation
authored
128 #
d4405e6 Ryan Bates adding cannot method to define which abilities cannot be done - closes #...
authored
129 # A block can be passed just like "can", however if the logic is complex it is recommended
130 # to use the "can" method.
dfd84a1 Ryan Bates improving inline documentation
authored
131 #
d4405e6 Ryan Bates adding cannot method to define which abilities cannot be done - closes #...
authored
132 # cannot :read, Product do |product|
133 # product.invisible?
134 # end
dfd84a1 Ryan Bates improving inline documentation
authored
135 #
d9f3c8b Ryan Bates renaming noun to subject internally
authored
136 def cannot(action, subject, conditions = nil, &block)
bbbc8a6 Ryan Bates refactoring much of Ability class into separate CanDefinition class
authored
137 can_definitions << CanDefinition.new(false, action, subject, conditions, block)
4b6f538 Ryan Bates moving can definition into ability instance instead of class, this remov...
authored
138 end
dfd84a1 Ryan Bates improving inline documentation
authored
139
5bd1a85 Ryan Bates little fixes to inline documentation (rdocs)
authored
140 # Alias one or more actions into another one.
dfd84a1 Ryan Bates improving inline documentation
authored
141 #
b9227eb Ryan Bates adding a lot of inline documentation to code for rdocs
authored
142 # alias_action :update, :destroy, :to => :modify
143 # can :modify, Comment
dfd84a1 Ryan Bates improving inline documentation
authored
144 #
5bd1a85 Ryan Bates little fixes to inline documentation (rdocs)
authored
145 # Then :modify permission will apply to both :update and :destroy requests.
dfd84a1 Ryan Bates improving inline documentation
authored
146 #
5bd1a85 Ryan Bates little fixes to inline documentation (rdocs)
authored
147 # can? :update, Comment # => true
148 # can? :destroy, Comment # => true
dfd84a1 Ryan Bates improving inline documentation
authored
149 #
5bd1a85 Ryan Bates little fixes to inline documentation (rdocs)
authored
150 # This only works in one direction. Passing the aliased action into the "can?" call
151 # will not work because aliases are meant to generate more generic actions.
dfd84a1 Ryan Bates improving inline documentation
authored
152 #
5bd1a85 Ryan Bates little fixes to inline documentation (rdocs)
authored
153 # alias_action :update, :destroy, :to => :modify
154 # can :update, Comment
155 # can? :modify, Comment # => false
dfd84a1 Ryan Bates improving inline documentation
authored
156 #
5bd1a85 Ryan Bates little fixes to inline documentation (rdocs)
authored
157 # Unless that exact alias is used.
dfd84a1 Ryan Bates improving inline documentation
authored
158 #
5bd1a85 Ryan Bates little fixes to inline documentation (rdocs)
authored
159 # can :modify, Comment
160 # can? :modify, Comment # => true
dfd84a1 Ryan Bates improving inline documentation
authored
161 #
b9227eb Ryan Bates adding a lot of inline documentation to code for rdocs
authored
162 # The following aliases are added by default for conveniently mapping common controller actions.
dfd84a1 Ryan Bates improving inline documentation
authored
163 #
b9227eb Ryan Bates adding a lot of inline documentation to code for rdocs
authored
164 # alias_action :index, :show, :to => :read
165 # alias_action :new, :to => :create
166 # alias_action :edit, :to => :update
dfd84a1 Ryan Bates improving inline documentation
authored
167 #
5bd1a85 Ryan Bates little fixes to inline documentation (rdocs)
authored
168 # This way one can use params[:action] in the controller to determine the permission.
4b6f538 Ryan Bates moving can definition into ability instance instead of class, this remov...
authored
169 def alias_action(*args)
170 target = args.pop[:to]
f99d506 Ryan Bates Append aliased actions (don't overwrite them) - closes #20
authored
171 aliased_actions[target] ||= []
172 aliased_actions[target] += args
4b6f538 Ryan Bates moving can definition into ability instance instead of class, this remov...
authored
173 end
dfd84a1 Ryan Bates improving inline documentation
authored
174
7d3b4cd Ryan Bates Adding clear_aliased_actions to Ability which removes previously defined...
authored
175 # Returns a hash of aliased actions. The key is the target and the value is an array of actions aliasing the key.
c40490d Ryan Bates refactoring ability can? method - closes #12
authored
176 def aliased_actions
177 @aliased_actions ||= default_alias_actions
178 end
dfd84a1 Ryan Bates improving inline documentation
authored
179
7d3b4cd Ryan Bates Adding clear_aliased_actions to Ability which removes previously defined...
authored
180 # Removes previously aliased actions including the defaults.
181 def clear_aliased_actions
182 @aliased_actions = {}
183 end
dfd84a1 Ryan Bates improving inline documentation
authored
184
7d7d249 Sokolov Yura passing throw matching rules with not matching conditions
funny-falcon authored
185 # Returns an array of arrays composing from desired action and hash of conditions which match the given ability.
186 # This is useful if you need to generate a database query based on the current ability.
baeef0b Ryan Bates adding conditions behavior to Ability#can and fetch with Ability#conditi...
authored
187 #
188 # can :read, Article, :visible => true
7d7d249 Sokolov Yura passing throw matching rules with not matching conditions
funny-falcon authored
189 # conditions :read, Article # returns [ [ true, { :visible => true } ] ]
190 #
191 # can :read, Article, :visible => true
192 # cannot :read, Article, :blocked => true
193 # conditions :read, Article # returns [ [ false, { :blocked => true } ], [ true, { :visible => true } ] ]
baeef0b Ryan Bates adding conditions behavior to Ability#can and fetch with Ability#conditi...
authored
194 #
240c281 Ryan Bates renaming ActiveRecordAdditions#can method to accessible_by since it flow...
authored
195 # Normally you will not call this method directly, but instead go through ActiveRecordAdditions#accessible_by method.
dfd84a1 Ryan Bates improving inline documentation
authored
196 #
baeef0b Ryan Bates adding conditions behavior to Ability#can and fetch with Ability#conditi...
authored
197 # If the ability is not defined then false is returned so be sure to take that into consideration.
198 # If the ability is defined using a block then this will raise an exception since a hash of conditions cannot be
199 # determined from that.
605063b Logan Raarup Make sure conditions on associations are pluralized
logandk authored
200 def conditions(action, subject, options = {})
6084814 Ryan Bates refactoring can definition matching behavior
authored
201 relevant = relevant_can_definitions(action, subject)
202 unless relevant.empty?
203 if relevant.any?{|can_definition| can_definition.only_block? }
7d7d249 Sokolov Yura passing throw matching rules with not matching conditions
funny-falcon authored
204 raise Error, "Cannot determine ability conditions from block for #{action.inspect} #{subject.inspect}"
205 end
6084814 Ryan Bates refactoring can definition matching behavior
authored
206 relevant.map{|can_definition|
46f0301 Merge remote branch 'upstream/master'
Yura Sokolov authored
207 [can_definition.base_behavior, can_definition.conditions(options)]
7d7d249 Sokolov Yura passing throw matching rules with not matching conditions
funny-falcon authored
208 }
bbbc8a6 Ryan Bates refactoring much of Ability class into separate CanDefinition class
authored
209 else
210 false
baeef0b Ryan Bates adding conditions behavior to Ability#can and fetch with Ability#conditi...
authored
211 end
212 end
213
7d7d249 Sokolov Yura passing throw matching rules with not matching conditions
funny-falcon authored
214 # Returns sql conditions for object, which responds to :sanitize_sql .
215 # This is useful if you need to generate a database query based on the current ability.
216 #
217 # can :manage, User, :id => 1
218 # can :manage, User, :manager_id => 1
219 # cannot :manage, User, :self_managed => true
220 # sql_conditions :manage, User # returns not (self_managed = 't') AND ((manager_id = 1) OR (id = 1))
221 #
222 # Normally you will not call this method directly, but instead go through ActiveRecordAdditions#accessible_by method.
223 #
224 # If the ability is not defined then false is returned so be sure to take that into consideration.
225 # If there is just one :can ability, it conditions returned untouched.
226 # If the ability is defined using a block then this will raise an exception since a hash of conditions cannot be
227 # determined from that.
46f0301 Merge remote branch 'upstream/master'
Yura Sokolov authored
228 def sql_conditions(action, subject, options = {})
229 conds = conditions(action, subject, options)
7d7d249 Sokolov Yura passing throw matching rules with not matching conditions
funny-falcon authored
230 return false if conds == false
231 return (conds[0][1] || {}) if conds.size==1 && conds[0][0] == true # to match previous spec
232
233 true_cond = subject.send(:sanitize_sql, ['?=?', true, true])
234 false_cond = subject.send(:sanitize_sql, ['?=?', true, false])
5fd7930 fix logic error for single `cannot` condition - it should return no reco...
Yura Sokolov authored
235 conds.reverse.inject(false_cond) do |sql, action|
7d7d249 Sokolov Yura passing throw matching rules with not matching conditions
funny-falcon authored
236 behavior, condition = action
dbc1538 small refactoring: CanDefinition #definitive? #conditions_empty?
Yura Sokolov authored
237 if condition && condition != {}
bcab8d6 fix error with single cannot condition
Yura Sokolov authored
238 condition = subject.send(:sanitize_sql, condition)
dbc1538 small refactoring: CanDefinition #definitive? #conditions_empty?
Yura Sokolov authored
239 case sql
240 when true_cond
241 behavior ? true_cond : "not (#{condition})"
242 when false_cond
243 behavior ? condition : false_cond
244 else
245 behavior ? "(#{condition}) OR (#{sql})" : "not (#{condition}) AND (#{sql})"
246 end
7d7d249 Sokolov Yura passing throw matching rules with not matching conditions
funny-falcon authored
247 else
dbc1538 small refactoring: CanDefinition #definitive? #conditions_empty?
Yura Sokolov authored
248 behavior ? true_cond : false_cond
7d7d249 Sokolov Yura passing throw matching rules with not matching conditions
funny-falcon authored
249 end
250 end
251 end
252
e200814 Ryan Bates adding joins clause to accessible_by when conditions are across associat...
authored
253 # Returns the associations used in conditions. This is usually used in the :joins option for a search.
254 # See ActiveRecordAdditions#accessible_by for use in Active Record.
255 def association_joins(action, subject)
6084814 Ryan Bates refactoring can definition matching behavior
authored
256 can_definitions = relevant_can_definitions(action, subject)
7d7d249 Sokolov Yura passing throw matching rules with not matching conditions
funny-falcon authored
257 unless can_definitions.empty?
b473d88 CanDefinition#only_block?
Yura Sokolov authored
258 if can_definitions.any?{|can_definition| can_definition.only_block? }
7d7d249 Sokolov Yura passing throw matching rules with not matching conditions
funny-falcon authored
259 raise Error, "Cannot determine association joins from block for #{action.inspect} #{subject.inspect}"
260 end
261 collect_association_joins(can_definitions)
262 else
263 nil
e200814 Ryan Bates adding joins clause to accessible_by when conditions are across associat...
authored
264 end
265 end
7d7d249 Sokolov Yura passing throw matching rules with not matching conditions
funny-falcon authored
266
7d3b4cd Ryan Bates Adding clear_aliased_actions to Ability which removes previously defined...
authored
267 private
cad4259 Ryan Bates supporting deeply nested aliases - closes #98
authored
268
269 # Accepts a hash of aliased actions and returns an array of actions which match.
270 # This should be called before "matches?" and other checking methods since they
271 # rely on the actions to be expanded.
272 def expand_actions(actions)
273 actions.map do |action|
274 aliased_actions[action] ? [action, *expand_actions(aliased_actions[action])] : action
275 end.flatten
276 end
dfd84a1 Ryan Bates improving inline documentation
authored
277
bbbc8a6 Ryan Bates refactoring much of Ability class into separate CanDefinition class
authored
278 def can_definitions
279 @can_definitions ||= []
280 end
dfd84a1 Ryan Bates improving inline documentation
authored
281
6084814 Ryan Bates refactoring can definition matching behavior
authored
282 # Returns an array of CanDefinition instances which match the action and subject
283 # This does not take into consideration any hash conditions or block statements
284 def relevant_can_definitions(action, subject)
285 can_definitions.reverse.select do |can_definition|
286 can_definition.expanded_actions = expand_actions(can_definition.actions)
287 can_definition.relevant? action, subject
baeef0b Ryan Bates adding conditions behavior to Ability#can and fetch with Ability#conditi...
authored
288 end
289 end
dfd84a1 Ryan Bates improving inline documentation
authored
290
4b6f538 Ryan Bates moving can definition into ability instance instead of class, this remov...
authored
291 def default_alias_actions
292 {
293 :read => [:index, :show],
294 :create => [:new],
295 :update => [:edit],
296 }
297 end
7d7d249 Sokolov Yura passing throw matching rules with not matching conditions
funny-falcon authored
298
299 def collect_association_joins(can_definitions)
300 joins = []
301 can_definitions.each do |can_definition|
302 merge_association_joins(joins, can_definition.association_joins || [])
303 end
9c0346b can accept array for sql sanitizing in conditions
Yura Sokolov authored
304 joins = clear_association_joins(joins)
305 joins unless joins.empty?
7d7d249 Sokolov Yura passing throw matching rules with not matching conditions
funny-falcon authored
306 end
307
308 def merge_association_joins(what, with)
309 with.each do |join|
310 name, nested = join.each_pair.first
311 if at = what.detect{|h| h.has_key?(name) }
312 at[name] = merge_association_joins(at[name], nested)
313 else
314 what << join
315 end
316 end
317 end
318
319 def clear_association_joins(joins)
320 joins.map do |join|
321 name, nested = join.each_pair.first
322 nested.empty? ? name : {name => clear_association_joins(nested)}
323 end
324 end
325
0cfb8c7 Ryan Bates adding basic ability module
authored
326 end
327 end
Something went wrong with that request. Please try again.