decidim-dev 0.32.1 → 0.32.2

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.
Files changed (78) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/decidim/dev/dummy_resources_controller.rb +3 -1
  3. data/config/locales/eu.yml +6 -6
  4. data/config/locales/gl.yml +15 -1
  5. data/config/rubocop/decidim-linters/configuration.yml +36 -0
  6. data/config/rubocop/rspec/configuration.yml +1 -0
  7. data/decidim-dev.gemspec +1 -1
  8. data/lib/decidim/dev/assets/empty_file.png +0 -0
  9. data/lib/decidim/dev/assets/malformed_svg_unclosed_element.svg +7 -0
  10. data/lib/decidim/dev/assets/malicious_svg_animation.svg +1 -0
  11. data/lib/decidim/dev/assets/malicious_svg_animation_attribute_names.svg +1 -0
  12. data/lib/decidim/dev/assets/malicious_svg_animation_fill.svg +1 -0
  13. data/lib/decidim/dev/assets/malicious_svg_animation_style.svg +1 -0
  14. data/lib/decidim/dev/assets/malicious_svg_animation_values.svg +1 -0
  15. data/lib/decidim/dev/assets/malicious_svg_attributename_case.svg +1 -0
  16. data/lib/decidim/dev/assets/malicious_svg_backslash_reference.svg +1 -0
  17. data/lib/decidim/dev/assets/malicious_svg_css_backslash_url.svg +1 -0
  18. data/lib/decidim/dev/assets/malicious_svg_css_comment_in_url.svg +1 -0
  19. data/lib/decidim/dev/assets/malicious_svg_css_image_set.svg +1 -0
  20. data/lib/decidim/dev/assets/malicious_svg_css_image_set_nested_parentheses.svg +1 -0
  21. data/lib/decidim/dev/assets/malicious_svg_cursor.svg +1 -0
  22. data/lib/decidim/dev/assets/malicious_svg_cursor_attribute.svg +1 -0
  23. data/lib/decidim/dev/assets/malicious_svg_data_uri_declared_encoding.svg +1 -0
  24. data/lib/decidim/dev/assets/malicious_svg_data_uri_deep_nested.svg +1 -0
  25. data/lib/decidim/dev/assets/malicious_svg_data_uri_leading_space.svg +1 -0
  26. data/lib/decidim/dev/assets/malicious_svg_data_uri_nested.svg +1 -0
  27. data/lib/decidim/dev/assets/malicious_svg_data_uri_parameter.svg +1 -0
  28. data/lib/decidim/dev/assets/malicious_svg_entity.svg +5 -0
  29. data/lib/decidim/dev/assets/malicious_svg_entity_parameter.svg +7 -0
  30. data/lib/decidim/dev/assets/malicious_svg_external_animation.svg +1 -0
  31. data/lib/decidim/dev/assets/malicious_svg_external_animation_missing_target.svg +1 -0
  32. data/lib/decidim/dev/assets/malicious_svg_external_animation_target.svg +1 -0
  33. data/lib/decidim/dev/assets/malicious_svg_external_feimage.svg +1 -0
  34. data/lib/decidim/dev/assets/malicious_svg_external_image.svg +1 -0
  35. data/lib/decidim/dev/assets/malicious_svg_external_use.svg +1 -0
  36. data/lib/decidim/dev/assets/malicious_svg_fill_external_reference.svg +1 -0
  37. data/lib/decidim/dev/assets/malicious_svg_foreign_object.svg +1 -0
  38. data/lib/decidim/dev/assets/malicious_svg_handler.svg +1 -0
  39. data/lib/decidim/dev/assets/malicious_svg_marker_external_reference.svg +1 -0
  40. data/lib/decidim/dev/assets/malicious_svg_mask_attribute.svg +1 -0
  41. data/lib/decidim/dev/assets/malicious_svg_script.svg +1 -0
  42. data/lib/decidim/dev/assets/malicious_svg_style_comment_string.svg +1 -0
  43. data/lib/decidim/dev/assets/malicious_svg_style_import.svg +1 -0
  44. data/lib/decidim/dev/assets/malicious_svg_style_import_string.svg +1 -0
  45. data/lib/decidim/dev/assets/malicious_svg_style_url_escape.svg +1 -0
  46. data/lib/decidim/dev/assets/malicious_svg_style_url_string.svg +1 -0
  47. data/lib/decidim/dev/assets/malicious_svg_stylesheet_pi.svg +1 -0
  48. data/lib/decidim/dev/assets/malicious_svg_uri.svg +1 -0
  49. data/lib/decidim/dev/assets/spoofed_image.png +0 -0
  50. data/lib/decidim/dev/assets/spoofed_image_binary.png +1 -0
  51. data/lib/decidim/dev/assets/spoofed_image_html.png +5 -0
  52. data/lib/decidim/dev/assets/spoofed_svg_xml.svg +2 -0
  53. data/lib/decidim/dev/assets/test.avif +0 -0
  54. data/lib/decidim/dev/assets/test.gif +0 -0
  55. data/lib/decidim/dev/assets/test.heic +0 -0
  56. data/lib/decidim/dev/assets/test.svg +1 -0
  57. data/lib/decidim/dev/assets/test_animation.svg +1 -0
  58. data/lib/decidim/dev/assets/test_animation_link.svg +7 -0
  59. data/lib/decidim/dev/assets/test_animation_presentation_reference.svg +8 -0
  60. data/lib/decidim/dev/assets/test_complex.svg +12 -0
  61. data/lib/decidim/dev/assets/test_css_references.svg +14 -0
  62. data/lib/decidim/dev/assets/test_data_uri_declared_encoding.svg +5 -0
  63. data/lib/decidim/dev/assets/test_entity_in_comment.svg +8 -0
  64. data/lib/decidim/dev/assets/test_entity_in_processing_instruction.svg +7 -0
  65. data/lib/decidim/dev/assets/test_percent_encoded_fragment.svg +7 -0
  66. data/lib/decidim/dev/assets/test_percent_encoded_multibyte_reference.svg +7 -0
  67. data/lib/decidim/dev/assets/test_percent_encoded_multibyte_reference_mixed.svg +8 -0
  68. data/lib/decidim/dev/assets/test_presentation_attribute_references.svg +13 -0
  69. data/lib/decidim/dev/assets/test_references.svg +12 -0
  70. data/lib/decidim/dev/assets/test_style_text.svg +9 -0
  71. data/lib/decidim/dev/assets/video.mp4 +0 -0
  72. data/lib/decidim/dev/rubocop/cop/decidim/enforce_permission_to.rb +459 -0
  73. data/lib/decidim/dev/rubocop/cop/decidim/organization_scoped_finder.rb +200 -0
  74. data/lib/decidim/dev/test/rspec_support/capybara.rb +54 -0
  75. data/lib/decidim/dev/test/rspec_support/editor_context.rb +1 -1
  76. data/lib/decidim/dev/test/rspec_support/helpers.rb +14 -1
  77. data/lib/decidim/dev/version.rb +1 -1
  78. metadata +79 -13
