2018-12-05 23:21:45 +05:30
|
|
|
# frozen_string_literal: true
|
|
|
|
|
2017-08-17 22:00:37 +05:30
|
|
|
class MembersFinder
|
2021-02-22 17:27:13 +05:30
|
|
|
RELATIONS = %i(direct inherited descendants invited_groups).freeze
|
|
|
|
DEFAULT_RELATIONS = %i(direct inherited).freeze
|
|
|
|
|
2020-03-13 15:44:24 +05:30
|
|
|
# Params can be any of the following:
|
|
|
|
# sort: string
|
|
|
|
# search: string
|
2020-05-24 23:13:21 +05:30
|
|
|
attr_reader :params
|
2017-08-17 22:00:37 +05:30
|
|
|
|
2020-05-24 23:13:21 +05:30
|
|
|
def initialize(project, current_user, params: {})
|
2017-08-17 22:00:37 +05:30
|
|
|
@project = project
|
|
|
|
@group = project.group
|
2020-05-24 23:13:21 +05:30
|
|
|
@current_user = current_user
|
|
|
|
@params = params
|
2017-08-17 22:00:37 +05:30
|
|
|
end
|
|
|
|
|
2021-02-22 17:27:13 +05:30
|
|
|
def execute(include_relations: DEFAULT_RELATIONS)
|
2020-05-24 23:13:21 +05:30
|
|
|
members = find_members(include_relations)
|
2020-03-13 15:44:24 +05:30
|
|
|
|
2020-05-24 23:13:21 +05:30
|
|
|
filter_members(members)
|
2020-03-13 15:44:24 +05:30
|
|
|
end
|
|
|
|
|
|
|
|
def can?(*args)
|
|
|
|
Ability.allowed?(*args)
|
|
|
|
end
|
|
|
|
|
|
|
|
private
|
|
|
|
|
|
|
|
attr_reader :project, :current_user, :group
|
|
|
|
|
2020-05-24 23:13:21 +05:30
|
|
|
def find_members(include_relations)
|
2017-08-17 22:00:37 +05:30
|
|
|
project_members = project.project_members
|
2020-10-24 23:57:45 +05:30
|
|
|
|
|
|
|
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
|
2017-08-17 22:00:37 +05:30
|
|
|
|
2020-01-01 13:55:28 +05:30
|
|
|
return project_members if include_relations == [:direct]
|
|
|
|
|
|
|
|
union_members = group_union_members(include_relations)
|
|
|
|
union_members << project_members if include_relations.include?(:direct)
|
2017-08-17 22:00:37 +05:30
|
|
|
|
2020-03-13 15:44:24 +05:30
|
|
|
return project_members unless union_members.any?
|
2017-08-17 22:00:37 +05:30
|
|
|
|
2020-03-13 15:44:24 +05:30
|
|
|
distinct_union_of_members(union_members)
|
2017-08-17 22:00:37 +05:30
|
|
|
end
|
2018-03-17 18:26:18 +05:30
|
|
|
|
2020-05-24 23:13:21 +05:30
|
|
|
def filter_members(members)
|
2020-03-13 15:44:24 +05:30
|
|
|
members = members.search(params[:search]) if params[:search].present?
|
|
|
|
members = members.sort_by_attribute(params[:sort]) if params[:sort].present?
|
2020-10-24 23:57:45 +05:30
|
|
|
members = members.owners_and_maintainers if params[:owners_and_maintainers].present?
|
2020-03-13 15:44:24 +05:30
|
|
|
members
|
|
|
|
end
|
2018-03-17 18:26:18 +05:30
|
|
|
|
2020-01-01 13:55:28 +05:30
|
|
|
def group_union_members(include_relations)
|
2019-09-04 21:01:54 +05:30
|
|
|
[].tap do |members|
|
2020-01-01 13:55:28 +05:30
|
|
|
members << direct_group_members(include_relations.include?(:descendants)) if group
|
2021-02-22 17:27:13 +05:30
|
|
|
members << project_invited_groups if include_relations.include?(:invited_groups)
|
2019-09-04 21:01:54 +05:30
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def direct_group_members(include_descendants)
|
2020-01-01 13:55:28 +05:30
|
|
|
requested_relations = [:inherited, :direct]
|
|
|
|
requested_relations << :descendants if include_descendants
|
2020-11-24 15:15:51 +05:30
|
|
|
GroupMembersFinder.new(group).execute(include_relations: requested_relations).non_invite.non_minimal_access # rubocop: disable CodeReuse/Finder
|
2019-09-04 21:01:54 +05:30
|
|
|
end
|
|
|
|
|
2021-02-22 17:27:13 +05:30
|
|
|
def project_invited_groups
|
2019-09-04 21:01:54 +05:30
|
|
|
invited_groups_ids_including_ancestors = Gitlab::ObjectHierarchy
|
|
|
|
.new(project.invited_groups)
|
|
|
|
.base_and_ancestors
|
|
|
|
.public_or_visible_to_user(current_user)
|
|
|
|
.select(:id)
|
|
|
|
|
2020-11-24 15:15:51 +05:30
|
|
|
GroupMember.with_source_id(invited_groups_ids_including_ancestors).non_minimal_access
|
2019-09-04 21:01:54 +05:30
|
|
|
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)
|
|
|
|
|
|
|
|
Member.includes(:user).from([Arel.sql("(#{sql}) AS #{Member.table_name}")]) # rubocop: disable CodeReuse/ActiveRecord
|
|
|
|
end
|
|
|
|
|
2018-03-17 18:26:18 +05:30
|
|
|
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.
|
2019-10-12 21:52:04 +05:30
|
|
|
<<~SQL
|
2019-12-04 20:38:33 +05:30
|
|
|
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
|
2019-10-12 21:52:04 +05:30
|
|
|
SQL
|
2018-03-17 18:26:18 +05:30
|
|
|
end
|
2019-12-04 20:38:33 +05:30
|
|
|
|
|
|
|
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
|
2017-08-17 22:00:37 +05:30
|
|
|
end
|