Welcome to mirror list, hosted at ThFree Co, Russian Federation.

members_finder.rb « finders « app - gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
blob: 6348bceb1574bea48ec150a5e38184c407fbb8ac (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
130
131
# frozen_string_literal: true

class MembersFinder
  RELATIONS = %i[direct inherited descendants invited_groups shared_into_ancestors].freeze
  DEFAULT_RELATIONS = %i[direct inherited].freeze

  # Params can be any of the following:
  #   sort:       string
  #   search:     string
  attr_reader :params

  def initialize(project, current_user, params: {})
    @project = project
    @group = project.group
    @current_user = current_user
    @params = params
  end

  def execute(include_relations: DEFAULT_RELATIONS)
    members = find_members(include_relations)

    filter_members(members)
  end

  def can?(...)
    Ability.allowed?(...)
  end

  private

  attr_reader :project, :current_user, :group

  def find_members(include_relations)
    project_members = project.namespace_members

    if params[:active_without_invites_and_requests].present?
      project_members = project_members.active_without_invites_and_requests
    else
      project_members = project_members.non_invite unless can?(current_user, :admin_project, project)
    end

    return project_members if include_relations == [:direct]

    union_members = group_union_members(include_relations)
    union_members << project_members if include_relations.include?(:direct)

    return project_members unless union_members.any?

    distinct_union_of_members(union_members)
  end

  def filter_members(members)
    members = members.search(params[:search]) if params[:search].present?
    members = members.sort_by_attribute(params[:sort]) if params[:sort].present?
    members = members.owners_and_maintainers if params[:owners_and_maintainers].present?
    members
  end

  def group_union_members(include_relations)
    [].tap do |members|
      members << direct_group_members(include_relations) if group
      members << project_invited_groups if include_relations.include?(:invited_groups)
    end
  end

  def direct_group_members(include_relations)
    requested_relations = [:inherited, :direct]
    requested_relations << :descendants if include_relations.include?(:descendants)
    requested_relations << :shared_from_groups if include_relations.include?(:shared_into_ancestors)

    GroupMembersFinder.new(group, current_user) # rubocop: disable CodeReuse/Finder
                      .execute(include_relations: requested_relations)
                      .non_invite
                      .non_minimal_access
  end

  def project_invited_groups
    invited_groups_ids_including_ancestors = project
                                               .invited_groups
                                               .self_and_ancestors
                                               .public_or_visible_to_user(current_user)
                                               .select(:id)

    GroupMember.with_source_id(invited_groups_ids_including_ancestors).non_minimal_access
  end

  def distinct_union_of_members(union_members)
    union = Gitlab::SQL::Union.new(union_members, remove_duplicates: false) # rubocop: disable Gitlab/Union
    sql = distinct_on(union)

    # enumerate the columns here since we are enumerating them in the union and want to be immune to
    # column caching issues when adding/removing columns
    members = Member.select(*Member.column_names)
          .includes(:user).from([Arel.sql("(#{sql}) AS #{Member.table_name}")]) # rubocop: disable CodeReuse/ActiveRecord
    # The left join with the table users in the method distinct_on needs to be resolved
    members.allow_cross_joins_across_databases(url: "https://gitlab.com/gitlab-org/gitlab/-/issues/417456")
  end

  def distinct_on(union)
    # We're interested in a list of members without duplicates by user_id.
    # We prefer project members over group members, project members should go first.
    <<~SQL
          SELECT DISTINCT ON (user_id, invite_email) #{member_columns}
          FROM (#{union.to_sql}) AS #{member_union_table}
          LEFT JOIN users on users.id = member_union.user_id
          LEFT JOIN project_authorizations on project_authorizations.user_id = users.id
               AND
               project_authorizations.project_id = #{project.id}
          ORDER BY user_id,
            invite_email,
            CASE
              WHEN type = 'ProjectMember' THEN 1
              WHEN type = 'GroupMember' THEN 2
              ELSE 3
            END
    SQL
  end

  def member_union_table
    'member_union'
  end

  def member_columns
    Member.column_names.map do |column_name|
      # fallback to members.access_level when project_authorizations.access_level is missing
      next "COALESCE(#{ProjectAuthorization.table_name}.access_level, #{member_union_table}.access_level) access_level" if column_name == 'access_level'

      "#{member_union_table}.#{column_name}"
    end.join(',')
  end
end