@@ -0,0 +1,459 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rubocop"
4
+
5
+ module RuboCop
6
+ module Cop
7
+ module Decidim
8
+ # Flags controller actions that lack authorization checks.
9
+ #
10
+ # In Decidim admin controllers, every action must explicitly call
11
+ # `enforce_permission_to` or `action_authorized_to` to verify the
12
+ # current user has permission to perform that action. Missing
13
+ # authorization checks can allow unauthorized access to resources.
14
+ #
15
+ # This cop checks all public method definitions in admin controllers
16
+ # and flags any that do not contain an authorization call.
17
+ #
18
+ # Actions are exempt when the controller uses a `before_action` that
19
+ # performs authorization (either by referencing a method that calls
20
+ # `enforce_permission_to`, or by using a name containing
21
+ # "permission", "authorize", or "enforce"). When such `before_action`
22
+ # is restricted with `only:` or `except:`, only the actions it covers
23
+ # are exempt.
24
+ #
25
+ # Methods referenced by framework lifecycle callbacks (`before_action`
26
+ # and its variants, `around_action`, `after_action`, `helper_method`,
27
+ # `rescue_from`, ...) are treated as lifecycle/helper methods and are
28
+ # never flagged, as they themselves are not controller actions.
29
+ #
30
+ # Calls to methods starting with `enforce_permission_to_` or
31
+ # `action_authorized_to_` (authorization helper wrappers) are also
32
+ # accepted as authorization checks.
33
+ #
34
+ # @example
35
+ # # bad
36
+ # class Admin::ResourcesController < Admin::ApplicationController
37
+ # def edit
38
+ # @resource = Resource.find(params[:id])
39
+ # end
40
+ # end
41
+ #
42
+ # # good
43
+ # class Admin::ResourcesController < Admin::ApplicationController
44
+ # def edit
45
+ # enforce_permission_to :update, :resource, resource:
46
+ # @resource = Resource.find(params[:id])
47
+ # end
48
+ # end
49
+ #
50
+ # # good - authorization via before_action
51
+ # class Admin::ResourcesController < Admin::ApplicationController
52
+ # before_action :ensure_permissions
53
+ #
54
+ # def edit
55
+ # @resource = Resource.find(params[:id])
56
+ # end
57
+ #
58
+ # private
59
+ #
60
+ # def ensure_permissions
61
+ # enforce_permission_to :update, :resource
62
+ # end
63
+ # end
64
+ class EnforcePermissionTo < RuboCop::Cop::Base
65
+ MSG = "Action `%{action}` is missing an authorization check. " \
66
+ "Add `enforce_permission_to` or `action_authorized_to` at the start of the action, " \
67
+ "or use `# rubocop:disable Decidim/EnforcePermissionTo` if authorization is handled elsewhere."
68
+
69
+ AUTHORIZATION_METHODS = [:enforce_permission_to, :action_authorized_to].freeze
70
+
71
+ # Prefixes accepted for authorization helper wrappers, such as
72
+ # `enforce_permission_to_update_resource`.
73
+ AUTHORIZATION_PREFIXES = %w(enforce_permission_to_ action_authorized_to_).freeze
74
+
75
+ BEFORE_ACTION_AUTH_KEYWORDS = %w(permission authorize enforce).freeze
76
+
77
+ # Visibility switch methods recognized when declared with no arguments.
78
+ VISIBILITY_METHODS = [:public, :protected, :private].freeze
79
+
80
+ # Framework declarations whose referenced methods are lifecycle or
81
+ # helper methods rather than controller actions.
82
+ CALLBACK_METHODS = [
83
+ :before_action, :prepend_before_action, :append_before_action, :skip_before_action,
84
+ :around_action, :prepend_around_action, :append_around_action, :skip_around_action,
85
+ :after_action, :prepend_after_action, :append_after_action, :skip_after_action,
86
+ :helper_method, :rescue_from
87
+ ].freeze
88
+
89
+ def on_new_investigation
90
+ @scope_callback_methods = {}
91
+ reset_scope_state
92
+ end
93
+
94
+ def on_class(node)
95
+ reset_scope_state
96
+ collect_callback_methods(node)
97
+ check_before_actions(node)
98
+ check_before_action_blocks(node)
99
+ end
100
+
101
+ def on_module(node)
102
+ reset_scope_state
103
+ collect_callback_methods(node)
104
+ check_before_actions(node)
105
+ check_before_action_blocks(node)
106
+ end
107
+
108
+ def on_def(node)
109
+ method_name = node.method_name
110
+
111
+ inline_visibility = @inline_visibility
112
+ @inline_visibility = nil
113
+
114
+ return if class_methods_block?(node)
115
+ return unless method_public?(node, method_name, inline_visibility)
116
+ return if action_exempted_by_before_action?(method_name)
117
+ return if callback_method?(node, method_name)
118
+ return if contains_authorization_check?(node)
119
+
120
+ action_name = method_name.to_s
121
+ add_offense(node, message: format(MSG, action: action_name))
122
+ end
123
+
124
+ def on_send(node)
125
+ return unless VISIBILITY_METHODS.include?(node.method_name)
126
+
127
+ if node.arguments.empty?
128
+ @in_private_section = node.method_name != :public
129
+ elsif visibility_def_argument?(node)
130
+ # `private def foo` only makes that single method private, it does
131
+ # not change the default visibility for the following methods.
132
+ @inline_visibility = node.method_name
133
+ end
134
+ end
135
+
136
+ private
137
+
138
+ # Resets the state that is scoped to a single class or module body.
139
+ # Callback methods are intentionally not part of this state: they are
140
+ # collected per scope in `collect_callback_methods`, so sibling scopes
141
+ # do not see each other's callbacks. A definition only inherits the
142
+ # callbacks of its own scope and of enclosing modules (never across a
143
+ # class boundary, as nested classes are not subclasses).
144
+ def reset_scope_state
145
+ @in_private_section = false
146
+ @inline_visibility = nil
147
+ @all_actions_auth = false
148
+ @only_actions = Set.new
149
+ @except_sets = []
150
+ end
151
+
152
+ # Definitions inside an `ActiveSupport::Concern` `class_methods do`
153
+ # block are class-level DSL methods, not controller actions, so they
154
+ # must never be flagged as missing an authorization check.
155
+ def class_methods_block?(node)
156
+ node.each_ancestor(:block).any? do |block|
157
+ block.send_node.method_name == :class_methods
158
+ end
159
+ end
160
+
161
+ def visibility_def_argument?(node)
162
+ node.arguments.any?(&:def_type?)
163
+ end
164
+
165
+ def method_public?(node, method_name, inline_visibility)
166
+ named_visibility = named_visibility_for(node, method_name)
167
+ return named_visibility == :public if named_visibility
168
+
169
+ visibility = inline_visibility || (@in_private_section ? :private : :public)
170
+ visibility == :public
171
+ end
172
+
173
+ # Resolves named visibility modifiers such as `public :publish` or
174
+ # `private :resource` that appear after the method definition. In Ruby
175
+ # these modifiers only apply to methods already defined, so a modifier
176
+ # placed before a later `def` does not change that definition's
177
+ # visibility. Among the applicable modifiers, the last one wins.
178
+ def named_visibility_for(node, method_name)
179
+ scope = node.each_ancestor(:class, :module).first
180
+ return nil unless scope
181
+
182
+ named_visibility_overrides(scope, node)[method_name]
183
+ end
184
+
185
+ def named_visibility_overrides(scope, node)
186
+ def_end = node.source_range.end_pos
187
+
188
+ scope.body&.each_child_node&.with_object({}) do |child, overrides|
189
+ next unless named_visibility_after?(child, def_end)
190
+
191
+ child.arguments.each do |arg|
192
+ covered_actions(arg).each do |name|
193
+ overrides[name] = child.method_name
194
+ end
195
+ end
196
+ end || {}
197
+ end
198
+
199
+ def named_visibility_after?(child, def_end)
200
+ named_visibility_modifier?(child) && child.source_range.begin_pos > def_end
201
+ end
202
+
203
+ def named_visibility_modifier?(node)
204
+ node.send_type? && VISIBILITY_METHODS.include?(node.method_name) &&
205
+ !node.arguments.empty? && !visibility_def_argument?(node)
206
+ end
207
+
208
+ # Collects the lifecycle/helper methods owned by a scope (a class or
209
+ # module). Only declarations and definitions belonging to the scope
210
+ # itself are considered; nested classes and modules are excluded so
211
+ # callbacks cannot leak between sibling scopes. The result is stored per
212
+ # scope so a definition can inherit the callbacks of its enclosing
213
+ # scopes without seeing those of its siblings.
214
+ def collect_callback_methods(node)
215
+ return unless node.body
216
+
217
+ definitions = own_scope_definitions(node)
218
+ declared = own_scope_callback_names(node)
219
+ @scope_callback_methods[node] = expand_callback_dependencies(declared, definitions)
220
+ end
221
+
222
+ # The methods referenced by callback declarations on the scope itself.
223
+ def own_scope_callback_names(node)
224
+ own_scope_nodes(node).each_with_object(Set.new) do |descendant, names|
225
+ next unless descendant.send_type?
226
+ next unless CALLBACK_METHODS.include?(descendant.method_name)
227
+
228
+ callback_method_names(descendant).each { |name| names << name }
229
+ end
230
+ end
231
+
232
+ # The method definitions belonging to the scope itself, indexed by name.
233
+ def own_scope_definitions(node)
234
+ own_scope_nodes(node).each_with_object({}) do |descendant, definitions|
235
+ next unless descendant.def_type?
236
+
237
+ definitions[descendant.method_name] ||= descendant
238
+ end
239
+ end
240
+
241
+ # Walks a scope body without descending into nested classes or modules,
242
+ # so each scope only sees its own declarations and definitions.
243
+ def own_scope_nodes(node)
244
+ nodes = []
245
+ queue = [node.body].compact
246
+
247
+ until queue.empty?
248
+ current = queue.shift
249
+ next if current.class_type? || current.module_type?
250
+
251
+ nodes << current
252
+ current.each_child_node { |child| queue << child }
253
+ end
254
+
255
+ nodes
256
+ end
257
+
258
+ # Methods called by a lifecycle callback are helper methods as well,
259
+ # e.g. a callback delegating to a public helper method.
260
+ def expand_callback_dependencies(declared, definitions)
261
+ names = Set.new(declared)
262
+ queue = declared.to_a
263
+
264
+ until queue.empty?
265
+ definition = definitions[queue.shift]
266
+ next unless definition&.body
267
+
268
+ sends = [definition.body]
269
+ sends.concat(definition.body.each_descendant(:send).to_a)
270
+
271
+ sends.each do |send_node|
272
+ next unless send_node.send_type?
273
+ next unless controller_call?(send_node)
274
+
275
+ callee = send_node.method_name
276
+ next unless definitions.has_key?(callee)
277
+ next unless names.add?(callee)
278
+
279
+ queue << callee
280
+ end
281
+ end
282
+
283
+ names
284
+ end
285
+
286
+ # Whether the method is a lifecycle/helper method of the def's own scope
287
+ # or of an enclosing module. Callbacks declared in a class are not
288
+ # inherited by definitions inside a nested class: Ruby does not treat a
289
+ # nested class as a subclass, so an outer controller's `helper_method`
290
+ # must not exempt an unrelated nested controller's action with the same
291
+ # name. Enclosing modules are still followed (a common concern pattern),
292
+ # but the walk stops at the first class boundary.
293
+ def callback_method?(node, method_name)
294
+ scopes = node.each_ancestor(:class, :module).to_a
295
+ return false if scopes.empty?
296
+
297
+ own_scope = scopes.shift
298
+ return true if scope_callback?(own_scope, method_name)
299
+
300
+ scopes.each do |scope|
301
+ break if scope.class_type?
302
+
303
+ return true if scope_callback?(scope, method_name)
304
+ end
305
+
306
+ false
307
+ end
308
+
309
+ def scope_callback?(scope, method_name)
310
+ @scope_callback_methods.fetch(scope, Set.new).include?(method_name)
311
+ end
312
+
313
+ # A call is made on the controller itself when the receiver is implicit
314
+ # or an explicit `self`. Calls with any other receiver, such as
315
+ # `Resource.find`, must not be followed because a same-named controller
316
+ # action could then be mistaken for a lifecycle/helper method, hiding a
317
+ # missing authorization check.
318
+ def controller_call?(send_node)
319
+ send_node.receiver.nil? || send_node.receiver.self_type?
320
+ end
321
+
322
+ def callback_method_names(send_node)
323
+ send_node.arguments.flat_map do |arg|
324
+ case arg.type
325
+ when :sym, :str
326
+ arg.value.to_sym
327
+ when :hash
328
+ rescue_handler_names(arg)
329
+ else
330
+ []
331
+ end
332
+ end
333
+ end
334
+
335
+ # `rescue_from SomeError, with: :handler` references the handler
336
+ # method through the `with:` option.
337
+ def rescue_handler_names(hash_node)
338
+ hash_node.pairs.filter_map do |pair|
339
+ next unless pair.key.sym_type? && pair.key.value == :with
340
+
341
+ value = pair.value
342
+ value.value.to_sym if value.sym_type? || value.str_type?
343
+ end
344
+ end
345
+
346
+ def check_before_actions(class_node)
347
+ class_node.body&.each_child_node(:send) do |send_node|
348
+ next unless send_node.method_name == :before_action
349
+
350
+ register_auth_before_action(send_node) if before_action_handles_auth?(send_node)
351
+ end
352
+ end
353
+
354
+ def check_before_action_blocks(class_node)
355
+ class_node.body&.each_descendant(:block) do |block_node|
356
+ next unless block_node.send_node.method_name == :before_action
357
+
358
+ register_auth_before_action(block_node.send_node) if block_contains_auth?(block_node)
359
+ end
360
+ end
361
+
362
+ def before_action_handles_auth?(node)
363
+ node.arguments.any? do |arg|
364
+ auth_symbol_argument?(arg)
365
+ end
366
+ end
367
+
368
+ def register_auth_before_action(node)
369
+ only, except = action_restrictions(node)
370
+
371
+ if only
372
+ only -= except if except
373
+ @only_actions.merge(only)
374
+ elsif except
375
+ @except_sets << except
376
+ else
377
+ @all_actions_auth = true
378
+ end
379
+ end
380
+
381
+ def action_exempted_by_before_action?(method_name)
382
+ return true if @all_actions_auth
383
+ return true if @only_actions.include?(method_name)
384
+ return true if @except_sets.any? { |actions| actions.none?(method_name) }
385
+
386
+ false
387
+ end
388
+
389
+ def action_restrictions(node)
390
+ only = nil
391
+ except = nil
392
+
393
+ node.arguments.each do |arg|
394
+ next unless arg.hash_type?
395
+
396
+ arg.pairs.each do |pair|
397
+ next unless pair.key.sym_type?
398
+
399
+ case pair.key.value
400
+ when :only
401
+ only = covered_actions(pair.value)
402
+ when :except
403
+ except = covered_actions(pair.value)
404
+ end
405
+ end
406
+ end
407
+
408
+ [only, except]
409
+ end
410
+
411
+ def covered_actions(value)
412
+ nodes = value.array_type? ? value.children : [value]
413
+
414
+ nodes.select { |node| node.sym_type? || node.str_type? }
415
+ .to_set { |node| node.value.to_sym }
416
+ end
417
+
418
+ def block_contains_auth?(block_node)
419
+ body = block_node.body
420
+ return false unless body
421
+
422
+ return true if authorization_call?(body)
423
+
424
+ body.each_descendant(:send).any? do |send_node|
425
+ authorization_call?(send_node)
426
+ end
427
+ end
428
+
429
+ def auth_symbol_argument?(arg)
430
+ return false unless arg.sym_type?
431
+
432
+ name = arg.value.to_s
433
+ BEFORE_ACTION_AUTH_KEYWORDS.any? { |keyword| name.include?(keyword) }
434
+ end
435
+
436
+ def contains_authorization_check?(node)
437
+ return false unless node.body
438
+
439
+ body = node.body
440
+
441
+ return true if authorization_call?(body)
442
+
443
+ body.each_descendant(:send).any? do |send_node|
444
+ authorization_call?(send_node)
445
+ end
446
+ end
447
+
448
+ def authorization_call?(send_node)
449
+ return false unless send_node.send_type?
450
+
451
+ return true if AUTHORIZATION_METHODS.include?(send_node.method_name)
452
+
453
+ method_name = send_node.method_name.to_s
454
+ AUTHORIZATION_PREFIXES.any? { |prefix| method_name.start_with?(prefix) }
455
+ end
456
+ end
457
+ end
458
+ end
459
+ end
@@ -0,0 +1,200 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rubocop"
4
+
5
+ module RuboCop
6
+ module Cop
7
+ module Decidim
8
+ # Flags unscoped ActiveRecord finder calls in controllers.
9
+ #
10
+ # In a multi-tenant Decidim deployment, loading a record without scoping
11
+ # it to the current organization can allow one tenant's users to access
12
+ # another tenant's resources. This cop promotes safe patterns such as
13
+ # `current_organization.templates.find_by(id: ...)` or using an already
14
+ # scoped `collection`.
15
+ #
16
+ # The cop targets chains that start with an ActiveRecord model constant
17
+ # and end with a terminal finder (`find`, `find_by`, `find_by!`, `first`,
18
+ # `take`, etc.) unless the chain includes a recognized scoping step.
19
+ class OrganizationScopedFinder < RuboCop::Cop::Base
20
+ MSG = "Unscoped ActiveRecord finder detected. Scope the query to the current organization, " \
21
+ "e.g. `current_organization.<relation>.find_by(id: params[:id])` or use an already-scoped `collection`."
22
+
23
+ FINDER_METHODS = [
24
+ :find, :find_by, :find_by!, :find_sole_by, :first, :first!, :last, :last!,
25
+ :sole, :sole!, :take, :take!,
26
+ :find_or_initialize_by, :find_or_create_by, :find_or_create_by!
27
+ ].freeze
28
+ RESTRICT_ON_SEND = [*FINDER_METHODS, :where].freeze
29
+
30
+ SCOPE_ROOT_METHODS = [:current_organization, :collection].freeze
31
+
32
+ SCOPED_WHERE_KEYS = [:current_organization, :current_component].freeze
33
+
34
+ ORGANIZATION_SCOPED_KEYS = [:decidim_organization_id, :organization_id].freeze
35
+
36
+ SCOPED_ASSOCIATION_KEYS = [:component, :organization, :participatory_space].freeze
37
+
38
+ SCOPED_HELPER_METHODS = [:current_organization, :current_component, :current_participatory_space].freeze
39
+
40
+ IGNORED_CONSTANT_PREFIXES = %w(Bundler ActiveRecord).freeze
41
+
42
+ def on_send(node)
43
+ return unless RESTRICT_ON_SEND.include?(node.method_name)
44
+ return if node.block_node
45
+ return if scoped_finder_arguments?(node)
46
+ return unless node.receiver
47
+ return if organization_scoped?(node.receiver)
48
+ return unless unscoped_model_class?(node.receiver)
49
+ return if constructed_scope_root?(node.receiver)
50
+
51
+ return if consumed_by_finder?(node)
52
+
53
+ add_offense(node)
54
+ end
55
+
56
+ private
57
+
58
+ def organization_scoped?(node)
59
+ return false unless node
60
+ return true if scoped_node?(node)
61
+ return organization_scoped?(node.receiver) if node.send_type?
62
+
63
+ false
64
+ end
65
+
66
+ def unscoped_model_class?(node)
67
+ return false unless node
68
+ return false if ignored_constant?(node)
69
+ return true if node.const_type?
70
+ return false unless node.send_type?
71
+
72
+ unscoped_model_class?(node.receiver)
73
+ end
74
+
75
+ def ignored_constant?(node)
76
+ node.const_type? &&
77
+ IGNORED_CONSTANT_PREFIXES.any? { |prefix| node.source.start_with?(prefix) }
78
+ end
79
+
80
+ def scoped_node?(node)
81
+ return false unless node.send_type?
82
+
83
+ scope_root?(node) || scoped_where?(node) || scoped_finder_arguments?(node)
84
+ end
85
+
86
+ def scope_root?(node)
87
+ return false unless SCOPE_ROOT_METHODS.include?(node.method_name)
88
+
89
+ node.receiver.nil? || node.receiver.self_type?
90
+ end
91
+
92
+ def scoped_finder_arguments?(node)
93
+ node.arguments.any? { |arg| scoped_argument?(arg) }
94
+ end
95
+
96
+ def scoped_where?(node)
97
+ return false unless node.method_name == :where
98
+
99
+ node.arguments.any? { |arg| scoped_argument?(arg) }
100
+ end
101
+
102
+ def scoped_argument?(node)
103
+ return false unless node.hash_type?
104
+
105
+ node.children.any? { |pair| scoped_pair?(pair) }
106
+ end
107
+
108
+ def scoped_pair?(pair_node)
109
+ return false unless pair_node.pair_type?
110
+
111
+ key = pair_node.children[0]
112
+ value = pair_node.children[1]
113
+
114
+ scoped_where_pair?(key, value) ||
115
+ scoped_association_pair?(key, value) ||
116
+ (value.hash_type? && value.children.any? { |inner| ownership_scoped_pair?(inner) })
117
+ end
118
+
119
+ def ownership_scoped_pair?(pair_node)
120
+ return false unless pair_node.pair_type?
121
+
122
+ key = pair_node.children[0]
123
+ value = pair_node.children[1]
124
+
125
+ scoped_association_pair?(key, value) ||
126
+ (value.hash_type? && value.children.any? { |inner| ownership_scoped_pair?(inner) })
127
+ end
128
+
129
+ def scoped_where_pair?(key, value)
130
+ return false unless key.sym_type?
131
+ return false unless scoped_where_key?(key)
132
+
133
+ scoped_trusted_value?(value)
134
+ end
135
+
136
+ def scoped_association_pair?(key, value)
137
+ return false unless key.sym_type?
138
+ return false unless SCOPED_ASSOCIATION_KEYS.include?(key.value)
139
+ return false unless value.send_type?
140
+ return false if value.receiver
141
+
142
+ value.method_name == :"current_#{key.value}"
143
+ end
144
+
145
+ def scoped_trusted_value?(node)
146
+ return false unless node
147
+
148
+ case node.type
149
+ when :send
150
+ scoped_helper_method?(node) || (node.receiver && scoped_trusted_value?(node.receiver))
151
+ when :hash
152
+ node.children.any? { |pair| scoped_pair?(pair) }
153
+ else
154
+ false
155
+ end
156
+ end
157
+
158
+ def scoped_helper_method?(node)
159
+ return false unless node.send_type?
160
+ return false if node.receiver
161
+
162
+ SCOPED_HELPER_METHODS.include?(node.method_name)
163
+ end
164
+
165
+ def scoped_where_key?(key)
166
+ key.sym_type? &&
167
+ (SCOPED_WHERE_KEYS.include?(key.value) ||
168
+ ORGANIZATION_SCOPED_KEYS.include?(key.value))
169
+ end
170
+
171
+ def constructed_scope_root?(node)
172
+ return false unless node.send_type?
173
+
174
+ if node.method_name == :new
175
+ return false unless node.receiver&.const_type?
176
+
177
+ return scoped_finder_arguments?(node)
178
+ end
179
+
180
+ constructed_scope_root?(node.receiver) if node.receiver
181
+ end
182
+
183
+ def consumed_by_finder?(node)
184
+ return false unless node.method_name == :where
185
+
186
+ ancestor = node.parent
187
+ while ancestor&.send_type?
188
+ break unless ancestor.receiver == node
189
+ return true if FINDER_METHODS.include?(ancestor.method_name)
190
+
191
+ node = ancestor
192
+ ancestor = node.parent
193
+ end
194
+
195
+ false
196
+ end
197
+ end
198
+ end
199
+ end
200
+ end