diff options
Diffstat (limited to 'app/models')
140 files changed, 1270 insertions, 862 deletions
diff --git a/app/models/ability.rb b/app/models/ability.rb index d8510524c1f..b8433191d84 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -166,3 +166,5 @@ class Ability end end end + +Ability.prepend_mod_with('AbilityPrepend') diff --git a/app/models/abuse/reports/user_mention.rb b/app/models/abuse/reports/user_mention.rb new file mode 100644 index 00000000000..e8091089ede --- /dev/null +++ b/app/models/abuse/reports/user_mention.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module Abuse + module Reports + class UserMention < UserMention + self.table_name = 'abuse_report_user_mentions' + + belongs_to :abuse_report, optional: false + belongs_to :note, optional: false + end + end +end diff --git a/app/models/abuse_report.rb b/app/models/abuse_report.rb index bf25c539830..872dedf07b1 100644 --- a/app/models/abuse_report.rb +++ b/app/models/abuse_report.rb @@ -6,6 +6,8 @@ class AbuseReport < ApplicationRecord include Gitlab::FileTypeDetection include WithUploads include Gitlab::Utils::StrongMemoize + include Mentionable + include Noteable MAX_CHAR_LIMIT_URL = 512 MAX_FILE_SIZE = 1.megabyte @@ -23,6 +25,9 @@ class AbuseReport < ApplicationRecord has_many :abuse_events, class_name: 'Abuse::Event', inverse_of: :abuse_report + has_many :notes, as: :noteable + has_many :user_mentions, class_name: 'Abuse::Reports::UserMention' + validates :reporter, presence: true, on: :create validates :user, presence: true, on: :create validates :message, presence: true @@ -158,6 +163,10 @@ class AbuseReport < ApplicationRecord Project.find_by_full_path(route_hash.values_at(:namespace_id, :project_id).join('/')) end + def group + Group.find_by_full_path(route_hash[:group_id]) + end + def route_hash match = Rails.application.routes.recognize_path(reported_from_url) return {} if match[:unmatched_route].present? @@ -200,7 +209,7 @@ class AbuseReport < ApplicationRecord format(_('contains URLs that exceed the %{limit} character limit'), limit: MAX_CHAR_LIMIT_URL) ) end - rescue ::Gitlab::UrlBlocker::BlockedUrlError + rescue ::Gitlab::HTTP_V2::UrlBlocker::BlockedUrlError errors.add(:links_to_spam, _('only supports valid HTTP(S) URLs')) end diff --git a/app/models/achievements/user_achievement.rb b/app/models/achievements/user_achievement.rb index 08ebadaa6b0..8b15b25c183 100644 --- a/app/models/achievements/user_achievement.rb +++ b/app/models/achievements/user_achievement.rb @@ -15,6 +15,23 @@ module Achievements optional: true scope :not_revoked, -> { where(revoked_by_user_id: nil) } + scope :order_by_priority_asc, -> { + keyset_order = Gitlab::Pagination::Keyset::Order.build([ + Gitlab::Pagination::Keyset::ColumnOrderDefinition.new( + attribute_name: 'priority', + order_expression: ::Achievements::UserAchievement.arel_table[:priority].asc, + nullable: :nulls_last, + distinct: false + ), + Gitlab::Pagination::Keyset::ColumnOrderDefinition.new( + attribute_name: 'id', + order_expression: ::Achievements::UserAchievement.arel_table[:id].asc, + nullable: :not_nullable, + distinct: true + ) + ]) + reorder(keyset_order) + } scope :order_by_id_asc, -> { order(id: :asc) } def revoked? diff --git a/app/models/analytics/cycle_analytics/issue_stage_event.rb b/app/models/analytics/cycle_analytics/issue_stage_event.rb index 837eb35c839..1a8f1b7c84a 100644 --- a/app/models/analytics/cycle_analytics/issue_stage_event.rb +++ b/app/models/analytics/cycle_analytics/issue_stage_event.rb @@ -17,13 +17,44 @@ module Analytics where(condition.arel.exists) end - def self.issuable_id_column - :issue_id - end + class << self + def project_column + :project_id + end + + def issuable_id_column + :issue_id + end + + def issuable_model + ::Issue + end + + def select_columns + [ + *super, + issuable_model.arel_table[:weight], + issuable_model.arel_table[:sprint_id] + ] + end + + def column_list + [ + *super, + :weight, + :sprint_id + ] + end - def self.issuable_model - ::Issue + def insert_column_list + [ + *super, + :weight, + :sprint_id + ] + end end end end end +Analytics::CycleAnalytics::IssueStageEvent.prepend_mod_with('Analytics::CycleAnalytics::IssueStageEvent') diff --git a/app/models/analytics/cycle_analytics/merge_request_stage_event.rb b/app/models/analytics/cycle_analytics/merge_request_stage_event.rb index 0dfa322b2c3..7f85d284034 100644 --- a/app/models/analytics/cycle_analytics/merge_request_stage_event.rb +++ b/app/models/analytics/cycle_analytics/merge_request_stage_event.rb @@ -17,6 +17,10 @@ module Analytics where(condition.arel.exists) end + def self.project_column + :target_project_id + end + def self.issuable_id_column :merge_request_id end diff --git a/app/models/analytics/cycle_analytics/value_stream.rb b/app/models/analytics/cycle_analytics/value_stream.rb index 16446a5b463..7f8c6eef704 100644 --- a/app/models/analytics/cycle_analytics/value_stream.rb +++ b/app/models/analytics/cycle_analytics/value_stream.rb @@ -51,3 +51,4 @@ module Analytics end end end +Analytics::CycleAnalytics::ValueStream.prepend_mod_with('Analytics::CycleAnalytics::ValueStream') diff --git a/app/models/application_record.rb b/app/models/application_record.rb index 7058bfd5650..15e44296635 100644 --- a/app/models/application_record.rb +++ b/app/models/application_record.rb @@ -6,7 +6,7 @@ class ApplicationRecord < ActiveRecord::Base include LegacyBulkInsert include CrossDatabaseModification include SensitiveSerializableHash - include ResetOnUnionError + include ResetOnColumnErrors self.abstract_class = true diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 153257636ba..824a2bd9fa4 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -16,12 +16,6 @@ class ApplicationSetting < MainClusterwide::ApplicationRecord ignore_columns %i[instance_administration_project_id instance_administrators_group_id], remove_with: '16.2', remove_after: '2023-06-22' ignore_column :database_apdex_settings, remove_with: '16.4', remove_after: '2023-08-22' - ignore_columns %i[ - dashboard_notification_limit - dashboard_enforcement_limit - dashboard_limit_new_namespace_creation_enforcement_date - ], remove_with: '16.5', remove_after: '2023-08-22' - ignore_column %i[ relay_state_domain_allowlist in_product_marketing_emails_enabled @@ -36,7 +30,7 @@ class ApplicationSetting < MainClusterwide::ApplicationRecord jitsu_project_xid jitsu_administrator_email ], remove_with: '16.5', remove_after: '2023-09-22' - ignore_columns %i[ai_access_token ai_access_token_iv], remove_with: '16.6', remove_after: '2023-10-22' + ignore_columns %i[encrypted_ai_access_token encrypted_ai_access_token_iv], remove_with: '16.6', remove_after: '2023-10-22' INSTANCE_REVIEW_MIN_USERS = 50 GRAFANA_URL_ERROR_MESSAGE = 'Please check your Grafana URL setting in ' \ @@ -122,6 +116,10 @@ class ApplicationSetting < MainClusterwide::ApplicationRecord validates :default_branch_protection_defaults, json_schema: { filename: 'default_branch_protection_defaults' } validates :default_branch_protection_defaults, bytesize: { maximum: -> { DEFAULT_BRANCH_PROTECTIONS_DEFAULT_MAX_SIZE } } + validates :failed_login_attempts_unlock_period_in_minutes, + allow_nil: true, + numericality: { only_integer: true, greater_than: 0 } + validates :grafana_url, system_hook_url: ADDRESSABLE_URL_VALIDATION_OPTIONS.merge({ blocked_message: "is blocked: %{exception_message}. #{GRAFANA_URL_ERROR_MESSAGE}" @@ -269,6 +267,10 @@ class ApplicationSetting < MainClusterwide::ApplicationRecord presence: true, numericality: { only_integer: true, greater_than_or_equal_to: 0 } + validates :max_login_attempts, + allow_nil: true, + numericality: { only_integer: true, greater_than: 0 } + validates :max_pages_size, presence: true, numericality: { @@ -311,7 +313,7 @@ class ApplicationSetting < MainClusterwide::ApplicationRecord if: :auto_devops_enabled? validates :enabled_git_access_protocol, - inclusion: { in: %w[ssh http], allow_blank: true } + inclusion: { in: ->(_) { enabled_git_access_protocol_values }, allow_blank: true } validates :domain_denylist, presence: { message: 'Domain denylist cannot be empty if denylist is enabled.' }, @@ -657,6 +659,7 @@ class ApplicationSetting < MainClusterwide::ApplicationRecord validates :throttle_authenticated_deprecated_api_period_in_seconds validates :throttle_protected_paths_requests_per_period validates :throttle_protected_paths_period_in_seconds + validates :project_jobs_api_rate_limit end with_options(numericality: { only_integer: true, greater_than_or_equal_to: 0 }) do @@ -805,11 +808,12 @@ class ApplicationSetting < MainClusterwide::ApplicationRecord attr_encrypted :openai_api_key, encryption_options_base_32_aes_256_gcm.merge(encode: false, encode_iv: false) attr_encrypted :anthropic_api_key, encryption_options_base_32_aes_256_gcm.merge(encode: false, encode_iv: false) attr_encrypted :vertex_ai_credentials, encryption_options_base_32_aes_256_gcm.merge(encode: false, encode_iv: false) + attr_encrypted :vertex_ai_access_token, encryption_options_base_32_aes_256_gcm.merge(encode: false, encode_iv: false) # Restricting the validation to `on: :update` only to avoid cyclical dependencies with # License <--> ApplicationSetting. This method calls a license check when we create # ApplicationSetting from defaults which in turn depends on ApplicationSetting record. - # The currect default is defined in the `defaults` method so we don't need to validate + # The correct default is defined in the `defaults` method so we don't need to validate # it here. validates :disable_feed_token, inclusion: { in: [true, false], message: N_('must be a boolean value') }, on: :update @@ -834,6 +838,9 @@ class ApplicationSetting < MainClusterwide::ApplicationRecord allow_nil: false, inclusion: { in: [true, false], message: N_('must be a boolean value') } + validates :math_rendering_limits_enabled, + inclusion: { in: [true, false], message: N_('must be a boolean value') } + before_validation :ensure_uuid! before_validation :coerce_repository_storages_weighted, if: :repository_storages_weighted_changed? before_validation :normalize_default_branch_name @@ -958,19 +965,31 @@ class ApplicationSetting < MainClusterwide::ApplicationRecord false end + def max_login_attempts_column_exists? + self.class.database.cached_column_exists?(:max_login_attempts) + end + + def failed_login_attempts_unlock_period_in_minutes_column_exists? + self.class.database.cached_column_exists?(:failed_login_attempts_unlock_period_in_minutes) + end + private def self.human_attribute_name(attribute, *options) HUMANIZED_ATTRIBUTES[attribute.to_sym] || super end + def self.enabled_git_access_protocol_values + %w[ssh http] + end + def parsed_grafana_url @parsed_grafana_url ||= Gitlab::Utils.parse_url(grafana_url) end def parsed_kroki_url @parsed_kroki_url ||= Gitlab::UrlBlocker.validate!(kroki_url, schemes: %w[http https], enforce_sanitization: true)[0] - rescue Gitlab::UrlBlocker::BlockedUrlError => e + rescue Gitlab::HTTP_V2::UrlBlocker::BlockedUrlError => e self.errors.add( :kroki_url, "is not valid. #{e}" diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb index 5a90e246499..1bd15a56de5 100644 --- a/app/models/application_setting_implementation.rb +++ b/app/models/application_setting_implementation.rb @@ -51,6 +51,7 @@ module ApplicationSettingImplementation container_registry_token_expire_delay: 5, container_registry_vendor: '', container_registry_version: '', + container_registry_db_enabled: false, custom_http_clone_url_root: nil, decompress_archive_file_timeout: 210, default_artifacts_expire_in: '30 days', @@ -87,6 +88,7 @@ module ApplicationSettingImplementation external_pipeline_validation_service_timeout: nil, external_pipeline_validation_service_token: nil, external_pipeline_validation_service_url: nil, + failed_login_attempts_unlock_period_in_minutes: nil, first_day_of_week: 0, floc_enabled: false, gitaly_timeout_default: 55, @@ -117,12 +119,14 @@ module ApplicationSettingImplementation login_recaptcha_protection_enabled: false, mailgun_signing_key: nil, mailgun_events_enabled: false, + math_rendering_limits_enabled: true, max_artifacts_size: Settings.artifacts['max_size'], max_attachment_size: Settings.gitlab['max_attachment_size'], + max_decompressed_archive_size: 25600, max_export_size: 0, max_import_size: 0, max_import_remote_file_size: 10240, - max_decompressed_archive_size: 25600, + max_login_attempts: nil, max_terraform_state_size_bytes: 0, max_yaml_size_bytes: 1.megabyte, max_yaml_depth: 100, @@ -267,7 +271,8 @@ module ApplicationSettingImplementation gitlab_dedicated_instance: false, ci_max_includes: 150, allow_account_deletion: true, - gitlab_shell_operation_limit: 600 + gitlab_shell_operation_limit: 600, + project_jobs_api_rate_limit: 600 }.tap do |hsh| hsh.merge!(non_production_defaults) unless Rails.env.production? end diff --git a/app/models/approval.rb b/app/models/approval.rb index ecc15077c8d..c3992994dd3 100644 --- a/app/models/approval.rb +++ b/app/models/approval.rb @@ -14,4 +14,7 @@ class Approval < ApplicationRecord validates :user_id, presence: true, uniqueness: { scope: [:merge_request_id] } scope :with_user, -> { joins(:user) } + scope :with_invalid_patch_id_sha, ->(patch_id_sha) do + where.not(patch_id_sha: patch_id_sha).or(where(patch_id_sha: nil)) + end end diff --git a/app/models/award_emoji.rb b/app/models/award_emoji.rb index 73e3fa709b0..e445d08a096 100644 --- a/app/models/award_emoji.rb +++ b/app/models/award_emoji.rb @@ -9,7 +9,7 @@ class AwardEmoji < ApplicationRecord include Importable include IgnorableColumns - ignore_column :awardable_id_convert_to_bigint, remove_with: '16.2', remove_after: '2023-07-22' + ignore_column :awardable_id_convert_to_bigint, remove_with: '16.7', remove_after: '2023-11-16' belongs_to :awardable, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations belongs_to :user diff --git a/app/models/badges/group_badge.rb b/app/models/badges/group_badge.rb index c0712f452df..f74c9f89e9f 100644 --- a/app/models/badges/group_badge.rb +++ b/app/models/badges/group_badge.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class GroupBadge < Badge + include EachBatch + belongs_to :group validates :group, presence: true diff --git a/app/models/bulk_import.rb b/app/models/bulk_import.rb index fde528e3fa0..a7ace7429d7 100644 --- a/app/models/bulk_import.rb +++ b/app/models/bulk_import.rb @@ -76,4 +76,8 @@ class BulkImport < ApplicationRecord def supports_batched_export? source_version_info >= self.class.min_gl_version_for_migration_in_batches end + + def completed? + finished? || failed? || timeout? + end end diff --git a/app/models/bulk_imports/tracker.rb b/app/models/bulk_imports/tracker.rb index d1a6f3b9a80..d9efd489af5 100644 --- a/app/models/bulk_imports/tracker.rb +++ b/app/models/bulk_imports/tracker.rb @@ -33,11 +33,9 @@ class BulkImports::Tracker < ApplicationRecord entity_scope.where(stage: next_stage_scope).with_status(:created) } - def self.stage_running?(entity_id, stage) - where(stage: stage, bulk_import_entity_id: entity_id) - .with_status(:created, :enqueued, :started) - .exists? - end + scope :running_trackers, -> (entity_id) { + where(bulk_import_entity_id: entity_id).with_status(:enqueued, :started) + } def pipeline_class unless entity.pipeline_exists?(pipeline_name) diff --git a/app/models/chat_name.rb b/app/models/chat_name.rb index d3fbfe3aa55..38e6273bf20 100644 --- a/app/models/chat_name.rb +++ b/app/models/chat_name.rb @@ -27,6 +27,6 @@ class ChatName < ApplicationRecord end def update_last_used_at? - last_used_at.nil? || last_used_at > LAST_USED_AT_INTERVAL.ago + last_used_at.nil? || last_used_at.before?(LAST_USED_AT_INTERVAL.ago) end end diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 2abb8e4be48..d2cf9058976 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -11,6 +11,7 @@ module Ci include Importable include Ci::HasRef include Ci::TrackEnvironmentUsage + include EachBatch extend ::Gitlab::Utils::Override @@ -414,7 +415,7 @@ module Ci end def options_scheduled_at - ChronicDuration.parse(options[:start_in], use_complete_matcher: true)&.seconds&.from_now + ChronicDuration.parse(options[:start_in])&.seconds&.from_now end def action? @@ -738,7 +739,7 @@ module Ci def artifacts_expire_in=(value) self.artifacts_expire_at = if value - ChronicDuration.parse(value, use_complete_matcher: true)&.seconds&.from_now + ChronicDuration.parse(value)&.seconds&.from_now end end @@ -1090,7 +1091,7 @@ module Ci end def has_expiring_artifacts? - artifacts_expire_at.present? && artifacts_expire_at > Time.current + artifacts_expire_at.present? && artifacts_expire_at.future? end def job_jwt_variables diff --git a/app/models/ci/build_metadata.rb b/app/models/ci/build_metadata.rb index 4c723bb7c0c..555565ff621 100644 --- a/app/models/ci/build_metadata.rb +++ b/app/models/ci/build_metadata.rb @@ -14,7 +14,7 @@ module Ci self.table_name = 'p_ci_builds_metadata' self.primary_key = 'id' - partitionable scope: :build + partitionable scope: :build, partitioned: true belongs_to :build, class_name: 'CommitStatus' belongs_to :project diff --git a/app/models/ci/build_need.rb b/app/models/ci/build_need.rb index 00241908644..1831b7868f9 100644 --- a/app/models/ci/build_need.rb +++ b/app/models/ci/build_need.rb @@ -7,7 +7,7 @@ module Ci include SafelyChangeColumnDefault include BulkInsertSafe - MAX_JOB_NAME_LENGTH = 128 + MAX_JOB_NAME_LENGTH = 255 columns_changing_default :partition_id diff --git a/app/models/ci/catalog/components_project.rb b/app/models/ci/catalog/components_project.rb new file mode 100644 index 00000000000..2bc33a6f050 --- /dev/null +++ b/app/models/ci/catalog/components_project.rb @@ -0,0 +1,94 @@ +# frozen_string_literal: true + +module Ci + module Catalog + class ComponentsProject + # ComponentsProject is a type of Catalog Resource which contains one or more + # CI/CD components. + # It is responsible for retrieving the data of a component file, including the content, name, and file path. + + TEMPLATE_FILE = 'template.yml' + TEMPLATES_DIR = 'templates' + TEMPLATE_PATH_REGEX = '^templates\/\w+\-?\w+(?:\/template)?\.yml$' + + ComponentData = Struct.new(:content, :path, keyword_init: true) + + def initialize(project, sha = project&.default_branch) + @project = project + @sha = sha + end + + def fetch_component_paths(sha) + project.repository.search_files_by_regexp(TEMPLATE_PATH_REGEX, sha) + end + + def extract_component_name(path) + return unless path.match?(TEMPLATE_PATH_REGEX) + + dirname = File.dirname(path) + filename = File.basename(path, '.*') + + if dirname == TEMPLATES_DIR + filename + else + File.basename(dirname) + end + end + + def extract_inputs(blob) + result = Gitlab::Ci::Config::Yaml::Loader.new(blob).load_uninterpolated_yaml + + raise result.error_class, result.error unless result.valid? + + result.inputs + end + + def fetch_component(component_name) + path = simple_template_path(component_name) + content = fetch_content(path) + + if content.nil? + path = complex_template_path(component_name) + content = fetch_content(path) + end + + if content.nil? + path = legacy_template_path(component_name) + content = fetch_content(path) + end + + ComponentData.new(content: content, path: path) + end + + private + + attr_reader :project, :sha + + def fetch_content(component_path) + project.repository.blob_data_at(sha, component_path) + end + + # A simple template consists of a single file + def simple_template_path(component_name) + # TODO: Extract this line and move to fetch_content once we remove legacy fetching + return unless component_name.index('/').nil? + + File.join(TEMPLATES_DIR, "#{component_name}.yml") + end + + # A complex template is directory-based and may consist of multiple files. + # Given a path like "my-org/sub-group/the-project/templates/component" + # returns the entry point path: "templates/component/template.yml". + def complex_template_path(component_name) + # TODO: Extract this line and move to fetch_content once we remove legacy fetching + return unless component_name.index('/').nil? + + File.join(TEMPLATES_DIR, component_name, TEMPLATE_FILE) + end + + def legacy_template_path(component_name) + File.join(component_name, TEMPLATE_FILE).delete_prefix('/') + end + end + end +end diff --git a/app/models/ci/catalog/listing.rb b/app/models/ci/catalog/listing.rb index 1cb030c67c3..c3b18af8c3f 100644 --- a/app/models/ci/catalog/listing.rb +++ b/app/models/ci/catalog/listing.rb @@ -18,6 +18,8 @@ module Ci case sort.to_s when 'name_desc' then all_resources.order_by_name_desc when 'name_asc' then all_resources.order_by_name_asc + when 'latest_released_at_desc' then all_resources.order_by_latest_released_at_desc + when 'latest_released_at_asc' then all_resources.order_by_latest_released_at_asc else all_resources.order_by_created_at_desc end diff --git a/app/models/ci/catalog/resource.rb b/app/models/ci/catalog/resource.rb index 799cdce4af7..8ffc0292a69 100644 --- a/app/models/ci/catalog/resource.rb +++ b/app/models/ci/catalog/resource.rb @@ -18,6 +18,8 @@ module Ci scope :order_by_created_at_desc, -> { reorder(created_at: :desc) } scope :order_by_name_desc, -> { joins(:project).merge(Project.sorted_by_name_desc) } scope :order_by_name_asc, -> { joins(:project).merge(Project.sorted_by_name_asc) } + scope :order_by_latest_released_at_desc, -> { reorder(arel_table[:latest_released_at].desc.nulls_last) } + scope :order_by_latest_released_at_asc, -> { reorder(arel_table[:latest_released_at].asc.nulls_last) } delegate :avatar_path, :description, :name, :star_count, :forks_count, to: :project diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb index 3f9d8f07b06..2a346f97958 100644 --- a/app/models/ci/job_artifact.rb +++ b/app/models/ci/job_artifact.rb @@ -310,7 +310,7 @@ module Ci end def expiring? - expire_at.present? && expire_at > Time.current + expire_at.present? && expire_at.future? end def expire_in diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 5bf4e846304..0a876d26cc9 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -1366,6 +1366,11 @@ module Ci merge_request.merge_request_diff_for(merge_request_diff_sha) end + def reduced_build_attributes_list_for_rules? + ::Feature.enabled?(:reduced_build_attributes_list_for_rules, project) + end + strong_memoize_attr :reduced_build_attributes_list_for_rules? + private def add_message(severity, content) diff --git a/app/models/ci/ref.rb b/app/models/ci/ref.rb index 199e1cd07e7..8655e8eb9b8 100644 --- a/app/models/ci/ref.rb +++ b/app/models/ci/ref.rb @@ -36,7 +36,7 @@ module Ci next unless ci_ref.artifacts_locked? ci_ref.run_after_commit do - Ci::PipelineSuccessUnlockArtifactsWorker.perform_async(ci_ref.last_finished_pipeline_id) + Ci::Refs::UnlockPreviousPipelinesWorker.perform_async(ci_ref.id) end end end @@ -52,7 +52,11 @@ module Ci end def last_finished_pipeline_id - Ci::Pipeline.last_finished_for_ref_id(self.id)&.id + last_finished_pipeline&.id + end + + def last_finished_pipeline + Ci::Pipeline.last_finished_for_ref_id(self.id) end def artifacts_locked? diff --git a/app/models/ci/unlock_pipeline_request.rb b/app/models/ci/unlock_pipeline_request.rb new file mode 100644 index 00000000000..c8fc82f3e55 --- /dev/null +++ b/app/models/ci/unlock_pipeline_request.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +module Ci + class UnlockPipelineRequest + QUEUE_REDIS_KEY = 'ci_unlock_pipeline_requests:queue' + + def self.enqueue(pipeline_id) + unix_timestamp = Time.current.utc.to_i + pipeline_ids = Array(pipeline_id).uniq + pipeline_ids_with_scores = pipeline_ids.map do |id| + # The order of values per pair is `[score, key]`, so in this case, the unix timestamp is the score. + # By default, the sort order of sorted sets is from lowest to highest, though this does not matter much + # because we use `ZPOPMIN` to make sure to return the lowest/oldest request in terms of unix timestamp score. + [unix_timestamp, id] + end + + with_redis do |redis| + added = redis.zadd(QUEUE_REDIS_KEY, pipeline_ids_with_scores, nx: true) + log_event(:enqueued, pipeline_ids) if added > 0 + added + end + end + + def self.next! + with_redis do |redis| + pipeline_id, enqueue_timestamp = redis.zpopmin(QUEUE_REDIS_KEY) + break unless pipeline_id + + pipeline_id = pipeline_id.to_i + log_event(:picked_next, pipeline_id) + + [pipeline_id, enqueue_timestamp.to_i] + end + end + + def self.total_pending + with_redis do |redis| + redis.zcard(QUEUE_REDIS_KEY) + end + end + + def self.with_redis(&block) + Gitlab::Redis::SharedState.with(&block) + end + + def self.log_event(event, pipeline_id) + Gitlab::AppLogger.info( + message: "Pipeline unlock - #{event}", + pipeline_id: pipeline_id + ) + end + end +end diff --git a/app/models/clusters/agent_token.rb b/app/models/clusters/agent_token.rb index f4c497a42cc..e2754db73b9 100644 --- a/app/models/clusters/agent_token.rb +++ b/app/models/clusters/agent_token.rb @@ -33,6 +33,10 @@ module Clusters revoked: 1 } + def revoke! + update(status: :revoked) + end + def to_ability_name :cluster end diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb index f9a34959675..5bd55fd6f4c 100644 --- a/app/models/clusters/cluster.rb +++ b/app/models/clusters/cluster.rb @@ -24,7 +24,6 @@ module Clusters has_one :cluster_project, -> { order(id: :desc) }, class_name: 'Clusters::Project' has_many :deployment_clusters has_many :deployments, inverse_of: :cluster, through: :deployment_clusters - has_many :successful_deployments, -> { success }, class_name: 'Deployment' has_many :environments, -> { distinct }, through: :deployments has_many :cluster_groups, class_name: 'Clusters::Group' diff --git a/app/models/clusters/concerns/prometheus_client.rb b/app/models/clusters/concerns/prometheus_client.rb index d2f69b813aa..b4234e9cc0a 100644 --- a/app/models/clusters/concerns/prometheus_client.rb +++ b/app/models/clusters/concerns/prometheus_client.rb @@ -35,7 +35,7 @@ module Clusters def configured? kube_client.present? && available? - rescue Gitlab::UrlBlocker::BlockedUrlError + rescue Gitlab::HTTP_V2::UrlBlocker::BlockedUrlError false end diff --git a/app/models/clusters/platforms/kubernetes.rb b/app/models/clusters/platforms/kubernetes.rb index 5efbec45561..6ae0cd8e3fd 100644 --- a/app/models/clusters/platforms/kubernetes.rb +++ b/app/models/clusters/platforms/kubernetes.rb @@ -12,7 +12,7 @@ module Clusters REQUIRED_K8S_MIN_VERSION = 23 IGNORED_CONNECTION_EXCEPTIONS = [ - Gitlab::UrlBlocker::BlockedUrlError, + Gitlab::HTTP_V2::UrlBlocker::BlockedUrlError, Kubeclient::HttpError, Errno::ECONNREFUSED, URI::InvalidURIError, diff --git a/app/models/commit_user_mention.rb b/app/models/commit_user_mention.rb index 9215e15f07d..fa7f065b6b4 100644 --- a/app/models/commit_user_mention.rb +++ b/app/models/commit_user_mention.rb @@ -3,7 +3,7 @@ class CommitUserMention < UserMention include IgnorableColumns - ignore_column :note_id_convert_to_bigint, remove_with: '16.2', remove_after: '2023-07-22' + ignore_column :note_id_convert_to_bigint, remove_with: '16.7', remove_after: '2023-11-16' belongs_to :note end diff --git a/app/models/concerns/analytics/cycle_analytics/stage_event_model.rb b/app/models/concerns/analytics/cycle_analytics/stage_event_model.rb index d268c32c088..1d9cf5729cd 100644 --- a/app/models/concerns/analytics/cycle_analytics/stage_event_model.rb +++ b/app/models/concerns/analytics/cycle_analytics/stage_event_model.rb @@ -16,6 +16,7 @@ module Analytics scope :start_event_timestamp_before, -> (date) { where(arel_table[:start_event_timestamp].lteq(date)) } scope :authored, ->(user) { where(author_id: user) } scope :with_milestone_id, ->(milestone_id) { where(milestone_id: milestone_id) } + scope :without_milestone_id, -> (milestone_id) { where('milestone_id <> ? or milestone_id IS NULL', milestone_id) } scope :end_event_is_not_happened_yet, -> { where(end_event_timestamp: nil) } scope :order_by_end_event, -> (direction) do # ORDER BY end_event_timestamp, merge_request_id/issue_id, start_event_timestamp @@ -57,45 +58,19 @@ module Analytics class_methods do def upsert_data(data) - upsert_values = data.map do |row| - row.values_at( - :stage_event_hash_id, - :issuable_id, - :group_id, - :project_id, - :milestone_id, - :author_id, - :state_id, - :start_event_timestamp, - :end_event_timestamp - ) - end + upsert_values = data.map { |row| row.values_at(*column_list) } value_list = Arel::Nodes::ValuesList.new(upsert_values).to_sql query = <<~SQL INSERT INTO #{quoted_table_name} ( - stage_event_hash_id, - #{connection.quote_column_name(issuable_id_column)}, - group_id, - project_id, - milestone_id, - author_id, - state_id, - start_event_timestamp, - end_event_timestamp + #{insert_column_list.join(",\n")} ) #{value_list} ON CONFLICT(stage_event_hash_id, #{issuable_id_column}) DO UPDATE SET - group_id = excluded.group_id, - project_id = excluded.project_id, - milestone_id = excluded.milestone_id, - author_id = excluded.author_id, - state_id = excluded.state_id, - start_event_timestamp = excluded.start_event_timestamp, - end_event_timestamp = excluded.end_event_timestamp + #{column_updates.join(",\n")} SQL result = connection.execute(query) @@ -113,6 +88,51 @@ module Analytics def arel_order(arel_node, direction) direction.to_sym == :desc ? arel_node.desc : arel_node.asc end + + def select_columns + [ + issuable_model.arel_table[:id], + issuable_model.arel_table[project_column].as('project_id'), + issuable_model.arel_table[:milestone_id], + issuable_model.arel_table[:author_id], + issuable_model.arel_table[:state_id], + Project.arel_table[:parent_id].as('group_id') + ] + end + + def column_list + [ + :stage_event_hash_id, + :issuable_id, + :group_id, + :project_id, + :milestone_id, + :author_id, + :state_id, + :start_event_timestamp, + :end_event_timestamp + ] + end + + def insert_column_list + [ + :stage_event_hash_id, + connection.quote_column_name(issuable_id_column), + :group_id, + :project_id, + :milestone_id, + :author_id, + :state_id, + :start_event_timestamp, + :end_event_timestamp + ] + end + + def column_updates + insert_column_list.map do |column| + "#{column} = excluded.#{column}" + end + end end end end diff --git a/app/models/concerns/awardable.rb b/app/models/concerns/awardable.rb index e830594af11..22e71c4fa13 100644 --- a/app/models/concerns/awardable.rb +++ b/app/models/concerns/awardable.rb @@ -13,26 +13,26 @@ module Awardable end class_methods do - def awarded(user, name = nil) + def awarded(user, name = nil, base_class_name = base_class.name, awardable_id_column = :id) award_emoji_table = Arel::Table.new('award_emoji') inner_query = award_emoji_table .project('true') .where(award_emoji_table[:user_id].eq(user.id)) - .where(award_emoji_table[:awardable_type].eq(base_class.name)) - .where(award_emoji_table[:awardable_id].eq(self.arel_table[:id])) + .where(award_emoji_table[:awardable_type].eq(base_class_name)) + .where(award_emoji_table[:awardable_id].eq(self.arel_table[awardable_id_column])) inner_query = inner_query.where(award_emoji_table[:name].eq(name)) if name.present? where(inner_query.exists) end - def not_awarded(user, name = nil) + def not_awarded(user, name = nil, base_class_name = base_class.name, awardable_id_column = :id) award_emoji_table = Arel::Table.new('award_emoji') inner_query = award_emoji_table .project('true') .where(award_emoji_table[:user_id].eq(user.id)) - .where(award_emoji_table[:awardable_type].eq(base_class.name)) - .where(award_emoji_table[:awardable_id].eq(self.arel_table[:id])) + .where(award_emoji_table[:awardable_type].eq(base_class_name)) + .where(award_emoji_table[:awardable_id].eq(self.arel_table[awardable_id_column])) inner_query = inner_query.where(award_emoji_table[:name].eq(name)) if name.present? @@ -52,14 +52,14 @@ module Awardable end # Order votes by emoji, optional sort order param `descending` defaults to true - def order_votes(emoji_name, direction) + def order_votes(emoji_name, direction, base_class_name = base_class.name, awardable_id_column = :id) awardable_table = self.arel_table awards_table = AwardEmoji.arel_table join_clause = awardable_table .join(awards_table, Arel::Nodes::OuterJoin) - .on(awards_table[:awardable_id].eq(awardable_table[:id]) - .and(awards_table[:awardable_type].eq(base_class.name).and(awards_table[:name].eq(emoji_name)))) + .on(awards_table[:awardable_id].eq(awardable_table[awardable_id_column]) + .and(awards_table[:awardable_type].eq(base_class_name).and(awards_table[:name].eq(emoji_name)))) .join_sources joins(join_clause).group(awardable_table[:id]).reorder( diff --git a/app/models/concerns/bulk_users_by_email_load.rb b/app/models/concerns/bulk_users_by_email_load.rb index edbd3e21458..55143ead30a 100644 --- a/app/models/concerns/bulk_users_by_email_load.rb +++ b/app/models/concerns/bulk_users_by_email_load.rb @@ -7,7 +7,7 @@ module BulkUsersByEmailLoad def users_by_emails(emails) Gitlab::SafeRequestLoader.execute(resource_key: user_by_email_resource_key, resource_ids: emails) do |emails| # have to consider all emails - even secondary, so use all_emails here - grouped_users_by_email = User.by_any_email(emails).preload(:emails).group_by(&:all_emails) + grouped_users_by_email = User.by_any_email(emails, confirmed: true).preload(:emails).group_by(&:all_emails) grouped_users_by_email.each_with_object({}) do |(found_emails, users), h| found_emails.each { |e| h[e] = users.first if emails.include?(e) } # don't include all emails for an account, only the ones we want diff --git a/app/models/concerns/chronic_duration_attribute.rb b/app/models/concerns/chronic_duration_attribute.rb index 7b7b61fdf06..44b34cf9b2f 100644 --- a/app/models/concerns/chronic_duration_attribute.rb +++ b/app/models/concerns/chronic_duration_attribute.rb @@ -18,7 +18,7 @@ module ChronicDurationAttribute begin new_value = if value.present? - ChronicDuration.parse(value, use_complete_matcher: true).to_i + ChronicDuration.parse(value).to_i else parameters[:default].presence end diff --git a/app/models/concerns/ci/deployable.rb b/app/models/concerns/ci/deployable.rb index d25151f9a34..844c8a1fa7d 100644 --- a/app/models/concerns/ci/deployable.rb +++ b/app/models/concerns/ci/deployable.rb @@ -4,6 +4,7 @@ module Ci module Deployable extend ActiveSupport::Concern + include Gitlab::Utils::StrongMemoize included do prepend_mod_with('Ci::Deployable') # rubocop: disable Cop/InjectEnterpriseEditionModule @@ -17,8 +18,16 @@ module Ci end end + after_transition any => [:failed] do |job| + next unless job.stops_environment? + + job.run_after_commit do + Environments::StopJobFailedWorker.perform_async(id) + end + end + # Synchronize Deployment Status - # Please note that the data integirty is not assured because we can't use + # Please note that the data integrity is not assured because we can't use # a database transaction due to DB decomposition. after_transition do |job, transition| next if transition.loopback? @@ -32,13 +41,12 @@ module Ci end def outdated_deployment? - strong_memoize(:outdated_deployment) do - deployment_job? && - project.ci_forward_deployment_enabled? && - (!project.ci_forward_deployment_rollback_allowed? || incomplete?) && - deployment&.older_than_last_successful_deployment? - end + deployment_job? && + project.ci_forward_deployment_enabled? && + (!project.ci_forward_deployment_rollback_allowed? || incomplete?) && + deployment&.older_than_last_successful_deployment? end + strong_memoize_attr :outdated_deployment? # Virtual deployment status depending on the environment status. def deployment_status @@ -106,10 +114,10 @@ module Ci namespace = options.dig(:environment, :kubernetes, :namespace) - if namespace.present? # rubocop:disable Style/GuardClause - strong_memoize(:expanded_kubernetes_namespace) do - ExpandVariables.expand(namespace, -> { simple_variables }) - end + return unless namespace.present? + + strong_memoize(:expanded_kubernetes_namespace) do + ExpandVariables.expand(namespace, -> { simple_variables }) end end @@ -146,12 +154,11 @@ module Ci end def environment_status - strong_memoize(:environment_status) do - if has_environment_keyword? && merge_request - EnvironmentStatus.new(project, persisted_environment, merge_request, pipeline.sha) - end - end + return unless has_environment_keyword? && merge_request + + EnvironmentStatus.new(project, persisted_environment, merge_request, pipeline.sha) end + strong_memoize_attr :environment_status def on_stop options&.dig(:environment, :on_stop) diff --git a/app/models/concerns/enums/issuable_link.rb b/app/models/concerns/enums/issuable_link.rb new file mode 100644 index 00000000000..ca5728c2600 --- /dev/null +++ b/app/models/concerns/enums/issuable_link.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module Enums + module IssuableLink + TYPE_RELATES_TO = 'relates_to' + TYPE_BLOCKS = 'blocks' + + def self.link_types + { TYPE_RELATES_TO => 0, TYPE_BLOCKS => 1 } + end + end +end diff --git a/app/models/concerns/import_state/sidekiq_job_tracker.rb b/app/models/concerns/import_state/sidekiq_job_tracker.rb index b7d0ed0f51b..9c892acb158 100644 --- a/app/models/concerns/import_state/sidekiq_job_tracker.rb +++ b/app/models/concerns/import_state/sidekiq_job_tracker.rb @@ -19,7 +19,7 @@ module ImportState end def self.jid_by(project_id:, status:) - select(:jid).where(status: status).find_by(project_id: project_id) + select(:id, :jid).where(status: status).find_by(project_id: project_id) end end end diff --git a/app/models/concerns/integrations/enable_ssl_verification.rb b/app/models/concerns/integrations/enable_ssl_verification.rb index 9735a9bf5f6..cb20955488a 100644 --- a/app/models/concerns/integrations/enable_ssl_verification.rb +++ b/app/models/concerns/integrations/enable_ssl_verification.rb @@ -5,7 +5,11 @@ module Integrations extend ActiveSupport::Concern prepended do - boolean_accessor :enable_ssl_verification + field :enable_ssl_verification, + type: :checkbox, + title: -> { s_('Integrations|SSL verification') }, + checkbox_label: -> { s_('Integrations|Enable SSL verification') }, + help: -> { s_('Integrations|Clear if using a self-signed certificate.') } end def initialize_properties @@ -17,18 +21,11 @@ module Integrations def fields super.tap do |fields| url_index = fields.index { |field| field[:name].ends_with?('_url') } - insert_index = url_index ? url_index + 1 : -1 + insert_index = url_index || -1 - fields.insert(insert_index, - Field.new( - name: 'enable_ssl_verification', - integration_class: self, - type: :checkbox, - title: s_('Integrations|SSL verification'), - checkbox_label: s_('Integrations|Enable SSL verification'), - help: s_('Integrations|Clear if using a self-signed certificate.') - ) - ) + enable_ssl_verification_index = fields.index { |field| field[:name] == 'enable_ssl_verification' } + + fields.insert(insert_index, fields.delete_at(enable_ssl_verification_index)) end end end diff --git a/app/models/concerns/issuable_link.rb b/app/models/concerns/issuable_link.rb index e884e5acecf..dcd2705185f 100644 --- a/app/models/concerns/issuable_link.rb +++ b/app/models/concerns/issuable_link.rb @@ -9,8 +9,8 @@ module IssuableLink extend ActiveSupport::Concern - TYPE_RELATES_TO = 'relates_to' - TYPE_BLOCKS = 'blocks' ## EE-only. Kept here to be used on link_type enum. + MAX_LINKS_COUNT = 100 + TYPE_RELATES_TO = Enums::IssuableLink::TYPE_RELATES_TO class_methods do def inverse_link_type(type) @@ -38,10 +38,11 @@ module IssuableLink validates :source, uniqueness: { scope: :target_id, message: 'is already related' } validate :check_self_relation validate :check_opposite_relation + validate :validate_max_number_of_links, on: :create scope :for_source_or_target, ->(issuable) { where(source: issuable).or(where(target: issuable)) } - enum link_type: { TYPE_RELATES_TO => 0, TYPE_BLOCKS => 1 } + enum link_type: Enums::IssuableLink.link_types private @@ -60,6 +61,27 @@ module IssuableLink errors.add(:source, "is already related to this #{self.class.issuable_name}") end end + + def validate_max_number_of_links + return unless source && target + + validate_max_number_of_links_for(source, :source) + validate_max_number_of_links_for(target, :target) + end + + def validate_max_number_of_links_for(item, attribute_name) + return unless item.linked_items_count >= MAX_LINKS_COUNT + + errors.add( + attribute_name, + format( + s_('This %{issuable} would exceed the maximum number of linked %{issuables} (%{limit}).'), + issuable: self.class.issuable_name, + issuables: self.class.issuable_name.pluralize, + limit: MAX_LINKS_COUNT + ) + ) + end end end diff --git a/app/models/concerns/noteable.rb b/app/models/concerns/noteable.rb index 06cee46645b..971089edc45 100644 --- a/app/models/concerns/noteable.rb +++ b/app/models/concerns/noteable.rb @@ -12,12 +12,12 @@ module Noteable class_methods do # `Noteable` class names that support replying to individual notes. def replyable_types - %w[Issue MergeRequest] + %w[Issue MergeRequest AbuseReport] end # `Noteable` class names that support resolvable notes. def resolvable_types - %w[Issue MergeRequest DesignManagement::Design] + %w[Issue MergeRequest DesignManagement::Design AbuseReport] end # `Noteable` class names that support creating/forwarding individual notes. diff --git a/app/models/concerns/protected_ref_access.rb b/app/models/concerns/protected_ref_access.rb index f0bb1cc359b..a5994b538ce 100644 --- a/app/models/concerns/protected_ref_access.rb +++ b/app/models/concerns/protected_ref_access.rb @@ -71,6 +71,8 @@ module ProtectedRefAccess return false if current_user.nil? || no_access? return current_user.admin? if admin_access? + return false if Feature.enabled?(:check_membership_in_protected_ref_access) && !project.member?(current_user) + yield if block_given? user_can_access?(current_user) diff --git a/app/models/concerns/repository_storage_movable.rb b/app/models/concerns/repository_storage_movable.rb index 87ff413f2c1..77edabb9706 100644 --- a/app/models/concerns/repository_storage_movable.rb +++ b/app/models/concerns/repository_storage_movable.rb @@ -49,6 +49,7 @@ module RepositoryStorageMovable begin storage_move.container.set_repository_read_only!(skip_git_transfer_check: true) rescue StandardError => e + storage_move.do_fail! storage_move.add_error(e.message) next false end diff --git a/app/models/concerns/reset_on_column_errors.rb b/app/models/concerns/reset_on_column_errors.rb new file mode 100644 index 00000000000..8ace52ebff5 --- /dev/null +++ b/app/models/concerns/reset_on_column_errors.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +module ResetOnColumnErrors + extend ActiveSupport::Concern + + MAX_RESET_PERIOD = 10.minutes + + included do |base| + base.rescue_from ActiveRecord::StatementInvalid, with: :reset_on_union_error + base.rescue_from ActiveModel::UnknownAttributeError, with: :reset_on_unknown_attribute_error + + base.class_attribute :previous_reset_columns_from_error + end + + class_methods do + def do_reset(exception) + class_to_be_reset = base_class + + class_to_be_reset.reset_column_information + Gitlab::ErrorTracking.log_exception(exception, { reset_model_name: class_to_be_reset.name }) + + class_to_be_reset.previous_reset_columns_from_error = Time.current + end + + def reset_on_union_error(exception) + if exception.message.include?("each UNION query must have the same number of columns") && should_reset? + do_reset(exception) + end + + raise + end + + def should_reset? + return false if base_class.previous_reset_columns_from_error? && + base_class.previous_reset_columns_from_error > MAX_RESET_PERIOD.ago + + Feature.enabled?(:reset_column_information_on_statement_invalid, type: :ops) + end + end + + def reset_on_union_error(exception) + self.class.reset_on_union_error(exception) + end + + def reset_on_unknown_attribute_error(exception) + self.class.do_reset(exception) if self.class.should_reset? + + raise + end +end diff --git a/app/models/concerns/reset_on_union_error.rb b/app/models/concerns/reset_on_union_error.rb deleted file mode 100644 index 42e350b0bed..00000000000 --- a/app/models/concerns/reset_on_union_error.rb +++ /dev/null @@ -1,37 +0,0 @@ -# frozen_string_literal: true - -module ResetOnUnionError - extend ActiveSupport::Concern - - MAX_RESET_PERIOD = 10.minutes - - included do |base| - base.rescue_from ActiveRecord::StatementInvalid, with: :reset_on_union_error - - base.class_attribute :previous_reset_columns_from_error - end - - class_methods do - def reset_on_union_error(exception) - if reset_on_statement_invalid?(exception) - class_to_be_reset = base_class - - class_to_be_reset.reset_column_information - Gitlab::ErrorTracking.log_exception(exception, { reset_model_name: class_to_be_reset.name }) - - class_to_be_reset.previous_reset_columns_from_error = Time.current - end - - raise - end - - def reset_on_statement_invalid?(exception) - return false unless exception.message.include?("each UNION query must have the same number of columns") - - return false if base_class.previous_reset_columns_from_error? && - base_class.previous_reset_columns_from_error > MAX_RESET_PERIOD.ago - - Feature.enabled?(:reset_column_information_on_statement_invalid, type: :ops) - end - end -end diff --git a/app/models/concerns/routable.rb b/app/models/concerns/routable.rb index ef14ff5fbe2..4c16ba18823 100644 --- a/app/models/concerns/routable.rb +++ b/app/models/concerns/routable.rb @@ -15,16 +15,7 @@ module Routable # # Returns a single object, or nil. - # rubocop:disable Metrics/CyclomaticComplexity - # rubocop:disable Metrics/PerceivedComplexity - def self.find_by_full_path( - path, - follow_redirects: false, - route_scope: Route, - redirect_route_scope: RedirectRoute, - optimize_routable: Routable.optimize_routable_enabled? - ) - + def self.find_by_full_path(path, follow_redirects: false, route_scope: nil) return unless path.present? # Convert path to string to prevent DB error: function lower(integer) does not exist @@ -35,49 +26,22 @@ module Routable # # We need to qualify the columns with the table name, to support both direct lookups on # Route/RedirectRoute, and scoped lookups through the Routable classes. - if optimize_routable - path_condition = { path: path } - - source_type_condition = if route_scope == Route - {} - else - { source_type: route_scope.klass.base_class } - end + path_condition = { path: path } - route = - Route.where(source_type_condition).find_by(path_condition) || - Route.where(source_type_condition).iwhere(path_condition).take + source_type_condition = route_scope ? { source_type: route_scope.klass.base_class } : {} - if follow_redirects - route ||= RedirectRoute.where(source_type_condition).iwhere(path_condition).take - end + route = + Route.where(source_type_condition).find_by(path_condition) || + Route.where(source_type_condition).iwhere(path_condition).take - return unless route - return route.source if route_scope == Route - - route_scope.find_by(id: route.source_id) - else - Gitlab::Database.allow_cross_joins_across_databases(url: - "https://gitlab.com/gitlab-org/gitlab/-/issues/420046") do - route = - route_scope.find_by(routes: { path: path }) || - route_scope.iwhere(Route.arel_table[:path] => path).take - - if follow_redirects - route ||= redirect_route_scope.iwhere(RedirectRoute.arel_table[:path] => path).take - end - - next unless route - - route.is_a?(Routable) ? route : route.source - end + if follow_redirects + route ||= RedirectRoute.where(source_type_condition).iwhere(path_condition).take end - end - # rubocop:enable Metrics/PerceivedComplexity - # rubocop:enable Metrics/CyclomaticComplexity - def self.optimize_routable_enabled? - Feature.enabled?(:optimize_routable) + return unless route + return route.source unless route_scope + + route_scope.find_by(id: route.source_id) end included do @@ -107,22 +71,12 @@ module Routable # # Returns a single object, or nil. def find_by_full_path(path, follow_redirects: false) - optimize_routable = Routable.optimize_routable_enabled? - - if optimize_routable - route_scope = all - redirect_route_scope = RedirectRoute - else - route_scope = includes(:route).references(:routes) - redirect_route_scope = joins(:redirect_routes) - end + route_scope = all Routable.find_by_full_path( path, follow_redirects: follow_redirects, - route_scope: route_scope, - redirect_route_scope: redirect_route_scope, - optimize_routable: optimize_routable + route_scope: route_scope ) end diff --git a/app/models/concerns/storage/legacy_namespace.rb b/app/models/concerns/storage/legacy_namespace.rb deleted file mode 100644 index 5455a2159cd..00000000000 --- a/app/models/concerns/storage/legacy_namespace.rb +++ /dev/null @@ -1,112 +0,0 @@ -# frozen_string_literal: true - -module Storage - module LegacyNamespace - extend ActiveSupport::Concern - - include Gitlab::ShellAdapter - - def move_dir - proj_with_tags = first_project_with_container_registry_tags - - if proj_with_tags - raise Gitlab::UpdatePathError, "Namespace #{name} (#{id}) cannot be moved because at least one project (e.g. #{proj_with_tags.name} (#{proj_with_tags.id})) has tags in container registry" - end - - parent_was = if saved_change_to_parent? && parent_id_before_last_save.present? - Namespace.find(parent_id_before_last_save) # raise NotFound early if needed - end - - if saved_change_to_parent? - former_parent_full_path = parent_was&.full_path - parent_full_path = parent&.full_path - Gitlab::UploadsTransfer.new.move_namespace(path, former_parent_full_path, parent_full_path) - else - Gitlab::UploadsTransfer.new.rename_namespace(full_path_before_last_save, full_path) - end - - # If repositories moved successfully we need to - # send update instructions to users. - # However we cannot allow rollback since we moved namespace dir - # So we basically we mute exceptions in next actions - begin - send_update_instructions - write_projects_repository_config - rescue StandardError => e - Gitlab::ErrorTracking.track_and_raise_for_dev_exception(e, - full_path_before_last_save: full_path_before_last_save, - full_path: full_path, - action: 'move_dir') - end - - true # false would cancel later callbacks but not rollback - end - - # Hooks - - # Save the storages before the projects are destroyed to use them on after destroy - def prepare_for_destroy - old_repository_storages - end - - private - - def move_repositories - # Move the namespace directory in all storages used by member projects - repository_storages(legacy_only: true).each do |repository_storage| - # Ensure old directory exists before moving it - Gitlab::GitalyClient::NamespaceService.allow do - gitlab_shell.add_namespace(repository_storage, full_path_before_last_save) - - # Ensure new directory exists before moving it (if there's a parent) - gitlab_shell.add_namespace(repository_storage, parent.full_path) if parent - - unless gitlab_shell.mv_namespace(repository_storage, full_path_before_last_save, full_path) - - Gitlab::AppLogger.error("Exception moving path #{repository_storage} from #{full_path_before_last_save} to #{full_path}") - - # if we cannot move namespace directory we should rollback - # db changes in order to prevent out of sync between db and fs - raise Gitlab::UpdatePathError, 'namespace directory cannot be moved' - end - end - end - end - - def old_repository_storages - @old_repository_storage_paths ||= repository_storages(legacy_only: true) - end - - def repository_storages(legacy_only: false) - # We need to get the storage paths for all the projects, even the ones that are - # pending delete. Unscoping also get rids of the default order, which causes - # problems with SELECT DISTINCT. - Project.unscoped do - namespace_projects = all_projects - namespace_projects = namespace_projects.without_storage_feature(:repository) if legacy_only - namespace_projects.pluck(Arel.sql('distinct(repository_storage)')) - end - end - - def rm_dir - # Remove the namespace directory in all storages paths used by member projects - old_repository_storages.each do |repository_storage| - # Move namespace directory into trash. - # We will remove it later async - new_path = "#{full_path}+#{id}+deleted" - - Gitlab::GitalyClient::NamespaceService.allow do - if gitlab_shell.mv_namespace(repository_storage, full_path, new_path) - Gitlab::AppLogger.info %(Namespace directory "#{full_path}" moved to "#{new_path}") - - # Remove namespace directory async with delay so - # GitLab has time to remove all projects first - run_after_commit do - GitlabShellWorker.perform_in(5.minutes, :rm_namespace, repository_storage, new_path) - end - end - end - end - end - end -end diff --git a/app/models/concerns/vulnerability_finding_helpers.rb b/app/models/concerns/vulnerability_finding_helpers.rb index e8a50497b20..94d091e8459 100644 --- a/app/models/concerns/vulnerability_finding_helpers.rb +++ b/app/models/concerns/vulnerability_finding_helpers.rb @@ -50,6 +50,7 @@ module VulnerabilityFindingHelpers finding_data = report_finding.to_hash.except( :compare_key, :identifiers, :location, :scanner, :links, :signatures, :flags, :evidence ) + identifiers = report_finding.identifiers.uniq(&:fingerprint).map do |identifier| Vulnerabilities::Identifier.new(identifier.to_hash.merge({ project: project })) end diff --git a/app/models/container_expiration_policy.rb b/app/models/container_expiration_policy.rb index f643fa7730b..a7ed5e28695 100644 --- a/app/models/container_expiration_policy.rb +++ b/app/models/container_expiration_policy.rb @@ -80,7 +80,7 @@ class ContainerExpirationPolicy < ApplicationRecord end def set_next_run_at - cadence_seconds = ChronicDuration.parse(cadence, use_complete_matcher: true).seconds + cadence_seconds = ChronicDuration.parse(cadence).seconds self.next_run_at = Time.zone.now + cadence_seconds end diff --git a/app/models/container_registry/protection.rb b/app/models/container_registry/protection.rb new file mode 100644 index 00000000000..33c94c0c893 --- /dev/null +++ b/app/models/container_registry/protection.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module ContainerRegistry + module Protection + def self.table_name_prefix + 'container_registry_protection_' + end + end +end diff --git a/app/models/container_registry/protection/rule.rb b/app/models/container_registry/protection/rule.rb new file mode 100644 index 00000000000..a91f3633d75 --- /dev/null +++ b/app/models/container_registry/protection/rule.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module ContainerRegistry + module Protection + class Rule < ApplicationRecord + enum delete_protected_up_to_access_level: + Gitlab::Access.sym_options_with_owner.slice(:maintainer, :owner, :developer), + _prefix: :delete_protected_up_to + enum push_protected_up_to_access_level: + Gitlab::Access.sym_options_with_owner.slice(:maintainer, :owner, :developer), + _prefix: :push_protected_up_to + + belongs_to :project, inverse_of: :container_registry_protection_rules + + validates :container_path_pattern, presence: true, uniqueness: { scope: :project_id }, length: { maximum: 255 } + validates :delete_protected_up_to_access_level, presence: true + validates :push_protected_up_to_access_level, presence: true + end + end +end diff --git a/app/models/design_user_mention.rb b/app/models/design_user_mention.rb index 7d0cd72e9eb..ba1ef1b5712 100644 --- a/app/models/design_user_mention.rb +++ b/app/models/design_user_mention.rb @@ -3,7 +3,7 @@ class DesignUserMention < UserMention include IgnorableColumns - ignore_column :note_id_convert_to_bigint, remove_with: '16.2', remove_after: '2023-07-22' + ignore_column :note_id_convert_to_bigint, remove_with: '16.7', remove_after: '2023-11-16' belongs_to :design, class_name: 'DesignManagement::Design' belongs_to :note diff --git a/app/models/discussion_note.rb b/app/models/discussion_note.rb index a1dfa0e72ec..fa830179022 100644 --- a/app/models/discussion_note.rb +++ b/app/models/discussion_note.rb @@ -9,7 +9,7 @@ class DiscussionNote < Note # Names of all implementers of `Noteable` that support discussions. def self.noteable_types - %w[MergeRequest Issue Commit Snippet] + %w[MergeRequest Issue Commit Snippet AbuseReport] end validates :noteable_type, inclusion: { in: noteable_types } diff --git a/app/models/environment.rb b/app/models/environment.rb index 29394c37e2c..efdcf7174aa 100644 --- a/app/models/environment.rb +++ b/app/models/environment.rb @@ -195,6 +195,10 @@ class Environment < ApplicationRecord transition %i[available stopping] => :stopped end + event :recover_stuck_stopping do + transition stopping: :available + end + state :available state :stopping state :stopped diff --git a/app/models/event.rb b/app/models/event.rb index 9e4a662aaa5..7de7ad8ccd6 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -12,7 +12,7 @@ class Event < ApplicationRecord include IgnorableColumns include EachBatch - ignore_column :target_id_convert_to_bigint, remove_with: '16.4', remove_after: '2023-09-22' + ignore_column :target_id_convert_to_bigint, remove_with: '16.7', remove_after: '2023-11-16' ACTIONS = HashWithIndifferentAccess.new( created: 1, diff --git a/app/models/group.rb b/app/models/group.rb index 9330ffef156..c83dd24e98e 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -423,15 +423,13 @@ class Group < Namespace owners.include?(user) end - def add_members(users, access_level, current_user: nil, expires_at: nil, tasks_to_be_done: [], tasks_project_id: nil) + def add_members(users, access_level, current_user: nil, expires_at: nil) Members::Groups::CreatorService.add_members( # rubocop:disable CodeReuse/ServiceClass self, users, access_level, current_user: current_user, - expires_at: expires_at, - tasks_to_be_done: tasks_to_be_done, - tasks_project_id: tasks_project_id + expires_at: expires_at ) end @@ -512,9 +510,15 @@ class Group < Namespace members_with_parents(only_active_users: false) end - members_from_hiearchy.all_owners.left_outer_joins(:user) - .merge(User.without_project_bot) - .allow_cross_joins_across_databases(url: "https://gitlab.com/gitlab-org/gitlab/-/issues/417455") + owners = [] + + members_from_hiearchy.all_owners.non_invite.each_batch do |relation| + owners += relation.preload(:user).load.reject do |member| + member.user.project_bot? + end + end + + owners end def ldap_synced? @@ -657,12 +661,6 @@ class Group < Namespace .non_invite end - def users_with_parents - User - .where(id: members_with_parents.select(:user_id)) - .reorder(nil) - end - def users_with_descendants User .where(id: members_with_descendants.select(:user_id)) @@ -694,7 +692,7 @@ class Group < Namespace return GroupMember::NO_ACCESS unless user return GroupMember::OWNER if user.can_admin_all_resources? && !only_concrete_membership - max_member_access([user.id])[user.id] + max_member_access(user) end def mattermost_team_params @@ -879,10 +877,6 @@ class Group < Namespace ].compact.min end - def content_editor_on_issues_feature_flag_enabled? - feature_flag_enabled_for_self_or_ancestor?(:content_editor_on_issues) - end - def work_items_feature_flag_enabled? feature_flag_enabled_for_self_or_ancestor?(:work_items) end @@ -953,16 +947,16 @@ class Group < Namespace end end - def max_member_access(user_ids) - ::Gitlab::Database.allow_cross_joins_across_databases(url: "https://gitlab.com/gitlab-org/gitlab/-/issues/417455") do - Gitlab::SafeRequestLoader.execute( - resource_key: max_member_access_for_resource_key(User), - resource_ids: user_ids, - default_value: Gitlab::Access::NO_ACCESS - ) do |user_ids| - members_with_parents.where(user_id: user_ids).group(:user_id).maximum(:access_level) - end - end + def max_member_access(user) + Gitlab::SafeRequestLoader.execute( + resource_key: max_member_access_for_resource_key(User), + resource_ids: [user.id], + default_value: Gitlab::Access::NO_ACCESS + ) do |_| + next {} unless user.active? + + members_with_parents(only_active_users: false).where(user_id: user.id).group(:user_id).maximum(:access_level) + end.fetch(user.id) end def update_two_factor_requirement diff --git a/app/models/identity.rb b/app/models/identity.rb index a4c59694050..1a3a9a300b6 100644 --- a/app/models/identity.rb +++ b/app/models/identity.rb @@ -14,6 +14,7 @@ class Identity < MainClusterwide::ApplicationRecord after_destroy :clear_user_synced_attributes, if: :user_synced_attributes_metadata_from_provider? scope :for_user, ->(user) { where(user: user) } + scope :for_user_ids, ->(user_ids) { where(user_id: user_ids) } scope :with_provider, ->(provider) { where(provider: provider) } scope :with_extern_uid, ->(provider, extern_uid) do iwhere(extern_uid: normalize_uid(provider, extern_uid)).with_provider(provider) diff --git a/app/models/integration.rb b/app/models/integration.rb index d4c76f743a3..b4408301c6d 100644 --- a/app/models/integration.rb +++ b/app/models/integration.rb @@ -47,6 +47,9 @@ class Integration < ApplicationRecord Integrations::BaseThirdPartyWiki ].freeze + BASE_ATTRIBUTES = %w[id instance project_id group_id created_at updated_at + encrypted_properties encrypted_properties_iv properties].freeze + SECTION_TYPE_CONFIGURATION = 'configuration' SECTION_TYPE_CONNECTION = 'connection' SECTION_TYPE_TRIGGER = 'trigger' @@ -111,18 +114,18 @@ class Integration < ApplicationRecord validate :validate_belongs_to_project_or_group scope :external_issue_trackers, -> { where(category: 'issue_tracker').active } - # TODO: Will be modified in 15.0 - # Details: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/74501#note_744393645 - scope :third_party_wikis, -> { where(type: %w[Integrations::Confluence Integrations::Shimo]).active } + scope :third_party_wikis, -> { where(category: 'third_party_wiki').active } scope :by_name, ->(name) { by_type(integration_name_to_type(name)) } scope :external_wikis, -> { by_name(:external_wiki).active } scope :active, -> { where(active: true) } scope :by_type, ->(type) { where(type: type) } # INTERNAL USE ONLY: use by_name instead - scope :by_active_flag, -> (flag) { where(active: flag) } - scope :inherit_from_id, -> (id) { where(inherit_from_id: id) } + scope :by_active_flag, ->(flag) { where(active: flag) } + scope :inherit_from_id, ->(id) { where(inherit_from_id: id) } scope :with_default_settings, -> { where.not(inherit_from_id: nil) } scope :with_custom_settings, -> { where(inherit_from_id: nil) } - scope :for_group, -> (group) { where(group_id: group, type: available_integration_types(include_project_specific: false)) } + scope :for_group, ->(group) { + where(group_id: group, type: available_integration_types(include_project_specific: false)) + } scope :for_instance, -> { where(instance: true, type: available_integration_types(include_project_specific: false)) } scope :push_hooks, -> { where(push_events: true, active: true) } @@ -216,13 +219,6 @@ class Integration < ApplicationRecord # Also keep track of updated properties in a similar way as ActiveModel::Dirty def self.boolean_accessor(*args) args.each do |arg| - # TODO: Allow legacy usage of `.boolean_accessor`, once all integrations - # are converted to the field DSL we can remove this and only call - # `.boolean_accessor` through `.field`. - # - # See https://gitlab.com/groups/gitlab-org/-/epics/7652 - prop_accessor(arg) unless method_defined?(arg) - class_eval <<~RUBY, __FILE__, __LINE__ + 1 # Make the original getter available as a private method. alias_method :#{arg}_before_type_cast, :#{arg} @@ -239,13 +235,14 @@ class Integration < ApplicationRecord RUBY end end + private_class_method :boolean_accessor def self.to_param raise NotImplementedError end def self.event_names - self.supported_events.map { |event| IntegrationsHelper.integration_event_field_name(event) } + supported_events.map { |event| IntegrationsHelper.integration_event_field_name(event) } end def self.supported_events @@ -406,7 +403,7 @@ class Integration < ApplicationRecord from_union([active.where(instance: true), active.where(group_id: group_ids, inherit_from_id: nil)]) .order(order) .group_by(&:type) - .count { |type, parents| build_from_integration(parents.first, association => owner.id).save } + .count { |_type, parents| build_from_integration(parents.first, association => owner.id).save } end def self.inherited_descendants_from_self_or_ancestors_from(integration) @@ -415,9 +412,10 @@ class Integration < ApplicationRecord .or(where(type: integration.type, instance: true)).select(:id) from_union([ - where(type: integration.type, inherit_from_id: inherit_from_ids, group: integration.group.descendants), - where(type: integration.type, inherit_from_id: inherit_from_ids, project: Project.in_namespace(integration.group.self_and_descendants)) - ]) + where(type: integration.type, inherit_from_id: inherit_from_ids, group: integration.group.descendants), + where(type: integration.type, inherit_from_id: inherit_from_ids, + project: Project.in_namespace(integration.group.self_and_descendants)) + ]) end def activated? @@ -490,10 +488,9 @@ class Integration < ApplicationRecord def to_database_hash column = self.class.attribute_aliases.fetch('type', 'type') - as_json( - except: %w[id instance project_id group_id created_at updated_at] - ).merge(column => type) - .merge(reencrypt_properties) + attributes_for_database.except(*BASE_ATTRIBUTES) + .merge(column => type) + .merge(reencrypt_properties) end def reencrypt_properties diff --git a/app/models/integrations/asana.rb b/app/models/integrations/asana.rb index 859522670ef..77555996cd9 100644 --- a/app/models/integrations/asana.rb +++ b/app/models/integrations/asana.rb @@ -1,9 +1,10 @@ # frozen_string_literal: true -require 'asana' - module Integrations class Asana < Integration + TASK_URL_TEMPLATE = 'https://app.asana.com/api/1.0/tasks/%{task_gid}' + STORY_URL_TEMPLATE = 'https://app.asana.com/api/1.0/tasks/%{task_gid}/stories' + validates :api_key, presence: true, if: :activated? field :api_key, @@ -40,12 +41,6 @@ module Integrations %w[push] end - def client - @_client ||= ::Asana::Client.new do |c| - c.authentication :access_token, api_key - end - end - def execute(data) return unless supported_events.include?(data[:object_kind]) @@ -78,11 +73,12 @@ module Integrations taskid = tuple[2] || tuple[1] begin - task = ::Asana::Resources::Task.find_by_id(client, taskid) - task.add_comment(text: "#{push_msg} #{message}") + story_on_task_url = format(STORY_URL_TEMPLATE, task_gid: taskid) + Gitlab::HTTP.post(story_on_task_url, headers: { "Authorization" => "Bearer #{api_key}" }, body: { text: "#{push_msg} #{message}" }) if tuple[0] - task.update(completed: true) + task_url = format(TASK_URL_TEMPLATE, task_gid: taskid) + Gitlab::HTTP.put(task_url, headers: { "Authorization" => "Bearer #{api_key}" }, body: { completed: true }) end rescue StandardError => e log_error(e.message) diff --git a/app/models/integrations/bamboo.rb b/app/models/integrations/bamboo.rb index 0b8432136dd..9f15532a0b0 100644 --- a/app/models/integrations/bamboo.rb +++ b/app/models/integrations/bamboo.rb @@ -28,14 +28,13 @@ module Integrations non_empty_password_title: -> { s_('ProjectService|Enter new password') }, non_empty_password_help: -> { s_('ProjectService|Leave blank to use your current password') } - validates :bamboo_url, presence: true, public_url: true, if: :activated? - validates :build_key, presence: true, if: :activated? - validates :username, - presence: true, - if: ->(service) { service.activated? && service.password } - validates :password, - presence: true, - if: ->(service) { service.activated? && service.username } + with_options if: :activated? do + validates :bamboo_url, presence: true, public_url: true + validates :build_key, presence: true + end + + validates :username, presence: true, if: ->(integration) { integration.activated? && integration.password } + validates :password, presence: true, if: ->(integration) { integration.activated? && integration.username } attr_accessor :response @@ -48,8 +47,16 @@ module Integrations end def help - docs_link = ActionController::Base.helpers.link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/bamboo'), target: '_blank', rel: 'noopener noreferrer' - s_('BambooService|Run CI/CD pipelines with Atlassian Bamboo. You must set up automatic revision labeling and a repository trigger in Bamboo. %{docs_link}').html_safe % { docs_link: docs_link.html_safe } + docs_link = ActionController::Base.helpers.link_to( + _('Learn more.'), + Rails.application.routes.url_helpers.help_page_url('user/project/integrations/bamboo'), + target: '_blank', + rel: 'noopener noreferrer' + ) + format( + s_('BambooService|Run CI/CD pipelines with Atlassian Bamboo. You must set up automatic revision labeling and ' \ + 'a repository trigger in Bamboo. %{docs_link}').html_safe, + docs_link: docs_link.html_safe) end def self.to_param @@ -70,12 +77,18 @@ module Integrations get_path("updateAndBuild.action", { buildKey: build_key }) end - def calculate_reactive_cache(sha, ref) + def calculate_reactive_cache(sha, _ref) response = try_get_path("rest/api/latest/result/byChangeset/#{sha}") { build_page: read_build_page(response), commit_status: read_commit_status(response) } end + def avatar_url + ActionController::Base.helpers.image_path( + 'illustrations/third-party-logos/integrations-logos/atlassian-bamboo.svg' + ) + end + private def get_build_result(response) @@ -112,7 +125,7 @@ module Integrations if result.blank? 'Pending' else - result.dig('buildState') + result['buildState'] end return :error unless status.present? diff --git a/app/models/integrations/base_chat_notification.rb b/app/models/integrations/base_chat_notification.rb index 2c929dc2cb3..b75801335bd 100644 --- a/app/models/integrations/base_chat_notification.rb +++ b/app/models/integrations/base_chat_notification.rb @@ -13,6 +13,8 @@ module Integrations tag_push pipeline wiki_page deployment incident ].freeze + GROUP_ONLY_SUPPORTED_EVENTS = %w[group_mention group_confidential_mention].freeze + SUPPORTED_EVENTS_FOR_LABEL_FILTER = %w[issue confidential_issue merge_request note confidential_note].freeze EVENT_CHANNEL = proc { |event| "#{event}_channel" } @@ -26,12 +28,12 @@ module Integrations attribute :category, default: 'chat' - prop_accessor :webhook, :username, :channel, :branches_to_be_notified, :labels_to_be_notified, :labels_to_be_notified_behavior + prop_accessor :webhook, :username, :channel, :branches_to_be_notified, :labels_to_be_notified, + :labels_to_be_notified_behavior, :notify_only_default_branch # Custom serialized properties initialization prop_accessor(*SUPPORTED_EVENTS.map { |event| EVENT_CHANNEL[event] }) - - boolean_accessor :notify_only_default_branch + prop_accessor(*GROUP_ONLY_SUPPORTED_EVENTS.map { |event| EVENT_CHANNEL[event] }) validates :webhook, presence: true, @@ -44,10 +46,10 @@ module Integrations super if properties.empty? - self.notify_only_broken_pipelines = true if self.respond_to?(:notify_only_broken_pipelines) + self.notify_only_broken_pipelines = true if respond_to?(:notify_only_broken_pipelines) self.branches_to_be_notified = "default" self.labels_to_be_notified_behavior = MATCH_ANY_LABEL - elsif !self.notify_only_default_branch.nil? + elsif !notify_only_default_branch.nil? # In older versions, there was only a boolean property named # `notify_only_default_branch`. Now we have a string property named # `branches_to_be_notified`. Instead of doing a background migration, we @@ -55,7 +57,7 @@ module Integrations # users haven't specified one already. When users edit the integration and # select a value for this new property, it will override everything. - self.branches_to_be_notified ||= notify_only_default_branch? ? "default" : "all" + self.branches_to_be_notified ||= notify_only_default_branch == 'true' ? "default" : "all" end end @@ -237,7 +239,7 @@ module Integrations case object_kind when "push", "tag_push" Integrations::ChatMessage::PushMessage.new(data) if notify_for_ref?(data) - when "issue" + when "issue", "incident" Integrations::ChatMessage::IssueMessage.new(data) unless update?(data) when "merge_request" Integrations::ChatMessage::MergeMessage.new(data) unless update?(data) @@ -249,8 +251,8 @@ module Integrations Integrations::ChatMessage::WikiPageMessage.new(data) when "deployment" Integrations::ChatMessage::DeploymentMessage.new(data) if notify_for_ref?(data) - when "incident" - Integrations::ChatMessage::IssueMessage.new(data) unless update?(data) + when "group_mention" + Integrations::ChatMessage::GroupMentionMessage.new(data) end end # rubocop:enable Metrics/CyclomaticComplexity diff --git a/app/models/integrations/base_slack_notification.rb b/app/models/integrations/base_slack_notification.rb index 65aec8b278f..09a0c9ba361 100644 --- a/app/models/integrations/base_slack_notification.rb +++ b/app/models/integrations/base_slack_notification.rb @@ -7,8 +7,6 @@ module Integrations ].freeze prop_accessor EVENT_CHANNEL['alert'] - prop_accessor EVENT_CHANNEL['group_mention'] - prop_accessor EVENT_CHANNEL['group_confidential_mention'] override :default_channel_placeholder def default_channel_placeholder @@ -18,7 +16,6 @@ module Integrations override :get_message def get_message(object_kind, data) return Integrations::ChatMessage::AlertMessage.new(data) if object_kind == 'alert' - return Integrations::ChatMessage::GroupMentionMessage.new(data) if object_kind == 'group_mention' super end diff --git a/app/models/integrations/chat_message/alert_message.rb b/app/models/integrations/chat_message/alert_message.rb index e2c689f9435..6c7ea9aed7c 100644 --- a/app/models/integrations/chat_message/alert_message.rb +++ b/app/models/integrations/chat_message/alert_message.rb @@ -34,12 +34,12 @@ module Integrations "Alert firing in #{strip_markup(project_name)}" end - private - def attachment_color "#C95823" end + private + def attachment_fields [ { diff --git a/app/models/integrations/chat_message/deployment_message.rb b/app/models/integrations/chat_message/deployment_message.rb index 0367459dfcb..4d3e962d885 100644 --- a/app/models/integrations/chat_message/deployment_message.rb +++ b/app/models/integrations/chat_message/deployment_message.rb @@ -30,7 +30,7 @@ module Integrations [{ text: format(description_message), - color: color + color: attachment_color }] end @@ -38,17 +38,7 @@ module Integrations {} end - private - - def message - if running? - "Starting deploy to #{strip_markup(environment)}" - else - "Deploy to #{strip_markup(environment)} #{humanized_status}" - end - end - - def color + def attachment_color case status when 'success' 'good' @@ -61,6 +51,16 @@ module Integrations end end + private + + def message + if running? + "Starting deploy to #{strip_markup(environment)}" + else + "Deploy to #{strip_markup(environment)} #{humanized_status}" + end + end + def project_link link(project_name, project_url) end diff --git a/app/models/integrations/chat_message/issue_message.rb b/app/models/integrations/chat_message/issue_message.rb index dd516362491..4c144bc2f68 100644 --- a/app/models/integrations/chat_message/issue_message.rb +++ b/app/models/integrations/chat_message/issue_message.rb @@ -41,6 +41,10 @@ module Integrations } end + def attachment_color + '#C95823' + end + private def message @@ -56,7 +60,7 @@ module Integrations title: issue_title, title_link: issue_url, text: format(SlackMarkdownSanitizer.sanitize_slack_link(description)), - color: '#C95823' + color: attachment_color }] end diff --git a/app/models/integrations/chat_message/pipeline_message.rb b/app/models/integrations/chat_message/pipeline_message.rb index f8a634be336..2abe4a6e9c7 100644 --- a/app/models/integrations/chat_message/pipeline_message.rb +++ b/app/models/integrations/chat_message/pipeline_message.rb @@ -89,6 +89,15 @@ module Integrations } end + def attachment_color + case status + when 'success' + detailed_status == 'passed with warnings' ? 'warning' : 'good' + else + 'danger' + end + end + private def actually_failed_jobs(builds) @@ -180,15 +189,6 @@ module Integrations end end - def attachment_color - case status - when 'success' - detailed_status == 'passed with warnings' ? 'warning' : 'good' - else - 'danger' - end - end - def ref_url if ref_type == 'tag' "#{project_url}/-/tags/#{ref}" diff --git a/app/models/integrations/chat_message/push_message.rb b/app/models/integrations/chat_message/push_message.rb index b17e28bb6c6..ee44fc98791 100644 --- a/app/models/integrations/chat_message/push_message.rb +++ b/app/models/integrations/chat_message/push_message.rb @@ -35,6 +35,10 @@ module Integrations } end + def attachment_color + '#345' + end + private def humanized_action(short: false) @@ -111,10 +115,6 @@ module Integrations ['pushed to', ref_link, "of #{project_link} (#{compare_link})"] end end - - def attachment_color - '#345' - end end end end diff --git a/app/models/integrations/discord.rb b/app/models/integrations/discord.rb index 815e3669d78..33b2b52fa62 100644 --- a/app/models/integrations/discord.rb +++ b/app/models/integrations/discord.rb @@ -42,8 +42,15 @@ module Integrations s_('DiscordService|Override the default webhook (e.g. https://discord.com/api/webhooks/…)') end + override :supported_events + def supported_events + additional = group_level? ? %w[group_mention group_confidential_mention] : [] + + (self.class.supported_events + additional).freeze + end + def self.supported_events - %w[push issue confidential_issue merge_request note confidential_note tag_push pipeline wiki_page] + %w[push issue confidential_issue merge_request note confidential_note tag_push pipeline wiki_page deployment] end def configurable_channels? @@ -68,7 +75,7 @@ module Integrations builder.add_embed do |embed| embed.author = Discordrb::Webhooks::EmbedAuthor.new(name: message.user_name, icon_url: message.user_avatar) embed.description = (message.pretext + "\n" + Array.wrap(message.attachments).join("\n")).gsub(ATTACHMENT_REGEX, " \\k<entry> - \\k<name>\n") - embed.colour = 16543014 # The hex "fc6d26" as an Integer + embed.colour = embed_color(message) embed.timestamp = Time.now.utc end end @@ -77,6 +84,33 @@ module Integrations false end + COLOR_OVERRIDES = { + 'good' => '#0d532a', + 'warning' => '#703800', + 'danger' => '#8d1300' + }.freeze + + def embed_color(message) + return 'fc6d26'.hex unless message.respond_to?(:attachment_color) + + color = message.attachment_color + + color = COLOR_OVERRIDES[color] if COLOR_OVERRIDES.key?(color) + + color = color.delete_prefix('#') + + normalize_color(color).hex + end + + # Expands the short notation to the full colorcode notation + # 123456 -> 123456 + # 123 -> 112233 + def normalize_color(color) + return (color[0, 1] * 2) + (color[1, 1] * 2) + (color[2, 1] * 2) if color.length == 3 + + color + end + def custom_data(data) super(data).merge(markdown: true) end diff --git a/app/models/integrations/hangouts_chat.rb b/app/models/integrations/hangouts_chat.rb index 680752c3d56..6e4753470a3 100644 --- a/app/models/integrations/hangouts_chat.rb +++ b/app/models/integrations/hangouts_chat.rb @@ -30,12 +30,15 @@ module Integrations end def help - docs_link = ActionController::Base.helpers.link_to _('How do I set up a Google Chat webhook?'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/hangouts_chat'), target: '_blank', rel: 'noopener noreferrer' - s_('Before enabling this integration, create a webhook for the room in Google Chat where you want to receive notifications from this project. %{docs_link}').html_safe % { docs_link: docs_link.html_safe } + docs_link = ActionController::Base.helpers.link_to(_('How do I set up a Google Chat webhook?'), + Rails.application.routes.url_helpers.help_page_url('user/project/integrations/hangouts_chat'), + target: '_blank', rel: 'noopener noreferrer') + format( + s_('Before enabling this integration, create a webhook for the room in Google Chat where you want to receive ' \ + 'notifications from this project. %{docs_link}').html_safe, docs_link: docs_link.html_safe) end - def default_channel_placeholder - end + def default_channel_placeholder; end def self.supported_events %w[push issue confidential_issue merge_request note confidential_note tag_push pipeline wiki_page] @@ -43,14 +46,20 @@ module Integrations private - def notify(message, opts) + def notify(message, _opts) url = webhook.dup key = parse_thread_key(message) url = Gitlab::Utils.add_url_parameters(url, { threadKey: key }) if key - simple_text = parse_simple_text_message(message) - ::HangoutsChat::Sender.new(url).simple(simple_text) + payload = { text: parse_simple_text_message(message) } + + Gitlab::HTTP.post( + url, + body: payload.to_json, + headers: { 'Content-Type' => 'application/json' }, + parse: nil + ).response end # Returns an appropriate key for threading messages in google chat diff --git a/app/models/integrations/integration_list.rb b/app/models/integrations/integration_list.rb new file mode 100644 index 00000000000..ab03e5c0e0a --- /dev/null +++ b/app/models/integrations/integration_list.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module Integrations + class IntegrationList + def initialize(batch, integration_hash, association) + @batch = batch + @integration_hash = integration_hash + @association = association + end + + def to_array + [Integration, columns, values] + end + + private + + attr_reader :batch, :integration_hash, :association + + def columns + integration_hash.keys << "#{association}_id" + end + + def values + batch.select(:id).map do |record| + integration_hash.values << record.id + end + end + end +end diff --git a/app/models/integrations/jira.rb b/app/models/integrations/jira.rb index d8d1f860e9a..f6e99454cb1 100644 --- a/app/models/integrations/jira.rb +++ b/app/models/integrations/jira.rb @@ -11,8 +11,12 @@ module Integrations PROJECTS_PER_PAGE = 50 JIRA_CLOUD_HOST = '.atlassian.net' - ATLASSIAN_REFERRER_GITLAB_COM = { atlOrigin: 'eyJpIjoiY2QyZTJiZDRkNGZhNGZlMWI3NzRkNTBmZmVlNzNiZTkiLCJwIjoianN3LWdpdGxhYi1pbnQifQ' }.freeze - ATLASSIAN_REFERRER_SELF_MANAGED = { atlOrigin: 'eyJpIjoiYjM0MTA4MzUyYTYxNDVkY2IwMzVjOGQ3ZWQ3NzMwM2QiLCJwIjoianN3LWdpdGxhYlNNLWludCJ9' }.freeze + ATLASSIAN_REFERRER_GITLAB_COM = { + atlOrigin: 'eyJpIjoiY2QyZTJiZDRkNGZhNGZlMWI3NzRkNTBmZmVlNzNiZTkiLCJwIjoianN3LWdpdGxhYi1pbnQifQ' + }.freeze + ATLASSIAN_REFERRER_SELF_MANAGED = { + atlOrigin: 'eyJpIjoiYjM0MTA4MzUyYTYxNDVkY2IwMzVjOGQ3ZWQ3NzMwM2QiLCJwIjoianN3LWdpdGxhYlNNLWludCJ9' + }.freeze API_ENDPOINTS = { find_issue: "/rest/api/2/issue/%s", @@ -28,11 +32,13 @@ module Integrations AUTH_TYPE_BASIC = 0 AUTH_TYPE_PAT = 1 - SNOWPLOW_EVENT_CATEGORY = self.name + SNOWPLOW_EVENT_CATEGORY = name validates :url, public_url: true, presence: true, if: :activated? validates :api_url, public_url: true, allow_blank: true - validates :username, presence: true, if: ->(object) { object.activated? && !object.personal_access_token_authorization? } + validates :username, presence: true, if: ->(object) { + object.activated? && !object.personal_access_token_authorization? + } validates :password, presence: true, if: :activated? validates :jira_auth_type, presence: true, inclusion: { in: [AUTH_TYPE_BASIC, AUTH_TYPE_PAT] }, if: :activated? validates :jira_issue_prefix, untrusted_regexp: true, length: { maximum: 255 }, if: :activated? @@ -130,7 +136,7 @@ module Integrations end # {PROJECT-KEY}-{NUMBER} Examples: JIRA-1, PROJECT-1 - def reference_pattern(only_long: true) + def reference_pattern(*) @reference_pattern ||= jira_issue_match_regex end @@ -144,7 +150,7 @@ module Integrations end def data_fields - jira_tracker_data || self.build_jira_tracker_data + jira_tracker_data || build_jira_tracker_data end def set_default_data @@ -186,8 +192,13 @@ module Integrations end def help - jira_doc_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('integration/jira/index') } - s_("JiraService|You must configure Jira before enabling this integration. %{jira_doc_link_start}Learn more.%{link_end}") % { jira_doc_link_start: jira_doc_link_start, link_end: '</a>'.html_safe } + jira_doc_link_start = format('<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe, + url: help_page_path('integration/jira/index')) + format( + s_("JiraService|You must configure Jira before enabling this integration. " \ + "%{jira_doc_link_start}Learn more.%{link_end}"), + jira_doc_link_start: jira_doc_link_start, + link_end: '</a>'.html_safe) end def title @@ -212,7 +223,8 @@ module Integrations { type: SECTION_TYPE_JIRA_TRIGGER, title: _('Trigger'), - description: s_('JiraService|When a Jira issue is mentioned in a commit or merge request, a remote link and comment (if enabled) will be created.') + description: s_('JiraService|When a Jira issue is mentioned in a commit or merge request, a remote link ' \ + 'and comment (if enabled) will be created.') }, { type: SECTION_TYPE_CONFIGURATION, @@ -313,7 +325,8 @@ module Integrations override :create_cross_reference_note def create_cross_reference_note(external_issue, mentioned_in, author) unless can_cross_reference?(mentioned_in) - return s_("JiraService|Events for %{noteable_model_name} are disabled.") % { noteable_model_name: mentioned_in.model_name.plural.humanize(capitalize: false) } + return format(s_("JiraService|Events for %{noteable_model_name} are disabled."), + noteable_model_name: mentioned_in.model_name.plural.humanize(capitalize: false)) end jira_issue = find_issue(external_issue.id) @@ -381,6 +394,10 @@ module Integrations jira_auth_type == AUTH_TYPE_PAT end + def avatar_url + ActionController::Base.helpers.image_path('illustrations/third-party-logos/integrations-logos/jira.svg') + end + private def jira_issue_match_regex @@ -398,10 +415,9 @@ module Integrations end def server_info - strong_memoize(:server_info) do - client_url.present? ? jira_request(API_ENDPOINTS[:server_info]) { client.ServerInfo.all.attrs } : nil - end + client_url.present? ? jira_request(API_ENDPOINTS[:server_info]) { client.ServerInfo.all.attrs } : nil end + strong_memoize_attr :server_info def can_cross_reference?(mentioned_in) case mentioned_in @@ -430,7 +446,8 @@ module Integrations true rescue StandardError => e path = API_ENDPOINTS[:transition_issue] % issue.id - log_exception(e, message: 'Issue transition failed', client_url: client_url, client_path: path, client_status: '400') + log_exception(e, message: 'Issue transition failed', client_url: client_url, client_path: path, + client_status: '400') false end @@ -488,9 +505,9 @@ module Integrations link_title = "#{entity_name.capitalize} - #{entity_title}" link_props = build_remote_link_props(url: entity_url, title: link_title) - unless comment_exists?(issue, message) - send_message(issue, message, link_props) - end + return if comment_exists?(issue, message) + + send_message(issue, message, link_props) end def comment_message(data) @@ -503,21 +520,22 @@ module Integrations project_link = build_jira_link(project.full_name, Gitlab::Routing.url_helpers.project_url(project)) branch = if entity[:branch].present? - s_('JiraService| on branch %{branch_link}') % { - branch_link: build_jira_link(entity[:branch], project_tree_url(project, entity[:branch])) - } + format(s_('JiraService| on branch %{branch_link}'), + branch_link: build_jira_link(entity[:branch], project_tree_url(project, entity[:branch]))) end entity_message = entity[:description].presence if all_details? entity_message ||= entity[:title].chomp - s_('JiraService|%{user_link} mentioned this issue in %{entity_link} of %{project_link}%{branch}:{quote}%{entity_message}{quote}') % { + format( + s_('JiraService|%{user_link} mentioned this issue in %{entity_link} of ' \ + '%{project_link}%{branch}:{quote}%{entity_message}{quote}'), user_link: user_link, entity_link: entity_link, project_link: project_link, branch: branch, entity_message: entity_message - } + ) end def build_jira_link(title, url) @@ -586,13 +604,13 @@ module Integrations end def resource_url(resource) - "#{Settings.gitlab.base_url.chomp("/")}#{resource}" + "#{Settings.gitlab.base_url.chomp('/')}#{resource}" end def build_entity_url(entity_type, entity_id) polymorphic_url( [ - self.project, + project, entity_type.to_sym ], id: entity_id, @@ -631,7 +649,8 @@ module Integrations yield rescue StandardError => e @error = e - log_exception(e, message: 'Error sending message', client_url: client_url, client_path: path, client_status: e.try(:code)) + log_exception(e, message: 'Error sending message', client_url: client_url, client_path: path, + client_status: e.try(:code)) nil end @@ -648,7 +667,8 @@ module Integrations results = server_info unless results.present? - Gitlab::AppLogger.warn(message: "Jira API returned no ServerInfo, setting deployment_type from URL", server_info: results, url: client_url) + Gitlab::AppLogger.warn(message: "Jira API returned no ServerInfo, setting deployment_type from URL", + server_info: results, url: client_url) return set_deployment_type_from_url end @@ -681,13 +701,25 @@ module Integrations end def jira_issues_section_description - jira_issues_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('integration/jira/issues') } - description = s_('JiraService|Work on Jira issues without leaving GitLab. Add a Jira menu to access a read-only list of your Jira issues. %{jira_issues_link_start}Learn more.%{link_end}') % { jira_issues_link_start: jira_issues_link_start, link_end: '</a>'.html_safe } + jira_issues_link_start = format('<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe, + url: help_page_path('integration/jira/issues')) + description = format( + s_('JiraService|Work on Jira issues without leaving GitLab. Add a Jira menu to access a read-only list of ' \ + 'your Jira issues. %{jira_issues_link_start}Learn more.%{link_end}'), + jira_issues_link_start: jira_issues_link_start, + link_end: '</a>'.html_safe + ) if project&.issues_enabled? - gitlab_issues_link_start = '<a href="%{url}">'.html_safe % { url: edit_project_path(project, anchor: 'js-shared-permissions') } + gitlab_issues_link_start = format('<a href="%{url}">'.html_safe, url: edit_project_path(project, + anchor: 'js-shared-permissions')) description += '<br><br>'.html_safe - description += s_("JiraService|Displaying Jira issues while leaving GitLab issues also enabled might be confusing. Consider %{gitlab_issues_link_start}disabling GitLab issues%{link_end} if they won't otherwise be used.") % { gitlab_issues_link_start: gitlab_issues_link_start, link_end: '</a>'.html_safe } + description += format( + s_("JiraService|Displaying Jira issues while leaving GitLab issues also enabled might be confusing. " \ + "Consider %{gitlab_issues_link_start}disabling GitLab issues%{link_end} if they won't otherwise be used."), + gitlab_issues_link_start: gitlab_issues_link_start, + link_end: '</a>'.html_safe + ) end description diff --git a/app/models/integrations/pipelines_email.rb b/app/models/integrations/pipelines_email.rb index fa22bd1a73c..01efbc3e4a4 100644 --- a/app/models/integrations/pipelines_email.rb +++ b/app/models/integrations/pipelines_email.rb @@ -37,8 +37,8 @@ module Integrations # `notify_only_default_branch`. Now we have a string property named # `branches_to_be_notified`. Instead of doing a background migration, we # opted to set a value for the new property based on the old one, if - # users hasn't specified one already. When users edit the service and - # selects a value for this new property, it will override everything. + # users haven't specified one already. When users edit the integration and + # select a value for this new property, it will override everything. self.branches_to_be_notified ||= notify_only_default_branch? ? "default" : "all" end diff --git a/app/models/integrations/pivotaltracker.rb b/app/models/integrations/pivotaltracker.rb index f42a872c49e..b3cbc988dd6 100644 --- a/app/models/integrations/pivotaltracker.rb +++ b/app/models/integrations/pivotaltracker.rb @@ -65,6 +65,10 @@ module Integrations end end + def avatar_url + ActionController::Base.helpers.image_path('illustrations/third-party-logos/integrations-logos/pivotal-tracker.svg') + end + private def allowed_branch?(ref) diff --git a/app/models/integrations/prometheus.rb b/app/models/integrations/prometheus.rb index 8474a5b7adf..ff8d07a1b4c 100644 --- a/app/models/integrations/prometheus.rb +++ b/app/models/integrations/prometheus.rb @@ -185,7 +185,7 @@ module Integrations # Remove in next required stop after %16.4 # https://gitlab.com/gitlab-org/gitlab/-/issues/338838 def sync_http_integration! - return unless manual_configuration_changed? + return unless manual_configuration_changed? && !manual_configuration_was.nil? project.alert_management_http_integrations .for_endpoint_identifier('legacy-prometheus') diff --git a/app/models/integrations/pushover.rb b/app/models/integrations/pushover.rb index e97c7e5e738..2feae29f627 100644 --- a/app/models/integrations/pushover.rb +++ b/app/models/integrations/pushover.rb @@ -125,5 +125,9 @@ module Integrations Gitlab::HTTP.post('/messages.json', base_uri: BASE_URI, body: pushover_data) end + + def avatar_url + ActionController::Base.helpers.image_path('illustrations/third-party-logos/integrations-logos/pushover.svg') + end end end diff --git a/app/models/integrations/telegram.rb b/app/models/integrations/telegram.rb index 7c196720386..71fe6f8d6ef 100644 --- a/app/models/integrations/telegram.rb +++ b/app/models/integrations/telegram.rb @@ -26,6 +26,12 @@ module Integrations section: SECTION_TYPE_CONFIGURATION, help: 'If selected, successful pipelines do not trigger a notification event.' + field :branches_to_be_notified, + type: :select, + section: SECTION_TYPE_CONFIGURATION, + title: -> { s_('Integrations|Branches for which notifications are to be sent') }, + choices: -> { branch_choices } + with_options if: :activated? do validates :token, :room, presence: true end @@ -60,6 +66,10 @@ module Integrations super - ['deployment'] end + def avatar_url + ActionController::Base.helpers.image_path('illustrations/third-party-logos/integrations-logos/telegram.svg') + end + private def set_webhook diff --git a/app/models/issue.rb b/app/models/issue.rb index 58383a6a329..b207785021d 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -543,7 +543,9 @@ class Issue < ApplicationRecord end end - def related_issues(current_user, preload: nil) + def related_issues(current_user = nil, authorize: true, preload: nil) + return [] if new_record? + related_issues = linked_issues_select .joins("INNER JOIN issue_links ON @@ -554,6 +556,7 @@ class Issue < ApplicationRecord .reorder('issue_link_id') related_issues = yield related_issues if block_given? + return related_issues unless authorize cross_project_filter = -> (issues) { issues.where(project: project) } Ability.issues_readable_by_user(related_issues, @@ -561,6 +564,10 @@ class Issue < ApplicationRecord filters: { read_cross_project: cross_project_filter }) end + def linked_items_count + related_issues(authorize: false).size + end + def can_be_worked_on? !self.closed? && !self.project.forked? end @@ -688,20 +695,14 @@ class Issue < ApplicationRecord # for performance reasons, check commit: 002ad215818450d2cbbc5fa065850a953dc7ada8 # Make sure to sync this method with issue_policy.rb def readable_by?(user) - if !project.issues_enabled? - false - elsif user.can_read_all_resources? - true - elsif project.personal? && project.team.owner?(user) + if user.can_read_all_resources? true - elsif confidential? && !assignee_or_author?(user) - project.member?(user, Gitlab::Access::REPORTER) elsif hidden? false - elsif project.public? || (project.internal? && !user.external?) - project.feature_available?(:issues, user) + elsif project + project_level_readable_by?(user) else - project.member?(user) + group_level_readable_by?(user) end end @@ -754,6 +755,31 @@ class Issue < ApplicationRecord private + def project_level_readable_by?(user) + if !project.issues_enabled? + false + elsif project.personal? && project.team.owner?(user) + true + elsif confidential? && !assignee_or_author?(user) + project.member?(user, Gitlab::Access::REPORTER) + elsif project.public? || (project.internal? && !user.external?) + project.feature_available?(:issues, user) + else + project.member?(user) + end + end + + def group_level_readable_by?(user) + # This should never happen as we don't support personal namespace level issues. Just additional safety. + return false unless namespace.is_a?(::Group) + + if confidential? && !assignee_or_author?(user) + namespace.member?(user, Gitlab::Access::REPORTER) + else + namespace.member?(user) + end + end + def due_date_after_start_date return unless start_date.present? && due_date.present? diff --git a/app/models/issue_user_mention.rb b/app/models/issue_user_mention.rb index ad0df0dca78..6c3bedfccca 100644 --- a/app/models/issue_user_mention.rb +++ b/app/models/issue_user_mention.rb @@ -5,5 +5,5 @@ class IssueUserMention < UserMention belongs_to :note include IgnorableColumns - ignore_column :note_id_convert_to_bigint, remove_with: '16.2', remove_after: '2023-07-22' + ignore_column :note_id_convert_to_bigint, remove_with: '16.7', remove_after: '2023-11-16' end diff --git a/app/models/lfs_download_object.rb b/app/models/lfs_download_object.rb index 046e47262dd..ec190ebf5d8 100644 --- a/app/models/lfs_download_object.rb +++ b/app/models/lfs_download_object.rb @@ -19,6 +19,15 @@ class LfsDownloadObject @headers = headers || {} end + def to_hash + { + oid: oid, + size: size, + link: link, + headers: headers + }.stringify_keys + end + def sanitized_uri @sanitized_uri ||= Gitlab::UrlSanitizer.new(link) end diff --git a/app/models/loose_foreign_keys/deleted_record.rb b/app/models/loose_foreign_keys/deleted_record.rb index 1d26c3c11e4..6af80686ec2 100644 --- a/app/models/loose_foreign_keys/deleted_record.rb +++ b/app/models/loose_foreign_keys/deleted_record.rb @@ -36,34 +36,24 @@ class LooseForeignKeys::DeletedRecord < Gitlab::Database::SharedModel enum status: { pending: 1, processed: 2 }, _prefix: :status def self.load_batch_for_table(table, batch_size) - if Feature.enabled?("loose_foreign_keys_batch_load_using_union") - partition_names = Gitlab::Database::PostgresPartitionedTable.each_partition(table_name).map(&:name) - - unions = partition_names.map do |partition_name| - partition_number = partition_name[/\d+/].to_i - - select(arel_table[Arel.star], arel_table[:partition].as('partition_number')) - .from("#{Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA}.#{partition_name} AS #{table_name}") - .for_table(table) - .where(partition: partition_number) - .status_pending - .consume_order - .limit(batch_size) - end - - select(arel_table[Arel.star]) - .from_union(unions, remove_duplicates: false, remove_order: false) - .limit(batch_size) - .to_a - else - # selecting partition as partition_number to workaround the sliding partitioning column ignore + partition_names = Gitlab::Database::PostgresPartitionedTable.each_partition(table_name).map(&:name) + + unions = partition_names.map do |partition_name| + partition_number = partition_name[/\d+/].to_i + select(arel_table[Arel.star], arel_table[:partition].as('partition_number')) + .from("#{Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA}.#{partition_name} AS #{table_name}") .for_table(table) + .where(partition: partition_number) .status_pending .consume_order .limit(batch_size) - .to_a end + + select(arel_table[Arel.star]) + .from_union(unions, remove_duplicates: false, remove_order: false) + .limit(batch_size) + .to_a end def self.mark_records_processed(records) diff --git a/app/models/member.rb b/app/models/member.rb index cdf40eaa8f5..77e283044ea 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -29,10 +29,8 @@ class Member < ApplicationRecord belongs_to :source, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations belongs_to :member_namespace, inverse_of: :namespace_members, foreign_key: 'member_namespace_id', class_name: 'Namespace' belongs_to :member_role - has_one :member_task delegate :name, :username, :email, :last_activity_on, to: :user, prefix: true - delegate :tasks_to_be_done, to: :member_task, allow_nil: true validates :expires_at, allow_blank: true, future_date: true validates :user, presence: true, unless: :invite? @@ -525,6 +523,7 @@ class Member < ApplicationRecord def validate_access_level_locked_for_member_role return unless member_role_id + return if member_role_changed? # it is ok to change the access level when changing member role if access_level_changed? errors.add(:access_level, _("cannot be changed since member is associated with a custom role")) @@ -577,12 +576,6 @@ class Member < ApplicationRecord def after_accept_invite post_create_hook - - run_after_commit_or_now do - if member_task - TasksToBeDone::CreateWorker.perform_async(member_task.id, created_by_id, [user_id.to_i]) - end - end end def after_decline_invite diff --git a/app/models/members/group_member.rb b/app/models/members/group_member.rb index 52b9c3a80e3..b5a590d646e 100644 --- a/app/models/members/group_member.rb +++ b/app/models/members/group_member.rb @@ -62,9 +62,13 @@ class GroupMember < Member return false unless access_level == Gitlab::Access::OWNER return last_owner unless last_owner.nil? - group.member_owners_excluding_project_bots.where.not( - group: group, user_id: user_id - ).empty? + owners = group.member_owners_excluding_project_bots + + owners.reject! do |member| + member.group == group && member.user_id == user_id + end + + owners.empty? end private diff --git a/app/models/members/last_group_owner_assigner.rb b/app/models/members/last_group_owner_assigner.rb index 45cd8d8b000..707cd7bf31c 100644 --- a/app/models/members/last_group_owner_assigner.rb +++ b/app/models/members/last_group_owner_assigner.rb @@ -22,7 +22,7 @@ class LastGroupOwnerAssigner end def owner_ids - @owner_ids ||= owners.where(id: member_ids).ids + @owner_ids ||= member_ids & owners.map(&:id) end def member_ids @@ -30,6 +30,6 @@ class LastGroupOwnerAssigner end def owners - @owners ||= group.member_owners_excluding_project_bots.load + @owners ||= group.member_owners_excluding_project_bots end end diff --git a/app/models/members/member_task.rb b/app/models/members/member_task.rb deleted file mode 100644 index 6cf6b1adb45..00000000000 --- a/app/models/members/member_task.rb +++ /dev/null @@ -1,44 +0,0 @@ -# frozen_string_literal: true - -class MemberTask < ApplicationRecord - TASKS = { - code: 0, - ci: 1, - issues: 2 - }.freeze - - belongs_to :member - belongs_to :project - - validates :member, :project, presence: true - validates :tasks, inclusion: { in: TASKS.values } - validate :tasks_uniqueness - validate :project_in_member_source - - scope :for_members, -> (members) { joins(:member).where(member: members) } - - def tasks_to_be_done - Array(self[:tasks]).map { |task| TASKS.key(task) } - end - - def tasks_to_be_done=(tasks) - self[:tasks] = Array(tasks).map do |task| - TASKS[task.to_sym] - end.uniq - end - - private - - def tasks_uniqueness - errors.add(:tasks, 'are not unique') unless Array(tasks).length == Array(tasks).uniq.length - end - - def project_in_member_source - case member - when GroupMember - errors.add(:project, _('is not in the member group')) unless project.namespace == member.source - when ProjectMember - errors.add(:project, _('is not the member project')) unless project == member.source - end - end -end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 6a72ed6476e..d9726e76c4b 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -337,15 +337,19 @@ class MergeRequest < ApplicationRecord scope :by_squash_commit_sha, -> (sha) do where(squash_commit_sha: sha) end - scope :by_merge_or_squash_commit_sha, -> (sha) do - from_union([by_squash_commit_sha(sha), by_merge_commit_sha(sha)]) + scope :by_merged_commit_sha, -> (sha) do + where(merged_commit_sha: sha) + end + scope :by_merged_or_merge_or_squash_commit_sha, -> (sha) do + from_union([by_squash_commit_sha(sha), by_merge_commit_sha(sha), by_merged_commit_sha(sha)]) end scope :by_related_commit_sha, -> (sha) do from_union( [ by_commit_sha(sha), by_squash_commit_sha(sha), - by_merge_commit_sha(sha) + by_merge_commit_sha(sha), + by_merged_commit_sha(sha) ] ) end @@ -1231,19 +1235,23 @@ class MergeRequest < ApplicationRecord } end - def mergeable?(skip_ci_check: false, skip_discussions_check: false, skip_approved_check: false, check_mergeability_retry_lease: false, skip_rebase_check: false) + def mergeable?( + skip_ci_check: false, skip_discussions_check: false, skip_approved_check: false, check_mergeability_retry_lease: false, + skip_draft_check: false, skip_rebase_check: false, skip_blocked_check: false) + return false unless mergeable_state?( skip_ci_check: skip_ci_check, skip_discussions_check: skip_discussions_check, - skip_approved_check: skip_approved_check + skip_draft_check: skip_draft_check, + skip_approved_check: skip_approved_check, + skip_blocked_check: skip_blocked_check ) check_mergeability(sync_retry_lease: check_mergeability_retry_lease) - - can_be_merged? && (!should_be_rebased? || skip_rebase_check) + mergeable_git_state?(skip_rebase_check: skip_rebase_check) end - def mergeability_checks + def self.mergeable_state_checks # We want to have the cheapest checks first in the list, that way we can # fail fast before running the more expensive ones. # @@ -1256,17 +1264,52 @@ class MergeRequest < ApplicationRecord ] end - def mergeable_state?(skip_ci_check: false, skip_discussions_check: false, skip_approved_check: false) + def self.mergeable_git_state_checks + [ + ::MergeRequests::Mergeability::CheckConflictStatusService, + ::MergeRequests::Mergeability::CheckRebaseStatusService + ] + end + + def self.all_mergeability_checks + mergeable_state_checks + mergeable_git_state_checks + end + + def mergeable_state?( + skip_ci_check: false, skip_discussions_check: false, skip_approved_check: false, + skip_draft_check: false, skip_blocked_check: false) additional_checks = execute_merge_checks( + self.class.mergeable_state_checks, params: { skip_ci_check: skip_ci_check, skip_discussions_check: skip_discussions_check, - skip_approved_check: skip_approved_check + skip_approved_check: skip_approved_check, + skip_draft_check: skip_draft_check, + skip_blocked_check: skip_blocked_check } ) additional_checks.success? end + def mergeable_git_state?(skip_rebase_check: false) + checks = execute_merge_checks( + self.class.mergeable_git_state_checks, + params: { + skip_rebase_check: skip_rebase_check + } + ) + + checks.success? + end + + def all_mergeability_checks_results + execute_merge_checks( + self.class.all_mergeability_checks, + params: {}, + execute_all: true + ).payload[:results] + end + def ff_merge_possible? project.repository.ancestor?(target_branch_sha, diff_head_sha) end @@ -1689,7 +1732,7 @@ class MergeRequest < ApplicationRecord end def has_terraform_reports? - actual_head_pipeline&.complete_and_has_reports?(Ci::JobArtifact.of_report_type(:terraform)) + actual_head_pipeline&.has_reports?(Ci::JobArtifact.of_report_type(:terraform)) end def compare_accessibility_reports @@ -1957,15 +2000,11 @@ class MergeRequest < ApplicationRecord end def base_pipeline - @base_pipeline ||= project.ci_pipelines - .order(id: :desc) - .find_by(sha: diff_base_sha, ref: target_branch) + @base_pipeline ||= base_pipelines.last end def merge_base_pipeline - @merge_base_pipeline ||= project.ci_pipelines - .order(id: :desc) - .find_by(sha: actual_head_pipeline.target_sha, ref: target_branch) + @merge_base_pipeline ||= merge_base_pipelines.last end def discussions_rendered_on_frontend? @@ -2081,9 +2120,11 @@ class MergeRequest < ApplicationRecord false # Overridden in EE end - def execute_merge_checks(params: {}) + def execute_merge_checks(checks, params: {}, execute_all: false) # rubocop: disable CodeReuse/ServiceClass - MergeRequests::Mergeability::RunChecksService.new(merge_request: self, params: params).execute + MergeRequests::Mergeability::RunChecksService + .new(merge_request: self, params: params) + .execute(checks, execute_all: execute_all) # rubocop: enable CodeReuse/ServiceClass end @@ -2115,10 +2156,35 @@ class MergeRequest < ApplicationRecord !squash && target_project.squash_always? end + def current_patch_id_sha + return merge_request_diff.patch_id_sha if merge_request_diff.patch_id_sha.present? + + base_sha = diff_refs&.base_sha + head_sha = diff_refs&.head_sha + + return unless base_sha && head_sha + return if base_sha == head_sha + + project.repository.get_patch_id(base_sha, head_sha) + end + private attr_accessor :skip_fetch_ref + def merge_base_pipelines + target_branch_pipelines_for(sha: actual_head_pipeline.target_sha) + end + + def base_pipelines + target_branch_pipelines_for(sha: diff_base_sha) + end + + def target_branch_pipelines_for(sha:) + project.ci_pipelines + .where(sha: sha, ref: target_branch) + end + def set_draft_status self.draft = draft? end diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index bddc03d8b21..900f4bcfeb2 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -210,6 +210,8 @@ class MergeRequestDiff < ApplicationRecord # and save it to the database as serialized data def save_git_content ensure_commit_shas + set_patch_id_sha + save_commits save_diffs @@ -223,6 +225,16 @@ class MergeRequestDiff < ApplicationRecord keep_around_commits unless importing? end + def set_patch_id_sha + return unless base_commit_sha && head_commit_sha + return if base_commit_sha == head_commit_sha + + self.patch_id_sha = project.repository&.get_patch_id( + base_commit_sha, + head_commit_sha + ) + end + def set_as_latest_diff # Don't set merge_head diff as latest so it won't get considered as the # MergeRequest#merge_request_diff. diff --git a/app/models/merge_request_user_mention.rb b/app/models/merge_request_user_mention.rb index 3157f1ca2aa..548a91162cd 100644 --- a/app/models/merge_request_user_mention.rb +++ b/app/models/merge_request_user_mention.rb @@ -3,7 +3,7 @@ class MergeRequestUserMention < UserMention include IgnorableColumns - ignore_column :note_id_convert_to_bigint, remove_with: '16.2', remove_after: '2023-07-22' + ignore_column :note_id_convert_to_bigint, remove_with: '16.7', remove_after: '2023-11-16' belongs_to :merge_request belongs_to :note diff --git a/app/models/milestone.rb b/app/models/milestone.rb index eb0da368c7b..d5b9a4dc30f 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -54,6 +54,7 @@ class Milestone < ApplicationRecord scope :order_by_name_asc, -> { order(Arel::Nodes::Ascending.new(arel_table[:title].lower)) } scope :reorder_by_due_date_asc, -> { reorder(arel_table[:due_date].asc.nulls_last) } scope :with_api_entity_associations, -> { preload(project: [:project_feature, :route, namespace: :route]) } + scope :preload_for_indexing, -> { includes(project: [:project_feature]) } scope :order_by_dates_and_title, -> { order(due_date: :asc, start_date: :asc, title: :asc) } validates :group, presence: true, unless: :project diff --git a/app/models/ml/model.rb b/app/models/ml/model.rb index fb15b9fea72..27f03ed5857 100644 --- a/app/models/ml/model.rb +++ b/app/models/ml/model.rb @@ -19,6 +19,11 @@ module Ml has_one :latest_version, -> { latest_by_model }, class_name: 'Ml::ModelVersion', inverse_of: :model scope :including_latest_version, -> { includes(:latest_version) } + scope :with_version_count, -> { + left_outer_joins(:versions) + .select("ml_models.*, count(ml_model_versions.id) as version_count") + .group(:id) + } scope :by_project, ->(project) { where(project_id: project.id) } def valid_default_experiment? @@ -32,5 +37,9 @@ module Ml create_with(default_experiment: experiment) .find_or_create_by(project: project, name: name) end + + def self.by_project_id_and_id(project_id, id) + find_by(project_id: project_id, id: id) + end end end diff --git a/app/models/namespace.rb b/app/models/namespace.rb index ea0ea4de5b5..733b89fcaf2 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -6,7 +6,6 @@ class Namespace < ApplicationRecord include Gitlab::VisibilityLevel include Routable include AfterCommitQueue - include Storage::LegacyNamespace include Gitlab::SQL::Pattern include FeatureGate include FromUnion @@ -18,6 +17,9 @@ class Namespace < ApplicationRecord include Ci::NamespaceSettings include Referable include CrossDatabaseIgnoredTables + include IgnorableColumns + + ignore_column :unlock_membership_to_ldap, remove_with: '16.7', remove_after: '2023-11-16' cross_database_ignore_tables %w[routes redirect_routes], url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/424277' @@ -97,7 +99,10 @@ class Namespace < ApplicationRecord validates :path, presence: true, length: { maximum: URL_MAX_LENGTH } - validate :container_registry_namespace_path_validation + + validates :path, + format: { with: Gitlab::Regex.oci_repository_path_regex, message: Gitlab::Regex.oci_repository_path_regex_message }, + if: :path_changed? validates :path, namespace_path: true, if: ->(n) { !n.project_namespace? } # Project path validator is used for project namespaces for now to assure @@ -147,7 +152,6 @@ class Namespace < ApplicationRecord before_update :sync_share_with_group_lock_with_parent, if: :parent_changed? after_update :force_share_with_group_lock_on_descendants, if: -> { saved_change_to_share_with_group_lock? && share_with_group_lock? } after_update :expire_first_auto_devops_config_cache, if: -> { saved_change_to_auto_devops_enabled? } - after_update :move_dir, if: :saved_change_to_path_or_parent?, unless: -> { is_a?(Namespaces::ProjectNamespace) } after_save :reload_namespace_details @@ -155,8 +159,6 @@ class Namespace < ApplicationRecord after_sync_traversal_ids :schedule_sync_event_worker # custom callback defined in Namespaces::Traversal::Linear - # Legacy Storage specific hooks - after_commit :expire_child_caches, on: :update, if: -> { Feature.enabled?(:cached_route_lookups, self, type: :ops) && saved_change_to_name? || saved_change_to_path? || saved_change_to_parent_id? @@ -289,13 +291,6 @@ class Namespace < ApplicationRecord "#{self.class.reference_prefix}#{full_path}" end - def container_registry_namespace_path_validation - return if Feature.disabled?(:restrict_special_characters_in_namespace_path, self) - return if !path_changed? || path.match?(Gitlab::Regex.oci_repository_path_regex) - - errors.add(:path, Gitlab::Regex.oci_repository_path_regex_message) - end - def package_settings package_setting_relation || build_package_setting_relation end @@ -313,7 +308,7 @@ class Namespace < ApplicationRecord end def human_name - owner_name + owner_name || path end def any_project_has_container_registry_tags? diff --git a/app/models/namespace/detail.rb b/app/models/namespace/detail.rb index a65027733e9..f5e850830bc 100644 --- a/app/models/namespace/detail.rb +++ b/app/models/namespace/detail.rb @@ -1,13 +1,6 @@ # frozen_string_literal: true class Namespace::Detail < ApplicationRecord - include IgnorableColumns - - ignore_column :dashboard_notification_at, remove_with: '16.5', remove_after: '2023-08-22' - ignore_column :dashboard_enforcement_at, remove_with: '16.5', remove_after: '2023-08-22' - ignore_column :next_over_limit_check_at, remove_with: '16.5', remove_after: '2023-08-22' - ignore_column :free_user_cap_over_limit_notified_at, remove_with: '16.5', remove_after: '2023-08-22' - belongs_to :namespace, inverse_of: :namespace_details validates :namespace, presence: true validates :description, length: { maximum: 255 } diff --git a/app/models/namespace_setting.rb b/app/models/namespace_setting.rb index 8d5d788c738..3befcdeaec5 100644 --- a/app/models/namespace_setting.rb +++ b/app/models/namespace_setting.rb @@ -10,7 +10,7 @@ class NamespaceSetting < ApplicationRecord belongs_to :namespace, inverse_of: :namespace_settings enum jobs_to_be_done: { basics: 0, move_repository: 1, code_storage: 2, exploring: 3, ci: 4, other: 5 }, _suffix: true - enum enabled_git_access_protocol: { all: 0, ssh: 1, http: 2 }, _suffix: true + enum enabled_git_access_protocol: { all: 0, ssh: 1, http: 2, ssh_certificates: 3 }, _suffix: true attribute :default_branch_protection_defaults, default: -> { {} } diff --git a/app/models/namespaces/traversal/linear.rb b/app/models/namespaces/traversal/linear.rb index 1ca3c8e85f3..c3348c49ea1 100644 --- a/app/models/namespaces/traversal/linear.rb +++ b/app/models/namespaces/traversal/linear.rb @@ -61,8 +61,6 @@ module Namespaces # INPUT: [[4909902], [4909902,51065789], [4909902,51065793], [7135830], [15599674, 1], [15599674, 1, 3], [15599674, 2]] # RESULT: [[4909902], [7135830], [15599674, 1], [15599674, 2]] def shortest_traversal_ids_prefixes - raise ArgumentError, 'Feature not supported since the `:use_traversal_ids` is disabled' unless use_traversal_ids? - prefixes = [] # The array needs to be sorted (O(nlogn)) to ensure shortest elements are always first @@ -91,8 +89,6 @@ module Namespaces end def use_traversal_ids? - return false unless Feature.enabled?(:use_traversal_ids) - traversal_ids.present? end diff --git a/app/models/namespaces/traversal/linear_scopes.rb b/app/models/namespaces/traversal/linear_scopes.rb index 6e79e3ac9a1..c63639e721a 100644 --- a/app/models/namespaces/traversal/linear_scopes.rb +++ b/app/models/namespaces/traversal/linear_scopes.rb @@ -12,14 +12,10 @@ module Namespaces # list of namespace IDs, it can be faster to reference the ID in # traversal_ids than the primary key ID column. def as_ids - return super unless use_traversal_ids? - select(Arel.sql('namespaces.traversal_ids[array_length(namespaces.traversal_ids, 1)]').as('id')) end def roots - return super unless use_traversal_ids? - root_ids = all.select("#{quoted_table_name}.traversal_ids[1]").distinct unscoped.where(id: root_ids) end @@ -37,20 +33,14 @@ module Namespaces end def self_and_descendants(include_self: true) - return super unless use_traversal_ids? - self_and_descendants_with_comparison_operators(include_self: include_self) end def self_and_descendant_ids(include_self: true) - return super unless use_traversal_ids? - self_and_descendants(include_self: include_self).as_ids end def self_and_hierarchy - return super unless use_traversal_ids_for_self_and_hierarchy_scopes? - unscoped.from_union([all.self_and_ancestors, all.self_and_descendants(include_self: false)]) end @@ -74,15 +64,6 @@ module Namespaces private - def use_traversal_ids? - Feature.enabled?(:use_traversal_ids) - end - - def use_traversal_ids_for_self_and_hierarchy_scopes? - Feature.enabled?(:use_traversal_ids_for_self_and_hierarchy_scopes) && - use_traversal_ids? - end - def self_and_ancestors_from_inner_join(include_self: true, upto: nil, hierarchy_order: nil) base_cte = all.reselect('namespaces.traversal_ids').as_cte(:base_ancestors_cte) diff --git a/app/models/note.rb b/app/models/note.rb index 8fc45436dc7..eae7a40fb4e 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -26,7 +26,7 @@ class Note < ApplicationRecord include IgnorableColumns include Spammable - ignore_column :id_convert_to_bigint, remove_with: '16.3', remove_after: '2023-08-22' + ignore_column :id_convert_to_bigint, remove_with: '16.7', remove_after: '2023-11-16' ISSUE_TASK_SYSTEM_NOTE_PATTERN = /\A.*marked\sthe\stask.+as\s(completed|incomplete).*\z/ @@ -105,7 +105,7 @@ class Note < ApplicationRecord validates :note, presence: true validates :note, length: { maximum: Gitlab::Database::MAX_TEXT_SIZE_LIMIT } validates :project, presence: true, if: :for_project_noteable? - validates :namespace, presence: true + validates :namespace, presence: true, unless: :for_abuse_report? # Attachments are deprecated and are handled by Markdown uploader validates :attachment, file_size: { maximum: :max_attachment_size } @@ -383,7 +383,7 @@ class Note < ApplicationRecord end def for_project_noteable? - !for_personal_snippet? + !(for_personal_snippet? || for_abuse_report?) end def for_design? @@ -394,6 +394,10 @@ class Note < ApplicationRecord for_issue? || for_merge_request? end + def for_abuse_report? + noteable_type == AbuseReport.name + end + def skip_project_check? !for_project_noteable? end @@ -830,7 +834,11 @@ class Note < ApplicationRecord def ensure_namespace_id return if namespace_id.present? && !noteable_changed? && !project_changed? - self.namespace_id = if for_project_noteable? + self.namespace_id = if for_issue? + # Some issues are not project noteables (e.g. group-level work items) + # so we need this separate condition + noteable&.namespace_id + elsif for_project_noteable? project&.project_namespace_id elsif for_personal_snippet? noteable&.author&.namespace&.id diff --git a/app/models/note_diff_file.rb b/app/models/note_diff_file.rb index b0f6af0d853..624a722e369 100644 --- a/app/models/note_diff_file.rb +++ b/app/models/note_diff_file.rb @@ -4,7 +4,7 @@ class NoteDiffFile < ApplicationRecord include DiffFile include IgnorableColumns - ignore_column :diff_note_id_convert_to_bigint, remove_with: '16.2', remove_after: '2023-07-22' + ignore_column :diff_note_id_convert_to_bigint, remove_with: '16.7', remove_after: '2023-11-16' scope :referencing_sha, -> (oids, project_id:) do joins(:diff_note).where(notes: { project_id: project_id, commit_id: oids }) diff --git a/app/models/packages/protection/rule.rb b/app/models/packages/protection/rule.rb index bb65be92b90..582b51475c2 100644 --- a/app/models/packages/protection/rule.rb +++ b/app/models/packages/protection/rule.rb @@ -4,18 +4,43 @@ module Packages module Protection class Rule < ApplicationRecord enum package_type: Packages::Package.package_types.slice(:npm) + enum push_protected_up_to_access_level: + Gitlab::Access.sym_options_with_owner.slice(:developer, :maintainer, :owner), + _prefix: :push_protected_up_to belongs_to :project, inverse_of: :package_protection_rules validates :package_name_pattern, presence: true, uniqueness: { scope: [:project_id, :package_type] }, length: { maximum: 255 } validates :package_type, presence: true - validates :push_protected_up_to_access_level, presence: true, - inclusion: { in: [ - Gitlab::Access::DEVELOPER, - Gitlab::Access::MAINTAINER, - Gitlab::Access::OWNER - ] } + validates :push_protected_up_to_access_level, presence: true + + before_save :set_package_name_pattern_ilike_query, if: :package_name_pattern_changed? + + scope :for_package_name, ->(package_name) { + return none if package_name.blank? + + where(":package_name ILIKE package_name_pattern_ilike_query", package_name: package_name) + } + + def self.push_protected_from?(access_level:, package_name:, package_type:) + return true if [access_level, package_name, package_type].any?(&:blank?) + + where(package_type: package_type, push_protected_up_to_access_level: access_level..) + .for_package_name(package_name) + .exists? + end + + private + + # We want to allow wildcard pattern (`*`) for the field `package_name_pattern` + # , e.g. `@my-scope/my-package-*`, etc. + # Therefore, we need to preprocess the field value before we can use the field in the ILIKE clause. + # E.g. convert wildcard character (`*`) to LIKE match character (`%`), escape certain characters, etc. + def set_package_name_pattern_ilike_query + self.package_name_pattern_ilike_query = self.class.sanitize_sql_like(package_name_pattern) + .tr('*', '%') + end end end end diff --git a/app/models/pages/lookup_path.rb b/app/models/pages/lookup_path.rb index e8becc833ca..8a02415aef4 100644 --- a/app/models/pages/lookup_path.rb +++ b/app/models/pages/lookup_path.rb @@ -55,6 +55,12 @@ module Pages strong_memoize_attr :prefix def unique_host + # When serving custom domain we don't present the unique host to avoid + # GitLab Pages auto-redirect to the unique domain instead of keeping serving + # from the custom domain. + # https://gitlab.com/gitlab-org/gitlab/-/issues/426435 + return if domain.present? + url_builder.unique_host end strong_memoize_attr :unique_host diff --git a/app/models/pages_deployment.rb b/app/models/pages_deployment.rb index de7b2416258..f05ed2aac6e 100644 --- a/app/models/pages_deployment.rb +++ b/app/models/pages_deployment.rb @@ -6,8 +6,6 @@ class PagesDeployment < ApplicationRecord include FileStoreMounter include Gitlab::Utils::StrongMemoize - MIGRATED_FILE_NAME = "_migrated.zip" - attribute :file_store, :integer, default: -> { ::Pages::DeploymentUploader.default_store } belongs_to :project, optional: false @@ -16,11 +14,11 @@ class PagesDeployment < ApplicationRecord belongs_to :ci_build, class_name: 'Ci::Build', optional: true scope :older_than, ->(id) { where('id < ?', id) } - scope :migrated_from_legacy_storage, -> { where(file: MIGRATED_FILE_NAME) } scope :with_files_stored_locally, -> { where(file_store: ::ObjectStorage::Store::LOCAL) } scope :with_files_stored_remotely, -> { where(file_store: ::ObjectStorage::Store::REMOTE) } scope :project_id_in, ->(ids) { where(project_id: ids) } scope :active, -> { where(deleted_at: nil) } + scope :deactivated, -> { where('deleted_at < ?', Time.now.utc) } validates :file, presence: true validates :file_store, presence: true, inclusion: { in: ObjectStorage::SUPPORTED_STORES } @@ -43,10 +41,6 @@ class PagesDeployment < ApplicationRecord .update_all(updated_at: now, deleted_at: time || now) end - def migrated? - file.filename == MIGRATED_FILE_NAME - end - private def set_size diff --git a/app/models/plan_limits.rb b/app/models/plan_limits.rb index 245c0719439..478fc1c418a 100644 --- a/app/models/plan_limits.rb +++ b/app/models/plan_limits.rb @@ -7,7 +7,6 @@ class PlanLimits < ApplicationRecord ignore_column :ci_max_artifact_size_running_container_scanning, remove_with: '14.3', remove_after: '2021-08-22' ignore_column :web_hook_calls_high, remove_with: '15.10', remove_after: '2022-02-22' - ignore_column :ci_active_pipelines, remove_with: '16.3', remove_after: '2022-07-22' attribute :limits_history, :ind_jsonb, default: -> { {} } validates :limits_history, json_schema: { filename: 'plan_limits_history' } diff --git a/app/models/preloaders/group_root_ancestor_preloader.rb b/app/models/preloaders/group_root_ancestor_preloader.rb index 29c60e90964..410f48c8176 100644 --- a/app/models/preloaders/group_root_ancestor_preloader.rb +++ b/app/models/preloaders/group_root_ancestor_preloader.rb @@ -8,8 +8,6 @@ module Preloaders end def execute - return unless ::Feature.enabled?(:use_traversal_ids) - # type == 'Group' condition located on subquery to prevent a filter in the query root_query = Namespace.joins("INNER JOIN (#{join_sql}) as root_query ON root_query.root_id = namespaces.id") .select('namespaces.*, root_query.id as source_id') diff --git a/app/models/preloaders/project_root_ancestor_preloader.rb b/app/models/preloaders/project_root_ancestor_preloader.rb index ccb9d2eab98..1e96e139f94 100644 --- a/app/models/preloaders/project_root_ancestor_preloader.rb +++ b/app/models/preloaders/project_root_ancestor_preloader.rb @@ -10,7 +10,6 @@ module Preloaders def execute return unless @projects.is_a?(ActiveRecord::Relation) - return unless ::Feature.enabled?(:use_traversal_ids) root_query = Namespace.joins("INNER JOIN (#{join_sql}) as root_query ON root_query.root_id = namespaces.id") .select('namespaces.*, root_query.id as source_id') diff --git a/app/models/preloaders/user_max_access_level_in_groups_preloader.rb b/app/models/preloaders/user_max_access_level_in_groups_preloader.rb index 16d46facb96..aaa54e0228b 100644 --- a/app/models/preloaders/user_max_access_level_in_groups_preloader.rb +++ b/app/models/preloaders/user_max_access_level_in_groups_preloader.rb @@ -10,27 +10,11 @@ module Preloaders end def execute - if ::Feature.enabled?(:use_traversal_ids) - preload_with_traversal_ids - else - preload_direct_memberships - end + preload_with_traversal_ids end private - def preload_direct_memberships - group_memberships = GroupMember.active_without_invites_and_requests - .where(user: @user, source_id: @groups) - .group(:source_id) - .maximum(:access_level) - - @groups.each do |group| - access_level = group_memberships[group.id] - group.merge_value_to_request_store(User, @user.id, access_level) if access_level.present? - end - end - def preload_with_traversal_ids # Diagrammatic representation of this step: # https://gitlab.com/gitlab-org/gitlab/-/merge_requests/111157#note_1271550140 diff --git a/app/models/project.rb b/app/models/project.rb index 5989584ce43..fd226d23e77 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -390,6 +390,7 @@ class Project < ApplicationRecord has_many :alert_management_alerts, class_name: 'AlertManagement::Alert', inverse_of: :project has_many :alert_management_http_integrations, class_name: 'AlertManagement::HttpIntegration', inverse_of: :project + has_many :container_registry_protection_rules, class_name: 'ContainerRegistry::Protection::Rule', inverse_of: :project # Container repositories need to remove data from the container registry, # which is not managed by the DB. Hence we're still using dependent: :destroy # here. @@ -559,13 +560,16 @@ class Project < ApplicationRecord allow_blank: true validates :name, presence: true, - length: { maximum: 255 }, - format: { with: Gitlab::Regex.project_name_regex, - message: Gitlab::Regex.project_name_regex_message } + length: { maximum: 255 } validates :path, presence: true, project_path: true, length: { maximum: 255 } + + validates :name, + format: { with: Gitlab::Regex.project_name_regex, + message: Gitlab::Regex.project_name_regex_message }, + if: :name_changed? validates :path, format: { with: Gitlab::Regex.oci_repository_path_regex, message: Gitlab::Regex.oci_repository_path_regex_message }, @@ -749,6 +753,7 @@ class Project < ApplicationRecord scope :service_desk_enabled, -> { where(service_desk_enabled: true) } scope :with_builds_enabled, -> { with_feature_enabled(:builds) } scope :with_issues_enabled, -> { with_feature_enabled(:issues) } + scope :with_package_registry_enabled, -> { with_feature_enabled(:package_registry) } scope :with_issues_available_for_user, ->(current_user) { with_feature_available_for_user(:issues, current_user) } scope :with_merge_requests_available_for_user, ->(current_user) { with_feature_available_for_user(:merge_requests, current_user) } scope :with_issues_or_mrs_available_for_user, -> (user) do @@ -1449,7 +1454,7 @@ class Project < ApplicationRecord super(import_url.sanitized_url) credentials = import_url.credentials.to_h.transform_values { |value| CGI.unescape(value.to_s) } - create_or_update_import_data(credentials: credentials) + build_or_assign_import_data(credentials: credentials) else super(value) end @@ -1470,9 +1475,7 @@ class Project < ApplicationRecord valid?(:import_url) || errors.messages[:import_url].nil? end - # TODO: rename to build_or_assign_import_data as it doesn't save record - # https://gitlab.com/gitlab-org/gitlab/-/issues/377319 - def create_or_update_import_data(data: nil, credentials: nil) + def build_or_assign_import_data(data: nil, credentials: nil) return if data.nil? && credentials.nil? project_import_data = import_data || build_import_data @@ -2236,15 +2239,6 @@ class Project < ApplicationRecord pages_metadatum&.deployed? end - def pages_path - # TODO: when we migrate Pages to work with new storage types, change here to use disk_path - File.join(Settings.pages.path, full_path) - end - - def pages_available? - Gitlab.config.pages.enabled - end - def pages_show_onboarding? !(pages_metadatum&.onboarding_complete || pages_metadatum&.deployed) end @@ -2693,26 +2687,6 @@ class Project < ApplicationRecord self.merge_requests_ff_only_enabled || self.merge_requests_rebase_enabled end - def migrate_to_hashed_storage! - return unless storage_upgradable? - - if git_transfer_in_progress? - HashedStorage::ProjectMigrateWorker.perform_in(Gitlab::ReferenceCounter::REFERENCE_EXPIRE_TIME, id) - else - HashedStorage::ProjectMigrateWorker.perform_async(id) - end - end - - def rollback_to_legacy_storage! - return if legacy_storage? - - if git_transfer_in_progress? - HashedStorage::ProjectRollbackWorker.perform_in(Gitlab::ReferenceCounter::REFERENCE_EXPIRE_TIME, id) - else - HashedStorage::ProjectRollbackWorker.perform_async(id) - end - end - override :git_transfer_in_progress? def git_transfer_in_progress? GL_REPOSITORY_TYPES.any? do |type| @@ -3195,10 +3169,6 @@ class Project < ApplicationRecord creator.banned? && team.max_member_access(creator.id) == Gitlab::Access::OWNER end - def content_editor_on_issues_feature_flag_enabled? - group&.content_editor_on_issues_feature_flag_enabled? || Feature.enabled?(:content_editor_on_issues, self) - end - def work_items_feature_flag_enabled? group&.work_items_feature_flag_enabled? || Feature.enabled?(:work_items, self) end @@ -3346,7 +3316,7 @@ class Project < ApplicationRecord end def merge_requests_allowing_collaboration(source_branch = nil) - relation = source_of_merge_requests.opened.where(allow_collaboration: true) + relation = source_of_merge_requests.from_fork.opened.where(allow_collaboration: true) relation = relation.where(source_branch: source_branch) if source_branch relation end diff --git a/app/models/project_authorization.rb b/app/models/project_authorization.rb index c328e7d37c8..4d0c6029235 100644 --- a/app/models/project_authorization.rb +++ b/app/models/project_authorization.rb @@ -11,6 +11,7 @@ class ProjectAuthorization < ApplicationRecord validates :access_level, inclusion: { in: Gitlab::Access.all_values }, presence: true validates :user, uniqueness: { scope: :project }, presence: true + scope :for_project, ->(projects) { where(project: projects) } scope :non_guests, -> { where('access_level > ?', ::Gitlab::Access::GUEST) } # TODO: To be removed after https://gitlab.com/gitlab-org/gitlab/-/issues/418205 diff --git a/app/models/project_import_data.rb b/app/models/project_import_data.rb index 7e0722ab68c..96c1ad7def8 100644 --- a/app/models/project_import_data.rb +++ b/app/models/project_import_data.rb @@ -5,6 +5,11 @@ require 'carrierwave/orm/activerecord' class ProjectImportData < ApplicationRecord prepend_mod_with('ProjectImportData') # rubocop: disable Cop/InjectEnterpriseEditionModule + # Timeout strategy can only be changed via API, currently only with GitHub and BitBucket Server + OPTIMISTIC_TIMEOUT = "optimistic" + PESSIMISTIC_TIMEOUT = "pessimistic" + TIMEOUT_STRATEGIES = [OPTIMISTIC_TIMEOUT, PESSIMISTIC_TIMEOUT].freeze + belongs_to :project, inverse_of: :import_data attr_encrypted :credentials, key: Settings.attr_encrypted_db_key_base, diff --git a/app/models/project_pages_metadatum.rb b/app/models/project_pages_metadatum.rb index 7a3ece4bc92..eca2e5a740e 100644 --- a/app/models/project_pages_metadatum.rb +++ b/app/models/project_pages_metadatum.rb @@ -12,6 +12,5 @@ class ProjectPagesMetadatum < ApplicationRecord belongs_to :pages_deployment scope :deployed, -> { where(deployed: true) } - scope :only_on_legacy_storage, -> { deployed.where(pages_deployment: nil) } scope :with_project_route_and_deployment, -> { preload(:pages_deployment, project: [:namespace, :route]) } end diff --git a/app/models/project_setting.rb b/app/models/project_setting.rb index 69d1a9f4aeb..d16fe996672 100644 --- a/app/models/project_setting.rb +++ b/app/models/project_setting.rb @@ -21,6 +21,8 @@ class ProjectSetting < ApplicationRecord jitsu_administrator_email ], remove_with: '16.5', remove_after: '2023-09-22' + ignore_column :jitsu_key, remove_with: '16.7', remove_after: '2023-11-17' + attr_encrypted :cube_api_key, mode: :per_attribute_iv, key: Settings.attr_encrypted_db_key_base_32, diff --git a/app/models/project_team.rb b/app/models/project_team.rb index 38521ae6090..586294f0dd0 100644 --- a/app/models/project_team.rb +++ b/app/models/project_team.rb @@ -43,15 +43,13 @@ class ProjectTeam member end - def add_members(users, access_level, current_user: nil, expires_at: nil, tasks_to_be_done: [], tasks_project_id: nil) + def add_members(users, access_level, current_user: nil, expires_at: nil) Members::Projects::CreatorService.add_members( # rubocop:disable CodeReuse/ServiceClass project, users, access_level, current_user: current_user, - expires_at: expires_at, - tasks_to_be_done: tasks_to_be_done, - tasks_project_id: tasks_project_id + expires_at: expires_at ) end diff --git a/app/models/repository.rb b/app/models/repository.rb index 1c27a7a64cf..e565de9c4ba 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -688,7 +688,7 @@ class Repository def head_tree(skip_flat_paths: true) return if empty? || root_ref.nil? - @head_tree ||= Tree.new(self, root_ref, nil, skip_flat_paths: skip_flat_paths) + @head_tree ||= Tree.new(self, root_ref, nil, skip_flat_paths: skip_flat_paths, ref_type: 'heads') end def tree(sha = :head, path = nil, recursive: false, skip_flat_paths: true, pagination_params: nil, ref_type: nil, rescue_not_found: true) @@ -1244,7 +1244,14 @@ class Repository def get_patch_id(old_revision, new_revision) raw_repository.get_patch_id(old_revision, new_revision) - rescue Gitlab::Git::CommandError + rescue Gitlab::Git::CommandError, Gitlab::Git::Repository::NoRepository => e + Gitlab::ErrorTracking.track_exception( + e, + project_id: project.id, + old_revision: old_revision, + new_revision: new_revision + ) + nil end @@ -1258,6 +1265,12 @@ class Repository Gitlab::Git::ObjectPool.init_from_gitaly(gitaly_object_pool, source_project&.repository) end + def get_file_attributes(revision, paths, attributes) + raw_repository + .get_file_attributes(revision, paths, attributes) + .map(&:to_h) + end + private def ancestor_cache_key(ancestor_id, descendant_id) diff --git a/app/models/resource_events/abuse_report_event.rb b/app/models/resource_events/abuse_report_event.rb index 59f88a63998..5881f87241d 100644 --- a/app/models/resource_events/abuse_report_event.rb +++ b/app/models/resource_events/abuse_report_event.rb @@ -16,7 +16,9 @@ module ResourceEvents close_report: 4, ban_user_and_close_report: 5, block_user_and_close_report: 6, - delete_user_and_close_report: 7 + delete_user_and_close_report: 7, + trust_user: 8, + trust_user_and_close_report: 9 } enum reason: { @@ -28,7 +30,8 @@ module ResourceEvents copyright: 6, malware: 7, other: 8, - unconfirmed: 9 + unconfirmed: 9, + trusted: 10 } def success_message diff --git a/app/models/service_desk/custom_email_credential.rb b/app/models/service_desk/custom_email_credential.rb index 8ccdd6f2261..5986ac8a43f 100644 --- a/app/models/service_desk/custom_email_credential.rb +++ b/app/models/service_desk/custom_email_credential.rb @@ -59,7 +59,7 @@ module ServiceDesk allow_localhost: false, allow_local_network: false ) - rescue Gitlab::UrlBlocker::BlockedUrlError => e + rescue Gitlab::HTTP_V2::UrlBlocker::BlockedUrlError => e errors.add(:smtp_address, e) end end diff --git a/app/models/service_list.rb b/app/models/service_list.rb deleted file mode 100644 index 8a52539d128..00000000000 --- a/app/models/service_list.rb +++ /dev/null @@ -1,27 +0,0 @@ -# frozen_string_literal: true - -class ServiceList - def initialize(batch, service_hash, association) - @batch = batch - @service_hash = service_hash - @association = association - end - - def to_array - [Integration, columns, values] - end - - private - - attr_reader :batch, :service_hash, :association - - def columns - service_hash.keys << "#{association}_id" - end - - def values - batch.select(:id).map do |record| - service_hash.values << record.id - end - end -end diff --git a/app/models/snippet.rb b/app/models/snippet.rb index d4f8c1b3b0b..78b0c0849e3 100644 --- a/app/models/snippet.rb +++ b/app/models/snippet.rb @@ -79,6 +79,10 @@ class Snippet < ApplicationRecord scope :with_statistics, -> { joins(:statistics) } scope :inc_projects_namespace_route, -> { includes(project: [:route, :namespace]) } + scope :without_created_by_banned_user, -> do + where_not_exists(Users::BannedUser.where('snippets.author_id = banned_users.user_id')) + end + attr_mentionable :description participant :author @@ -365,6 +369,10 @@ class Snippet < ApplicationRecord def multiple_files? list_files.size > 1 end + + def hidden_due_to_author_ban? + Feature.enabled?(:hide_snippets_of_banned_users) && author.banned? + end end Snippet.prepend_mod_with('Snippet') diff --git a/app/models/snippet_user_mention.rb b/app/models/snippet_user_mention.rb index 8ef2c579a5a..2b6845495bc 100644 --- a/app/models/snippet_user_mention.rb +++ b/app/models/snippet_user_mention.rb @@ -3,7 +3,7 @@ class SnippetUserMention < UserMention include IgnorableColumns - ignore_column :note_id_convert_to_bigint, remove_with: '16.2', remove_after: '2023-07-22' + ignore_column :note_id_convert_to_bigint, remove_with: '16.7', remove_after: '2023-11-16' belongs_to :snippet belongs_to :note diff --git a/app/models/ssh_host_key.rb b/app/models/ssh_host_key.rb index daa64f4e087..672a6d64127 100644 --- a/app/models/ssh_host_key.rb +++ b/app/models/ssh_host_key.rb @@ -157,7 +157,7 @@ class SshHostKey url.port = url.inferred_port [url, ip] - rescue Gitlab::UrlBlocker::BlockedUrlError + rescue Gitlab::HTTP_V2::UrlBlocker::BlockedUrlError raise ArgumentError, "Invalid URL" end diff --git a/app/models/storage/hashed.rb b/app/models/storage/hashed.rb index 05e93f00912..5cef033e672 100644 --- a/app/models/storage/hashed.rb +++ b/app/models/storage/hashed.rb @@ -31,10 +31,6 @@ module Storage "#{base_dir}/#{disk_hash}" if disk_hash end - def rename_repo(old_full_path: nil, new_full_path: nil) - true - end - private # Generates the hash for the repository path and name on disk diff --git a/app/models/storage/legacy_project.rb b/app/models/storage/legacy_project.rb index 0d12a629b8e..700314e277a 100644 --- a/app/models/storage/legacy_project.rb +++ b/app/models/storage/legacy_project.rb @@ -23,27 +23,5 @@ module Storage def disk_path project.full_path end - - def rename_repo(old_full_path: nil, new_full_path: nil) - old_full_path ||= project.full_path_before_last_save - new_full_path ||= project.build_full_path - - if gitlab_shell.mv_repository(repository_storage, old_full_path, new_full_path) - # If repository moved successfully we need to send update instructions to users. - # However we cannot allow rollback since we moved repository - # So we basically we mute exceptions in next actions - begin - gitlab_shell.mv_repository(repository_storage, "#{old_full_path}.wiki", "#{new_full_path}.wiki") - return true - rescue StandardError => e - Gitlab::AppLogger.error("Exception renaming #{old_full_path} -> #{new_full_path}: #{e}") - # Returning false does not rollback after_* transaction but gives - # us information about failing some of tasks - return false - end - end - - false - end end end diff --git a/app/models/suggestion.rb b/app/models/suggestion.rb index 58a154b8986..c4178d3c5f1 100644 --- a/app/models/suggestion.rb +++ b/app/models/suggestion.rb @@ -5,7 +5,7 @@ class Suggestion < ApplicationRecord include Suggestible include IgnorableColumns - ignore_column :note_id_convert_to_bigint, remove_with: '16.2', remove_after: '2023-07-22' + ignore_column :note_id_convert_to_bigint, remove_with: '16.7', remove_after: '2023-11-16' belongs_to :note, inverse_of: :suggestions validates :note, presence: true, unless: :importing? diff --git a/app/models/system/broadcast_message.rb b/app/models/system/broadcast_message.rb index 332baea4449..06f0115ade6 100644 --- a/app/models/system/broadcast_message.rb +++ b/app/models/system/broadcast_message.rb @@ -125,7 +125,7 @@ module System end def future? - starts_at > Time.current + starts_at.future? end def now_or_future? diff --git a/app/models/system_note_metadata.rb b/app/models/system_note_metadata.rb index 4e71a13a3a1..dc93decce5e 100644 --- a/app/models/system_note_metadata.rb +++ b/app/models/system_note_metadata.rb @@ -4,7 +4,7 @@ class SystemNoteMetadata < ApplicationRecord include Importable include IgnorableColumns - ignore_column :note_id_convert_to_bigint, remove_with: '16.2', remove_after: '2023-07-22' + ignore_column :note_id_convert_to_bigint, remove_with: '16.7', remove_after: '2023-11-16' # These notes's action text might contain a reference that is external. # We should always force a deep validation upon references that are found diff --git a/app/models/timelog.rb b/app/models/timelog.rb index eb72456b435..b6b4decc64b 100644 --- a/app/models/timelog.rb +++ b/app/models/timelog.rb @@ -5,7 +5,7 @@ class Timelog < ApplicationRecord include IgnorableColumns include Sortable - ignore_column :note_id_convert_to_bigint, remove_with: '16.2', remove_after: '2023-07-22' + ignore_column :note_id_convert_to_bigint, remove_with: '16.7', remove_after: '2023-11-16' before_save :set_project diff --git a/app/models/todo.rb b/app/models/todo.rb index d159b51a0eb..e64dbf83a4c 100644 --- a/app/models/todo.rb +++ b/app/models/todo.rb @@ -6,7 +6,7 @@ class Todo < ApplicationRecord include EachBatch include IgnorableColumns - ignore_column :note_id_convert_to_bigint, remove_with: '16.2', remove_after: '2023-07-22' + ignore_column :note_id_convert_to_bigint, remove_with: '16.7', remove_after: '2023-11-16' # Time to wait for todos being removed when not visible for user anymore. # Prevents TODOs being removed by mistake, for example, removing access from a user @@ -25,6 +25,7 @@ class Todo < ApplicationRecord REVIEW_REQUESTED = 9 MEMBER_ACCESS_REQUESTED = 10 REVIEW_SUBMITTED = 11 # This is an EE-only feature + OKR_CHECKIN_REQUESTED = 12 # This is an EE-only feature ACTION_NAMES = { ASSIGNED => :assigned, @@ -37,7 +38,8 @@ class Todo < ApplicationRecord DIRECTLY_ADDRESSED => :directly_addressed, MERGE_TRAIN_REMOVED => :merge_train_removed, MEMBER_ACCESS_REQUESTED => :member_access_requested, - REVIEW_SUBMITTED => :review_submitted + REVIEW_SUBMITTED => :review_submitted, + OKR_CHECKIN_REQUESTED => :okr_checkin_requested }.freeze ACTIONS_MULTIPLE_ALLOWED = [Todo::MENTIONED, Todo::DIRECTLY_ADDRESSED, Todo::MEMBER_ACCESS_REQUESTED].freeze @@ -78,6 +80,7 @@ class Todo < ApplicationRecord scope :for_type, -> (type) { where(target_type: type) } scope :for_target, -> (id) { where(target_id: id) } scope :for_commit, -> (id) { where(commit_id: id) } + scope :not_in_users, -> (user_ids) { where.not('todos.user_id' => user_ids) } scope :with_entity_associations, -> do preload(:target, :author, :note, group: :route, project: [:route, :group, { namespace: [:route, :owner] }, :project_setting]) end diff --git a/app/models/tree.rb b/app/models/tree.rb index 4d62334800d..030e7d9e85f 100644 --- a/app/models/tree.rb +++ b/app/models/tree.rb @@ -13,10 +13,10 @@ class Tree @repository = repository @sha = sha @path = path - @ref_type = ExtractsRef.ref_type(ref_type) + @ref_type = ExtractsRef::RefExtractor.ref_type(ref_type) git_repo = @repository.raw_repository - ref = ExtractsRef.qualify_ref(@sha, ref_type) + ref = ExtractsRef::RefExtractor.qualify_ref(@sha, ref_type) @entries, @cursor = Gitlab::Git::Tree.where(git_repo, ref, @path, recursive, skip_flat_paths, rescue_not_found, pagination_params) diff --git a/app/models/upload.rb b/app/models/upload.rb index a4fbc703146..59ce9a1f37a 100644 --- a/app/models/upload.rb +++ b/app/models/upload.rb @@ -2,6 +2,7 @@ class Upload < ApplicationRecord include Checksummable + include EachBatch # Upper limit for foreground checksum processing CHECKSUM_THRESHOLD = 100.megabytes diff --git a/app/models/user.rb b/app/models/user.rb index c4e867ab571..4034677509f 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -271,6 +271,7 @@ class User < MainClusterwide::ApplicationRecord has_many :bulk_imports has_many :custom_attributes, class_name: 'UserCustomAttribute' + has_one :trusted_with_spam_attribute, -> { UserCustomAttribute.trusted_with_spam }, class_name: 'UserCustomAttribute' has_many :callouts, class_name: 'Users::Callout' has_many :group_callouts, class_name: 'Users::GroupCallout' has_many :project_callouts, class_name: 'Users::ProjectCallout' @@ -306,6 +307,7 @@ class User < MainClusterwide::ApplicationRecord has_many :awarded_user_achievements, class_name: 'Achievements::UserAchievement', foreign_key: 'awarded_by_user_id', inverse_of: :awarded_by_user has_many :revoked_user_achievements, class_name: 'Achievements::UserAchievement', foreign_key: 'revoked_by_user_id', inverse_of: :revoked_by_user has_many :achievements, through: :user_achievements, class_name: 'Achievements::Achievement', inverse_of: :users + has_many :vscode_settings, class_name: 'VsCode::Settings::VsCodeSetting', inverse_of: :user # # Validations @@ -1234,10 +1236,6 @@ class User < MainClusterwide::ApplicationRecord authorized_projects(Gitlab::Access::REPORTER).non_archived.with_issues_enabled end - def preloaded_member_roles_for_projects(projects) - # overridden in EE - end - # rubocop: disable CodeReuse/ServiceClass def require_ssh_key? count = Users::KeysCountService.new(self).count @@ -2226,8 +2224,8 @@ class User < MainClusterwide::ApplicationRecord } end - def allow_possible_spam? - custom_attributes.by_key(UserCustomAttribute::ALLOW_POSSIBLE_SPAM).exists? + def trusted? + trusted_with_spam_attribute.present? end def namespace_commit_email_for_namespace(namespace) @@ -2511,14 +2509,6 @@ class User < MainClusterwide::ApplicationRecord def ci_namespace_mirrors_for_group_members(level) search_members = group_members.where('access_level >= ?', level) - # This reduces searched prefixes to only shortest ones - # to avoid querying descendants since they are already covered - # by ancestor namespaces. If the FF is not available fallback to - # inefficient search: https://gitlab.com/gitlab-org/gitlab/-/issues/336436 - unless Feature.enabled?(:use_traversal_ids) - return Ci::NamespaceMirror.contains_any_of_namespaces(search_members.pluck(:source_id)) - end - traversal_ids = Group.joins(:all_group_members) .merge(search_members) .shortest_traversal_ids_prefixes diff --git a/app/models/user_custom_attribute.rb b/app/models/user_custom_attribute.rb index 15d50071bf6..728c1f4844a 100644 --- a/app/models/user_custom_attribute.rb +++ b/app/models/user_custom_attribute.rb @@ -10,13 +10,15 @@ class UserCustomAttribute < ApplicationRecord scope :by_user_id, ->(user_id) { where(user_id: user_id) } scope :by_updated_at, ->(updated_at) { where(updated_at: updated_at) } scope :arkose_sessions, -> { by_key('arkose_session') } + scope :trusted_with_spam, -> { by_key(TRUSTED_BY) } BLOCKED_BY = 'blocked_by' UNBLOCKED_BY = 'unblocked_by' ARKOSE_RISK_BAND = 'arkose_risk_band' AUTO_BANNED_BY_ABUSE_REPORT_ID = 'auto_banned_by_abuse_report_id' AUTO_BANNED_BY_SPAM_LOG_ID = 'auto_banned_by_spam_log_id' - ALLOW_POSSIBLE_SPAM = 'allow_possible_spam' + TRUSTED_BY = 'trusted_by' + AUTO_BANNED_BY = 'auto_banned_by' IDENTITY_VERIFICATION_PHONE_EXEMPT = 'identity_verification_phone_exempt' class << self @@ -50,6 +52,17 @@ class UserCustomAttribute < ApplicationRecord return unless spam_log custom_attribute = { user_id: spam_log.user_id, key: AUTO_BANNED_BY_SPAM_LOG_ID, value: spam_log.id } + upsert_custom_attributes([custom_attribute]) + end + + def set_trusted_by(user:, trusted_by:) + return unless user && trusted_by + + custom_attribute = { + user_id: user.id, + key: UserCustomAttribute::TRUSTED_BY, + value: "#{trusted_by.username}/#{trusted_by.id}+#{Time.current}" + } upsert_custom_attributes([custom_attribute]) end diff --git a/app/models/users/callout.rb b/app/models/users/callout.rb index def0765560e..60dd89c3ee7 100644 --- a/app/models/users/callout.rb +++ b/app/models/users/callout.rb @@ -73,9 +73,10 @@ module Users new_navigation_callout: 71, # 72 removed in https://gitlab.com/gitlab-org/gitlab/-/merge_requests/129022 namespace_over_storage_users_combined_alert: 73, # EE-only - rich_text_editor: 74, + # 74 removed in https://gitlab.com/gitlab-org/gitlab/-/merge_requests/132751 vsd_feedback_banner: 75, # EE-only - security_policy_protected_branch_modification: 76 # EE-only + security_policy_protected_branch_modification: 76, # EE-only + vulnerability_report_grouping: 77 # EE-only } validates :feature_name, diff --git a/app/models/users/credit_card_validation.rb b/app/models/users/credit_card_validation.rb index 086943884a5..276d549006f 100644 --- a/app/models/users/credit_card_validation.rb +++ b/app/models/users/credit_card_validation.rb @@ -23,18 +23,18 @@ module Users scope :find_or_initialize_by_user, ->(user_id) { where(user_id: user_id).first_or_initialize } scope :by_banned_user, -> { joins(:banned_user) } - scope :similar_by_holder_name, ->(holder_name) do - if holder_name.present? - where('lower(holder_name) = lower(:value)', value: holder_name) + scope :similar_by_holder_name, ->(holder_name_hash) do + if holder_name_hash.present? + where(holder_name_hash: holder_name_hash) else none end end scope :similar_to, ->(credit_card_validation) do where( - expiration_date: credit_card_validation.expiration_date, - last_digits: credit_card_validation.last_digits, - network: credit_card_validation.network + expiration_date_hash: credit_card_validation.expiration_date_hash, + last_digits_hash: credit_card_validation.last_digits_hash, + network_hash: credit_card_validation.network_hash ) end @@ -48,11 +48,11 @@ module Users end def similar_holder_names_count - self.class.similar_by_holder_name(holder_name).count + self.class.similar_by_holder_name(holder_name_hash).count end def used_by_banned_user? - self.class.by_banned_user.similar_to(self).similar_by_holder_name(holder_name).exists? + self.class.by_banned_user.similar_to(self).similar_by_holder_name(holder_name_hash).exists? end def set_last_digits_hash diff --git a/app/models/users/in_product_marketing_email.rb b/app/models/users/in_product_marketing_email.rb index f220cfd17c5..5b9255f93b1 100644 --- a/app/models/users/in_product_marketing_email.rb +++ b/app/models/users/in_product_marketing_email.rb @@ -3,30 +3,21 @@ module Users class InProductMarketingEmail < ApplicationRecord include BulkInsertSafe + include IgnorableColumns - BUILD_IOS_APP_GUIDE = 'build_ios_app_guide' - CAMPAIGNS = [BUILD_IOS_APP_GUIDE].freeze + ignore_column :campaign, remove_with: '16.7', remove_after: '2023-11-15' belongs_to :user validates :user, presence: true - - validates :track, :series, presence: true, if: -> { campaign.blank? } - validates :campaign, presence: true, if: -> { track.blank? && series.blank? } - validates :campaign, inclusion: { in: CAMPAIGNS }, allow_nil: true + validates :track, presence: true + validates :series, presence: true validates :user_id, uniqueness: { scope: [:track, :series], message: 'track series email has already been sent' }, if: -> { track.present? } - validates :user_id, uniqueness: { - scope: :campaign, - message: 'campaign email has already been sent' - }, if: -> { campaign.present? } - - validate :campaign_or_track_series - enum track: { create: 0, verify: 1, @@ -44,20 +35,15 @@ module Users INACTIVE_TRACK_NAMES = %w[invite_team experience].freeze ACTIVE_TRACKS = tracks.except(*INACTIVE_TRACK_NAMES) - scope :for_user_with_track_and_series, -> (user, track, series) do + scope :for_user_with_track_and_series, ->(user, track, series) do where(user: user, track: track, series: series) end - scope :without_track_and_series, -> (track, series) do + scope :without_track_and_series, ->(track, series) do join_condition = for_user.and(for_track_and_series(track, series)) users_without_records(join_condition) end - scope :without_campaign, -> (campaign) do - join_condition = for_user.and(for_campaign(campaign)) - users_without_records(join_condition) - end - def self.users_table User.arel_table end @@ -78,10 +64,6 @@ module Users arel_table[:user_id].eq(users_table[:id]) end - def self.for_campaign(campaign) - arel_table[:campaign].eq(campaign) - end - def self.for_track_and_series(track, series) arel_table[:track].eq(ACTIVE_TRACKS[track]) .and(arel_table[:series]).eq(series) @@ -92,13 +74,5 @@ module Users email.update(cta_clicked_at: Time.zone.now) if email && email.cta_clicked_at.blank? end - - private - - def campaign_or_track_series - if campaign.present? && (track.present? || series.present?) - errors.add(:campaign, 'should be a campaign or a track and series but not both') - end - end end end diff --git a/app/models/users/phone_number_validation.rb b/app/models/users/phone_number_validation.rb index 52f16a7861f..e033445d76b 100644 --- a/app/models/users/phone_number_validation.rb +++ b/app/models/users/phone_number_validation.rb @@ -2,9 +2,13 @@ module Users class PhoneNumberValidation < ApplicationRecord + include IgnorableColumns + self.primary_key = :user_id self.table_name = 'user_phone_number_validations' + ignore_column :verification_attempts, remove_with: '16.7', remove_after: '2023-11-17' + belongs_to :user, foreign_key: :user_id belongs_to :banned_user, class_name: '::Users::BannedUser', foreign_key: :user_id diff --git a/app/models/vs_code/settings/vs_code_setting.rb b/app/models/vs_code/settings/vs_code_setting.rb new file mode 100644 index 00000000000..e55d958d2b4 --- /dev/null +++ b/app/models/vs_code/settings/vs_code_setting.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module VsCode + module Settings + class VsCodeSetting < ApplicationRecord + belongs_to :user, inverse_of: :vscode_settings + + validates :setting_type, presence: true + validates :content, presence: true + + scope :by_setting_type, ->(setting_type) { where(setting_type: setting_type) } + scope :by_user, ->(user) { where(user: user) } + end + end +end diff --git a/app/models/vulnerability.rb b/app/models/vulnerability.rb index 650e8942132..0e3fe2cc8ac 100644 --- a/app/models/vulnerability.rb +++ b/app/models/vulnerability.rb @@ -9,6 +9,9 @@ class Vulnerability < ApplicationRecord scope :with_projects, -> { includes(:project) } + validates :cvss, json_schema: { filename: "vulnerability_cvss_vectors", draft: 7 } + attribute :cvss, :ind_jsonb + def self.link_reference_pattern nil end diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb index a7e2be0eae5..2eed693ca76 100644 --- a/app/models/wiki_page.rb +++ b/app/models/wiki_page.rb @@ -205,7 +205,7 @@ class WikiPage update_attributes(attrs) save do - wiki.create_page(title, content, format, attrs[:message]) + wiki.create_page(title, raw_content, format, attrs[:message]) end end diff --git a/app/models/work_item.rb b/app/models/work_item.rb index 62b837eeeb6..0761a213532 100644 --- a/app/models/work_item.rb +++ b/app/models/work_item.rb @@ -148,6 +148,8 @@ class WorkItem < Issue end def linked_work_items(current_user = nil, authorize: true, preload: nil, link_type: nil) + return [] if new_record? + linked_work_items = linked_work_items_query(link_type).preload(preload).reorder('issue_link_id') return linked_work_items unless authorize @@ -159,6 +161,10 @@ class WorkItem < Issue ) end + def linked_items_count + linked_work_items(authorize: false).size + end + private override :parent_link_confidentiality diff --git a/app/models/work_items/parent_link.rb b/app/models/work_items/parent_link.rb index ea7755b03b4..32232c93d11 100644 --- a/app/models/work_items/parent_link.rb +++ b/app/models/work_items/parent_link.rb @@ -15,7 +15,6 @@ module WorkItems validates :work_item, presence: true, uniqueness: true validate :validate_hierarchy_restrictions validate :validate_cyclic_reference - validate :validate_same_project validate :validate_max_children validate :validate_confidentiality validate :check_existing_related_link @@ -50,14 +49,6 @@ module WorkItems private - def validate_same_project - return if work_item.nil? || work_item_parent.nil? - - if work_item.resource_parent != work_item_parent.resource_parent - errors.add :work_item_parent, _('parent must be in the same project as child.') - end - end - def validate_max_children return unless work_item_parent @@ -88,6 +79,14 @@ module WorkItems end validate_depth(restriction.maximum_depth) + validate_cross_hierarchy(restriction.cross_hierarchy_enabled) + end + + def validate_cross_hierarchy(cross_hierarchy_enabled) + return if cross_hierarchy_enabled + return if work_item.resource_parent == work_item_parent.resource_parent + + errors.add :work_item_parent, _('parent must be in the same project or group as child.') end def validate_depth(depth) diff --git a/app/models/work_items/related_link_restriction.rb b/app/models/work_items/related_link_restriction.rb new file mode 100644 index 00000000000..d4a66c95ffb --- /dev/null +++ b/app/models/work_items/related_link_restriction.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module WorkItems + class RelatedLinkRestriction < ApplicationRecord + self.table_name = 'work_item_related_link_restrictions' + + belongs_to :source_type, class_name: 'WorkItems::Type' + belongs_to :target_type, class_name: 'WorkItems::Type' + + validates :source_type, presence: true + validates :target_type, presence: true + validates :target_type, uniqueness: { scope: [:source_type_id, :link_type] } + + enum link_type: Enums::IssuableLink.link_types + end +end diff --git a/app/models/work_items/related_work_item_link.rb b/app/models/work_items/related_work_item_link.rb index a911ef5f05d..fb0069541fb 100644 --- a/app/models/work_items/related_work_item_link.rb +++ b/app/models/work_items/related_work_item_link.rb @@ -11,7 +11,7 @@ module WorkItems belongs_to :source, class_name: 'WorkItem' belongs_to :target, class_name: 'WorkItem' - validate :validate_max_number_of_links, on: :create + validate :validate_related_link_restrictions class << self extend ::Gitlab::Utils::Override @@ -28,14 +28,39 @@ module WorkItems end end - def validate_max_number_of_links - if source && source.linked_work_items(authorize: false).size >= MAX_LINKS_COUNT - errors.add :source, s_('WorkItems|This work item would exceed the maximum number of linked items.') - end + private + + def validate_related_link_restrictions + return unless source && target + + source_type = source.work_item_type + target_type = target.work_item_type - return unless target && target.linked_work_items(authorize: false).size >= MAX_LINKS_COUNT + return if link_restriction_exists?(source_type.id, target_type.id) - errors.add :target, s_('WorkItems|This work item would exceed the maximum number of linked items.') + errors.add :source, format( + s_('%{source_type} cannot be related to %{type_type}'), + source_type: source_type.name.downcase.pluralize, + type_type: target_type.name.downcase.pluralize + ) + end + + def link_restriction_exists?(source_type_id, target_type_id) + source_restriction = find_restriction(source_type_id, target_type_id) + return true if source_restriction.present? + return false if source_type_id == target_type_id + + find_restriction(target_type_id, source_type_id).present? + end + + def find_restriction(source_type_id, target_type_id) + ::WorkItems::RelatedLinkRestriction.find_by_source_type_id_and_target_type_id_and_link_type( + source_type_id, + target_type_id, + link_type + ) end end end + +WorkItems::RelatedWorkItemLink.prepend_mod diff --git a/app/models/work_items/type.rb b/app/models/work_items/type.rb index b7ceeecbc7f..4ccef4c93d3 100644 --- a/app/models/work_items/type.rb +++ b/app/models/work_items/type.rb @@ -73,6 +73,7 @@ module WorkItems Gitlab::DatabaseImporters::WorkItems::BaseTypeImporter.upsert_types Gitlab::DatabaseImporters::WorkItems::HierarchyRestrictionsImporter.upsert_restrictions + Gitlab::DatabaseImporters::WorkItems::RelatedLinksRestrictionsImporter.upsert_restrictions find_by(namespace_id: nil, base_type: type) end diff --git a/app/models/work_items/widgets/hierarchy.rb b/app/models/work_items/widgets/hierarchy.rb index 8f54cb32f43..fc6714f1e08 100644 --- a/app/models/work_items/widgets/hierarchy.rb +++ b/app/models/work_items/widgets/hierarchy.rb @@ -10,6 +10,26 @@ module WorkItems def children work_item.work_item_children_by_relative_position end + + def ancestors + work_item.ancestors + end + + def self.quick_action_commands + [:set_parent, :add_child] + end + + def self.quick_action_params + [:set_parent, :add_child] + end + + def self.process_quick_action_param(param_name, value) + return super unless param_name.in?(quick_action_params) && value.present? + + return { parent: value } if param_name == :set_parent + + return { children: value } if param_name == :add_child + end end end end |