debian-mirror-gitlab/app/services/members/creator_service.rb
2023-06-20 00:43:36 +05:30

291 lines
8.1 KiB
Ruby

# frozen_string_literal: true
module Members
# This class serves as more of an app-wide way we add/create members
# All roads to add members should take this path.
class CreatorService
class << self
def parsed_access_level(access_level)
access_levels.fetch(access_level) { access_level.to_i }
end
def access_levels
Gitlab::Access.sym_options_with_owner
end
# Add members to sources with passed access option
#
# access can be an integer representing a access code
# or symbol like :maintainer representing role
#
# Ex.
# add_members(
# sources,
# user_ids,
# Member::MAINTAINER
# )
#
# add_members(
# sources,
# user_ids,
# :maintainer
# )
#
# @param sources [Group, Project, Array<Group>, Array<Project>, Group::ActiveRecord_Relation,
# Project::ActiveRecord_Relation] - Can't be an array of source ids because we don't know the type of source.
# @return Array<Member>
def add_members(
sources,
invitees,
access_level,
current_user: nil,
expires_at: nil,
tasks_to_be_done: [],
tasks_project_id: nil,
ldap: nil
) # rubocop:disable Metrics/ParameterLists
return [] unless invitees.present?
sources = Array.wrap(sources) if sources.is_a?(ApplicationRecord) # For single source
Member.transaction do
sources.flat_map do |source|
# If this user is attempting to manage Owner members and doesn't have permission, do not allow
next [] if managing_owners?(current_user, access_level) && cannot_manage_owners?(source, current_user)
emails, users, existing_members = parse_users_list(source, invitees)
common_arguments = {
source: source,
access_level: access_level,
existing_members: existing_members,
current_user: current_user,
expires_at: expires_at,
tasks_to_be_done: tasks_to_be_done,
tasks_project_id: tasks_project_id,
ldap: ldap
}
members = emails.map do |email|
new(invitee: email, builder: InviteMemberBuilder, **common_arguments).execute
end
members += users.map do |user|
new(invitee: user, **common_arguments).execute
end
members
end
end
end
def add_member(
source,
invitee,
access_level,
current_user: nil,
expires_at: nil,
ldap: nil
) # rubocop:disable Metrics/ParameterLists
add_members(
source,
[invitee],
access_level,
current_user: current_user,
expires_at: expires_at,
ldap: ldap
).first
end
private
def managing_owners?(current_user, access_level)
current_user && Gitlab::Access.sym_options_with_owner[access_level] == Gitlab::Access::OWNER
end
def parse_users_list(source, list)
emails = []
user_ids = []
users = []
existing_members = {}
list.each do |item|
case item
when User
users << item
when Integer
user_ids << item
when /\A\d+\Z/
user_ids << item.to_i
when Devise.email_regexp
emails << item
end
end
# the below will automatically discard invalid user_ids
users.concat(User.id_in(user_ids)) if user_ids.present?
# de-duplicate just in case as there is no controlling if user records and ids are sent multiple times
users.uniq!
users_by_emails = source.users_by_emails(emails) # preloads our request store for all emails
# in case emails belong to a user that is being invited by user or user_id, remove them from
# emails and let users/user_ids handle it.
parsed_emails = emails.select do |email|
user = users_by_emails[email]
!user || (users.exclude?(user) && user_ids.exclude?(user.id))
end
if users.present? || users_by_emails.present?
# helps not have to perform another query per user id to see if the member exists later on when fetching
existing_members = source.members_and_requesters.with_user(users + users_by_emails.values).index_by(&:user_id)
end
[parsed_emails, users, existing_members]
end
end
def initialize(invitee:, builder: StandardMemberBuilder, **args)
@invitee = invitee
@builder = builder
@args = args
@access_level = self.class.parsed_access_level(args[:access_level])
end
private_class_method :new
def execute
find_or_build_member
commit_member
after_commit_tasks
member
end
private
delegate :new_record?, to: :member
attr_reader :invitee, :access_level, :member, :args, :builder
def assign_member_attributes
member.attributes = member_attributes
end
def commit_member
if can_commit_member?
assign_member_attributes
commit_changes
else
add_commit_error
end
end
def can_commit_member?
# There is no current user for bulk actions, in which case anything is allowed
return true if skip_authorization?
if new_record?
can_create_new_member?
else
can_update_existing_member?
end
end
def can_create_new_member?
raise NotImplementedError
end
def can_update_existing_member?
raise NotImplementedError
end
# Populates the attributes of a member.
#
# This logic resides in a separate method so that EE can extend this logic,
# without having to patch the `add_members` method directly.
def member_attributes
{
created_by: member.created_by || current_user,
access_level: access_level,
expires_at: args[:expires_at]
}
end
def commit_changes
if member.request?
approve_request
elsif member.changed?
# Calling #save triggers callbacks even if there is no change on object.
# This previously caused an incident due to the hard to predict
# behaviour caused by the large number of callbacks.
# See https://gitlab.com/gitlab-com/gl-infra/production/-/issues/6351
# and https://gitlab.com/gitlab-org/gitlab/-/merge_requests/80920#note_911569038
# for details.
member.save
end
end
def after_commit_tasks
create_member_task
end
def create_member_task
return unless member.persisted?
return if member_task_attributes.value?(nil)
return if member.member_task.present?
member.create_member_task(member_task_attributes)
end
def member_task_attributes
{
tasks_to_be_done: args[:tasks_to_be_done],
project_id: args[:tasks_project_id]
}
end
def approve_request
::Members::ApproveAccessRequestService.new(current_user, access_level: access_level)
.execute(
member,
skip_authorization: ldap || skip_authorization?,
skip_log_audit_event: ldap
)
end
def current_user
args[:current_user]
end
def skip_authorization?
!current_user
end
def add_commit_error
msg = if new_record?
_('not authorized to create member')
else
_('not authorized to update member')
end
member.errors.add(:base, msg)
end
def find_or_build_member
@member = builder.new(source, invitee, existing_members).execute
end
def ldap
args[:ldap] || false
end
def source
args[:source]
end
def existing_members
args[:existing_members] || {}
end
end
end
Members::CreatorService.prepend_mod_with('Members::CreatorService')