finder_with_cross_project_access.rb 2.2 KB
Newer Older
1 2
# frozen_string_literal: true

3 4 5 6 7
# Module to prepend into finders to specify wether or not the finder requires
# cross project access
#
# This module depends on the finder implementing the following methods:
#
8 9
# - `#execute` should return an `ActiveRecord::Relation` or the `model` needs to
#              be defined in the call to `requires_cross_project_access`.
10 11 12 13 14 15 16
# - `#current_user` the user that requires access (or nil)
module FinderWithCrossProjectAccess
  extend ActiveSupport::Concern
  extend ::Gitlab::Utils::Override

  prepended do
    extend Gitlab::CrossProjectAccess::ClassMethods
17 18 19 20 21 22 23 24 25 26 27 28 29 30 31

    cattr_accessor :finder_model

    def self.requires_cross_project_access(*args)
      super

      self.finder_model = extract_model_from_arguments(args)
    end

    private

    def self.extract_model_from_arguments(args)
      args.detect { |argument| argument.is_a?(Hash) && argument[:model] }
        &.fetch(:model)
    end
32 33 34 35 36
  end

  override :execute
  def execute(*args)
    check = Gitlab::CrossProjectAccess.find_check(self)
37
    original = -> { super }
38

39 40
    return original.call unless check
    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 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86
    end
  end

  # We can skip the cross project check for finding indivitual records.
  # this would be handled by the `can?(:read_*, result)` call in `FinderMethods`
  # itself.
  override :find_by!
  def find_by!(*args)
    skip_cross_project_check { super }
  end

  override :find_by
  def find_by(*args)
    skip_cross_project_check { super }
  end

  override :find
  def find(*args)
    skip_cross_project_check { super }
  end

  attr_accessor :should_skip_cross_project_check

  def skip_cross_project_check
    self.should_skip_cross_project_check = true

    yield
  ensure
    # The find could raise an `ActiveRecord::RecordNotFound`, after which we
    # still want to re-enable the check.
    self.should_skip_cross_project_check = false
  end

  def can_read_cross_project?
    Ability.allowed?(current_user, :read_cross_project)
  end

  def can_read_project?(project)
    Ability.allowed?(current_user, :read_project, project)
  end
end