diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-07-19 17:16:28 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-07-19 17:16:28 +0300 |
commit | e4384360a16dd9a19d4d2d25d0ef1f2b862ed2a6 (patch) | |
tree | 2fcdfa7dcdb9db8f5208b2562f4b4e803d671243 /app/models | |
parent | ffda4e7bcac36987f936b4ba515995a6698698f0 (diff) |
Add latest changes from gitlab-org/gitlab@16-2-stable-eev16.2.0-rc42
Diffstat (limited to 'app/models')
127 files changed, 1228 insertions, 931 deletions
diff --git a/app/models/abuse/trust_score.rb b/app/models/abuse/trust_score.rb index b7ed504a0ba..2e8b7ed6686 100644 --- a/app/models/abuse/trust_score.rb +++ b/app/models/abuse/trust_score.rb @@ -2,9 +2,6 @@ module Abuse class TrustScore < ApplicationRecord - MAX_EVENTS = 100 - SPAMCHECK_HAM_THRESHOLD = 0.5 - self.table_name = 'abuse_trust_scores' enum source: Enums::Abuse::Source.sources @@ -15,6 +12,9 @@ module Abuse validates :score, presence: true validates :source, presence: true + scope :order_created_at_asc, -> { order(created_at: :asc) } + scope :order_created_at_desc, -> { order(created_at: :desc) } + before_create :assign_correlation_id after_commit :remove_old_scores @@ -25,14 +25,7 @@ module Abuse end def remove_old_scores - count = user.trust_scores_for_source(source).count - return unless count > MAX_EVENTS - - TrustScore.delete( - user.trust_scores_for_source(source) - .order(created_at: :asc) - .limit(count - MAX_EVENTS) - ) + Abuse::UserTrustScore.new(user).remove_old_scores(source) end end end diff --git a/app/models/abuse/user_trust_score.rb b/app/models/abuse/user_trust_score.rb new file mode 100644 index 00000000000..3a935e230ae --- /dev/null +++ b/app/models/abuse/user_trust_score.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +module Abuse + class UserTrustScore + MAX_EVENTS = 100 + SPAMCHECK_HAM_THRESHOLD = 0.5 + + def initialize(user) + @user = user + end + + def spammer? + spam_score > SPAMCHECK_HAM_THRESHOLD + end + + def spam_score + user_scores.spamcheck.average(:score) || 0.0 + end + + def telesign_score + user_scores.telesign.order_created_at_desc.first&.score || 0.0 + end + + def arkose_global_score + user_scores.arkose_global_score.order_created_at_desc.first&.score || 0.0 + end + + def arkose_custom_score + user_scores.arkose_custom_score.order_created_at_desc.first&.score || 0.0 + end + + def trust_scores_for_source(source) + user_scores.where(source: source) + end + + def remove_old_scores(source) + count = trust_scores_for_source(source).count + return unless count > MAX_EVENTS + + Abuse::TrustScore.delete( + trust_scores_for_source(source) + .order_created_at_asc + .limit(count - MAX_EVENTS) + ) + end + + private + + def user_scores + Abuse::TrustScore.where(user_id: @user.id) + end + end +end diff --git a/app/models/ai/service_access_token.rb b/app/models/ai/service_access_token.rb new file mode 100644 index 00000000000..863bdfc7899 --- /dev/null +++ b/app/models/ai/service_access_token.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module Ai + class ServiceAccessToken < ApplicationRecord + self.table_name = 'service_access_tokens' + + scope :expired, -> { where('expires_at < :now', now: Time.current) } + scope :for_category, ->(category) { where(category: category) } + + attr_encrypted :token, + mode: :per_attribute_iv, + key: Settings.attr_encrypted_db_key_base_32, + algorithm: 'aes-256-gcm', + encode: false, + encode_iv: false + + validates :token, :expires_at, presence: true + + enum category: { + code_suggestions: 1 + } + + validates :category, presence: true + end +end diff --git a/app/models/alert_management/http_integration.rb b/app/models/alert_management/http_integration.rb index d5162865a79..a70168dc0d8 100644 --- a/app/models/alert_management/http_integration.rb +++ b/app/models/alert_management/http_integration.rb @@ -4,7 +4,7 @@ module AlertManagement class HttpIntegration < ApplicationRecord include ::Gitlab::Routing - LEGACY_IDENTIFIER = 'legacy' + LEGACY_IDENTIFIERS = %w[legacy legacy-prometheus].freeze belongs_to :project, inverse_of: :alert_management_http_integrations @@ -20,8 +20,8 @@ module AlertManagement validates :token, presence: true, format: { with: /\A\h{32}\z/ } validates :name, presence: true, length: { maximum: 255 } validates :type_identifier, presence: true - validates :endpoint_identifier, presence: true, length: { maximum: 255 }, format: { with: /\A[A-Za-z0-9]+\z/ } - validates :endpoint_identifier, uniqueness: { scope: [:project_id, :active] }, if: :active? + validates :endpoint_identifier, presence: true, length: { maximum: 255 }, format: { with: /\A[A-Za-z0-9-]+\z/ } + validates :endpoint_identifier, uniqueness: { scope: [:project_id] } validates :payload_attribute_mapping, json_schema: { filename: 'http_integration_payload_attribute_mapping' } before_validation :prevent_token_assignment @@ -33,7 +33,6 @@ module AlertManagement scope :for_type, ->(type) { where(type_identifier: type) } scope :for_project, ->(project_ids) { where(project: project_ids) } scope :active, -> { where(active: true) } - scope :legacy, -> { for_endpoint_identifier(LEGACY_IDENTIFIER) } scope :ordered_by_type_and_id, -> { order(:type_identifier, :id) } enum type_identifier: { @@ -42,16 +41,18 @@ module AlertManagement } def url - if legacy? - return project_alerts_notify_url(project, format: :json) if http? - return notify_project_prometheus_alerts_url(project, format: :json) if prometheus? + case endpoint_identifier + when 'legacy' + project_alerts_notify_url(project, format: :json) + when 'legacy-prometheus' + notify_project_prometheus_alerts_url(project, format: :json) + else + project_alert_http_integration_url(project, name_slug, endpoint_identifier, format: :json) end - - project_alert_http_integration_url(project, name_slug, endpoint_identifier, format: :json) end def legacy? - endpoint_identifier == LEGACY_IDENTIFIER + LEGACY_IDENTIFIERS.include?(endpoint_identifier) end private diff --git a/app/models/analytics/cycle_analytics/stage.rb b/app/models/analytics/cycle_analytics/stage.rb index c7bff7c8d7f..6f152e7749e 100644 --- a/app/models/analytics/cycle_analytics/stage.rb +++ b/app/models/analytics/cycle_analytics/stage.rb @@ -3,6 +3,8 @@ module Analytics module CycleAnalytics class Stage < ApplicationRecord + MAX_STAGES_PER_VALUE_STREAM = 15 + self.table_name = :analytics_cycle_analytics_group_stages include DatabaseEventTracking @@ -10,6 +12,8 @@ module Analytics include Analytics::CycleAnalytics::Parentable validates :name, uniqueness: { scope: [:group_id, :group_value_stream_id] } + validate :max_stages_count, on: :create + belongs_to :value_stream, class_name: 'Analytics::CycleAnalytics::ValueStream', foreign_key: :group_value_stream_id, inverse_of: :stages @@ -49,6 +53,15 @@ module Analytics name group_value_stream_id ].freeze + + private + + def max_stages_count + return unless value_stream + return unless value_stream.stages.count >= MAX_STAGES_PER_VALUE_STREAM + + errors.add(:value_stream, _('Maximum number of stages per value stream exceeded')) + end end end end diff --git a/app/models/analytics/cycle_analytics/value_stream.rb b/app/models/analytics/cycle_analytics/value_stream.rb index 31e06075bcb..16446a5b463 100644 --- a/app/models/analytics/cycle_analytics/value_stream.rb +++ b/app/models/analytics/cycle_analytics/value_stream.rb @@ -3,6 +3,8 @@ module Analytics module CycleAnalytics class ValueStream < ApplicationRecord + MAX_VALUE_STREAMS_PER_NAMESPACE = 50 + self.table_name = :analytics_cycle_analytics_group_value_streams include Analytics::CycleAnalytics::Parentable @@ -15,6 +17,7 @@ module Analytics validates :name, presence: true validates :name, length: { minimum: 3, maximum: 100, allow_nil: false }, uniqueness: { scope: :group_id } + validate :max_value_streams_count, on: :create accepts_nested_attributes_for :stages, allow_destroy: true @@ -35,6 +38,13 @@ module Analytics private + def max_value_streams_count + return unless namespace + return unless namespace.value_streams.count >= MAX_VALUE_STREAMS_PER_NAMESPACE + + errors.add(:namespace, _('Maximum number of value streams per namespace exceeded')) + end + def ensure_aggregation_record_presence Analytics::CycleAnalytics::Aggregation.safe_create_for_namespace(namespace) end diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index a71b47e88d8..827f8bc93be 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -38,7 +38,7 @@ class ApplicationSetting < MainClusterwide::ApplicationRecord encrypted_tofa_url encrypted_tofa_url_iv vertex_project - ], remove_with: '16.2', remove_after: '2023-06-22' + ], remove_with: '16.3', remove_after: '2023-07-22' INSTANCE_REVIEW_MIN_USERS = 50 GRAFANA_URL_ERROR_MESSAGE = 'Please check your Grafana URL setting in ' \ @@ -596,6 +596,13 @@ class ApplicationSetting < MainClusterwide::ApplicationRecord allow_blank: true, public_url: ADDRESSABLE_URL_VALIDATION_OPTIONS + with_options(presence: true, if: :slack_app_enabled?) do + validates :slack_app_id + validates :slack_app_secret + validates :slack_app_signing_secret + validates :slack_app_verification_token + end + with_options(presence: true, numericality: { only_integer: true, greater_than: 0 }) do validates :throttle_unauthenticated_api_requests_per_period validates :throttle_unauthenticated_api_period_in_seconds diff --git a/app/models/audit_event.rb b/app/models/audit_event.rb index 9370982be47..163e741d990 100644 --- a/app/models/audit_event.rb +++ b/app/models/audit_event.rb @@ -100,40 +100,6 @@ class AuditEvent < ApplicationRecord super || details[:target_details] end - def self.by_group(group) - group_id = group.id - - # Bring entity_type and entity_id from projects and group into one query - scope1 = Group.find(group_id).all_projects.select("'Project' as entity_type", 'id AS entity_id') - scope2 = Project.from("(VALUES ('Group', #{group_id})) as projects(entity_type, entity_id)").select('entity_type', - 'entity_id') - array_scope = Project.from_union([scope1, scope2], remove_duplicates: false).select(:entity_type, :entity_id) - - # order by created_at (id is the tie breaker) - scope = AuditEvent.order(:created_at, :id) - - array_mapping_scope = ->(entity_type_expression, entity_id_expression) do - AuditEvent.where(AuditEvent.arel_table[:entity_id].eq(entity_id_expression)) - .where(AuditEvent.arel_table[:entity_type].eq(entity_type_expression)) - end - - finder_query = ->(created_at_expression, id_expression) do - # we need to add created_at filter as well because that's the partitioning key - AuditEvent.where( - AuditEvent.arel_table[:id].eq(id_expression) - ).where( - AuditEvent.arel_table[:created_at].eq(created_at_expression) - ) - end - - Gitlab::Pagination::Keyset::InOperatorOptimization::QueryBuilder.new( - scope: scope, - array_scope: array_scope, - array_mapping_scope: array_mapping_scope, - finder_query: finder_query - ).execute - end - private def sanitize_message diff --git a/app/models/award_emoji.rb b/app/models/award_emoji.rb index 31bee8db1b4..ebc43b04b1b 100644 --- a/app/models/award_emoji.rb +++ b/app/models/award_emoji.rb @@ -31,6 +31,7 @@ class AwardEmoji < ApplicationRecord after_destroy :expire_cache after_save :expire_cache + after_commit :broadcast_note_update, if: -> { !importing? && awardable.is_a?(Note) } class << self def votes_for_collection(ids, type) @@ -73,11 +74,19 @@ class AwardEmoji < ApplicationRecord def expire_cache awardable.try(:bump_updated_at) - awardable.expire_etag_cache if awardable.is_a?(Note) awardable.try(:update_upvotes_count) if upvote? end + def broadcast_note_update + awardable.expire_etag_cache + awardable.trigger_note_subscription_update + end + def to_ability_name 'emoji' end + + def hook_attrs + Gitlab::HookData::EmojiBuilder.new(self).build + end end diff --git a/app/models/broadcast_message.rb b/app/models/broadcast_message.rb index bf25ea7367c..ccc5ca7395d 100644 --- a/app/models/broadcast_message.rb +++ b/app/models/broadcast_message.rb @@ -3,7 +3,6 @@ class BroadcastMessage < MainClusterwide::ApplicationRecord include CacheMarkdownField include Sortable - include IgnorableColumns ALLOWED_TARGET_ACCESS_LEVELS = [ Gitlab::Access::GUEST, @@ -13,8 +12,6 @@ class BroadcastMessage < MainClusterwide::ApplicationRecord Gitlab::Access::OWNER ].freeze - ignore_column :namespace_id, remove_with: '16.0', remove_after: '2022-06-22' - cache_markdown_field :message, pipeline: :broadcast_message, whitelisted: true validates :message, presence: true diff --git a/app/models/bulk_import.rb b/app/models/bulk_import.rb index c2d7529f468..fde528e3fa0 100644 --- a/app/models/bulk_import.rb +++ b/app/models/bulk_import.rb @@ -58,6 +58,10 @@ class BulkImport < ApplicationRecord Gitlab::VersionInfo.new(MIN_MAJOR_VERSION, MIN_MINOR_VERSION_FOR_PROJECT) end + def self.min_gl_version_for_migration_in_batches + Gitlab::VersionInfo.new(16, 2) + end + def self.all_human_statuses state_machine.states.map(&:human_name) end @@ -68,4 +72,8 @@ class BulkImport < ApplicationRecord update!(has_failures: true) end + + def supports_batched_export? + source_version_info >= self.class.min_gl_version_for_migration_in_batches + end end diff --git a/app/models/bulk_imports/batch_tracker.rb b/app/models/bulk_imports/batch_tracker.rb index df1fab89ee6..2e79d41d46e 100644 --- a/app/models/bulk_imports/batch_tracker.rb +++ b/app/models/bulk_imports/batch_tracker.rb @@ -25,9 +25,7 @@ module BulkImports end event :finish do - transition started: :finished - transition failed: :failed - transition skipped: :skipped + transition any => :finished end event :skip do diff --git a/app/models/bulk_imports/entity.rb b/app/models/bulk_imports/entity.rb index 94e4a8165eb..4f50a112141 100644 --- a/app/models/bulk_imports/entity.rb +++ b/app/models/bulk_imports/entity.rb @@ -144,12 +144,27 @@ class BulkImports::Entity < ApplicationRecord end end - def export_relations_url_path - "#{base_resource_path}/export_relations" + def export_relations_url_path_base + File.join(base_resource_path, 'export_relations') end - def relation_download_url_path(relation) - "#{export_relations_url_path}/download?relation=#{relation}" + def export_relations_url_path(batched: false) + if batched && bulk_import.supports_batched_export? + Gitlab::Utils.add_url_parameters(export_relations_url_path_base, batched: batched) + else + export_relations_url_path_base + end + end + + def relation_download_url_path(relation, batch_number = nil) + url = File.join(export_relations_url_path_base, 'download') + params = { relation: relation } + + if batch_number && bulk_import.supports_batched_export? + params.merge!(batched: true, batch_number: batch_number) + end + + Gitlab::Utils.add_url_parameters(url, params) end def wikis_url_path diff --git a/app/models/bulk_imports/export.rb b/app/models/bulk_imports/export.rb index 93cf047c690..5c3f8e4b8d4 100644 --- a/app/models/bulk_imports/export.rb +++ b/app/models/bulk_imports/export.rb @@ -32,9 +32,7 @@ module BulkImports end event :finish do - transition started: :finished - transition finished: :finished - transition failed: :failed + transition any => :finished end event :fail_op do @@ -63,5 +61,12 @@ module BulkImports FileTransfer.config_for(portable) end end + + def remove_existing_upload! + return unless upload&.export_file&.file + + upload.remove_export_file! + upload.save! + end end end diff --git a/app/models/bulk_imports/export_status.rb b/app/models/bulk_imports/export_status.rb index cbd7b189007..3d820e65d5b 100644 --- a/app/models/bulk_imports/export_status.rb +++ b/app/models/bulk_imports/export_status.rb @@ -13,28 +13,48 @@ module BulkImports end def started? - !empty? && export_status['status'] == Export::STARTED + !empty? && status['status'] == Export::STARTED end def failed? - !empty? && export_status['status'] == Export::FAILED + !empty? && status['status'] == Export::FAILED end def empty? - export_status.nil? + status.nil? end def error - export_status['error'] + status['error'] + end + + def batched? + status['batched'] == true + end + + def batches_count + status['batches_count'].to_i + end + + def batch(batch_number) + raise ArgumentError if batch_number < 1 + + return unless batched? + + status['batches'].find { |item| item['batch_number'] == batch_number } end private attr_reader :client, :entity, :relation, :pipeline_tracker - def export_status - strong_memoize(:export_status) do - fetch_export_status&.find { |item| item['relation'] == relation } + def status + strong_memoize(:status) do + status = fetch_status + + next status if status.is_a?(Hash) || status.nil? + + status.find { |item| item['relation'] == relation } rescue BulkImports::NetworkError => e raise BulkImports::RetryPipelineError.new(e.message, 2.seconds) if e.retriable?(pipeline_tracker) @@ -44,12 +64,12 @@ module BulkImports end end - def fetch_export_status - client.get(status_endpoint).parsed_response + def fetch_status + client.get(status_endpoint, relation: relation).parsed_response end def status_endpoint - File.join(entity.export_relations_url_path, 'status') + File.join(entity.export_relations_url_path_base, 'status') end def default_error_response(message) diff --git a/app/models/bulk_imports/tracker.rb b/app/models/bulk_imports/tracker.rb index 55502721a76..d1a6f3b9a80 100644 --- a/app/models/bulk_imports/tracker.rb +++ b/app/models/bulk_imports/tracker.rb @@ -24,6 +24,7 @@ class BulkImports::Tracker < ApplicationRecord delegate :file_extraction_pipeline?, to: :pipeline_class DEFAULT_PAGE_SIZE = 500 + STALE_AFTER = 4.hours scope :next_pipeline_trackers_for, -> (entity_id) { entity_scope = where(bulk_import_entity_id: entity_id) @@ -89,4 +90,8 @@ class BulkImports::Tracker < ApplicationRecord transition [:created, :started] => :timeout end end + + def stale? + created_at < STALE_AFTER.ago + end end diff --git a/app/models/ci/artifact_blob.rb b/app/models/ci/artifact_blob.rb index f87b18d516f..1f6d218b015 100644 --- a/app/models/ci/artifact_blob.rb +++ b/app/models/ci/artifact_blob.rb @@ -4,8 +4,6 @@ module Ci class ArtifactBlob include BlobLike - EXTENSIONS_SERVED_BY_PAGES = %w[.html .htm .txt .json .xml .log].freeze - attr_reader :entry def initialize(entry) @@ -35,31 +33,18 @@ module Ci :build_artifact end - def external_url(project, job) - return unless external_link?(job) - - url_project_path = project.full_path.partition('/').last - - artifact_path = [ - '-', url_project_path, '-', - 'jobs', job.id, - 'artifacts', path - ].join('/') - - "#{project.pages_namespace_url}/#{artifact_path}" + def external_url(job) + pages_url_builder(job.project).artifact_url(entry, job) end def external_link?(job) - pages_config.enabled && - pages_config.artifacts_server && - EXTENSIONS_SERVED_BY_PAGES.include?(File.extname(name)) && - (pages_config.access_control || job.project.public?) + pages_url_builder(job.project).artifact_url_available?(entry, job) end private - def pages_config - Gitlab.config.pages + def pages_url_builder(project) + @pages_url_builder ||= Gitlab::Pages::UrlBuilder.new(project) end end end diff --git a/app/models/ci/bridge.rb b/app/models/ci/bridge.rb index 7cdd0d56a98..5052d84378f 100644 --- a/app/models/ci/bridge.rb +++ b/app/models/ci/bridge.rb @@ -224,15 +224,46 @@ module Ci end end + def target_revision_ref + downstream_pipeline_params.dig(:target_revision, :ref) + end + def downstream_variables - calculate_downstream_variables - .reverse # variables priority - .uniq { |var| var[:key] } # only one variable key to pass - .reverse + Gitlab::Ci::Variables::Downstream::Generator.new(self).calculate end - def target_revision_ref - downstream_pipeline_params.dig(:target_revision, :ref) + def variables + strong_memoize(:variables) do + Gitlab::Ci::Variables::Collection.new + .concat(scoped_variables) + .concat(pipeline.persisted_variables) + end + end + + def pipeline_variables + pipeline.variables + end + + def pipeline_schedule_variables + return [] unless pipeline.pipeline_schedule + + pipeline.pipeline_schedule.variables.to_a + end + + def forward_yaml_variables? + strong_memoize(:forward_yaml_variables) do + result = options&.dig(:trigger, :forward, :yaml_variables) + + result.nil? ? FORWARD_DEFAULTS[:yaml_variables] : result + end + end + + def forward_pipeline_variables? + strong_memoize(:forward_pipeline_variables) do + result = options&.dig(:trigger, :forward, :pipeline_variables) + + result.nil? ? FORWARD_DEFAULTS[:pipeline_variables] : result + end end private @@ -273,70 +304,6 @@ module Ci } } end - - def calculate_downstream_variables - expand_variables = scoped_variables - .concat(pipeline.persisted_variables) - .to_runner_variables - - # The order of this list refers to the priority of the variables - downstream_yaml_variables(expand_variables) + - downstream_pipeline_variables(expand_variables) + - downstream_pipeline_schedule_variables(expand_variables) - end - - def downstream_yaml_variables(expand_variables) - return [] unless forward_yaml_variables? - - yaml_variables.to_a.map do |hash| - if hash[:raw] - { key: hash[:key], value: hash[:value], raw: true } - else - { key: hash[:key], value: ::ExpandVariables.expand(hash[:value], expand_variables) } - end - end - end - - def downstream_pipeline_variables(expand_variables) - return [] unless forward_pipeline_variables? - - pipeline.variables.to_a.map do |variable| - if variable.raw? - { key: variable.key, value: variable.value, raw: true } - else - { key: variable.key, value: ::ExpandVariables.expand(variable.value, expand_variables) } - end - end - end - - def downstream_pipeline_schedule_variables(expand_variables) - return [] unless forward_pipeline_variables? - return [] unless pipeline.pipeline_schedule - - pipeline.pipeline_schedule.variables.to_a.map do |variable| - if variable.raw? - { key: variable.key, value: variable.value, raw: true } - else - { key: variable.key, value: ::ExpandVariables.expand(variable.value, expand_variables) } - end - end - end - - def forward_yaml_variables? - strong_memoize(:forward_yaml_variables) do - result = options&.dig(:trigger, :forward, :yaml_variables) - - result.nil? ? FORWARD_DEFAULTS[:yaml_variables] : result - end - end - - def forward_pipeline_variables? - strong_memoize(:forward_pipeline_variables) do - result = options&.dig(:trigger, :forward, :pipeline_variables) - - result.nil? ? FORWARD_DEFAULTS[:pipeline_variables] : result - end - end end end diff --git a/app/models/ci/build_metadata.rb b/app/models/ci/build_metadata.rb index 382f861a802..4c723bb7c0c 100644 --- a/app/models/ci/build_metadata.rb +++ b/app/models/ci/build_metadata.rb @@ -10,11 +10,9 @@ module Ci include Presentable include ChronicDurationAttribute include Gitlab::Utils::StrongMemoize - include SafelyChangeColumnDefault self.table_name = 'p_ci_builds_metadata' self.primary_key = 'id' - columns_changing_default :partition_id partitionable scope: :build diff --git a/app/models/ci/build_need.rb b/app/models/ci/build_need.rb index 940221619b3..317f2523f69 100644 --- a/app/models/ci/build_need.rb +++ b/app/models/ci/build_need.rb @@ -3,8 +3,12 @@ module Ci class BuildNeed < Ci::ApplicationRecord include Ci::Partitionable - include BulkInsertSafe include IgnorableColumns + include SafelyChangeColumnDefault + include BulkInsertSafe + + columns_changing_default :partition_id + ignore_column :id_convert_to_bigint, remove_with: '16.4', remove_after: '2023-09-22' belongs_to :build, class_name: "Ci::Processable", foreign_key: :build_id, inverse_of: :needs diff --git a/app/models/ci/build_pending_state.rb b/app/models/ci/build_pending_state.rb index 966884ae158..0b88f745d78 100644 --- a/app/models/ci/build_pending_state.rb +++ b/app/models/ci/build_pending_state.rb @@ -2,6 +2,9 @@ class Ci::BuildPendingState < Ci::ApplicationRecord include Ci::Partitionable + include SafelyChangeColumnDefault + + columns_changing_default :partition_id belongs_to :build, class_name: 'Ci::Build', foreign_key: :build_id, inverse_of: :pending_state diff --git a/app/models/ci/build_report_result.rb b/app/models/ci/build_report_result.rb index b2d99fab295..90b621b8da1 100644 --- a/app/models/ci/build_report_result.rb +++ b/app/models/ci/build_report_result.rb @@ -3,6 +3,9 @@ module Ci class BuildReportResult < Ci::ApplicationRecord include Ci::Partitionable + include SafelyChangeColumnDefault + + columns_changing_default :partition_id self.primary_key = :build_id diff --git a/app/models/ci/build_runner_session.rb b/app/models/ci/build_runner_session.rb index 5773b6132be..eaa2e1c428e 100644 --- a/app/models/ci/build_runner_session.rb +++ b/app/models/ci/build_runner_session.rb @@ -5,6 +5,9 @@ module Ci # Data will be removed after transitioning from running to any state. class BuildRunnerSession < Ci::ApplicationRecord include Ci::Partitionable + include SafelyChangeColumnDefault + + columns_changing_default :partition_id TERMINAL_SUBPROTOCOL = 'terminal.gitlab.com' DEFAULT_SERVICE_NAME = 'build' diff --git a/app/models/ci/build_trace_chunk.rb b/app/models/ci/build_trace_chunk.rb index 03b59b19ef1..0a0f401c9d5 100644 --- a/app/models/ci/build_trace_chunk.rb +++ b/app/models/ci/build_trace_chunk.rb @@ -8,6 +8,9 @@ module Ci include ::Checksummable include ::Gitlab::ExclusiveLeaseHelpers include ::Gitlab::OptimisticLocking + include SafelyChangeColumnDefault + + columns_changing_default :partition_id belongs_to :build, class_name: "Ci::Build", foreign_key: :build_id, inverse_of: :trace_chunks @@ -166,7 +169,7 @@ module Ci raise FailedToPersistDataError, 'Modifed build trace chunk detected' if has_changes_to_save? self.class.with_read_consistency(build) do - reset.then(&:unsafe_persist_data!) + reset.unsafe_persist_data! end end rescue FailedToObtainLockError @@ -242,7 +245,7 @@ module Ci ## # We need to so persist data then save a new store identifier before we # remove data from the previous store to make this operation - # trasnaction-safe. `unsafe_set_data! calls `save!` because of this + # transaction-safe. `unsafe_set_data! calls `save!` because of this # reason. # # TODO consider using callbacks and state machine to remove old data diff --git a/app/models/ci/build_trace_metadata.rb b/app/models/ci/build_trace_metadata.rb index 4c76089617f..c5ad3d19425 100644 --- a/app/models/ci/build_trace_metadata.rb +++ b/app/models/ci/build_trace_metadata.rb @@ -3,6 +3,9 @@ module Ci class BuildTraceMetadata < Ci::ApplicationRecord include Ci::Partitionable + include SafelyChangeColumnDefault + + columns_changing_default :partition_id MAX_ATTEMPTS = 5 self.table_name = 'ci_build_trace_metadata' diff --git a/app/models/ci/catalog/resource.rb b/app/models/ci/catalog/resource.rb index 77cfe91ddd6..38603ddfe59 100644 --- a/app/models/ci/catalog/resource.rb +++ b/app/models/ci/catalog/resource.rb @@ -19,6 +19,8 @@ module Ci delegate :avatar_path, :description, :name, :star_count, :forks_count, to: :project + enum state: { draft: 0, published: 1 } + def versions project.releases.order_released_desc end diff --git a/app/models/ci/external_pull_request.rb b/app/models/ci/external_pull_request.rb new file mode 100644 index 00000000000..bd37aa9f85a --- /dev/null +++ b/app/models/ci/external_pull_request.rb @@ -0,0 +1,106 @@ +# frozen_string_literal: true + +# This model stores pull requests coming from external providers, such as +# GitHub, when GitLab project is set as CI/CD only and remote mirror. +# +# When setting up a remote mirror with GitHub we subscribe to push and +# pull_request webhook events. When a pull request is opened on GitHub, +# a webhook is sent out, we create or update the status of the pull +# request locally. +# +# When the mirror is updated and changes are pushed to branches we check +# if there are open pull requests for the source and target branch. +# If so, we create pipelines for external pull requests. +module Ci + class ExternalPullRequest < Ci::ApplicationRecord + include Gitlab::Utils::StrongMemoize + include ShaAttribute + include EachBatch + + belongs_to :project + + sha_attribute :source_sha + sha_attribute :target_sha + + validates :source_branch, presence: true + validates :target_branch, presence: true + validates :source_sha, presence: true + validates :target_sha, presence: true + validates :source_repository, presence: true + validates :target_repository, presence: true + validates :status, presence: true + + enum status: { + open: 1, + closed: 2 + } + + # We currently don't support pull requests from fork, so + # we are going to return an error to the webhook + validate :not_from_fork + + scope :by_source_branch, ->(branch) { where(source_branch: branch) } + scope :by_source_repository, ->(repository) { where(source_repository: repository) } + + # Needed to override Ci::ApplicationRecord as this does not have ci_ table prefix + self.table_name = 'external_pull_requests' + + def self.create_or_update_from_params(params) + find_params = params.slice(:project_id, :source_branch, :target_branch) + + safe_find_or_initialize_and_update(find: find_params, update: params) do |pull_request| + yield(pull_request) if block_given? + end + end + + def actual_branch_head? + actual_source_branch_sha == source_sha + end + + def from_fork? + source_repository != target_repository + end + + def source_ref + Gitlab::Git::BRANCH_REF_PREFIX + source_branch + end + + def predefined_variables + Gitlab::Ci::Variables::Collection.new.tap do |variables| + variables.append(key: 'CI_EXTERNAL_PULL_REQUEST_IID', value: pull_request_iid.to_s) + variables.append(key: 'CI_EXTERNAL_PULL_REQUEST_SOURCE_REPOSITORY', value: source_repository) + variables.append(key: 'CI_EXTERNAL_PULL_REQUEST_TARGET_REPOSITORY', value: target_repository) + variables.append(key: 'CI_EXTERNAL_PULL_REQUEST_SOURCE_BRANCH_SHA', value: source_sha) + variables.append(key: 'CI_EXTERNAL_PULL_REQUEST_TARGET_BRANCH_SHA', value: target_sha) + variables.append(key: 'CI_EXTERNAL_PULL_REQUEST_SOURCE_BRANCH_NAME', value: source_branch) + variables.append(key: 'CI_EXTERNAL_PULL_REQUEST_TARGET_BRANCH_NAME', value: target_branch) + end + end + + def modified_paths + project.repository.diff_stats(target_sha, source_sha).paths + end + + private + + def actual_source_branch_sha + project.commit(source_ref)&.sha + end + + def not_from_fork + return unless from_fork? + + errors.add(:base, _('Pull requests from fork are not supported')) + end + + def self.safe_find_or_initialize_and_update(find:, update:) + safe_ensure_unique(retries: 1) do + model = find_or_initialize_by(find) + + yield(model) if model.update(update) && block_given? + + model + end + end + end +end diff --git a/app/models/ci/group_variable.rb b/app/models/ci/group_variable.rb index 5522a01758f..25d0228beb0 100644 --- a/app/models/ci/group_variable.rb +++ b/app/models/ci/group_variable.rb @@ -14,6 +14,7 @@ module Ci alias_attribute :secret_value, :value + validates :description, length: { maximum: 255 }, allow_blank: true validates :key, uniqueness: { scope: [:group_id, :environment_scope], message: "(%{value}) has already been taken" @@ -36,6 +37,12 @@ module Ci .pluck(:environment_scope) end + # Sorting + scope :order_created_asc, -> { reorder(created_at: :asc) } + scope :order_created_desc, -> { reorder(created_at: :desc) } + scope :order_key_asc, -> { reorder(key: :asc) } + scope :order_key_desc, -> { reorder(key: :desc) } + self.limit_name = 'group_ci_variables' self.limit_scope = :group @@ -50,5 +57,14 @@ module Ci def group_ci_cd_settings_path Gitlab::Routing.url_helpers.group_settings_ci_cd_path(group) end + + def self.sort_by_attribute(method) + case method.to_s + when 'created_at_asc' then order_created_asc + when 'created_at_desc' then order_created_desc + when 'key_asc' then order_key_asc + when 'key_desc' then order_key_desc + end + end end end diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb index 5cd7988837e..11d70e088e9 100644 --- a/app/models/ci/job_artifact.rb +++ b/app/models/ci/job_artifact.rb @@ -13,6 +13,9 @@ module Ci include FileStoreMounter include EachBatch include Gitlab::Utils::StrongMemoize + include SafelyChangeColumnDefault + + columns_changing_default :partition_id enum accessibility: { public: 0, private: 1 }, _suffix: true diff --git a/app/models/ci/job_variable.rb b/app/models/ci/job_variable.rb index 573999995bc..21c9842399e 100644 --- a/app/models/ci/job_variable.rb +++ b/app/models/ci/job_variable.rb @@ -5,8 +5,11 @@ module Ci include Ci::Partitionable include Ci::NewHasVariable include Ci::RawVariable + include SafelyChangeColumnDefault include BulkInsertSafe + columns_changing_default :partition_id + belongs_to :job, class_name: "Ci::Build", foreign_key: :job_id, inverse_of: :job_variables partitionable scope: :job diff --git a/app/models/ci/pending_build.rb b/app/models/ci/pending_build.rb index 14050a1e78e..dc9a8b7a1bf 100644 --- a/app/models/ci/pending_build.rb +++ b/app/models/ci/pending_build.rb @@ -4,6 +4,9 @@ module Ci class PendingBuild < Ci::ApplicationRecord include EachBatch include Ci::Partitionable + include SafelyChangeColumnDefault + + columns_changing_default :partition_id belongs_to :project belongs_to :build, class_name: 'Ci::Build' diff --git a/app/models/ci/persistent_ref.rb b/app/models/ci/persistent_ref.rb index 57aa1962bd2..f713d5952bc 100644 --- a/app/models/ci/persistent_ref.rb +++ b/app/models/ci/persistent_ref.rb @@ -19,6 +19,11 @@ module Ci false end + # This needs to be kept in sync with `Ci::Pipeline#after_transition` calling `pipeline.persistent_ref.delete` + def should_delete? + pipeline.status.to_sym.in?(::Ci::Pipeline.stopped_statuses) + end + def create create_ref(sha, path) rescue StandardError => e @@ -27,6 +32,8 @@ module Ci end def delete + return unless should_delete? + delete_refs(path) rescue Gitlab::Git::Repository::NoRepository # no-op diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 6f2939583e0..bd327cfbe7b 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -17,6 +17,9 @@ module Ci include UpdatedAtFilterable include EachBatch include FastDestroyAll::Helpers + include SafelyChangeColumnDefault + + columns_changing_default :partition_id include IgnorableColumns ignore_column :id_convert_to_bigint, remove_with: '16.3', remove_after: '2023-08-22' @@ -51,7 +54,7 @@ module Ci belongs_to :auto_canceled_by, class_name: 'Ci::Pipeline', inverse_of: :auto_canceled_pipelines belongs_to :pipeline_schedule, class_name: 'Ci::PipelineSchedule' belongs_to :merge_request, class_name: 'MergeRequest' - belongs_to :external_pull_request + belongs_to :external_pull_request, class_name: 'Ci::ExternalPullRequest' belongs_to :ci_ref, class_name: 'Ci::Ref', foreign_key: :ci_ref_id, inverse_of: :pipelines has_internal_id :iid, scope: :project, presence: false, @@ -335,9 +338,14 @@ module Ci end end + # This needs to be kept in sync with `Ci::PipelineRef#should_delete?` after_transition any => ::Ci::Pipeline.stopped_statuses do |pipeline| pipeline.run_after_commit do - pipeline.persistent_ref.delete + if Feature.enabled?(:pipeline_cleanup_ref_worker_async, pipeline.project) + ::Ci::PipelineCleanupRefWorker.perform_async(pipeline.id) + else + pipeline.persistent_ref.delete + end end end diff --git a/app/models/ci/pipeline_variable.rb b/app/models/ci/pipeline_variable.rb index f2457af0074..9747f9ef527 100644 --- a/app/models/ci/pipeline_variable.rb +++ b/app/models/ci/pipeline_variable.rb @@ -5,9 +5,12 @@ module Ci include Ci::Partitionable include Ci::HasVariable include Ci::RawVariable - include IgnorableColumns + include SafelyChangeColumnDefault + + columns_changing_default :partition_id ignore_column :id_convert_to_bigint, remove_with: '16.3', remove_after: '2023-08-22' + ignore_column :pipeline_id_convert_to_bigint, remove_with: '16.5', remove_after: '2023-10-22' belongs_to :pipeline diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb index 6319163b0d7..4eb5c3c9ed2 100644 --- a/app/models/ci/runner.rb +++ b/app/models/ci/runner.rb @@ -72,6 +72,7 @@ module Ci has_many :runner_managers, inverse_of: :runner has_many :builds + has_many :running_builds, inverse_of: :runner has_many :runner_projects, inverse_of: :runner, autosave: true, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :projects, through: :runner_projects, disable_joins: true has_many :runner_namespaces, inverse_of: :runner, autosave: true @@ -198,6 +199,7 @@ module Ci scope :order_created_at_desc, -> { order(created_at: :desc) } scope :order_token_expires_at_asc, -> { order(token_expires_at: :asc) } scope :order_token_expires_at_desc, -> { order(token_expires_at: :desc) } + scope :with_tags, -> { preload(:tags) } scope :with_creator, -> { preload(:creator) } @@ -456,7 +458,7 @@ module Ci end new_version = values[:version] - schedule_runner_version_update(new_version) if new_version && values[:version] != version + schedule_runner_version_update(new_version) if new_version && new_version != version merge_cache_attributes(values) diff --git a/app/models/ci/runner_manager.rb b/app/models/ci/runner_manager.rb index e36024d9f5b..3a3f95a8c69 100644 --- a/app/models/ci/runner_manager.rb +++ b/app/models/ci/runner_manager.rb @@ -44,6 +44,10 @@ module Ci remove_duplicates: false).where(created_some_time_ago) end + scope :for_runner, ->(runner_id) do + where(runner_id: runner_id) + end + def self.online_contact_time_deadline Ci::Runner.online_contact_time_deadline end @@ -52,6 +56,13 @@ module Ci STALE_TIMEOUT.ago end + def self.aggregate_upgrade_status_by_runner_id + joins(:runner_version) + .group(:runner_id) + .maximum(:status) + .transform_values { |s| Ci::RunnerVersion.statuses.key(s).to_sym } + end + def heartbeat(values, update_contacted_at: true) ## # We can safely ignore writes performed by a runner heartbeat. We do @@ -66,7 +77,7 @@ module Ci end new_version = values[:version] - schedule_runner_version_update(new_version) if new_version && values[:version] != version + schedule_runner_version_update(new_version) if new_version && new_version != version merge_cache_attributes(values) diff --git a/app/models/ci/running_build.rb b/app/models/ci/running_build.rb index e6f80658f5d..cfdc47de531 100644 --- a/app/models/ci/running_build.rb +++ b/app/models/ci/running_build.rb @@ -10,6 +10,9 @@ module Ci # of the running builds there is worth the additional pressure. class RunningBuild < Ci::ApplicationRecord include Ci::Partitionable + include SafelyChangeColumnDefault + + columns_changing_default :partition_id partitionable scope: :build diff --git a/app/models/ci/sources/pipeline.rb b/app/models/ci/sources/pipeline.rb index 719d19f4169..4853c57d41f 100644 --- a/app/models/ci/sources/pipeline.rb +++ b/app/models/ci/sources/pipeline.rb @@ -5,6 +5,9 @@ module Ci class Pipeline < Ci::ApplicationRecord include Ci::Partitionable include Ci::NamespacedModelName + include SafelyChangeColumnDefault + + columns_changing_default :partition_id self.table_name = "ci_sources_pipelines" diff --git a/app/models/ci/stage.rb b/app/models/ci/stage.rb index d61760bd0fc..4f9a2e44562 100644 --- a/app/models/ci/stage.rb +++ b/app/models/ci/stage.rb @@ -7,6 +7,9 @@ module Ci include Ci::HasStatus include Gitlab::OptimisticLocking include Presentable + include SafelyChangeColumnDefault + + columns_changing_default :partition_id partitionable scope: :pipeline @@ -148,7 +151,7 @@ module Ci end def manual_playable? - blocked? || skipped? + blocked? end # This will be removed with ci_remove_ensure_stage_service diff --git a/app/models/ci/unit_test_failure.rb b/app/models/ci/unit_test_failure.rb index cfef1249164..37893f6cdae 100644 --- a/app/models/ci/unit_test_failure.rb +++ b/app/models/ci/unit_test_failure.rb @@ -3,6 +3,9 @@ module Ci class UnitTestFailure < Ci::ApplicationRecord include Ci::Partitionable + include SafelyChangeColumnDefault + + columns_changing_default :partition_id REPORT_WINDOW = 14.days diff --git a/app/models/ci/variable.rb b/app/models/ci/variable.rb index 23fe89c38df..6f5972ebefa 100644 --- a/app/models/ci/variable.rb +++ b/app/models/ci/variable.rb @@ -14,6 +14,7 @@ module Ci alias_attribute :secret_value, :value + validates :description, length: { maximum: 255 }, allow_blank: true validates :key, uniqueness: { scope: [:project_id, :environment_scope], message: "(%{value}) has already been taken" diff --git a/app/models/clusters/agent.rb b/app/models/clusters/agent.rb index 372fdfda1ea..8dc866929f3 100644 --- a/app/models/clusters/agent.rb +++ b/app/models/clusters/agent.rb @@ -66,7 +66,6 @@ module Clusters def ci_access_authorized_for?(user) return false unless user - return false unless ::Feature.enabled?(:expose_authorized_cluster_agents, project) all_ci_access_authorized_projects_for(user).exists? || all_ci_access_authorized_namespaces_for(user).exists? @@ -74,7 +73,6 @@ module Clusters def user_access_authorized_for?(user) return false unless user - return false unless ::Feature.enabled?(:expose_authorized_cluster_agents, project) Clusters::Agents::Authorizations::UserAccess::Finder .new(user, agent: self, preload: false, limit: 1).execute.any? diff --git a/app/models/clusters/concerns/prometheus_client.rb b/app/models/clusters/concerns/prometheus_client.rb index 10cb307addd..d2f69b813aa 100644 --- a/app/models/clusters/concerns/prometheus_client.rb +++ b/app/models/clusters/concerns/prometheus_client.rb @@ -29,7 +29,7 @@ module Clusters rescue Kubeclient::HttpError, Errno::ECONNRESET, Errno::ECONNREFUSED, Errno::ENETUNREACH # If users have mistakenly set parameters or removed the depended clusters, # `proxy_url` could raise an exception because gitlab can not communicate with the cluster. - # Since `PrometheusAdapter#can_query?` is eargely loaded on environement pages in gitlab, + # Since `PrometheusAdapter#can_query?` is eargely loaded on environment pages in gitlab, # we need to silence the exceptions end diff --git a/app/models/commit.rb b/app/models/commit.rb index 26412205899..ded4b06a028 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -149,6 +149,10 @@ class Commit from_hash(hash, project) end + + def underscore + 'commit' + end end attr_accessor :raw diff --git a/app/models/commit_range.rb b/app/models/commit_range.rb index 90cdd267cbd..c6e507e4b6c 100644 --- a/app/models/commit_range.rb +++ b/app/models/commit_range.rb @@ -64,7 +64,7 @@ class CommitRange range_string = range_string.strip - unless range_string =~ /\A#{PATTERN}\z/o + unless /\A#{PATTERN}\z/o.match?(range_string) raise ArgumentError, "invalid CommitRange string format: #{range_string}" end diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index f26831c1049..3f631f583b6 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -8,13 +8,11 @@ class CommitStatus < Ci::ApplicationRecord include Presentable include BulkInsertableAssociations include TaggableQueries - include SafelyChangeColumnDefault self.table_name = 'ci_builds' self.sequence_name = 'ci_builds_id_seq' self.primary_key = :id partitionable scope: :pipeline - columns_changing_default :partition_id belongs_to :user belongs_to :project @@ -290,7 +288,7 @@ class CommitStatus < Ci::ApplicationRecord def sortable_name name.to_s.split(/(\d+)/).map do |v| - v =~ /\d+/ ? v.to_i : v + /\d+/.match?(v) ? v.to_i : v end end diff --git a/app/models/concerns/commit_signature.rb b/app/models/concerns/commit_signature.rb index 5dac3c7833a..5bdf6bb31bf 100644 --- a/app/models/concerns/commit_signature.rb +++ b/app/models/concerns/commit_signature.rb @@ -16,7 +16,8 @@ module CommitSignature unverified_key: 4, unknown_key: 5, multiple_signatures: 6, - revoked_key: 7 + revoked_key: 7, + verified_system: 8 } belongs_to :project, class_name: 'Project', foreign_key: 'project_id', optional: false diff --git a/app/models/concerns/database_event_tracking.rb b/app/models/concerns/database_event_tracking.rb index 26e184c202f..7e2f445189e 100644 --- a/app/models/concerns/database_event_tracking.rb +++ b/app/models/concerns/database_event_tracking.rb @@ -3,8 +3,6 @@ module DatabaseEventTracking extend ActiveSupport::Concern - FEATURE_FLAG_BATCH2_CLASSES = %w[Vulnerability MergeRequest::Metrics].freeze - included do after_create_commit :publish_database_create_event after_destroy_commit :publish_database_destroy_event @@ -24,9 +22,6 @@ module DatabaseEventTracking end def publish_database_event(name) - return unless database_events_for_class_enabled? - return unless database_events_feature_flag_enabled? - # Gitlab::Tracking#event is triggering Snowplow event # Snowplow events are sent with usage of # https://snowplow.github.io/snowplow-ruby-tracker/SnowplowTracker/AsyncEmitter.html @@ -54,14 +49,4 @@ module DatabaseEventTracking .with_indifferent_access .slice(*self.class::SNOWPLOW_ATTRIBUTES) end - - def database_events_for_class_enabled? - is_batch2 = FEATURE_FLAG_BATCH2_CLASSES.include?(self.class.to_s) - - !is_batch2 || Feature.enabled?(:product_intelligence_database_event_tracking_batch2) - end - - def database_events_feature_flag_enabled? - Feature.enabled?(:product_intelligence_database_event_tracking) - end end diff --git a/app/models/concerns/enums/ci/pipeline.rb b/app/models/concerns/enums/ci/pipeline.rb index d798a13741f..f5ffeb8c425 100644 --- a/app/models/concerns/enums/ci/pipeline.rb +++ b/app/models/concerns/enums/ci/pipeline.rb @@ -85,7 +85,8 @@ module Enums external_project_source: 5, bridge_source: 6, parameter_source: 7, - compliance_source: 8 + compliance_source: 8, + security_policies_default_source: 9 } end end diff --git a/app/models/concerns/enums/vulnerability.rb b/app/models/concerns/enums/vulnerability.rb index 4b325de61bc..dbf05dbc428 100644 --- a/app/models/concerns/enums/vulnerability.rb +++ b/app/models/concerns/enums/vulnerability.rb @@ -50,6 +50,10 @@ module Enums CONFIDENCE_LEVELS end + def self.parse_confidence_level(input) + input&.downcase.then { |value| confidence_levels.key?(value) ? value : 'unknown' } + end + def self.report_types REPORT_TYPES end @@ -58,6 +62,10 @@ module Enums SEVERITY_LEVELS end + def self.parse_severity_level(input) + input&.downcase.then { |value| severity_levels.key?(value) ? value : 'unknown' } + end + def self.detection_methods DETECTION_METHODS end diff --git a/app/models/concerns/expirable.rb b/app/models/concerns/expirable.rb index cc55315d6d7..af139e735af 100644 --- a/app/models/concerns/expirable.rb +++ b/app/models/concerns/expirable.rb @@ -6,10 +6,8 @@ module Expirable DAYS_TO_EXPIRE = 7 included do - scope :not, ->(scope) { where(scope.arel.constraints.reduce(:and).not) } - - scope :expired, -> { where.not(expires_at: nil).where(arel_table[:expires_at].lteq(Time.current)) } - scope :not_expired, -> { self.not(expired) } + scope :expired, -> { where(arel_table[:expires_at].lteq(Time.current)) } + scope :not_expired, -> { where(arel_table[:expires_at].gt(Time.current)).or(where(expires_at: nil)) } end def expired? diff --git a/app/models/concerns/has_user_type.rb b/app/models/concerns/has_user_type.rb index 9d4b8328e8d..2d0ff82e624 100644 --- a/app/models/concerns/has_user_type.rb +++ b/app/models/concerns/has_user_type.rb @@ -14,7 +14,7 @@ module HasUserType migration_bot: 7, security_bot: 8, automation_bot: 9, - security_policy_bot: 10, # Currently not in use. See https://gitlab.com/gitlab-org/gitlab/-/issues/384174 + security_policy_bot: 10, admin_bot: 11, suggested_reviewers_bot: 12, service_account: 13, diff --git a/app/models/concerns/ignorable_columns.rb b/app/models/concerns/ignorable_columns.rb index 4cbcb25406d..249d0b99494 100644 --- a/app/models/concerns/ignorable_columns.rb +++ b/app/models/concerns/ignorable_columns.rb @@ -18,7 +18,7 @@ module IgnorableColumns # # Indicate the earliest date and release we can stop ignoring the column with +remove_after+ (a date string) and +remove_with+ (a release) def ignore_columns(*columns, remove_after:, remove_with:) - raise ArgumentError, 'Please indicate when we can stop ignoring columns with remove_after (date string YYYY-MM-DD), example: ignore_columns(:name, remove_after: \'2019-12-01\', remove_with: \'12.6\')' unless remove_after =~ Gitlab::Regex.utc_date_regex + raise ArgumentError, 'Please indicate when we can stop ignoring columns with remove_after (date string YYYY-MM-DD), example: ignore_columns(:name, remove_after: \'2019-12-01\', remove_with: \'12.6\')' unless Gitlab::Regex.utc_date_regex.match?(remove_after) raise ArgumentError, 'Please indicate in which release we can stop ignoring columns with remove_with, example: ignore_columns(:name, remove_after: \'2019-12-01\', remove_with: \'12.6\')' unless remove_with self.ignored_columns += columns.flatten # rubocop:disable Cop/IgnoredColumns diff --git a/app/models/concerns/issue_available_features.rb b/app/models/concerns/issue_available_features.rb index 209456f8b67..3f65e701da7 100644 --- a/app/models/concerns/issue_available_features.rb +++ b/app/models/concerns/issue_available_features.rb @@ -19,7 +19,7 @@ module IssueAvailableFeatures end included do - scope :with_feature, ->(feature) { where(issue_type: available_features_for_issue_types[feature]) } + scope :with_feature, ->(feature) { with_issue_type(available_features_for_issue_types[feature]) } end def issue_type_supports?(feature) diff --git a/app/models/concerns/issues/forbid_issue_type_column_usage.rb b/app/models/concerns/issues/forbid_issue_type_column_usage.rb deleted file mode 100644 index 46a8a0278d9..00000000000 --- a/app/models/concerns/issues/forbid_issue_type_column_usage.rb +++ /dev/null @@ -1,59 +0,0 @@ -# frozen_string_literal: true - -# TODO: Remove with https://gitlab.com/gitlab-org/gitlab/-/issues/402699 -module Issues - module ForbidIssueTypeColumnUsage - extend ActiveSupport::Concern - - ForbiddenColumnUsed = Class.new(StandardError) - - included do - WorkItems::Type.base_types.each do |base_type, _value| - define_method "#{base_type}?".to_sym do - error_message = <<~ERROR - `#{model_name.element}.#{base_type}?` uses the `issue_type` column underneath. As we want to remove the column, - its usage is forbidden. You should use the `work_item_types` table instead. - - # Before - - #{model_name.element}.#{base_type}? => true - - # After - - #{model_name.element}.work_item_type.#{base_type}? => true - - More details in https://gitlab.com/groups/gitlab-org/-/epics/10529 - ERROR - - raise ForbiddenColumnUsed, error_message - end - - define_singleton_method base_type.to_sym do - error = ForbiddenColumnUsed.new( - <<~ERROR - `#{name}.#{base_type}` uses the `issue_type` column underneath. As we want to remove the column, - its usage is forbidden. You should use the `work_item_types` table instead. - - # Before - - #{name}.#{base_type} - - # After - - #{name}.with_issue_type(:#{base_type}) - - More details in https://gitlab.com/groups/gitlab-org/-/epics/10529 - ERROR - ) - - Gitlab::ErrorTracking.track_and_raise_for_dev_exception( - error, - method_name: "#{name}.#{base_type}" - ) - - with_issue_type(base_type.to_sym) - end - end - end - end -end diff --git a/app/models/concerns/milestoneish.rb b/app/models/concerns/milestoneish.rb index 4f2ea58f36d..3d9e09acf44 100644 --- a/app/models/concerns/milestoneish.rb +++ b/app/models/concerns/milestoneish.rb @@ -51,6 +51,7 @@ module Milestoneish def issue_participants_visible_by_user(user) User.joins(:issue_assignees) .where('issue_assignees.issue_id' => issues_visible_to_user(user).select(:id)) + .allow_cross_joins_across_databases(url: "https://gitlab.com/gitlab-org/gitlab/-/issues/417457") .distinct end @@ -90,9 +91,9 @@ module Milestoneish def expires_at if due_date if due_date.past? - "expired on #{due_date.to_s(:medium)}" + "expired on #{due_date.to_fs(:medium)}" else - "expires on #{due_date.to_s(:medium)}" + "expires on #{due_date.to_fs(:medium)}" end end end diff --git a/app/models/concerns/packages/debian/component_file.rb b/app/models/concerns/packages/debian/component_file.rb index cc7279d05f8..90d3abddbf1 100644 --- a/app/models/concerns/packages/debian/component_file.rb +++ b/app/models/concerns/packages/debian/component_file.rb @@ -10,8 +10,6 @@ module Packages include FileStoreMounter include IgnorableColumns - ignore_column :file_md5, remove_with: '16.2', remove_after: '2023-06-22' - def self.container_foreign_key "#{container_type}_id".to_sym end diff --git a/app/models/concerns/project_features_compatibility.rb b/app/models/concerns/project_features_compatibility.rb index 76c733b1c0b..c70100c03c8 100644 --- a/app/models/concerns/project_features_compatibility.rb +++ b/app/models/concerns/project_features_compatibility.rb @@ -4,7 +4,7 @@ # # After migrating issues_enabled merge_requests_enabled builds_enabled snippets_enabled and wiki_enabled # fields to a new table "project_features", support for the old fields is still needed in the API. -require 'gitlab/utils' +require 'gitlab/utils/all' module ProjectFeaturesCompatibility extend ActiveSupport::Concern diff --git a/app/models/concerns/protected_ref.rb b/app/models/concerns/protected_ref.rb index 7e1ebd1eba3..a87eadb9332 100644 --- a/app/models/concerns/protected_ref.rb +++ b/app/models/concerns/protected_ref.rb @@ -32,7 +32,12 @@ module ProtectedRef # to fail. has_many :"#{type}_access_levels", inverse_of: self.model_name.singular - validates :"#{type}_access_levels", length: { is: 1, message: "are restricted to a single instance per #{self.model_name.human}." }, unless: -> { allow_multiple?(type) } + validates :"#{type}_access_levels", + length: { + is: 1, + message: "are restricted to a single instance per #{self.model_name.human}." + }, + unless: -> { allow_multiple?(type) } accepts_nested_attributes_for :"#{type}_access_levels", allow_destroy: true end diff --git a/app/models/concerns/protected_ref_access.rb b/app/models/concerns/protected_ref_access.rb index c1c670db543..f0bb1cc359b 100644 --- a/app/models/concerns/protected_ref_access.rb +++ b/app/models/concerns/protected_ref_access.rb @@ -29,14 +29,30 @@ module ProtectedRefAccess def humanize(access_level) human_access_levels[access_level] end + + def non_role_types + [] + end end included do scope :maintainer, -> { where(access_level: Gitlab::Access::MAINTAINER) } scope :developer, -> { where(access_level: Gitlab::Access::DEVELOPER) } - scope :for_role, -> { where(user_id: nil, group_id: nil) } - - validates :access_level, presence: true, if: :role?, inclusion: { in: allowed_access_levels } + scope :for_role, -> { + if non_role_types.present? + where.missing(*non_role_types) + .allow_cross_joins_across_databases(url: "https://gitlab.com/gitlab-org/gitlab/-/issues/417457") + else + all + end + } + + protected_ref_fk = "#{module_parent.model_name.singular}_id" + validates :access_level, + presence: true, + inclusion: { in: allowed_access_levels }, + uniqueness: { scope: protected_ref_fk, conditions: -> { for_role } }, + if: :role? end def humanize diff --git a/app/models/concerns/protected_ref_deploy_key_access.rb b/app/models/concerns/protected_ref_deploy_key_access.rb new file mode 100644 index 00000000000..4275476a1ff --- /dev/null +++ b/app/models/concerns/protected_ref_deploy_key_access.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +module ProtectedRefDeployKeyAccess + extend ActiveSupport::Concern + + included do + belongs_to :deploy_key + + protected_ref_fk = "#{module_parent.model_name.singular}_id" + validates :deploy_key_id, uniqueness: { scope: protected_ref_fk, allow_nil: true } + validate :validate_deploy_key_membership + end + + class_methods do + def non_role_types + super << :deploy_key + end + end + + def type + return :deploy_key if deploy_key.present? + + super + end + + def humanize + return deploy_key.title if deploy_key? + + super + end + + def check_access(current_user) + super do + break enabled_deploy_key_for_user?(current_user) if deploy_key? + + yield if block_given? + end + end + + private + + def deploy_key? + type == :deploy_key + end + + def validate_deploy_key_membership + return if deploy_key.nil? || deploy_key_has_write_access_to_project? + + errors.add(:deploy_key, 'is not enabled for this project') + end + + def enabled_deploy_key_for_user?(current_user) + current_user.can?(:read_project, project) && + deploy_key.user_id == current_user.id && + deploy_key_has_write_access_to_project? + end + + def deploy_key_has_write_access_to_project? + DeployKey.with_write_access_for_project(project, deploy_key: deploy_key).exists? + end +end diff --git a/app/models/concerns/spammable.rb b/app/models/concerns/spammable.rb index 6550c5a94a0..5986f8f5b5f 100644 --- a/app/models/concerns/spammable.rb +++ b/app/models/concerns/spammable.rb @@ -138,7 +138,7 @@ module Spammable result.reject(&:blank?).join("\n") end - # Override in Spammable if further checks are necessary + # Override in included class if further checks are necessary def check_for_spam?(*) spammable_attribute_changed? end @@ -153,8 +153,8 @@ module Spammable end end - # Override in Spammable if differs - def allow_possible_spam? + # Override in included class if you want to allow possible spam under specific circumstances + def allow_possible_spam?(*) Gitlab::CurrentSettings.allow_possible_spam end end diff --git a/app/models/concerns/triggerable_hooks.rb b/app/models/concerns/triggerable_hooks.rb index e3800caa43f..0e72bd30a37 100644 --- a/app/models/concerns/triggerable_hooks.rb +++ b/app/models/concerns/triggerable_hooks.rb @@ -17,7 +17,8 @@ module TriggerableHooks feature_flag_hooks: :feature_flag_events, release_hooks: :releases_events, member_hooks: :member_events, - subgroup_hooks: :subgroup_events + subgroup_hooks: :subgroup_events, + emoji_hooks: :emoji_events }.freeze extend ActiveSupport::Concern diff --git a/app/models/concerns/vulnerability_finding_helpers.rb b/app/models/concerns/vulnerability_finding_helpers.rb index a5b69997900..e8a50497b20 100644 --- a/app/models/concerns/vulnerability_finding_helpers.rb +++ b/app/models/concerns/vulnerability_finding_helpers.rb @@ -59,6 +59,7 @@ module VulnerabilityFindingHelpers evidence = Vulnerabilities::Finding::Evidence.new(data: report_finding.evidence.data) if report_finding.evidence Vulnerabilities::Finding.new(finding_data).tap do |finding| + finding.uuid = security_finding.uuid finding.location_fingerprint = report_finding.location.fingerprint finding.vulnerability = vulnerability_for(security_finding.uuid) finding.project = project diff --git a/app/models/concerns/vulnerability_finding_signature_helpers.rb b/app/models/concerns/vulnerability_finding_signature_helpers.rb index 71a12b4077b..a225625815b 100644 --- a/app/models/concerns/vulnerability_finding_signature_helpers.rb +++ b/app/models/concerns/vulnerability_finding_signature_helpers.rb @@ -2,12 +2,17 @@ module VulnerabilityFindingSignatureHelpers extend ActiveSupport::Concern + # If the location object describes a physical location within a file # (filename + line numbers), the 'location' algorithm_type should be used # If the location object describes arbitrary data, then the 'hash' # algorithm_type should be used. - - ALGORITHM_TYPES = { hash: 1, location: 2, scope_offset: 3 }.with_indifferent_access.freeze + ALGORITHM_TYPES = { + hash: 1, + location: 2, + scope_offset: 3, + scope_offset_compressed: 4 + }.with_indifferent_access.freeze class_methods do def priority(algorithm_type) diff --git a/app/models/container_repository.rb b/app/models/container_repository.rb index 0f0abeae795..6a52f6a0112 100644 --- a/app/models/container_repository.rb +++ b/app/models/container_repository.rb @@ -403,7 +403,7 @@ class ContainerRepository < ApplicationRecord end def migrated? - Gitlab.com? + Gitlab.com_except_jh? end def last_import_step_done_at @@ -526,7 +526,7 @@ class ContainerRepository < ApplicationRecord def size strong_memoize(:size) do - next unless Gitlab.com? + next unless Gitlab.com_except_jh? next if self.created_at.before?(MIGRATION_PHASE_1_STARTED_AT) && self.migration_state != 'import_done' next unless gitlab_api_client.supports_gitlab_api? diff --git a/app/models/deployment.rb b/app/models/deployment.rb index 1e3a80087c8..b59b22c10c4 100644 --- a/app/models/deployment.rb +++ b/app/models/deployment.rb @@ -351,7 +351,7 @@ class Deployment < ApplicationRecord end def formatted_deployment_time - deployed_at&.to_time&.in_time_zone&.to_s(:medium) + deployed_at&.to_time&.in_time_zone&.to_fs(:medium) end def deployed_by @@ -447,7 +447,7 @@ class Deployment < ApplicationRecord # when refs_by_oid is passed an SHA, returns refs for that commit def tags(limit: 100) strong_memoize_with(:tag, limit) do - project.repository.refs_by_oid(oid: sha, limit: limit, ref_patterns: [Gitlab::Git::TAG_REF_PREFIX]) || [] + project.repository.refs_by_oid(oid: sha, limit: limit, ref_patterns: [Gitlab::Git::TAG_REF_PREFIX]) end end diff --git a/app/models/design_management/repository.rb b/app/models/design_management/repository.rb index 39077fdbcb1..7410944e174 100644 --- a/app/models/design_management/repository.rb +++ b/app/models/design_management/repository.rb @@ -8,7 +8,7 @@ module DesignManagement belongs_to :project, inverse_of: :design_management_repository validates :project, presence: true, uniqueness: true - delegate :lfs_enabled?, :storage, :repository_storage, to: :project + delegate :lfs_enabled?, :storage, :repository_storage, :run_after_commit, to: :project def repository ::DesignManagement::GitRepository.new( diff --git a/app/models/environment.rb b/app/models/environment.rb index 8480272eced..241b454f5ce 100644 --- a/app/models/environment.rb +++ b/app/models/environment.rb @@ -18,7 +18,7 @@ class Environment < ApplicationRecord belongs_to :cluster_agent, class_name: 'Clusters::Agent', optional: true, inverse_of: :environments use_fast_destroy :all_deployments - nullify_if_blank :external_url + nullify_if_blank :external_url, :kubernetes_namespace has_many :all_deployments, class_name: 'Deployment' has_many :deployments, -> { visible } @@ -70,13 +70,15 @@ class Environment < ApplicationRecord length: { maximum: 255 }, allow_nil: true - # Currently, the tier presence is validaed for newly created environments. - # After the `BackfillEnvironmentTiers` background migration has been completed, we should remove `on: :create`. - # See https://gitlab.com/gitlab-org/gitlab/-/issues/385253. - # Todo: Remove along with FF `validate_environment_tier_presence`. - validates :tier, presence: true, on: :create, unless: :validate_environment_tier_present? + validates :kubernetes_namespace, + allow_nil: true, + length: 1..63, + format: { + with: Gitlab::Regex.kubernetes_namespace_regex, + message: Gitlab::Regex.kubernetes_namespace_regex_message + } - validates :tier, presence: true, if: :validate_environment_tier_present? + validates :tier, presence: true validate :safe_external_url validate :merge_request_not_changed @@ -602,10 +604,6 @@ class Environment < ApplicationRecord self.class.tiers[:other] end end - - def validate_environment_tier_present? - Feature.enabled?(:validate_environment_tier_presence, self.project) - end end Environment.prepend_mod_with('Environment') diff --git a/app/models/external_issue.rb b/app/models/external_issue.rb index 36030b80370..06dc9cad5f9 100644 --- a/app/models/external_issue.rb +++ b/app/models/external_issue.rb @@ -44,7 +44,7 @@ class ExternalIssue end def reference_link_text(from = nil) - return "##{id}" if id =~ /^\d+$/ + return "##{id}" if /^\d+$/.match?(id) id end diff --git a/app/models/external_pull_request.rb b/app/models/external_pull_request.rb deleted file mode 100644 index 94c242782c1..00000000000 --- a/app/models/external_pull_request.rb +++ /dev/null @@ -1,106 +0,0 @@ -# frozen_string_literal: true - -# This model stores pull requests coming from external providers, such as -# GitHub, when GitLab project is set as CI/CD only and remote mirror. -# -# When setting up a remote mirror with GitHub we subscribe to push and -# pull_request webhook events. When a pull request is opened on GitHub, -# a webhook is sent out, we create or update the status of the pull -# request locally. -# -# When the mirror is updated and changes are pushed to branches we check -# if there are open pull requests for the source and target branch. -# If so, we create pipelines for external pull requests. -class ExternalPullRequest < Ci::ApplicationRecord - include Gitlab::Utils::StrongMemoize - include ShaAttribute - include EachBatch - - belongs_to :project - - sha_attribute :source_sha - sha_attribute :target_sha - - validates :source_branch, presence: true - validates :target_branch, presence: true - validates :source_sha, presence: true - validates :target_sha, presence: true - validates :source_repository, presence: true - validates :target_repository, presence: true - validates :status, presence: true - - enum status: { - open: 1, - closed: 2 - } - - # We currently don't support pull requests from fork, so - # we are going to return an error to the webhook - validate :not_from_fork - - scope :by_source_branch, ->(branch) { where(source_branch: branch) } - scope :by_source_repository, -> (repository) { where(source_repository: repository) } - - # Needed to override Ci::ApplicationRecord as this does not have ci_ table prefix - self.table_name = 'external_pull_requests' - - def self.create_or_update_from_params(params) - find_params = params.slice(:project_id, :source_branch, :target_branch) - - safe_find_or_initialize_and_update(find: find_params, update: params) do |pull_request| - yield(pull_request) if block_given? - end - end - - def actual_branch_head? - actual_source_branch_sha == source_sha - end - - def from_fork? - source_repository != target_repository - end - - def source_ref - Gitlab::Git::BRANCH_REF_PREFIX + source_branch - end - - def predefined_variables - Gitlab::Ci::Variables::Collection.new.tap do |variables| - variables.append(key: 'CI_EXTERNAL_PULL_REQUEST_IID', value: pull_request_iid.to_s) - variables.append(key: 'CI_EXTERNAL_PULL_REQUEST_SOURCE_REPOSITORY', value: source_repository) - variables.append(key: 'CI_EXTERNAL_PULL_REQUEST_TARGET_REPOSITORY', value: target_repository) - variables.append(key: 'CI_EXTERNAL_PULL_REQUEST_SOURCE_BRANCH_SHA', value: source_sha) - variables.append(key: 'CI_EXTERNAL_PULL_REQUEST_TARGET_BRANCH_SHA', value: target_sha) - variables.append(key: 'CI_EXTERNAL_PULL_REQUEST_SOURCE_BRANCH_NAME', value: source_branch) - variables.append(key: 'CI_EXTERNAL_PULL_REQUEST_TARGET_BRANCH_NAME', value: target_branch) - end - end - - def modified_paths - project.repository.diff_stats(target_sha, source_sha).paths - end - - private - - def actual_source_branch_sha - project.commit(source_ref)&.sha - end - - def not_from_fork - if from_fork? - errors.add(:base, _('Pull requests from fork are not supported')) - end - end - - def self.safe_find_or_initialize_and_update(find:, update:) - safe_ensure_unique(retries: 1) do - model = find_or_initialize_by(find) - - if model.update(update) - yield(model) if block_given? - end - - model - end - end -end diff --git a/app/models/group.rb b/app/models/group.rb index 85971c48567..2b5a392e02c 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -510,7 +510,9 @@ 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) + 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") end def ldap_synced? @@ -663,13 +665,24 @@ class Group < Namespace # 2. They belong to a project that belongs to the group # 3. They belong to a sub-group or project in such sub-group # 4. They belong to an ancestor group - def direct_and_indirect_users + # 5. They belong to a group that is shared with this group, if share_with_groups is true + def direct_and_indirect_users(share_with_groups: false) + members = if share_with_groups + # We only need :user_id column, but + # `members_from_self_and_ancestor_group_shares` needs more + # columns to make the CTE query work. + GroupMember.from_union([ + direct_and_indirect_members.select(:user_id, :source_type, :type), + members_from_self_and_ancestor_group_shares.reselect(:user_id, :source_type, :type) + ]) + else + direct_and_indirect_members + end + User.from_union([ - User - .where(id: direct_and_indirect_members.select(:user_id)) - .reorder(nil), - project_users_with_descendants - ]) + User.where(id: members.select(:user_id)).reorder(nil), + project_users_with_descendants + ]) end # Returns all users (also inactive) that are members of the group because: @@ -683,7 +696,7 @@ class Group < Namespace .where(id: direct_and_indirect_members_with_inactive.select(:user_id)) .reorder(nil), project_users_with_descendants - ]) + ]).allow_cross_joins_across_databases(url: "https://gitlab.com/gitlab-org/gitlab/-/issues/417455") # failed in spec/tasks/gitlab/user_management_rake_spec.rb end def users_count @@ -696,6 +709,7 @@ class Group < Namespace User .joins(projects: :group) .where(namespaces: { id: self_and_descendants.select(:id) }) + .allow_cross_joins_across_databases(url: "https://gitlab.com/gitlab-org/gitlab/-/issues/417455") end # Return the highest access level for a user @@ -802,8 +816,11 @@ class Group < Namespace end def execute_integrations(data, hooks_scope) - # NOOP - # TODO: group hooks https://gitlab.com/gitlab-org/gitlab/-/issues/216904 + return unless Feature.enabled?(:group_mentions, self) + + integrations.public_send(hooks_scope).each do |integration| # rubocop:disable GitlabSecurity/PublicSend + integration.async_execute(data) + end end def preload_shared_group_links @@ -813,16 +830,6 @@ class Group < Namespace ).call end - def update_shared_runners_setting!(state) - raise ArgumentError unless SHARED_RUNNERS_SETTINGS.include?(state) - - case state - when SR_DISABLED_AND_UNOVERRIDABLE then disable_shared_runners! # also disallows override - when SR_DISABLED_WITH_OVERRIDE, SR_DISABLED_AND_OVERRIDABLE then disable_shared_runners_and_allow_override! - when SR_ENABLED then enable_shared_runners! # set both to true - end - end - def first_owner owners.first || parent&.first_owner || owner end @@ -969,12 +976,14 @@ class Group < Namespace end def max_member_access(user_ids) - 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) + ::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 end @@ -1057,45 +1066,6 @@ class Group < Namespace Arel::Nodes::SqlLiteral.new(column_alias)) end - def disable_shared_runners! - update!( - shared_runners_enabled: false, - allow_descendants_override_disabled_shared_runners: false) - - group_ids = descendants - unless group_ids.empty? - Group.by_id(group_ids).update_all( - shared_runners_enabled: false, - allow_descendants_override_disabled_shared_runners: false) - end - - all_projects.update_all(shared_runners_enabled: false) - end - - def disable_shared_runners_and_allow_override! - # enabled -> disabled_and_overridable - if shared_runners_enabled? - update!( - shared_runners_enabled: false, - allow_descendants_override_disabled_shared_runners: true) - - group_ids = descendants - unless group_ids.empty? - Group.by_id(group_ids).update_all(shared_runners_enabled: false) - end - - all_projects.update_all(shared_runners_enabled: false) - - # disabled_and_unoverridable -> disabled_and_overridable - else - update!(allow_descendants_override_disabled_shared_runners: true) - end - end - - def enable_shared_runners! - update!(shared_runners_enabled: true) - end - def runners_token_prefix RunnersTokenPrefixable::RUNNERS_TOKEN_PREFIX end diff --git a/app/models/hooks/project_hook.rb b/app/models/hooks/project_hook.rb index 695041f0247..05c5ad22218 100644 --- a/app/models/hooks/project_hook.rb +++ b/app/models/hooks/project_hook.rb @@ -21,7 +21,8 @@ class ProjectHook < WebHook :wiki_page_hooks, :deployment_hooks, :feature_flag_hooks, - :release_hooks + :release_hooks, + :emoji_hooks ] belongs_to :project diff --git a/app/models/hooks/web_hook_log.rb b/app/models/hooks/web_hook_log.rb index e08294058e4..4c35f699468 100644 --- a/app/models/hooks/web_hook_log.rb +++ b/app/models/hooks/web_hook_log.rb @@ -66,7 +66,7 @@ class WebHookLog < ApplicationRecord def redact_user_emails self.request_data.deep_transform_values! do |value| - value.to_s =~ URI::MailTo::EMAIL_REGEXP ? _('[REDACTED]') : value + URI::MailTo::EMAIL_REGEXP.match?(value.to_s) ? _('[REDACTED]') : value end end diff --git a/app/models/integration.rb b/app/models/integration.rb index f2f242136ab..f823a385022 100644 --- a/app/models/integration.rb +++ b/app/models/integration.rb @@ -90,6 +90,8 @@ class Integration < ApplicationRecord attribute :push_events, default: true attribute :tag_push_events, default: true attribute :wiki_page_events, default: true + attribute :group_mention_events, default: false + attribute :group_confidential_mention_events, default: false after_initialize :initialize_properties @@ -137,6 +139,8 @@ class Integration < ApplicationRecord scope :alert_hooks, -> { where(alert_events: true, active: true) } scope :incident_hooks, -> { where(incident_events: true, active: true) } scope :deployment, -> { where(category: 'deployment') } + scope :group_mention_hooks, -> { where(group_mention_events: true, active: true) } + scope :group_confidential_mention_hooks, -> { where(group_confidential_mention_events: true, active: true) } class << self private @@ -586,6 +590,7 @@ class Integration < ApplicationRecord end def async_execute(data) + return if ::Gitlab::SilentMode.enabled? return unless supported_events.include?(data[:object_kind]) Integrations::ExecuteWorker.perform_async(id, data) @@ -600,6 +605,10 @@ class Integration < ApplicationRecord category == :chat end + def ci? + category == :ci + end + private # Ancestors sorted by hierarchy depth in bottom-top order. diff --git a/app/models/integrations/base_chat_notification.rb b/app/models/integrations/base_chat_notification.rb index 4477f3d207f..c9de4d2b3bb 100644 --- a/app/models/integrations/base_chat_notification.rb +++ b/app/models/integrations/base_chat_notification.rb @@ -262,11 +262,11 @@ module Integrations end def project_name - project.full_name + project.try(:full_name) end def project_url - project.web_url + project.try(:web_url) end def update?(data) diff --git a/app/models/integrations/base_slack_notification.rb b/app/models/integrations/base_slack_notification.rb index c83a559e0da..29a20419809 100644 --- a/app/models/integrations/base_slack_notification.rb +++ b/app/models/integrations/base_slack_notification.rb @@ -7,6 +7,8 @@ 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 @@ -16,15 +18,20 @@ 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 override :supported_events def supported_events - additional = ['alert'] + additional = %w[alert] - super + additional + if group_level? && Feature.enabled?(:group_mentions, group) + additional += %w[group_mention group_confidential_mention] + end + + (super + additional).freeze end override :configurable_channels? diff --git a/app/models/integrations/chat_message/group_mention_message.rb b/app/models/integrations/chat_message/group_mention_message.rb new file mode 100644 index 00000000000..a2bc00ddbd9 --- /dev/null +++ b/app/models/integrations/chat_message/group_mention_message.rb @@ -0,0 +1,102 @@ +# frozen_string_literal: true + +module Integrations + module ChatMessage + class GroupMentionMessage < BaseMessage + ISSUE_KIND = 'issue' + MR_KIND = 'merge_request' + NOTE_KIND = 'note' + + KNOWN_KINDS = [ISSUE_KIND, MR_KIND, NOTE_KIND].freeze + + def initialize(params) + super + params = HashWithIndifferentAccess.new(params) + + @group_name, @group_url = params[:mentioned].values_at(:name, :url) + @detail = nil + + obj_attr = params[:object_attributes] + obj_kind = obj_attr[:object_kind] + raise NotImplementedError unless KNOWN_KINDS.include?(obj_kind) + + case obj_kind + when 'issue' + @source_name, @title = get_source_for_issue(obj_attr) + @detail = obj_attr[:description] + when 'merge_request' + @source_name, @title = get_source_for_merge_request(obj_attr) + @detail = obj_attr[:description] + when 'note' + if params[:commit] + @source_name, @title = get_source_for_commit(params[:commit]) + elsif params[:issue] + @source_name, @title = get_source_for_issue(params[:issue]) + elsif params[:merge_request] + @source_name, @title = get_source_for_merge_request(params[:merge_request]) + else + raise NotImplementedError + end + + @detail = obj_attr[:note] + end + + @source_url = obj_attr[:url] + end + + def attachments + if markdown + detail + else + [{ text: format(detail), color: attachment_color }] + end + end + + def activity + { + title: "Group #{group_link} was mentioned in #{source_link}", + subtitle: "of #{project_link}", + text: strip_markup(formatted_title), + image: user_avatar + } + end + + private + + attr_reader :group_name, :group_url, :source_name, :source_url, :title, :detail + + def get_source_for_commit(params) + commit_sha = Commit.truncate_sha(params[:id]) + ["commit #{commit_sha}", params[:title]] + end + + def get_source_for_issue(params) + ["issue ##{params[:iid]}", params[:title]] + end + + def get_source_for_merge_request(params) + ["merge request !#{params[:iid]}", params[:title]] + end + + def message + "Group #{group_link} was mentioned in #{source_link} of #{project_link}: *#{formatted_title}*" + end + + def formatted_title + strip_markup(title.lines.first.chomp) + end + + def group_link + link(group_name, group_url) + end + + def source_link + link(source_name, source_url) + end + + def project_link + link(project_name, project_url) + end + end + end +end diff --git a/app/models/integrations/hangouts_chat.rb b/app/models/integrations/hangouts_chat.rb index ad82f1b916f..7ba9bbc38e6 100644 --- a/app/models/integrations/hangouts_chat.rb +++ b/app/models/integrations/hangouts_chat.rb @@ -2,6 +2,23 @@ module Integrations class HangoutsChat < BaseChatNotification + undef :notify_only_broken_pipelines + + field :webhook, + section: SECTION_TYPE_CONNECTION, + help: 'https://chat.googleapis.com/v1/spaces…', + required: true + + field :notify_only_broken_pipelines, + type: 'checkbox', + section: SECTION_TYPE_CONFIGURATION + + 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 } + def title 'Google Chat' end @@ -19,25 +36,15 @@ module Integrations 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 + def fields + self.class.fields + build_event_channels end - def self.supported_events - %w[push issue confidential_issue merge_request note confidential_note tag_push - pipeline wiki_page] + def default_channel_placeholder end - def default_fields - [ - { type: 'text', name: 'webhook', help: 'https://chat.googleapis.com/v1/spaces…' }, - { type: 'checkbox', name: 'notify_only_broken_pipelines' }, - { - type: 'select', - name: 'branches_to_be_notified', - title: s_('Integrations|Branches for which notifications are to be sent'), - choices: self.class.branch_choices - } - ] + def self.supported_events + %w[push issue confidential_issue merge_request note confidential_note tag_push pipeline wiki_page] end private diff --git a/app/models/integrations/microsoft_teams.rb b/app/models/integrations/microsoft_teams.rb index d6cbe5760e8..a9ed0bd3da1 100644 --- a/app/models/integrations/microsoft_teams.rb +++ b/app/models/integrations/microsoft_teams.rb @@ -2,6 +2,24 @@ module Integrations class MicrosoftTeams < BaseChatNotification + undef :notify_only_broken_pipelines + + field :webhook, + section: SECTION_TYPE_CONNECTION, + help: 'https://outlook.office.com/webhook/…', + required: true + + field :notify_only_broken_pipelines, + type: 'checkbox', + 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 } + def title 'Microsoft Teams notifications' end @@ -26,23 +44,8 @@ module Integrations pipeline wiki_page] end - def default_fields - [ - { type: 'text', section: SECTION_TYPE_CONNECTION, name: 'webhook', help: 'https://outlook.office.com/webhook/…', required: true }, - { - type: 'checkbox', - section: SECTION_TYPE_CONFIGURATION, - name: 'notify_only_broken_pipelines', - help: 'If selected, successful pipelines do not trigger a notification event.' - }, - { - type: 'select', - section: SECTION_TYPE_CONFIGURATION, - name: 'branches_to_be_notified', - title: s_('Integrations|Branches for which notifications are to be sent'), - choices: self.class.branch_choices - } - ] + def fields + self.class.fields + build_event_channels end def sections diff --git a/app/models/integrations/prometheus.rb b/app/models/integrations/prometheus.rb index 2dc0fd7d011..8969c6c13b2 100644 --- a/app/models/integrations/prometheus.rb +++ b/app/models/integrations/prometheus.rb @@ -15,7 +15,7 @@ module Integrations title: 'API URL', placeholder: -> { s_('PrometheusService|https://prometheus.example.com/') }, help: -> { s_('PrometheusService|The Prometheus API base URL.') }, - required: true + required: false field :google_iap_audience_client_id, title: 'Google IAP Audience Client ID', @@ -34,8 +34,8 @@ module Integrations # to allow localhost URLs when the following conditions are true: # 1. api_url is the internal Prometheus URL. with_options presence: true do - validates :api_url, public_url: true, if: ->(object) { object.manual_configuration? && !object.allow_local_api_url? } - validates :api_url, url: true, if: ->(object) { object.manual_configuration? && object.allow_local_api_url? } + validates :api_url, public_url: true, if: ->(object) { object.api_url.present? && object.manual_configuration? && !object.allow_local_api_url? } + validates :api_url, url: true, if: ->(object) { object.api_url.present? && object.manual_configuration? && object.allow_local_api_url? } end before_save :synchronize_service_state diff --git a/app/models/integrations/unify_circuit.rb b/app/models/integrations/unify_circuit.rb index aa19133b8c2..6c447c8f4e4 100644 --- a/app/models/integrations/unify_circuit.rb +++ b/app/models/integrations/unify_circuit.rb @@ -2,6 +2,23 @@ module Integrations class UnifyCircuit < BaseChatNotification + undef :notify_only_broken_pipelines + + field :webhook, + section: SECTION_TYPE_CONNECTION, + help: 'https://yourcircuit.com/rest/v2/webhooks/incoming/…', + required: true + + field :notify_only_broken_pipelines, + type: 'checkbox', + section: SECTION_TYPE_CONFIGURATION + + 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 } + def title 'Unify Circuit' end @@ -14,6 +31,10 @@ module Integrations 'unify_circuit' end + def fields + self.class.fields + build_event_channels + end + def help docs_link = ActionController::Base.helpers.link_to _('How do I set up this service?'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/unify_circuit'), target: '_blank', rel: 'noopener noreferrer' s_('Integrations|Send notifications about project events to a Unify Circuit conversation. %{docs_link}').html_safe % { docs_link: docs_link.html_safe } @@ -27,19 +48,6 @@ module Integrations pipeline wiki_page] end - def default_fields - [ - { type: 'text', name: 'webhook', help: 'https://yourcircuit.com/rest/v2/webhooks/incoming/…', required: true }, - { type: 'checkbox', name: 'notify_only_broken_pipelines' }, - { - type: 'select', - name: 'branches_to_be_notified', - title: s_('Integrations|Branches for which notifications are to be sent'), - choices: self.class.branch_choices - } - ] - end - private def notify(message, opts) diff --git a/app/models/integrations/webex_teams.rb b/app/models/integrations/webex_teams.rb index 8e6f5ca6d17..ef1bc81ea58 100644 --- a/app/models/integrations/webex_teams.rb +++ b/app/models/integrations/webex_teams.rb @@ -2,6 +2,23 @@ module Integrations class WebexTeams < BaseChatNotification + undef :notify_only_broken_pipelines + + field :webhook, + section: SECTION_TYPE_CONNECTION, + help: 'https://api.ciscospark.com/v1/webhooks/incoming/...', + required: true + + field :notify_only_broken_pipelines, + type: 'checkbox', + section: SECTION_TYPE_CONFIGURATION + + 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 } + def title s_("WebexTeamsService|Webex Teams") end @@ -14,6 +31,10 @@ module Integrations 'webex_teams' end + def fields + self.class.fields + build_event_channels + end + def help docs_link = ActionController::Base.helpers.link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/webex_teams'), target: '_blank', rel: 'noopener noreferrer' s_("WebexTeamsService|Send notifications about project events to a Webex Teams conversation. %{docs_link}") % { docs_link: docs_link.html_safe } @@ -23,21 +44,7 @@ module Integrations end def self.supported_events - %w[push issue confidential_issue merge_request note confidential_note tag_push - pipeline wiki_page] - end - - def default_fields - [ - { type: 'text', name: 'webhook', help: 'https://api.ciscospark.com/v1/webhooks/incoming/...', required: true }, - { type: 'checkbox', name: 'notify_only_broken_pipelines' }, - { - type: 'select', - name: 'branches_to_be_notified', - title: s_('Integrations|Branches for which notifications are to be sent'), - choices: self.class.branch_choices - } - ] + %w[push issue confidential_issue merge_request note confidential_note tag_push pipeline wiki_page] end private diff --git a/app/models/issue.rb b/app/models/issue.rb index 890af8a27a0..6e48dcab9ed 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -56,6 +56,8 @@ class Issue < ApplicationRecord # This default came from the enum `issue_type` column. Defined as default in the DB DEFAULT_ISSUE_TYPE = :issue + ignore_column :issue_type, remove_with: '16.4', remove_after: '2023-08-22' + belongs_to :project belongs_to :namespace, inverse_of: :issues @@ -133,12 +135,6 @@ class Issue < ApplicationRecord validate :allowed_work_item_type_change, on: :update, if: :work_item_type_id_changed? validate :due_date_after_start_date validate :parent_link_confidentiality - # using a custom validation since we are overwriting the `issue_type` method to use the work_item_types table - validate :issue_type_attribute_present - - enum issue_type: WorkItems::Type.base_types - # TODO: Remove with https://gitlab.com/gitlab-org/gitlab/-/issues/402699 - include ::Issues::ForbidIssueTypeColumnUsage alias_method :issuing_parent, :project alias_attribute :issuing_parent_id, :project_id @@ -187,7 +183,10 @@ class Issue < ApplicationRecord scope :order_closed_at_desc, -> { reorder(arel_table[:closed_at].desc.nulls_last) } scope :preload_associated_models, -> { preload(:assignees, :labels, project: :namespace) } - scope :with_web_entity_associations, -> { preload(:author, :namespace, project: [:project_feature, :route, namespace: :route]) } + scope :with_web_entity_associations, -> do + preload(:author, :namespace, :labels, project: [:project_feature, :route, namespace: :route]) + end + scope :preload_awardable, -> { preload(:award_emoji) } scope :with_alert_management_alerts, -> { joins(:alert_management_alert) } scope :with_prometheus_alert_events, -> { joins(:issues_prometheus_alert_events) } @@ -201,24 +200,17 @@ class Issue < ApplicationRecord scope :with_issue_type, ->(types) { types = Array(types) - if Feature.enabled?(:issue_type_uses_work_item_types_table) - # Using != 1 since we also want the guard clause to handle empty arrays - return joins(:work_item_type).where(work_item_types: { base_type: types }) if types.size != 1 + # Using != 1 since we also want the guard clause to handle empty arrays + return joins(:work_item_type).where(work_item_types: { base_type: types }) if types.size != 1 - where( - '"issues"."work_item_type_id" = (?)', - WorkItems::Type.by_type(types.first).select(:id).limit(1) - ) - else - where(issue_type: types) - end + # This optimization helps the planer use the correct indexes when filtering by a single type + where( + '"issues"."work_item_type_id" = (?)', + WorkItems::Type.by_type(types.first).select(:id).limit(1) + ) } scope :without_issue_type, ->(types) { - if Feature.enabled?(:issue_type_uses_work_item_types_table) - joins(:work_item_type).where.not(work_item_types: { base_type: types }) - else - where.not(issue_type: types) - end + joins(:work_item_type).where.not(work_item_types: { base_type: types }) } scope :public_only, -> { where(confidential: false) } @@ -258,7 +250,6 @@ class Issue < ApplicationRecord scope :with_projects_matching_search_data, -> { where('issue_search_data.project_id = issues.project_id') } before_validation :ensure_namespace_id, :ensure_work_item_type - before_save :check_issue_type_in_sync! after_save :ensure_metrics!, unless: :importing? after_commit :expire_etag_cache, unless: :importing? @@ -588,16 +579,12 @@ class Issue < ApplicationRecord user, project.external_authorization_classification_label) end - def check_for_spam?(user:) - # content created via support bots is always checked for spam, EVEN if - # the issue is not publicly visible and/or confidential - return true if user.support_bot? && spammable_attribute_changed? - - # Only check for spam on issues which are publicly visible (and thus indexed in search engines) - return false unless publicly_visible? + # Always enforce spam check for support bot but allow for other users when issue is not publicly visible + def allow_possible_spam?(user) + return true if Gitlab::CurrentSettings.allow_possible_spam + return false if user.support_bot? - # Only check for spam if certain attributes have changed - spammable_attribute_changed? + !publicly_visible? end def supports_recaptcha? @@ -753,11 +740,7 @@ class Issue < ApplicationRecord end def issue_type - if ::Feature.enabled?(:issue_type_uses_work_item_types_table) - work_item_type_with_default.base_type - else - super - end + work_item_type_with_default.base_type end def unsubscribe_email_participant(email) @@ -766,41 +749,11 @@ class Issue < ApplicationRecord issue_email_participants.find_by_email(email)&.destroy end - private - - def check_issue_type_in_sync! - # We might have existing records out of sync, so we need to skip this check unless the value is changed - # so those records can still be updated until we fix them and remove the issue_type column - # https://gitlab.com/gitlab-org/gitlab/-/work_items/403158 - return unless (changes.keys & %w[issue_type work_item_type_id]).any? - - # Do not replace the use of attributes with `issue_type` here - if attributes['issue_type'] != work_item_type.base_type - error = IssueTypeOutOfSyncError.new( - <<~ERROR - Issue `issue_type` out of sync with `work_item_type_id` column. - `issue_type` must be equal to `work_item.base_type`. - You can assign the correct work_item_type like this for example: - - Issue.new(issue_type: :incident, work_item_type: WorkItems::Type.default_by_type(:incident)) - - More details in https://gitlab.com/gitlab-org/gitlab/-/issues/338005 - ERROR - ) - - Gitlab::ErrorTracking.track_and_raise_for_dev_exception( - error, - issue_type: attributes['issue_type'], - work_item_type_id: work_item_type_id - ) - end + def hook_attrs + Gitlab::HookData::IssueBuilder.new(self).build end - def issue_type_attribute_present - return if attributes['issue_type'].present? - - errors.add(:issue_type, 'Must be present') - end + private def due_date_after_start_date return unless start_date.present? && due_date.present? @@ -834,12 +787,6 @@ class Issue < ApplicationRecord Issues::SearchData.upsert({ project_id: project_id, issue_id: id, search_vector: search_vector }, unique_by: %i(project_id issue_id)) end - def spammable_attribute_changed? - # NOTE: We need to check them for spam when issues are made non-confidential, because spam - # may have been added while they were confidential and thus not being checked for spam. - super || confidential_changed?(from: true, to: false) - end - def ensure_metrics! Issue::Metrics.record!(self) end @@ -868,9 +815,7 @@ class Issue < ApplicationRecord def ensure_work_item_type return if work_item_type_id.present? || work_item_type_id_change&.last.present? - # TODO: We should switch to DEFAULT_ISSUE_TYPE here when the issue_type column is dropped - # https://gitlab.com/gitlab-org/gitlab/-/work_items/402700 - self.work_item_type = WorkItems::Type.default_by_type(attributes['issue_type']) + self.work_item_type = WorkItems::Type.default_by_type(DEFAULT_ISSUE_TYPE) end def allowed_work_item_type_change diff --git a/app/models/member.rb b/app/models/member.rb index 0700b1a8448..f164ea244b4 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -66,6 +66,7 @@ class Member < ApplicationRecord scope :with_invited_user_state, -> do joins('LEFT JOIN users as invited_user ON invited_user.email = members.invite_email') .select('members.*', 'invited_user.state as invited_user_state') + .allow_cross_joins_across_databases(url: "https://gitlab.com/gitlab-org/gitlab/-/issues/417456") end scope :in_hierarchy, ->(source) do @@ -174,7 +175,10 @@ class Member < ApplicationRecord scope :by_access_level, -> (access_level) { active.where(access_level: access_level) } scope :all_by_access_level, -> (access_level) { where(access_level: access_level) } - scope :preload_user_and_notification_settings, -> { preload(user: :notification_settings) } + scope :preload_user_and_notification_settings, -> do + preload(user: :notification_settings) + .allow_cross_joins_across_databases(url: "https://gitlab.com/gitlab-org/gitlab/-/issues/417456") + end scope :with_source_id, ->(source_id) { where(source_id: source_id) } scope :including_source, -> { includes(:source) } @@ -288,7 +292,9 @@ class Member < ApplicationRecord class << self def search(query) - scope = joins(:user).merge(User.search(query, use_minimum_char_limit: false)) + scope = joins(:user) + .merge(User.search(query, use_minimum_char_limit: false)) + .allow_cross_joins_across_databases(url: "https://gitlab.com/gitlab-org/gitlab/-/issues/417456") return scope unless Gitlab::Pagination::Keyset::Order.keyset_aware?(scope) @@ -347,6 +353,7 @@ class Member < ApplicationRecord def left_join_users left_outer_joins(:user) + .allow_cross_joins_across_databases(url: "https://gitlab.com/gitlab-org/gitlab/-/issues/417456") end def access_for_user_ids(user_ids) diff --git a/app/models/members/group_member.rb b/app/models/members/group_member.rb index 237054587bc..ada89345a7f 100644 --- a/app/models/members/group_member.rb +++ b/app/models/members/group_member.rb @@ -20,7 +20,6 @@ class GroupMember < Member scope :of_groups, ->(groups) { where(source_id: groups&.select(:id)) } scope :of_ldap_type, -> { where(ldap: true) } scope :count_users_by_group_id, -> { group(:source_id).count } - scope :with_user, -> (user) { where(user: user) } after_create :update_two_factor_requirement, unless: :invite? after_destroy :update_two_factor_requirement, unless: :invite? diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 116108ceaf9..2773569161d 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -421,7 +421,9 @@ class MergeRequest < ApplicationRecord scope :preload_latest_diff_commit, -> { preload(latest_merge_request_diff: { merge_request_diff_commits: [:commit_author, :committer] }) } scope :preload_milestoneish_associations, -> { preload_routables.preload(:assignees, :labels) } - scope :with_web_entity_associations, -> { preload(:author, target_project: [:project_feature, group: [:route, :parent], namespace: :route]) } + scope :with_web_entity_associations, -> do + preload(:author, :labels, target_project: [:project_feature, group: [:route, :parent], namespace: :route]) + end scope :with_auto_merge_enabled, -> do with_state(:opened).where(auto_merge_enabled: true) @@ -1199,10 +1201,17 @@ class MergeRequest < ApplicationRecord end alias_method :wip_title, :draft_title - def mergeable?(skip_ci_check: false, skip_discussions_check: false) + def skipped_mergeable_checks(options = {}) + { + skip_ci_check: options.fetch(:auto_merge_requested, false) + } + end + + def mergeable?(skip_ci_check: false, skip_discussions_check: false, skip_approved_check: false) return false unless mergeable_state?( skip_ci_check: skip_ci_check, - skip_discussions_check: skip_discussions_check + skip_discussions_check: skip_discussions_check, + skip_approved_check: skip_approved_check ) check_mergeability @@ -1223,11 +1232,12 @@ class MergeRequest < ApplicationRecord ] end - def mergeable_state?(skip_ci_check: false, skip_discussions_check: false) + def mergeable_state?(skip_ci_check: false, skip_discussions_check: false, skip_approved_check: false) additional_checks = execute_merge_checks( params: { skip_ci_check: skip_ci_check, - skip_discussions_check: skip_discussions_check + skip_discussions_check: skip_discussions_check, + skip_approved_check: skip_approved_check } ) additional_checks.success? @@ -1526,6 +1536,14 @@ class MergeRequest < ApplicationRecord "refs/#{Repository::REF_MERGE_REQUEST}/#{iid}/train" end + def schedule_cleanup_refs(only: :all) + if Feature.enabled?(:merge_request_cleanup_ref_worker_async, target_project) + MergeRequests::CleanupRefWorker.perform_async(id, only.to_s) + else + cleanup_refs(only: only) + end + end + def cleanup_refs(only: :all) target_refs = [] target_refs << ref_path if %i[all head].include?(only) diff --git a/app/models/merge_request/diff_llm_summary.rb b/app/models/merge_request/diff_llm_summary.rb deleted file mode 100644 index e13fe5e1f50..00000000000 --- a/app/models/merge_request/diff_llm_summary.rb +++ /dev/null @@ -1,14 +0,0 @@ -# rubocop:disable Style/ClassAndModuleChildren -# frozen_string_literal: true - -class MergeRequest::DiffLlmSummary < ApplicationRecord - belongs_to :merge_request_diff - belongs_to :user, optional: true - - validates :merge_request_diff_id, uniqueness: true - validates :provider, presence: true - validates :content, presence: true, length: { maximum: 2056 } - - enum provider: { openai: 0 } -end -# rubocop:enable Style/ClassAndModuleChildren diff --git a/app/models/merge_request/metrics.rb b/app/models/merge_request/metrics.rb index 70216144035..a13cb353c7b 100644 --- a/app/models/merge_request/metrics.rb +++ b/app/models/merge_request/metrics.rb @@ -13,7 +13,7 @@ class MergeRequest::Metrics < ApplicationRecord before_save :ensure_target_project_id scope :merged_after, ->(date) { where(arel_table[:merged_at].gteq(date)) } - scope :merged_before, ->(date) { where(arel_table[:merged_at].lteq(date)) } + scope :merged_before, ->(date) { where(arel_table[:merged_at].lteq(date.is_a?(Time) ? date.end_of_day : date)) } scope :with_valid_time_to_merge, -> { where(arel_table[:merged_at].gt(arel_table[:created_at])) } scope :by_target_project, ->(project) { where(target_project_id: project) } diff --git a/app/models/milestone.rb b/app/models/milestone.rb index d300b938fc0..8de717fb61d 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -10,6 +10,7 @@ class Milestone < ApplicationRecord include IidRoutes include UpdatedAtFilterable include EachBatch + include Spammable prepend_mod_with('Milestone') # rubocop: disable Cop/InjectEnterpriseEditionModule @@ -62,6 +63,9 @@ class Milestone < ApplicationRecord validate :parent_type_check validate :uniqueness_of_title, if: :title_changed? + attr_spammable :title, spam_title: true + attr_spammable :description, spam_description: true + state_machine :state, initial: :active do event :close do transition active: :closed @@ -255,6 +259,10 @@ class Milestone < ApplicationRecord end end + def check_for_spam?(*) + spammable_attribute_changed? && parent.public? + end + private def timebox_format_reference(format = :iid) diff --git a/app/models/ml/experiment.rb b/app/models/ml/experiment.rb index d1277efac7b..5c5f8d3b2db 100644 --- a/app/models/ml/experiment.rb +++ b/app/models/ml/experiment.rb @@ -11,6 +11,7 @@ module Ml belongs_to :project belongs_to :user + belongs_to :model, optional: true, inverse_of: :default_experiment has_many :candidates, class_name: 'Ml::Candidate' has_many :metadata, class_name: 'Ml::ExperimentMetadata' @@ -22,10 +23,21 @@ module Ml has_internal_id :iid, scope: :project + before_destroy :stop_destroy + def package_name "#{PACKAGE_PREFIX}#{iid}" end + def stop_destroy + return unless model_id + + errors[:base] << "Cannot delete an experiment associated to a model" + # According to docs, throw is the correct way to stop on a callback + # https://api.rubyonrails.org/classes/ActiveRecord/Callbacks.html#module-ActiveRecord::Callbacks-label-Canceling+callbacks + throw :abort # rubocop:disable Cop/BanCatchThrow + end + class << self def by_project_id_and_iid(project_id, iid) find_by(project_id: project_id, iid: iid) diff --git a/app/models/ml/model.rb b/app/models/ml/model.rb new file mode 100644 index 00000000000..684b8e1983b --- /dev/null +++ b/app/models/ml/model.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module Ml + class Model < ApplicationRecord + validates :project, :default_experiment, presence: true + validates :name, + format: Gitlab::Regex.ml_model_name_regex, + uniqueness: { scope: :project }, + presence: true, + length: { maximum: 255 } + + validate :valid_default_experiment? + + has_one :default_experiment, class_name: 'Ml::Experiment' + belongs_to :project + has_many :versions, class_name: 'Ml::ModelVersion' + + def valid_default_experiment? + return unless default_experiment + + errors.add(:default_experiment) unless default_experiment.name == name + errors.add(:default_experiment) unless default_experiment.project_id == project_id + end + end +end diff --git a/app/models/ml/model_version.rb b/app/models/ml/model_version.rb new file mode 100644 index 00000000000..540fe6018a1 --- /dev/null +++ b/app/models/ml/model_version.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +module Ml + class ModelVersion < ApplicationRecord + validates :project, :model, presence: true + + validates :version, + format: Gitlab::Regex.ml_model_version_regex, + uniqueness: { scope: [:project, :model_id] }, + presence: true, + length: { maximum: 255 } + + validate :valid_model?, :valid_package? + + belongs_to :model, class_name: 'Ml::Model' + belongs_to :project + belongs_to :package, class_name: 'Packages::Package', optional: true + + delegate :name, to: :model + + private + + def valid_model? + return unless model + + errors.add(:model, 'model project must be the same') unless model.project_id == project_id + end + + def valid_package? + return unless package + + errors.add(:package, 'package must be ml_model') unless package.ml_model? + errors.add(:package, 'package name must be the same') unless package.name == name + errors.add(:package, 'package version must be the same') unless package.version == version + errors.add(:package, 'package project must be the same') unless package.project_id == project_id + end + end +end diff --git a/app/models/namespace.rb b/app/models/namespace.rb index 7b3bb04da5b..5449f006a2e 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -57,6 +57,7 @@ class Namespace < ApplicationRecord # This should _not_ be `inverse_of: :namespace`, because that would also set # `user.namespace` when this user creates a group with themselves as `owner`. belongs_to :owner, class_name: 'User' + belongs_to :organization, class_name: 'Organizations::Organization' belongs_to :parent, class_name: "Namespace" has_many :children, -> { where(type: Group.sti_name) }, class_name: "Namespace", foreign_key: :parent_id @@ -305,7 +306,7 @@ class Namespace < ApplicationRecord end def first_project_with_container_registry_tags - if ContainerRegistry::GitlabApiClient.supports_gitlab_api? + if Gitlab.com_except_jh? && ContainerRegistry::GitlabApiClient.supports_gitlab_api? ContainerRegistry::GitlabApiClient.one_project_with_container_registry_tag(full_path) else all_projects.includes(:container_repositories).find(&:has_container_registry_tags?) @@ -423,6 +424,10 @@ class Namespace < ApplicationRecord false end + def all_project_ids + all_projects.pluck(:id) + end + def all_project_ids_except(ids) all_projects.where.not(id: ids).pluck(:id) end @@ -478,7 +483,7 @@ class Namespace < ApplicationRecord def container_repositories_size strong_memoize(:container_repositories_size) do - next unless Gitlab.com? + next unless Gitlab.com_except_jh? next unless root? next unless ContainerRegistry::GitlabApiClient.supports_gitlab_api? next 0 if all_container_repositories.empty? diff --git a/app/models/namespaces/traversal/linear.rb b/app/models/namespaces/traversal/linear.rb index 9006f104c64..1ca3c8e85f3 100644 --- a/app/models/namespaces/traversal/linear.rb +++ b/app/models/namespaces/traversal/linear.rb @@ -96,27 +96,6 @@ module Namespaces traversal_ids.present? end - def use_traversal_ids_for_self_and_hierarchy? - return false unless use_traversal_ids? - return false unless Feature.enabled?(:use_traversal_ids_for_self_and_hierarchy, root_ancestor) - - traversal_ids.present? - end - - def use_traversal_ids_for_ancestors? - return false unless use_traversal_ids? - return false unless Feature.enabled?(:use_traversal_ids_for_ancestors, root_ancestor) - - traversal_ids.present? - end - - def use_traversal_ids_for_ancestors_upto? - return false unless use_traversal_ids? - return false unless Feature.enabled?(:use_traversal_ids_for_ancestors_upto, root_ancestor) - - traversal_ids.present? - end - def root_ancestor strong_memoize(:root_ancestor) do if association(:parent).loaded? && parent.present? @@ -150,13 +129,13 @@ module Namespaces end def self_and_hierarchy - return super unless use_traversal_ids_for_self_and_hierarchy? + return super unless use_traversal_ids? self_and_descendants.or(ancestors) end def ancestors(hierarchy_order: nil) - return super unless use_traversal_ids_for_ancestors? + return super unless use_traversal_ids? return self.class.none if parent_id.blank? @@ -164,7 +143,7 @@ module Namespaces end def ancestor_ids(hierarchy_order: nil) - return super unless use_traversal_ids_for_ancestors? + return super unless use_traversal_ids? hierarchy_order == :desc ? traversal_ids[0..-2] : traversal_ids[0..-2].reverse end @@ -176,7 +155,7 @@ module Namespaces # This copies the behavior of the recursive method. We will deprecate # this behavior soon. def ancestors_upto(top = nil, hierarchy_order: nil) - return super unless use_traversal_ids_for_ancestors_upto? + return super unless use_traversal_ids? # We can't use a default value in the method definition above because # we need to preserve those specific parameters for super. @@ -198,7 +177,7 @@ module Namespaces end def self_and_ancestors(hierarchy_order: nil) - return super unless use_traversal_ids_for_ancestors? + return super unless use_traversal_ids? return self.class.where(id: id) if parent_id.blank? @@ -206,7 +185,7 @@ module Namespaces end def self_and_ancestor_ids(hierarchy_order: nil) - return super unless use_traversal_ids_for_ancestors? + return super unless use_traversal_ids? hierarchy_order == :desc ? traversal_ids : traversal_ids.reverse end diff --git a/app/models/namespaces/traversal/linear_scopes.rb b/app/models/namespaces/traversal/linear_scopes.rb index c50d3dd1de6..6e79e3ac9a1 100644 --- a/app/models/namespaces/traversal/linear_scopes.rb +++ b/app/models/namespaces/traversal/linear_scopes.rb @@ -18,7 +18,7 @@ module Namespaces end def roots - return super unless use_traversal_ids_roots? + return super unless use_traversal_ids? root_ids = all.select("#{quoted_table_name}.traversal_ids[1]").distinct unscoped.where(id: root_ids) @@ -37,13 +37,13 @@ module Namespaces end def self_and_descendants(include_self: true) - return super unless use_traversal_ids_for_descendants_scopes? + 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_for_descendants_scopes? + return super unless use_traversal_ids? self_and_descendants(include_self: include_self).as_ids end @@ -78,16 +78,6 @@ module Namespaces Feature.enabled?(:use_traversal_ids) end - def use_traversal_ids_roots? - Feature.enabled?(:use_traversal_ids_roots) && - use_traversal_ids? - end - - def use_traversal_ids_for_descendants_scopes? - Feature.enabled?(:use_traversal_ids_for_descendants_scopes) && - 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? diff --git a/app/models/note.rb b/app/models/note.rb index 09ff7ad3979..2df643c46aa 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.0', remove_after: '2023-05-22' + ignore_column :id_convert_to_bigint, remove_with: '16.3', remove_after: '2023-08-22' ISSUE_TASK_SYSTEM_NOTE_PATTERN = /\A.*marked\sthe\stask.+as\s(completed|incomplete).*\z/.freeze @@ -756,7 +756,7 @@ class Note < ApplicationRecord Ability.users_that_can_read_internal_notes(users, resource_parent).pluck(:id) end - # Method necesary while we transition into the new format for task system notes + # Method necessary while we transition into the new format for task system notes # TODO: https://gitlab.com/gitlab-org/gitlab/-/issues/369923 def note return super unless system? && for_issue? && super&.match?(ISSUE_TASK_SYSTEM_NOTE_PATTERN) @@ -792,6 +792,14 @@ class Note < ApplicationRecord true end + # Use attributes.keys instead of attribute_names to filter out the fields that are skipped during export: + # + # - note_html + # - cached_markdown_version + def attribute_names_for_serialization + attributes.keys + end + private def trigger_note_subscription? diff --git a/app/models/organizations/organization.rb b/app/models/organizations/organization.rb index ce89f57a73b..8aeca2eb137 100644 --- a/app/models/organizations/organization.rb +++ b/app/models/organizations/organization.rb @@ -8,6 +8,14 @@ module Organizations before_destroy :check_if_default_organization + has_many :namespaces + has_many :groups + + has_one :settings, class_name: "OrganizationSetting" + + has_many :organization_users, inverse_of: :organization + has_many :users, through: :organization_users, inverse_of: :organizations + validates :name, presence: true, length: { maximum: 255 } diff --git a/app/models/organizations/organization_setting.rb b/app/models/organizations/organization_setting.rb new file mode 100644 index 00000000000..108531e6701 --- /dev/null +++ b/app/models/organizations/organization_setting.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module Organizations + class OrganizationSetting < ApplicationRecord + belongs_to :organization + + validates :settings, json_schema: { filename: "organization_settings" } + + jsonb_accessor :settings, + restricted_visibility_levels: [:integer, { array: true }] + + validates_each :restricted_visibility_levels do |record, attr, value| + value&.each do |level| + unless Gitlab::VisibilityLevel.options.value?(level) + record.errors.add(attr, format(_("'%{level}' is not a valid visibility level"), level: level)) + end + end + end + end +end diff --git a/app/models/organizations/organization_user.rb b/app/models/organizations/organization_user.rb new file mode 100644 index 00000000000..5aa1133b017 --- /dev/null +++ b/app/models/organizations/organization_user.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +module Organizations + class OrganizationUser < ApplicationRecord + belongs_to :organization, inverse_of: :organization_users, optional: false + belongs_to :user, inverse_of: :organization_users, optional: false + end +end diff --git a/app/models/packages/npm/metadatum.rb b/app/models/packages/npm/metadatum.rb index ccbf056ec7b..2fc1c05cd48 100644 --- a/app/models/packages/npm/metadatum.rb +++ b/app/models/packages/npm/metadatum.rb @@ -26,6 +26,11 @@ class Packages::Npm::Metadatum < ApplicationRecord def ensure_package_json_size return if package_json.to_s.size < MAX_PACKAGE_JSON_SIZE - errors.add(:package_json, _('structure is too large')) + errors.add(:package_json, :too_large, + message: format( + _('structure is too large. Maximum size is %{max_size} characters'), + max_size: MAX_PACKAGE_JSON_SIZE + ) + ) end end diff --git a/app/models/packages/package.rb b/app/models/packages/package.rb index 58305b45457..b618c7c20c4 100644 --- a/app/models/packages/package.rb +++ b/app/models/packages/package.rb @@ -83,7 +83,7 @@ class Packages::Package < ApplicationRecord validates :name, format: { with: Gitlab::Regex.conan_recipe_component_regex }, if: :conan? validates :name, format: { with: Gitlab::Regex.generic_package_name_regex }, if: :generic? validates :name, format: { with: Gitlab::Regex.helm_package_regex }, if: :helm? - validates :name, format: { with: Gitlab::Regex.npm_package_name_regex }, if: :npm? + validates :name, format: { with: Gitlab::Regex.npm_package_name_regex, message: Gitlab::Regex.npm_package_name_regex_message }, if: :npm? validates :name, format: { with: Gitlab::Regex.nuget_package_name_regex }, if: :nuget? validates :name, format: { with: Gitlab::Regex.terraform_module_package_name_regex }, if: :terraform_module? validates :name, format: { with: Gitlab::Regex.debian_package_name_regex }, if: :debian_package? @@ -94,7 +94,8 @@ class Packages::Package < ApplicationRecord validates :version, format: { with: Gitlab::Regex.pypi_version_regex }, if: :pypi? validates :version, format: { with: Gitlab::Regex.prefixed_semver_regex }, if: :golang? validates :version, format: { with: Gitlab::Regex.helm_version_regex }, if: :helm? - validates :version, format: { with: Gitlab::Regex.semver_regex }, if: -> { composer_tag_version? || npm? || terraform_module? } + validates :version, format: { with: Gitlab::Regex.semver_regex, message: Gitlab::Regex.semver_regex_message }, + if: -> { composer_tag_version? || npm? || terraform_module? } validates :version, presence: true, @@ -166,16 +167,16 @@ class Packages::Package < ApplicationRecord scope :preload_files, -> { preload(:installable_package_files) } scope :preload_nuget_files, -> { preload(:installable_nuget_package_files) } scope :preload_pipelines, -> { preload(pipelines: :user) } - scope :last_of_each_version, -> { where(id: all.last_of_each_version_ids) } - scope :last_of_each_version_ids, -> { select('MAX(id) AS id').unscope(where: :id).group(:version) } scope :limit_recent, ->(limit) { order_created_desc.limit(limit) } scope :select_distinct_name, -> { select(:name).distinct } + scope :select_only_first_by_name, -> { select('DISTINCT ON (name) *') } # Sorting scope :order_created, -> { reorder(created_at: :asc) } scope :order_created_desc, -> { reorder(created_at: :desc) } scope :order_name, -> { reorder(name: :asc) } scope :order_name_desc, -> { reorder(name: :desc) } + scope :order_name_desc_version_desc, -> { reorder(name: :desc, version: :desc) } scope :order_version, -> { reorder(version: :asc) } scope :order_version_desc, -> { reorder(version: :desc) } scope :order_type, -> { reorder(package_type: :asc) } diff --git a/app/models/pages/lookup_path.rb b/app/models/pages/lookup_path.rb index 864ea04c019..2ffb2e84cbf 100644 --- a/app/models/pages/lookup_path.rb +++ b/app/models/pages/lookup_path.rb @@ -46,7 +46,7 @@ module Pages strong_memoize_attr :source def prefix - if project.pages_namespace_url == project.pages_url + if url_builder.namespace_pages? '/' else "#{project.full_path.delete_prefix(trim_prefix)}/" @@ -55,9 +55,7 @@ module Pages strong_memoize_attr :prefix def unique_host - return unless project.project_setting.pages_unique_domain_enabled? - - project.pages_unique_host + url_builder.unique_host end strong_memoize_attr :unique_host @@ -76,5 +74,10 @@ module Pages project.pages_metadatum.pages_deployment end strong_memoize_attr :deployment + + def url_builder + Gitlab::Pages::UrlBuilder.new(project) + end + strong_memoize_attr :url_builder end end diff --git a/app/models/personal_access_token.rb b/app/models/personal_access_token.rb index 2749404b7b5..08f725de980 100644 --- a/app/models/personal_access_token.rb +++ b/app/models/personal_access_token.rb @@ -20,6 +20,7 @@ class PersonalAccessToken < ApplicationRecord serialize :scopes, Array # rubocop:disable Cop/ActiveRecordSerialize belongs_to :user + belongs_to :previous_personal_access_token, class_name: 'PersonalAccessToken' after_initialize :set_default_scopes, if: :persisted? before_save :ensure_token @@ -99,9 +100,13 @@ class PersonalAccessToken < ApplicationRecord def expires_at_before_instance_max_expiry_date return unless expires_at - if expires_at > MAX_PERSONAL_ACCESS_TOKEN_LIFETIME_IN_DAYS.days.from_now - errors.add(:expires_at, _('must expire in 365 days')) - end + max_expiry_date = Date.current.advance(days: MAX_PERSONAL_ACCESS_TOKEN_LIFETIME_IN_DAYS) + return unless expires_at > max_expiry_date + + errors.add( + :expires_at, + format(_("must be before %{expiry_date}"), expiry_date: max_expiry_date) + ) end end diff --git a/app/models/plan_limits.rb b/app/models/plan_limits.rb index 6795e7a3049..245c0719439 100644 --- a/app/models/plan_limits.rb +++ b/app/models/plan_limits.rb @@ -2,6 +2,9 @@ class PlanLimits < ApplicationRecord include IgnorableColumns + ALLOWED_LIMITS_HISTORY_ATTRIBUTES = %i[notification_limit enforcement_limit storage_size_limit + dashboard_limit_enabled_at].freeze + 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' @@ -50,32 +53,23 @@ class PlanLimits < ApplicationRecord false end - def log_limits_changes(user, new_limits) - new_limits.each do |attribute, value| + def format_limits_history(user, new_limits) + allowed_limits = new_limits.slice(*ALLOWED_LIMITS_HISTORY_ATTRIBUTES) + return {} if allowed_limits.empty? + + allowed_limits.each do |attribute, value| + next if value == self[attribute] + limits_history[attribute] ||= [] limits_history[attribute] << { - user_id: user&.id, - username: user&.username, - timestamp: Time.current.utc.to_i, - value: value + "user_id" => user.id, + "username" => user.username, + "timestamp" => Time.current.utc.to_i, + "value" => value } end - update(limits_history: limits_history) - end - - def limit_attribute_changes(attribute) - limit_history = limits_history[attribute] - return [] unless limit_history - - limit_history.map do |entry| - { - timestamp: entry[:timestamp], - value: entry[:value], - username: entry[:username], - user_id: entry[:user_id] - } - end + limits_history end end diff --git a/app/models/project.rb b/app/models/project.rb index 452a5c8973c..931f4db3a54 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -415,7 +415,7 @@ class Project < ApplicationRecord has_one :ci_cd_settings, class_name: 'ProjectCiCdSetting', inverse_of: :project, autosave: true, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :remote_mirrors, inverse_of: :project - has_many :external_pull_requests, inverse_of: :project + has_many :external_pull_requests, inverse_of: :project, class_name: 'Ci::ExternalPullRequest' has_many :sourced_pipelines, class_name: 'Ci::Sources::Pipeline', foreign_key: :source_project_id has_many :source_pipelines, class_name: 'Ci::Sources::Pipeline', foreign_key: :project_id @@ -692,6 +692,10 @@ class Project < ApplicationRecord scope :with_integration, -> (integration_class) { joins(:integrations).merge(integration_class.all) } scope :with_active_integration, -> (integration_class) { with_integration(integration_class).merge(integration_class.active) } scope :with_shared_runners_enabled, -> { where(shared_runners_enabled: true) } + # .with_slack_integration can generate poorly performing queries. It is intended only for UsagePing. + scope :with_slack_integration, -> { joins(:slack_integration) } + # .with_slack_slash_commands_integration can generate poorly performing queries. It is intended only for UsagePing. + scope :with_slack_slash_commands_integration, -> { joins(:slack_slash_commands_integration) } scope :inside_path, ->(path) do # We need routes alias rs for JOIN so it does not conflict with # includes(:route) which we use in ProjectsFinder. @@ -775,6 +779,7 @@ class Project < ApplicationRecord scope :pending_data_repair_analysis, -> do left_outer_joins(:container_registry_data_repair_detail) .where(container_registry_data_repair_details: { project_id: nil }) + .order(id: :desc) end enum auto_cancel_pending_pipelines: { disabled: 0, enabled: 1 } @@ -904,6 +909,16 @@ class Project < ApplicationRecord scope :for_group_and_its_ancestor_groups, ->(group) { where(namespace_id: group.self_and_ancestors.select(:id)) } scope :is_importing, -> { with_import_state.where(import_state: { status: %w[started scheduled] }) } + scope :without_created_and_owned_by_banned_user, -> do + where_not_exists( + Users::BannedUser.joins( + 'INNER JOIN project_authorizations ON project_authorizations.user_id = banned_users.user_id' + ).where('projects.creator_id = banned_users.user_id') + .where('project_authorizations.project_id = projects.id') + .where(project_authorizations: { access_level: Gitlab::Access::OWNER }) + ) + end + class << self # Searches for a list of projects based on the query given in `query`. # @@ -1840,10 +1855,12 @@ class Project < ApplicationRecord triggered.add_hooks(hooks) end - def execute_integrations(data, hooks_scope = :push_hooks) + def execute_integrations(data, hooks_scope = :push_hooks, skip_ci: false) # Call only service hooks that are active for this scope run_after_commit_or_now do association("#{hooks_scope}_integrations").reader.each do |integration| + next if skip_ci && integration.ci? + integration.async_execute(data) end end @@ -2201,42 +2218,6 @@ class Project < ApplicationRecord pages_metadatum&.deployed? end - def pages_url(with_unique_domain: false) - return pages_unique_url if with_unique_domain && pages_unique_domain_enabled? - - url = pages_namespace_url - url_path = full_path.partition('/').last - namespace_url = "#{Settings.pages.protocol}://#{url_path}".downcase - - if Rails.env.development? - url_without_port = URI.parse(url) - url_without_port.port = nil - - return url if url_without_port.to_s == namespace_url - end - - # If the project path is the same as host, we serve it as group page - return url if url == namespace_url - - "#{url}/#{url_path}" - end - - def pages_unique_url - pages_url_for(project_setting.pages_unique_domain) - end - - def pages_unique_host - URI(pages_unique_url).host - end - - def pages_namespace_url - pages_url_for(pages_subdomain) - end - - def pages_subdomain - full_path.partition('/').first - 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) @@ -2483,7 +2464,7 @@ class Project < ApplicationRecord break unless pages_enabled? variables.append(key: 'CI_PAGES_DOMAIN', value: Gitlab.config.pages.host) - variables.append(key: 'CI_PAGES_URL', value: pages_url) + variables.append(key: 'CI_PAGES_URL', value: Gitlab::Pages::UrlBuilder.new(self).pages_url) end end @@ -3167,6 +3148,10 @@ class Project < ApplicationRecord pending_delete? || hidden? end + def created_and_owned_by_banned_user? + 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 @@ -3236,25 +3221,8 @@ class Project < ApplicationRecord group.crm_enabled? end - def frozen_outbound_job_token_scopes? - Feature.enabled?(:frozen_outbound_job_token_scopes, self) && Feature.disabled?(:frozen_outbound_job_token_scopes_override, self) - end - strong_memoize_attr :frozen_outbound_job_token_scopes? - private - def pages_unique_domain_enabled? - Feature.enabled?(:pages_unique_domain, self) && - project_setting.pages_unique_domain_enabled? - end - - def pages_url_for(domain) - # The host in URL always needs to be downcased - Gitlab.config.pages.url.sub(%r{^https?://}) do |prefix| - "#{prefix}#{domain}." - end.downcase - end - # overridden in EE def project_group_links_with_preload project_group_links diff --git a/app/models/project_ci_cd_setting.rb b/app/models/project_ci_cd_setting.rb index aa65f27870d..cc9003423be 100644 --- a/app/models/project_ci_cd_setting.rb +++ b/app/models/project_ci_cd_setting.rb @@ -2,7 +2,6 @@ class ProjectCiCdSetting < ApplicationRecord include ChronicDurationAttribute - include IgnorableColumns belongs_to :project, inverse_of: :ci_cd_settings @@ -23,8 +22,6 @@ class ProjectCiCdSetting < ApplicationRecord chronic_duration_attr :runner_token_expiration_interval_human_readable, :runner_token_expiration_interval - ignore_column :opt_in_jwt, remove_with: '16.2', remove_after: '2023-07-01' - def keep_latest_artifacts_available? # The project level feature can only be enabled when the feature is enabled instance wide Gitlab::CurrentSettings.current_application_settings.keep_latest_artifact? && keep_latest_artifact? diff --git a/app/models/project_statistics.rb b/app/models/project_statistics.rb index 14f6a90e5ed..365bb5237c3 100644 --- a/app/models/project_statistics.rb +++ b/app/models/project_statistics.rb @@ -34,7 +34,6 @@ class ProjectStatistics < ApplicationRecord :build_artifacts_size, :packages_size, :snippets_size, - :pipeline_artifacts_size, :uploads_size ].freeze diff --git a/app/models/projects/topic.rb b/app/models/projects/topic.rb index ed1795b43e0..347d65841ed 100644 --- a/app/models/projects/topic.rb +++ b/app/models/projects/topic.rb @@ -71,7 +71,7 @@ module Projects # /\R/ - A linebreak: \n, \v, \f, \r \u0085 (NEXT LINE), # \u2028 (LINE SEPARATOR), \u2029 (PARAGRAPH SEPARATOR) or \r\n. - return unless name =~ /\R/ + return unless /\R/.match?(name) errors.add(:name, 'has characters that are not allowed') end diff --git a/app/models/projects/triggered_hooks.rb b/app/models/projects/triggered_hooks.rb index e3aa3d106b7..1f51ced5b57 100644 --- a/app/models/projects/triggered_hooks.rb +++ b/app/models/projects/triggered_hooks.rb @@ -17,6 +17,8 @@ module Projects # Assumes that the relations implement TriggerableHooks @relations.each do |hooks| hooks.hooks_for(@scope).select_active(@scope, @data).each do |hook| + next if @scope == :emoji_hooks && Feature.disabled?(:emoji_webhooks, hook.parent) + hook.async_execute(@data, @scope.to_s) end end diff --git a/app/models/protected_branch/push_access_level.rb b/app/models/protected_branch/push_access_level.rb index c86ca5723fa..53cec0c5511 100644 --- a/app/models/protected_branch/push_access_level.rb +++ b/app/models/protected_branch/push_access_level.rb @@ -3,49 +3,7 @@ class ProtectedBranch::PushAccessLevel < ApplicationRecord include Importable include ProtectedBranchAccess + include ProtectedRefDeployKeyAccess # default value for the access_level column GITLAB_DEFAULT_ACCESS_LEVEL = Gitlab::Access::MAINTAINER - - belongs_to :deploy_key - - validates :access_level, uniqueness: { scope: :protected_branch_id, if: :role?, - conditions: -> { where(user_id: nil, group_id: nil, deploy_key_id: nil) } } - validates :deploy_key_id, uniqueness: { scope: :protected_branch_id, allow_nil: true } - validate :validate_deploy_key_membership - - def type - if self.deploy_key.present? - :deploy_key - else - super - end - end - - def humanize - return "Deploy key" if deploy_key.present? - - super - end - - def check_access(user) - if user && deploy_key.present? - return user.can?(:read_project, project) && enabled_deploy_key_for_user?(deploy_key, user) - end - - super - end - - private - - def validate_deploy_key_membership - return unless deploy_key - - unless project.deploy_keys_projects.where(deploy_key: deploy_key).exists? - self.errors.add(:deploy_key, 'is not enabled for this project') - end - end - - def enabled_deploy_key_for_user?(deploy_key, user) - deploy_key.user_id == user.id && DeployKey.with_write_access_for_project(protected_branch.project, deploy_key: deploy_key).any? - end end diff --git a/app/models/protected_tag/create_access_level.rb b/app/models/protected_tag/create_access_level.rb index 5837f3a5afb..0eff9924153 100644 --- a/app/models/protected_tag/create_access_level.rb +++ b/app/models/protected_tag/create_access_level.rb @@ -3,48 +3,5 @@ class ProtectedTag::CreateAccessLevel < ApplicationRecord include Importable include ProtectedTagAccess - - belongs_to :deploy_key - - validates :access_level, uniqueness: { scope: :protected_tag_id, if: :role?, - conditions: -> { where(user_id: nil, group_id: nil, deploy_key_id: nil) } } - validates :deploy_key_id, uniqueness: { scope: :protected_tag_id, allow_nil: true } - validate :validate_deploy_key_membership - - def type - return :deploy_key if deploy_key.present? - - super - end - - def humanize - return "Deploy key" if deploy_key.present? - - super - end - - def check_access(current_user) - super do - break enabled_deploy_key_for_user?(current_user) if deploy_key? - end - end - - private - - def deploy_key? - type == :deploy_key - end - - def validate_deploy_key_membership - return unless deploy_key - return if project.deploy_keys_projects.where(deploy_key: deploy_key).exists? - - errors.add(:deploy_key, 'is not enabled for this project') - end - - def enabled_deploy_key_for_user?(current_user) - current_user.can?(:read_project, project) && - deploy_key.user_id == current_user.id && - DeployKey.with_write_access_for_project(protected_tag.project, deploy_key: deploy_key).any? - end + include ProtectedRefDeployKeyAccess end diff --git a/app/models/release.rb b/app/models/release.rb index 7f74872cf67..f0ba56390ab 100644 --- a/app/models/release.rb +++ b/app/models/release.rb @@ -64,10 +64,10 @@ class Release < ApplicationRecord end # This query uses LATERAL JOIN to find the latest release for each project. To avoid - # joining the `releases` table, we build an in-memory table using the project ids. + # joining the `projects` table, we build an in-memory table using the project ids. # Example: # SELECT ... - # FROM (VALUES (PROJECT_ID_1),(PROJECT_ID_2)) project_ids (id) + # FROM (VALUES (PROJECT_ID_1),(PROJECT_ID_2)) projects (id) # INNER JOIN LATERAL (...) def latest_for_projects(projects, order_by: 'released_at') return Release.none if projects.empty? diff --git a/app/models/remote_mirror.rb b/app/models/remote_mirror.rb index 8b2f3bdcedf..934053cb92d 100644 --- a/app/models/remote_mirror.rb +++ b/app/models/remote_mirror.rb @@ -137,6 +137,7 @@ class RemoteMirror < ApplicationRecord return false unless project.remote_mirror_available? return false unless project.repository_exists? return false if project.pending_delete? + return false if Gitlab::SilentMode.enabled? true end diff --git a/app/models/repository.rb b/app/models/repository.rb index b21df6baf0e..1321c9da780 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -838,7 +838,7 @@ class Repository files = ls_files(options[:branch_name]) options[:actions] = files.each_with_object([]) do |item, list| - next unless item =~ regex + next unless regex.match?(item) list.push( action: :move, diff --git a/app/models/service_desk_setting.rb b/app/models/service_desk_setting.rb index 4216ad7e70f..6560b25b39c 100644 --- a/app/models/service_desk_setting.rb +++ b/app/models/service_desk_setting.rb @@ -21,6 +21,7 @@ class ServiceDeskSetting < ApplicationRecord validates :project_id, presence: true validate :valid_issue_template validate :valid_project_key + validate :custom_email_enabled_state validates :outgoing_name, length: { maximum: 255 }, allow_blank: true validates :project_key, length: { maximum: 255 }, @@ -86,6 +87,14 @@ class ServiceDeskSetting < ApplicationRecord end end + def custom_email_enabled_state + return unless custom_email_enabled? + + if custom_email_verification.blank? || !custom_email_verification.finished? + errors.add(:custom_email_enabled, 'cannot be enabled until verification process has finished.') + end + end + private def source_template_project diff --git a/app/models/system_access.rb b/app/models/system_access.rb new file mode 100644 index 00000000000..9ffc63c5ca8 --- /dev/null +++ b/app/models/system_access.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module SystemAccess + def self.table_name_prefix + 'system_access_' + end +end diff --git a/app/models/todo.rb b/app/models/todo.rb index 724f97c4812..f202e1a266d 100644 --- a/app/models/todo.rb +++ b/app/models/todo.rb @@ -117,7 +117,18 @@ class Todo < ApplicationRecord # target - The value of the `target_type` column, such as `Issue`. # state - The value of the `state` column, such as `pending` or `done`. def any_for_target?(target, state = nil) - state.nil? ? exists?(target: target) : exists?(target: target, state: state) + conditions = {} + + if target.respond_to?(:todoable_target_type_name) + conditions[:target_type] = target.todoable_target_type_name + conditions[:target_id] = target.id + else + conditions[:target] = target + end + + conditions[:state] = state unless state.nil? + + exists?(conditions) end # Updates attributes of a relation of todos to the new state. diff --git a/app/models/user.rb b/app/models/user.rb index 96cdbb192bc..4a57cc2e2e2 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -60,7 +60,7 @@ class User < ApplicationRecord INCOMING_MAIL_TOKEN_PREFIX = 'glimt-' FEED_TOKEN_PREFIX = 'glft-' - columns_changing_default :notified_of_own_activity + columns_changing_default :project_view # lib/tasks/tokens.rake needs to be updated when changing mail and feed tokens add_authentication_token_field :incoming_email_token, token_generator: -> { self.generate_incoming_mail_token } @@ -170,8 +170,11 @@ class User < ApplicationRecord has_many :following_users, foreign_key: :followee_id, class_name: 'Users::UserFollowUser' has_many :followers, through: :following_users - # Groups + # Namespaces has_many :members + has_many :member_namespaces, through: :members + + # Groups has_many :group_members, -> { where(requested_at: nil).where("access_level >= ?", Gitlab::Access::GUEST) }, class_name: 'GroupMember' has_many :groups, through: :group_members has_many :groups_with_active_memberships, -> { where(members: { state: ::Member::STATE_ACTIVE }) }, through: :group_members, source: :group @@ -256,6 +259,9 @@ class User < ApplicationRecord has_many :term_agreements belongs_to :accepted_term, class_name: 'ApplicationSetting::Term' + has_many :organization_users, class_name: 'Organizations::OrganizationUser', inverse_of: :user + has_many :organizations, through: :organization_users, class_name: 'Organizations::Organization', inverse_of: :users + has_many :metrics_users_starred_dashboards, class_name: 'Metrics::UsersStarredDashboard', inverse_of: :user has_one :status, class_name: 'UserStatus' @@ -1541,7 +1547,7 @@ class User < ApplicationRecord end def full_website_url - return "http://#{website_url}" if website_url !~ %r{\Ahttps?://} + return "http://#{website_url}" unless %r{\Ahttps?://}.match?(website_url) website_url end @@ -1827,8 +1833,12 @@ class User < ApplicationRecord Project.where(id: events).not_aimed_for_deletion end + # Returns true if the user can be removed, false otherwise. + # A user can be removed if they do not own any groups where they are the sole owner + # Method `none?` is used to ensure faster retrieval, See https://gitlab.com/gitlab-org/gitlab/-/issues/417105 + def can_be_removed? - !solo_owned_groups.present? + solo_owned_groups.none? end def can_remove_self? @@ -2063,9 +2073,17 @@ class User < ApplicationRecord # override, from Devise def lock_access!(opts = {}) Gitlab::AppLogger.info("Account Locked: username=#{username}") + audit_lock_access(reason: opts.delete(:reason)) super end + # override, from Devise + def unlock_access!(unlocked_by: self) + audit_unlock_access(author: unlocked_by) + + super() + end + # Determine the maximum access level for a group of projects in bulk. # # Returns a Hash mapping project ID -> maximum access level. @@ -2103,7 +2121,7 @@ class User < ApplicationRecord end def terms_accepted? - return true if project_bot? + return true if project_bot? || service_account? || security_policy_bot? accepted_term_id.present? end @@ -2279,30 +2297,6 @@ class User < ApplicationRecord namespace_commit_emails.find_by(namespace: project.root_namespace) end - def spammer? - spam_score > Abuse::TrustScore::SPAMCHECK_HAM_THRESHOLD - end - - def spam_score - abuse_trust_scores.spamcheck.average(:score) || 0.0 - end - - def telesign_score - abuse_trust_scores.telesign.order(created_at: :desc).first&.score || 0.0 - end - - def arkose_global_score - abuse_trust_scores.arkose_global_score.order(created_at: :desc).first&.score || 0.0 - end - - def arkose_custom_score - abuse_trust_scores.arkose_custom_score.order(created_at: :desc).first&.score || 0.0 - end - - def trust_scores_for_source(source) - abuse_trust_scores.where(source: source) - end - def abuse_metadata { account_age: account_age_in_days, @@ -2310,6 +2304,10 @@ class User < ApplicationRecord } end + def allow_possible_spam? + custom_attributes.by_key(UserCustomAttribute::ALLOW_POSSIBLE_SPAM).exists? + end + def namespace_commit_email_for_namespace(namespace) return if namespace.nil? @@ -2330,7 +2328,7 @@ class User < ApplicationRecord return super if ::Gitlab::CurrentSettings.email_confirmation_setting_soft? # Following devise logic for method, we want to return `true` - # See: https://github.com/heartcombo/devise/blob/main/lib/devise/models/confirmable.rb#L191-L218 + # See: https://github.com/heartcombo/devise/blob/ec0674523e7909579a5a008f16fb9fe0c3a71712/lib/devise/models/confirmable.rb#L191-L218 true end alias_method :in_confirmation_period?, :confirmation_period_valid? @@ -2355,7 +2353,8 @@ class User < ApplicationRecord private def block_or_ban - if spammer? && account_age_in_days < 7 + user_scores = Abuse::UserTrustScore.new(self) + if user_scores.spammer? && account_age_in_days < 7 ban_and_report else block @@ -2608,6 +2607,12 @@ class User < ApplicationRecord def prefix_for_feed_token FEED_TOKEN_PREFIX end + + # method overriden in EE + def audit_lock_access(reason: nil); end + + # method overriden in EE + def audit_unlock_access(author: self); end end User.prepend_mod_with('User') diff --git a/app/models/user_custom_attribute.rb b/app/models/user_custom_attribute.rb index 63a5ee9770f..425f2cc062b 100644 --- a/app/models/user_custom_attribute.rb +++ b/app/models/user_custom_attribute.rb @@ -15,6 +15,8 @@ class UserCustomAttribute < ApplicationRecord UNBLOCKED_BY = 'unblocked_by' ARKOSE_RISK_BAND = 'arkose_risk_band' AUTO_BANNED_BY_ABUSE_REPORT_ID = 'auto_banned_by_abuse_report_id' + ALLOW_POSSIBLE_SPAM = 'allow_possible_spam' + IDENTITY_VERIFICATION_PHONE_EXEMPT = 'identity_verification_phone_exempt' class << self def upsert_custom_attributes(custom_attributes) diff --git a/app/models/user_preference.rb b/app/models/user_preference.rb index 4d517408154..c263d552d40 100644 --- a/app/models/user_preference.rb +++ b/app/models/user_preference.rb @@ -2,15 +2,12 @@ class UserPreference < ApplicationRecord include IgnorableColumns - include SafelyChangeColumnDefault # We could use enums, but Rails 4 doesn't support multiple # enum options with same name for multiple fields, also it creates # extra methods that aren't really needed here. NOTES_FILTERS = { all_notes: 0, only_comments: 1, only_activity: 2 }.freeze - columns_changing_default :tab_width, :time_display_relative, :render_whitespace_in_code - belongs_to :user scope :with_user, -> { joins(:user) } @@ -31,7 +28,6 @@ class UserPreference < ApplicationRecord validates :pinned_nav_items, json_schema: { filename: 'pinned_nav_items' } ignore_columns :experience_level, remove_with: '14.10', remove_after: '2021-03-22' - ignore_columns :time_format_in_24h, remove_with: '16.2', remove_after: '2023-07-22' # 2023-06-22 is after 16.1 release and during 16.2 release https://docs.gitlab.com/ee/development/database/avoiding_downtime_in_migrations.html#ignoring-the-column-release-m ignore_columns :use_legacy_web_ide, remove_with: '16.2', remove_after: '2023-06-22' diff --git a/app/models/users/callout.rb b/app/models/users/callout.rb index 38e518b6d3e..0d02a3b99aa 100644 --- a/app/models/users/callout.rb +++ b/app/models/users/callout.rb @@ -55,10 +55,10 @@ module Users submit_license_usage_data_banner: 52, # EE-only personal_project_limitations_banner: 53, # EE-only mr_experience_survey: 54, - namespace_storage_limit_banner_info_threshold: 55, # EE-only - namespace_storage_limit_banner_warning_threshold: 56, # EE-only - namespace_storage_limit_banner_alert_threshold: 57, # EE-only - namespace_storage_limit_banner_error_threshold: 58, # EE-only + # 55 removed in https://gitlab.com/gitlab-org/gitlab/-/merge_requests/121920 + namespace_storage_limit_alert_warning_threshold: 56, # EE-only + namespace_storage_limit_alert_alert_threshold: 57, # EE-only + namespace_storage_limit_alert_error_threshold: 58, # EE-only project_quality_summary_feedback: 59, # EE-only merge_request_settings_moved_callout: 60, new_top_level_group_alert: 61, @@ -66,13 +66,14 @@ module Users # 63 and 64 were removed with https://gitlab.com/gitlab-org/gitlab/-/merge_requests/120233 branch_rules_info_callout: 65, create_runner_workflow_banner: 66, - repository_storage_limit_banner_info_threshold: 67, # EE-only - repository_storage_limit_banner_warning_threshold: 68, # EE-only - repository_storage_limit_banner_alert_threshold: 69, # EE-only - repository_storage_limit_banner_error_threshold: 70, # EE-only + # 67 removed in https://gitlab.com/gitlab-org/gitlab/-/merge_requests/121920 + project_repository_limit_alert_warning_threshold: 68, # EE-only + project_repository_limit_alert_alert_threshold: 69, # EE-only + project_repository_limit_alert_error_threshold: 70, # EE-only new_navigation_callout: 71, code_suggestions_third_party_callout: 72, # EE-only - namespace_over_storage_users_combined_alert: 73 # EE-only + namespace_over_storage_users_combined_alert: 73, # EE-only + rich_text_editor: 74 } validates :feature_name, diff --git a/app/models/users/group_callout.rb b/app/models/users/group_callout.rb index c5946197b6f..74b653b5777 100644 --- a/app/models/users/group_callout.rb +++ b/app/models/users/group_callout.rb @@ -17,19 +17,19 @@ module Users preview_user_over_limit_free_plan_alert: 7, # EE-only user_reached_limit_free_plan_alert: 8, # EE-only free_group_limited_alert: 9, # EE-only - namespace_storage_limit_banner_info_threshold: 10, # EE-only - namespace_storage_limit_banner_warning_threshold: 11, # EE-only - namespace_storage_limit_banner_alert_threshold: 12, # EE-only - namespace_storage_limit_banner_error_threshold: 13, # EE-only + # 10 removed in https://gitlab.com/gitlab-org/gitlab/-/merge_requests/121920 + namespace_storage_limit_alert_warning_threshold: 11, # EE-only + namespace_storage_limit_alert_alert_threshold: 12, # EE-only + namespace_storage_limit_alert_error_threshold: 13, # EE-only usage_quota_trial_alert: 14, # EE-only preview_usage_quota_free_plan_alert: 15, # EE-only enforcement_at_limit_alert: 16, # EE-only web_hook_disabled: 17, # EE-only unlimited_members_during_trial_alert: 18, # EE-only - repository_storage_limit_banner_info_threshold: 19, # EE-only - repository_storage_limit_banner_warning_threshold: 20, # EE-only - repository_storage_limit_banner_alert_threshold: 21, # EE-only - repository_storage_limit_banner_error_threshold: 22, # EE-only + # 19 removed in https://gitlab.com/gitlab-org/gitlab/-/merge_requests/121920 + project_repository_limit_alert_warning_threshold: 20, # EE-only + project_repository_limit_alert_alert_threshold: 21, # EE-only + project_repository_limit_alert_error_threshold: 22, # EE-only namespace_over_storage_users_combined_alert: 23 # EE-only } diff --git a/app/models/webauthn_registration.rb b/app/models/webauthn_registration.rb index c8b2513e702..5480b9e9c4a 100644 --- a/app/models/webauthn_registration.rb +++ b/app/models/webauthn_registration.rb @@ -3,10 +3,6 @@ # Registration information for WebAuthn credentials class WebauthnRegistration < ApplicationRecord - include IgnorableColumns - - ignore_column :u2f_registration_id, remove_with: '16.2', remove_after: '2023-06-22' - belongs_to :user validates :credential_xid, :public_key, :counter, presence: true diff --git a/app/models/work_item.rb b/app/models/work_item.rb index 9f28ffbf7b6..adf424a1d94 100644 --- a/app/models/work_item.rb +++ b/app/models/work_item.rb @@ -65,6 +65,12 @@ class WorkItem < Issue 'issue' end + # Todo: remove method after target_type cleanup + # See https://gitlab.com/gitlab-org/gitlab/-/issues/416009 + def todoable_target_type_name + %w[Issue WorkItem] + end + def widgets strong_memoize(:widgets) do work_item_type.widgets.map do |widget_class| @@ -114,7 +120,9 @@ class WorkItem < Issue .filter { |param_name| common_params.key?(param_name) } .each do |param_name| widget_params[widget.api_symbol] ||= {} - widget_params[widget.api_symbol][param_name] = common_params.delete(param_name) + param_value = common_params.delete(param_name) + + widget_params[widget.api_symbol].merge!(widget.process_quick_action_param(param_name, param_value)) end end diff --git a/app/models/work_items/widgets/base.rb b/app/models/work_items/widgets/base.rb index a8b1b3f9a59..c4e87decdbf 100644 --- a/app/models/work_items/widgets/base.rb +++ b/app/models/work_items/widgets/base.rb @@ -15,6 +15,10 @@ module WorkItems [] end + def self.process_quick_action_param(param_name, value) + { param_name => value } + end + def self.callback_class WorkItems::Callbacks.const_get(name.demodulize, false) rescue NameError diff --git a/app/models/work_items/widgets/current_user_todos.rb b/app/models/work_items/widgets/current_user_todos.rb index 61c4fcb453b..64297b433dd 100644 --- a/app/models/work_items/widgets/current_user_todos.rb +++ b/app/models/work_items/widgets/current_user_todos.rb @@ -3,6 +3,19 @@ module WorkItems module Widgets class CurrentUserTodos < Base + def self.quick_action_commands + [:todo, :done] + end + + def self.quick_action_params + [:todo_event] + end + + def self.process_quick_action_param(param_name, value) + return super unless param_name == :todo_event + + { action: value == 'done' ? 'mark_as_done' : 'add' } + end end end end |