diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-07-19 17:16:28 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-07-19 17:16:28 +0300 |
commit | e4384360a16dd9a19d4d2d25d0ef1f2b862ed2a6 (patch) | |
tree | 2fcdfa7dcdb9db8f5208b2562f4b4e803d671243 /app/services | |
parent | ffda4e7bcac36987f936b4ba515995a6698698f0 (diff) |
Add latest changes from gitlab-org/gitlab@16-2-stable-eev16.2.0-rc42
Diffstat (limited to 'app/services')
90 files changed, 1027 insertions, 561 deletions
diff --git a/app/services/admin/plan_limits/update_service.rb b/app/services/admin/plan_limits/update_service.rb index a71d1f14112..cda9a7e7f8c 100644 --- a/app/services/admin/plan_limits/update_service.rb +++ b/app/services/admin/plan_limits/update_service.rb @@ -7,26 +7,34 @@ module Admin @current_user = current_user @params = params @plan = plan + @plan_limits = plan.actual_limits end def execute return error(_('Access denied'), :forbidden) unless can_update? - if plan.actual_limits.update(parsed_params) + add_history_to_params! + + if plan_limits.update(parsed_params) success else - error(plan.actual_limits.errors.full_messages, :bad_request) + error(plan_limits.errors.full_messages, :bad_request) end end private - attr_accessor :current_user, :params, :plan + attr_accessor :current_user, :params, :plan, :plan_limits def can_update? current_user.can_admin_all_resources? end + def add_history_to_params! + formatted_limits_history = plan_limits.format_limits_history(current_user, parsed_params) + parsed_params.merge!(limits_history: formatted_limits_history) unless formatted_limits_history.empty? + end + # Overridden in EE def parsed_params params diff --git a/app/services/application_settings/update_service.rb b/app/services/application_settings/update_service.rb index 7728982779e..6d484c4fa22 100644 --- a/app/services/application_settings/update_service.rb +++ b/app/services/application_settings/update_service.rb @@ -26,6 +26,7 @@ module ApplicationSettings end update_terms(@params.delete(:terms)) + update_default_branch_protection_defaults(@params[:default_branch_protection]) add_to_outbound_local_requests_whitelist(@params.delete(:add_to_outbound_local_requests_whitelist)) @@ -77,6 +78,19 @@ module ApplicationSettings @application_setting.reset_memoized_terms end + def update_default_branch_protection_defaults(default_branch_protection) + return unless default_branch_protection.present? + + # We are migrating default_branch_protection from an integer + # column to a jsonb column. While completing the rest of the + # work, we want to start translating the updates sent to the + # existing column into the json. Eventually, we will be updating + # the jsonb column directly and deprecating the original update + # path. Until then, we want to sync up both columns. + protection = Gitlab::Access::BranchProtection.new(default_branch_protection.to_i) + @application_setting.default_branch_protection_defaults = protection.to_hash + end + def process_performance_bar_allowed_group_id group_full_path = params.delete(:performance_bar_allowed_group_path) enable_param_on = Gitlab::Utils.to_boolean(params.delete(:performance_bar_enabled)) 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 2bbb8f925a4..cb8e531f0e1 100644 --- a/app/services/auto_merge/merge_when_pipeline_succeeds_service.rb +++ b/app/services/auto_merge/merge_when_pipeline_succeeds_service.rb @@ -4,9 +4,7 @@ module AutoMerge class MergeWhenPipelineSucceedsService < AutoMerge::BaseService def execute(merge_request) super do - if merge_request.saved_change_to_auto_merge_enabled? - SystemNoteService.merge_when_pipeline_succeeds(merge_request, project, current_user, merge_request.actual_head_pipeline.sha) - end + add_system_note(merge_request) end end @@ -36,12 +34,20 @@ module AutoMerge def available_for?(merge_request) super do - merge_request.actual_head_pipeline&.active? + check_availability(merge_request) end end private + def add_system_note(merge_request) + SystemNoteService.merge_when_pipeline_succeeds(merge_request, project, current_user, merge_request.actual_head_pipeline.sha) if merge_request.saved_change_to_auto_merge_enabled? + end + + def check_availability(merge_request) + merge_request.actual_head_pipeline&.active? + end + def notify(merge_request) notification_service.async.merge_when_pipeline_succeeds(merge_request, current_user) if merge_request.saved_change_to_auto_merge_enabled? end diff --git a/app/services/award_emojis/add_service.rb b/app/services/award_emojis/add_service.rb index f45a4330c09..065ef9dc708 100644 --- a/app/services/award_emojis/add_service.rb +++ b/app/services/award_emojis/add_service.rb @@ -27,6 +27,8 @@ module AwardEmojis def after_create(award) TodoService.new.new_award_emoji(todoable, current_user) if todoable + + execute_hooks(award, 'award') end def todoable diff --git a/app/services/award_emojis/base_service.rb b/app/services/award_emojis/base_service.rb index 626e26d63b5..274c528acf2 100644 --- a/app/services/award_emojis/base_service.rb +++ b/app/services/award_emojis/base_service.rb @@ -11,6 +11,13 @@ module AwardEmojis super(awardable.project, current_user) end + def execute_hooks(award_emoji, action) + return unless awardable.project&.has_active_hooks?(:emoji_hooks) + + hook_data = Gitlab::DataBuilder::Emoji.build(award_emoji, current_user, action) + awardable.project.execute_hooks(hook_data, :emoji_hooks) + end + private def normalize_name(name) diff --git a/app/services/award_emojis/destroy_service.rb b/app/services/award_emojis/destroy_service.rb index 47dc8418e07..b7146d69bf0 100644 --- a/app/services/award_emojis/destroy_service.rb +++ b/app/services/award_emojis/destroy_service.rb @@ -22,6 +22,7 @@ module AwardEmojis private def after_destroy(award) + execute_hooks(award, 'revoke') end end end diff --git a/app/services/boards/base_items_list_service.rb b/app/services/boards/base_items_list_service.rb index bf68aee2c1f..a4b1be1e599 100644 --- a/app/services/boards/base_items_list_service.rb +++ b/app/services/boards/base_items_list_service.rb @@ -13,14 +13,17 @@ module Boards # rubocop: disable CodeReuse/ActiveRecord def metadata(required_fields = [:issue_count, :total_issue_weight]) - fields = metadata_fields(required_fields) - keys = fields.keys - # TODO: eliminate need for SQL literal fragment - columns = Arel.sql(fields.values_at(*keys).join(', ')) - results = item_model.where(id: collection_ids) - results = results.select(columns) - - Hash[keys.zip(results.pluck(columns).flatten)] + # Failing tests in spec/requests/api/graphql/boards/board_lists_query_spec.rb + ::Gitlab::Database.allow_cross_joins_across_databases(url: "https://gitlab.com/gitlab-org/gitlab/-/issues/417465") do + fields = metadata_fields(required_fields) + keys = fields.keys + # TODO: eliminate need for SQL literal fragment + columns = Arel.sql(fields.values_at(*keys).join(', ')) + results = item_model.where(id: collection_ids) + results = results.select(columns) + + Hash[keys.zip(results.pluck(columns).flatten)] + end end # rubocop: enable CodeReuse/ActiveRecord diff --git a/app/services/bulk_imports/create_service.rb b/app/services/bulk_imports/create_service.rb index 636c636255f..7fc3511a253 100644 --- a/app/services/bulk_imports/create_service.rb +++ b/app/services/bulk_imports/create_service.rb @@ -105,7 +105,7 @@ module BulkImports def validate_setting_enabled! source_full_path, source_type = Array.wrap(params)[0].values_at(:source_full_path, :source_type) entity_type = ENTITY_TYPES_MAPPING.fetch(source_type) - if source_full_path =~ /^[0-9]+$/ + if /^[0-9]+$/.match?(source_full_path) query = query_type(entity_type) response = graphql_client.execute( graphql_client.parse(query.to_s), @@ -154,7 +154,7 @@ module BulkImports end def validate_destination_slug(destination_slug) - return if destination_slug =~ Gitlab::Regex.oci_repository_path_regex + return if Gitlab::Regex.oci_repository_path_regex.match?(destination_slug) raise BulkImports::Error.destination_slug_validation_failure end diff --git a/app/services/bulk_imports/relation_export_service.rb b/app/services/bulk_imports/relation_export_service.rb index 142bc48efe3..ed71c09420b 100644 --- a/app/services/bulk_imports/relation_export_service.rb +++ b/app/services/bulk_imports/relation_export_service.rb @@ -16,7 +16,7 @@ module BulkImports def execute find_or_create_export! do |export| - remove_existing_export_file!(export) + export.remove_existing_upload! export_service.execute compress_exported_relation upload_compressed_file(export) @@ -45,15 +45,6 @@ module BulkImports fail_export!(export, e) end - def remove_existing_export_file!(export) - upload = export.upload - - return unless upload&.export_file&.file - - upload.remove_export_file! - upload.save! - end - def export_service @export_service ||= if config.tree_relation?(relation) || config.self_relation?(relation) TreeExportService.new(portable, export_path, relation, user) diff --git a/app/services/ci/create_pipeline_schedule_service.rb b/app/services/ci/create_pipeline_schedule_service.rb index 0d5f50c26a1..4fdd65bcdb4 100644 --- a/app/services/ci/create_pipeline_schedule_service.rb +++ b/app/services/ci/create_pipeline_schedule_service.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true module Ci + # This class is deprecated and will be removed with the FF ci_refactoring_pipeline_schedule_create_service class CreatePipelineScheduleService < BaseService def execute project.pipeline_schedules.create(pipeline_schedule_params) diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb index a8da83e84a1..fe0e842f542 100644 --- a/app/services/ci/create_pipeline_service.rb +++ b/app/services/ci/create_pipeline_service.rb @@ -40,22 +40,22 @@ module Ci # Create a new pipeline in the specified project. # - # @param [Symbol] source What event (Ci::Pipeline.sources) triggers the pipeline - # creation. - # @param [Boolean] ignore_skip_ci Whether skipping a pipeline creation when `[skip ci]` comment - # is present in the commit body - # @param [Boolean] save_on_errors Whether persisting an invalid pipeline when it encounters an - # error during creation (e.g. invalid yaml) - # @param [Ci::TriggerRequest] trigger_request The pipeline trigger triggers the pipeline creation. - # @param [Ci::PipelineSchedule] schedule The pipeline schedule triggers the pipeline creation. - # @param [MergeRequest] merge_request The merge request triggers the pipeline creation. - # @param [ExternalPullRequest] external_pull_request The external pull request triggers the pipeline creation. - # @param [Ci::Bridge] bridge The bridge job that triggers the downstream pipeline creation. - # @param [String] content The content of .gitlab-ci.yml to override the default config - # contents (e.g. .gitlab-ci.yml in repostiry). Mainly used for - # generating a dangling pipeline. + # @param [Symbol] source What event (Ci::Pipeline.sources) triggers the pipeline + # creation. + # @param [Boolean] ignore_skip_ci Whether skipping a pipeline creation when `[skip ci]` comment + # is present in the commit body + # @param [Boolean] save_on_errors Whether persisting an invalid pipeline when it encounters an + # error during creation (e.g. invalid yaml) + # @param [Ci::TriggerRequest] trigger_request The pipeline trigger triggers the pipeline creation. + # @param [Ci::PipelineSchedule] schedule The pipeline schedule triggers the pipeline creation. + # @param [MergeRequest] merge_request The merge request triggers the pipeline creation. + # @param [Ci::ExternalPullRequest] external_pull_request The external pull request triggers the pipeline creation. + # @param [Ci::Bridge] bridge The bridge job that triggers the downstream pipeline creation. + # @param [String] content The content of .gitlab-ci.yml to override the default config + # contents (e.g. .gitlab-ci.yml in repostiry). Mainly used for + # generating a dangling pipeline. # - # @return [Ci::Pipeline] The created Ci::Pipeline object. + # @return [Ci::Pipeline] The created Ci::Pipeline object. # rubocop: disable Metrics/ParameterLists def execute(source, ignore_skip_ci: false, save_on_errors: true, trigger_request: nil, schedule: nil, merge_request: nil, external_pull_request: nil, bridge: nil, **options, &block) @logger = build_logger diff --git a/app/services/ci/destroy_pipeline_service.rb b/app/services/ci/destroy_pipeline_service.rb index bdec13f98a7..a9d2e17657e 100644 --- a/app/services/ci/destroy_pipeline_service.rb +++ b/app/services/ci/destroy_pipeline_service.rb @@ -7,7 +7,7 @@ module Ci Ci::ExpirePipelineCacheService.new.execute(pipeline, delete: true) - # ensure cancellation happens sync so we accumulate compute credits successfully + # ensure cancellation happens sync so we accumulate compute minutes successfully # before deleting the pipeline. ::Ci::CancelPipelineService.new( pipeline: pipeline, diff --git a/app/services/ci/pipeline_processing/atomic_processing_service.rb b/app/services/ci/pipeline_processing/atomic_processing_service.rb index c0ffbb401f6..8211507fb95 100644 --- a/app/services/ci/pipeline_processing/atomic_processing_service.rb +++ b/app/services/ci/pipeline_processing/atomic_processing_service.rb @@ -23,14 +23,12 @@ module Ci success = try_obtain_lease { process! } if success - if ::Feature.enabled?(:ci_reset_skipped_jobs_in_atomic_processing, project) - # If any jobs changed from stopped to alive status during pipeline processing, we must - # re-reset their dependent jobs; see https://gitlab.com/gitlab-org/gitlab/-/issues/388539. - new_alive_jobs.group_by(&:user).each do |user, jobs| - log_running_reset_skipped_jobs_service(jobs) - - ResetSkippedJobsService.new(project, user).execute(jobs) - end + # If any jobs changed from stopped to alive status during pipeline processing, we must + # re-reset their dependent jobs; see https://gitlab.com/gitlab-org/gitlab/-/issues/388539. + new_alive_jobs.group_by(&:user).each do |user, jobs| + log_running_reset_skipped_jobs_service(jobs) + + ResetSkippedJobsService.new(project, user).execute(jobs) end # Re-schedule if we need further processing diff --git a/app/services/ci/pipeline_schedules/create_service.rb b/app/services/ci/pipeline_schedules/create_service.rb new file mode 100644 index 00000000000..c1825865bc0 --- /dev/null +++ b/app/services/ci/pipeline_schedules/create_service.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +module Ci + module PipelineSchedules + class CreateService + def initialize(project, user, params) + @project = project + @user = user + @params = params + + @schedule = project.pipeline_schedules.new + end + + def execute + return forbidden unless allowed? + + schedule.assign_attributes(params.merge(owner: user)) + + if schedule.save + ServiceResponse.success(payload: schedule) + else + ServiceResponse.error(payload: schedule, message: schedule.errors.full_messages) + end + end + + private + + attr_reader :project, :user, :params, :schedule + + def allowed? + user.can?(:create_pipeline_schedule, schedule) + end + + def forbidden + # We add the error to the base object too + # because model errors are used in the API responses and the `form_errors` helper. + schedule.errors.add(:base, forbidden_message) + + ServiceResponse.error(payload: schedule, message: [forbidden_message], reason: :forbidden) + end + + def forbidden_message + _('The current user is not authorized to create the pipeline schedule') + end + end + end +end diff --git a/app/services/ci/pipeline_schedules/update_service.rb b/app/services/ci/pipeline_schedules/update_service.rb index 2412b5cbd81..28c22e0a868 100644 --- a/app/services/ci/pipeline_schedules/update_service.rb +++ b/app/services/ci/pipeline_schedules/update_service.rb @@ -12,7 +12,9 @@ module Ci def execute return forbidden unless allowed? - if schedule.update(@params) + schedule.assign_attributes(params) + + if schedule.save ServiceResponse.success(payload: schedule) else ServiceResponse.error(message: schedule.errors.full_messages) @@ -21,17 +23,22 @@ module Ci private - attr_reader :schedule, :user + attr_reader :schedule, :user, :params def allowed? user.can?(:update_pipeline_schedule, schedule) end def forbidden - ServiceResponse.error( - message: _('The current user is not authorized to update the pipeline schedule'), - reason: :forbidden - ) + # We add the error to the base object too + # because model errors are used in the API responses and the `form_errors` helper. + schedule.errors.add(:base, forbidden_message) + + ServiceResponse.error(message: [forbidden_message], reason: :forbidden) + end + + def forbidden_message + _('The current user is not authorized to update the pipeline schedule') end end end diff --git a/app/services/clusters/agent_tokens/create_service.rb b/app/services/clusters/agent_tokens/create_service.rb index efa9716d2c8..136afd108e7 100644 --- a/app/services/clusters/agent_tokens/create_service.rb +++ b/app/services/clusters/agent_tokens/create_service.rb @@ -40,8 +40,6 @@ module Clusters end def active_tokens_limit_reached? - return false unless Feature.enabled?(:cluster_agents_limit_tokens_created) - ::Clusters::AgentTokensFinder.new(agent, current_user, status: :active).execute.count >= ACTIVE_TOKENS_LIMIT end diff --git a/app/services/clusters/agents/authorize_proxy_user_service.rb b/app/services/clusters/agents/authorize_proxy_user_service.rb index fbcf25153c1..abf451ed350 100644 --- a/app/services/clusters/agents/authorize_proxy_user_service.rb +++ b/app/services/clusters/agents/authorize_proxy_user_service.rb @@ -11,17 +11,14 @@ module Clusters end def execute - return forbidden unless user_access_config.present? + return forbidden('`user_access` keyword is not found in agent config file.') unless user_access_config.present? access_as = user_access_config['access_as'] - return forbidden unless access_as.present? - return forbidden if access_as.size != 1 - if payload = handle_access(access_as) - return success(payload: payload) - end + return forbidden('`access_as` is not found under the `user_access` keyword.') unless access_as.present? + return forbidden('`access_as` must exist only once under the `user_access` keyword.') if access_as.size != 1 - forbidden + handle_access(access_as) end private @@ -52,9 +49,11 @@ module Clusters end def access_as_agent - return if authorizations.empty? + if authorizations.empty? + return forbidden('You must be a member of `projects` or `groups` under the `user_access` keyword.') + end - response_base.merge(access_as: { agent: {} }) + success(payload: response_base.merge(access_as: { agent: {} })) end def user_access_config @@ -64,8 +63,8 @@ module Clusters delegate :success, to: ServiceResponse, private: true - def forbidden - ServiceResponse.error(reason: :forbidden, message: '403 Forbidden') + def forbidden(message) + ServiceResponse.error(reason: :forbidden, message: message) end end end diff --git a/app/services/clusters/cleanup/project_namespace_service.rb b/app/services/clusters/cleanup/project_namespace_service.rb index 80192aa14ab..f6ac06d0594 100644 --- a/app/services/clusters/cleanup/project_namespace_service.rb +++ b/app/services/clusters/cleanup/project_namespace_service.rb @@ -29,7 +29,7 @@ module Clusters rescue Kubeclient::HttpError => e # unauthorized, forbidden: GitLab's access has been revoked # certificate verify failed: Cluster is probably gone forever - raise unless e.message =~ /unauthorized|forbidden|certificate verify failed/i + raise unless /unauthorized|forbidden|certificate verify failed/i.match?(e.message) end kubernetes_namespace.destroy! diff --git a/app/services/clusters/cleanup/service_account_service.rb b/app/services/clusters/cleanup/service_account_service.rb index dce41d2a39c..0ce4bf9bb9c 100644 --- a/app/services/clusters/cleanup/service_account_service.rb +++ b/app/services/clusters/cleanup/service_account_service.rb @@ -27,7 +27,7 @@ module Clusters rescue Kubeclient::HttpError => e # unauthorized, forbidden: GitLab's access has been revoked # certificate verify failed: Cluster is probably gone forever - raise unless e.message =~ /unauthorized|forbidden|certificate verify failed/i + raise unless /unauthorized|forbidden|certificate verify failed/i.match?(e.message) end end end diff --git a/app/services/clusters/integrations/prometheus_health_check_service.rb b/app/services/clusters/integrations/prometheus_health_check_service.rb deleted file mode 100644 index cd06e59449c..00000000000 --- a/app/services/clusters/integrations/prometheus_health_check_service.rb +++ /dev/null @@ -1,101 +0,0 @@ -# frozen_string_literal: true - -module Clusters - module Integrations - class PrometheusHealthCheckService - include Gitlab::Utils::StrongMemoize - include Gitlab::Routing - - def initialize(cluster) - @cluster = cluster - @logger = Gitlab::AppJsonLogger.build - end - - def execute - raise 'Invalid cluster type. Only project types are allowed.' unless @cluster.project_type? - - return unless prometheus_integration.enabled - - project = @cluster.clusterable - - @logger.info( - message: 'Prometheus health check', - cluster_id: @cluster.id, - newly_unhealthy: became_unhealthy?, - currently_healthy: currently_healthy?, - was_healthy: was_healthy? - ) - - send_notification(project) if became_unhealthy? - - prometheus_integration.update_columns(health_status: current_health_status) if health_changed? - end - - private - - def prometheus_integration - strong_memoize(:prometheus_integration) do - @cluster.integration_prometheus - end - end - - def current_health_status - if currently_healthy? - :healthy - else - :unhealthy - end - end - - def currently_healthy? - strong_memoize(:currently_healthy) do - prometheus_integration.prometheus_client.healthy? - end - end - - def became_unhealthy? - strong_memoize(:became_unhealthy) do - (was_healthy? || was_unknown?) && !currently_healthy? - end - end - - def was_healthy? - strong_memoize(:was_healthy) do - prometheus_integration.healthy? - end - end - - def was_unknown? - strong_memoize(:was_unknown) do - prometheus_integration.unknown? - end - end - - def health_changed? - was_healthy? != currently_healthy? - end - - def send_notification(project) - notification_payload = build_notification_payload(project) - integration = project.alert_management_http_integrations.active.first - - Projects::Alerting::NotifyService.new(project, notification_payload).execute(integration&.token, integration) - - @logger.info(message: 'Successfully notified of Prometheus newly unhealthy', cluster_id: @cluster.id, project_id: project.id) - end - - def build_notification_payload(project) - cluster_path = namespace_project_cluster_path( - project_id: project.path, - namespace_id: project.namespace.path, - id: @cluster.id - ) - - { - title: "Prometheus is Unhealthy. Cluster Name: #{@cluster.name}", - description: "Prometheus is unhealthy for the cluster: [#{@cluster.name}](#{cluster_path}) attached to project #{project.name}." - } - end - end - end -end diff --git a/app/services/concerns/integrations/project_test_data.rb b/app/services/concerns/integrations/project_test_data.rb index b3427697052..fcef22a8cab 100644 --- a/app/services/concerns/integrations/project_test_data.rb +++ b/app/services/concerns/integrations/project_test_data.rb @@ -77,5 +77,20 @@ module Integrations release.to_hook_data('create') end + + def emoji_events_data + no_data_error(s_('TestHooks|Ensure the project has notes.')) unless project.notes.any? + + award_emoji = AwardEmoji.new( + id: 1, + name: 'thumbsup', + user: current_user, + awardable: project.notes.last, + created_at: Time.zone.now, + updated_at: Time.zone.now + ) + + Gitlab::DataBuilder::Emoji.build(award_emoji, current_user, 'award') + end end end diff --git a/app/services/concerns/projects/remove_refs.rb b/app/services/concerns/projects/remove_refs.rb new file mode 100644 index 00000000000..d133aa0ced6 --- /dev/null +++ b/app/services/concerns/projects/remove_refs.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module Projects + module RemoveRefs + extend ActiveSupport::Concern + include Gitlab::ExclusiveLeaseHelpers + + LOCK_RETRY = 3 + LOCK_TTL = 5.minutes + LOCK_SLEEP = 0.5.seconds + + def serialized_remove_refs(project_id, &blk) + in_lock("projects/#{project_id}/serialized_remove_refs", **lock_params, &blk) + end + + def lock_params + { + ttl: LOCK_TTL, + retries: LOCK_RETRY, + sleep_sec: LOCK_SLEEP + } + end + end +end diff --git a/app/services/draft_notes/create_service.rb b/app/services/draft_notes/create_service.rb index 5ff971b66c1..e5a070e9db7 100644 --- a/app/services/draft_notes/create_service.rb +++ b/app/services/draft_notes/create_service.rb @@ -25,7 +25,8 @@ module DraftNotes draft_note = DraftNote.new(params) draft_note.merge_request = merge_request draft_note.author = current_user - draft_note.save + + return draft_note unless draft_note.save if in_reply_to_discussion_id.blank? && draft_note.diff_file&.unfolded? merge_request.diffs.clear_cache diff --git a/app/services/draft_notes/publish_service.rb b/app/services/draft_notes/publish_service.rb index 9e1e381c568..a7a2ad63c1c 100644 --- a/app/services/draft_notes/publish_service.rb +++ b/app/services/draft_notes/publish_service.rb @@ -49,6 +49,7 @@ module DraftNotes notification_service.async.new_review(review) MergeRequests::ResolvedDiscussionNotificationService.new(project: project, current_user: current_user).execute(merge_request) GraphqlTriggers.merge_request_merge_status_updated(merge_request) + after_publish(review) end def create_note_from_draft(draft, skip_capture_diff_note_position: false, skip_keep_around_commits: false, skip_merge_status_trigger: false) @@ -108,5 +109,11 @@ module DraftNotes project.repository.keep_around(*shas) end end + + def after_publish(review) + # Overridden in EE + end end end + +DraftNotes::PublishService.prepend_mod diff --git a/app/services/environments/create_service.rb b/app/services/environments/create_service.rb index 760c8a6e306..fd78a886e29 100644 --- a/app/services/environments/create_service.rb +++ b/app/services/environments/create_service.rb @@ -2,7 +2,7 @@ module Environments class CreateService < BaseService - ALLOWED_ATTRIBUTES = %i[name external_url tier cluster_agent].freeze + ALLOWED_ATTRIBUTES = %i[name external_url tier cluster_agent kubernetes_namespace].freeze def execute unless can?(current_user, :create_environment, project) diff --git a/app/services/environments/update_service.rb b/app/services/environments/update_service.rb index 5eb4880ec4b..52f6198bada 100644 --- a/app/services/environments/update_service.rb +++ b/app/services/environments/update_service.rb @@ -2,7 +2,7 @@ module Environments class UpdateService < BaseService - ALLOWED_ATTRIBUTES = %i[external_url tier cluster_agent].freeze + ALLOWED_ATTRIBUTES = %i[external_url tier cluster_agent kubernetes_namespace].freeze def execute(environment) unless can?(current_user, :update_environment, environment) diff --git a/app/services/git/base_hooks_service.rb b/app/services/git/base_hooks_service.rb index acf54dec51b..f9280be7ee2 100644 --- a/app/services/git/base_hooks_service.rb +++ b/app/services/git/base_hooks_service.rb @@ -67,7 +67,10 @@ module Git # Creating push_data invokes one CommitDelta RPC per commit. Only # build this data if we actually need it. project.execute_hooks(push_data, hook_name) if project.has_active_hooks?(hook_name) - project.execute_integrations(push_data, hook_name) if project.has_active_integrations?(hook_name) + + return unless project.has_active_integrations?(hook_name) + + project.execute_integrations(push_data, hook_name, skip_ci: integration_push_options&.fetch(:skip_ci).present?) end def enqueue_invalidate_cache @@ -101,7 +104,19 @@ module Git def ci_variables_from_push_options strong_memoize(:ci_variables_from_push_options) do - params[:push_options]&.deep_symbolize_keys&.dig(:ci, :variable) + push_options&.dig(:ci, :variable) + end + end + + def integration_push_options + strong_memoize(:integration_push_options) do + push_options&.dig(:integrations) + end + end + + def push_options + strong_memoize(:push_options) do + params[:push_options]&.deep_symbolize_keys end end diff --git a/app/services/groups/participants_service.rb b/app/services/groups/participants_service.rb index 1de2b3c5a2e..e939d27d464 100644 --- a/app/services/groups/participants_service.rb +++ b/app/services/groups/participants_service.rb @@ -2,6 +2,7 @@ module Groups class ParticipantsService < Groups::BaseService + include Gitlab::Utils::StrongMemoize include Users::ParticipableService def execute(noteable) @@ -17,15 +18,20 @@ module Groups render_participants_as_hash(participants.uniq) end + private + def all_members - count = group_members.count - [{ username: "all", name: "All Group Members", count: count }] + return [] if group.nil? || Feature.enabled?(:disable_all_mention) + + [{ username: "all", name: "All Group Members", count: group.users_count }] end def group_members return [] unless group - @group_members ||= sorted(group.direct_and_indirect_users) + sorted( + group.direct_and_indirect_users(share_with_groups: group.member?(current_user)) + ) end end end diff --git a/app/services/groups/transfer_service.rb b/app/services/groups/transfer_service.rb index 16454360ee2..81d4dfddaab 100644 --- a/app/services/groups/transfer_service.rb +++ b/app/services/groups/transfer_service.rb @@ -197,6 +197,11 @@ module Groups return if @new_parent_group return unless @group.owners.empty? + add_owner_on_transferred_group + end + + # Overridden in EE + def add_owner_on_transferred_group @group.add_owner(current_user) end diff --git a/app/services/groups/update_service.rb b/app/services/groups/update_service.rb index 925a2acbb58..df6ede87ef9 100644 --- a/app/services/groups/update_service.rb +++ b/app/services/groups/update_service.rb @@ -117,6 +117,7 @@ module Groups def handle_settings_update settings_params = params.slice(*allowed_settings_params) + settings_params.merge!({ default_branch_protection: params[:default_branch_protection] }.compact) allowed_settings_params.each { |param| params.delete(param) } ::NamespaceSettings::UpdateService.new(current_user, group, settings_params).execute diff --git a/app/services/groups/update_shared_runners_service.rb b/app/services/groups/update_shared_runners_service.rb index c09dce0761f..08b43037c4c 100644 --- a/app/services/groups/update_shared_runners_service.rb +++ b/app/services/groups/update_shared_runners_service.rb @@ -25,7 +25,14 @@ module Groups end def update_shared_runners - group.update_shared_runners_setting!(params[:shared_runners_setting]) + case params[:shared_runners_setting] + when Namespace::SR_DISABLED_AND_UNOVERRIDABLE + set_shared_runners_enabled!(false) + when Namespace::SR_DISABLED_WITH_OVERRIDE, Namespace::SR_DISABLED_AND_OVERRIDABLE + disable_shared_runners_and_allow_override! + when Namespace::SR_ENABLED + set_shared_runners_enabled!(true) + end end def update_pending_builds? @@ -41,5 +48,38 @@ module Groups ::Ci::UpdatePendingBuildService.new(group, pending_builds_params).execute end end + + def set_shared_runners_enabled!(enabled) + group.update!( + shared_runners_enabled: enabled, + allow_descendants_override_disabled_shared_runners: false) + + group_ids = group.descendants + unless group_ids.empty? + Group.by_id(group_ids).update_all( + shared_runners_enabled: enabled, + allow_descendants_override_disabled_shared_runners: false) + end + + group.all_projects.update_all(shared_runners_enabled: enabled) + end + + def disable_shared_runners_and_allow_override! + # enabled -> disabled_and_overridable + if group.shared_runners_enabled? + group.update!( + shared_runners_enabled: false, + allow_descendants_override_disabled_shared_runners: true) + + group_ids = group.descendants + Group.by_id(group_ids).update_all(shared_runners_enabled: false) unless group_ids.empty? + + group.all_projects.update_all(shared_runners_enabled: false) + + # disabled_and_unoverridable -> disabled_and_overridable + else + group.update!(allow_descendants_override_disabled_shared_runners: true) + end + end end end diff --git a/app/services/import/github_service.rb b/app/services/import/github_service.rb index 7e7f7ea9810..df255a7ae24 100644 --- a/app/services/import/github_service.rb +++ b/app/services/import/github_service.rb @@ -16,7 +16,7 @@ module Import track_access_level('github') if project.persisted? - store_import_settings(project) + store_import_settings(project, access_params) success(project) elsif project.errors[:import_source_disabled].present? error(project.errors[:import_source_disabled], :forbidden) @@ -134,8 +134,13 @@ module Import error(translated_message, http_status) end - def store_import_settings(project) - Gitlab::GithubImport::Settings.new(project).write(params[:optional_stages]) + def store_import_settings(project, access_params) + Gitlab::GithubImport::Settings + .new(project) + .write( + optional_stages: params[:optional_stages], + additional_access_tokens: access_params[:additional_access_tokens] + ) end end end diff --git a/app/services/import_csv/base_service.rb b/app/services/import_csv/base_service.rb index cfaf3e831eb..9c1bad9e7da 100644 --- a/app/services/import_csv/base_service.rb +++ b/app/services/import_csv/base_service.rb @@ -8,7 +8,7 @@ module ImportCsv @user = user @project = project @csv_io = csv_io - @results = { success: 0, error_lines: [], parse_error: false } + @results = { success: 0, error_lines: [], parse_error: false, preprocess_errors: {} } end PreprocessError = Class.new(StandardError) diff --git a/app/services/import_csv/preprocess_milestones_service.rb b/app/services/import_csv/preprocess_milestones_service.rb new file mode 100644 index 00000000000..97fb381c58e --- /dev/null +++ b/app/services/import_csv/preprocess_milestones_service.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +module ImportCsv + class PreprocessMilestonesService < BaseService + def initialize(user, project, provided_titles) + @user = user + @project = project + @provided_titles = provided_titles + + @results = { success: 0, errors: nil } + @milestone_errors = { missing: { header: {}, titles: [] } } + end + + attr_reader :user, :project, :provided_titles, :results, :milestone_errors + + def execute + available_milestones = find_milestones_by_titles + return ServiceResponse.success if provided_titles.sort == available_milestones.sort + + milestone_errors[:missing][:header] = 'Milestone' + milestone_errors[:missing][:titles] = provided_titles.difference(available_milestones) || [] + ServiceResponse.error(message: "", payload: milestone_errors) + end + + def find_milestones_by_titles + # Find if these milestones exist in the project or its group and group ancestors + finder_params = { + project_ids: [project.id], + title: provided_titles + } + finder_params[:group_ids] = project.group.self_and_ancestors.select(:id) if project.group + MilestonesFinder.new(finder_params).execute.map(&:title).uniq + end + end +end diff --git a/app/services/integrations/group_mention_service.rb b/app/services/integrations/group_mention_service.rb new file mode 100644 index 00000000000..2389bf33432 --- /dev/null +++ b/app/services/integrations/group_mention_service.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +# GroupMentionService class +# +# Used for sending group mention notifications +# +# Ex. +# Integrations::GroupMentionService.new(mentionable, hook_data: data, is_confidential: true).execute +# +module Integrations + class GroupMentionService + def initialize(mentionable, hook_data:, is_confidential:) + @mentionable = mentionable + @hook_data = hook_data + @is_confidential = is_confidential + end + + def execute + return ServiceResponse.success if mentionable.nil? || hook_data.nil? + + @hook_data = hook_data.clone + # Fake a "group_mention" object kind so integrations can handle this as a separate class of event + hook_data[:object_attributes][:object_kind] = hook_data[:object_kind] + hook_data[:object_kind] = 'group_mention' + + if confidential? + hook_data[:event_type] = 'group_confidential_mention' + hook_scope = :group_confidential_mention_hooks + else + hook_data[:event_type] = 'group_mention' + hook_scope = :group_mention_hooks + end + + groups = mentionable.referenced_groups(mentionable.author) + groups.each do |group| + group_hook_data = hook_data.merge( + mentioned: { + object_kind: 'group', + name: group.full_path, + url: group.web_url + } + ) + group.execute_integrations(group_hook_data, hook_scope) + end + + ServiceResponse.success + end + + private + + attr_reader :mentionable, :hook_data, :is_confidential + + def confidential? + return is_confidential if is_confidential.present? + + mentionable.project.visibility_level != Gitlab::VisibilityLevel::PUBLIC + end + end +end diff --git a/app/services/integrations/test/project_service.rb b/app/services/integrations/test/project_service.rb index 31c8f02c7b6..48240f297fe 100644 --- a/app/services/integrations/test/project_service.rb +++ b/app/services/integrations/test/project_service.rb @@ -35,6 +35,8 @@ module Integrations deployment_events_data when 'release' releases_events_data + when 'award_emoji' + emoji_events_data end end end diff --git a/app/services/issuable/import_csv/base_service.rb b/app/services/issuable/import_csv/base_service.rb index 9ef9fb76e3c..95338374ca6 100644 --- a/app/services/issuable/import_csv/base_service.rb +++ b/app/services/issuable/import_csv/base_service.rb @@ -23,6 +23,24 @@ module Issuable raise CSV::MalformedCSVError.new('Invalid CSV format - missing required headers.', 1) end + + def preprocess! + preprocess_milestones! + + raise PreprocessError if results[:preprocess_errors].any? + end + + def preprocess_milestones! + # Pre-Process Milestone if header is present + return unless csv_data.lines.first.downcase.include?('milestone') + + provided_titles = with_csv_lines.filter_map { |row| row[:milestone]&.strip&.downcase }.uniq + result = ::ImportCsv::PreprocessMilestonesService.new(user, project, provided_titles).execute + return if result.success? + + # collate errors here and throw errors + results[:preprocess_errors][:milestone_errors] = result.payload + end end end end diff --git a/app/services/issues/base_service.rb b/app/services/issues/base_service.rb index f982d66eb08..b9b7cd08b68 100644 --- a/app/services/issues/base_service.rb +++ b/app/services/issues/base_service.rb @@ -111,6 +111,10 @@ module Issues issue.namespace.execute_integrations(issue_data, hooks_scope) execute_incident_hooks(issue, issue_data) if issue.work_item_type&.incident? + + return unless Feature.enabled?(:group_mentions, issue.project) + + execute_group_mention_hooks(issue, issue_data) if action == 'open' end # We can remove this code after proposal in @@ -121,6 +125,21 @@ module Issues issue.namespace.execute_integrations(issue_data, :incident_hooks) end + def execute_group_mention_hooks(issue, issue_data) + return unless issue.instance_of?(Issue) + + args = { + mentionable_type: 'Issue', + mentionable_id: issue.id, + hook_data: issue_data, + is_confidential: issue.confidential? + } + + issue.run_after_commit_or_now do + Integrations::GroupMentionWorker.perform_async(args) + end + end + def update_project_counter_caches?(issue) super || issue.confidential_changed? end diff --git a/app/services/issues/build_service.rb b/app/services/issues/build_service.rb index a65fc0c7c87..63cad593936 100644 --- a/app/services/issues/build_service.rb +++ b/app/services/issues/build_service.rb @@ -83,18 +83,17 @@ module Issues params.delete(:work_item_type) end - base_type = work_item_type&.base_type - - if create_issue_type_allowed?(container, base_type) - issue.work_item_type = work_item_type - # Up to this point issue_type might be set to the default, so we need to sync if a work item type is provided - issue.issue_type = base_type - else - # If no work item type was provided or not allowed, we need to set it to issue_type, - # and that includes the column default - issue_type = issue_params[:issue_type] || ::Issue::DEFAULT_ISSUE_TYPE - issue.work_item_type = WorkItems::Type.default_by_type(issue_type) - end + # We need to support the legacy input params[:issue_type] even if we don't have the issue_type column anymore. + # In the future only params[:work_item_type] should be provided + base_type = work_item_type&.base_type || params[:issue_type] + + issue.work_item_type = if create_issue_type_allowed?(container, base_type) + work_item_type || WorkItems::Type.default_by_type(base_type) + else + # If no work item type was provided or not allowed, we need to set it to + # the default issue_type + WorkItems::Type.default_by_type(::Issue::DEFAULT_ISSUE_TYPE) + end end def model_klass @@ -109,8 +108,6 @@ module Issues :confidential ] - public_issue_params << :issue_type if create_issue_type_allowed?(container, params[:issue_type]) - params.slice(*public_issue_params) end diff --git a/app/services/issues/create_service.rb b/app/services/issues/create_service.rb index 17b6866773e..e1ddfe47439 100644 --- a/app/services/issues/create_service.rb +++ b/app/services/issues/create_service.rb @@ -51,6 +51,7 @@ module Issues # current_user (defined in BaseService) is not available within run_after_commit block user = current_user + assign_description_from_template(issue) issue.run_after_commit do NewIssueWorker.perform_async(issue.id, user.id, issue.class.to_s) Issues::PlacementWorker.perform_async(nil, issue.project_id) @@ -127,6 +128,35 @@ module Issues set_crm_contacts(issue, contacts) end + + def assign_description_from_template(issue) + return if issue.description.present? + + # Find the exact name for the default template (if the project has one). + # Since there are multiple possibilities regarding the capitalization(s) that the + # default template file name can have, getting the exact template name here will + # allow us to extract the contents later, and bail early if the project does not have + # a default template + templates = TemplateFinder.all_template_names(project, :issues) + template = templates.values.flatten.find { |tmpl| tmpl[:name].casecmp?('default') } + + return unless template + + begin + default_template = TemplateFinder.build( + :issues, + issue.project, + { + name: template[:name], + source_template_project_id: issue.project.id + } + ).execute + rescue ::Gitlab::Template::Finders::RepoTemplateFinder::FileNotFoundError + nil + end + + issue.description = default_template.content if default_template.present? + end end end diff --git a/app/services/issues/export_csv_service.rb b/app/services/issues/export_csv_service.rb index 9e524d90505..99c0e9f1a37 100644 --- a/app/services/issues/export_csv_service.rb +++ b/app/services/issues/export_csv_service.rb @@ -34,14 +34,14 @@ module Issues '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) }, + 'Due Date' => -> (issue) { issue.due_date&.to_fs(:csv) }, + 'Created At (UTC)' => -> (issue) { issue.created_at&.to_fs(:csv) }, + 'Updated At (UTC)' => -> (issue) { issue.updated_at&.to_fs(:csv) }, + 'Closed At (UTC)' => -> (issue) { issue.closed_at&.to_fs(: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 Estimate' => ->(issue) { issue.time_estimate.to_fs(:csv) }, 'Time Spent' => -> (issue) { issue_time_spent(issue) } } end diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb index 7ad56d5a755..839d0e664a4 100644 --- a/app/services/issues/update_service.rb +++ b/app/services/issues/update_service.rb @@ -32,10 +32,9 @@ module Issues end def change_work_item_type(issue) - return unless issue.changed_attributes['issue_type'] + return unless params[:issue_type].present? - issue_type = params[:issue_type] || ::Issue::DEFAULT_ISSUE_TYPE - type_id = find_work_item_type_id(issue_type) + type_id = find_work_item_type_id(params[:issue_type]) issue.work_item_type_id = type_id end @@ -180,16 +179,22 @@ module Issues end def handle_issue_type_change(issue) - return unless issue.previous_changes.include?('issue_type') + return unless issue.previous_changes.include?('work_item_type_id') do_handle_issue_type_change(issue) end def do_handle_issue_type_change(issue) - SystemNoteService.change_issue_type(issue, current_user, issue.issue_type_before_last_save) + old_work_item_type = ::WorkItems::Type.find(issue.work_item_type_id_before_last_save).base_type + SystemNoteService.change_issue_type(issue, current_user, old_work_item_type) ::IncidentManagement::IssuableEscalationStatuses::CreateService.new(issue).execute if issue.supports_escalation? end + + override :allowed_update_params + def allowed_update_params(params) + super.except(:issue_type) + end end end diff --git a/app/services/members/creator_service.rb b/app/services/members/creator_service.rb index 699c5b94c53..a6fff3003ac 100644 --- a/app/services/members/creator_service.rb +++ b/app/services/members/creator_service.rb @@ -34,16 +34,7 @@ module Members # @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 + def add_members(sources, invitees, access_level, **args) return [] unless invitees.present? sources = Array.wrap(sources) if sources.is_a?(ApplicationRecord) # For single source @@ -51,7 +42,9 @@ module Members 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) + if managing_owners?(args[:current_user], access_level) && cannot_manage_owners?(source, args[:current_user]) + next [] + end emails, users, existing_members = parse_users_list(source, invitees) @@ -59,12 +52,8 @@ module Members 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 - } + tasks_to_be_done: args[:tasks_to_be_done] || [] + }.merge(parsed_args(args)) members = emails.map do |email| new(invitee: email, builder: InviteMemberBuilder, **common_arguments).execute @@ -79,26 +68,21 @@ module Members 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 + def add_member(source, invitee, access_level, **args) + add_members(source, [invitee], access_level, **args).first end private + def parsed_args(args) + { + current_user: args[:current_user], + expires_at: args[:expires_at], + tasks_project_id: args[:tasks_project_id], + ldap: args[:ldap] + } + end + def managing_owners?(current_user, access_level) current_user && Gitlab::Access.sym_options_with_owner[access_level] == Gitlab::Access::OWNER end diff --git a/app/services/members/groups/creator_service.rb b/app/services/members/groups/creator_service.rb index dd3d44e4d96..864be01a96d 100644 --- a/app/services/members/groups/creator_service.rb +++ b/app/services/members/groups/creator_service.rb @@ -21,3 +21,5 @@ module Members end end end + +Members::Groups::CreatorService.prepend_mod_with('Members::Groups::CreatorService') diff --git a/app/services/merge_requests/base_service.rb b/app/services/merge_requests/base_service.rb index ec8a17162ca..aaa91548d19 100644 --- a/app/services/merge_requests/base_service.rb +++ b/app/services/merge_requests/base_service.rb @@ -36,6 +36,10 @@ module MergeRequests execute_external_hooks(merge_request, merge_data) + if action == 'open' && Feature.enabled?(:group_mentions, merge_request.project) + execute_group_mention_hooks(merge_request, merge_data) + end + enqueue_jira_connect_messages_for(merge_request) end @@ -43,6 +47,21 @@ module MergeRequests # Implemented in EE end + def execute_group_mention_hooks(merge_request, merge_data) + return unless merge_request.instance_of?(MergeRequest) + + args = { + mentionable_type: 'MergeRequest', + mentionable_id: merge_request.id, + hook_data: merge_data, + is_confidential: false + } + + merge_request.run_after_commit_or_now do + Integrations::GroupMentionWorker.perform_async(args) + end + end + def handle_changes(merge_request, options) old_associations = options.fetch(:old_associations, {}) old_assignees = old_associations.fetch(:assignees, []) diff --git a/app/services/merge_requests/cleanup_refs_service.rb b/app/services/merge_requests/cleanup_refs_service.rb index 2094ea00160..5081655601b 100644 --- a/app/services/merge_requests/cleanup_refs_service.rb +++ b/app/services/merge_requests/cleanup_refs_service.rb @@ -16,7 +16,6 @@ module MergeRequests @merge_request = merge_request @repository = merge_request.project.repository @ref_path = merge_request.ref_path - @merge_ref_path = merge_request.merge_ref_path @ref_head_sha = @repository.commit(merge_request.ref_path)&.id @merge_ref_sha = merge_request.merge_ref_head&.id end @@ -42,7 +41,7 @@ module MergeRequests private - attr_reader :repository, :ref_path, :merge_ref_path, :ref_head_sha, :merge_ref_sha + attr_reader :repository, :ref_path, :ref_head_sha, :merge_ref_sha def scheduled? merge_request.cleanup_schedule.present? && merge_request.cleanup_schedule.scheduled_at <= Time.current @@ -79,7 +78,7 @@ module MergeRequests end def delete_refs - repository.delete_refs(ref_path, merge_ref_path) + merge_request.schedule_cleanup_refs end def update_schedule diff --git a/app/services/merge_requests/merge_to_ref_service.rb b/app/services/merge_requests/merge_to_ref_service.rb index 1bd26f06e41..acd3bc36e1d 100644 --- a/app/services/merge_requests/merge_to_ref_service.rb +++ b/app/services/merge_requests/merge_to_ref_service.rb @@ -56,13 +56,6 @@ module MergeRequests params[:first_parent_ref] || merge_request.target_branch_ref end - ## - # The parameter `allow_conflicts` is a flag whether merge conflicts should be merged into diff - # Default is false - def allow_conflicts - params[:allow_conflicts] || false - end - def commit(cache_merge_to_ref_calls = false) if cache_merge_to_ref_calls Rails.cache.fetch(cache_key, expires_in: 1.day) do @@ -79,8 +72,7 @@ module MergeRequests branch: merge_request.target_branch, target_ref: target_ref, message: commit_message, - first_parent_ref: first_parent_ref, - allow_conflicts: allow_conflicts) + first_parent_ref: first_parent_ref) rescue Gitlab::Git::PreReceiveError, Gitlab::Git::CommandError => error raise MergeError, error.message end diff --git a/app/services/merge_requests/mergeability_check_batch_service.rb b/app/services/merge_requests/mergeability_check_batch_service.rb new file mode 100644 index 00000000000..7697b596a83 --- /dev/null +++ b/app/services/merge_requests/mergeability_check_batch_service.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module MergeRequests + class MergeabilityCheckBatchService + def initialize(merge_requests, user) + @merge_requests = merge_requests + @user = user + end + + def execute + return unless merge_requests.present? + + MergeRequests::MergeabilityCheckBatchWorker.perform_async(merge_requests.map(&:id), user&.id) + end + + private + + attr_reader :merge_requests, :user + end +end diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb index d6740cdf1ac..447f4f9428c 100644 --- a/app/services/merge_requests/refresh_service.rb +++ b/app/services/merge_requests/refresh_service.rb @@ -169,7 +169,13 @@ module MergeRequests @outdate_service ||= Suggestions::OutdateService.new end + def abort_auto_merges?(merge_request) + merge_request.merge_params.with_indifferent_access[:sha] != @push.newrev + end + def abort_auto_merges(merge_request) + return unless abort_auto_merges?(merge_request) + abort_auto_merge(merge_request, 'source branch was updated') end diff --git a/app/services/milestones/create_service.rb b/app/services/milestones/create_service.rb index 6c3edd2e147..e8a14adc10d 100644 --- a/app/services/milestones/create_service.rb +++ b/app/services/milestones/create_service.rb @@ -5,11 +5,19 @@ module Milestones def execute milestone = parent.milestones.new(params) + before_create(milestone) + if milestone.save && milestone.project_milestone? event_service.open_milestone(milestone, current_user) end milestone end + + private + + def before_create(milestone) + milestone.check_for_spam(user: current_user, action: :create) + end end end diff --git a/app/services/milestones/update_service.rb b/app/services/milestones/update_service.rb index b9a12a35d31..90cb8ea9f5c 100644 --- a/app/services/milestones/update_service.rb +++ b/app/services/milestones/update_service.rb @@ -13,11 +13,22 @@ module Milestones end if params.present? - milestone.update(params.except(:state_event)) + milestone.assign_attributes(params.except(:state_event)) end + if milestone.changed? + before_update(milestone) + end + + milestone.save milestone end + + private + + def before_update(milestone) + milestone.check_for_spam(user: current_user, action: :update) + end end end diff --git a/app/services/namespace_settings/update_service.rb b/app/services/namespace_settings/update_service.rb index 25525265e1c..c391320db5e 100644 --- a/app/services/namespace_settings/update_service.rb +++ b/app/services/namespace_settings/update_service.rb @@ -23,6 +23,12 @@ module NamespaceSettings param_key: :new_user_signups_cap, user_policy: :change_new_user_signups_cap ) + validate_settings_param_for_root_group( + param_key: :default_branch_protection, + user_policy: :update_default_branch_protection + ) + + handle_default_branch_protection unless settings_params[:default_branch_protection].blank? if group.namespace_settings group.namespace_settings.attributes = settings_params @@ -33,6 +39,17 @@ module NamespaceSettings private + def handle_default_branch_protection + # We are migrating default_branch_protection from an integer + # column to a jsonb column. While completing the rest of the + # work, we want to start translating the updates sent to the + # existing column into the json. Eventually, we will be updating + # the jsonb column directly and deprecating the original update + # path. Until then, we want to sync up both columns. + protection = Gitlab::Access::BranchProtection.new(settings_params.delete(:default_branch_protection).to_i) + settings_params[:default_branch_protection_defaults] = protection.to_hash + end + def validate_resource_access_token_creation_allowed_param return if settings_params[:resource_access_token_creation_allowed].nil? diff --git a/app/services/notes/post_process_service.rb b/app/services/notes/post_process_service.rb index c9375fe14a1..9465b5218b0 100644 --- a/app/services/notes/post_process_service.rb +++ b/app/services/notes/post_process_service.rb @@ -36,10 +36,19 @@ module Notes return unless note.project note_data = hook_data - hooks_scope = note.confidential?(include_noteable: true) ? :confidential_note_hooks : :note_hooks + is_confidential = note.confidential?(include_noteable: true) + hooks_scope = is_confidential ? :confidential_note_hooks : :note_hooks note.project.execute_hooks(note_data, hooks_scope) note.project.execute_integrations(note_data, hooks_scope) + + return unless Feature.enabled?(:group_mentions, note.project) + + execute_group_mention_hooks(note, note_data, is_confidential) + end + + def execute_group_mention_hooks(note, note_data, is_confidential) + Integrations::GroupMentionService.new(note, hook_data: note_data, is_confidential: is_confidential).execute end end end diff --git a/app/services/packages/debian/find_or_create_package_service.rb b/app/services/packages/debian/find_or_create_package_service.rb deleted file mode 100644 index a9481504d2b..00000000000 --- a/app/services/packages/debian/find_or_create_package_service.rb +++ /dev/null @@ -1,42 +0,0 @@ -# frozen_string_literal: true - -module Packages - module Debian - class FindOrCreatePackageService < ::Packages::CreatePackageService - include Gitlab::Utils::StrongMemoize - - def execute - packages = project.packages - .existing_debian_packages_with(name: params[:name], version: params[:version]) - - package = packages.with_debian_codename_or_suite(params[:distribution_name]).first - - unless package - package_in_other_distribution = packages.first - - if package_in_other_distribution - raise ArgumentError, "Debian package #{params[:name]} #{params[:version]} exists " \ - "in distribution #{package_in_other_distribution.debian_distribution.codename}" - end - end - - package ||= create_package!( - :debian, - debian_publication_attributes: { distribution_id: distribution.id } - ) - - ServiceResponse.success(payload: { package: package }) - end - - private - - def distribution - Packages::Debian::DistributionsFinder.new( - project, - codename_or_suite: params[:distribution_name] - ).execute.last! - end - strong_memoize_attr :distribution - end - end -end diff --git a/app/services/packages/debian/process_changes_service.rb b/app/services/packages/debian/process_changes_service.rb deleted file mode 100644 index eb88e7c9b59..00000000000 --- a/app/services/packages/debian/process_changes_service.rb +++ /dev/null @@ -1,113 +0,0 @@ -# frozen_string_literal: true - -module Packages - module Debian - class ProcessChangesService - include ExclusiveLeaseGuard - include Gitlab::Utils::StrongMemoize - - # used by ExclusiveLeaseGuard - DEFAULT_LEASE_TIMEOUT = 1.hour.to_i.freeze - - def initialize(package_file, creator) - @package_file = package_file - @creator = creator - end - - def execute - # return if changes file has already been processed - return if package_file.debian_file_metadatum&.changes? - - validate! - - try_obtain_lease do - package_file.transaction do - update_files_metadata - update_changes_metadata - end - - ::Packages::Debian::GenerateDistributionWorker.perform_async(:project, package.debian_distribution.id) - end - end - - private - - attr_reader :package_file, :creator - - def validate! - raise ArgumentError, 'invalid package file' unless package_file.debian_file_metadatum - raise ArgumentError, 'invalid package file' unless package_file.debian_file_metadatum.unknown? - raise ArgumentError, 'invalid package file' unless metadata[:file_type] == :changes - raise ArgumentError, 'missing Source field' unless metadata.dig(:fields, 'Source').present? - raise ArgumentError, 'missing Version field' unless metadata.dig(:fields, 'Version').present? - raise ArgumentError, 'missing Distribution field' unless metadata.dig(:fields, 'Distribution').present? - end - - def update_files_metadata - files.each do |filename, entry| - file_metadata = ::Packages::Debian::ExtractMetadataService.new(entry.package_file).execute - - ::Packages::UpdatePackageFileService.new(entry.package_file, package_id: package.id) - .execute - - # Force reload from database, as package has changed - entry.package_file.reload_package - - entry.package_file.debian_file_metadatum.update!( - file_type: file_metadata[:file_type], - component: files[filename].component, - architecture: file_metadata[:architecture], - fields: file_metadata[:fields] - ) - end - end - - def update_changes_metadata - ::Packages::UpdatePackageFileService.new(package_file, package_id: package.id) - .execute - - # Force reload from database, as package has changed - package_file.reload_package - - package_file.debian_file_metadatum.update!( - file_type: metadata[:file_type], - fields: metadata[:fields] - ) - end - - def metadata - ::Packages::Debian::ExtractChangesMetadataService.new(package_file).execute - end - strong_memoize_attr :metadata - - def files - metadata[:files] - end - - def project - package_file.package.project - end - - def package - params = { - name: metadata[:fields]['Source'], - version: metadata[:fields]['Version'], - distribution_name: metadata[:fields]['Distribution'] - } - response = Packages::Debian::FindOrCreatePackageService.new(project, creator, params).execute - response.payload[:package] - end - strong_memoize_attr :package - - # used by ExclusiveLeaseGuard - def lease_key - "packages:debian:process_changes_service:package_file:#{package_file.id}" - end - - # used by ExclusiveLeaseGuard - def lease_timeout - DEFAULT_LEASE_TIMEOUT - end - end - end -end diff --git a/app/services/packages/npm/create_metadata_cache_service.rb b/app/services/packages/npm/create_metadata_cache_service.rb index 75cff5c5453..f470b9f1202 100644 --- a/app/services/packages/npm/create_metadata_cache_service.rb +++ b/app/services/packages/npm/create_metadata_cache_service.rb @@ -30,7 +30,7 @@ module Packages attr_reader :package_name, :project def metadata_content - metadata.payload.to_json + ::API::Entities::NpmPackage.represent(metadata.payload).to_json end strong_memoize_attr :metadata_content diff --git a/app/services/packages/npm/create_package_service.rb b/app/services/packages/npm/create_package_service.rb index 2c578760cc5..f6f2dbb8415 100644 --- a/app/services/packages/npm/create_package_service.rb +++ b/app/services/packages/npm/create_package_service.rb @@ -18,7 +18,7 @@ module Packages ApplicationRecord.transaction { create_npm_package! } end - return error('Could not obtain package lease.', 400) unless package + return error('Could not obtain package lease. Please try again.', 400) unless package package end @@ -40,7 +40,7 @@ module Packages def create_npm_metadatum!(package) package.create_npm_metadatum!(package_json: package_json) rescue ActiveRecord::RecordInvalid => e - if package.npm_metadatum && package.npm_metadatum.errors.added?(:package_json, 'structure is too large') + if package.npm_metadatum && package.npm_metadatum.errors.where(:package_json, :too_large).any? # rubocop: disable CodeReuse/ActiveRecord Gitlab::ErrorTracking.track_exception(e, field_sizes: field_sizes_for_error_tracking) end diff --git a/app/services/packages/npm/deprecate_package_service.rb b/app/services/packages/npm/deprecate_package_service.rb index 2633e9f877c..bca81ebe1de 100644 --- a/app/services/packages/npm/deprecate_package_service.rb +++ b/app/services/packages/npm/deprecate_package_service.rb @@ -31,7 +31,7 @@ module Packages def packages ::Packages::Npm::PackageFinder - .new(params['package_name'], project: project, last_of_each_version: false) + .new(params['package_name'], project: project) .execute end diff --git a/app/services/packages/npm/generate_metadata_service.rb b/app/services/packages/npm/generate_metadata_service.rb index 800c3ce19b4..e1795079513 100644 --- a/app/services/packages/npm/generate_metadata_service.rb +++ b/app/services/packages/npm/generate_metadata_service.rb @@ -98,7 +98,7 @@ module Packages end def package_tags - Packages::Tag.for_package_ids(packages.last_of_each_version_ids) + Packages::Tag.for_package_ids(packages) .preload_package end diff --git a/app/services/packages/nuget/extract_metadata_content_service.rb b/app/services/packages/nuget/extract_metadata_content_service.rb new file mode 100644 index 00000000000..28653654018 --- /dev/null +++ b/app/services/packages/nuget/extract_metadata_content_service.rb @@ -0,0 +1,85 @@ +# frozen_string_literal: true + +module Packages + module Nuget + class ExtractMetadataContentService + ROOT_XPATH = '//xmlns:package/xmlns:metadata/xmlns' + + XPATHS = { + package_name: "#{ROOT_XPATH}:id", + package_version: "#{ROOT_XPATH}:version", + authors: "#{ROOT_XPATH}:authors", + description: "#{ROOT_XPATH}:description", + license_url: "#{ROOT_XPATH}:licenseUrl", + project_url: "#{ROOT_XPATH}:projectUrl", + icon_url: "#{ROOT_XPATH}:iconUrl" + }.freeze + + XPATH_DEPENDENCIES = "#{ROOT_XPATH}:dependencies/xmlns:dependency".freeze + XPATH_DEPENDENCY_GROUPS = "#{ROOT_XPATH}:dependencies/xmlns:group".freeze + XPATH_TAGS = "#{ROOT_XPATH}:tags".freeze + XPATH_PACKAGE_TYPES = "#{ROOT_XPATH}:packageTypes/xmlns:packageType".freeze + + def initialize(nuspec_file_content) + @nuspec_file_content = nuspec_file_content + end + + def execute + ServiceResponse.success(payload: extract_metadata(nuspec_file_content)) + end + + private + + attr_reader :nuspec_file_content + + def extract_metadata(file) + doc = Nokogiri::XML(file) + + XPATHS.transform_values { |query| doc.xpath(query).text.presence } + .compact + .tap do |metadata| + metadata[:package_dependencies] = extract_dependencies(doc) + metadata[:package_tags] = extract_tags(doc) + metadata[:package_types] = extract_package_types(doc) + end + end + + def extract_dependencies(doc) + dependencies = [] + + doc.xpath(XPATH_DEPENDENCIES).each do |node| + dependencies << extract_dependency(node) + end + + doc.xpath(XPATH_DEPENDENCY_GROUPS).each do |group_node| + target_framework = group_node.attr('targetFramework') + + group_node.xpath('xmlns:dependency').each do |node| + dependencies << extract_dependency(node).merge(target_framework: target_framework) + end + end + + dependencies + end + + def extract_dependency(node) + { + name: node.attr('id'), + version: node.attr('version') + }.compact + end + + def extract_tags(doc) + tags = doc.xpath(XPATH_TAGS).text + + return [] if tags.blank? + + tags.split(::Packages::Tag::NUGET_TAGS_SEPARATOR) + end + + def extract_package_types(doc) + doc.xpath(XPATH_PACKAGE_TYPES).map { |node| node.attr('name') }.uniq + end + end + end +end diff --git a/app/services/packages/nuget/extract_metadata_file_service.rb b/app/services/packages/nuget/extract_metadata_file_service.rb new file mode 100644 index 00000000000..61e4892fee7 --- /dev/null +++ b/app/services/packages/nuget/extract_metadata_file_service.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +module Packages + module Nuget + class ExtractMetadataFileService + include Gitlab::Utils::StrongMemoize + + ExtractionError = Class.new(StandardError) + + MAX_FILE_SIZE = 4.megabytes.freeze + + def initialize(package_file_id) + @package_file_id = package_file_id + end + + def execute + raise ExtractionError, 'invalid package file' unless valid_package_file? + + ServiceResponse.success(payload: nuspec_file_content) + end + + private + + attr_reader :package_file_id + + def package_file + ::Packages::PackageFile.find_by_id(package_file_id) + end + strong_memoize_attr :package_file + + def valid_package_file? + package_file && + package_file.package&.nuget? && + package_file.file.size > 0 # rubocop:disable Style/ZeroLengthPredicate + end + + def nuspec_file_content + with_zip_file do |zip_file| + entry = zip_file.glob('*.nuspec').first + + raise ExtractionError, 'nuspec file not found' unless entry + raise ExtractionError, 'nuspec file too big' if MAX_FILE_SIZE < entry.size + + Tempfile.open("nuget_extraction_package_file_#{package_file_id}") do |file| + entry.extract(file.path) { true } # allow #extract to overwrite the file + file.unlink + file.read + end + rescue Zip::EntrySizeError => e + raise ExtractionError, "nuspec file has the wrong entry size: #{e.message}" + end + end + + def with_zip_file + package_file.file.use_open_file do |open_file| + zip_file = Zip::File.new(open_file, false, true) # rubocop:disable Performance/Rubyzip + yield(zip_file) + end + end + end + end +end diff --git a/app/services/packages/nuget/metadata_extraction_service.rb b/app/services/packages/nuget/metadata_extraction_service.rb index 5c60a2912ae..e1ee29ef2c6 100644 --- a/app/services/packages/nuget/metadata_extraction_service.rb +++ b/app/services/packages/nuget/metadata_extraction_service.rb @@ -3,123 +3,30 @@ module Packages module Nuget class MetadataExtractionService - include Gitlab::Utils::StrongMemoize - - ExtractionError = Class.new(StandardError) - - ROOT_XPATH = '//xmlns:package/xmlns:metadata/xmlns' - - XPATHS = { - package_name: "#{ROOT_XPATH}:id", - package_version: "#{ROOT_XPATH}:version", - authors: "#{ROOT_XPATH}:authors", - description: "#{ROOT_XPATH}:description", - license_url: "#{ROOT_XPATH}:licenseUrl", - project_url: "#{ROOT_XPATH}:projectUrl", - icon_url: "#{ROOT_XPATH}:iconUrl" - }.freeze - - XPATH_DEPENDENCIES = "#{ROOT_XPATH}:dependencies/xmlns:dependency".freeze - XPATH_DEPENDENCY_GROUPS = "#{ROOT_XPATH}:dependencies/xmlns:group".freeze - XPATH_TAGS = "#{ROOT_XPATH}:tags".freeze - XPATH_PACKAGE_TYPES = "#{ROOT_XPATH}:packageTypes/xmlns:packageType".freeze - - MAX_FILE_SIZE = 4.megabytes.freeze - def initialize(package_file_id) @package_file_id = package_file_id end def execute - raise ExtractionError, 'invalid package file' unless valid_package_file? - - extract_metadata(nuspec_file_content) + ServiceResponse.success(payload: metadata) end private - def package_file - ::Packages::PackageFile.find_by_id(@package_file_id) - end - strong_memoize_attr :package_file - - def valid_package_file? - package_file && - package_file.package&.nuget? && - package_file.file.size > 0 # rubocop:disable Style/ZeroLengthPredicate - end - - def extract_metadata(file) - doc = Nokogiri::XML(file) - - XPATHS.transform_values { |query| doc.xpath(query).text.presence } - .compact - .tap do |metadata| - metadata[:package_dependencies] = extract_dependencies(doc) - metadata[:package_tags] = extract_tags(doc) - metadata[:package_types] = extract_package_types(doc) - end - end - - def extract_dependencies(doc) - dependencies = [] - - doc.xpath(XPATH_DEPENDENCIES).each do |node| - dependencies << extract_dependency(node) - end - - doc.xpath(XPATH_DEPENDENCY_GROUPS).each do |group_node| - target_framework = group_node.attr("targetFramework") - - group_node.xpath("xmlns:dependency").each do |node| - dependencies << extract_dependency(node).merge(target_framework: target_framework) - end - end - - dependencies - end - - def extract_dependency(node) - { - name: node.attr('id'), - version: node.attr('version') - }.compact - end - - def extract_package_types(doc) - doc.xpath(XPATH_PACKAGE_TYPES).map { |node| node.attr('name') }.uniq - end - - def extract_tags(doc) - tags = doc.xpath(XPATH_TAGS).text - - return [] if tags.blank? - - tags.split(::Packages::Tag::NUGET_TAGS_SEPARATOR) - end + attr_reader :package_file_id def nuspec_file_content - with_zip_file do |zip_file| - entry = zip_file.glob('*.nuspec').first - - raise ExtractionError, 'nuspec file not found' unless entry - raise ExtractionError, 'nuspec file too big' if MAX_FILE_SIZE < entry.size - - Tempfile.open("nuget_extraction_package_file_#{@package_file_id}") do |file| - entry.extract(file.path) { true } # allow #extract to overwrite the file - file.unlink - file.read - end - rescue Zip::EntrySizeError => e - raise ExtractionError, "nuspec file has the wrong entry size: #{e.message}" - end + ExtractMetadataFileService + .new(package_file_id) + .execute + .payload end - def with_zip_file(&block) - package_file.file.use_open_file do |open_file| - zip_file = Zip::File.new(open_file, false, true) - yield(zip_file) - end + def metadata + ExtractMetadataContentService + .new(nuspec_file_content) + .execute + .payload end end end diff --git a/app/services/packages/nuget/update_package_from_metadata_service.rb b/app/services/packages/nuget/update_package_from_metadata_service.rb index 8e2679db31b..d82509fff5e 100644 --- a/app/services/packages/nuget/update_package_from_metadata_service.rb +++ b/app/services/packages/nuget/update_package_from_metadata_service.rb @@ -145,7 +145,7 @@ module Packages end def metadata - ::Packages::Nuget::MetadataExtractionService.new(@package_file.id).execute + ::Packages::Nuget::MetadataExtractionService.new(@package_file.id).execute.payload end strong_memoize_attr :metadata diff --git a/app/services/personal_access_tokens/last_used_service.rb b/app/services/personal_access_tokens/last_used_service.rb index 6fc3110a70b..3b075364458 100644 --- a/app/services/personal_access_tokens/last_used_service.rb +++ b/app/services/personal_access_tokens/last_used_service.rb @@ -24,12 +24,7 @@ module PersonalAccessTokens return true if last_used.nil? - if Feature.enabled?(:update_personal_access_token_usage_information_every_10_minutes) && - last_used <= 10.minutes.ago - return true - end - - last_used <= 1.day.ago + last_used <= 10.minutes.ago end end end diff --git a/app/services/personal_access_tokens/revoke_token_family_service.rb b/app/services/personal_access_tokens/revoke_token_family_service.rb new file mode 100644 index 00000000000..547ba6c3bdc --- /dev/null +++ b/app/services/personal_access_tokens/revoke_token_family_service.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +module PersonalAccessTokens + class RevokeTokenFamilyService + def initialize(token) + @token = token + end + + def execute + # Despite using #update_all, there should only be a single active token. + # A token family is a chain of rotated tokens. Once rotated, the + # previous token is revoked. + pat_family.active.update_all(revoked: true) + + ServiceResponse.success + end + + private + + attr_reader :token + + def pat_family + # rubocop: disable CodeReuse/ActiveRecord + cte = Gitlab::SQL::RecursiveCTE.new(:personal_access_tokens_cte) + personal_access_token_table = Arel::Table.new(:personal_access_tokens) + + cte << PersonalAccessToken + .where(personal_access_token_table[:previous_personal_access_token_id].eq(token.id)) + cte << PersonalAccessToken + .from([personal_access_token_table, cte.table]) + .where(personal_access_token_table[:previous_personal_access_token_id].eq(cte.table[:id])) + PersonalAccessToken.with.recursive(cte.to_arel).from(cte.alias_to(personal_access_token_table)) + # rubocop: enable CodeReuse/ActiveRecord + end + end +end diff --git a/app/services/personal_access_tokens/rotate_service.rb b/app/services/personal_access_tokens/rotate_service.rb index 64b0c5c98a9..b765aacef68 100644 --- a/app/services/personal_access_tokens/rotate_service.rb +++ b/app/services/personal_access_tokens/rotate_service.rb @@ -41,6 +41,7 @@ module PersonalAccessTokens def create_token_params(token) { name: token.name, + previous_personal_access_token_id: token.id, impersonation: token.impersonation, scopes: token.scopes, expires_at: Date.today + EXPIRATION_PERIOD } diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb index 2279ab301dc..a5c12384b59 100644 --- a/app/services/projects/destroy_service.rb +++ b/app/services/projects/destroy_service.rb @@ -58,6 +58,10 @@ module Projects unless remove_repository(project.wiki.repository) raise_error(s_('DeleteProject|Failed to remove wiki repository. Please try again or contact administrator.')) end + + unless remove_repository(project.design_repository) + raise_error(s_('DeleteProject|Failed to remove design repository. Please try again or contact administrator.')) + end end def trash_relation_repositories! diff --git a/app/services/projects/download_service.rb b/app/services/projects/download_service.rb index 72cb3997045..22104409199 100644 --- a/app/services/projects/download_service.rb +++ b/app/services/projects/download_service.rb @@ -2,7 +2,7 @@ module Projects class DownloadService < BaseService - WHITELIST = [ + ALLOWLIST = [ /^[^.]+\.fogbugz.com$/ ].freeze @@ -33,7 +33,7 @@ module Projects def valid_domain?(url) host = URI.parse(url).host - WHITELIST.any? { |entry| entry === host } + ALLOWLIST.any? { |entry| entry === host } end end end diff --git a/app/services/projects/participants_service.rb b/app/services/projects/participants_service.rb index 8c807e0016b..44cd6e9926f 100644 --- a/app/services/projects/participants_service.rb +++ b/app/services/projects/participants_service.rb @@ -30,6 +30,8 @@ module Projects end def all_members + return [] if Feature.enabled?(:disable_all_mention) + [{ username: "all", name: "All Project and Group Members", count: project_members.count }] end diff --git a/app/services/projects/prometheus/alerts/notify_service.rb b/app/services/projects/prometheus/alerts/notify_service.rb index f1c093c89b7..22a882c4648 100644 --- a/app/services/projects/prometheus/alerts/notify_service.rb +++ b/app/services/projects/prometheus/alerts/notify_service.rb @@ -89,7 +89,9 @@ module Projects # AlertManagement::HttpIntegrations is complete, # we should use use the HttpIntegration as SSOT. # Remove with https://gitlab.com/gitlab-org/gitlab/-/issues/409734 - return false if project.alert_management_http_integrations.legacy.prometheus.any? + return false if project.alert_management_http_integrations + .for_endpoint_identifier('legacy-prometheus') + .any? prometheus = project.find_or_initialize_integration('prometheus') return false unless prometheus.manual_configuration? diff --git a/app/services/projects/update_remote_mirror_service.rb b/app/services/projects/update_remote_mirror_service.rb index b048ec128d8..d5c8e958bbd 100644 --- a/app/services/projects/update_remote_mirror_service.rb +++ b/app/services/projects/update_remote_mirror_service.rb @@ -93,7 +93,7 @@ module Projects # TODO: Support LFS sync over SSH # https://gitlab.com/gitlab-org/gitlab/-/issues/249587 - return unless remote_mirror.url =~ %r{\Ahttps?://}i + return unless %r{\Ahttps?://}i.match?(remote_mirror.url) return unless remote_mirror.password_auth? Lfs::PushService.new( diff --git a/app/services/quick_actions/interpret_service.rb b/app/services/quick_actions/interpret_service.rb index b5f6bff756b..d1798ce6fc0 100644 --- a/app/services/quick_actions/interpret_service.rb +++ b/app/services/quick_actions/interpret_service.rb @@ -188,7 +188,8 @@ module QuickActions next unless definition definition.execute(self, arg) - usage_ping_tracking(definition.name, arg) + # summarize_diff will be removed https://gitlab.com/gitlab-org/gitlab/-/issues/407258#note_1385269274 + usage_ping_tracking(definition.name, arg) unless definition.name == :summarize_diff end end diff --git a/app/services/search/project_service.rb b/app/services/search/project_service.rb index 71314f85984..73d46a9ba70 100644 --- a/app/services/search/project_service.rb +++ b/app/services/search/project_service.rb @@ -4,7 +4,6 @@ module Search class ProjectService include Search::Filter include Gitlab::Utils::StrongMemoize - include ProjectsHelper ALLOWED_SCOPES = %w(blobs issues merge_requests wiki_blobs commits notes milestones users).freeze @@ -18,13 +17,13 @@ module Search def execute Gitlab::ProjectSearchResults.new(current_user, - params[:search], - project: project, - repository_ref: params[:repository_ref], - order_by: params[:order_by], - sort: params[:sort], - filters: filters - ) + params[:search], + project: project, + repository_ref: params[:repository_ref], + order_by: params[:order_by], + sort: params[:sort], + filters: filters + ) end def allowed_scopes @@ -33,10 +32,12 @@ module Search def scope strong_memoize(:scope) do - next params[:scope] if allowed_scopes.include?(params[:scope]) && project_search_tabs?(params[:scope].to_sym) + search_navigation = Search::Navigation.new(user: current_user, project: project) + scope = params[:scope] + next scope if allowed_scopes.include?(scope) && search_navigation.tab_enabled_for_project?(scope.to_sym) - allowed_scopes.find do |scope| - project_search_tabs?(scope.to_sym) + allowed_scopes.find do |s| + search_navigation.tab_enabled_for_project?(s.to_sym) end end end diff --git a/app/services/search_service.rb b/app/services/search_service.rb index 5705e4c7cef..433e9b0da6d 100644 --- a/app/services/search_service.rb +++ b/app/services/search_service.rb @@ -102,16 +102,6 @@ class SearchService end end - def show_elasticsearch_tabs? - # overridden in EE - false - end - - def show_epics? - # overridden in EE - false - end - def global_search_enabled_for_scope? return false if show_snippets? && Feature.disabled?(:global_search_snippet_titles_tab, current_user, type: :ops) diff --git a/app/services/service_desk/custom_emails/base_service.rb b/app/services/service_desk/custom_emails/base_service.rb new file mode 100644 index 00000000000..62152f31012 --- /dev/null +++ b/app/services/service_desk/custom_emails/base_service.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +module ServiceDesk + module CustomEmails + class BaseService < ::BaseProjectService + private + + def legitimate_user? + can?(current_user, :admin_project, project) + end + + def setting? + project.service_desk_setting.present? + end + + def credential? + project.service_desk_custom_email_verification.present? + end + + def verification? + project.service_desk_custom_email_credential.present? + end + + def feature_flag_enabled? + Feature.enabled?(:service_desk_custom_email, project) + end + + def error_user_not_authorized + error_response(s_('ServiceDesk|User cannot manage project.')) + end + + def error_feature_flag_disabled + error_response('Feature flag service_desk_custom_email is not enabled') + end + + def error_response(message) + ServiceResponse.error(message: message) + end + end + end +end diff --git a/app/services/service_desk/custom_emails/create_service.rb b/app/services/service_desk/custom_emails/create_service.rb new file mode 100644 index 00000000000..c3ca98a0259 --- /dev/null +++ b/app/services/service_desk/custom_emails/create_service.rb @@ -0,0 +1,85 @@ +# frozen_string_literal: true + +module ServiceDesk + module CustomEmails + class CreateService < BaseService + def execute + return error_feature_flag_disabled unless feature_flag_enabled? + return error_user_not_authorized unless legitimate_user? + return error_params_missing unless has_required_params? + return error_custom_email_exists if credential? || verification? + + return error_cannot_create_custom_email unless create_credential + + if update_settings.error? + # We don't warp everything in a single transaction here and roll it back + # because ServiceDeskSettings::UpdateService uses safe_find_or_create_by! + rollback_credential + return error_cannot_create_custom_email + end + + project.reset + + # The create service may return an error response if the verification fails early. + # Here We want to indicate whether adding a custom email address was successful, so + # we don't use its response here. + create_verification + + ServiceResponse.success + end + + private + + def update_settings + ServiceDeskSettings::UpdateService.new(project, current_user, create_setting_params).execute + end + + def rollback_credential + ::ServiceDesk::CustomEmailCredential.find_by_project_id(project.id)&.destroy + end + + def create_credential + credential = ::ServiceDesk::CustomEmailCredential.new(create_credential_params.merge(project: project)) + credential.save + end + + def create_verification + ::ServiceDesk::CustomEmailVerifications::CreateService.new(project: project, current_user: current_user).execute + end + + def create_setting_params + ensure_params.permit(:custom_email) + end + + def create_credential_params + ensure_params.permit(:smtp_address, :smtp_port, :smtp_username, :smtp_password) + end + + def ensure_params + return params if params.is_a?(ActionController::Parameters) + + ActionController::Parameters.new(params) + end + + def has_required_params? + required_keys.all? { |key| params.key?(key) && params[key].present? } + end + + def required_keys + %i[custom_email smtp_address smtp_port smtp_username smtp_password] + end + + def error_custom_email_exists + error_response(s_('ServiceDesk|Custom email already exists')) + end + + def error_params_missing + error_response(s_('ServiceDesk|Parameters missing')) + end + + def error_cannot_create_custom_email + error_response(s_('ServiceDesk|Cannot create custom email')) + end + end + end +end diff --git a/app/services/service_desk/custom_emails/destroy_service.rb b/app/services/service_desk/custom_emails/destroy_service.rb new file mode 100644 index 00000000000..1aa5994edd8 --- /dev/null +++ b/app/services/service_desk/custom_emails/destroy_service.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module ServiceDesk + module CustomEmails + class DestroyService < BaseService + def execute + return error_feature_flag_disabled unless feature_flag_enabled? + return error_user_not_authorized unless legitimate_user? + return error_does_not_exist unless verification? || credential? || setting? + + project.service_desk_custom_email_verification&.destroy + project.service_desk_custom_email_credential&.destroy + project.reset + project.service_desk_setting&.update!(custom_email: nil, custom_email_enabled: false) + + ServiceResponse.success + end + + private + + def error_does_not_exist + error_response(s_('ServiceDesk|Custom email does not exist')) + end + end + end +end diff --git a/app/services/service_desk_settings/update_service.rb b/app/services/service_desk_settings/update_service.rb index 5fe74f1f2ff..61cb6fce11f 100644 --- a/app/services/service_desk_settings/update_service.rb +++ b/app/services/service_desk_settings/update_service.rb @@ -8,9 +8,9 @@ module ServiceDeskSettings params[:project_key] = nil if params[:project_key].blank? if settings.update(params) - success + ServiceResponse.success else - error(settings.errors.full_messages.to_sentence) + ServiceResponse.error(message: settings.errors.full_messages.to_sentence) end end end diff --git a/app/services/service_response.rb b/app/services/service_response.rb index da4773ab9c7..86efc01bd30 100644 --- a/app/services/service_response.rb +++ b/app/services/service_response.rb @@ -56,6 +56,10 @@ class ServiceResponse reason: reason) end + def deconstruct_keys(keys) + to_h.slice(*keys) + end + def success? status == :success end diff --git a/app/services/spam/spam_verdict_service.rb b/app/services/spam/spam_verdict_service.rb index 2ecd431fd91..e0a6d58b904 100644 --- a/app/services/spam/spam_verdict_service.rb +++ b/app/services/spam/spam_verdict_service.rb @@ -85,7 +85,7 @@ module Spam # than the override verdict's priority value), then we don't need to override it. return false if SUPPORTED_VERDICTS[verdict][:priority] > SUPPORTED_VERDICTS[OVERRIDE_VIA_ALLOW_POSSIBLE_SPAM][:priority] - target.allow_possible_spam? + target.allow_possible_spam?(user) || user.allow_possible_spam? end def spamcheck_client diff --git a/app/services/system_notes/merge_requests_service.rb b/app/services/system_notes/merge_requests_service.rb index 7758c1e8597..d71388a1552 100644 --- a/app/services/system_notes/merge_requests_service.rb +++ b/app/services/system_notes/merge_requests_service.rb @@ -181,3 +181,5 @@ module SystemNotes end end end + +SystemNotes::MergeRequestsService.prepend_mod_with('SystemNotes::MergeRequestsService') diff --git a/app/services/system_notes/time_tracking_service.rb b/app/services/system_notes/time_tracking_service.rb index b7a2afbaf15..f9084ed67d3 100644 --- a/app/services/system_notes/time_tracking_service.rb +++ b/app/services/system_notes/time_tracking_service.rb @@ -147,9 +147,9 @@ module SystemNotes readable_date = date_key.humanize.downcase if changed_date.nil? - "removed #{readable_date} #{changed_dates[date_key].first.to_s(:long)}" + "removed #{readable_date} #{changed_dates[date_key].first.to_fs(:long)}" else - "changed #{readable_date} to #{changed_date.to_s(:long)}" + "changed #{readable_date} to #{changed_date.to_fs(:long)}" end end diff --git a/app/services/test_hooks/project_service.rb b/app/services/test_hooks/project_service.rb index dcd92ac2b8c..42af65ebd57 100644 --- a/app/services/test_hooks/project_service.rb +++ b/app/services/test_hooks/project_service.rb @@ -32,6 +32,8 @@ module TestHooks wiki_page_events_data when 'releases_events' releases_events_data + when 'emoji_events' + emoji_events_data end end end diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb index c55e1680bfe..1f6cf2c83c9 100644 --- a/app/services/todo_service.rb +++ b/app/services/todo_service.rb @@ -377,7 +377,7 @@ class TodoService attributes = { project_id: target&.project&.id, target_id: target.id, - target_type: target.class.name, + target_type: target.class.try(:polymorphic_name) || target.class.name, commit_id: nil } diff --git a/app/services/users/allow_possible_spam_service.rb b/app/services/users/allow_possible_spam_service.rb new file mode 100644 index 00000000000..d9273fe0fc1 --- /dev/null +++ b/app/services/users/allow_possible_spam_service.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Users + class AllowPossibleSpamService < BaseService + def initialize(current_user) + @current_user = current_user + end + + def execute(user) + custom_attribute = { + user_id: user.id, + key: UserCustomAttribute::ALLOW_POSSIBLE_SPAM, + value: "#{current_user.username}/#{current_user.id}+#{Time.current}" + } + UserCustomAttribute.upsert_custom_attributes([custom_attribute]) + end + end +end diff --git a/app/services/users/ban_service.rb b/app/services/users/ban_service.rb index 5ed31cdb778..20c34b15f15 100644 --- a/app/services/users/ban_service.rb +++ b/app/services/users/ban_service.rb @@ -2,6 +2,8 @@ module Users class BanService < BannedUserBaseService + extend ::Gitlab::Utils::Override + private def update_user(user) @@ -15,6 +17,11 @@ module Users def action :ban end + + override :track_event + def track_event(user) + experiment(:phone_verification_for_low_risk_users, user: user).track(:banned) + end end end diff --git a/app/services/users/banned_user_base_service.rb b/app/services/users/banned_user_base_service.rb index 74c10581a6e..cec351904a9 100644 --- a/app/services/users/banned_user_base_service.rb +++ b/app/services/users/banned_user_base_service.rb @@ -12,6 +12,7 @@ module Users if update_user(user) log_event(user) + track_event(user) success else messages = user.errors.full_messages @@ -23,6 +24,9 @@ module Users attr_reader :current_user + # Overridden in Users::BanService + def track_event(_); end + def state_error(user) error(_("You cannot %{action} %{state} users." % { action: action.to_s, state: user.state }), :forbidden) end diff --git a/app/services/users/disallow_possible_spam_service.rb b/app/services/users/disallow_possible_spam_service.rb new file mode 100644 index 00000000000..e31ba7ddff0 --- /dev/null +++ b/app/services/users/disallow_possible_spam_service.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Users + class DisallowPossibleSpamService < BaseService + def initialize(current_user) + @current_user = current_user + end + + def execute(user) + user.custom_attributes.by_key(UserCustomAttribute::ALLOW_POSSIBLE_SPAM).delete_all + end + end +end diff --git a/app/services/web_hook_service.rb b/app/services/web_hook_service.rb index 9ab6fcc9832..6837bc47035 100644 --- a/app/services/web_hook_service.rb +++ b/app/services/web_hook_service.rb @@ -189,6 +189,7 @@ class WebHookService 'Content-Type' => 'application/json', 'User-Agent' => "GitLab/#{Gitlab::VERSION}", Gitlab::WebHooks::GITLAB_EVENT_HEADER => self.class.hook_to_event(hook_name), + Gitlab::WebHooks::GITLAB_UUID_HEADER => SecureRandom.uuid, Gitlab::WebHooks::GITLAB_INSTANCE_HEADER => Gitlab.config.gitlab.base_url } diff --git a/app/services/work_items/export_csv_service.rb b/app/services/work_items/export_csv_service.rb index ee20a2832ce..74bc1f526bf 100644 --- a/app/services/work_items/export_csv_service.rb +++ b/app/services/work_items/export_csv_service.rb @@ -28,7 +28,7 @@ module WorkItems 'Type' => ->(work_item) { work_item.work_item_type.name }, 'Author' => 'author_name', 'Author Username' => ->(work_item) { work_item.author.username }, - 'Created At (UTC)' => ->(work_item) { work_item.created_at.to_s(:csv) } + 'Created At (UTC)' => ->(work_item) { work_item.created_at.to_fs(:csv) } } end |