diff options
Diffstat (limited to 'app/services')
18 files changed, 377 insertions, 193 deletions
diff --git a/app/services/chat_names/find_user_service.rb b/app/services/chat_names/find_user_service.rb index 4f5c5567b42..d458b814183 100644 --- a/app/services/chat_names/find_user_service.rb +++ b/app/services/chat_names/find_user_service.rb @@ -9,8 +9,8 @@ module ChatNames chat_name = find_chat_name return unless chat_name - chat_name.touch(:last_used_at) - chat_name.user + chat_name.update_last_used_at + chat_name end private diff --git a/app/services/ci/create_trace_artifact_service.rb b/app/services/ci/create_trace_artifact_service.rb index 280a2c3afa4..ffde824972c 100644 --- a/app/services/ci/create_trace_artifact_service.rb +++ b/app/services/ci/create_trace_artifact_service.rb @@ -4,13 +4,33 @@ module Ci return if job.job_artifacts_trace job.trace.read do |stream| - if stream.file? - job.create_job_artifacts_trace!( - project: job.project, - file_type: :trace, - file: stream) + break unless stream.file? + + clone_file!(stream.path, JobArtifactUploader.workhorse_upload_path) do |clone_path| + create_job_trace!(job, clone_path) + FileUtils.rm(stream.path) end end end + + private + + def create_job_trace!(job, path) + File.open(path) do |stream| + job.create_job_artifacts_trace!( + project: job.project, + file_type: :trace, + file: stream) + end + end + + def clone_file!(src_path, temp_dir) + FileUtils.mkdir_p(temp_dir) + Dir.mktmpdir('tmp-trace', temp_dir) do |dir_path| + temp_path = File.join(dir_path, "job.log") + FileUtils.copy(src_path, temp_path) + yield(temp_path) + end + end end end diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index e7463e6e25c..e87fd49d193 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -77,8 +77,12 @@ class IssuableBaseService < BaseService return unless labels params[:label_ids] = labels.split(",").map do |label_name| - service = Labels::FindOrCreateService.new(current_user, project, title: label_name.strip) - label = service.execute + label = Labels::FindOrCreateService.new( + current_user, + parent, + title: label_name.strip, + available_labels: available_labels + ).execute label.try(:id) end.compact @@ -102,7 +106,7 @@ class IssuableBaseService < BaseService end def available_labels - LabelsFinder.new(current_user, project_id: @project.id).execute + @available_labels ||= LabelsFinder.new(current_user, project_id: @project.id).execute end def merge_quick_actions_into_params!(issuable) @@ -247,7 +251,7 @@ class IssuableBaseService < BaseService when 'add' todo_service.mark_todo(issuable, current_user) when 'done' - todo = TodosFinder.new(current_user).execute.find_by(target: issuable) + todo = TodosFinder.new(current_user).find_by(target: issuable) todo_service.mark_todos_as_done_by_ids(todo, current_user) if todo end end @@ -303,4 +307,8 @@ class IssuableBaseService < BaseService def update_project_counter_caches?(issuable) issuable.state_changed? end + + def parent + project + end end diff --git a/app/services/labels/find_or_create_service.rb b/app/services/labels/find_or_create_service.rb index 940c8b333d3..079f611b3f3 100644 --- a/app/services/labels/find_or_create_service.rb +++ b/app/services/labels/find_or_create_service.rb @@ -1,8 +1,9 @@ module Labels class FindOrCreateService - def initialize(current_user, project, params = {}) + def initialize(current_user, parent, params = {}) @current_user = current_user - @project = project + @parent = parent + @available_labels = params.delete(:available_labels) @params = params.dup.with_indifferent_access end @@ -13,12 +14,13 @@ module Labels private - attr_reader :current_user, :project, :params, :skip_authorization + attr_reader :current_user, :parent, :params, :skip_authorization def available_labels @available_labels ||= LabelsFinder.new( current_user, - project_id: project.id + "#{parent_type}_id".to_sym => parent.id, + only_group_labels: parent_is_group? ).execute(skip_authorization: skip_authorization) end @@ -27,8 +29,8 @@ module Labels def find_or_create_label new_label = available_labels.find_by(title: title) - if new_label.nil? && (skip_authorization || Ability.allowed?(current_user, :admin_label, project)) - new_label = Labels::CreateService.new(params).execute(project: project) + if new_label.nil? && (skip_authorization || Ability.allowed?(current_user, :admin_label, parent)) + new_label = Labels::CreateService.new(params).execute(parent_type.to_sym => parent) end new_label @@ -37,5 +39,13 @@ module Labels def title params[:title] || params[:name] end + + def parent_type + parent.model_name.param_key + end + + def parent_is_group? + parent_type == "group" + end end end diff --git a/app/services/members/approve_access_request_service.rb b/app/services/members/approve_access_request_service.rb index 2a2bb0cae5b..6be08b590bc 100644 --- a/app/services/members/approve_access_request_service.rb +++ b/app/services/members/approve_access_request_service.rb @@ -1,51 +1,20 @@ module Members - class ApproveAccessRequestService < BaseService - include MembersHelper - - attr_accessor :source - - # source - The source object that respond to `#requesters` (i.g. project or group) - # current_user - The user that performs the access request approval - # params - A hash of parameters - # :user_id - User ID used to retrieve the access requester - # :id - Member ID used to retrieve the access requester - # :access_level - Optional access level set when the request is accepted - def initialize(source, current_user, params = {}) - @source = source - @current_user = current_user - @params = params.slice(:user_id, :id, :access_level) - end - - # opts - A hash of options - # :force - Bypass permission check: current_user can be nil in that case - def execute(opts = {}) - condition = params[:user_id] ? { user_id: params[:user_id] } : { id: params[:id] } - access_requester = source.requesters.find_by!(condition) - - raise Gitlab::Access::AccessDeniedError unless can_update_access_requester?(access_requester, opts) + class ApproveAccessRequestService < Members::BaseService + def execute(access_requester, skip_authorization: false, skip_log_audit_event: false) + raise Gitlab::Access::AccessDeniedError unless skip_authorization || can_update_access_requester?(access_requester) access_requester.access_level = params[:access_level] if params[:access_level] access_requester.accept_request + after_execute(member: access_requester, skip_log_audit_event: skip_log_audit_event) + access_requester end private - def can_update_access_requester?(access_requester, opts = {}) - access_requester && ( - opts[:force] || - can?(current_user, update_member_permission(access_requester), access_requester) - ) - end - - def update_member_permission(member) - case member - when GroupMember - :update_group_member - when ProjectMember - :update_project_member - end + def can_update_access_requester?(access_requester) + can?(current_user, update_member_permission(access_requester), access_requester) end end end diff --git a/app/services/members/authorized_destroy_service.rb b/app/services/members/authorized_destroy_service.rb deleted file mode 100644 index 2e89f00dad8..00000000000 --- a/app/services/members/authorized_destroy_service.rb +++ /dev/null @@ -1,61 +0,0 @@ -module Members - class AuthorizedDestroyService < BaseService - attr_accessor :member, :user - - def initialize(member, user = nil) - @member, @user = member, user - end - - def execute - return false if member.is_a?(GroupMember) && member.source.last_owner?(member.user) - - Member.transaction do - unassign_issues_and_merge_requests(member) unless member.invite? - member.notification_setting&.destroy - - member.destroy - end - - if member.request? && member.user != user - notification_service.decline_access_request(member) - end - - member - end - - private - - def unassign_issues_and_merge_requests(member) - if member.is_a?(GroupMember) - issues = Issue.unscoped.select(1) - .joins(:project) - .where('issues.id = issue_assignees.issue_id AND projects.namespace_id = ?', member.source_id) - - # DELETE FROM issue_assignees WHERE user_id = X AND EXISTS (...) - IssueAssignee.unscoped - .where('user_id = :user_id AND EXISTS (:sub)', user_id: member.user_id, sub: issues) - .delete_all - - MergeRequestsFinder.new(user, group_id: member.source_id, assignee_id: member.user_id) - .execute - .update_all(assignee_id: nil) - else - project = member.source - - # SELECT 1 FROM issues WHERE issues.id = issue_assignees.issue_id AND issues.project_id = X - issues = Issue.unscoped.select(1) - .where('issues.id = issue_assignees.issue_id') - .where(project_id: project.id) - - # DELETE FROM issue_assignees WHERE user_id = X AND EXISTS (...) - IssueAssignee.unscoped - .where('user_id = :user_id AND EXISTS (:sub)', user_id: member.user_id, sub: issues) - .delete_all - - project.merge_requests.opened.assigned_to(member.user).update_all(assignee_id: nil) - end - - member.user.invalidate_cache_counts - end - end -end diff --git a/app/services/members/base_service.rb b/app/services/members/base_service.rb new file mode 100644 index 00000000000..74556fb20cf --- /dev/null +++ b/app/services/members/base_service.rb @@ -0,0 +1,49 @@ +module Members + class BaseService < ::BaseService + # current_user - The user that performs the action + # params - A hash of parameters + def initialize(current_user = nil, params = {}) + @current_user = current_user + @params = params + end + + def after_execute(args) + # overriden in EE::Members modules + end + + private + + def update_member_permission(member) + case member + when GroupMember + :update_group_member + when ProjectMember + :update_project_member + else + raise "Unknown member type: #{member}!" + end + end + + def override_member_permission(member) + case member + when GroupMember + :override_group_member + when ProjectMember + :override_project_member + else + raise "Unknown member type: #{member}!" + end + end + + def action_member_permission(action, member) + case action + when :update + update_member_permission(member) + when :override + override_member_permission(member) + else + raise "Unknown action '#{action}' on #{member}!" + end + end + end +end diff --git a/app/services/members/create_service.rb b/app/services/members/create_service.rb index 26906ae7167..bc6a9405aac 100644 --- a/app/services/members/create_service.rb +++ b/app/services/members/create_service.rb @@ -1,15 +1,8 @@ module Members - class CreateService < BaseService + class CreateService < Members::BaseService DEFAULT_LIMIT = 100 - def initialize(source, current_user, params = {}) - @source = source - @current_user = current_user - @params = params - @error = nil - end - - def execute + def execute(source) return error('No users specified.') if params[:user_ids].blank? user_ids = params[:user_ids].split(',').uniq @@ -17,13 +10,15 @@ module Members return error("Too many users specified (limit is #{user_limit})") if user_limit && user_ids.size > user_limit - @source.add_users( + members = source.add_users( user_ids, params[:access_level], expires_at: params[:expires_at], current_user: current_user ) + members.each { |member| after_execute(member: member) } + success end diff --git a/app/services/members/destroy_service.rb b/app/services/members/destroy_service.rb index 05b93ac8fdb..b141bfd5fbc 100644 --- a/app/services/members/destroy_service.rb +++ b/app/services/members/destroy_service.rb @@ -1,42 +1,30 @@ module Members - class DestroyService < BaseService - include MembersHelper + class DestroyService < Members::BaseService + def execute(member, skip_authorization: false) + raise Gitlab::Access::AccessDeniedError unless skip_authorization || can_destroy_member?(member) - attr_accessor :source + return member if member.is_a?(GroupMember) && member.source.last_owner?(member.user) - ALLOWED_SCOPES = %i[members requesters all].freeze + Member.transaction do + unassign_issues_and_merge_requests(member) unless member.invite? + member.notification_setting&.destroy - def initialize(source, current_user, params = {}) - @source = source - @current_user = current_user - @params = params - end - - def execute(scope = :members) - raise "scope :#{scope} is not allowed!" unless ALLOWED_SCOPES.include?(scope) + member.destroy + end - member = find_member!(scope) + if member.request? && member.user != current_user + notification_service.decline_access_request(member) + end - raise Gitlab::Access::AccessDeniedError unless can_destroy_member?(member) + after_execute(member: member) - AuthorizedDestroyService.new(member, current_user).execute + member end private - def find_member!(scope) - condition = params[:user_id] ? { user_id: params[:user_id] } : { id: params[:id] } - case scope - when :all - source.members.find_by(condition) || - source.requesters.find_by!(condition) - else - source.public_send(scope).find_by!(condition) # rubocop:disable GitlabSecurity/PublicSend - end - end - def can_destroy_member?(member) - member && can?(current_user, destroy_member_permission(member), member) + can?(current_user, destroy_member_permission(member), member) end def destroy_member_permission(member) @@ -45,7 +33,42 @@ module Members :destroy_group_member when ProjectMember :destroy_project_member + else + raise "Unknown member type: #{member}!" end end + + def unassign_issues_and_merge_requests(member) + if member.is_a?(GroupMember) + issues = Issue.unscoped.select(1) + .joins(:project) + .where('issues.id = issue_assignees.issue_id AND projects.namespace_id = ?', member.source_id) + + # DELETE FROM issue_assignees WHERE user_id = X AND EXISTS (...) + IssueAssignee.unscoped + .where('user_id = :user_id AND EXISTS (:sub)', user_id: member.user_id, sub: issues) + .delete_all + + MergeRequestsFinder.new(current_user, group_id: member.source_id, assignee_id: member.user_id) + .execute + .update_all(assignee_id: nil) + else + project = member.source + + # SELECT 1 FROM issues WHERE issues.id = issue_assignees.issue_id AND issues.project_id = X + issues = Issue.unscoped.select(1) + .where('issues.id = issue_assignees.issue_id') + .where(project_id: project.id) + + # DELETE FROM issue_assignees WHERE user_id = X AND EXISTS (...) + IssueAssignee.unscoped + .where('user_id = :user_id AND EXISTS (:sub)', user_id: member.user_id, sub: issues) + .delete_all + + project.merge_requests.opened.assigned_to(member.user).update_all(assignee_id: nil) + end + + member.user.invalidate_cache_counts + end end end diff --git a/app/services/members/request_access_service.rb b/app/services/members/request_access_service.rb index 2614153d900..24293b30005 100644 --- a/app/services/members/request_access_service.rb +++ b/app/services/members/request_access_service.rb @@ -1,13 +1,6 @@ module Members - class RequestAccessService < BaseService - attr_accessor :source - - def initialize(source, current_user) - @source = source - @current_user = current_user - end - - def execute + class RequestAccessService < Members::BaseService + def execute(source) raise Gitlab::Access::AccessDeniedError unless can_request_access?(source) source.members.create( @@ -19,7 +12,7 @@ module Members private def can_request_access?(source) - source && can?(current_user, :request_access, source) + can?(current_user, :request_access, source) end end end diff --git a/app/services/members/update_service.rb b/app/services/members/update_service.rb new file mode 100644 index 00000000000..48b3d59f7bd --- /dev/null +++ b/app/services/members/update_service.rb @@ -0,0 +1,16 @@ +module Members + class UpdateService < Members::BaseService + # returns the updated member + def execute(member, permission: :update) + raise Gitlab::Access::AccessDeniedError unless can?(current_user, action_member_permission(permission, member), member) + + old_access_level = member.human_access + + if member.update_attributes(params) + after_execute(action: permission, old_access_level: old_access_level, member: member) + end + + member + end + end +end diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index 56e941d90ff..e07ecda27b5 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -339,6 +339,30 @@ class NotificationService end end + def pages_domain_verification_succeeded(domain) + recipients_for_pages_domain(domain).each do |user| + mailer.pages_domain_verification_succeeded_email(domain, user).deliver_later + end + end + + def pages_domain_verification_failed(domain) + recipients_for_pages_domain(domain).each do |user| + mailer.pages_domain_verification_failed_email(domain, user).deliver_later + end + end + + def pages_domain_enabled(domain) + recipients_for_pages_domain(domain).each do |user| + mailer.pages_domain_enabled_email(domain, user).deliver_later + end + end + + def pages_domain_disabled(domain) + recipients_for_pages_domain(domain).each do |user| + mailer.pages_domain_disabled_email(domain, user).deliver_later + end + end + protected def new_resource_email(target, method) @@ -433,6 +457,14 @@ class NotificationService private + def recipients_for_pages_domain(domain) + project = domain.project + + return [] unless project + + notifiable_users(project.team.masters, :watch, target: project) + end + def notifiable?(*args) NotificationRecipientService.notifiable?(*args) end diff --git a/app/services/projects/autocomplete_service.rb b/app/services/projects/autocomplete_service.rb index 1ae2c40872a..e61ecb696d0 100644 --- a/app/services/projects/autocomplete_service.rb +++ b/app/services/projects/autocomplete_service.rb @@ -50,16 +50,7 @@ module Projects return [] unless noteable&.is_a?(Issuable) - opts = { - project: project, - issuable: noteable, - current_user: current_user - } - QuickActions::InterpretService.command_definitions.map do |definition| - next unless definition.available?(opts) - - definition.to_h(opts) - end.compact + QuickActions::InterpretService.new(project, current_user).available_commands(noteable) end end end diff --git a/app/services/projects/update_pages_configuration_service.rb b/app/services/projects/update_pages_configuration_service.rb index cacb74b1205..52ff64cc938 100644 --- a/app/services/projects/update_pages_configuration_service.rb +++ b/app/services/projects/update_pages_configuration_service.rb @@ -23,7 +23,7 @@ module Projects end def pages_domains_config - project.pages_domains.map do |domain| + enabled_pages_domains.map do |domain| { domain: domain.domain, certificate: domain.certificate, @@ -32,6 +32,14 @@ module Projects end end + def enabled_pages_domains + if Gitlab::CurrentSettings.pages_domain_verification_enabled? + project.pages_domains.enabled + else + project.pages_domains + end + end + def reload_daemon # GitLab Pages daemon constantly watches for modification time of `pages.path` # It reloads configuration when `pages.path` is modified diff --git a/app/services/projects/update_service.rb b/app/services/projects/update_service.rb index 0e235a6d2a0..379a8068023 100644 --- a/app/services/projects/update_service.rb +++ b/app/services/projects/update_service.rb @@ -15,6 +15,8 @@ module Projects return error("Could not set the default branch") unless project.change_head(params[:default_branch]) end + ensure_wiki_exists if enabling_wiki? + if project.update_attributes(params.except(:default_branch)) if project.previous_changes.include?('path') project.rename_repo @@ -52,5 +54,18 @@ module Projects project.repository.exists? && new_branch && new_branch != project.default_branch end + + def enabling_wiki? + return false if @project.wiki_enabled? + + params[:project_feature_attributes][:wiki_access_level].to_i > ProjectFeature::DISABLED + end + + def ensure_wiki_exists + ProjectWiki.new(project, project.owner).wiki + rescue ProjectWiki::CouldNotCreateWikiError + log_error("Could not create wiki for #{project.full_name}") + Gitlab::Metrics.counter(:wiki_can_not_be_created_total, 'Counts the times we failed to create a wiki') + end end end diff --git a/app/services/quick_actions/interpret_service.rb b/app/services/quick_actions/interpret_service.rb index 669c1ba0a22..1e9bd84e749 100644 --- a/app/services/quick_actions/interpret_service.rb +++ b/app/services/quick_actions/interpret_service.rb @@ -7,6 +7,18 @@ module QuickActions SHRUG = '¯\\_(ツ)_/¯'.freeze TABLEFLIP = '(╯°□°)╯︵ ┻━┻'.freeze + # Takes an issuable and returns an array of all the available commands + # represented with .to_h + def available_commands(issuable) + @issuable = issuable + + self.class.command_definitions.map do |definition| + next unless definition.available?(self) + + definition.to_h(self) + end.compact + end + # Takes a text and interprets the commands that are extracted from it. # Returns the content without commands, and hash of changes to be applied to a record. def execute(content, issuable) @@ -15,8 +27,8 @@ module QuickActions @issuable = issuable @updates = {} - content, commands = extractor.extract_commands(content, context) - extract_updates(commands, context) + content, commands = extractor.extract_commands(content) + extract_updates(commands) [content, @updates] end @@ -28,8 +40,8 @@ module QuickActions @issuable = issuable - content, commands = extractor.extract_commands(content, context) - commands = explain_commands(commands, context) + content, commands = extractor.extract_commands(content) + commands = explain_commands(commands) [content, commands] end @@ -157,11 +169,11 @@ module QuickActions params '%"milestone"' condition do current_user.can?(:"admin_#{issuable.to_ability_name}", project) && - project.milestones.active.any? + find_milestones(project, state: 'active').any? end parse_params do |milestone_param| extract_references(milestone_param, :milestone).first || - project.milestones.find_by(title: milestone_param.strip) + find_milestones(project, title: milestone_param.strip).first end command :milestone do |milestone| @updates[:milestone_id] = milestone.id if milestone @@ -544,6 +556,10 @@ module QuickActions users end + def find_milestones(project, params = {}) + MilestonesFinder.new(params.merge(project_ids: [project.id], group_ids: [project.group&.id])).execute + end + def find_labels(labels_param) extract_references(labels_param, :label) | LabelsFinder.new(current_user, project_id: project.id, name: labels_param.split).execute @@ -557,21 +573,21 @@ module QuickActions find_labels(labels_param).map(&:id) end - def explain_commands(commands, opts) + def explain_commands(commands) commands.map do |name, arg| definition = self.class.definition_by_name(name) next unless definition - definition.explain(self, opts, arg) + definition.explain(self, arg) end.compact end - def extract_updates(commands, opts) + def extract_updates(commands) commands.each do |name, arg| definition = self.class.definition_by_name(name) next unless definition - definition.execute(self, opts, arg) + definition.execute(self, arg) end end @@ -581,14 +597,5 @@ module QuickActions ext.references(type) end - - def context - { - issuable: issuable, - current_user: current_user, - project: project, - params: params - } - end end end diff --git a/app/services/system_hooks_service.rb b/app/services/system_hooks_service.rb index a6b7a6e1416..af8c02a10b7 100644 --- a/app/services/system_hooks_service.rb +++ b/app/services/system_hooks_service.rb @@ -11,6 +11,8 @@ class SystemHooksService SystemHook.hooks_for(hooks_scope).find_each do |hook| hook.async_execute(data, 'system_hooks') end + + Gitlab::Plugin.execute_all_async(data) end private diff --git a/app/services/verify_pages_domain_service.rb b/app/services/verify_pages_domain_service.rb new file mode 100644 index 00000000000..86166047302 --- /dev/null +++ b/app/services/verify_pages_domain_service.rb @@ -0,0 +1,107 @@ +require 'resolv' + +class VerifyPagesDomainService < BaseService + # The maximum number of seconds to be spent on each DNS lookup + RESOLVER_TIMEOUT_SECONDS = 15 + + # How long verification lasts for + VERIFICATION_PERIOD = 7.days + + attr_reader :domain + + def initialize(domain) + @domain = domain + end + + def execute + return error("No verification code set for #{domain.domain}") unless domain.verification_code.present? + + if !verification_enabled? || dns_record_present? + verify_domain! + elsif expired? + disable_domain! + else + unverify_domain! + end + end + + private + + def verify_domain! + was_disabled = !domain.enabled? + was_unverified = domain.unverified? + + # Prevent any pre-existing grace period from being truncated + reverify = [domain.enabled_until, VERIFICATION_PERIOD.from_now].compact.max + + domain.update!(verified_at: Time.now, enabled_until: reverify) + + if was_disabled + notify(:enabled) + elsif was_unverified + notify(:verification_succeeded) + end + + success + end + + def unverify_domain! + if domain.verified? + domain.update!(verified_at: nil) + notify(:verification_failed) + end + + error("Couldn't verify #{domain.domain}") + end + + def disable_domain! + domain.update!(verified_at: nil, enabled_until: nil) + + notify(:disabled) + + error("Couldn't verify #{domain.domain}. It is now disabled.") + end + + # A domain is only expired until `disable!` has been called + def expired? + domain.enabled_until && domain.enabled_until < Time.now + end + + def dns_record_present? + Resolv::DNS.open do |resolver| + resolver.timeouts = RESOLVER_TIMEOUT_SECONDS + + check(domain.domain, resolver) || check(domain.verification_domain, resolver) + end + end + + def check(domain_name, resolver) + records = parse(txt_records(domain_name, resolver)) + + records.any? do |record| + record == domain.keyed_verification_code || record == domain.verification_code + end + rescue => err + log_error("Failed to check TXT records on #{domain_name} for #{domain.domain}: #{err}") + false + end + + def txt_records(domain_name, resolver) + resolver.getresources(domain_name, Resolv::DNS::Resource::IN::TXT) + end + + def parse(records) + records.flat_map(&:strings).flat_map(&:split) + end + + def verification_enabled? + Gitlab::CurrentSettings.pages_domain_verification_enabled? + end + + def notify(type) + return unless verification_enabled? + + Gitlab::AppLogger.info("Pages domain '#{domain.domain}' changed state to '#{type}'") + notification_service.public_send("pages_domain_#{type}", domain) # rubocop:disable GitlabSecurity/PublicSend + end +end |