Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Newer
Older
100644 328 lines (307 sloc) 12.328 kB
0cfb8c7 @ryanb adding basic ability module
authored
1 module CanCan
dfd84a1 @ryanb improving inline documentation
authored
2
b9227eb @ryanb 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 @ryanb improving inline documentation
authored
5 #
b9227eb @ryanb 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 @ryanb improving inline documentation
authored
17 #
0cfb8c7 @ryanb adding basic ability module
authored
18 module Ability
dfd84a1 @ryanb improving inline documentation
authored
19 # Use to check if the user has permission to perform a given action on an object.
20 #
5bd1a85 @ryanb little fixes to inline documentation (rdocs)
authored
21 # can? :destroy, @project
dfd84a1 @ryanb improving inline documentation
authored
22 #
5bd1a85 @ryanb 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 @ryanb improving inline documentation
authored
24 #
5bd1a85 @ryanb little fixes to inline documentation (rdocs)
authored
25 # can? :create, Project
dfd84a1 @ryanb improving inline documentation
authored
26 #
510cf50 @ryanb 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 @ryanb improving inline documentation
authored
29 #
510cf50 @ryanb adding documentation for passing additional arguments to can?
authored
30 # can? :create, Project, request.remote_ip
dfd84a1 @ryanb improving inline documentation
authored
31 #
510cf50 @ryanb adding documentation for passing additional arguments to can?
authored
32 # can :create Project do |project, remote_ip|
33 # # ...
34 # end
dfd84a1 @ryanb improving inline documentation
authored
35 #
36 # Not only can you use the can? method in the controller and view (see ControllerAdditions),
b9227eb @ryanb adding a lot of inline documentation to code for rdocs
authored
37 # but you can also call it directly on an ability instance.
dfd84a1 @ryanb improving inline documentation
authored
38 #
b9227eb @ryanb adding a lot of inline documentation to code for rdocs
authored
39 # ability.can? :destroy, @project
dfd84a1 @ryanb improving inline documentation
authored
40 #
b9227eb @ryanb adding a lot of inline documentation to code for rdocs
authored
41 # This makes testing a user's abilities very easy.
dfd84a1 @ryanb improving inline documentation
authored
42 #
b9227eb @ryanb 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 @ryanb improving inline documentation
authored
49 #
50 # Also see the RSpec Matchers to aid in testing.
d9f3c8b @ryanb renaming noun to subject internally
authored
51 def can?(action, subject, *extra_args)
4da31c0 @ryanb can has cheezburger? (thanks Seivan)
authored
52 raise Error, "Nom nom nom. I eated it." if action == :has && subject == :cheezburger
6084814 @ryanb 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 @funny-falcon passing throw matching rules with not matching conditions
funny-falcon authored
55 end
6084814 @ryanb refactoring can definition matching behavior
authored
56 match ? match.base_behavior : false
0cfb8c7 @ryanb adding basic ability module
authored
57 end
dfd84a1 @ryanb improving inline documentation
authored
58
b9227eb @ryanb 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 @ryanb improving inline documentation
authored
60 #
b9227eb @ryanb adding a lot of inline documentation to code for rdocs
authored
61 # cannot? :destroy, @project
dfd84a1 @ryanb improving inline documentation
authored
62 #
0f49b54 @ryanb adding 'cannot?' method which performs opposite check of 'can?' - clo…
authored
63 def cannot?(*args)
64 !can?(*args)
65 end
dfd84a1 @ryanb improving inline documentation
authored
66
b9227eb @ryanb 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 @ryanb improving inline documentation
authored
69 #
b9227eb @ryanb adding a lot of inline documentation to code for rdocs
authored
70 # can :update, Article
dfd84a1 @ryanb improving inline documentation
authored
71 #
b9227eb @ryanb 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 @ryanb improving inline documentation
authored
77 #
baeef0b @ryanb adding conditions behavior to Ability#can and fetch with Ability#cond…
authored
78 # You can pass a hash of conditions as the third argument.
b9227eb @ryanb adding a lot of inline documentation to code for rdocs
authored
79 #
baeef0b @ryanb adding conditions behavior to Ability#can and fetch with Ability#cond…
authored
80 # can :read, Project, :active => true, :user_id => user.id
dfd84a1 @ryanb 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 @ryanb adding conditions behavior to Ability#can and fetch with Ability#cond…
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 @ryanb adding a lot of inline documentation to code for rdocs
authored
87 #
baeef0b @ryanb adding conditions behavior to Ability#can and fetch with Ability#cond…
authored
88 # can :update, Project do |project|
89 # project && project.groups.include?(user.group)
b9227eb @ryanb adding a lot of inline documentation to code for rdocs
authored
90 # end
dfd84a1 @ryanb improving inline documentation
authored
91 #
baeef0b @ryanb adding conditions behavior to Ability#can and fetch with Ability#cond…
authored
92 # If the block returns true then the user has that :update ability for that project, otherwise he
b9227eb @ryanb 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 @ryanb improving inline documentation
authored
95 #
baeef0b @ryanb adding conditions behavior to Ability#can and fetch with Ability#cond…
authored
96 # The downside to using a block is that it cannot be used to generate conditions for database queries.
dfd84a1 @ryanb improving inline documentation
authored
97 #
b9227eb @ryanb 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 @ryanb improving inline documentation
authored
100 #
b9227eb @ryanb 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 @ryanb improving inline documentation
authored
104 #
b9227eb @ryanb adding a lot of inline documentation to code for rdocs
authored
105 # Here the user has permission to read all objects except orders.
dfd84a1 @ryanb 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 @ryanb adding a lot of inline documentation to code for rdocs
authored
108 # passed to the block.
dfd84a1 @ryanb improving inline documentation
authored
109 #
b9227eb @ryanb adding a lot of inline documentation to code for rdocs
authored
110 # can :manage, Comment do |action, comment|
111 # action != :destroy
112 # end
dfd84a1 @ryanb improving inline documentation
authored
113 #
e603655 @ryanb 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 @ryanb improving inline documentation
authored
116 #
e603655 @ryanb support custom objects (usually symbols) in can definition - closes #8
authored
117 # can :read, :stats
118 # can? :read, :stats # => true
dfd84a1 @ryanb improving inline documentation
authored
119 #
d9f3c8b @ryanb renaming noun to subject internally
authored
120 def can(action, subject, conditions = nil, &block)
bbbc8a6 @ryanb refactoring much of Ability class into separate CanDefinition class
authored
121 can_definitions << CanDefinition.new(true, action, subject, conditions, block)
d4405e6 @ryanb adding cannot method to define which abilities cannot be done - close…
authored
122 end
dfd84a1 @ryanb improving inline documentation
authored
123
124 # Defines an ability which cannot be done. Accepts the same arguments as "can".
125 #
d4405e6 @ryanb adding cannot method to define which abilities cannot be done - close…
authored
126 # can :read, :all
127 # cannot :read, Comment
dfd84a1 @ryanb improving inline documentation
authored
128 #
d4405e6 @ryanb adding cannot method to define which abilities cannot be done - close…
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 @ryanb improving inline documentation
authored
131 #
d4405e6 @ryanb adding cannot method to define which abilities cannot be done - close…
authored
132 # cannot :read, Product do |product|
133 # product.invisible?
134 # end
dfd84a1 @ryanb improving inline documentation
authored
135 #
d9f3c8b @ryanb renaming noun to subject internally
authored
136 def cannot(action, subject, conditions = nil, &block)
bbbc8a6 @ryanb refactoring much of Ability class into separate CanDefinition class
authored
137 can_definitions << CanDefinition.new(false, action, subject, conditions, block)
4b6f538 @ryanb moving can definition into ability instance instead of class, this re…
authored
138 end
dfd84a1 @ryanb improving inline documentation
authored
139
5bd1a85 @ryanb little fixes to inline documentation (rdocs)
authored
140 # Alias one or more actions into another one.
dfd84a1 @ryanb improving inline documentation
authored
141 #
b9227eb @ryanb adding a lot of inline documentation to code for rdocs
authored
142 # alias_action :update, :destroy, :to => :modify
143 # can :modify, Comment
dfd84a1 @ryanb improving inline documentation
authored
144 #
5bd1a85 @ryanb little fixes to inline documentation (rdocs)
authored
145 # Then :modify permission will apply to both :update and :destroy requests.
dfd84a1 @ryanb improving inline documentation
authored
146 #
5bd1a85 @ryanb little fixes to inline documentation (rdocs)
authored
147 # can? :update, Comment # => true
148 # can? :destroy, Comment # => true
dfd84a1 @ryanb improving inline documentation
authored
149 #
5bd1a85 @ryanb 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 @ryanb improving inline documentation
authored
152 #
5bd1a85 @ryanb little fixes to inline documentation (rdocs)
authored
153 # alias_action :update, :destroy, :to => :modify
154 # can :update, Comment
155 # can? :modify, Comment # => false
dfd84a1 @ryanb improving inline documentation
authored
156 #
5bd1a85 @ryanb little fixes to inline documentation (rdocs)
authored
157 # Unless that exact alias is used.
dfd84a1 @ryanb improving inline documentation
authored
158 #
5bd1a85 @ryanb little fixes to inline documentation (rdocs)
authored
159 # can :modify, Comment
160 # can? :modify, Comment # => true
dfd84a1 @ryanb improving inline documentation
authored
161 #
b9227eb @ryanb 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 @ryanb improving inline documentation
authored
163 #
b9227eb @ryanb 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 @ryanb improving inline documentation
authored
167 #
5bd1a85 @ryanb little fixes to inline documentation (rdocs)
authored
168 # This way one can use params[:action] in the controller to determine the permission.
4b6f538 @ryanb moving can definition into ability instance instead of class, this re…
authored
169 def alias_action(*args)
170 target = args.pop[:to]
f99d506 @ryanb Append aliased actions (don't overwrite them) - closes #20
authored
171 aliased_actions[target] ||= []
172 aliased_actions[target] += args
4b6f538 @ryanb moving can definition into ability instance instead of class, this re…
authored
173 end
dfd84a1 @ryanb improving inline documentation
authored
174
7d3b4cd @ryanb Adding clear_aliased_actions to Ability which removes previously defi…
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 @ryanb refactoring ability can? method - closes #12
authored
176 def aliased_actions
177 @aliased_actions ||= default_alias_actions
178 end
dfd84a1 @ryanb improving inline documentation
authored
179
7d3b4cd @ryanb Adding clear_aliased_actions to Ability which removes previously defi…
authored
180 # Removes previously aliased actions including the defaults.
181 def clear_aliased_actions
182 @aliased_actions = {}
183 end
dfd84a1 @ryanb improving inline documentation
authored
184
7d7d249 @funny-falcon 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 @ryanb adding conditions behavior to Ability#can and fetch with Ability#cond…
authored
187 #
188 # can :read, Article, :visible => true
7d7d249 @funny-falcon 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 @ryanb adding conditions behavior to Ability#can and fetch with Ability#cond…
authored
194 #
240c281 @ryanb renaming ActiveRecordAdditions#can method to accessible_by since it f…
authored
195 # Normally you will not call this method directly, but instead go through ActiveRecordAdditions#accessible_by method.
dfd84a1 @ryanb improving inline documentation
authored
196 #
baeef0b @ryanb adding conditions behavior to Ability#can and fetch with Ability#cond…
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 @logandk Make sure conditions on associations are pluralized
logandk authored
200 def conditions(action, subject, options = {})
6084814 @ryanb 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 @funny-falcon 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 @ryanb 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 @funny-falcon passing throw matching rules with not matching conditions
funny-falcon authored
208 }
bbbc8a6 @ryanb refactoring much of Ability class into separate CanDefinition class
authored
209 else
210 false
baeef0b @ryanb adding conditions behavior to Ability#can and fetch with Ability#cond…
authored
211 end
212 end
213
7d7d249 @funny-falcon 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 @funny-falcon 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 r…
Yura Sokolov authored
235 conds.reverse.inject(false_cond) do |sql, action|
7d7d249 @funny-falcon 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 @funny-falcon 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 @funny-falcon passing throw matching rules with not matching conditions
funny-falcon authored
249 end
250 end
251 end
252
e200814 @ryanb adding joins clause to accessible_by when conditions are across assoc…
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 @ryanb refactoring can definition matching behavior
authored
256 can_definitions = relevant_can_definitions(action, subject)
7d7d249 @funny-falcon 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 @funny-falcon 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 @ryanb adding joins clause to accessible_by when conditions are across assoc…
authored
264 end
265 end
7d7d249 @funny-falcon passing throw matching rules with not matching conditions
funny-falcon authored
266
7d3b4cd @ryanb Adding clear_aliased_actions to Ability which removes previously defi…
authored
267 private
cad4259 @ryanb 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 @ryanb improving inline documentation
authored
277
bbbc8a6 @ryanb refactoring much of Ability class into separate CanDefinition class
authored
278 def can_definitions
279 @can_definitions ||= []
280 end
dfd84a1 @ryanb improving inline documentation
authored
281
6084814 @ryanb 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 @ryanb adding conditions behavior to Ability#can and fetch with Ability#cond…
authored
288 end
289 end
dfd84a1 @ryanb improving inline documentation
authored
290
4b6f538 @ryanb moving can definition into ability instance instead of class, this re…
authored
291 def default_alias_actions
292 {
293 :read => [:index, :show],
294 :create => [:new],
295 :update => [:edit],
296 }
297 end
7d7d249 @funny-falcon 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 @funny-falcon 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 @ryanb adding basic ability module
authored
326 end
327 end
Something went wrong with that request. Please try again.