# 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_list = Arel::Nodes::ValuesList.new(user_ids_and_access_levels).to_sql "(#{values_list}) members (user_id, access_level)" 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 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 # workaround until we migrate Project#owners to have membership with # OWNER access level def project_owner user_id = project.namespace.owner.id access_level = Gitlab::Access::OWNER Member .from(generate_from_statement([[user_id, access_level]])) # rubocop: disable CodeReuse/ActiveRecord .limit(1) end def include_membership_from_project_group_shares? !project.namespace.share_with_group_lock && 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