diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-04-20 21:38:24 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-04-20 21:38:24 +0300 |
commit | 983a0bba5d2a042c4a3bbb22432ec192c7501d82 (patch) | |
tree | b153cd387c14ba23bd5a07514c7c01fddf6a78a0 /app/services | |
parent | a2bddee2cdb38673df0e004d5b32d9f77797de64 (diff) |
Add latest changes from gitlab-org/gitlab@12-10-stable-ee
Diffstat (limited to 'app/services')
26 files changed, 524 insertions, 63 deletions
diff --git a/app/services/auto_merge/base_service.rb b/app/services/auto_merge/base_service.rb index e08b4ac2260..1de2f31f87c 100644 --- a/app/services/auto_merge/base_service.rb +++ b/app/services/auto_merge/base_service.rb @@ -49,6 +49,14 @@ module AutoMerge end end + def available_for?(merge_request) + strong_memoize("available_for_#{merge_request.id}") do + merge_request.can_be_merged_by?(current_user) && + merge_request.mergeable_state?(skip_ci_check: true) && + yield + end + end + private def strategy diff --git a/app/services/auto_merge/merge_when_pipeline_succeeds_service.rb b/app/services/auto_merge/merge_when_pipeline_succeeds_service.rb index 7c0e9228b28..9ae5bd1b5ec 100644 --- a/app/services/auto_merge/merge_when_pipeline_succeeds_service.rb +++ b/app/services/auto_merge/merge_when_pipeline_succeeds_service.rb @@ -30,7 +30,9 @@ module AutoMerge end def available_for?(merge_request) - merge_request.actual_head_pipeline&.active? + super do + merge_request.actual_head_pipeline&.active? + end end end end diff --git a/app/services/auto_merge_service.rb b/app/services/auto_merge_service.rb index eee227be202..c5cbcc7c93b 100644 --- a/app/services/auto_merge_service.rb +++ b/app/services/auto_merge_service.rb @@ -1,23 +1,26 @@ # frozen_string_literal: true class AutoMergeService < BaseService + include Gitlab::Utils::StrongMemoize + STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS = 'merge_when_pipeline_succeeds' STRATEGIES = [STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS].freeze class << self - def all_strategies + def all_strategies_ordered_by_preference STRATEGIES end def get_service_class(strategy) - return unless all_strategies.include?(strategy) + return unless all_strategies_ordered_by_preference.include?(strategy) "::AutoMerge::#{strategy.camelize}Service".constantize end end - def execute(merge_request, strategy) - service = get_service_instance(strategy) + def execute(merge_request, strategy = nil) + strategy ||= preferred_strategy(merge_request) + service = get_service_instance(merge_request, strategy) return :failed unless service&.available_for?(merge_request) @@ -27,37 +30,47 @@ class AutoMergeService < BaseService def update(merge_request) return :failed unless merge_request.auto_merge_enabled? - get_service_instance(merge_request.auto_merge_strategy).update(merge_request) + strategy = merge_request.auto_merge_strategy + get_service_instance(merge_request, strategy).update(merge_request) end def process(merge_request) return unless merge_request.auto_merge_enabled? - get_service_instance(merge_request.auto_merge_strategy).process(merge_request) + strategy = merge_request.auto_merge_strategy + get_service_instance(merge_request, strategy).process(merge_request) end def cancel(merge_request) return error("Can't cancel the automatic merge", 406) unless merge_request.auto_merge_enabled? - get_service_instance(merge_request.auto_merge_strategy).cancel(merge_request) + strategy = merge_request.auto_merge_strategy + get_service_instance(merge_request, strategy).cancel(merge_request) end def abort(merge_request, reason) return error("Can't abort the automatic merge", 406) unless merge_request.auto_merge_enabled? - get_service_instance(merge_request.auto_merge_strategy).abort(merge_request, reason) + strategy = merge_request.auto_merge_strategy + get_service_instance(merge_request, strategy).abort(merge_request, reason) end def available_strategies(merge_request) - self.class.all_strategies.select do |strategy| - get_service_instance(strategy).available_for?(merge_request) + self.class.all_strategies_ordered_by_preference.select do |strategy| + get_service_instance(merge_request, strategy).available_for?(merge_request) end end + def preferred_strategy(merge_request) + available_strategies(merge_request).first + end + private - def get_service_instance(strategy) - self.class.get_service_class(strategy)&.new(project, current_user, params) + def get_service_instance(merge_request, strategy) + strong_memoize("service_instance_#{merge_request.id}_#{strategy}") do + self.class.get_service_class(strategy)&.new(project, current_user, params) + end end end diff --git a/app/services/clusters/applications/base_service.rb b/app/services/clusters/applications/base_service.rb index bd4ce693085..86b48b5228d 100644 --- a/app/services/clusters/applications/base_service.rb +++ b/app/services/clusters/applications/base_service.rb @@ -35,6 +35,18 @@ module Clusters application.modsecurity_mode = params[:modsecurity_mode] || 0 end + if application.has_attribute?(:host) + application.host = params[:host] + end + + if application.has_attribute?(:protocol) + application.protocol = params[:protocol] + end + + if application.has_attribute?(:port) + application.port = params[:port] + end + if application.respond_to?(:oauth_application) application.oauth_application = create_oauth_application(application, request) end diff --git a/app/services/concerns/deploy_token_methods.rb b/app/services/concerns/deploy_token_methods.rb index c875342a07c..f59a50d6878 100644 --- a/app/services/concerns/deploy_token_methods.rb +++ b/app/services/concerns/deploy_token_methods.rb @@ -14,4 +14,12 @@ module DeployTokenMethods deploy_token.destroy end + + def create_deploy_token_payload_for(deploy_token) + if deploy_token.persisted? + success(deploy_token: deploy_token, http_status: :created) + else + error(deploy_token.errors.full_messages.to_sentence, :bad_request, pass_back: { deploy_token: deploy_token }) + end + end end diff --git a/app/services/emails/destroy_service.rb b/app/services/emails/destroy_service.rb index a0b43ad3d08..6e671f52d57 100644 --- a/app/services/emails/destroy_service.rb +++ b/app/services/emails/destroy_service.rb @@ -13,7 +13,7 @@ module Emails user.update_secondary_emails! end - result[:status] == 'success' + result[:status] == :success end end end diff --git a/app/services/git/branch_push_service.rb b/app/services/git/branch_push_service.rb index da45bcc7eaa..5c1ee981d0c 100644 --- a/app/services/git/branch_push_service.rb +++ b/app/services/git/branch_push_service.rb @@ -36,6 +36,8 @@ module Git # Update merge requests that may be affected by this push. A new branch # could cause the last commit of a merge request to change. def enqueue_update_mrs + return if params[:merge_request_branches]&.exclude?(branch_name) + UpdateMergeRequestsWorker.perform_async( project.id, current_user.id, diff --git a/app/services/git/process_ref_changes_service.rb b/app/services/git/process_ref_changes_service.rb index 387cd29d69d..6d1ff97016b 100644 --- a/app/services/git/process_ref_changes_service.rb +++ b/app/services/git/process_ref_changes_service.rb @@ -42,6 +42,7 @@ module Git push_service_class = push_service_class_for(ref_type) create_bulk_push_event = changes.size > Gitlab::CurrentSettings.push_event_activities_limit + merge_request_branches = merge_request_branches_for(changes) changes.each do |change| push_service_class.new( @@ -49,6 +50,7 @@ module Git current_user, change: change, push_options: params[:push_options], + merge_request_branches: merge_request_branches, create_pipelines: change[:index] < PIPELINE_PROCESS_LIMIT || Feature.enabled?(:git_push_create_all_pipelines, project), execute_project_hooks: execute_project_hooks, create_push_event: !create_bulk_push_event @@ -71,5 +73,11 @@ module Git Git::BranchPushService end + + def merge_request_branches_for(changes) + return if Feature.disabled?(:refresh_only_existing_merge_requests_on_push, default_enabled: true) + + @merge_requests_branches ||= MergeRequests::PushedBranchesService.new(project, current_user, changes: changes).execute + end end end diff --git a/app/services/groups/deploy_tokens/create_service.rb b/app/services/groups/deploy_tokens/create_service.rb index 81f761eb61d..aee423659ef 100644 --- a/app/services/groups/deploy_tokens/create_service.rb +++ b/app/services/groups/deploy_tokens/create_service.rb @@ -8,11 +8,7 @@ module Groups def execute deploy_token = create_deploy_token_for(@group, params) - if deploy_token.persisted? - success(deploy_token: deploy_token, http_status: :created) - else - error(deploy_token.errors.full_messages.to_sentence, :bad_request) - end + create_deploy_token_payload_for(deploy_token) end end end diff --git a/app/services/groups/import_export/import_service.rb b/app/services/groups/import_export/import_service.rb index 548a4a98dc1..f62b9d3c8a6 100644 --- a/app/services/groups/import_export/import_service.rb +++ b/app/services/groups/import_export/import_service.rb @@ -33,10 +33,12 @@ module Groups end def restorer - @restorer ||= Gitlab::ImportExport::Group::TreeRestorer.new(user: @current_user, - shared: @shared, - group: @group, - group_hash: nil) + @restorer ||= Gitlab::ImportExport::Group::LegacyTreeRestorer.new( + user: @current_user, + shared: @shared, + group: @group, + group_hash: nil + ) end def remove_import_file diff --git a/app/services/groups/transfer_service.rb b/app/services/groups/transfer_service.rb index 4e7875e0491..fe3ab884302 100644 --- a/app/services/groups/transfer_service.rb +++ b/app/services/groups/transfer_service.rb @@ -2,15 +2,6 @@ module Groups class TransferService < Groups::BaseService - ERROR_MESSAGES = { - database_not_supported: s_('TransferGroup|Database is not supported.'), - namespace_with_same_path: s_('TransferGroup|The parent group already has a subgroup with the same path.'), - group_is_already_root: s_('TransferGroup|Group is already a root group.'), - same_parent_as_current: s_('TransferGroup|Group is already associated to the parent group.'), - invalid_policies: s_("TransferGroup|You don't have enough permissions."), - group_contains_images: s_('TransferGroup|Cannot update the path because there are projects under this group that contain Docker images in their Container Registry. Please remove the images from your projects first and try again.') - }.freeze - TransferError = Class.new(StandardError) attr_reader :error, :new_parent_group @@ -124,7 +115,18 @@ module Groups end def raise_transfer_error(message) - raise TransferError, ERROR_MESSAGES[message] + raise TransferError, localized_error_messages[message] + end + + def localized_error_messages + { + database_not_supported: s_('TransferGroup|Database is not supported.'), + namespace_with_same_path: s_('TransferGroup|The parent group already has a subgroup with the same path.'), + group_is_already_root: s_('TransferGroup|Group is already a root group.'), + same_parent_as_current: s_('TransferGroup|Group is already associated to the parent group.'), + invalid_policies: s_("TransferGroup|You don't have enough permissions."), + group_contains_images: s_('TransferGroup|Cannot update the path because there are projects under this group that contain Docker images in their Container Registry. Please remove the images from your projects first and try again.') + }.freeze end end end diff --git a/app/services/issues/export_csv_service.rb b/app/services/issues/export_csv_service.rb new file mode 100644 index 00000000000..1dcdfb9faea --- /dev/null +++ b/app/services/issues/export_csv_service.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +module Issues + class ExportCsvService + include Gitlab::Routing.url_helpers + include GitlabRoutingHelper + + # Target attachment size before base64 encoding + TARGET_FILESIZE = 15000000 + + attr_reader :project + + def initialize(issues_relation, project) + @issues = issues_relation + @labels = @issues.labels_hash + @project = project + end + + def csv_data + csv_builder.render(TARGET_FILESIZE) + end + + def email(user) + Notify.issues_csv_email(user, project, csv_data, csv_builder.status).deliver_now + end + + # rubocop: disable CodeReuse/ActiveRecord + def csv_builder + @csv_builder ||= + CsvBuilder.new(@issues.preload(associations_to_preload), header_to_value_hash) + end + # rubocop: enable CodeReuse/ActiveRecord + + private + + def associations_to_preload + %i(author assignees timelogs) + end + + def header_to_value_hash + { + 'Issue ID' => 'iid', + 'URL' => -> (issue) { issue_url(issue) }, + 'Title' => 'title', + 'State' => -> (issue) { issue.closed? ? 'Closed' : 'Open' }, + 'Description' => 'description', + 'Author' => 'author_name', + 'Author Username' => -> (issue) { issue.author&.username }, + 'Assignee' => -> (issue) { issue.assignees.map(&:name).join(', ') }, + 'Assignee Username' => -> (issue) { issue.assignees.map(&:username).join(', ') }, + 'Confidential' => -> (issue) { issue.confidential? ? 'Yes' : 'No' }, + 'Locked' => -> (issue) { issue.discussion_locked? ? 'Yes' : 'No' }, + 'Due Date' => -> (issue) { issue.due_date&.to_s(:csv) }, + 'Created At (UTC)' => -> (issue) { issue.created_at&.to_s(:csv) }, + 'Updated At (UTC)' => -> (issue) { issue.updated_at&.to_s(:csv) }, + 'Closed At (UTC)' => -> (issue) { issue.closed_at&.to_s(:csv) }, + 'Milestone' => -> (issue) { issue.milestone&.title }, + 'Weight' => -> (issue) { issue.weight }, + 'Labels' => -> (issue) { issue_labels(issue) }, + 'Time Estimate' => ->(issue) { issue.time_estimate.to_s(:csv) }, + 'Time Spent' => -> (issue) { issue_time_spent(issue) } + } + end + + def issue_labels(issue) + @labels[issue.id].sort.join(',').presence + end + + # rubocop: disable CodeReuse/ActiveRecord + def issue_time_spent(issue) + issue.timelogs.map(&:time_spent).sum + end + # rubocop: enable CodeReuse/ActiveRecord + end +end + +Issues::ExportCsvService.prepend_if_ee('EE::Issues::ExportCsvService') diff --git a/app/services/jira_import/start_import_service.rb b/app/services/jira_import/start_import_service.rb index e8d9e6734bd..de4e490281f 100644 --- a/app/services/jira_import/start_import_service.rb +++ b/app/services/jira_import/start_import_service.rb @@ -62,12 +62,12 @@ module JiraImport end def validate - return build_error_response(_('Jira import feature is disabled.')) unless project.jira_issues_import_feature_flag_enabled? - return build_error_response(_('You do not have permissions to run the import.')) unless user.can?(:admin_project, project) - return build_error_response(_('Cannot import because issues are not available in this project.')) unless project.feature_available?(:issues, user) - return build_error_response(_('Jira integration not configured.')) unless project.jira_service&.active? + project.validate_jira_import_settings!(user: user) + return build_error_response(_('Unable to find Jira project to import data from.')) if jira_project_key.blank? return build_error_response(_('Jira import is already running.')) if import_in_progress? + rescue Projects::ImportService::Error => e + build_error_response(e.message) end def build_error_response(message) diff --git a/app/services/merge_requests/merge_orchestration_service.rb b/app/services/merge_requests/merge_orchestration_service.rb new file mode 100644 index 00000000000..24341ef1145 --- /dev/null +++ b/app/services/merge_requests/merge_orchestration_service.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +module MergeRequests + class MergeOrchestrationService < ::BaseService + def execute(merge_request) + return unless can_merge?(merge_request) + + merge_request.update(merge_error: nil) + + if can_merge_automatically?(merge_request) + auto_merge_service.execute(merge_request) + else + merge_request.merge_async(current_user.id, params) + end + end + + def can_merge?(merge_request) + can_merge_automatically?(merge_request) || can_merge_immediately?(merge_request) + end + + def preferred_auto_merge_strategy(merge_request) + auto_merge_service.preferred_strategy(merge_request) + end + + private + + def can_merge_immediately?(merge_request) + merge_request.can_be_merged_by?(current_user) && + merge_request.mergeable_state? + end + + def can_merge_automatically?(merge_request) + auto_merge_service.available_strategies(merge_request).any? + end + + def auto_merge_service + @auto_merge_service ||= AutoMergeService.new(project, current_user, params) + end + end +end diff --git a/app/services/merge_requests/pushed_branches_service.rb b/app/services/merge_requests/pushed_branches_service.rb new file mode 100644 index 00000000000..afcf0f7678a --- /dev/null +++ b/app/services/merge_requests/pushed_branches_service.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +module MergeRequests + class PushedBranchesService < MergeRequests::BaseService + include ::Gitlab::Utils::StrongMemoize + + # Skip moving this logic into models since it's too specific + # rubocop: disable CodeReuse/ActiveRecord + def execute + return [] if branch_names.blank? + + source_branches = project.source_of_merge_requests.opened + .from_source_branches(branch_names).pluck(:source_branch) + + target_branches = project.merge_requests.opened + .by_target_branch(branch_names).distinct.pluck(:target_branch) + + source_branches.concat(target_branches).to_set + end + # rubocop: enable CodeReuse/ActiveRecord + + private + + def branch_names + strong_memoize(:branch_names) do + params[:changes].map do |change| + Gitlab::Git.branch_name(change[:ref]) + end.compact + end + end + end +end diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb index 1516e33a7c6..2d33e87bf4b 100644 --- a/app/services/merge_requests/update_service.rb +++ b/app/services/merge_requests/update_service.rb @@ -79,14 +79,21 @@ module MergeRequests def merge_from_quick_action(merge_request) last_diff_sha = params.delete(:merge) - return unless merge_request.mergeable_with_quick_action?(current_user, last_diff_sha: last_diff_sha) - merge_request.update(merge_error: nil) - - if merge_request.head_pipeline_active? - AutoMergeService.new(project, current_user, { sha: last_diff_sha }).execute(merge_request, AutoMergeService::STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS) + if Feature.enabled?(:merge_orchestration_service, merge_request.project, default_enabled: true) + MergeRequests::MergeOrchestrationService + .new(project, current_user, { sha: last_diff_sha }) + .execute(merge_request) else - merge_request.merge_async(current_user.id, { sha: last_diff_sha }) + return unless merge_request.mergeable_with_quick_action?(current_user, last_diff_sha: last_diff_sha) + + merge_request.update(merge_error: nil) + + if merge_request.head_pipeline_active? + AutoMergeService.new(project, current_user, { sha: last_diff_sha }).execute(merge_request, AutoMergeService::STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS) + else + merge_request.merge_async(current_user.id, { sha: last_diff_sha }) + end end end diff --git a/app/services/metrics/dashboard/transient_embed_service.rb b/app/services/metrics/dashboard/transient_embed_service.rb index 035707dceb9..ce81f337e47 100644 --- a/app/services/metrics/dashboard/transient_embed_service.rb +++ b/app/services/metrics/dashboard/transient_embed_service.rb @@ -30,6 +30,11 @@ module Metrics def sequence [STAGES::EndpointInserter] end + + override :identifiers + def identifiers + Digest::SHA256.hexdigest(params[:embed_json]) + end end end end diff --git a/app/services/personal_access_tokens/create_service.rb b/app/services/personal_access_tokens/create_service.rb new file mode 100644 index 00000000000..ff9bb7d6802 --- /dev/null +++ b/app/services/personal_access_tokens/create_service.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +module PersonalAccessTokens + class CreateService < BaseService + def initialize(current_user, params = {}) + @current_user = current_user + @params = params.dup + end + + def execute + personal_access_token = current_user.personal_access_tokens.create(params.slice(*allowed_params)) + + if personal_access_token.persisted? + ServiceResponse.success(payload: { personal_access_token: personal_access_token }) + else + ServiceResponse.error(message: personal_access_token.errors.full_messages.to_sentence) + end + end + + private + + def allowed_params + [ + :name, + :impersonation, + :scopes, + :expires_at + ] + end + end +end diff --git a/app/services/pod_logs/base_service.rb b/app/services/pod_logs/base_service.rb index 8cc8fb913a2..2451ab8e0ce 100644 --- a/app/services/pod_logs/base_service.rb +++ b/app/services/pod_logs/base_service.rb @@ -62,13 +62,11 @@ module PodLogs end def get_raw_pods(result) - result[:raw_pods] = cluster.kubeclient.get_pods(namespace: namespace) - - success(result) + raise NotImplementedError end def get_pod_names(result) - result[:pods] = result[:raw_pods].map(&:metadata).map(&:name) + result[:pods] = result[:raw_pods].map { |p| p[:name] } success(result) end diff --git a/app/services/pod_logs/elasticsearch_service.rb b/app/services/pod_logs/elasticsearch_service.rb index 0a5185999ab..aac0fa424ca 100644 --- a/app/services/pod_logs/elasticsearch_service.rb +++ b/app/services/pod_logs/elasticsearch_service.rb @@ -23,6 +23,23 @@ module PodLogs super + %i(cursor) end + def get_raw_pods(result) + client = cluster&.application_elastic_stack&.elasticsearch_client + return error(_('Unable to connect to Elasticsearch')) unless client + + result[:raw_pods] = ::Gitlab::Elasticsearch::Logs::Pods.new(client).pods(namespace) + + success(result) + rescue Elasticsearch::Transport::Transport::ServerError => e + ::Gitlab::ErrorTracking.track_exception(e) + + error(_('Elasticsearch returned status code: %{status_code}') % { + # ServerError is the parent class of exceptions named after HTTP status codes, eg: "Elasticsearch::Transport::Transport::Errors::NotFound" + # there is no method on the exception other than the class name to determine the type of error encountered. + status_code: e.class.name.split('::').last + }) + end + def check_times(result) result[:start_time] = params['start_time'] if params.key?('start_time') && Time.iso8601(params['start_time']) result[:end_time] = params['end_time'] if params.key?('end_time') && Time.iso8601(params['end_time']) @@ -48,7 +65,7 @@ module PodLogs client = cluster&.application_elastic_stack&.elasticsearch_client return error(_('Unable to connect to Elasticsearch')) unless client - response = ::Gitlab::Elasticsearch::Logs.new(client).pod_logs( + response = ::Gitlab::Elasticsearch::Logs::Lines.new(client).pod_logs( namespace, pod_name: result[:pod_name], container_name: result[:container_name], @@ -69,7 +86,7 @@ module PodLogs # there is no method on the exception other than the class name to determine the type of error encountered. status_code: e.class.name.split('::').last }) - rescue ::Gitlab::Elasticsearch::Logs::InvalidCursor + rescue ::Gitlab::Elasticsearch::Logs::Lines::InvalidCursor error(_('Invalid cursor value provided')) end end diff --git a/app/services/pod_logs/kubernetes_service.rb b/app/services/pod_logs/kubernetes_service.rb index 31e26912c73..0a8072a9037 100644 --- a/app/services/pod_logs/kubernetes_service.rb +++ b/app/services/pod_logs/kubernetes_service.rb @@ -21,6 +21,17 @@ module PodLogs private + def get_raw_pods(result) + result[:raw_pods] = cluster.kubeclient.get_pods(namespace: namespace).map do |pod| + { + name: pod.metadata.name, + container_names: pod.spec.containers.map(&:name) + } + end + + success(result) + end + def check_pod_name(result) # If pod_name is not received as parameter, get the pod logs of the first # pod of this namespace. @@ -43,11 +54,11 @@ module PodLogs end def check_container_name(result) - pod_details = result[:raw_pods].find { |p| p.metadata.name == result[:pod_name] } - containers = pod_details.spec.containers.map(&:name) + pod_details = result[:raw_pods].find { |p| p[:name] == result[:pod_name] } + container_names = pod_details[:container_names] # select first container if not specified - result[:container_name] ||= containers.first + result[:container_name] ||= container_names.first unless result[:container_name] return error(_('No containers available')) @@ -58,7 +69,7 @@ module PodLogs ' %{max_length} chars' % { max_length: K8S_NAME_MAX_LENGTH })) end - unless containers.include?(result[:container_name]) + unless container_names.include?(result[:container_name]) return error(_('Container does not exist')) end diff --git a/app/services/projects/deploy_tokens/create_service.rb b/app/services/projects/deploy_tokens/create_service.rb index 2e71650b066..592198ef241 100644 --- a/app/services/projects/deploy_tokens/create_service.rb +++ b/app/services/projects/deploy_tokens/create_service.rb @@ -8,11 +8,7 @@ module Projects def execute deploy_token = create_deploy_token_for(@project, params) - if deploy_token.persisted? - success(deploy_token: deploy_token, http_status: :created) - else - error(deploy_token.errors.full_messages.to_sentence, :bad_request) - end + create_deploy_token_payload_for(deploy_token) end end end diff --git a/app/services/resources/create_access_token_service.rb b/app/services/resources/create_access_token_service.rb new file mode 100644 index 00000000000..fd3c8d78e58 --- /dev/null +++ b/app/services/resources/create_access_token_service.rb @@ -0,0 +1,111 @@ +# frozen_string_literal: true + +module Resources + class CreateAccessTokenService < BaseService + attr_accessor :resource_type, :resource + + def initialize(resource_type, resource, user, params = {}) + @resource_type = resource_type + @resource = resource + @current_user = user + @params = params.dup + end + + def execute + return unless feature_enabled? + return error("User does not have permission to create #{resource_type} Access Token") unless has_permission_to_create? + + # We skip authorization by default, since the user creating the bot is not an admin + # and project/group bot users are not created via sign-up + user = create_user + + return error(user.errors.full_messages.to_sentence) unless user.persisted? + return error("Failed to provide maintainer access") unless provision_access(resource, user) + + token_response = create_personal_access_token(user) + + if token_response.success? + success(token_response.payload[:personal_access_token]) + else + error(token_response.message) + end + end + + private + + def feature_enabled? + ::Feature.enabled?(:resource_access_token, resource) + end + + def has_permission_to_create? + case resource_type + when 'project' + can?(current_user, :admin_project, resource) + when 'group' + can?(current_user, :admin_group, resource) + else + false + end + end + + def create_user + Users::CreateService.new(current_user, default_user_params).execute(skip_authorization: true) + end + + def default_user_params + { + name: params[:name] || "#{resource.name.to_s.humanize} bot", + email: generate_email, + username: generate_username, + user_type: "#{resource_type}_bot".to_sym + } + end + + def generate_username + base_username = "#{resource_type}_#{resource.id}_bot" + + uniquify.string(base_username) { |s| User.find_by_username(s) } + end + + def generate_email + email_pattern = "#{resource_type}#{resource.id}_bot%s@example.com" + + uniquify.string(-> (n) { Kernel.sprintf(email_pattern, n) }) do |s| + User.find_by_email(s) + end + end + + def uniquify + Uniquify.new + end + + def create_personal_access_token(user) + PersonalAccessTokens::CreateService.new(user, personal_access_token_params).execute + end + + def personal_access_token_params + { + name: "#{resource_type}_bot", + impersonation: false, + scopes: params[:scopes] || default_scopes, + expires_at: params[:expires_at] || nil + } + end + + def default_scopes + Gitlab::Auth::API_SCOPES + Gitlab::Auth::REPOSITORY_SCOPES + Gitlab::Auth.registry_scopes - [:read_user] + end + + def provision_access(resource, user) + resource.add_maintainer(user) + end + + def error(message) + ServiceResponse.error(message: message) + end + + def success(access_token) + ServiceResponse.success(payload: { access_token: access_token }) + end + end +end diff --git a/app/services/snippets/create_service.rb b/app/services/snippets/create_service.rb index 0b74bd77e28..155013db344 100644 --- a/app/services/snippets/create_service.rb +++ b/app/services/snippets/create_service.rb @@ -38,9 +38,7 @@ module Snippets private def save_and_commit - snippet_saved = @snippet.with_transaction_returning_status do - @snippet.save && @snippet.store_mentions! - end + snippet_saved = @snippet.save if snippet_saved && Feature.enabled?(:version_snippets, current_user) create_repository diff --git a/app/services/terraform/remote_state_handler.rb b/app/services/terraform/remote_state_handler.rb new file mode 100644 index 00000000000..5bb6f6a1dee --- /dev/null +++ b/app/services/terraform/remote_state_handler.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +module Terraform + class RemoteStateHandler < BaseService + include Gitlab::OptimisticLocking + + StateLockedError = Class.new(StandardError) + + # rubocop: disable CodeReuse/ActiveRecord + def find_with_lock + raise ArgumentError unless params[:name].present? + + state = Terraform::State.find_by(project: project, name: params[:name]) + raise ActiveRecord::RecordNotFound.new("Couldn't find state") unless state + + retry_optimistic_lock(state) { |state| yield state } if state && block_given? + state + end + # rubocop: enable CodeReuse/ActiveRecord + + def create_or_find! + raise ArgumentError unless params[:name].present? + + Terraform::State.create_or_find_by(project: project, name: params[:name]) + end + + def handle_with_lock + retrieve_with_lock do |state| + raise StateLockedError unless lock_matches?(state) + + yield state if block_given? + + state.save! unless state.destroyed? + end + end + + def lock! + raise ArgumentError if params[:lock_id].blank? + + retrieve_with_lock do |state| + raise StateLockedError if state.locked? + + state.lock_xid = params[:lock_id] + state.locked_by_user = current_user + state.locked_at = Time.now + + state.save! + end + end + + def unlock! + retrieve_with_lock do |state| + # force-unlock does not pass ID, so we ignore it if it is missing + raise StateLockedError unless params[:lock_id].nil? || lock_matches?(state) + + state.lock_xid = nil + state.locked_by_user = nil + state.locked_at = nil + + state.save! + end + end + + private + + def retrieve_with_lock + create_or_find!.tap { |state| retry_optimistic_lock(state) { |state| yield state } } + end + + def lock_matches?(state) + return true if state.lock_xid.nil? && params[:lock_id].nil? + + ActiveSupport::SecurityUtils + .secure_compare(state.lock_xid.to_s, params[:lock_id].to_s) + end + end +end diff --git a/app/services/users/build_service.rb b/app/services/users/build_service.rb index 6f9f307c322..3938d675596 100644 --- a/app/services/users/build_service.rb +++ b/app/services/users/build_service.rb @@ -81,7 +81,8 @@ module Users :private_profile, :organization, :location, - :public_email + :public_email, + :user_type ] end @@ -95,7 +96,8 @@ module Users :first_name, :last_name, :password, - :username + :username, + :user_type ] end @@ -127,6 +129,8 @@ module Users user_params[:external] = user_external? end + user_params.delete(:user_type) unless project_bot?(user_params[:user_type]) + user_params end @@ -137,6 +141,10 @@ module Users def user_external? user_default_internal_regex_instance.match(params[:email]).nil? end + + def project_bot?(user_type) + user_type&.to_sym == :project_bot + end end end |