1# frozen_string_literal: true
2
3# Module to prepend into finders to specify whether or not the finder requires
4# cross project access
5#
6# This module depends on the finder implementing the following methods:
7#
8# - `#execute` should return an `ActiveRecord::Relation` or the `model` needs to
9#              be defined in the call to `requires_cross_project_access`.
10# - `#current_user` the user that requires access (or nil)
11module FinderWithCrossProjectAccess
12  extend ActiveSupport::Concern
13  extend ::Gitlab::Utils::Override
14
15  prepended do
16    extend Gitlab::CrossProjectAccess::ClassMethods
17
18    cattr_accessor :finder_model
19
20    def self.requires_cross_project_access(*args)
21      super
22
23      self.finder_model = extract_model_from_arguments(args)
24    end
25
26    private
27
28    def self.extract_model_from_arguments(args)
29      args.detect { |argument| argument.is_a?(Hash) && argument[:model] }
30        &.fetch(:model)
31    end
32  end
33
34  override :execute
35  def execute(*args, **kwargs)
36    check = Gitlab::CrossProjectAccess.find_check(self)
37    original = -> { super }
38
39    return original.call unless check
40    return original.call if should_skip_cross_project_check || can_read_cross_project?
41
42    if check.should_run?(self)
43      finder_model&.none || original.call.model.none
44    else
45      original.call
46    end
47  end
48
49  # We can skip the cross project check for finding indivitual records.
50  # this would be handled by the `can?(:read_*, result)` call in `FinderMethods`
51  # itself.
52  override :find_by!
53  def find_by!(*args)
54    skip_cross_project_check { super }
55  end
56
57  override :find_by
58  def find_by(*args)
59    skip_cross_project_check { super }
60  end
61
62  override :find
63  def find(*args)
64    skip_cross_project_check { super }
65  end
66
67  attr_accessor :should_skip_cross_project_check
68
69  def skip_cross_project_check
70    self.should_skip_cross_project_check = true
71
72    yield
73  ensure
74    # The find could raise an `ActiveRecord::RecordNotFound`, after which we
75    # still want to re-enable the check.
76    self.should_skip_cross_project_check = false
77  end
78
79  def can_read_cross_project?
80    Ability.allowed?(current_user, :read_cross_project)
81  end
82
83  def can_read_project?(project)
84    Ability.allowed?(current_user, :read_project, project)
85  end
86end
87