summaryrefslogtreecommitdiff
path: root/app/finders/projects/members/effective_access_level_finder.rb
blob: c1e3842a9e4fa6cfd3f14df6642609fdde0cb59b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
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
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
# frozen_string_literal: true

module Projects
  module Members
    class EffectiveAccessLevelFinder
      include Gitlab::Utils::StrongMemoize

      USER_ID_AND_ACCESS_LEVEL = [:user_id, :access_level].freeze
      BATCH_SIZE = 5

      def initialize(project)
        @project = project
      end

      def execute
        return Member.none if no_members?

        # rubocop: disable CodeReuse/ActiveRecord
        Member.from(generate_from_statement(user_ids_and_access_levels_from_all_memberships))
          .select([:user_id, 'MAX(access_level) AS access_level'])
          .group(:user_id)
        # rubocop: enable CodeReuse/ActiveRecord
      end

      private

      attr_reader :project

      def generate_from_statement(user_ids_and_access_levels)
        "(VALUES #{generate_values_expression(user_ids_and_access_levels)}) members (user_id, access_level)"
      end

      def generate_values_expression(user_ids_and_access_levels)
        user_ids_and_access_levels.map do |user_id, access_level|
          "(#{user_id}, #{access_level})"
        end.join(",")
      end

      def no_members?
        user_ids_and_access_levels_from_all_memberships.blank?
      end

      def all_possible_avenues_of_membership
        avenues = [authorizable_project_members]

        avenues << if project.personal?
                     project_owner_acting_as_maintainer
                   else
                     authorizable_group_members
                   end

        if include_membership_from_project_group_shares?
          avenues << members_from_project_group_shares
        end

        avenues
      end

      # @return [Array<[user_id, access_level]>]
      def user_ids_and_access_levels_from_all_memberships
        strong_memoize(:user_ids_and_access_levels_from_all_memberships) do
          all_possible_avenues_of_membership.flat_map do |members|
            apply_scopes(members).pluck(*USER_ID_AND_ACCESS_LEVEL) # rubocop: disable CodeReuse/ActiveRecord
          end
        end
      end

      def authorizable_project_members
        project.members.authorizable
      end

      def authorizable_group_members
        project.group.authorizable_members_with_parents
      end

      def members_from_project_group_shares
        members = []

        project.project_group_links.each_batch(of: BATCH_SIZE) do |relation|
          members_per_batch = []

          relation.includes(:group).each do |link| # rubocop: disable CodeReuse/ActiveRecord
            members_per_batch << link.group.authorizable_members_with_parents.select(*user_id_and_access_level_for_project_group_shares(link))
          end

          members << Member.from_union(members_per_batch)
        end

        Member.from_union(members)
      end

      def project_owner_acting_as_maintainer
        user_id = project.namespace.owner.id
        access_level = Gitlab::Access::MAINTAINER

        Member
          .from(generate_from_statement([[user_id, access_level]])) # rubocop: disable CodeReuse/ActiveRecord
          .limit(1)
      end

      def include_membership_from_project_group_shares?
        project.allowed_to_share_with_group? && project.project_group_links.any?
      end

      # methods for `select` options

      def user_id_and_access_level_for_project_group_shares(link)
        least_access_level_among_group_membership_and_project_share =
          smallest_value_arel([link.group_access, GroupMember.arel_table[:access_level]], 'access_level')

        [
          :user_id,
          least_access_level_among_group_membership_and_project_share
        ]
      end

      def smallest_value_arel(args, column_alias)
        Arel::Nodes::As.new(
          Arel::Nodes::NamedFunction.new('LEAST', args),
          Arel.sql(column_alias)
        )
      end

      def apply_scopes(members)
        members
      end
    end
  end
end