cancancan 3.6.2 → 3.7.0
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.
- checksums.yaml +4 -4
- data/cancancan.gemspec +3 -2
- data/lib/cancan/ability/actions.rb +6 -3
- data/lib/cancan/ability/rules.rb +28 -1
- data/lib/cancan/ability/strong_parameter_support.rb +29 -15
- data/lib/cancan/ability.rb +13 -10
- data/lib/cancan/class_matcher.rb +1 -1
- data/lib/cancan/conditions_matcher.rb +20 -11
- data/lib/cancan/config.rb +21 -7
- data/lib/cancan/controller_additions.rb +3 -2
- data/lib/cancan/controller_resource.rb +247 -17
- data/lib/cancan/model_adapters/abstract_adapter.rb +9 -0
- data/lib/cancan/model_adapters/active_record_5_adapter.rb +28 -11
- data/lib/cancan/model_adapters/active_record_adapter.rb +81 -23
- data/lib/cancan/model_adapters/conditions_extractor.rb +94 -38
- data/lib/cancan/model_adapters/conditions_normalizer.rb +9 -1
- data/lib/cancan/model_adapters/sti_normalizer.rb +6 -13
- data/lib/cancan/model_adapters/strategies/joined_alias_each_rule_as_exists_subquery.rb +2 -2
- data/lib/cancan/relevant.rb +4 -0
- data/lib/cancan/rule.rb +3 -2
- data/lib/cancan/rules_compressor.rb +3 -2
- data/lib/cancan/sti_detector.rb +1 -1
- data/lib/cancan/unauthorized_message_resolver.rb +1 -0
- data/lib/cancan/version.rb +1 -1
- metadata +4 -14
- data/lib/cancan/controller_resource_builder.rb +0 -26
- data/lib/cancan/controller_resource_finder.rb +0 -42
- data/lib/cancan/controller_resource_loader.rb +0 -120
- data/lib/cancan/controller_resource_name_finder.rb +0 -23
- data/lib/cancan/controller_resource_sanitizer.rb +0 -32
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative 'controller_resource_loader.rb'
|
|
4
3
|
module CanCan
|
|
5
4
|
# Handle the load and authorization controller logic
|
|
6
5
|
# so we don't clutter up all controllers with non-interface methods.
|
|
7
6
|
# This class is used internally, so you do not need to call methods directly on it.
|
|
8
7
|
class ControllerResource # :nodoc:
|
|
9
|
-
include ControllerResourceLoader
|
|
10
|
-
|
|
11
8
|
def self.add_before_action(controller_class, method, *args)
|
|
12
9
|
options = args.extract_options!
|
|
13
10
|
resource_name = args.first
|
|
@@ -26,7 +23,7 @@ module CanCan
|
|
|
26
23
|
@controller = controller
|
|
27
24
|
@params = controller.params
|
|
28
25
|
@options = args.extract_options!
|
|
29
|
-
@name = args.first
|
|
26
|
+
@name = args.first&.to_sym
|
|
30
27
|
end
|
|
31
28
|
|
|
32
29
|
def load_and_authorize_resource
|
|
@@ -34,6 +31,16 @@ module CanCan
|
|
|
34
31
|
authorize_resource
|
|
35
32
|
end
|
|
36
33
|
|
|
34
|
+
def load_resource
|
|
35
|
+
return if skip?(:load)
|
|
36
|
+
|
|
37
|
+
if load_instance?
|
|
38
|
+
self.resource_instance ||= load_resource_instance
|
|
39
|
+
elsif load_collection?
|
|
40
|
+
self.collection_instance ||= load_collection
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
37
44
|
def authorize_resource
|
|
38
45
|
return if skip?(:authorize)
|
|
39
46
|
|
|
@@ -54,10 +61,34 @@ module CanCan
|
|
|
54
61
|
|
|
55
62
|
protected
|
|
56
63
|
|
|
64
|
+
def name
|
|
65
|
+
@name || name_from_controller
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def name_from_controller
|
|
69
|
+
@params[:controller].split('/').last.singularize
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def namespace
|
|
73
|
+
@params[:controller].split('/')[0..-2]
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def namespaced_name
|
|
77
|
+
[namespace, name].join('/').singularize.camelize.safe_constantize || name
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def instance_name
|
|
81
|
+
@options[:instance_name] || name
|
|
82
|
+
end
|
|
83
|
+
|
|
57
84
|
# Returns the class used for this resource. This can be overridden by the :class option.
|
|
58
85
|
# If +false+ is passed in it will use the resource name as a symbol in which case it should
|
|
59
86
|
# only be used for authorization, not loading since there's no class to load through.
|
|
60
87
|
def resource_class
|
|
88
|
+
@resource_class ||= resource_class_from_options
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def resource_class_from_options
|
|
61
92
|
case @options[:class]
|
|
62
93
|
when false
|
|
63
94
|
name.to_sym
|
|
@@ -70,12 +101,20 @@ module CanCan
|
|
|
70
101
|
end
|
|
71
102
|
end
|
|
72
103
|
|
|
73
|
-
def
|
|
74
|
-
|
|
104
|
+
def resource_class_with_parent
|
|
105
|
+
parent_resource ? { parent_resource => resource_class } : resource_class
|
|
75
106
|
end
|
|
76
107
|
|
|
77
|
-
def
|
|
78
|
-
|
|
108
|
+
def new_actions
|
|
109
|
+
%i[new create] + Array(@options[:new]).map(&:to_sym)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def collection_actions
|
|
113
|
+
[:index] + Array(@options[:collection]).map(&:to_sym)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def save_actions
|
|
117
|
+
%i[create update]
|
|
79
118
|
end
|
|
80
119
|
|
|
81
120
|
def member_action?
|
|
@@ -84,8 +123,20 @@ module CanCan
|
|
|
84
123
|
!collection_actions.include?(@params[:action].to_sym))
|
|
85
124
|
end
|
|
86
125
|
|
|
87
|
-
def
|
|
88
|
-
|
|
126
|
+
def load_instance?
|
|
127
|
+
parent? || member_action?
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def load_collection?
|
|
131
|
+
resource_base.respond_to?(:accessible_by) && !current_ability.has_block?(authorization_action, resource_class)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def parent_authorization_action
|
|
135
|
+
@options[:parent_action] || :show
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def authorization_action
|
|
139
|
+
parent? ? parent_authorization_action : @params[:action].to_sym
|
|
89
140
|
end
|
|
90
141
|
|
|
91
142
|
def resource_instance=(instance)
|
|
@@ -108,26 +159,205 @@ module CanCan
|
|
|
108
159
|
@controller.instance_variable_get("@#{instance_name.to_s.pluralize}")
|
|
109
160
|
end
|
|
110
161
|
|
|
162
|
+
def load_collection
|
|
163
|
+
resource_base.accessible_by(current_ability, authorization_action)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def load_resource_instance
|
|
167
|
+
if !parent? && new_actions.include?(@params[:action].to_sym)
|
|
168
|
+
build_resource
|
|
169
|
+
elsif id_param || @options[:singleton]
|
|
170
|
+
find_resource
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def fetch_parent(name)
|
|
175
|
+
if @controller.instance_variable_defined? "@#{name}"
|
|
176
|
+
@controller.instance_variable_get("@#{name}")
|
|
177
|
+
elsif @controller.respond_to?(name, true)
|
|
178
|
+
@controller.send(name)
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# The object to load this resource through.
|
|
183
|
+
def parent_resource
|
|
184
|
+
parent_name && fetch_parent(parent_name)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def parent_name
|
|
188
|
+
@options[:through] && [@options[:through]].flatten.detect { |i| fetch_parent(i) }
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def resource_base_through_parent_resource
|
|
192
|
+
if @options[:singleton]
|
|
193
|
+
resource_class
|
|
194
|
+
else
|
|
195
|
+
parent_resource.send(@options[:through_association] || name.to_s.pluralize)
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def resource_base_through
|
|
200
|
+
if parent_resource
|
|
201
|
+
resource_base_through_parent_resource
|
|
202
|
+
elsif @options[:shallow]
|
|
203
|
+
resource_class
|
|
204
|
+
else
|
|
205
|
+
# maybe this should be a record not found error instead?
|
|
206
|
+
raise AccessDenied.new(nil, authorization_action, resource_class)
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# The object that methods (such as "find", "new" or "build") are called on.
|
|
211
|
+
# If the :through option is passed it will go through an association on that instance.
|
|
212
|
+
# If the :shallow option is passed it will use the resource_class if there's no parent
|
|
213
|
+
# If the :singleton option is passed it won't use the association because it needs to be handled later.
|
|
214
|
+
def resource_base
|
|
215
|
+
@options[:through] ? resource_base_through : resource_class
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def find_resource
|
|
219
|
+
singleton_association_name = @options[:through_association] || name
|
|
220
|
+
if @options[:singleton] && parent_resource.respond_to?(singleton_association_name)
|
|
221
|
+
parent_resource.send(singleton_association_name)
|
|
222
|
+
elsif @options[:find_by]
|
|
223
|
+
find_resource_using_find_by
|
|
224
|
+
else
|
|
225
|
+
adapter.find(resource_base, id_param)
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def find_resource_using_find_by
|
|
230
|
+
find_by_dynamic_finder || find_by_find_by_finder || resource_base.send(@options[:find_by], id_param)
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def find_by_dynamic_finder
|
|
234
|
+
method_name = "find_by_#{@options[:find_by]}!"
|
|
235
|
+
resource_base.send(method_name, id_param) if resource_base.respond_to? method_name
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def find_by_find_by_finder
|
|
239
|
+
resource_base.find_by(@options[:find_by].to_sym => id_param) if resource_base.respond_to? :find_by
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def id_param
|
|
243
|
+
# A whitespace-only id is blank but given, so it must also be looked up.
|
|
244
|
+
id = @params[id_param_key].to_s
|
|
245
|
+
id unless id.empty?
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def id_param_key
|
|
249
|
+
if @options[:id_param]
|
|
250
|
+
@options[:id_param]
|
|
251
|
+
else
|
|
252
|
+
parent? ? :"#{name}_id" : :id
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def build_resource
|
|
257
|
+
resource = resource_base.new(resource_params || {})
|
|
258
|
+
assign_attributes(resource)
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def assign_attributes(resource)
|
|
262
|
+
assign_parent(resource) if @options[:singleton] && parent_resource
|
|
263
|
+
initial_attributes.each do |attr_name, value|
|
|
264
|
+
resource.send("#{attr_name}=", value)
|
|
265
|
+
end
|
|
266
|
+
resource
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
# The parent is assigned through the setter named after the :through option when the resource has one,
|
|
270
|
+
# otherwise through the inverse of the association on the parent.
|
|
271
|
+
def assign_parent(resource)
|
|
272
|
+
setter = "#{parent_name}="
|
|
273
|
+
setter = "#{inverse_association_name}=" unless resource.respond_to?(setter)
|
|
274
|
+
resource.send(setter, parent_resource)
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def inverse_association_name
|
|
278
|
+
association_name = @options[:through_association] || name
|
|
279
|
+
inverse = inverse_of_association(parent_resource.class, association_name)
|
|
280
|
+
return inverse if inverse
|
|
281
|
+
|
|
282
|
+
raise Error, "Cannot assign the parent to the singleton resource: #{resource_class} has no " \
|
|
283
|
+
"#{parent_name}= method and the association #{association_name} on #{parent_resource.class} " \
|
|
284
|
+
'has no inverse. Define inverse_of on the association or use a through option named after it.'
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def inverse_of_association(parent_class, association_name)
|
|
288
|
+
return unless parent_class.respond_to?(:reflect_on_association)
|
|
289
|
+
|
|
290
|
+
reflection = parent_class.reflect_on_association(association_name)
|
|
291
|
+
reflection && (reflection.inverse_of&.name || reflection.options[:as])
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def initial_attributes
|
|
295
|
+
current_ability.attributes_for(@params[:action].to_sym, resource_class).delete_if do |key, _value|
|
|
296
|
+
resource_params&.include?(key)
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
# The params method of the controller can have side effects, so it is called at most once per request.
|
|
301
|
+
def resource_params
|
|
302
|
+
return @resource_params if defined?(@resource_params)
|
|
303
|
+
|
|
304
|
+
@resource_params =
|
|
305
|
+
if parameters_require_sanitizing? && params_method.present?
|
|
306
|
+
sanitize_parameters
|
|
307
|
+
else
|
|
308
|
+
resource_params_by_namespaced_name
|
|
309
|
+
end
|
|
310
|
+
end
|
|
311
|
+
|
|
111
312
|
def parameters_require_sanitizing?
|
|
112
313
|
save_actions.include?(@params[:action].to_sym) || resource_params_by_namespaced_name.present?
|
|
113
314
|
end
|
|
114
315
|
|
|
115
|
-
def
|
|
116
|
-
@options[
|
|
316
|
+
def resource_params_by_key(key)
|
|
317
|
+
return unless @options[key] && @params.key?(extract_key(@options[key]))
|
|
318
|
+
|
|
319
|
+
@params[extract_key(@options[key])]
|
|
117
320
|
end
|
|
118
321
|
|
|
119
|
-
def
|
|
120
|
-
|
|
322
|
+
def resource_params_by_namespaced_name
|
|
323
|
+
resource_params_by_key(:instance_name) || resource_params_by_key(:class) || (
|
|
324
|
+
params = @params[extract_key(namespaced_name)]
|
|
325
|
+
params.respond_to?(:to_h) ? params : nil)
|
|
121
326
|
end
|
|
122
327
|
|
|
123
|
-
def
|
|
124
|
-
|
|
328
|
+
def sanitize_parameters
|
|
329
|
+
case params_method
|
|
330
|
+
when Symbol
|
|
331
|
+
@controller.send(params_method)
|
|
332
|
+
when String
|
|
333
|
+
@controller.instance_eval(params_method)
|
|
334
|
+
when Proc
|
|
335
|
+
params_method.call(@controller)
|
|
336
|
+
end
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
def params_methods
|
|
340
|
+
methods = ["#{@params[:action]}_params".to_sym, "#{name}_params".to_sym, :resource_params]
|
|
341
|
+
methods.unshift(@options[:param_method]) if @options[:param_method].present?
|
|
342
|
+
methods
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
def params_method
|
|
346
|
+
params_methods.each do |method|
|
|
347
|
+
return method if (method.is_a?(Symbol) && @controller.respond_to?(method, true)) ||
|
|
348
|
+
method.is_a?(String) || method.is_a?(Proc)
|
|
349
|
+
end
|
|
350
|
+
nil
|
|
125
351
|
end
|
|
126
352
|
|
|
127
353
|
private
|
|
128
354
|
|
|
129
355
|
def action_exists_in?(options)
|
|
130
|
-
Array(options).include?(@params[:action].to_sym)
|
|
356
|
+
Array(options).map(&:to_sym).include?(@params[:action].to_sym)
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
def extract_key(value)
|
|
360
|
+
value.to_s.underscore.tr('/', '_')
|
|
131
361
|
end
|
|
132
362
|
|
|
133
363
|
def adapter
|
|
@@ -40,6 +40,15 @@ module CanCan
|
|
|
40
40
|
parent.class.name.downcase.to_sym
|
|
41
41
|
end
|
|
42
42
|
|
|
43
|
+
# Override if conditions on the parent could be under other keys, like a foreign key
|
|
44
|
+
def self.parent_condition_keys(parent, child)
|
|
45
|
+
[parent_condition_name(parent, child)]
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def self.conditions_only_on_parent?(parent, child, conditions)
|
|
49
|
+
conditions.is_a?(Hash) && (conditions.keys.map(&:to_sym) - parent_condition_keys(parent, child)).empty?
|
|
50
|
+
end
|
|
51
|
+
|
|
43
52
|
# Used above override_conditions_hash_matching to determine if this model adapter will override the
|
|
44
53
|
# matching behavior for nested subject.
|
|
45
54
|
# If this returns true then nested_subject_matches_conditions? will be called.
|
|
@@ -14,8 +14,8 @@ module CanCan
|
|
|
14
14
|
def self.matches_condition?(subject, name, value)
|
|
15
15
|
return super if Array.wrap(value).all? { |x| x.is_a? Integer }
|
|
16
16
|
|
|
17
|
-
attribute = subject.send(name)
|
|
18
|
-
raw_attribute = subject.class.send(name.to_s.pluralize)[attribute]
|
|
17
|
+
attribute = subject.send(name).to_s
|
|
18
|
+
raw_attribute = subject.class.send(name.to_s.pluralize)[attribute].to_s
|
|
19
19
|
!(Array(value).map(&:to_s) & [attribute, raw_attribute]).empty?
|
|
20
20
|
end
|
|
21
21
|
|
|
@@ -26,24 +26,41 @@ module CanCan
|
|
|
26
26
|
end
|
|
27
27
|
|
|
28
28
|
def strategy_class
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
strategy = CanCan.accessible_by_strategy
|
|
30
|
+
return strategy if strategy.is_a?(Class)
|
|
31
|
+
|
|
32
|
+
CanCan::ModelAdapters::Strategies.const_get(strategy.to_s.camelize)
|
|
31
33
|
end
|
|
32
34
|
|
|
33
|
-
def sanitize_sql(conditions)
|
|
35
|
+
def sanitize_sql(conditions, conditions_extractor = nil)
|
|
34
36
|
if conditions.is_a?(Hash)
|
|
35
|
-
sanitize_sql_activerecord5(conditions)
|
|
37
|
+
sanitize_sql_activerecord5(conditions, conditions_extractor)
|
|
36
38
|
else
|
|
37
39
|
@model_class.send(:sanitize_sql, conditions)
|
|
38
40
|
end
|
|
39
41
|
end
|
|
40
42
|
|
|
41
|
-
def
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
def where_node(conditions, conditions_extractor)
|
|
44
|
+
Arel::Nodes::And.new(predicates(conditions, conditions_extractor))
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def sanitize_sql_activerecord5(conditions, conditions_extractor)
|
|
48
|
+
predicates(conditions, conditions_extractor).map { |b| visit_nodes(b) }.join(' AND ')
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def predicates(conditions, conditions_extractor)
|
|
52
|
+
conditions.flat_map do |key, value|
|
|
53
|
+
table_class = value.is_a?(Hash) && conditions_extractor&.model_class_for(key)
|
|
54
|
+
if table_class
|
|
55
|
+
predicate_builder(table_class, table_class.arel_table.alias(key.to_s)).build_from_hash(value.stringify_keys)
|
|
56
|
+
else
|
|
57
|
+
predicate_builder(@model_class, @model_class.send(:arel_table)).build_from_hash(key.to_s => value)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
45
61
|
|
|
46
|
-
|
|
62
|
+
def predicate_builder(model_class, table)
|
|
63
|
+
ActiveRecord::PredicateBuilder.new(ActiveRecord::TableMetadata.new(model_class, table))
|
|
47
64
|
end
|
|
48
65
|
|
|
49
66
|
def visit_nodes(node)
|
|
@@ -23,6 +23,8 @@ module CanCan
|
|
|
23
23
|
else
|
|
24
24
|
@rules
|
|
25
25
|
end
|
|
26
|
+
# The normalizers change the rules. Work on copies, so the rules of the ability stay the same.
|
|
27
|
+
@compressed_rules = @compressed_rules.map(&:dup)
|
|
26
28
|
StiNormalizer.normalize(@compressed_rules)
|
|
27
29
|
ConditionsNormalizer.normalize(model_class, @compressed_rules)
|
|
28
30
|
end
|
|
@@ -43,7 +45,16 @@ module CanCan
|
|
|
43
45
|
false
|
|
44
46
|
end
|
|
45
47
|
|
|
48
|
+
def parent_condition_keys(parent, child)
|
|
49
|
+
[parent_condition_name(parent, child), parent_child_foreign_key(parent, child)].compact
|
|
50
|
+
end
|
|
51
|
+
|
|
46
52
|
def parent_child_conditions(parent, child, all_conditions)
|
|
53
|
+
foreign_key = parent_child_foreign_key(parent, child)
|
|
54
|
+
foreign_key.nil? ? nil : all_conditions[foreign_key]
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def parent_child_foreign_key(parent, child)
|
|
47
58
|
child_class = child.is_a?(Class) ? child : child.class
|
|
48
59
|
parent_class = parent.is_a?(Class) ? parent : parent.class
|
|
49
60
|
|
|
@@ -54,11 +65,9 @@ module CanCan
|
|
|
54
65
|
|
|
55
66
|
# Search again in case of polymorphic associations, this time matching on the :has_many side
|
|
56
67
|
# via the :as option, as well as klass
|
|
57
|
-
foreign_key
|
|
68
|
+
foreign_key || parent_class.reflect_on_all_associations(:has_many).find do |has_many_assoc|
|
|
58
69
|
matching_parent_child_polymorphic_association(has_many_assoc, child_class)
|
|
59
70
|
end&.foreign_key&.to_sym
|
|
60
|
-
|
|
61
|
-
foreign_key.nil? ? nil : all_conditions[foreign_key]
|
|
62
71
|
end
|
|
63
72
|
|
|
64
73
|
def matching_parent_child_polymorphic_association(parent_assoc, child_class)
|
|
@@ -110,26 +119,22 @@ module CanCan
|
|
|
110
119
|
# query(:manage, User).conditions # => "not (self_managed = 't') AND ((manager_id = 1) OR (id = 1))"
|
|
111
120
|
#
|
|
112
121
|
def conditions
|
|
113
|
-
|
|
114
|
-
if @compressed_rules.size == 1 && @compressed_rules.first.base_behavior
|
|
115
|
-
# Return the conditions directly if there's just one definition
|
|
116
|
-
conditions_extractor.tableize_conditions(@compressed_rules.first.conditions).dup
|
|
117
|
-
else
|
|
118
|
-
extract_multiple_conditions(conditions_extractor, @compressed_rules)
|
|
119
|
-
end
|
|
122
|
+
extract_conditions(ConditionsExtractor.new(@model_class, joins))
|
|
120
123
|
end
|
|
121
124
|
|
|
122
125
|
def extract_multiple_conditions(conditions_extractor, rules)
|
|
123
|
-
rules.reverse.inject(false_sql) do |sql, rule|
|
|
124
|
-
|
|
126
|
+
sql, = rules.reverse.inject([false_sql, false]) do |(sql, or_chain), rule|
|
|
127
|
+
conditions_hash = conditions_extractor.tableize_conditions(rule.conditions).dup
|
|
128
|
+
merge_conditions(sql, or_chain, conditions_hash, rule.base_behavior, conditions_extractor)
|
|
125
129
|
end
|
|
130
|
+
sql
|
|
126
131
|
end
|
|
127
132
|
|
|
128
133
|
def database_records
|
|
129
134
|
if override_scope
|
|
130
|
-
|
|
135
|
+
merge_override_scope
|
|
131
136
|
elsif @model_class.respond_to?(:where) && @model_class.respond_to?(:joins)
|
|
132
|
-
build_relation(
|
|
137
|
+
build_relation(where_conditions)
|
|
133
138
|
else
|
|
134
139
|
@model_class.all(conditions: conditions, joins: joins)
|
|
135
140
|
end
|
|
@@ -155,6 +160,29 @@ module CanCan
|
|
|
155
160
|
|
|
156
161
|
private
|
|
157
162
|
|
|
163
|
+
def extract_conditions(conditions_extractor)
|
|
164
|
+
if @compressed_rules.size == 1 && @compressed_rules.first.base_behavior
|
|
165
|
+
# Return the conditions directly if there's just one definition
|
|
166
|
+
conditions_extractor.tableize_conditions(@compressed_rules.first.conditions).dup
|
|
167
|
+
else
|
|
168
|
+
extract_multiple_conditions(conditions_extractor, @compressed_rules)
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Active Record cannot find the model of an aliased table in a hash of conditions,
|
|
173
|
+
# so the conditions on associations are converted to SQL with the model of each table.
|
|
174
|
+
def where_conditions
|
|
175
|
+
conditions_extractor = ConditionsExtractor.new(@model_class, joins)
|
|
176
|
+
conditions = extract_conditions(conditions_extractor)
|
|
177
|
+
return conditions unless conditions.is_a?(Hash) && conditions.values.any? { |value| value.is_a?(Hash) }
|
|
178
|
+
|
|
179
|
+
where_node(conditions, conditions_extractor)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def where_node(conditions, conditions_extractor)
|
|
183
|
+
sanitize_sql(conditions, conditions_extractor)
|
|
184
|
+
end
|
|
185
|
+
|
|
158
186
|
# Removes empty hashes and moves everything into arrays.
|
|
159
187
|
def deep_clean(joins_hash)
|
|
160
188
|
joins_hash.map { |name, nested| nested.empty? ? name : { name => deep_clean(nested) } }
|
|
@@ -171,6 +199,17 @@ module CanCan
|
|
|
171
199
|
end
|
|
172
200
|
end
|
|
173
201
|
|
|
202
|
+
# `merge` replaces the conditions of the base relation on the columns the scope also constrains,
|
|
203
|
+
# so the conditions it dropped, like the one of an association like `category.articles`, are applied again.
|
|
204
|
+
# The conditions the scope removes with `unscope`, like the ones of a default scope, stay removed.
|
|
205
|
+
def merge_override_scope
|
|
206
|
+
merged = @model_class.where(nil).merge(override_scope)
|
|
207
|
+
base = @model_class.where(nil)
|
|
208
|
+
base = base.unscope(*override_scope.unscope_values) if override_scope.unscope_values.any?
|
|
209
|
+
dropped = base.where_clause.send(:predicates) - merged.where_clause.send(:predicates)
|
|
210
|
+
dropped.inject(merged) { |relation, predicate| relation.where(predicate) }
|
|
211
|
+
end
|
|
212
|
+
|
|
174
213
|
def override_scope
|
|
175
214
|
conditions = @compressed_rules.map(&:conditions).compact
|
|
176
215
|
return unless conditions.any? { |c| c.is_a?(ActiveRecord::Relation) }
|
|
@@ -186,26 +225,45 @@ module CanCan
|
|
|
186
225
|
"Instead use a hash or SQL for #{rule_found.actions.first} #{rule_found.subjects.first} ability."
|
|
187
226
|
end
|
|
188
227
|
|
|
189
|
-
|
|
228
|
+
# Returns the merged sql and whether it is a flat chain of OR conditions.
|
|
229
|
+
def merge_conditions(sql, or_chain, conditions_hash, behavior, conditions_extractor)
|
|
190
230
|
if conditions_hash.blank?
|
|
191
|
-
behavior ? true_sql : false_sql
|
|
231
|
+
[behavior ? true_sql : false_sql, false]
|
|
192
232
|
else
|
|
193
|
-
merge_non_empty_conditions(behavior, conditions_hash, sql)
|
|
233
|
+
merge_non_empty_conditions(behavior, sanitize_sql(conditions_hash, conditions_extractor), sql, or_chain)
|
|
194
234
|
end
|
|
195
235
|
end
|
|
196
236
|
|
|
197
|
-
|
|
198
|
-
|
|
237
|
+
# Consecutive `can` rules are joined in a flat chain of ORs, so that the nesting of the query only grows
|
|
238
|
+
# when `can` and `cannot` rules alternate. Deeply nested conditions overflow the parser of SQLite.
|
|
239
|
+
def merge_non_empty_conditions(behavior, conditions, sql, or_chain)
|
|
199
240
|
case sql
|
|
200
241
|
when true_sql
|
|
201
|
-
behavior ? true_sql :
|
|
242
|
+
behavior ? [true_sql, false] : [not_conditions(conditions), false]
|
|
202
243
|
when false_sql
|
|
203
|
-
behavior ? conditions : false_sql
|
|
244
|
+
behavior ? [conditions, false] : [false_sql, false]
|
|
245
|
+
else
|
|
246
|
+
merge_with_previous_conditions(behavior, conditions, sql, or_chain)
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def merge_with_previous_conditions(behavior, conditions, sql, or_chain)
|
|
251
|
+
if behavior
|
|
252
|
+
[or_conditions(conditions, sql, or_chain), true]
|
|
204
253
|
else
|
|
205
|
-
|
|
254
|
+
["#{not_conditions(conditions)} AND (#{sql})", false]
|
|
206
255
|
end
|
|
207
256
|
end
|
|
208
257
|
|
|
258
|
+
# A cannot rule only excludes the records that match its conditions: NULL does not match.
|
|
259
|
+
def not_conditions(conditions)
|
|
260
|
+
"(#{conditions}) IS NOT TRUE"
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def or_conditions(conditions, sql, or_chain)
|
|
264
|
+
"(#{conditions}) OR #{or_chain ? sql : "(#{sql})"}"
|
|
265
|
+
end
|
|
266
|
+
|
|
209
267
|
def false_sql
|
|
210
268
|
sanitize_sql(['?=?', true, false])
|
|
211
269
|
end
|
|
@@ -214,7 +272,7 @@ module CanCan
|
|
|
214
272
|
sanitize_sql(['?=?', true, true])
|
|
215
273
|
end
|
|
216
274
|
|
|
217
|
-
def sanitize_sql(conditions)
|
|
275
|
+
def sanitize_sql(conditions, _conditions_extractor = nil)
|
|
218
276
|
@model_class.send(:sanitize_sql, conditions)
|
|
219
277
|
end
|
|
220
278
|
end
|