Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'app/models')
-rw-r--r--app/models/ability.rb7
-rw-r--r--app/models/application_setting.rb16
-rw-r--r--app/models/application_setting_implementation.rb5
-rw-r--r--app/models/authentication_event.rb5
-rw-r--r--app/models/awareness_session.rb236
-rw-r--r--app/models/ci/build.rb47
-rw-r--r--app/models/ci/build_report_result.rb4
-rw-r--r--app/models/ci/group.rb3
-rw-r--r--app/models/ci/group_variable.rb4
-rw-r--r--app/models/ci/job_artifact.rb2
-rw-r--r--app/models/ci/legacy_stage.rb73
-rw-r--r--app/models/ci/pending_build.rb14
-rw-r--r--app/models/ci/pipeline.rb85
-rw-r--r--app/models/ci/pipeline_artifact.rb17
-rw-r--r--app/models/ci/runner.rb20
-rw-r--r--app/models/ci/runner_version.rb34
-rw-r--r--app/models/ci/stage.rb2
-rw-r--r--app/models/ci/trigger.rb3
-rw-r--r--app/models/ci/variable.rb4
-rw-r--r--app/models/clusters/agent.rb2
-rw-r--r--app/models/clusters/applications/elastic_stack.rb113
-rw-r--r--app/models/clusters/cluster.rb21
-rw-r--r--app/models/clusters/concerns/elasticsearch_client.rb38
-rw-r--r--app/models/clusters/integrations/elastic_stack.rb40
-rw-r--r--app/models/clusters/integrations/prometheus.rb18
-rw-r--r--app/models/commit_status.rb9
-rw-r--r--app/models/concerns/awareness.rb41
-rw-r--r--app/models/concerns/cache_markdown_field.rb9
-rw-r--r--app/models/concerns/ci/artifactable.rb2
-rw-r--r--app/models/concerns/ci/bulk_insertable_tags.rb24
-rw-r--r--app/models/concerns/ci/has_status.rb6
-rw-r--r--app/models/concerns/each_batch.rb61
-rw-r--r--app/models/concerns/enums/ci/commit_status.rb5
-rw-r--r--app/models/concerns/integrations/has_issue_tracker_fields.rb18
-rw-r--r--app/models/concerns/integrations/slack_mattermost_notifier.rb2
-rw-r--r--app/models/concerns/loose_index_scan.rb67
-rw-r--r--app/models/concerns/milestoneable.rb2
-rw-r--r--app/models/concerns/notification_branch_selection.rb16
-rw-r--r--app/models/concerns/packages/fips.rb11
-rw-r--r--app/models/concerns/participable.rb23
-rw-r--r--app/models/concerns/require_email_verification.rb52
-rw-r--r--app/models/concerns/vulnerability_finding_helpers.rb37
-rw-r--r--app/models/container_registry/event.rb11
-rw-r--r--app/models/container_repository.rb2
-rw-r--r--app/models/customer_relations/contact.rb17
-rw-r--r--app/models/deploy_token.rb3
-rw-r--r--app/models/deployment.rb22
-rw-r--r--app/models/environment.rb8
-rw-r--r--app/models/error_tracking/client_key.rb2
-rw-r--r--app/models/group.rb20
-rw-r--r--app/models/hooks/project_hook.rb15
-rw-r--r--app/models/hooks/system_hook.rb2
-rw-r--r--app/models/hooks/web_hook.rb39
-rw-r--r--app/models/incident_management/issuable_escalation_status.rb2
-rw-r--r--app/models/integration.rb35
-rw-r--r--app/models/integrations/asana.rb37
-rw-r--r--app/models/integrations/assembla.rb29
-rw-r--r--app/models/integrations/bamboo.rb1
-rw-r--r--app/models/integrations/base_chat_notification.rb46
-rw-r--r--app/models/integrations/base_issue_tracker.rb2
-rw-r--r--app/models/integrations/campfire.rb63
-rw-r--r--app/models/integrations/confluence.rb19
-rw-r--r--app/models/integrations/datadog.rb157
-rw-r--r--app/models/integrations/discord.rb6
-rw-r--r--app/models/integrations/drone_ci.rb3
-rw-r--r--app/models/integrations/emails_on_push.rb51
-rw-r--r--app/models/integrations/external_wiki.rb22
-rw-r--r--app/models/integrations/field.rb27
-rw-r--r--app/models/integrations/flowdock.rb23
-rw-r--r--app/models/integrations/hangouts_chat.rb5
-rw-r--r--app/models/integrations/harbor.rb2
-rw-r--r--app/models/integrations/irker.rb93
-rw-r--r--app/models/integrations/jira.rb3
-rw-r--r--app/models/integrations/mattermost.rb6
-rw-r--r--app/models/integrations/microsoft_teams.rb5
-rw-r--r--app/models/integrations/mock_ci.rb2
-rw-r--r--app/models/integrations/packagist.rb51
-rw-r--r--app/models/integrations/pipelines_email.rb34
-rw-r--r--app/models/integrations/pivotaltracker.rb35
-rw-r--r--app/models/integrations/prometheus.rb66
-rw-r--r--app/models/integrations/pushover.rb141
-rw-r--r--app/models/integrations/shimo.rb18
-rw-r--r--app/models/integrations/slack.rb5
-rw-r--r--app/models/integrations/teamcity.rb5
-rw-r--r--app/models/integrations/unify_circuit.rb8
-rw-r--r--app/models/integrations/webex_teams.rb7
-rw-r--r--app/models/integrations/youtrack.rb5
-rw-r--r--app/models/integrations/zentao.rb56
-rw-r--r--app/models/issue.rb72
-rw-r--r--app/models/key.rb14
-rw-r--r--app/models/member.rb18
-rw-r--r--app/models/members/project_member.rb12
-rw-r--r--app/models/merge_request.rb5
-rw-r--r--app/models/merge_request_diff.rb51
-rw-r--r--app/models/merge_request_diff_file.rb43
-rw-r--r--app/models/namespace.rb15
-rw-r--r--app/models/namespace_setting.rb23
-rw-r--r--app/models/note.rb27
-rw-r--r--app/models/notification_recipient.rb4
-rw-r--r--app/models/oauth_access_token.rb2
-rw-r--r--app/models/operations/feature_flags_client.rb24
-rw-r--r--app/models/packages/cleanup/policy.rb15
-rw-r--r--app/models/packages/debian/file_entry.rb3
-rw-r--r--app/models/pages/virtual_domain.rb9
-rw-r--r--app/models/pages_domain.rb10
-rw-r--r--app/models/preloaders/users_max_access_level_in_projects_preloader.rb8
-rw-r--r--app/models/project.rb154
-rw-r--r--app/models/project_export_job.rb1
-rw-r--r--app/models/project_feature.rb5
-rw-r--r--app/models/project_import_state.rb9
-rw-r--r--app/models/project_setting.rb2
-rw-r--r--app/models/project_team.rb18
-rw-r--r--app/models/project_tracing_setting.rb15
-rw-r--r--app/models/projects/import_export/relation_export.rb22
-rw-r--r--app/models/projects/import_export/relation_export_upload.rb19
-rw-r--r--app/models/protected_branch.rb4
-rw-r--r--app/models/remote_mirror.rb5
-rw-r--r--app/models/repository.rb4
-rw-r--r--app/models/snippet.rb1
-rw-r--r--app/models/ssh_host_key.rb10
-rw-r--r--app/models/terraform/state.rb1
-rw-r--r--app/models/todo.rb4
-rw-r--r--app/models/user.rb118
-rw-r--r--app/models/users/callout.rb7
-rw-r--r--app/models/users/group_callout.rb3
-rw-r--r--app/models/users/in_product_marketing_email.rb2
-rw-r--r--app/models/users/namespace_callout.rb33
-rw-r--r--app/models/wiki_page.rb3
-rw-r--r--app/models/work_item.rb9
-rw-r--r--app/models/work_items/parent_link.rb19
-rw-r--r--app/models/work_items/type.rb6
-rw-r--r--app/models/work_items/widgets/assignees.rb10
-rw-r--r--app/models/work_items/widgets/description.rb4
-rw-r--r--app/models/work_items/widgets/hierarchy.rb4
-rw-r--r--app/models/work_items/widgets/weight.rb9
-rw-r--r--app/models/x509_certificate.rb2
-rw-r--r--app/models/x509_issuer.rb2
137 files changed, 2082 insertions, 1187 deletions
diff --git a/app/models/ability.rb b/app/models/ability.rb
index a185448d5ea..b15143c8c9c 100644
--- a/app/models/ability.rb
+++ b/app/models/ability.rb
@@ -26,6 +26,13 @@ class Ability
end
end
+ # A list of users that can read confidential notes in a project
+ def users_that_can_read_internal_notes(users, note_parent)
+ DeclarativePolicy.subject_scope do
+ users.select { |u| allowed?(u, :reporter_access, note_parent) }
+ end
+ end
+
# Returns an Array of Issues that can be read by the given user.
#
# issues - The issues to reduce down to those readable by the user.
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index 6acdc02c799..17b46f929c3 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -28,6 +28,7 @@ class ApplicationSetting < ApplicationRecord
add_authentication_token_field :runners_registration_token, encrypted: -> { Feature.enabled?(:application_settings_tokens_optional_encryption) ? :optional : :required }
add_authentication_token_field :health_check_access_token
add_authentication_token_field :static_objects_external_storage_auth_token, encrypted: :required
+ add_authentication_token_field :error_tracking_access_token, encrypted: :required
belongs_to :self_monitoring_project, class_name: "Project", foreign_key: 'instance_administration_project_id'
belongs_to :push_rule
@@ -171,6 +172,11 @@ class ApplicationSetting < ApplicationRecord
validates :kroki_formats, json_schema: { filename: 'application_setting_kroki_formats' }
+ validates :metrics_method_call_threshold,
+ numericality: { greater_than_or_equal_to: 0 },
+ presence: true,
+ if: :prometheus_metrics_enabled
+
validates :plantuml_url,
presence: true,
if: :plantuml_enabled
@@ -393,6 +399,7 @@ class ApplicationSetting < ApplicationRecord
numericality: { only_integer: true, greater_than_or_equal_to: 0 }
validates :packages_cleanup_package_file_worker_capacity,
+ :package_registry_cleanup_policies_worker_capacity,
allow_nil: false,
numericality: { only_integer: true, greater_than_or_equal_to: 0 }
@@ -589,6 +596,14 @@ class ApplicationSetting < ApplicationRecord
presence: true, length: { maximum: 255 },
if: :sentry_enabled?
+ validates :error_tracking_enabled,
+ inclusion: { in: [true, false], message: _('must be a boolean value') }
+ validates :error_tracking_api_url,
+ presence: true,
+ addressable_url: true,
+ length: { maximum: 255 },
+ if: :error_tracking_enabled?
+
validates :users_get_by_id_limit,
numericality: { only_integer: true, greater_than_or_equal_to: 0 }
validates :users_get_by_id_limit_allowlist,
@@ -653,6 +668,7 @@ class ApplicationSetting < ApplicationRecord
before_save :ensure_runners_registration_token
before_save :ensure_health_check_access_token
+ before_save :ensure_error_tracking_access_token
after_commit do
reset_memoized_terms
diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb
index a89ea05fb62..e9a0a156121 100644
--- a/app/models/application_setting_implementation.rb
+++ b/app/models/application_setting_implementation.rb
@@ -217,6 +217,7 @@ module ApplicationSettingImplementation
user_show_add_ssh_key_message: true,
valid_runner_registrars: VALID_RUNNER_REGISTRAR_TYPES,
wiki_page_max_content_bytes: 50.megabytes,
+ package_registry_cleanup_policies_worker_capacity: 2,
container_registry_delete_tags_service_timeout: 250,
container_registry_expiration_policies_worker_capacity: 4,
container_registry_cleanup_tags_service_max_list_size: 200,
@@ -445,6 +446,10 @@ module ApplicationSettingImplementation
ensure_health_check_access_token!
end
+ def error_tracking_access_token
+ ensure_error_tracking_access_token!
+ end
+
def usage_ping_can_be_configured?
Settings.gitlab.usage_ping_enabled
end
diff --git a/app/models/authentication_event.rb b/app/models/authentication_event.rb
index 1e822629ba1..0ed197f32df 100644
--- a/app/models/authentication_event.rb
+++ b/app/models/authentication_event.rb
@@ -25,4 +25,9 @@ class AuthenticationEvent < ApplicationRecord
def self.providers
STATIC_PROVIDERS | Devise.omniauth_providers.map(&:to_s)
end
+
+ def self.initial_login_or_known_ip_address?(user, ip_address)
+ !where(user_id: user).exists? ||
+ where(user_id: user, ip_address: ip_address).success.exists?
+ end
end
diff --git a/app/models/awareness_session.rb b/app/models/awareness_session.rb
new file mode 100644
index 00000000000..a84a3454a27
--- /dev/null
+++ b/app/models/awareness_session.rb
@@ -0,0 +1,236 @@
+# frozen_string_literal: true
+
+# A Redis backed session store for real-time collaboration. A session is defined
+# by its documents and the users that join this session. An online user can have
+# two states within the session: "active" and "away".
+#
+# By design, session must eventually be cleaned up. If this doesn't happen
+# explicitly, all keys used within the session model must have an expiry
+# timestamp set.
+class AwarenessSession # rubocop:disable Gitlab/NamespacedClass
+ # An awareness session expires automatically after 1 hour of no activity
+ SESSION_LIFETIME = 1.hour
+ private_constant :SESSION_LIFETIME
+
+ # Expire user awareness keys after some time of inactivity
+ USER_LIFETIME = 1.hour
+ private_constant :USER_LIFETIME
+
+ PRESENCE_LIFETIME = 10.minutes
+ private_constant :PRESENCE_LIFETIME
+
+ KEY_NAMESPACE = "gitlab:awareness"
+ private_constant :KEY_NAMESPACE
+
+ class << self
+ def for(value = nil)
+ # Creates a unique value for situations where we have no unique value to
+ # create a session with. This could be when creating a new issue, a new
+ # merge request, etc.
+ value = SecureRandom.uuid unless value.present?
+
+ # We use SHA-256 based session identifiers (similar to abbreviated git
+ # hashes). There is always a chance for Hash collisions (birthday
+ # problem), we therefore have to pick a good tradeoff between the amount
+ # of data stored and the probability of a collision.
+ #
+ # The approximate probability for a collision can be calculated:
+ #
+ # p ~= n^2 / 2m
+ # ~= (2^18)^2 / (2 * 16^15)
+ # ~= 2^36 / 2^61
+ #
+ # n is the number of awareness sessions and m the number of possibilities
+ # for each item. For a hex number, this is 16^c, where c is the number of
+ # characters. With 260k (~2^18) sessions, the probability for a collision
+ # is ~2^-25.
+ #
+ # The number of 15 is selected carefully. The integer representation fits
+ # nicely into a signed 64 bit integer and eventually allows Redis to
+ # optimize its memory usage. 16 chars would exceed the space for
+ # this datatype.
+ id = Digest::SHA256.hexdigest(value.to_s)[0, 15]
+
+ AwarenessSession.new(id)
+ end
+ end
+
+ def initialize(id)
+ @id = id
+ end
+
+ def join(user)
+ user_key = user_sessions_key(user.id)
+
+ with_redis do |redis|
+ redis.pipelined do |pipeline|
+ pipeline.sadd(user_key, id_i)
+ pipeline.expire(user_key, USER_LIFETIME.to_i)
+
+ pipeline.zadd(users_key, timestamp.to_f, user.id)
+
+ # We also mark for expiry when a session key is created (first user joins),
+ # because some users might never actively leave a session and the key could
+ # therefore become stale, w/o us noticing.
+ reset_session_expiry(pipeline)
+ end
+ end
+
+ nil
+ end
+
+ def leave(user)
+ user_key = user_sessions_key(user.id)
+
+ with_redis do |redis|
+ redis.pipelined do |pipeline|
+ pipeline.srem(user_key, id_i)
+ pipeline.zrem(users_key, user.id)
+ end
+
+ # cleanup orphan sessions and users
+ #
+ # this needs to be a second pipeline due to the delete operations being
+ # dependent on the result of the cardinality checks
+ user_sessions_count, session_users_count = redis.pipelined do |pipeline|
+ pipeline.scard(user_key)
+ pipeline.zcard(users_key)
+ end
+
+ redis.pipelined do |pipeline|
+ pipeline.del(user_key) unless user_sessions_count > 0
+
+ unless session_users_count > 0
+ pipeline.del(users_key)
+ @id = nil
+ end
+ end
+ end
+
+ nil
+ end
+
+ def present?(user, threshold: PRESENCE_LIFETIME)
+ with_redis do |redis|
+ user_timestamp = redis.zscore(users_key, user.id)
+ break false unless user_timestamp.present?
+
+ timestamp - user_timestamp < threshold
+ end
+ end
+
+ def away?(user, threshold: PRESENCE_LIFETIME)
+ !present?(user, threshold: threshold)
+ end
+
+ # Updates the last_activity timestamp for a user in this session
+ def touch!(user)
+ with_redis do |redis|
+ redis.pipelined do |pipeline|
+ pipeline.zadd(users_key, timestamp.to_f, user.id)
+
+ # extend the session lifetime due to user activity
+ reset_session_expiry(pipeline)
+ end
+ end
+
+ nil
+ end
+
+ def size
+ with_redis do |redis|
+ redis.zcard(users_key)
+ end
+ end
+
+ def to_param
+ id&.to_s
+ end
+
+ def to_s
+ "awareness_session=#{id}"
+ end
+
+ def online_users_with_last_activity(threshold: PRESENCE_LIFETIME)
+ users_with_last_activity.filter do |_user, last_activity|
+ user_online?(last_activity, threshold: threshold)
+ end
+ end
+
+ def users
+ User.where(id: user_ids)
+ end
+
+ def users_with_last_activity
+ # where in (x, y, [...z]) is a set and does not maintain any order, we need
+ # to make sure to establish a stable order for both, the pairs returned from
+ # redis and the ActiveRecord query. Using IDs in ascending order.
+ user_ids, last_activities = user_ids_with_last_activity
+ .sort_by(&:first)
+ .transpose
+
+ return [] if user_ids.blank?
+
+ users = User.where(id: user_ids).order(id: :asc)
+ users.zip(last_activities)
+ end
+
+ private
+
+ attr_reader :id
+
+ def user_online?(last_activity, threshold:)
+ last_activity.to_i + threshold.to_i > Time.zone.now.to_i
+ end
+
+ # converts session id from hex to integer representation
+ def id_i
+ Integer(id, 16) if id.present?
+ end
+
+ def users_key
+ "#{KEY_NAMESPACE}:session:#{id}:users"
+ end
+
+ def user_sessions_key(user_id)
+ "#{KEY_NAMESPACE}:user:#{user_id}:sessions"
+ end
+
+ def with_redis
+ Gitlab::Redis::SharedState.with do |redis|
+ yield redis if block_given?
+ end
+ end
+
+ def timestamp
+ Time.now.to_i
+ end
+
+ def user_ids
+ with_redis do |redis|
+ redis.zrange(users_key, 0, -1)
+ end
+ end
+
+ # Returns an array of tuples, where the first element in the tuple represents
+ # the user ID and the second part the last_activity timestamp.
+ def user_ids_with_last_activity
+ pairs = with_redis do |redis|
+ redis.zrange(users_key, 0, -1, with_scores: true)
+ end
+
+ # map data type of score (float) to Time
+ pairs.map do |user_id, score|
+ [user_id, Time.zone.at(score.to_i)]
+ end
+ end
+
+ # We want sessions to cleanup automatically after a certain period of
+ # inactivity. This sets the expiry timestamp for this session to
+ # [SESSION_LIFETIME].
+ def reset_session_expiry(redis)
+ redis.expire(users_key, SESSION_LIFETIME)
+
+ nil
+ end
+end
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index e35198ba31f..7f9697d0424 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -2,6 +2,7 @@
module Ci
class Build < Ci::Processable
+ prepend Ci::BulkInsertableTags
include Ci::Metadatable
include Ci::Contextable
include TokenAuthenticatable
@@ -14,8 +15,6 @@ module Ci
extend ::Gitlab::Utils::Override
- BuildArchivedError = Class.new(StandardError)
-
belongs_to :project, inverse_of: :builds
belongs_to :runner
belongs_to :trigger_request
@@ -30,10 +29,6 @@ module Ci
return_exit_code: -> (build) { build.exit_codes_defined? }
}.freeze
- DEFAULT_RETRIES = {
- scheduler_failure: 2
- }.freeze
-
DEGRADATION_THRESHOLD_VARIABLE_NAME = 'DEGRADATION_THRESHOLD'
RUNNERS_STATUS_CACHE_EXPIRATION = 1.minute
@@ -172,7 +167,6 @@ module Ci
end
scope :with_artifacts_not_expired, -> { with_downloadable_artifacts.where('artifacts_expire_at IS NULL OR artifacts_expire_at > ?', Time.current) }
- scope :with_expired_artifacts, -> { with_downloadable_artifacts.where('artifacts_expire_at < ?', Time.current) }
scope :with_pipeline_locked_artifacts, -> { joins(:pipeline).where('pipeline.locked': Ci::Pipeline.lockeds[:artifacts_locked]) }
scope :last_month, -> { where('created_at > ?', Date.today - 1.month) }
scope :manual_actions, -> { where(when: :manual, status: COMPLETED_STATUSES + %i[manual]) }
@@ -187,13 +181,6 @@ module Ci
joins(:metadata).where("ci_builds_metadata.config_options -> 'artifacts' -> 'reports' ?| array[:job_types]", job_types: job_types)
end
- scope :queued_before, ->(time) { where(arel_table[:queued_at].lt(time)) }
-
- scope :preload_project_and_pipeline_project, -> do
- preload(Ci::Pipeline::PROJECT_ROUTE_AND_NAMESPACE_ROUTE,
- pipeline: Ci::Pipeline::PROJECT_ROUTE_AND_NAMESPACE_ROUTE)
- end
-
scope :with_coverage, -> { where.not(coverage: nil) }
scope :without_coverage, -> { where(coverage: nil) }
scope :with_coverage_regex, -> { where.not(coverage_regex: nil) }
@@ -207,7 +194,7 @@ module Ci
after_save :stick_build_if_status_changed
after_create unless: :importing? do |build|
- run_after_commit { BuildHooksWorker.perform_async(build.id) }
+ run_after_commit { BuildHooksWorker.perform_async(build) }
end
class << self
@@ -217,10 +204,6 @@ module Ci
ActiveModel::Name.new(self, nil, 'job')
end
- def first_pending
- pending.unstarted.order('created_at ASC').first
- end
-
def with_preloads
preload(:job_artifacts_archive, :job_artifacts, :tags, project: [:namespace])
end
@@ -302,7 +285,7 @@ module Ci
build.run_after_commit do
BuildQueueWorker.perform_async(id)
- BuildHooksWorker.perform_async(id)
+ BuildHooksWorker.perform_async(build)
end
end
@@ -330,7 +313,7 @@ module Ci
build.run_after_commit do
build.ensure_persistent_ref
- BuildHooksWorker.perform_async(id)
+ BuildHooksWorker.perform_async(build)
end
end
@@ -338,11 +321,7 @@ module Ci
build.run_after_commit do
build.run_status_commit_hooks!
- if Feature.enabled?(:ci_build_finished_worker_namespace_changed, build.project)
- Ci::BuildFinishedWorker.perform_async(id)
- else
- ::BuildFinishedWorker.perform_async(id)
- end
+ Ci::BuildFinishedWorker.perform_async(id)
end
end
@@ -446,10 +425,6 @@ module Ci
true
end
- def save_tags
- super unless Thread.current['ci_bulk_insert_tags']
- end
-
def archived?
return true if degenerated?
@@ -556,10 +531,6 @@ module Ci
self.options.dig(:environment, :deployment_tier) if self.options
end
- def outdated_deployment?
- success? && !deployment.try(:last?)
- end
-
def triggered_by?(current_user)
user == current_user
end
@@ -1162,6 +1133,14 @@ module Ci
Gitlab::Utils::UsageData.track_usage_event('ci_users_executing_deployment_job', user_id) if user_id.present? && count_user_deployment?
end
+ def track_verify_usage
+ Gitlab::Utils::UsageData.track_usage_event('ci_users_executing_verify_environment_job', user_id) if user_id.present? && count_user_verification?
+ end
+
+ def count_user_verification?
+ has_environment? && environment_action == 'verify'
+ end
+
def each_report(report_types)
job_artifacts_for_types(report_types).each do |report_artifact|
report_artifact.each_blob do |blob|
diff --git a/app/models/ci/build_report_result.rb b/app/models/ci/build_report_result.rb
index 2c08fc4c8bf..b674c1b1a0e 100644
--- a/app/models/ci/build_report_result.rb
+++ b/app/models/ci/build_report_result.rb
@@ -39,9 +39,5 @@ module Ci
def suite_error
tests.dig("suite_error")
end
-
- def tests_total
- [tests_success, tests_failed, tests_errored, tests_skipped].sum
- end
end
end
diff --git a/app/models/ci/group.rb b/app/models/ci/group.rb
index e5cb2026503..0105366d99b 100644
--- a/app/models/ci/group.rb
+++ b/app/models/ci/group.rb
@@ -50,8 +50,7 @@ module Ci
def status_struct
strong_memoize(:status_struct) do
- Gitlab::Ci::Status::Composite
- .new(@jobs, project: project)
+ Gitlab::Ci::Status::Composite.new(@jobs)
end
end
diff --git a/app/models/ci/group_variable.rb b/app/models/ci/group_variable.rb
index 0af5533613f..e11edbda6dc 100644
--- a/app/models/ci/group_variable.rb
+++ b/app/models/ci/group_variable.rb
@@ -19,5 +19,9 @@ module Ci
scope :unprotected, -> { where(protected: false) }
scope :by_environment_scope, -> (environment_scope) { where(environment_scope: environment_scope) }
scope :for_groups, ->(group_ids) { where(group_id: group_ids) }
+
+ def audit_details
+ key
+ end
end
end
diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb
index 81943cfa651..ee7175a4f69 100644
--- a/app/models/ci/job_artifact.rb
+++ b/app/models/ci/job_artifact.rb
@@ -322,7 +322,7 @@ module Ci
def expire_in=(value)
self.expire_at =
if value
- ::Gitlab::Ci::Build::Artifacts::ExpireInParser.new(value).seconds_from_now
+ ::Gitlab::Ci::Build::DurationParser.new(value).seconds_from_now
end
end
diff --git a/app/models/ci/legacy_stage.rb b/app/models/ci/legacy_stage.rb
deleted file mode 100644
index ffd3d3fcd88..00000000000
--- a/app/models/ci/legacy_stage.rb
+++ /dev/null
@@ -1,73 +0,0 @@
-# frozen_string_literal: true
-
-module Ci
- # Currently this is artificial object, constructed dynamically
- # We should migrate this object to actual database record in the future
- class LegacyStage
- include StaticModel
- include Presentable
-
- attr_reader :pipeline, :name
-
- delegate :project, to: :pipeline
-
- def initialize(pipeline, name:, status: nil, warnings: nil)
- @pipeline = pipeline
- @name = name
- @status = status
- # support ints and booleans
- @has_warnings = ActiveRecord::Type::Boolean.new.cast(warnings)
- end
-
- def groups
- @groups ||= Ci::Group.fabricate(project, self)
- end
-
- def to_param
- name
- end
-
- def statuses_count
- @statuses_count ||= statuses.count
- end
-
- def status
- @status ||= statuses.latest.composite_status(project: project)
- end
-
- def detailed_status(current_user)
- Gitlab::Ci::Status::Stage::Factory
- .new(self, current_user)
- .fabricate!
- end
-
- def latest_statuses
- statuses.ordered.latest
- end
-
- def statuses
- @statuses ||= pipeline.statuses.where(stage: name)
- end
-
- def builds
- @builds ||= pipeline.builds.where(stage: name)
- end
-
- def success?
- status.to_s == 'success'
- end
-
- def has_warnings?
- # lazilly calculate the warnings
- if @has_warnings.nil?
- @has_warnings = statuses.latest.failed_but_allowed.any?
- end
-
- @has_warnings
- end
-
- def manual_playable?
- %[manual scheduled skipped].include?(status.to_s)
- end
- end
-end
diff --git a/app/models/ci/pending_build.rb b/app/models/ci/pending_build.rb
index d900a056242..0fa6a234a3d 100644
--- a/app/models/ci/pending_build.rb
+++ b/app/models/ci/pending_build.rb
@@ -30,10 +30,6 @@ module Ci
self.upsert(entry.attributes.compact, returning: %w[build_id], unique_by: :build_id)
end
- def maintain_denormalized_data?
- ::Feature.enabled?(:ci_pending_builds_maintain_denormalized_data)
- end
-
private
def args_from_build(build)
@@ -43,13 +39,13 @@ module Ci
build: build,
project: project,
protected: build.protected?,
- namespace: project.namespace
+ namespace: project.namespace,
+ tag_ids: build.tags_ids,
+ instance_runners_enabled: shared_runners_enabled?(project)
}
- if maintain_denormalized_data?
- args.store(:tag_ids, build.tags_ids)
- args.store(:instance_runners_enabled, shared_runners_enabled?(project))
- args.store(:namespace_traversal_ids, project.namespace.traversal_ids) if group_runners_enabled?(project)
+ if group_runners_enabled?(project)
+ args.store(:namespace_traversal_ids, project.namespace.traversal_ids)
end
args
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 5d316906bd3..78b55680b5e 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -27,8 +27,6 @@ module Ci
DEFAULT_CONFIG_PATH = CONFIG_EXTENSION
CANCELABLE_STATUSES = (Ci::HasStatus::CANCELABLE_STATUSES + ['manual']).freeze
- BridgeStatusError = Class.new(StandardError)
-
paginates_per 15
sha_attribute :source_sha
@@ -133,6 +131,7 @@ module Ci
validates :source, exclusion: { in: %w(unknown), unless: :importing? }, on: :create
after_create :keep_around_commits, unless: :importing?
+ after_find :observe_age_in_minutes, unless: :importing?
use_fast_destroy :job_artifacts
use_fast_destroy :build_trace_chunks
@@ -241,6 +240,13 @@ module Ci
pipeline.run_after_commit do
unless pipeline.user&.blocked?
+ Gitlab::AppLogger.info(
+ message: "Enqueuing hooks for Pipeline #{pipeline.id}: #{pipeline.status}",
+ class: self.class.name,
+ pipeline_id: pipeline.id,
+ project_id: pipeline.project_id,
+ pipeline_status: pipeline.status)
+
PipelineHooksWorker.perform_async(pipeline.id)
end
@@ -332,8 +338,8 @@ module Ci
scope :for_id, -> (id) { where(id: id) }
scope :for_iid, -> (iid) { where(iid: iid) }
scope :for_project, -> (project_id) { where(project_id: project_id) }
- scope :created_after, -> (time) { where('ci_pipelines.created_at > ?', time) }
- scope :created_before_id, -> (id) { where('ci_pipelines.id < ?', id) }
+ scope :created_after, -> (time) { where(arel_table[:created_at].gt(time)) }
+ scope :created_before_id, -> (id) { where(arel_table[:id].lt(id)) }
scope :before_pipeline, -> (pipeline) { created_before_id(pipeline.id).outside_pipeline_family(pipeline) }
scope :with_pipeline_source, -> (source) { where(source: source) }
@@ -490,40 +496,16 @@ module Ci
.pluck(:stage, :stage_idx).map(&:first)
end
- def legacy_stage(name)
- stage = Ci::LegacyStage.new(self, name: name)
- stage unless stage.statuses_count == 0
- end
-
def ref_exists?
project.repository.ref_exists?(git_ref)
rescue Gitlab::Git::Repository::NoRepository
false
end
- def legacy_stages_using_composite_status
- stages = latest_statuses_ordered_by_stage.group_by(&:stage)
-
- stages.map do |stage_name, jobs|
- composite_status = Gitlab::Ci::Status::Composite
- .new(jobs)
-
- Ci::LegacyStage.new(self,
- name: stage_name,
- status: composite_status.status,
- warnings: composite_status.warnings?)
- end
- end
-
def triggered_pipelines_with_preloads
triggered_pipelines.preload(:source_job)
end
- # TODO: Remove usage of this method in templates
- def legacy_stages
- legacy_stages_using_composite_status
- end
-
def valid_commit_sha
if self.sha == Gitlab::Git::BLANK_SHA
self.errors.add(:sha, " cant be 00000000 (branch removal)")
@@ -1004,6 +986,10 @@ module Ci
object_hierarchy(project_condition: :same).base_and_descendants
end
+ def self_and_descendants_complete?
+ self_and_descendants.all?(&:complete?)
+ end
+
# Follow the parent-child relationships and return the top-level parent
def root_ancestor
return self unless child?
@@ -1078,7 +1064,11 @@ module Ci
end
def has_reports?(reports_scope)
- complete? && latest_report_builds(reports_scope).exists?
+ if Feature.enabled?(:mr_show_reports_immediately, project, type: :development)
+ latest_report_builds(reports_scope).exists?
+ else
+ complete? && latest_report_builds(reports_scope).exists?
+ end
end
def has_coverage_reports?
@@ -1100,7 +1090,7 @@ module Ci
end
def test_reports
- Gitlab::Ci::Reports::TestReports.new.tap do |test_reports|
+ Gitlab::Ci::Reports::TestReport.new.tap do |test_reports|
latest_test_report_builds.find_each do |build|
build.collect_test_reports!(test_reports)
end
@@ -1222,6 +1212,10 @@ module Ci
Gitlab::Utils.slugify(source_ref.to_s)
end
+ def stage(name)
+ stages.find_by(name: name)
+ end
+
def find_stage_by_name!(name)
stages.find_by!(name: name)
end
@@ -1307,10 +1301,20 @@ module Ci
end
end
- def has_expired_test_reports?
- strong_memoize(:has_expired_test_reports) do
- has_reports?(::Ci::JobArtifact.test_reports.expired)
+ def has_test_reports?
+ strong_memoize(:has_test_reports) do
+ has_reports?(::Ci::JobArtifact.test_reports)
+ end
+ end
+
+ def age_in_minutes
+ return 0 unless persisted?
+
+ unless has_attribute?(:created_at)
+ raise ArgumentError, 'pipeline not fully loaded'
end
+
+ (Time.current - created_at).ceil / 60
end
private
@@ -1363,6 +1367,21 @@ module Ci
project.repository.keep_around(self.sha, self.before_sha)
end
+ def observe_age_in_minutes
+ return unless age_metric_enabled?
+ return unless persisted? && has_attribute?(:created_at)
+
+ ::Gitlab::Ci::Pipeline::Metrics
+ .pipeline_age_histogram
+ .observe({}, age_in_minutes)
+ end
+
+ def age_metric_enabled?
+ ::Gitlab::SafeRequestStore.fetch(:age_metric_enabled) do
+ ::Feature.enabled?(:ci_pipeline_age_histogram, type: :ops)
+ end
+ end
+
# Without using `unscoped`, caller scope is also included into the query.
# Using `unscoped` here will be redundant after Rails 6.1
def object_hierarchy(options = {})
diff --git a/app/models/ci/pipeline_artifact.rb b/app/models/ci/pipeline_artifact.rb
index 2284a05bcc9..cdc3d69f754 100644
--- a/app/models/ci/pipeline_artifact.rb
+++ b/app/models/ci/pipeline_artifact.rb
@@ -51,6 +51,23 @@ module Ci
def find_by_file_type(file_type)
find_by(file_type: file_type)
end
+
+ def create_or_replace_for_pipeline!(pipeline:, file_type:, file:, size:)
+ transaction do
+ pipeline.pipeline_artifacts.find_by_file_type(file_type)&.destroy!
+
+ pipeline.pipeline_artifacts.create!(
+ file_type: file_type,
+ project_id: pipeline.project_id,
+ size: size,
+ file: file,
+ file_format: REPORT_TYPES[file_type],
+ expire_at: EXPIRATION_DATE.from_now
+ )
+ end
+ rescue ActiveRecord::ActiveRecordError => err
+ Gitlab::ErrorTracking.track_and_raise_exception(err, { pipeline_id: pipeline.id, file_type: file_type })
+ end
end
def present
diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb
index 61194c9b7d1..f41ad890184 100644
--- a/app/models/ci/runner.rb
+++ b/app/models/ci/runner.rb
@@ -2,6 +2,7 @@
module Ci
class Runner < Ci::ApplicationRecord
+ prepend Ci::BulkInsertableTags
include Gitlab::SQL::Pattern
include RedisCacheable
include ChronicDurationAttribute
@@ -14,6 +15,8 @@ module Ci
include Presentable
include EachBatch
+ ignore_column :semver, remove_with: '15.3', remove_after: '2022-07-22'
+
add_authentication_token_field :token, encrypted: :optional, expires_at: :compute_token_expiration, expiration_enforced?: :token_expiration_enforced?
enum access_level: {
@@ -75,9 +78,9 @@ module Ci
has_many :groups, through: :runner_namespaces, disable_joins: true
has_one :last_build, -> { order('id DESC') }, class_name: 'Ci::Build'
+ has_one :runner_version, primary_key: :version, foreign_key: :version, class_name: 'Ci::RunnerVersion'
before_save :ensure_token
- before_save :update_semver, if: -> { version_changed? }
scope :active, -> (value = true) { where(active: value) }
scope :paused, -> { active(false) }
@@ -430,7 +433,6 @@ module Ci
values = values&.slice(:version, :revision, :platform, :architecture, :ip_address, :config, :executor) || {}
values[:contacted_at] = Time.current
values[:executor_type] = EXECUTOR_NAME_TO_TYPES.fetch(values.delete(:executor), :unknown)
- values[:semver] = semver_from_version(values[:version])
cache_attributes(values)
@@ -451,16 +453,6 @@ module Ci
read_attribute(:contacted_at)
end
- def semver_from_version(version)
- parsed_runner_version = ::Gitlab::VersionInfo.parse(version)
-
- parsed_runner_version.valid? ? parsed_runner_version.to_s : nil
- end
-
- def update_semver
- self.semver = semver_from_version(self.version)
- end
-
def namespace_ids
strong_memoize(:namespace_ids) do
runner_namespaces.pluck(:namespace_id).compact
@@ -484,6 +476,10 @@ module Ci
private
+ scope :with_upgrade_status, ->(upgrade_status) do
+ Ci::Runner.joins(:runner_version).where(runner_version: { status: upgrade_status })
+ end
+
EXECUTOR_NAME_TO_TYPES = {
'unknown' => :unknown,
'custom' => :custom,
diff --git a/app/models/ci/runner_version.rb b/app/models/ci/runner_version.rb
new file mode 100644
index 00000000000..6b2d0060c9b
--- /dev/null
+++ b/app/models/ci/runner_version.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+module Ci
+ class RunnerVersion < Ci::ApplicationRecord
+ include EachBatch
+ include EnumWithNil
+
+ enum_with_nil status: {
+ not_processed: nil,
+ invalid_version: -1,
+ unknown: 0,
+ not_available: 1,
+ available: 2,
+ recommended: 3
+ }
+
+ STATUS_DESCRIPTIONS = {
+ invalid_version: 'Runner version is not valid.',
+ unknown: 'Upgrade status is unknown.',
+ not_available: 'Upgrade is not available for the runner.',
+ available: 'Upgrade is available for the runner.',
+ recommended: 'Upgrade is available and recommended for the runner.'
+ }.freeze
+
+ # Override auto generated negative scope (from available) so the scope has expected behavior
+ scope :not_available, -> { where(status: :not_available) }
+
+ # This scope returns all versions that might need recalculating. For instance, once a version is considered
+ # :recommended, it normally doesn't change status even if the instance is upgraded
+ scope :potentially_outdated, -> { where(status: [nil, :not_available, :available, :unknown]) }
+
+ validates :version, length: { maximum: 2048 }
+ end
+end
diff --git a/app/models/ci/stage.rb b/app/models/ci/stage.rb
index 8c4e97ac840..f03d1e96a4b 100644
--- a/app/models/ci/stage.rb
+++ b/app/models/ci/stage.rb
@@ -142,7 +142,7 @@ module Ci
end
def latest_stage_status
- statuses.latest.composite_status(project: project) || 'skipped'
+ statuses.latest.composite_status || 'skipped'
end
end
end
diff --git a/app/models/ci/trigger.rb b/app/models/ci/trigger.rb
index 5bf5ae51ec8..c4db4754c52 100644
--- a/app/models/ci/trigger.rb
+++ b/app/models/ci/trigger.rb
@@ -4,6 +4,9 @@ module Ci
class Trigger < Ci::ApplicationRecord
include Presentable
include Limitable
+ include IgnorableColumns
+
+ ignore_column :ref, remove_with: '15.4', remove_after: '2022-08-22'
self.limit_name = 'pipeline_triggers'
self.limit_scope = :project
diff --git a/app/models/ci/variable.rb b/app/models/ci/variable.rb
index 1e91f248fc4..c80c2ebe69a 100644
--- a/app/models/ci/variable.rb
+++ b/app/models/ci/variable.rb
@@ -18,5 +18,9 @@ module Ci
scope :unprotected, -> { where(protected: false) }
scope :by_environment_scope, -> (environment_scope) { where(environment_scope: environment_scope) }
+
+ def audit_details
+ key
+ end
end
end
diff --git a/app/models/clusters/agent.rb b/app/models/clusters/agent.rb
index fb12ce7d292..3478bb69707 100644
--- a/app/models/clusters/agent.rb
+++ b/app/models/clusters/agent.rb
@@ -53,3 +53,5 @@ module Clusters
end
end
end
+
+Clusters::Agent.prepend_mod_with('Clusters::Agent')
diff --git a/app/models/clusters/applications/elastic_stack.rb b/app/models/clusters/applications/elastic_stack.rb
deleted file mode 100644
index 73c731aab1a..00000000000
--- a/app/models/clusters/applications/elastic_stack.rb
+++ /dev/null
@@ -1,113 +0,0 @@
-# frozen_string_literal: true
-
-module Clusters
- module Applications
- class ElasticStack < ApplicationRecord
- include ::Clusters::Concerns::ElasticsearchClient
-
- VERSION = '3.0.0'
-
- self.table_name = 'clusters_applications_elastic_stacks'
-
- include ::Clusters::Concerns::ApplicationCore
- include ::Clusters::Concerns::ApplicationStatus
- include ::Clusters::Concerns::ApplicationVersion
- include ::Clusters::Concerns::ApplicationData
-
- default_value_for :version, VERSION
-
- after_destroy do
- cluster&.find_or_build_integration_elastic_stack&.update(enabled: false, chart_version: nil)
- end
-
- state_machine :status do
- after_transition any => [:installed] do |application|
- application.cluster&.find_or_build_integration_elastic_stack&.update(enabled: true, chart_version: application.version)
- end
-
- after_transition any => [:uninstalled] do |application|
- application.cluster&.find_or_build_integration_elastic_stack&.update(enabled: false, chart_version: nil)
- end
- end
-
- def chart
- 'elastic-stack/elastic-stack'
- end
-
- def repository
- 'https://charts.gitlab.io'
- end
-
- def install_command
- helm_command_module::InstallCommand.new(
- name: 'elastic-stack',
- version: VERSION,
- rbac: cluster.platform_kubernetes_rbac?,
- chart: chart,
- repository: repository,
- files: files,
- preinstall: migrate_to_3_script,
- postinstall: post_install_script
- )
- end
-
- def uninstall_command
- helm_command_module::DeleteCommand.new(
- name: 'elastic-stack',
- rbac: cluster.platform_kubernetes_rbac?,
- files: files,
- postdelete: post_delete_script
- )
- end
-
- def files
- super.merge('wait-for-elasticsearch.sh': File.read("#{Rails.root}/vendor/elastic_stack/wait-for-elasticsearch.sh"))
- end
-
- def chart_above_v2?
- Gem::Version.new(version) >= Gem::Version.new('2.0.0')
- end
-
- def chart_above_v3?
- Gem::Version.new(version) >= Gem::Version.new('3.0.0')
- end
-
- private
-
- def service_name
- chart_above_v3? ? 'elastic-stack-elasticsearch-master' : 'elastic-stack-elasticsearch-client'
- end
-
- def pvc_selector
- chart_above_v3? ? "app=elastic-stack-elasticsearch-master" : "release=elastic-stack"
- end
-
- def post_install_script
- [
- "timeout 60 sh /data/helm/elastic-stack/config/wait-for-elasticsearch.sh http://elastic-stack-elasticsearch-master:9200"
- ]
- end
-
- def post_delete_script
- [
- Gitlab::Kubernetes::KubectlCmd.delete("pvc", "--selector", pvc_selector, "--namespace", Gitlab::Kubernetes::Helm::NAMESPACE)
- ]
- end
-
- def migrate_to_3_script
- return [] if !updating? || chart_above_v3?
-
- # Chart version 3.0.0 moves to our own chart at https://gitlab.com/gitlab-org/charts/elastic-stack
- # and is not compatible with pre-existing resources. We first remove them.
- [
- helm_command_module::DeleteCommand.new(
- name: 'elastic-stack',
- rbac: cluster.platform_kubernetes_rbac?,
- files: files
- ).delete_command,
- Gitlab::Kubernetes::KubectlCmd.delete("pvc", "--selector", "release=elastic-stack", "--namespace", Gitlab::Kubernetes::Helm::NAMESPACE)
- ]
- end
- end
- end
-end
diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb
index 014f7530357..ad1e7dc305f 100644
--- a/app/models/clusters/cluster.rb
+++ b/app/models/clusters/cluster.rb
@@ -20,7 +20,6 @@ module Clusters
Clusters::Applications::Runner.application_name => Clusters::Applications::Runner,
Clusters::Applications::Jupyter.application_name => Clusters::Applications::Jupyter,
Clusters::Applications::Knative.application_name => Clusters::Applications::Knative,
- Clusters::Applications::ElasticStack.application_name => Clusters::Applications::ElasticStack,
Clusters::Applications::Cilium.application_name => Clusters::Applications::Cilium
}.freeze
DEFAULT_ENVIRONMENT = '*'
@@ -51,7 +50,6 @@ module Clusters
has_one :platform_kubernetes, class_name: 'Clusters::Platforms::Kubernetes', inverse_of: :cluster, autosave: true
has_one :integration_prometheus, class_name: 'Clusters::Integrations::Prometheus', inverse_of: :cluster
- has_one :integration_elastic_stack, class_name: 'Clusters::Integrations::ElasticStack', inverse_of: :cluster
def self.has_one_cluster_application(name) # rubocop:disable Naming/PredicateName
application = APPLICATIONS[name.to_s]
@@ -66,7 +64,6 @@ module Clusters
has_one_cluster_application :runner
has_one_cluster_application :jupyter
has_one_cluster_application :knative
- has_one_cluster_application :elastic_stack
has_one_cluster_application :cilium
has_many :kubernetes_namespaces
@@ -102,7 +99,6 @@ module Clusters
delegate :available?, to: :application_helm, prefix: true, allow_nil: true
delegate :available?, to: :application_ingress, prefix: true, allow_nil: true
delegate :available?, to: :application_knative, prefix: true, allow_nil: true
- delegate :available?, to: :integration_elastic_stack, prefix: true, allow_nil: true
delegate :available?, to: :integration_prometheus, prefix: true, allow_nil: true
delegate :external_ip, to: :application_ingress, prefix: true, allow_nil: true
delegate :external_hostname, to: :application_ingress, prefix: true, allow_nil: true
@@ -136,7 +132,6 @@ module Clusters
scope :gcp_installed, -> { gcp_provided.joins(:provider_gcp).merge(Clusters::Providers::Gcp.with_status(:created)) }
scope :aws_installed, -> { aws_provided.joins(:provider_aws).merge(Clusters::Providers::Aws.with_status(:created)) }
- scope :with_available_elasticstack, -> { joins(:application_elastic_stack).merge(::Clusters::Applications::ElasticStack.available) }
scope :distinct_with_deployed_environments, -> { joins(:environments).merge(::Deployment.success).distinct }
scope :managed, -> { where(managed: true) }
@@ -271,10 +266,6 @@ module Clusters
integration_prometheus || build_integration_prometheus
end
- def find_or_build_integration_elastic_stack
- integration_elastic_stack || build_integration_elastic_stack
- end
-
def provider
if gcp?
provider_gcp
@@ -309,18 +300,6 @@ module Clusters
platform_kubernetes&.kubeclient if kubernetes?
end
- def elastic_stack_adapter
- integration_elastic_stack
- end
-
- def elasticsearch_client
- elastic_stack_adapter&.elasticsearch_client
- end
-
- def elastic_stack_available?
- !!integration_elastic_stack_available?
- end
-
def kubernetes_namespace_for(environment, deployable: environment.last_deployable)
if deployable && environment.project_id != deployable.project_id
raise ArgumentError, 'environment.project_id must match deployable.project_id'
diff --git a/app/models/clusters/concerns/elasticsearch_client.rb b/app/models/clusters/concerns/elasticsearch_client.rb
deleted file mode 100644
index e9aab7897a8..00000000000
--- a/app/models/clusters/concerns/elasticsearch_client.rb
+++ /dev/null
@@ -1,38 +0,0 @@
-# frozen_string_literal: true
-
-module Clusters
- module Concerns
- module ElasticsearchClient
- include ::Gitlab::Utils::StrongMemoize
-
- ELASTICSEARCH_PORT = 9200
- ELASTICSEARCH_NAMESPACE = 'gitlab-managed-apps'
-
- def elasticsearch_client(timeout: nil)
- strong_memoize(:elasticsearch_client) do
- kube_client = cluster&.kubeclient&.core_client
- next unless kube_client
-
- proxy_url = kube_client.proxy_url('service', service_name, ELASTICSEARCH_PORT, ELASTICSEARCH_NAMESPACE)
-
- Elasticsearch::Client.new(url: proxy_url, adapter: :net_http) do |faraday|
- # ensures headers containing auth data are appended to original client options
- faraday.headers.merge!(kube_client.headers)
- # ensure TLS certs are properly verified
- faraday.ssl[:verify] = kube_client.ssl_options[:verify_ssl]
- faraday.ssl[:cert_store] = kube_client.ssl_options[:cert_store]
- faraday.options.timeout = timeout unless timeout.nil?
- end
-
- rescue Kubeclient::HttpError => error
- # 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.
- # We check for a nil client in downstream use and behaviour is equivalent to an empty state
- log_exception(error, :failed_to_create_elasticsearch_client)
-
- nil
- end
- end
- end
- end
-end
diff --git a/app/models/clusters/integrations/elastic_stack.rb b/app/models/clusters/integrations/elastic_stack.rb
deleted file mode 100644
index 97d73d252b9..00000000000
--- a/app/models/clusters/integrations/elastic_stack.rb
+++ /dev/null
@@ -1,40 +0,0 @@
-# frozen_string_literal: true
-
-module Clusters
- module Integrations
- class ElasticStack < ApplicationRecord
- include ::Clusters::Concerns::ElasticsearchClient
- include ::Clusters::Concerns::KubernetesLogger
-
- self.table_name = 'clusters_integration_elasticstack'
- self.primary_key = :cluster_id
-
- belongs_to :cluster, class_name: 'Clusters::Cluster', foreign_key: :cluster_id
-
- validates :cluster, presence: true
- validates :enabled, inclusion: { in: [true, false] }
-
- scope :enabled, -> { where(enabled: true) }
-
- def available?
- enabled
- end
-
- def service_name
- chart_above_v3? ? 'elastic-stack-elasticsearch-master' : 'elastic-stack-elasticsearch-client'
- end
-
- def chart_above_v2?
- return true if chart_version.nil?
-
- Gem::Version.new(chart_version) >= Gem::Version.new('2.0.0')
- end
-
- def chart_above_v3?
- return true if chart_version.nil?
-
- Gem::Version.new(chart_version) >= Gem::Version.new('3.0.0')
- end
- end
- end
-end
diff --git a/app/models/clusters/integrations/prometheus.rb b/app/models/clusters/integrations/prometheus.rb
index 0d6177beae7..899529ff49f 100644
--- a/app/models/clusters/integrations/prometheus.rb
+++ b/app/models/clusters/integrations/prometheus.rb
@@ -55,23 +55,13 @@ module Clusters
private
def activate_project_integrations
- if Feature.enabled?(:rename_integrations_workers)
- ::Clusters::Applications::ActivateIntegrationWorker
- .perform_async(cluster_id, ::Integrations::Prometheus.to_param)
- else
- ::Clusters::Applications::ActivateServiceWorker
- .perform_async(cluster_id, ::Integrations::Prometheus.to_param)
- end
+ ::Clusters::Applications::ActivateIntegrationWorker
+ .perform_async(cluster_id, ::Integrations::Prometheus.to_param)
end
def deactivate_project_integrations
- if Feature.enabled?(:rename_integrations_workers)
- ::Clusters::Applications::DeactivateIntegrationWorker
- .perform_async(cluster_id, ::Integrations::Prometheus.to_param)
- else
- ::Clusters::Applications::DeactivateServiceWorker
- .perform_async(cluster_id, ::Integrations::Prometheus.to_param)
- end
+ ::Clusters::Applications::DeactivateIntegrationWorker
+ .perform_async(cluster_id, ::Integrations::Prometheus.to_param)
end
end
end
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index ac9d8c39bd2..afe4927ee73 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -8,9 +8,12 @@ class CommitStatus < Ci::ApplicationRecord
include EnumWithNil
include BulkInsertableAssociations
include TaggableQueries
+ include IgnorableColumns
self.table_name = 'ci_builds'
+ ignore_column :token, remove_with: '15.4', remove_after: '2022-08-22'
+
belongs_to :user
belongs_to :project
belongs_to :pipeline, class_name: 'Ci::Pipeline', foreign_key: :commit_id
@@ -220,10 +223,6 @@ class CommitStatus < Ci::ApplicationRecord
false
end
- def self.bulk_insert_tags!(statuses)
- Gitlab::Ci::Tags::BulkInsert.new(statuses).insert!
- end
-
def locking_enabled?
will_save_change_to_status?
end
@@ -325,5 +324,3 @@ class CommitStatus < Ci::ApplicationRecord
script_failure? || missing_dependency_failure? || archived_failure? || scheduler_failure? || data_integrity_failure?
end
end
-
-CommitStatus.prepend_mod_with('CommitStatus')
diff --git a/app/models/concerns/awareness.rb b/app/models/concerns/awareness.rb
new file mode 100644
index 00000000000..da87d87e838
--- /dev/null
+++ b/app/models/concerns/awareness.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+module Awareness
+ extend ActiveSupport::Concern
+
+ KEY_NAMESPACE = "gitlab:awareness"
+ private_constant :KEY_NAMESPACE
+
+ def join(session)
+ session.join(self)
+
+ nil
+ end
+
+ def leave(session)
+ session.leave(self)
+
+ nil
+ end
+
+ def session_ids
+ with_redis do |redis|
+ redis
+ .smembers(user_sessions_key)
+ # converts session ids from (internal) integer to hex presentation
+ .map { |key| key.to_i.to_s(16) }
+ end
+ end
+
+ private
+
+ def user_sessions_key
+ "#{KEY_NAMESPACE}:user:#{id}:sessions"
+ end
+
+ def with_redis
+ Gitlab::Redis::SharedState.with do |redis|
+ yield redis if block_given?
+ end
+ end
+end
diff --git a/app/models/concerns/cache_markdown_field.rb b/app/models/concerns/cache_markdown_field.rb
index 99dbe464a7c..9ee0fd1db1d 100644
--- a/app/models/concerns/cache_markdown_field.rb
+++ b/app/models/concerns/cache_markdown_field.rb
@@ -172,7 +172,7 @@ module CacheMarkdownField
refs = all_references(self.author)
references = {}
- references[:mentioned_users_ids] = refs.mentioned_user_ids.presence
+ references[:mentioned_users_ids] = mentioned_filtered_user_ids_for(refs)
references[:mentioned_groups_ids] = refs.mentioned_group_ids.presence
references[:mentioned_projects_ids] = refs.mentioned_project_ids.presence
@@ -185,6 +185,13 @@ module CacheMarkdownField
true
end
+ # Overriden on objects that needs to filter
+ # mentioned users that cannot read them, for example,
+ # guest users that are referenced on a confidential note.
+ def mentioned_filtered_user_ids_for(refs)
+ refs.mentioned_user_ids.presence
+ end
+
def mentionable_attributes_changed?(changes = saved_changes)
return false unless is_a?(Mentionable)
diff --git a/app/models/concerns/ci/artifactable.rb b/app/models/concerns/ci/artifactable.rb
index 78340cf967b..fb4ea4206f4 100644
--- a/app/models/concerns/ci/artifactable.rb
+++ b/app/models/concerns/ci/artifactable.rb
@@ -30,6 +30,8 @@ module Ci
raise NotSupportedAdapterError, 'This file format requires a dedicated adapter'
end
+ ::Gitlab::ApplicationContext.push(artifact: file.model)
+
file.open do |stream|
file_format_adapter_class.new(stream).each_blob(&blk)
end
diff --git a/app/models/concerns/ci/bulk_insertable_tags.rb b/app/models/concerns/ci/bulk_insertable_tags.rb
new file mode 100644
index 00000000000..453b3b3fbc9
--- /dev/null
+++ b/app/models/concerns/ci/bulk_insertable_tags.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+module Ci
+ module BulkInsertableTags
+ extend ActiveSupport::Concern
+
+ BULK_INSERT_TAG_THREAD_KEY = 'ci_bulk_insert_tags'
+
+ class << self
+ def with_bulk_insert_tags
+ previous = Thread.current[BULK_INSERT_TAG_THREAD_KEY]
+ Thread.current[BULK_INSERT_TAG_THREAD_KEY] = true
+ yield
+ ensure
+ Thread.current[BULK_INSERT_TAG_THREAD_KEY] = previous
+ end
+ end
+
+ # overrides save_tags from acts-as-taggable
+ def save_tags
+ super unless Thread.current[BULK_INSERT_TAG_THREAD_KEY]
+ end
+ end
+end
diff --git a/app/models/concerns/ci/has_status.rb b/app/models/concerns/ci/has_status.rb
index cca66c3ec94..721cb14201f 100644
--- a/app/models/concerns/ci/has_status.rb
+++ b/app/models/concerns/ci/has_status.rb
@@ -23,11 +23,9 @@ module Ci
UnknownStatusError = Class.new(StandardError)
class_methods do
- # The parameter `project` is only used for the feature flag check, and will be removed with
- # https://gitlab.com/gitlab-org/gitlab/-/issues/321972
- def composite_status(project: nil)
+ def composite_status
Gitlab::Ci::Status::Composite
- .new(all, with_allow_failure: columns_hash.key?('allow_failure'), project: project)
+ .new(all, with_allow_failure: columns_hash.key?('allow_failure'))
.status
end
diff --git a/app/models/concerns/each_batch.rb b/app/models/concerns/each_batch.rb
index 443e1ab53b4..dbc0887dc97 100644
--- a/app/models/concerns/each_batch.rb
+++ b/app/models/concerns/each_batch.rb
@@ -2,6 +2,7 @@
module EachBatch
extend ActiveSupport::Concern
+ include LooseIndexScan
class_methods do
# Iterates over the rows in a relation in batches, similar to Rails'
@@ -100,5 +101,65 @@ module EachBatch
break unless stop
end
end
+
+ # Iterates over the rows in a relation in batches by skipping duplicated values in the column.
+ # Example: counting the number of distinct authors in `issues`
+ #
+ # - Table size: 100_000
+ # - Column: author_id
+ # - Distinct author_ids in the table: 1000
+ #
+ # The query will read maximum 1000 rows if we have index coverage on user_id.
+ #
+ # > count = 0
+ # > Issue.distinct_each_batch(column: 'author_id', of: 1000) { |r| count += r.count(:author_id) }
+ def distinct_each_batch(column:, order: :asc, of: 1000)
+ start = except(:select)
+ .select(column)
+ .reorder(column => order)
+
+ start = start.take
+
+ return unless start
+
+ start_id = start[column]
+ arel_table = self.arel_table
+ arel_column = arel_table[column.to_s]
+
+ 1.step do |index|
+ stop = loose_index_scan(column: column, order: order) do |cte_query, inner_query|
+ if order == :asc
+ [cte_query.where(arel_column.gteq(start_id)), inner_query]
+ else
+ [cte_query.where(arel_column.lteq(start_id)), inner_query]
+ end
+ end.offset(of).take
+
+ if stop
+ stop_id = stop[column]
+
+ relation = loose_index_scan(column: column, order: order) do |cte_query, inner_query|
+ if order == :asc
+ [cte_query.where(arel_column.gteq(start_id)), inner_query.where(arel_column.lt(stop_id))]
+ else
+ [cte_query.where(arel_column.lteq(start_id)), inner_query.where(arel_column.gt(stop_id))]
+ end
+ end
+ start_id = stop_id
+ else
+ relation = loose_index_scan(column: column, order: order) do |cte_query, inner_query|
+ if order == :asc
+ [cte_query.where(arel_column.gteq(start_id)), inner_query]
+ else
+ [cte_query.where(arel_column.lteq(start_id)), inner_query]
+ end
+ end
+ end
+
+ unscoped { yield relation, index }
+
+ break unless stop
+ end
+ end
end
end
diff --git a/app/models/concerns/enums/ci/commit_status.rb b/app/models/concerns/enums/ci/commit_status.rb
index 445277a7a7c..ecb120d8013 100644
--- a/app/models/concerns/enums/ci/commit_status.rb
+++ b/app/models/concerns/enums/ci/commit_status.rb
@@ -29,9 +29,12 @@ module Enums
builds_disabled: 20,
environment_creation_failure: 21,
deployment_rejected: 22,
+ protected_environment_failure: 1_000,
insufficient_bridge_permissions: 1_001,
downstream_bridge_project_not_found: 1_002,
invalid_bridge_trigger: 1_003,
+ upstream_bridge_project_not_found: 1_004,
+ insufficient_upstream_permissions: 1_005,
bridge_pipeline_is_child_pipeline: 1_006, # not used anymore, but cannot be deleted because of old data
downstream_pipeline_creation_failed: 1_007,
secrets_provider_not_found: 1_008,
@@ -42,5 +45,3 @@ module Enums
end
end
end
-
-Enums::Ci::CommitStatus.prepend_mod_with('Enums::Ci::CommitStatus')
diff --git a/app/models/concerns/integrations/has_issue_tracker_fields.rb b/app/models/concerns/integrations/has_issue_tracker_fields.rb
index b1def38d019..57f8e21c5a6 100644
--- a/app/models/concerns/integrations/has_issue_tracker_fields.rb
+++ b/app/models/concerns/integrations/has_issue_tracker_fields.rb
@@ -5,26 +5,32 @@ module Integrations
extend ActiveSupport::Concern
included do
+ self.field_storage = :data_fields
+
field :project_url,
required: true,
- storage: :data_fields,
title: -> { _('Project URL') },
- help: -> { s_('IssueTracker|The URL to the project in the external issue tracker.') }
+ help: -> do
+ s_('IssueTracker|The URL to the project in the external issue tracker.')
+ end
field :issues_url,
required: true,
- storage: :data_fields,
title: -> { s_('IssueTracker|Issue URL') },
help: -> do
- format s_('IssueTracker|The URL to view an issue in the external issue tracker. Must contain %{colon_id}.'),
+ ERB::Util.html_escape(
+ s_('IssueTracker|The URL to view an issue in the external issue tracker. Must contain %{colon_id}.')
+ ) % {
colon_id: '<code>:id</code>'.html_safe
+ }
end
field :new_issue_url,
required: true,
- storage: :data_fields,
title: -> { s_('IssueTracker|New issue URL') },
- help: -> { s_('IssueTracker|The URL to create an issue in the external issue tracker.') }
+ help: -> do
+ s_('IssueTracker|The URL to create an issue in the external issue tracker.')
+ end
end
end
end
diff --git a/app/models/concerns/integrations/slack_mattermost_notifier.rb b/app/models/concerns/integrations/slack_mattermost_notifier.rb
index 3bdaa852ddf..142e62bb501 100644
--- a/app/models/concerns/integrations/slack_mattermost_notifier.rb
+++ b/app/models/concerns/integrations/slack_mattermost_notifier.rb
@@ -34,7 +34,7 @@ module Integrations
class HTTPClient
def self.post(uri, params = {})
params.delete(:http_options) # these are internal to the client and we do not want them
- Gitlab::HTTP.post(uri, body: params, use_read_total_timeout: true)
+ Gitlab::HTTP.post(uri, body: params)
end
end
end
diff --git a/app/models/concerns/loose_index_scan.rb b/app/models/concerns/loose_index_scan.rb
new file mode 100644
index 00000000000..5d37a30171a
--- /dev/null
+++ b/app/models/concerns/loose_index_scan.rb
@@ -0,0 +1,67 @@
+# frozen_string_literal: true
+
+module LooseIndexScan
+ extend ActiveSupport::Concern
+
+ class_methods do
+ # Builds a recursive query to read distinct values from a column.
+ #
+ # Example 1: collect all distinct author ids for the `issues` table
+ #
+ # Bad: The DB reads all issues, sorts and dedups them in memory
+ #
+ # > Issue.select(:author_id).distinct.map(&:author_id)
+ #
+ # Good: Use loose index scan (skip index scan)
+ #
+ # > Issue.loose_index_scan(column: :author_id).map(&:author_id)
+ #
+ # Example 2: List of users for the DONE todos selector. Select all users who created a todo.
+ #
+ # Bad: Loads all DONE todos for the given user and extracts the author_ids
+ #
+ # > User.where(id: Todo.where(user_id: 4156052).done.select(:author_id))
+ #
+ # Good: Loads distinct author_ids from todos and then loads users
+ #
+ # > distinct_authors = Todo.where(user_id: 4156052).done.loose_index_scan(column: :author_id).select(:author_id)
+ # > User.where(id: distinct_authors)
+ def loose_index_scan(column:, order: :asc)
+ arel_table = self.arel_table
+ arel_column = arel_table[column.to_s]
+
+ cte = Gitlab::SQL::RecursiveCTE.new(:loose_index_scan_cte, union_args: { remove_order: false })
+
+ cte_query = except(:select)
+ .select(column)
+ .order(column => order)
+ .limit(1)
+
+ inner_query = except(:select)
+
+ cte_query, inner_query = yield([cte_query, inner_query]) if block_given?
+ cte << cte_query
+
+ inner_query = if order == :asc
+ inner_query.where(arel_column.gt(cte.table[column.to_s]))
+ else
+ inner_query.where(arel_column.lt(cte.table[column.to_s]))
+ end
+
+ inner_query = inner_query.order(column => order)
+ .select(column)
+ .limit(1)
+
+ cte << cte.table
+ .project(Arel::Nodes::Grouping.new(Arel.sql(inner_query.to_sql)).as(column.to_s))
+
+ unscoped do
+ select(column)
+ .with
+ .recursive(cte.to_arel)
+ .from(cte.alias_to(arel_table))
+ .where(arel_column.not_eq(nil)) # filtering out the last NULL value
+ end
+ end
+ end
+end
diff --git a/app/models/concerns/milestoneable.rb b/app/models/concerns/milestoneable.rb
index 12041b103f6..14c54d99ef3 100644
--- a/app/models/concerns/milestoneable.rb
+++ b/app/models/concerns/milestoneable.rb
@@ -16,7 +16,7 @@ module Milestoneable
scope :any_milestone, -> { where.not(milestone_id: nil) }
scope :with_milestone, ->(title) { left_joins_milestones.where(milestones: { title: title }) }
- scope :without_particular_milestone, ->(title) { left_outer_joins(:milestone).where("milestones.title != ? OR milestone_id IS NULL", title) }
+ scope :without_particular_milestones, ->(titles) { left_outer_joins(:milestone).where("milestones.title NOT IN (?) OR milestone_id IS NULL", titles) }
scope :any_release, -> { joins_milestone_releases }
scope :with_release, -> (tag, project_id) { joins_milestone_releases.where( milestones: { releases: { tag: tag, project_id: project_id } } ) }
scope :without_particular_release, -> (tag, project_id) { joins_milestone_releases.where.not(milestones: { releases: { tag: tag, project_id: project_id } }) }
diff --git a/app/models/concerns/notification_branch_selection.rb b/app/models/concerns/notification_branch_selection.rb
index 18ec996c3df..f2df7579a65 100644
--- a/app/models/concerns/notification_branch_selection.rb
+++ b/app/models/concerns/notification_branch_selection.rb
@@ -6,13 +6,15 @@
module NotificationBranchSelection
extend ActiveSupport::Concern
- def branch_choices
- [
- [_('All branches'), 'all'].freeze,
- [_('Default branch'), 'default'].freeze,
- [_('Protected branches'), 'protected'].freeze,
- [_('Default branch and protected branches'), 'default_and_protected'].freeze
- ].freeze
+ class_methods do
+ def branch_choices
+ [
+ [_('All branches'), 'all'].freeze,
+ [_('Default branch'), 'default'].freeze,
+ [_('Protected branches'), 'protected'].freeze,
+ [_('Default branch and protected branches'), 'default_and_protected'].freeze
+ ].freeze
+ end
end
def notify_for_branch?(data)
diff --git a/app/models/concerns/packages/fips.rb b/app/models/concerns/packages/fips.rb
new file mode 100644
index 00000000000..b8589cdc991
--- /dev/null
+++ b/app/models/concerns/packages/fips.rb
@@ -0,0 +1,11 @@
+# rubocop:disable Naming/FileName
+# frozen_string_literal: true
+
+module Packages
+ module FIPS
+ extend ActiveSupport::Concern
+
+ DisabledError = Class.new(StandardError)
+ end
+end
+# rubocop:enable Naming/FileName
diff --git a/app/models/concerns/participable.rb b/app/models/concerns/participable.rb
index 20743ebcb52..f59b5d1ecc8 100644
--- a/app/models/concerns/participable.rb
+++ b/app/models/concerns/participable.rb
@@ -92,7 +92,13 @@ module Participable
end
def raw_participants(current_user = nil, verify_access: false)
- ext = Gitlab::ReferenceExtractor.new(project, current_user)
+ extractor = Gitlab::ReferenceExtractor.new(project, current_user)
+
+ # Used to extract references from confidential notes.
+ # Referenced users that cannot read confidential notes are
+ # later removed from participants array.
+ internal_notes_extractor = Gitlab::ReferenceExtractor.new(project, current_user)
+
participants = Set.new
process = [self]
@@ -107,6 +113,8 @@ module Participable
source.class.participant_attrs.each do |attr|
if attr.respond_to?(:call)
+ ext = use_internal_notes_extractor_for?(source) ? internal_notes_extractor : extractor
+
source.instance_exec(current_user, ext, &attr)
else
process << source.__send__(attr) # rubocop:disable GitlabSecurity/PublicSend
@@ -121,7 +129,18 @@ module Participable
end
end
- participants.merge(ext.users)
+ participants.merge(users_that_can_read_internal_notes(internal_notes_extractor))
+ participants.merge(extractor.users)
+ end
+
+ def use_internal_notes_extractor_for?(source)
+ source.is_a?(Note) && source.confidential?
+ end
+
+ def users_that_can_read_internal_notes(extractor)
+ return [] unless self.is_a?(Noteable) && self.try(:resource_parent)
+
+ Ability.users_that_can_read_internal_notes(extractor.users, self.resource_parent)
end
def source_visible_to_user?(source, user)
diff --git a/app/models/concerns/require_email_verification.rb b/app/models/concerns/require_email_verification.rb
new file mode 100644
index 00000000000..cf6a31e6ebd
--- /dev/null
+++ b/app/models/concerns/require_email_verification.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+# == Require Email Verification module
+#
+# Contains functionality to handle email verification
+module RequireEmailVerification
+ extend ActiveSupport::Concern
+ include Gitlab::Utils::StrongMemoize
+
+ # This value is twice the amount we want it to be, because due to a bug in the devise-two-factor
+ # gem every failed login attempt increments the value of failed_attempts by 2 instead of 1.
+ # See: https://github.com/tinfoil/devise-two-factor/issues/127
+ MAXIMUM_ATTEMPTS = 3 * 2
+ UNLOCK_IN = 24.hours
+
+ included do
+ # Virtual attribute for the email verification token form
+ attr_accessor :verification_token
+ end
+
+ # When overridden, do not send Devise unlock instructions when locking access.
+ def lock_access!(opts = {})
+ return super unless override_devise_lockable?
+
+ super({ send_instructions: false })
+ end
+
+ protected
+
+ # We cannot override the class methods `maximum_attempts` and `unlock_in`, because we want to
+ # check for 2FA being enabled on the instance. So instead override the Devise Lockable methods
+ # where those values are used.
+ def attempts_exceeded?
+ return super unless override_devise_lockable?
+
+ failed_attempts >= MAXIMUM_ATTEMPTS
+ end
+
+ def lock_expired?
+ return super unless override_devise_lockable?
+
+ locked_at && locked_at < UNLOCK_IN.ago
+ end
+
+ private
+
+ def override_devise_lockable?
+ strong_memoize(:override_devise_lockable) do
+ Feature.enabled?(:require_email_verification, self) && !two_factor_enabled?
+ end
+ end
+end
diff --git a/app/models/concerns/vulnerability_finding_helpers.rb b/app/models/concerns/vulnerability_finding_helpers.rb
index 7f96b3901f1..4cf36f83857 100644
--- a/app/models/concerns/vulnerability_finding_helpers.rb
+++ b/app/models/concerns/vulnerability_finding_helpers.rb
@@ -42,4 +42,41 @@ module VulnerabilityFindingHelpers
)
end
end
+
+ def build_vulnerability_finding(security_finding)
+ report_finding = report_finding_for(security_finding)
+ return Vulnerabilities::Finding.new unless report_finding
+
+ finding_data = report_finding.to_hash.except(:compare_key, :identifiers, :location, :scanner, :links, :signatures,
+ :flags, :evidence)
+ identifiers = report_finding.identifiers.map do |identifier|
+ Vulnerabilities::Identifier.new(identifier.to_hash)
+ end
+ signatures = report_finding.signatures.map do |signature|
+ Vulnerabilities::FindingSignature.new(signature.to_hash)
+ end
+ evidence = Vulnerabilities::Finding::Evidence.new(data: report_finding.evidence.data) if report_finding.evidence
+
+ Vulnerabilities::Finding.new(finding_data).tap do |finding|
+ finding.location_fingerprint = report_finding.location.fingerprint
+ finding.vulnerability = vulnerability_for(security_finding.uuid)
+ finding.project = project
+ finding.sha = pipeline.sha
+ finding.scanner = security_finding.scanner
+ finding.finding_evidence = evidence
+
+ if calculate_false_positive?
+ finding.vulnerability_flags = report_finding.flags.map do |flag|
+ Vulnerabilities::Flag.new(flag)
+ end
+ end
+
+ finding.identifiers = identifiers
+ finding.signatures = signatures
+ end
+ end
+
+ def calculate_false_positive?
+ project.licensed_feature_available?(:sast_fp_reduction)
+ end
end
diff --git a/app/models/container_registry/event.rb b/app/models/container_registry/event.rb
index 47d21d21afd..d4075e1ff1b 100644
--- a/app/models/container_registry/event.rb
+++ b/app/models/container_registry/event.rb
@@ -6,6 +6,7 @@ module ContainerRegistry
ALLOWED_ACTIONS = %w(push delete).freeze
PUSH_ACTION = 'push'
+ DELETE_ACTION = 'delete'
EVENT_TRACKING_CATEGORY = 'container_registry:notification'
attr_reader :event
@@ -41,6 +42,10 @@ module ContainerRegistry
event['target'].has_key?('tag')
end
+ def target_digest?
+ event['target'].has_key?('digest')
+ end
+
def target_repository?
!target_tag? && event['target'].has_key?('repository')
end
@@ -53,6 +58,10 @@ module ContainerRegistry
PUSH_ACTION == action
end
+ def action_delete?
+ DELETE_ACTION == action
+ end
+
def container_repository_exists?
return unless container_registry_path
@@ -74,7 +83,7 @@ module ContainerRegistry
def update_project_statistics
return unless supported?
- return unless target_tag?
+ return unless target_tag? || (action_delete? && target_digest?)
return unless project
Rails.cache.delete(project.root_ancestor.container_repositories_size_cache_key)
diff --git a/app/models/container_repository.rb b/app/models/container_repository.rb
index c965d7cffe1..cdfd24e00aa 100644
--- a/app/models/container_repository.rb
+++ b/app/models/container_repository.rb
@@ -468,7 +468,7 @@ class ContainerRepository < ApplicationRecord
def size
strong_memoize(:size) do
next unless Gitlab.com?
- next if self.created_at.before?(MIGRATION_PHASE_1_STARTED_AT)
+ next if self.created_at.before?(MIGRATION_PHASE_1_STARTED_AT) && self.migration_state != 'import_done'
next unless gitlab_api_client.supports_gitlab_api?
gitlab_api_client.repository_details(self.path, sizing: :self)['size_bytes']
diff --git a/app/models/customer_relations/contact.rb b/app/models/customer_relations/contact.rb
index ded6ab8687a..0f13c45b84d 100644
--- a/app/models/customer_relations/contact.rb
+++ b/app/models/customer_relations/contact.rb
@@ -57,7 +57,22 @@ class CustomerRelations::Contact < ApplicationRecord
end
def self.sort_by_name
- order("last_name ASC, first_name ASC")
+ order(Gitlab::Pagination::Keyset::Order.build([
+ Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
+ attribute_name: 'last_name',
+ order_expression: arel_table[:last_name].asc,
+ distinct: false
+ ),
+ Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
+ attribute_name: 'first_name',
+ order_expression: arel_table[:first_name].asc,
+ distinct: false
+ ),
+ Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
+ attribute_name: 'id',
+ order_expression: arel_table[:id].asc
+ )
+ ]))
end
def self.find_ids_by_emails(group, emails)
diff --git a/app/models/deploy_token.rb b/app/models/deploy_token.rb
index 3c0f7d91a03..20d19ec9541 100644
--- a/app/models/deploy_token.rb
+++ b/app/models/deploy_token.rb
@@ -5,9 +5,6 @@ class DeployToken < ApplicationRecord
include TokenAuthenticatable
include PolicyActor
include Gitlab::Utils::StrongMemoize
- include IgnorableColumns
-
- ignore_column :token, remove_with: '15.2', remove_after: '2022-07-22'
add_authentication_token_field :token, encrypted: :required
diff --git a/app/models/deployment.rb b/app/models/deployment.rb
index fc0dd7e00c7..c25ba6f9268 100644
--- a/app/models/deployment.rb
+++ b/app/models/deployment.rb
@@ -108,13 +108,9 @@ class Deployment < ApplicationRecord
end
end
- after_transition any => :running do |deployment|
+ after_transition any => :running do |deployment, transition|
deployment.run_after_commit do
- if Feature.enabled?(:deployment_hooks_skip_worker, deployment.project)
- deployment.execute_hooks(Time.current)
- else
- Deployments::HooksWorker.perform_async(deployment_id: id, status_changed_at: Time.current)
- end
+ Deployments::HooksWorker.perform_async(deployment_id: id, status: transition.to, status_changed_at: Time.current)
end
end
@@ -126,13 +122,9 @@ class Deployment < ApplicationRecord
end
end
- after_transition any => FINISHED_STATUSES do |deployment|
+ after_transition any => FINISHED_STATUSES do |deployment, transition|
deployment.run_after_commit do
- if Feature.enabled?(:deployment_hooks_skip_worker, deployment.project)
- deployment.execute_hooks(Time.current)
- else
- Deployments::HooksWorker.perform_async(deployment_id: id, status_changed_at: Time.current)
- end
+ Deployments::HooksWorker.perform_async(deployment_id: id, status: transition.to, status_changed_at: Time.current)
end
end
@@ -193,7 +185,7 @@ class Deployment < ApplicationRecord
def self.last_deployment_group_for_environment(env)
return self.none unless env.last_deployment_pipeline&.latest_successful_builds&.present?
- BatchLoader.for(env).batch do |environments, loader|
+ BatchLoader.for(env).batch(default_value: self.none) do |environments, loader|
latest_successful_build_ids = []
environments_hash = {}
@@ -269,8 +261,8 @@ class Deployment < ApplicationRecord
Commit.truncate_sha(sha)
end
- def execute_hooks(status_changed_at)
- deployment_data = Gitlab::DataBuilder::Deployment.build(self, status_changed_at)
+ def execute_hooks(status, status_changed_at)
+ deployment_data = Gitlab::DataBuilder::Deployment.build(self, status, status_changed_at)
project.execute_hooks(deployment_data, :deployment_hooks)
project.execute_integrations(deployment_data, :deployment_hooks)
end
diff --git a/app/models/environment.rb b/app/models/environment.rb
index da6ab5ed077..68540ce0f5c 100644
--- a/app/models/environment.rb
+++ b/app/models/environment.rb
@@ -451,13 +451,11 @@ class Environment < ApplicationRecord
def auto_stop_in=(value)
return unless value
- return unless parsed_result = ChronicDuration.parse(value)
- self.auto_stop_at = parsed_result.seconds.from_now
- end
+ parser = ::Gitlab::Ci::Build::DurationParser.new(value)
+ return if parser.seconds_from_now.nil?
- def elastic_stack_available?
- !!deployment_platform&.cluster&.elastic_stack_available?
+ self.auto_stop_at = parser.seconds_from_now
end
def rollout_status
diff --git a/app/models/error_tracking/client_key.rb b/app/models/error_tracking/client_key.rb
index bbc57573aa9..d58a183f223 100644
--- a/app/models/error_tracking/client_key.rb
+++ b/app/models/error_tracking/client_key.rb
@@ -16,7 +16,7 @@ class ErrorTracking::ClientKey < ApplicationRecord
end
def sentry_dsn
- @sentry_dsn ||= ErrorTracking::Collector::Dsn.build_url(public_key, project_id)
+ @sentry_dsn ||= ::Gitlab::ErrorTracking::ErrorRepository.build(project).dsn_url(public_key)
end
private
diff --git a/app/models/group.rb b/app/models/group.rb
index f5aad6e74ff..6d8f8bd7613 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -112,6 +112,8 @@ class Group < Namespace
has_many :dependency_proxy_blobs, class_name: 'DependencyProxy::Blob'
has_many :dependency_proxy_manifests, class_name: 'DependencyProxy::Manifest'
+ has_one :harbor_integration, class_name: 'Integrations::Harbor'
+
# debian_distributions and associated component_files must be destroyed by ruby code in order to properly remove carrierwave uploads
has_many :debian_distributions, class_name: 'Packages::Debian::GroupDistribution', dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
@@ -361,8 +363,8 @@ class Group < Namespace
owners.include?(user)
end
- def add_users(users, access_level, current_user: nil, expires_at: nil, tasks_to_be_done: [], tasks_project_id: nil)
- Members::Groups::CreatorService.add_users( # rubocop:disable CodeReuse/ServiceClass
+ def add_members(users, access_level, current_user: nil, expires_at: nil, tasks_to_be_done: [], tasks_project_id: nil)
+ Members::Groups::CreatorService.add_members( # rubocop:disable CodeReuse/ServiceClass
self,
users,
access_level,
@@ -373,8 +375,8 @@ class Group < Namespace
)
end
- def add_user(user, access_level, current_user: nil, expires_at: nil, ldap: false, blocking_refresh: true)
- Members::Groups::CreatorService.add_user( # rubocop:disable CodeReuse/ServiceClass
+ def add_member(user, access_level, current_user: nil, expires_at: nil, ldap: false, blocking_refresh: true)
+ Members::Groups::CreatorService.add_member( # rubocop:disable CodeReuse/ServiceClass
self,
user,
access_level,
@@ -386,23 +388,23 @@ class Group < Namespace
end
def add_guest(user, current_user = nil)
- add_user(user, :guest, current_user: current_user)
+ add_member(user, :guest, current_user: current_user)
end
def add_reporter(user, current_user = nil)
- add_user(user, :reporter, current_user: current_user)
+ add_member(user, :reporter, current_user: current_user)
end
def add_developer(user, current_user = nil)
- add_user(user, :developer, current_user: current_user)
+ add_member(user, :developer, current_user: current_user)
end
def add_maintainer(user, current_user = nil)
- add_user(user, :maintainer, current_user: current_user)
+ add_member(user, :maintainer, current_user: current_user)
end
def add_owner(user, current_user = nil)
- add_user(user, :owner, current_user: current_user)
+ add_member(user, :owner, current_user: current_user)
end
def member?(user, min_access_level = Gitlab::Access::GUEST)
diff --git a/app/models/hooks/project_hook.rb b/app/models/hooks/project_hook.rb
index b7ace34141e..bcbf43ee38b 100644
--- a/app/models/hooks/project_hook.rb
+++ b/app/models/hooks/project_hook.rb
@@ -27,6 +27,8 @@ class ProjectHook < WebHook
belongs_to :project
validates :project, presence: true
+ scope :for_projects, ->(project) { where(project: project) }
+
def pluralized_name
_('Webhooks')
end
@@ -41,6 +43,19 @@ class ProjectHook < WebHook
project
end
+ override :update_last_failure
+ def update_last_failure
+ return if executable?
+
+ key = "web_hooks:last_failure:project-#{project_id}"
+ time = Time.current.utc.iso8601
+
+ Gitlab::Redis::SharedState.with do |redis|
+ prev = redis.get(key)
+ redis.set(key, time) if !prev || prev < time
+ end
+ end
+
private
override :web_hooks_disable_failed?
diff --git a/app/models/hooks/system_hook.rb b/app/models/hooks/system_hook.rb
index c8a0cc05912..c0073f9a9b8 100644
--- a/app/models/hooks/system_hook.rb
+++ b/app/models/hooks/system_hook.rb
@@ -26,6 +26,6 @@ class SystemHook < WebHook
end
def help_path
- 'system_hooks/system_hooks'
+ 'administration/system_hooks'
end
end
diff --git a/app/models/hooks/web_hook.rb b/app/models/hooks/web_hook.rb
index 37fd612e652..f428d07cd7f 100644
--- a/app/models/hooks/web_hook.rb
+++ b/app/models/hooks/web_hook.rb
@@ -3,6 +3,8 @@
class WebHook < ApplicationRecord
include Sortable
+ InterpolationError = Class.new(StandardError)
+
MAX_FAILURES = 100
FAILURE_THRESHOLD = 3 # three strikes
INITIAL_BACKOFF = 10.minutes
@@ -36,6 +38,7 @@ class WebHook < ApplicationRecord
validates :token, format: { without: /\n/ }
validates :push_events_branch_filter, branch_filter: true
validates :url_variables, json_schema: { filename: 'web_hooks_url_variables' }
+ validate :no_missing_url_variables
after_initialize :initialize_url_variables
@@ -45,6 +48,11 @@ class WebHook < ApplicationRecord
where('recent_failures <= ? AND (disabled_until IS NULL OR disabled_until < ?)', FAILURE_THRESHOLD, Time.current)
end
+ # Inverse of executable
+ scope :disabled, -> do
+ where('recent_failures > ? OR disabled_until >= ?', FAILURE_THRESHOLD, Time.current)
+ end
+
def executable?
!temporarily_disabled? && !permanently_disabled?
end
@@ -164,6 +172,24 @@ class WebHook < ApplicationRecord
super(options)
end
+ # See app/validators/json_schemas/web_hooks_url_variables.json
+ VARIABLE_REFERENCE_RE = /\{([A-Za-z_][A-Za-z0-9_]+)\}/.freeze
+
+ def interpolated_url
+ return url unless url.include?('{')
+
+ vars = url_variables
+ url.gsub(VARIABLE_REFERENCE_RE) do
+ vars.fetch(_1.delete_prefix('{').delete_suffix('}'))
+ end
+ rescue KeyError => e
+ raise InterpolationError, "Invalid URL template. Missing key #{e.key}"
+ end
+
+ def update_last_failure
+ # Overridden in child classes.
+ end
+
private
def web_hooks_disable_failed?
@@ -177,4 +203,17 @@ class WebHook < ApplicationRecord
def rate_limiter
@rate_limiter ||= Gitlab::WebHooks::RateLimiter.new(self)
end
+
+ def no_missing_url_variables
+ return if url.nil?
+
+ variable_names = url_variables.keys
+ used_variables = url.scan(VARIABLE_REFERENCE_RE).map(&:first)
+
+ missing = used_variables - variable_names
+
+ return if missing.empty?
+
+ errors.add(:url, "Invalid URL template. Missing keys: #{missing}")
+ end
end
diff --git a/app/models/incident_management/issuable_escalation_status.rb b/app/models/incident_management/issuable_escalation_status.rb
index fc881e62efd..3c581f0489a 100644
--- a/app/models/incident_management/issuable_escalation_status.rb
+++ b/app/models/incident_management/issuable_escalation_status.rb
@@ -7,7 +7,7 @@ module IncidentManagement
self.table_name = 'incident_management_issuable_escalation_statuses'
belongs_to :issue
- has_one :project, through: :issue, inverse_of: :incident_management_issuable_escalation_status
+ has_one :project, through: :issue, inverse_of: :incident_management_issuable_escalation_statuses
validates :issue, presence: true, uniqueness: true
diff --git a/app/models/integration.rb b/app/models/integration.rb
index 726e95b7cbf..f5f701662e7 100644
--- a/app/models/integration.rb
+++ b/app/models/integration.rb
@@ -13,8 +13,6 @@ class Integration < ApplicationRecord
include IgnorableColumns
extend ::Gitlab::Utils::Override
- ignore_column :properties, remove_with: '15.1', remove_after: '2022-05-22'
-
UnknownType = Class.new(StandardError)
self.inheritance_column = :type_new
@@ -154,6 +152,8 @@ class Integration < ApplicationRecord
else
raise ArgumentError, "Unknown field storage: #{storage}"
end
+
+ boolean_accessor(name) if attrs[:type] == 'checkbox'
end
# :nocov:
@@ -200,14 +200,21 @@ class Integration < ApplicationRecord
# Provide convenient boolean accessor methods for each serialized property.
# Also keep track of updated properties in a similar way as ActiveModel::Dirty
def self.boolean_accessor(*args)
- prop_accessor(*args)
-
args.each do |arg|
+ # TODO: Allow legacy usage of `.boolean_accessor`, once all integrations
+ # are converted to the field DSL we can remove this and only call
+ # `.boolean_accessor` through `.field`.
+ #
+ # See https://gitlab.com/groups/gitlab-org/-/epics/7652
+ prop_accessor(arg) unless method_defined?(arg)
+
class_eval <<~RUBY, __FILE__, __LINE__ + 1
- def #{arg}
- return if properties.blank?
+ # Make the original getter available as a private method.
+ alias_method :#{arg}_before_type_cast, :#{arg}
+ private(:#{arg}_before_type_cast)
- Gitlab::Utils.to_boolean(properties['#{arg}'])
+ def #{arg}
+ Gitlab::Utils.to_boolean(#{arg}_before_type_cast)
end
def #{arg}?
@@ -494,16 +501,12 @@ class Integration < ApplicationRecord
self.class.event_names
end
- def event_field(event)
- nil
- end
-
def api_field_names
fields.reject { _1[:type] == 'password' }.pluck(:name)
end
- def global_fields
- fields
+ def form_fields
+ fields.reject { _1[:api_only] == true }
end
def configurable_events
@@ -574,11 +577,7 @@ class Integration < ApplicationRecord
def async_execute(data)
return unless supported_events.include?(data[:object_kind])
- if Feature.enabled?(:rename_integrations_workers)
- Integrations::ExecuteWorker.perform_async(id, data)
- else
- ProjectServiceWorker.perform_async(id, data)
- end
+ Integrations::ExecuteWorker.perform_async(id, data)
end
# override if needed
diff --git a/app/models/integrations/asana.rb b/app/models/integrations/asana.rb
index d25bf8b1b1e..2cfd71c9eb2 100644
--- a/app/models/integrations/asana.rb
+++ b/app/models/integrations/asana.rb
@@ -4,9 +4,22 @@ require 'asana'
module Integrations
class Asana < Integration
- prop_accessor :api_key, :restrict_to_branch
validates :api_key, presence: true, if: :activated?
+ field :api_key,
+ type: 'password',
+ title: 'API key',
+ help: -> { s_('AsanaService|User Personal Access Token. User must have access to the task. All comments are attributed to this user.') },
+ non_empty_password_title: -> { s_('ProjectService|Enter new API key') },
+ non_empty_password_help: -> { s_('ProjectService|Leave blank to use your current API key.') },
+ # Example Personal Access Token from Asana docs
+ placeholder: '0/68a9e79b868c6789e79a124c30b0',
+ required: true
+
+ field :restrict_to_branch,
+ title: -> { s_('Integrations|Restrict to branch (optional)') },
+ help: -> { s_('AsanaService|Comma-separated list of branches to be automatically inspected. Leave blank to include all branches.') }
+
def title
'Asana'
end
@@ -24,28 +37,6 @@ module Integrations
'asana'
end
- def fields
- [
- {
- type: 'password',
- name: 'api_key',
- title: 'API key',
- help: s_('AsanaService|User Personal Access Token. User must have access to the task. All comments are attributed to this user.'),
- non_empty_password_title: s_('ProjectService|Enter new API key'),
- non_empty_password_help: s_('ProjectService|Leave blank to use your current API key.'),
- # Example Personal Access Token from Asana docs
- placeholder: '0/68a9e79b868c6789e79a124c30b0',
- required: true
- },
- {
- type: 'text',
- name: 'restrict_to_branch',
- title: 'Restrict to branch (optional)',
- help: s_('AsanaService|Comma-separated list of branches to be automatically inspected. Leave blank to include all branches.')
- }
- ]
- end
-
def self.supported_events
%w(push)
end
diff --git a/app/models/integrations/assembla.rb b/app/models/integrations/assembla.rb
index ccd24c1fb2c..88dbf2915ef 100644
--- a/app/models/integrations/assembla.rb
+++ b/app/models/integrations/assembla.rb
@@ -2,9 +2,18 @@
module Integrations
class Assembla < Integration
- prop_accessor :token, :subdomain
validates :token, presence: true, if: :activated?
+ field :token,
+ type: 'password',
+ non_empty_password_title: -> { s_('ProjectService|Enter new token') },
+ non_empty_password_help: -> { s_('ProjectService|Leave blank to use your current token.') },
+ placeholder: '',
+ required: true
+
+ field :subdomain,
+ placeholder: ''
+
def title
'Assembla'
end
@@ -17,24 +26,6 @@ module Integrations
'assembla'
end
- def fields
- [
- {
- type: 'password',
- name: 'token',
- non_empty_password_title: s_('ProjectService|Enter new token'),
- non_empty_password_help: s_('ProjectService|Leave blank to use your current token.'),
- placeholder: '',
- required: true
- },
- {
- type: 'text',
- name: 'subdomain',
- placeholder: ''
- }
- ]
- end
-
def self.supported_events
%w(push)
end
diff --git a/app/models/integrations/bamboo.rb b/app/models/integrations/bamboo.rb
index 4e30c1ccc69..230dc6bb336 100644
--- a/app/models/integrations/bamboo.rb
+++ b/app/models/integrations/bamboo.rb
@@ -155,7 +155,6 @@ module Integrations
query_params[:os_authType] = 'basic'
params[:basic_auth] = basic_auth
- params[:use_read_total_timeout] = true
params
end
diff --git a/app/models/integrations/base_chat_notification.rb b/app/models/integrations/base_chat_notification.rb
index 33d4eecbf49..c7992e4083c 100644
--- a/app/models/integrations/base_chat_notification.rb
+++ b/app/models/integrations/base_chat_notification.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-# Base class for Chat notifications services
+# Base class for Chat notifications integrations
# This class is not meant to be used directly, but only to inherit from.
module Integrations
@@ -46,7 +46,7 @@ module Integrations
# `notify_only_default_branch`. Now we have a string property named
# `branches_to_be_notified`. Instead of doing a background migration, we
# opted to set a value for the new property based on the old one, if
- # users haven't specified one already. When users edit the service and
+ # users haven't specified one already. When users edit the integration and
# select a value for this new property, it will override everything.
self.branches_to_be_notified ||= notify_only_default_branch? ? "default" : "all"
@@ -78,7 +78,7 @@ module Integrations
type: 'select',
name: 'branches_to_be_notified',
title: s_('Integrations|Branches for which notifications are to be sent'),
- choices: branch_choices
+ choices: self.class.branch_choices
}.freeze,
{
type: 'text',
@@ -118,7 +118,7 @@ module Integrations
event_type = data[:event_type] || object_kind
- channel_names = get_channel_field(event_type).presence || channel.presence
+ channel_names = event_channel_value(event_type).presence || channel.presence
channels = channel_names&.split(',')&.map(&:strip)
opts = {}
@@ -134,15 +134,13 @@ module Integrations
end
def event_channel_names
- supported_events.map { |event| event_channel_name(event) }
- end
+ return [] unless configurable_channels?
- def event_field(event)
- fields.find { |field| field[:name] == event_channel_name(event) }
+ supported_events.map { |event| event_channel_name(event) }
end
- def global_fields
- fields.reject { |field| field[:name].end_with?('channel') }
+ def form_fields
+ super.reject { |field| field[:name].end_with?('channel') }
end
def default_channel_placeholder
@@ -153,6 +151,21 @@ module Integrations
raise NotImplementedError
end
+ # With some integrations the webhook is already tied to a specific channel,
+ # for others the channels are configurable for each event.
+ def configurable_channels?
+ false
+ end
+
+ def event_channel_name(event)
+ EVENT_CHANNEL[event]
+ end
+
+ def event_channel_value(event)
+ field_name = event_channel_name(event)
+ self.public_send(field_name) # rubocop:disable GitlabSecurity/PublicSend
+ end
+
private
def log_usage(_, _)
@@ -213,21 +226,12 @@ module Integrations
end
end
- def get_channel_field(event)
- field_name = event_channel_name(event)
- self.public_send(field_name) # rubocop:disable GitlabSecurity/PublicSend
- end
-
def build_event_channels
- supported_events.reduce([]) do |channels, event|
- channels << { type: 'text', name: event_channel_name(event), placeholder: default_channel_placeholder }
+ event_channel_names.map do |channel_field|
+ { type: 'text', name: channel_field, placeholder: default_channel_placeholder }
end
end
- def event_channel_name(event)
- EVENT_CHANNEL[event]
- end
-
def project_name
project.full_name
end
diff --git a/app/models/integrations/base_issue_tracker.rb b/app/models/integrations/base_issue_tracker.rb
index bffe87c21ee..fe4a2f43b13 100644
--- a/app/models/integrations/base_issue_tracker.rb
+++ b/app/models/integrations/base_issue_tracker.rb
@@ -94,7 +94,7 @@ module Integrations
result = false
begin
- response = Gitlab::HTTP.head(self.project_url, verify: true, use_read_total_timeout: true)
+ response = Gitlab::HTTP.head(self.project_url, verify: true)
if response
message = "#{self.type} received response #{response.code} when attempting to connect to #{self.project_url}"
diff --git a/app/models/integrations/campfire.rb b/app/models/integrations/campfire.rb
index 7889cd8f9a9..bf1358ac0f6 100644
--- a/app/models/integrations/campfire.rb
+++ b/app/models/integrations/campfire.rb
@@ -2,9 +2,34 @@
module Integrations
class Campfire < Integration
- prop_accessor :token, :subdomain, :room
validates :token, presence: true, if: :activated?
+ field :token,
+ type: 'password',
+ title: -> { _('Campfire token') },
+ help: -> { s_('CampfireService|API authentication token from Campfire.') },
+ non_empty_password_title: -> { s_('ProjectService|Enter new token') },
+ non_empty_password_help: -> { s_('ProjectService|Leave blank to use your current token.') },
+ placeholder: '',
+ required: true
+
+ field :subdomain,
+ title: -> { _('Campfire subdomain (optional)') },
+ placeholder: '',
+ help: -> do
+ ERB::Util.html_escape(
+ s_('CampfireService|The %{code_open}.campfirenow.com%{code_close} subdomain.')
+ ) % {
+ code_open: '<code>'.html_safe,
+ code_close: '</code>'.html_safe
+ }
+ end
+
+ field :room,
+ title: -> { _('Campfire room ID (optional)') },
+ placeholder: '123456',
+ help: -> { s_('CampfireService|From the end of the room URL.') }
+
def title
'Campfire'
end
@@ -15,42 +40,18 @@ module Integrations
def help
docs_link = ActionController::Base.helpers.link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('api/services', anchor: 'campfire'), target: '_blank', rel: 'noopener noreferrer'
- s_('CampfireService|Send notifications about push events to Campfire chat rooms. %{docs_link}').html_safe % { docs_link: docs_link.html_safe }
+
+ ERB::Util.html_escape(
+ s_('CampfireService|Send notifications about push events to Campfire chat rooms. %{docs_link}')
+ ) % {
+ docs_link: docs_link.html_safe
+ }
end
def self.to_param
'campfire'
end
- def fields
- [
- {
- type: 'password',
- name: 'token',
- title: _('Campfire token'),
- help: s_('CampfireService|API authentication token from Campfire.'),
- non_empty_password_title: s_('ProjectService|Enter new token'),
- non_empty_password_help: s_('ProjectService|Leave blank to use your current token.'),
- placeholder: '',
- required: true
- },
- {
- type: 'text',
- name: 'subdomain',
- title: _('Campfire subdomain (optional)'),
- placeholder: '',
- help: s_('CampfireService|The %{code_open}.campfirenow.com%{code_close} subdomain.') % { code_open: '<code>'.html_safe, code_close: '</code>'.html_safe }
- },
- {
- type: 'text',
- name: 'room',
- title: _('Campfire room ID (optional)'),
- placeholder: '123456',
- help: s_('CampfireService|From the end of the room URL.')
- }
- ]
- end
-
def self.supported_events
%w(push)
end
diff --git a/app/models/integrations/confluence.rb b/app/models/integrations/confluence.rb
index 4e1d1993d02..c1c43af99bf 100644
--- a/app/models/integrations/confluence.rb
+++ b/app/models/integrations/confluence.rb
@@ -6,11 +6,14 @@ module Integrations
VALID_HOST_MATCH = %r{\A.+\.atlassian\.net\Z}.freeze
VALID_PATH_MATCH = %r{\A/wiki(/|\Z)}.freeze
- prop_accessor :confluence_url
-
validates :confluence_url, presence: true, if: :activated?
validate :validate_confluence_url_is_cloud, if: :activated?
+ field :confluence_url,
+ title: -> { s_('Confluence Cloud Workspace URL') },
+ placeholder: 'https://example.atlassian.net/wiki',
+ required: true
+
def self.to_param
'confluence'
end
@@ -38,18 +41,6 @@ module Integrations
end
end
- def fields
- [
- {
- type: 'text',
- name: 'confluence_url',
- title: s_('Confluence Cloud Workspace URL'),
- placeholder: 'https://example.atlassian.net/wiki',
- required: true
- }
- ]
- end
-
def testable?
false
end
diff --git a/app/models/integrations/datadog.rb b/app/models/integrations/datadog.rb
index bb0fb6b9079..97e586c0662 100644
--- a/app/models/integrations/datadog.rb
+++ b/app/models/integrations/datadog.rb
@@ -15,7 +15,75 @@ module Integrations
TAG_KEY_VALUE_RE = %r{\A [\w-]+ : .*\S.* \z}x.freeze
- prop_accessor :datadog_site, :api_url, :api_key, :datadog_service, :datadog_env, :datadog_tags
+ field :datadog_site,
+ placeholder: DEFAULT_DOMAIN,
+ help: -> do
+ ERB::Util.html_escape(
+ s_('DatadogIntegration|The Datadog site to send data to. To send data to the EU site, use %{codeOpen}datadoghq.eu%{codeClose}.')
+ ) % {
+ codeOpen: '<code>'.html_safe,
+ codeClose: '</code>'.html_safe
+ }
+ end
+
+ field :api_url,
+ title: -> { s_('DatadogIntegration|API URL') },
+ help: -> { s_('DatadogIntegration|(Advanced) The full URL for your Datadog site.') }
+
+ field :api_key,
+ type: 'password',
+ title: -> { _('API key') },
+ non_empty_password_title: -> { s_('ProjectService|Enter new API key') },
+ non_empty_password_help: -> { s_('ProjectService|Leave blank to use your current API key') },
+ help: -> do
+ ERB::Util.html_escape(
+ s_('DatadogIntegration|%{linkOpen}API key%{linkClose} used for authentication with Datadog.')
+ ) % {
+ linkOpen: %Q{<a href="#{URL_API_KEYS_DOCS}" target="_blank" rel="noopener noreferrer">}.html_safe,
+ linkClose: '</a>'.html_safe
+ }
+ end,
+ required: true
+
+ field :archive_trace_events,
+ type: 'checkbox',
+ title: -> { s_('Logs') },
+ checkbox_label: -> { s_('Enable logs collection') },
+ help: -> { s_('When enabled, job logs are collected by Datadog and displayed along with pipeline execution traces.') }
+
+ field :datadog_service,
+ title: -> { s_('DatadogIntegration|Service') },
+ placeholder: 'gitlab-ci',
+ help: -> { s_('DatadogIntegration|Tag all data from this GitLab instance in Datadog. Useful when managing several self-managed deployments.') }
+
+ field :datadog_env,
+ title: -> { s_('DatadogIntegration|Environment') },
+ placeholder: 'ci',
+ help: -> do
+ ERB::Util.html_escape(
+ s_('DatadogIntegration|For self-managed deployments, set the %{codeOpen}env%{codeClose} tag for all the data sent to Datadog. %{linkOpen}How do I use tags?%{linkClose}')
+ ) % {
+ codeOpen: '<code>'.html_safe,
+ codeClose: '</code>'.html_safe,
+ linkOpen: '<a href="https://docs.datadoghq.com/getting_started/tagging/#using-tags" target="_blank" rel="noopener noreferrer">'.html_safe,
+ linkClose: '</a>'.html_safe
+ }
+ end
+
+ field :datadog_tags,
+ type: 'textarea',
+ title: -> { s_('DatadogIntegration|Tags') },
+ placeholder: "tag:value\nanother_tag:value",
+ help: -> do
+ ERB::Util.html_escape(
+ s_('DatadogIntegration|Custom tags in Datadog. Enter one tag per line in the %{codeOpen}key:value%{codeClose} format. %{linkOpen}How do I use tags?%{linkClose}')
+ ) % {
+ codeOpen: '<code>'.html_safe,
+ codeClose: '</code>'.html_safe,
+ linkOpen: '<a href="https://docs.datadoghq.com/getting_started/tagging/#using-tags" target="_blank" rel="noopener noreferrer">'.html_safe,
+ linkClose: '</a>'.html_safe
+ }
+ end
before_validation :strip_properties
@@ -77,92 +145,11 @@ module Integrations
end
def fields
- f = [
- {
- type: 'text',
- name: 'datadog_site',
- placeholder: DEFAULT_DOMAIN,
- help: ERB::Util.html_escape(
- s_('DatadogIntegration|The Datadog site to send data to. To send data to the EU site, use %{codeOpen}datadoghq.eu%{codeClose}.')
- ) % {
- codeOpen: '<code>'.html_safe,
- codeClose: '</code>'.html_safe
- },
- required: false
- },
- {
- type: 'text',
- name: 'api_url',
- title: s_('DatadogIntegration|API URL'),
- help: s_('DatadogIntegration|(Advanced) The full URL for your Datadog site.'),
- required: false
- },
- {
- type: 'password',
- name: 'api_key',
- title: _('API key'),
- non_empty_password_title: s_('ProjectService|Enter new API key'),
- non_empty_password_help: s_('ProjectService|Leave blank to use your current API key'),
- help: ERB::Util.html_escape(
- s_('DatadogIntegration|%{linkOpen}API key%{linkClose} used for authentication with Datadog.')
- ) % {
- linkOpen: %Q{<a href="#{URL_API_KEYS_DOCS}" target="_blank" rel="noopener noreferrer">}.html_safe,
- linkClose: '</a>'.html_safe
- },
- required: true
- }
- ]
-
if Feature.enabled?(:datadog_integration_logs_collection, parent)
- f.append({
- type: 'checkbox',
- name: 'archive_trace_events',
- title: s_('Logs'),
- checkbox_label: s_('Enable logs collection'),
- help: s_('When enabled, job logs are collected by Datadog and displayed along with pipeline execution traces.'),
- required: false
- })
+ super
+ else
+ super.reject { _1.name == 'archive_trace_events' }
end
-
- f += [
- {
- type: 'text',
- name: 'datadog_service',
- title: s_('DatadogIntegration|Service'),
- placeholder: 'gitlab-ci',
- help: s_('DatadogIntegration|Tag all data from this GitLab instance in Datadog. Useful when managing several self-managed deployments.')
- },
- {
- type: 'text',
- name: 'datadog_env',
- title: s_('DatadogIntegration|Environment'),
- placeholder: 'ci',
- help: ERB::Util.html_escape(
- s_('DatadogIntegration|For self-managed deployments, set the %{codeOpen}env%{codeClose} tag for all the data sent to Datadog. %{linkOpen}How do I use tags?%{linkClose}')
- ) % {
- codeOpen: '<code>'.html_safe,
- codeClose: '</code>'.html_safe,
- linkOpen: '<a href="https://docs.datadoghq.com/getting_started/tagging/#using-tags" target="_blank" rel="noopener noreferrer">'.html_safe,
- linkClose: '</a>'.html_safe
- }
- },
- {
- type: 'textarea',
- name: 'datadog_tags',
- title: s_('DatadogIntegration|Tags'),
- placeholder: "tag:value\nanother_tag:value",
- help: ERB::Util.html_escape(
- s_('DatadogIntegration|Custom tags in Datadog. Enter one tag per line in the %{codeOpen}key:value%{codeClose} format. %{linkOpen}How do I use tags?%{linkClose}')
- ) % {
- codeOpen: '<code>'.html_safe,
- codeClose: '</code>'.html_safe,
- linkOpen: '<a href="https://docs.datadoghq.com/getting_started/tagging/#using-tags" target="_blank" rel="noopener noreferrer">'.html_safe,
- linkClose: '</a>'.html_safe
- }
- }
- ]
-
- f
end
override :hook_url
diff --git a/app/models/integrations/discord.rb b/app/models/integrations/discord.rb
index 790e41e5a2a..ecabf23c90b 100644
--- a/app/models/integrations/discord.rb
+++ b/app/models/integrations/discord.rb
@@ -23,10 +23,6 @@ module Integrations
s_('Send notifications about project events to a Discord channel. %{docs_link}').html_safe % { docs_link: docs_link.html_safe }
end
- def event_field(event)
- # No-op.
- end
-
def default_channel_placeholder
# No-op.
end
@@ -43,7 +39,7 @@ module Integrations
type: 'select',
name: 'branches_to_be_notified',
title: s_('Integrations|Branches for which notifications are to be sent'),
- choices: branch_choices
+ choices: self.class.branch_choices
}
]
end
diff --git a/app/models/integrations/drone_ci.rb b/app/models/integrations/drone_ci.rb
index 35524503dea..b1f72b7144e 100644
--- a/app/models/integrations/drone_ci.rb
+++ b/app/models/integrations/drone_ci.rb
@@ -60,8 +60,7 @@ module Integrations
response = Gitlab::HTTP.try_get(
commit_status_path(sha, ref),
verify: enable_ssl_verification,
- extra_log_info: { project_id: project_id },
- use_read_total_timeout: true
+ extra_log_info: { project_id: project_id }
)
status =
diff --git a/app/models/integrations/emails_on_push.rb b/app/models/integrations/emails_on_push.rb
index ab458bb2c27..ed12a3a8d63 100644
--- a/app/models/integrations/emails_on_push.rb
+++ b/app/models/integrations/emails_on_push.rb
@@ -6,12 +6,35 @@ module Integrations
RECIPIENTS_LIMIT = 750
- boolean_accessor :send_from_committer_email
- boolean_accessor :disable_diffs
- prop_accessor :recipients, :branches_to_be_notified
validates :recipients, presence: true, if: :validate_recipients?
validate :number_of_recipients_within_limit, if: :validate_recipients?
+ field :send_from_committer_email,
+ type: 'checkbox',
+ title: -> { s_("EmailsOnPushService|Send from committer") },
+ help: -> do
+ @help ||= begin
+ domains = Notify.allowed_email_domains.map { |domain| "user@#{domain}" }.join(", ")
+
+ s_("EmailsOnPushService|Send notifications from the committer's email address if the domain matches the domain used by your GitLab instance (such as %{domains}).") % { domains: domains }
+ end
+ end
+
+ field :disable_diffs,
+ type: 'checkbox',
+ title: -> { s_("EmailsOnPushService|Disable code diffs") },
+ help: -> { s_("EmailsOnPushService|Don't include possibly sensitive code diffs in notification body.") }
+
+ field :branches_to_be_notified,
+ type: 'select',
+ title: -> { s_('Integrations|Branches for which notifications are to be sent') },
+ choices: branch_choices
+
+ field :recipients,
+ type: 'textarea',
+ placeholder: -> { s_('EmailsOnPushService|tanuki@example.com gitlab@example.com') },
+ help: -> { s_('EmailsOnPushService|Emails separated by whitespace.') }
+
def self.valid_recipients(recipients)
recipients.split.grep(Devise.email_regexp).uniq(&:downcase)
end
@@ -67,28 +90,6 @@ module Integrations
Gitlab::Utils.to_boolean(self.disable_diffs)
end
- def fields
- domains = Notify.allowed_email_domains.map { |domain| "user@#{domain}" }.join(", ")
- [
- { type: 'checkbox', name: 'send_from_committer_email', title: s_("EmailsOnPushService|Send from committer"),
- help: s_("EmailsOnPushService|Send notifications from the committer's email address if the domain matches the domain used by your GitLab instance (such as %{domains}).") % { domains: domains } },
- { type: 'checkbox', name: 'disable_diffs', title: s_("EmailsOnPushService|Disable code diffs"),
- help: s_("EmailsOnPushService|Don't include possibly sensitive code diffs in notification body.") },
- {
- type: 'select',
- name: 'branches_to_be_notified',
- title: s_('Integrations|Branches for which notifications are to be sent'),
- choices: branch_choices
- },
- {
- type: 'textarea',
- name: 'recipients',
- placeholder: s_('EmailsOnPushService|tanuki@example.com gitlab@example.com'),
- help: s_('EmailsOnPushService|Emails separated by whitespace.')
- }
- ]
- end
-
private
def number_of_recipients_within_limit
diff --git a/app/models/integrations/external_wiki.rb b/app/models/integrations/external_wiki.rb
index 18c48411e30..bc2ea193a84 100644
--- a/app/models/integrations/external_wiki.rb
+++ b/app/models/integrations/external_wiki.rb
@@ -2,9 +2,14 @@
module Integrations
class ExternalWiki < Integration
- prop_accessor :external_wiki_url
validates :external_wiki_url, presence: true, public_url: true, if: :activated?
+ field :external_wiki_url,
+ title: -> { s_('ExternalWikiService|External wiki URL') },
+ placeholder: -> { s_('ExternalWikiService|https://example.com/xxx/wiki/...') },
+ help: -> { s_('ExternalWikiService|Enter the URL to the external wiki.') },
+ required: true
+
def title
s_('ExternalWikiService|External wiki')
end
@@ -17,19 +22,6 @@ module Integrations
'external_wiki'
end
- def fields
- [
- {
- type: 'text',
- name: 'external_wiki_url',
- title: s_('ExternalWikiService|External wiki URL'),
- placeholder: s_('ExternalWikiService|https://example.com/xxx/wiki/...'),
- help: 'Enter the URL to the external wiki.',
- required: true
- }
- ]
- end
-
def help
docs_link = ActionController::Base.helpers.link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/wiki/index', anchor: 'link-an-external-wiki'), target: '_blank', rel: 'noopener noreferrer'
@@ -37,7 +29,7 @@ module Integrations
end
def execute(_data)
- response = Gitlab::HTTP.get(properties['external_wiki_url'], verify: true, use_read_total_timeout: true)
+ response = Gitlab::HTTP.get(properties['external_wiki_url'], verify: true)
response.body if response.code == 200
rescue StandardError
nil
diff --git a/app/models/integrations/field.rb b/app/models/integrations/field.rb
index cbda418755b..53c8f5f623e 100644
--- a/app/models/integrations/field.rb
+++ b/app/models/integrations/field.rb
@@ -4,14 +4,16 @@ module Integrations
class Field
SECRET_NAME = %r/token|key|password|passphrase|secret/.freeze
+ BOOLEAN_ATTRIBUTES = %i[required api_only exposes_secrets].freeze
+
ATTRIBUTES = %i[
- section type placeholder required choices value checkbox_label
+ section type placeholder choices value checkbox_label
title help
non_empty_password_help
non_empty_password_title
- api_only
- exposes_secrets
- ].freeze
+ ].concat(BOOLEAN_ATTRIBUTES).freeze
+
+ TYPES = %w[text textarea password checkbox select].freeze
attr_reader :name, :integration_class
@@ -22,6 +24,13 @@ module Integrations
attributes[:type] = SECRET_NAME.match?(@name) ? 'password' : type
attributes[:api_only] = api_only
@attributes = attributes.freeze
+
+ invalid_attributes = attributes.keys - ATTRIBUTES
+ if invalid_attributes.present?
+ raise ArgumentError, "Invalid attributes #{invalid_attributes.inspect}"
+ elsif !TYPES.include?(self[:type])
+ raise ArgumentError, "Invalid type #{self[:type].inspect}"
+ end
end
def [](key)
@@ -34,11 +43,19 @@ module Integrations
end
def secret?
- @attributes[:type] == 'password'
+ self[:type] == 'password'
end
ATTRIBUTES.each do |name|
define_method(name) { self[name] }
end
+
+ BOOLEAN_ATTRIBUTES.each do |name|
+ define_method("#{name}?") { !!self[name] }
+ end
+
+ TYPES.each do |type|
+ define_method("#{type}?") { self[:type] == type }
+ end
end
end
diff --git a/app/models/integrations/flowdock.rb b/app/models/integrations/flowdock.rb
index 703d8013bab..52efb29f2c1 100644
--- a/app/models/integrations/flowdock.rb
+++ b/app/models/integrations/flowdock.rb
@@ -2,9 +2,16 @@
module Integrations
class Flowdock < Integration
- prop_accessor :token
validates :token, presence: true, if: :activated?
+ field :token,
+ type: 'password',
+ help: -> { s_('FlowdockService|Enter your Flowdock token.') },
+ non_empty_password_title: -> { s_('ProjectService|Enter new token') },
+ non_empty_password_help: -> { s_('ProjectService|Leave blank to use your current token.') },
+ placeholder: '1b609b52537...',
+ required: true
+
def title
'Flowdock'
end
@@ -22,20 +29,6 @@ module Integrations
'flowdock'
end
- def fields
- [
- {
- type: 'password',
- name: 'token',
- help: s_('FlowdockService|Enter your Flowdock token.'),
- non_empty_password_title: s_('ProjectService|Enter new token'),
- non_empty_password_help: s_('ProjectService|Leave blank to use your current token.'),
- placeholder: '1b609b52537...',
- required: true
- }
- ]
- end
-
def self.supported_events
%w(push)
end
diff --git a/app/models/integrations/hangouts_chat.rb b/app/models/integrations/hangouts_chat.rb
index 8c68c9ff95a..df112ad6ca8 100644
--- a/app/models/integrations/hangouts_chat.rb
+++ b/app/models/integrations/hangouts_chat.rb
@@ -19,9 +19,6 @@ 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 event_field(event)
- end
-
def default_channel_placeholder
end
@@ -42,7 +39,7 @@ module Integrations
type: 'select',
name: 'branches_to_be_notified',
title: s_('Integrations|Branches for which notifications are to be sent'),
- choices: branch_choices
+ choices: self.class.branch_choices
}
]
end
diff --git a/app/models/integrations/harbor.rb b/app/models/integrations/harbor.rb
index 44813795fc0..82981493822 100644
--- a/app/models/integrations/harbor.rb
+++ b/app/models/integrations/harbor.rb
@@ -4,7 +4,7 @@ module Integrations
class Harbor < Integration
prop_accessor :url, :project_name, :username, :password
- validates :url, public_url: true, presence: true, if: :activated?
+ validates :url, public_url: true, presence: true, addressable_url: { allow_localhost: false, allow_local_network: false }, if: :activated?
validates :project_name, presence: true, if: :activated?
validates :username, presence: true, if: :activated?
validates :password, format: { with: ::Ci::Maskable::REGEX }, if: :activated?
diff --git a/app/models/integrations/irker.rb b/app/models/integrations/irker.rb
index 780f4bef0c9..3f3e321f45e 100644
--- a/app/models/integrations/irker.rb
+++ b/app/models/integrations/irker.rb
@@ -4,13 +4,55 @@ require 'uri'
module Integrations
class Irker < Integration
- prop_accessor :server_host, :server_port, :default_irc_uri
- prop_accessor :recipients, :channels
- boolean_accessor :colorize_messages
validates :recipients, presence: true, if: :validate_recipients?
-
before_validation :get_channels
+ field :server_host,
+ placeholder: 'localhost',
+ title: -> { s_('IrkerService|Server host (optional)') },
+ help: -> { s_('IrkerService|irker daemon hostname (defaults to localhost).') }
+
+ field :server_port,
+ placeholder: 6659,
+ title: -> { s_('IrkerService|Server port (optional)') },
+ help: -> { s_('IrkerService|irker daemon port (defaults to 6659).') }
+
+ field :default_irc_uri,
+ title: -> { s_('IrkerService|Default IRC URI (optional)') },
+ help: -> { s_('IrkerService|URI to add before each recipient.') },
+ placeholder: 'irc://irc.network.net:6697/'
+
+ field :recipients,
+ type: 'textarea',
+ title: -> { s_('IrkerService|Recipients') },
+ placeholder: 'irc[s]://irc.network.net[:port]/#channel',
+ required: true,
+ help: -> do
+ recipients_docs_link = ActionController::Base.helpers.link_to(
+ s_('IrkerService|How to enter channels or users?'),
+ Rails.application.routes.url_helpers.help_page_url(
+ 'user/project/integrations/irker',
+ anchor: 'enter-irker-recipients'
+ ),
+ target: '_blank', rel: 'noopener noreferrer'
+ )
+
+ ERB::Util.html_escape(
+ s_('IrkerService|Channels and users separated by whitespaces. %{recipients_docs_link}')
+ ) % {
+ recipients_docs_link: recipients_docs_link.html_safe
+ }
+ end
+
+ field :colorize_messages,
+ type: 'checkbox',
+ title: -> { _('Colorize messages') }
+
+ # NOTE: This field is only used internally to store the parsed
+ # channels from the `recipients` field, it should not be exposed
+ # in the UI or API.
+ prop_accessor :channels
+
def title
s_('IrkerService|irker (IRC gateway)')
end
@@ -30,17 +72,10 @@ module Integrations
def execute(data)
return unless supported_events.include?(data[:object_kind])
- if Feature.enabled?(:rename_integrations_workers)
- Integrations::IrkerWorker.perform_async(
- project_id, channels,
- colorize_messages, data, settings
- )
- else
- ::IrkerWorker.perform_async(
- project_id, channels,
- colorize_messages, data, settings
- )
- end
+ Integrations::IrkerWorker.perform_async(
+ project_id, channels,
+ colorize_messages, data, settings
+ )
end
def settings
@@ -50,34 +85,6 @@ module Integrations
}
end
- def fields
- recipients_docs_link = ActionController::Base.helpers.link_to(
- s_('IrkerService|How to enter channels or users?'),
- Rails.application.routes.url_helpers.help_page_url(
- 'user/project/integrations/irker',
- anchor: 'enter-irker-recipients'
- ),
- target: '_blank', rel: 'noopener noreferrer'
- )
-
- [
- { type: 'text', name: 'server_host', placeholder: 'localhost', title: s_('IrkerService|Server host (optional)'),
- help: s_('IrkerService|irker daemon hostname (defaults to localhost).') },
- { type: 'text', name: 'server_port', placeholder: 6659, title: s_('IrkerService|Server port (optional)'),
- help: s_('IrkerService|irker daemon port (defaults to 6659).') },
- { type: 'text', name: 'default_irc_uri', title: s_('IrkerService|Default IRC URI (optional)'),
- help: s_('IrkerService|URI to add before each recipient.'),
- placeholder: 'irc://irc.network.net:6697/' },
- { type: 'textarea', name: 'recipients', title: s_('IrkerService|Recipients'),
- placeholder: 'irc[s]://irc.network.net[:port]/#channel', required: true,
- help: format(
- s_('IrkerService|Channels and users separated by whitespaces. %{recipients_docs_link}').html_safe,
- recipients_docs_link: recipients_docs_link.html_safe
- ) },
- { type: 'checkbox', name: 'colorize_messages', title: _('Colorize messages') }
- ]
- end
-
def help
docs_link = ActionController::Base.helpers.link_to(
_('Learn more.'),
diff --git a/app/models/integrations/jira.rb b/app/models/integrations/jira.rb
index 125f52104d4..c9c9b9d59d6 100644
--- a/app/models/integrations/jira.rb
+++ b/app/models/integrations/jira.rb
@@ -71,11 +71,12 @@ module Integrations
non_empty_password_help: -> { s_('JiraService|Leave blank to use your current password or API token.') },
help: -> { s_('JiraService|Use a password for server version and an API token for cloud version.') }
+ field :jira_issue_transition_id, api_only: true
+
# TODO: we can probably just delegate as part of
# https://gitlab.com/gitlab-org/gitlab/issues/29404
# These fields are API only, so no field definition is required.
data_field :jira_issue_transition_automatic
- data_field :jira_issue_transition_id
data_field :project_key
data_field :issues_enabled
data_field :vulnerabilities_enabled
diff --git a/app/models/integrations/mattermost.rb b/app/models/integrations/mattermost.rb
index d9ccbb7ea34..dae11b99bc5 100644
--- a/app/models/integrations/mattermost.rb
+++ b/app/models/integrations/mattermost.rb
@@ -3,6 +3,7 @@
module Integrations
class Mattermost < BaseChatNotification
include SlackMattermostNotifier
+ extend ::Gitlab::Utils::Override
def title
s_('Mattermost notifications')
@@ -28,5 +29,10 @@ module Integrations
def webhook_placeholder
'http://mattermost.example.com/hooks/'
end
+
+ override :configurable_channels?
+ def configurable_channels?
+ true
+ end
end
end
diff --git a/app/models/integrations/microsoft_teams.rb b/app/models/integrations/microsoft_teams.rb
index 625ee0bc522..69863f164cd 100644
--- a/app/models/integrations/microsoft_teams.rb
+++ b/app/models/integrations/microsoft_teams.rb
@@ -22,9 +22,6 @@ module Integrations
'https://outlook.office.com/webhook/…'
end
- def event_field(event)
- end
-
def default_channel_placeholder
end
@@ -47,7 +44,7 @@ module Integrations
section: SECTION_TYPE_CONFIGURATION,
name: 'branches_to_be_notified',
title: s_('Integrations|Branches for which notifications are to be sent'),
- choices: branch_choices
+ choices: self.class.branch_choices
}
]
end
diff --git a/app/models/integrations/mock_ci.rb b/app/models/integrations/mock_ci.rb
index 0b3a9bc5405..2d8e26d409f 100644
--- a/app/models/integrations/mock_ci.rb
+++ b/app/models/integrations/mock_ci.rb
@@ -49,7 +49,7 @@ module Integrations
# # => 'running'
#
def commit_status(sha, ref)
- response = Gitlab::HTTP.get(commit_status_path(sha), verify: enable_ssl_verification, use_read_total_timeout: true)
+ response = Gitlab::HTTP.get(commit_status_path(sha), verify: enable_ssl_verification)
read_commit_status(response)
rescue Errno::ECONNREFUSED
:error
diff --git a/app/models/integrations/packagist.rb b/app/models/integrations/packagist.rb
index 758c9e4761b..05ee919892d 100644
--- a/app/models/integrations/packagist.rb
+++ b/app/models/integrations/packagist.rb
@@ -5,7 +5,25 @@ module Integrations
include HasWebHook
extend Gitlab::Utils::Override
- prop_accessor :username, :token, :server
+ field :username,
+ title: -> { _('Username') },
+ help: -> { s_('Enter your Packagist username.') },
+ placeholder: '',
+ required: true
+
+ field :token,
+ type: 'password',
+ title: -> { _('Token') },
+ help: -> { s_('Enter your Packagist token.') },
+ non_empty_password_title: -> { s_('ProjectService|Enter new token') },
+ non_empty_password_help: -> { s_('ProjectService|Leave blank to use your current token.') },
+ placeholder: '',
+ required: true
+
+ field :server,
+ title: -> { _('Server (optional)') },
+ help: -> { s_('Enter your Packagist server. Defaults to https://packagist.org.') },
+ placeholder: 'https://packagist.org'
validates :username, presence: true, if: :activated?
validates :token, presence: true, if: :activated?
@@ -22,37 +40,6 @@ module Integrations
'packagist'
end
- def fields
- [
- {
- type: 'text',
- name: 'username',
- title: _('Username'),
- help: s_('Enter your Packagist username.'),
- placeholder: '',
- required: true
- },
- {
- type: 'password',
- name: 'token',
- title: _('Token'),
- help: s_('Enter your Packagist token.'),
- non_empty_password_title: s_('ProjectService|Enter new token'),
- non_empty_password_help: s_('ProjectService|Leave blank to use your current token.'),
- placeholder: '',
- required: true
- },
- {
- type: 'text',
- name: 'server',
- title: _('Server (optional)'),
- help: s_('Enter your Packagist server. Defaults to https://packagist.org.'),
- placeholder: 'https://packagist.org',
- required: false
- }
- ]
- end
-
def self.supported_events
%w(push merge_request tag_push)
end
diff --git a/app/models/integrations/pipelines_email.rb b/app/models/integrations/pipelines_email.rb
index f15482dc2e1..77cbba25f2c 100644
--- a/app/models/integrations/pipelines_email.rb
+++ b/app/models/integrations/pipelines_email.rb
@@ -6,11 +6,26 @@ module Integrations
RECIPIENTS_LIMIT = 30
- prop_accessor :recipients, :branches_to_be_notified
- boolean_accessor :notify_only_broken_pipelines, :notify_only_default_branch
validates :recipients, presence: true, if: :validate_recipients?
validate :number_of_recipients_within_limit, if: :validate_recipients?
+ field :recipients,
+ type: 'textarea',
+ help: -> { _('Comma-separated list of email addresses.') },
+ required: true
+
+ field :notify_only_broken_pipelines,
+ type: 'checkbox'
+
+ field :notify_only_default_branch,
+ type: 'checkbox',
+ api_only: true
+
+ field :branches_to_be_notified,
+ type: 'select',
+ title: -> { s_('Integrations|Branches for which notifications are to be sent') },
+ choices: branch_choices
+
def initialize_properties
super
@@ -65,21 +80,6 @@ module Integrations
project&.ci_pipelines&.any?
end
- def fields
- [
- { type: 'textarea',
- name: 'recipients',
- help: _('Comma-separated list of email addresses.'),
- 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: branch_choices }
- ]
- end
-
def test(data)
result = execute(data, force: true)
diff --git a/app/models/integrations/pivotaltracker.rb b/app/models/integrations/pivotaltracker.rb
index 931ccf46655..d32fb974339 100644
--- a/app/models/integrations/pivotaltracker.rb
+++ b/app/models/integrations/pivotaltracker.rb
@@ -4,9 +4,22 @@ module Integrations
class Pivotaltracker < Integration
API_ENDPOINT = 'https://www.pivotaltracker.com/services/v5/source_commits'
- prop_accessor :token, :restrict_to_branch
validates :token, presence: true, if: :activated?
+ field :token,
+ type: 'password',
+ help: -> { s_('PivotalTrackerService|Pivotal Tracker API token. User must have access to the story. All comments are attributed to this user.') },
+ non_empty_password_title: -> { s_('ProjectService|Enter new token') },
+ non_empty_password_help: -> { s_('ProjectService|Leave blank to use your current token.') },
+ required: true
+
+ field :restrict_to_branch,
+ title: -> { s_('Integrations|Restrict to branch (optional)') },
+ help: -> do
+ s_('PivotalTrackerService|Comma-separated list of branches to ' \
+ 'automatically inspect. Leave blank to include all branches.')
+ end
+
def title
'Pivotal Tracker'
end
@@ -24,26 +37,6 @@ module Integrations
'pivotaltracker'
end
- def fields
- [
- {
- type: 'password',
- name: 'token',
- help: s_('PivotalTrackerService|Pivotal Tracker API token. User must have access to the story. All comments are attributed to this user.'),
- non_empty_password_title: s_('ProjectService|Enter new token'),
- non_empty_password_help: s_('ProjectService|Leave blank to use your current token.'),
- required: true
- },
- {
- type: 'text',
- name: 'restrict_to_branch',
- title: 'Restrict to branch (optional)',
- help: s_('PivotalTrackerService|Comma-separated list of branches to ' \
- 'automatically inspect. Leave blank to include all branches.')
- }
- ]
- end
-
def self.supported_events
%w(push)
end
diff --git a/app/models/integrations/prometheus.rb b/app/models/integrations/prometheus.rb
index 36060565317..e672a985810 100644
--- a/app/models/integrations/prometheus.rb
+++ b/app/models/integrations/prometheus.rb
@@ -4,11 +4,30 @@ module Integrations
class Prometheus < BaseMonitoring
include PrometheusAdapter
- # Access to prometheus is directly through the API
- prop_accessor :api_url
- prop_accessor :google_iap_service_account_json
- prop_accessor :google_iap_audience_client_id
- boolean_accessor :manual_configuration
+ field :manual_configuration,
+ type: 'checkbox',
+ title: -> { s_('PrometheusService|Active') },
+ help: -> { s_('PrometheusService|Select this checkbox to override the auto configuration settings with your own settings.') },
+ required: true
+
+ field :api_url,
+ title: 'API URL',
+ placeholder: -> { s_('PrometheusService|https://prometheus.example.com/') },
+ help: -> { s_('PrometheusService|The Prometheus API base URL.') },
+ required: true
+
+ field :google_iap_audience_client_id,
+ title: 'Google IAP Audience Client ID',
+ placeholder: -> { s_('PrometheusService|IAP_CLIENT_ID.apps.googleusercontent.com') },
+ help: -> { s_('PrometheusService|The ID of the IAP-secured resource.') },
+ required: false
+
+ field :google_iap_service_account_json,
+ type: 'textarea',
+ title: 'Google IAP Service Account JSON',
+ placeholder: -> { s_('PrometheusService|{ "type": "service_account", "project_id": ... }') },
+ help: -> { s_('PrometheusService|The contents of the credentials.json file of your service account.') },
+ required: false
# We need to allow the self-monitoring project to connect to the internal
# Prometheus instance.
@@ -45,43 +64,6 @@ module Integrations
'prometheus'
end
- def fields
- [
- {
- type: 'checkbox',
- name: 'manual_configuration',
- title: s_('PrometheusService|Active'),
- help: s_('PrometheusService|Select this checkbox to override the auto configuration settings with your own settings.'),
- required: true
- },
- {
- type: 'text',
- name: 'api_url',
- title: 'API URL',
- placeholder: s_('PrometheusService|https://prometheus.example.com/'),
- help: s_('PrometheusService|The Prometheus API base URL.'),
- required: true
- },
- {
- type: 'text',
- name: 'google_iap_audience_client_id',
- title: 'Google IAP Audience Client ID',
- placeholder: s_('PrometheusService|IAP_CLIENT_ID.apps.googleusercontent.com'),
- help: s_('PrometheusService|The ID of the IAP-secured resource.'),
- autocomplete: 'off',
- required: false
- },
- {
- type: 'textarea',
- name: 'google_iap_service_account_json',
- title: 'Google IAP Service Account JSON',
- placeholder: s_('PrometheusService|{ "type": "service_account", "project_id": ... }'),
- help: s_('PrometheusService|The contents of the credentials.json file of your service account.'),
- required: false
- }
- ]
- end
-
# Check we can connect to the Prometheus API
def test(*args)
return { success: false, result: 'Prometheus configuration error' } unless prometheus_client
diff --git a/app/models/integrations/pushover.rb b/app/models/integrations/pushover.rb
index 7fd5efa8765..791e27c5db7 100644
--- a/app/models/integrations/pushover.rb
+++ b/app/models/integrations/pushover.rb
@@ -4,9 +4,73 @@ module Integrations
class Pushover < Integration
BASE_URI = 'https://api.pushover.net/1'
- prop_accessor :api_key, :user_key, :device, :priority, :sound
validates :api_key, :user_key, :priority, presence: true, if: :activated?
+ field :api_key,
+ type: 'password',
+ title: -> { _('API key') },
+ help: -> { s_('PushoverService|Enter your application key.') },
+ non_empty_password_title: -> { s_('ProjectService|Enter new API key') },
+ non_empty_password_help: -> { s_('ProjectService|Leave blank to use your current API key.') },
+ placeholder: '',
+ required: true
+
+ field :user_key,
+ type: 'password',
+ title: -> { _('User key') },
+ help: -> { s_('PushoverService|Enter your user key.') },
+ non_empty_password_title: -> { s_('PushoverService|Enter new user key') },
+ non_empty_password_help: -> { s_('PushoverService|Leave blank to use your current user key.') },
+ placeholder: '',
+ required: true
+
+ field :device,
+ title: -> { _('Devices (optional)') },
+ help: -> { s_('PushoverService|Leave blank for all active devices.') },
+ placeholder: ''
+
+ field :priority,
+ type: 'select',
+ required: true,
+ choices: -> do
+ [
+ [s_('PushoverService|Lowest priority'), -2],
+ [s_('PushoverService|Low priority'), -1],
+ [s_('PushoverService|Normal priority'), 0],
+ [s_('PushoverService|High priority'), 1]
+ ]
+ end
+
+ field :sound,
+ type: 'select',
+ choices: -> do
+ [
+ ['Device default sound', nil],
+ ['Pushover (default)', 'pushover'],
+ %w(Bike bike),
+ %w(Bugle bugle),
+ ['Cash Register', 'cashregister'],
+ %w(Classical classical),
+ %w(Cosmic cosmic),
+ %w(Falling falling),
+ %w(Gamelan gamelan),
+ %w(Incoming incoming),
+ %w(Intermission intermission),
+ %w(Magic magic),
+ %w(Mechanical mechanical),
+ ['Piano Bar', 'pianobar'],
+ %w(Siren siren),
+ ['Space Alarm', 'spacealarm'],
+ ['Tug Boat', 'tugboat'],
+ ['Alien Alarm (long)', 'alien'],
+ ['Climb (long)', 'climb'],
+ ['Persistent (long)', 'persistent'],
+ ['Pushover Echo (long)', 'echo'],
+ ['Up Down (long)', 'updown'],
+ ['None (silent)', 'none']
+ ]
+ end
+
def title
'Pushover'
end
@@ -19,81 +83,6 @@ module Integrations
'pushover'
end
- def fields
- [
- {
- type: 'password',
- name: 'api_key',
- title: _('API key'),
- help: s_('PushoverService|Enter your application key.'),
- non_empty_password_title: s_('ProjectService|Enter new API key'),
- non_empty_password_help: s_('ProjectService|Leave blank to use your current API key.'),
- placeholder: '',
- required: true
- },
- {
- type: 'password',
- name: 'user_key',
- title: _('User key'),
- help: s_('PushoverService|Enter your user key.'),
- non_empty_password_title: s_('PushoverService|Enter new user key'),
- non_empty_password_help: s_('PushoverService|Leave blank to use your current user key.'),
- placeholder: '',
- required: true
- },
- {
- type: 'text',
- name: 'device',
- title: _('Devices (optional)'),
- help: s_('PushoverService|Leave blank for all active devices.'),
- placeholder: ''
- },
- {
- type: 'select',
- name: 'priority',
- required: true,
- choices:
- [
- [s_('PushoverService|Lowest priority'), -2],
- [s_('PushoverService|Low priority'), -1],
- [s_('PushoverService|Normal priority'), 0],
- [s_('PushoverService|High priority'), 1]
- ],
- default_choice: 0
- },
- {
- type: 'select',
- name: 'sound',
- choices:
- [
- ['Device default sound', nil],
- ['Pushover (default)', 'pushover'],
- %w(Bike bike),
- %w(Bugle bugle),
- ['Cash Register', 'cashregister'],
- %w(Classical classical),
- %w(Cosmic cosmic),
- %w(Falling falling),
- %w(Gamelan gamelan),
- %w(Incoming incoming),
- %w(Intermission intermission),
- %w(Magic magic),
- %w(Mechanical mechanical),
- ['Piano Bar', 'pianobar'],
- %w(Siren siren),
- ['Space Alarm', 'spacealarm'],
- ['Tug Boat', 'tugboat'],
- ['Alien Alarm (long)', 'alien'],
- ['Climb (long)', 'climb'],
- ['Persistent (long)', 'persistent'],
- ['Pushover Echo (long)', 'echo'],
- ['Up Down (long)', 'updown'],
- ['None (silent)', 'none']
- ]
- }
- ]
- end
-
def self.supported_events
%w(push)
end
diff --git a/app/models/integrations/shimo.rb b/app/models/integrations/shimo.rb
index dd25a0bc558..8bc296e0320 100644
--- a/app/models/integrations/shimo.rb
+++ b/app/models/integrations/shimo.rb
@@ -2,9 +2,12 @@
module Integrations
class Shimo < BaseThirdPartyWiki
- prop_accessor :external_wiki_url
validates :external_wiki_url, presence: true, public_url: true, if: :activated?
+ field :external_wiki_url,
+ title: -> { s_('Shimo|Shimo Workspace URL') },
+ required: true
+
def render?
return false unless Feature.enabled?(:shimo_integration, project)
@@ -25,21 +28,10 @@ module Integrations
# support for `test` method
def execute(_data)
- response = Gitlab::HTTP.get(properties['external_wiki_url'], verify: true, use_read_total_timeout: true)
+ response = Gitlab::HTTP.get(properties['external_wiki_url'], verify: true)
response.body if response.code == 200
rescue StandardError
nil
end
-
- def fields
- [
- {
- type: 'text',
- name: 'external_wiki_url',
- title: s_('Shimo|Shimo Workspace URL'),
- required: true
- }
- ]
- end
end
end
diff --git a/app/models/integrations/slack.rb b/app/models/integrations/slack.rb
index 0381db3a67e..93263229109 100644
--- a/app/models/integrations/slack.rb
+++ b/app/models/integrations/slack.rb
@@ -55,5 +55,10 @@ module Integrations
Gitlab::UsageDataCounters::HLLRedisCounter.track_event(key, values: user_id)
end
+
+ override :configurable_channels?
+ def configurable_channels?
+ true
+ end
end
end
diff --git a/app/models/integrations/teamcity.rb b/app/models/integrations/teamcity.rb
index a23aa5f783d..e0299c9ac5f 100644
--- a/app/models/integrations/teamcity.rb
+++ b/app/models/integrations/teamcity.rb
@@ -156,7 +156,7 @@ module Integrations
end
def get_path(path)
- Gitlab::HTTP.try_get(build_url(path), verify: enable_ssl_verification, basic_auth: basic_auth, extra_log_info: { project_id: project_id }, use_read_total_timeout: true)
+ Gitlab::HTTP.try_get(build_url(path), verify: enable_ssl_verification, basic_auth: basic_auth, extra_log_info: { project_id: project_id })
end
def post_to_build_queue(data, branch)
@@ -167,8 +167,7 @@ module Integrations
'</build>',
headers: { 'Content-type' => 'application/xml' },
verify: enable_ssl_verification,
- basic_auth: basic_auth,
- use_read_total_timeout: true
+ basic_auth: basic_auth
)
end
diff --git a/app/models/integrations/unify_circuit.rb b/app/models/integrations/unify_circuit.rb
index f085423d229..f10a75fac5d 100644
--- a/app/models/integrations/unify_circuit.rb
+++ b/app/models/integrations/unify_circuit.rb
@@ -19,9 +19,6 @@ module Integrations
s_('Integrations|Send notifications about project events to a Unify Circuit conversation. %{docs_link}').html_safe % { docs_link: docs_link.html_safe }
end
- def event_field(event)
- end
-
def default_channel_placeholder
end
@@ -38,7 +35,7 @@ module Integrations
type: 'select',
name: 'branches_to_be_notified',
title: s_('Integrations|Branches for which notifications are to be sent'),
- choices: branch_choices
+ choices: self.class.branch_choices
}
]
end
@@ -49,8 +46,7 @@ module Integrations
response = Gitlab::HTTP.post(webhook, body: {
subject: message.project_name,
text: message.summary,
- markdown: true,
- use_read_total_timeout: true
+ markdown: true
}.to_json)
response if response.success?
diff --git a/app/models/integrations/webex_teams.rb b/app/models/integrations/webex_teams.rb
index 345dd98cbc1..75be457dcf5 100644
--- a/app/models/integrations/webex_teams.rb
+++ b/app/models/integrations/webex_teams.rb
@@ -19,9 +19,6 @@ module Integrations
s_("WebexTeamsService|Send notifications about project events to a Webex Teams conversation. %{docs_link}") % { docs_link: docs_link.html_safe }
end
- def event_field(event)
- end
-
def default_channel_placeholder
end
@@ -38,7 +35,7 @@ module Integrations
type: 'select',
name: 'branches_to_be_notified',
title: s_('Integrations|Branches for which notifications are to be sent'),
- choices: branch_choices
+ choices: self.class.branch_choices
}
]
end
@@ -47,7 +44,7 @@ module Integrations
def notify(message, opts)
header = { 'Content-Type' => 'application/json' }
- response = Gitlab::HTTP.post(webhook, headers: header, body: { markdown: message.summary }.to_json, use_read_total_timeout: true)
+ response = Gitlab::HTTP.post(webhook, headers: header, body: { markdown: message.summary }.to_json)
response if response.success?
end
diff --git a/app/models/integrations/youtrack.rb b/app/models/integrations/youtrack.rb
index ab6e1da27f8..fa719f925ed 100644
--- a/app/models/integrations/youtrack.rb
+++ b/app/models/integrations/youtrack.rb
@@ -33,10 +33,7 @@ module Integrations
end
def fields
- [
- { type: 'text', name: 'project_url', title: _('Project URL'), help: s_('IssueTracker|The URL to the project in YouTrack.'), required: true },
- { type: 'text', name: 'issues_url', title: s_('ProjectService|Issue URL'), help: s_('IssueTracker|The URL to view an issue in the YouTrack project. Must contain %{colon_id}.') % { colon_id: '<code>:id</code>'.html_safe }, required: true }
- ]
+ super.select { _1.name.in?(%w[project_url issues_url]) }
end
end
end
diff --git a/app/models/integrations/zentao.rb b/app/models/integrations/zentao.rb
index c33df465fde..11db469f7ee 100644
--- a/app/models/integrations/zentao.rb
+++ b/app/models/integrations/zentao.rb
@@ -4,7 +4,28 @@ module Integrations
class Zentao < Integration
include Gitlab::Routing
- data_field :url, :api_url, :api_token, :zentao_product_xid
+ self.field_storage = :data_fields
+
+ field :url,
+ title: -> { s_('ZentaoIntegration|ZenTao Web URL') },
+ placeholder: 'https://www.zentao.net',
+ help: -> { s_('ZentaoIntegration|Base URL of the ZenTao instance.') },
+ required: true
+
+ field :api_url,
+ title: -> { s_('ZentaoIntegration|ZenTao API URL (optional)') },
+ help: -> { s_('ZentaoIntegration|If different from Web URL.') }
+
+ field :api_token,
+ type: 'password',
+ title: -> { s_('ZentaoIntegration|ZenTao API token') },
+ non_empty_password_title: -> { s_('ZentaoIntegration|Enter new ZenTao API token') },
+ non_empty_password_help: -> { s_('ProjectService|Leave blank to use your current token.') },
+ required: true
+
+ field :zentao_product_xid,
+ title: -> { s_('ZentaoIntegration|ZenTao Product ID') },
+ required: true
validates :url, public_url: true, presence: true, if: :activated?
validates :api_url, public_url: true, allow_blank: true
@@ -47,39 +68,6 @@ module Integrations
%w()
end
- def fields
- [
- {
- type: 'text',
- name: 'url',
- title: s_('ZentaoIntegration|ZenTao Web URL'),
- placeholder: 'https://www.zentao.net',
- help: s_('ZentaoIntegration|Base URL of the ZenTao instance.'),
- required: true
- },
- {
- type: 'text',
- name: 'api_url',
- title: s_('ZentaoIntegration|ZenTao API URL (optional)'),
- help: s_('ZentaoIntegration|If different from Web URL.')
- },
- {
- type: 'password',
- name: 'api_token',
- title: s_('ZentaoIntegration|ZenTao API token'),
- non_empty_password_title: s_('ZentaoIntegration|Enter new ZenTao API token'),
- non_empty_password_help: s_('ProjectService|Leave blank to use your current token.'),
- required: true
- },
- {
- type: 'text',
- name: 'zentao_product_xid',
- title: s_('ZentaoIntegration|ZenTao Product ID'),
- required: true
- }
- ]
- end
-
private
def client
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 47aa2b24feb..cae42115bef 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -46,7 +46,7 @@ class Issue < ApplicationRecord
TYPES_FOR_LIST = %w(issue incident).freeze
belongs_to :project
- has_one :namespace, through: :project
+ belongs_to :namespace, inverse_of: :issues
belongs_to :duplicated_to, class_name: 'Issue'
belongs_to :closed_by, class_name: 'User'
@@ -98,6 +98,7 @@ class Issue < ApplicationRecord
validates :project, presence: true
validates :issue_type, presence: true
+ validates :namespace, presence: true, if: -> { project.present? }
enum issue_type: WorkItems::Type.base_types
@@ -123,8 +124,24 @@ class Issue < ApplicationRecord
scope :order_due_date_desc, -> { reorder(arel_table[:due_date].desc.nulls_last) }
scope :order_closest_future_date, -> { reorder(Arel.sql('CASE WHEN issues.due_date >= CURRENT_DATE THEN 0 ELSE 1 END ASC, ABS(CURRENT_DATE - issues.due_date) ASC')) }
scope :order_created_at_desc, -> { reorder(created_at: :desc) }
- scope :order_severity_asc, -> { includes(:issuable_severity).order('issuable_severities.severity ASC NULLS FIRST') }
- scope :order_severity_desc, -> { includes(:issuable_severity).order('issuable_severities.severity DESC NULLS LAST') }
+ scope :order_severity_asc, -> do
+ build_keyset_order_on_joined_column(
+ scope: includes(:issuable_severity),
+ attribute_name: 'issuable_severities_severity',
+ column: IssuableSeverity.arel_table[:severity],
+ direction: :asc,
+ nullable: :nulls_first
+ )
+ end
+ scope :order_severity_desc, -> do
+ build_keyset_order_on_joined_column(
+ scope: includes(:issuable_severity),
+ attribute_name: 'issuable_severities_severity',
+ column: IssuableSeverity.arel_table[:severity],
+ direction: :desc,
+ nullable: :nulls_last
+ )
+ end
scope :order_escalation_status_asc, -> { includes(:incident_management_issuable_escalation_status).order(IncidentManagement::IssuableEscalationStatus.arel_table[:status].asc.nulls_last).references(:incident_management_issuable_escalation_status) }
scope :order_escalation_status_desc, -> { includes(:incident_management_issuable_escalation_status).order(IncidentManagement::IssuableEscalationStatus.arel_table[:status].desc.nulls_last).references(:incident_management_issuable_escalation_status) }
scope :order_closed_at_asc, -> { reorder(arel_table[:closed_at].asc.nulls_last) }
@@ -184,6 +201,8 @@ class Issue < ApplicationRecord
scope :with_null_relative_position, -> { where(relative_position: nil) }
scope :with_non_null_relative_position, -> { where.not(relative_position: nil) }
+ before_validation :ensure_namespace_id
+
after_commit :expire_etag_cache, unless: :importing?
after_save :ensure_metrics, unless: :importing?
after_create_commit :record_create_action, unless: :importing?
@@ -231,6 +250,31 @@ class Issue < ApplicationRecord
alias_method :with_state, :with_state_id
alias_method :with_states, :with_state_ids
+ def build_keyset_order_on_joined_column(scope:, attribute_name:, column:, direction:, nullable:)
+ reversed_direction = direction == :asc ? :desc : :asc
+
+ # rubocop: disable GitlabSecurity/PublicSend
+ order = ::Gitlab::Pagination::Keyset::Order.build([
+ ::Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
+ attribute_name: attribute_name,
+ column_expression: column,
+ order_expression: column.send(direction).send(nullable),
+ reversed_order_expression: column.send(reversed_direction).send(nullable),
+ order_direction: direction,
+ distinct: false,
+ add_to_projections: true,
+ nullable: nullable
+ ),
+ ::Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
+ attribute_name: 'id',
+ order_expression: arel_table['id'].desc
+ )
+ ])
+ # rubocop: enable GitlabSecurity/PublicSend
+
+ order.apply_cursor_conditions(scope).order(order)
+ end
+
override :order_upvotes_desc
def order_upvotes_desc
reorder(upvotes_count: :desc)
@@ -328,11 +372,11 @@ class Issue < ApplicationRecord
when 'due_date', 'due_date_asc' then order_due_date_asc.with_order_id_desc
when 'due_date_desc' then order_due_date_desc.with_order_id_desc
when 'relative_position', 'relative_position_asc' then order_by_relative_position
- when 'severity_asc' then order_severity_asc.with_order_id_desc
- when 'severity_desc' then order_severity_desc.with_order_id_desc
- when 'escalation_status_asc' then order_escalation_status_asc.with_order_id_desc
- when 'escalation_status_desc' then order_escalation_status_desc.with_order_id_desc
- when 'closed_at_asc' then order_closed_at_asc
+ when 'severity_asc' then order_severity_asc
+ when 'severity_desc' then order_severity_desc
+ when 'escalation_status_asc' then order_escalation_status_asc
+ when 'escalation_status_desc' then order_escalation_status_desc
+ when 'closed_at', 'closed_at_asc' then order_closed_at_asc
when 'closed_at_desc' then order_closed_at_desc
else
super
@@ -405,14 +449,6 @@ class Issue < ApplicationRecord
end
end
- # Returns boolean if a related branch exists for the current issue
- # ignores merge requests branchs
- def has_related_branch?
- project.repository.branch_names.any? do |branch|
- /\A#{iid}-(?!\d+-stable)/i =~ branch
- end
- end
-
# To allow polymorphism with MergeRequest.
def source_project
project
@@ -656,6 +692,10 @@ class Issue < ApplicationRecord
# Symptom of running out of space - schedule rebalancing
Issues::RebalancingWorker.perform_async(nil, *project.self_or_root_group_ids)
end
+
+ def ensure_namespace_id
+ self.namespace = project.project_namespace if project
+ end
end
Issue.prepend_mod_with('Issue')
diff --git a/app/models/key.rb b/app/models/key.rb
index 5268ce2e040..9f6029cc5d4 100644
--- a/app/models/key.rb
+++ b/app/models/key.rb
@@ -28,7 +28,7 @@ class Key < ApplicationRecord
validate :key_meets_restrictions
validate :expiration, on: :create
- validate :banned_key, if: :should_check_for_banned_key?
+ validate :banned_key, if: :key_changed?
delegate :name, :email, to: :user, prefix: true
@@ -121,6 +121,12 @@ class Key < ApplicationRecord
@public_key ||= Gitlab::SSHPublicKey.new(key)
end
+ def ensure_sha256_fingerprint!
+ return if self.fingerprint_sha256
+
+ save if generate_fingerprint
+ end
+
private
def generate_fingerprint
@@ -143,12 +149,6 @@ class Key < ApplicationRecord
end
end
- def should_check_for_banned_key?
- return false unless user
-
- key_changed? && Feature.enabled?(:ssh_banned_key, user)
- end
-
def banned_key
return unless public_key.banned?
diff --git a/app/models/member.rb b/app/models/member.rb
index bb5d2b10f8e..dcca63b5691 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -219,7 +219,23 @@ class Member < ApplicationRecord
class << self
def search(query)
- joins(:user).merge(User.search(query, use_minimum_char_limit: false))
+ scope = joins(:user).merge(User.search(query, use_minimum_char_limit: false))
+
+ return scope unless Gitlab::Pagination::Keyset::Order.keyset_aware?(scope)
+
+ # If the User.search method returns keyset pagination aware AR scope then we
+ # need call apply_cursor_conditions which adds the ORDER BY columns from the scope
+ # to the SELECT clause.
+ #
+ # Why is this needed:
+ # When using keyset pagination, the next page is loaded using the ORDER BY
+ # values of the last record (cursor). This query selects `members.*` and
+ # orders by a custom SQL expression on `users` and `users.name`. The values
+ # will not be part of `members.*`.
+ #
+ # Result: `SELECT members.*, users.column1, users.column2 FROM members ...`
+ order = Gitlab::Pagination::Keyset::Order.extract_keyset_order_object(scope)
+ order.apply_cursor_conditions(scope).reorder(order)
end
def search_invite_email(query)
diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb
index 791cb6f0dff..c97f00364fd 100644
--- a/app/models/members/project_member.rb
+++ b/app/models/members/project_member.rb
@@ -21,30 +21,30 @@ class ProjectMember < Member
end
class << self
- # Add users to projects with passed access option
+ # Add members to projects with passed access option
#
# access can be an integer representing a access code
# or symbol like :maintainer representing role
#
# Ex.
- # add_users_to_projects(
+ # add_members_to_projects(
# project_ids,
# user_ids,
# ProjectMember::MAINTAINER
# )
#
- # add_users_to_projects(
+ # add_members_to_projects(
# project_ids,
# user_ids,
# :maintainer
# )
#
- def add_users_to_projects(project_ids, users, access_level, current_user: nil, expires_at: nil)
+ def add_members_to_projects(project_ids, users, access_level, current_user: nil, expires_at: nil)
self.transaction do
project_ids.each do |project_id|
project = Project.find(project_id)
- Members::Projects::CreatorService.add_users( # rubocop:disable CodeReuse/ServiceClass
+ Members::Projects::CreatorService.add_members( # rubocop:disable CodeReuse/ServiceClass
project,
users,
access_level,
@@ -111,7 +111,7 @@ class ProjectMember < Member
# rubocop:disable CodeReuse/ServiceClass
if blocking
- AuthorizedProjectUpdate::ProjectRecalculatePerUserService.new(project, user).execute
+ AuthorizedProjectUpdate::ProjectRecalculatePerUserWorker.bulk_perform_and_wait([[project.id, user.id]])
else
AuthorizedProjectUpdate::ProjectRecalculatePerUserWorker.perform_async(project.id, user.id)
end
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 1a3464d05a2..ec97ab0ea42 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -174,6 +174,10 @@ class MergeRequest < ApplicationRecord
merge_request.merge_jid = nil
end
+ before_transition any => :closed do |merge_request|
+ merge_request.merge_error = nil
+ end
+
after_transition any => :opened do |merge_request|
merge_request.run_after_commit do
UpdateHeadPipelineForMergeRequestWorker.perform_async(merge_request.id)
@@ -1567,6 +1571,7 @@ class MergeRequest < ApplicationRecord
variables.append(key: 'CI_MERGE_REQUEST_PROJECT_PATH', value: project.full_path)
variables.append(key: 'CI_MERGE_REQUEST_PROJECT_URL', value: project.web_url)
variables.append(key: 'CI_MERGE_REQUEST_TARGET_BRANCH_NAME', value: target_branch.to_s)
+ variables.append(key: 'CI_MERGE_REQUEST_TARGET_BRANCH_PROTECTED', value: ProtectedBranch.protected?(target_project, target_branch).to_s)
variables.append(key: 'CI_MERGE_REQUEST_TITLE', value: title)
variables.append(key: 'CI_MERGE_REQUEST_ASSIGNEES', value: assignee_username_list) if assignees.present?
variables.append(key: 'CI_MERGE_REQUEST_MILESTONE', value: milestone.title) if milestone
diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb
index 87afb7a489a..e08b2cc2a7d 100644
--- a/app/models/merge_request_diff.rb
+++ b/app/models/merge_request_diff.rb
@@ -21,6 +21,10 @@ class MergeRequestDiff < ApplicationRecord
# from the database if this sentinel is seen
FILES_COUNT_SENTINEL = 2**15 - 1
+ # External diff cache key used by diffs export
+ EXTERNAL_DIFFS_CACHE_TMPDIR = 'project-%{project_id}-external-mr-%{mr_id}-diff-%{id}-cache'
+ EXTERNAL_DIFF_CACHE_CHUNK_SIZE = 8.megabytes
+
belongs_to :merge_request
manual_inverse_association :merge_request, :merge_request_diff
@@ -545,6 +549,28 @@ class MergeRequestDiff < ApplicationRecord
merge_request_diff_files.reset
end
+ # Yields locally cached external diff if it's externally stored.
+ # Used during Project Export to speed up externally
+ # stored merge request diffs export
+ def cached_external_diff
+ return yield(nil) unless stored_externally?
+
+ cache_external_diff unless File.exist?(external_diff_cache_filepath)
+
+ File.open(external_diff_cache_filepath) do |file|
+ yield(file)
+ end
+ end
+
+ def remove_cached_external_diff
+ Gitlab::Utils.check_path_traversal!(external_diff_cache_dir)
+ Gitlab::Utils.check_allowed_absolute_path!(external_diff_cache_dir, [Dir.tmpdir])
+
+ return unless Dir.exist?(external_diff_cache_dir)
+
+ FileUtils.rm_rf(external_diff_cache_dir)
+ end
+
private
def convert_external_diffs_to_database
@@ -791,6 +817,31 @@ class MergeRequestDiff < ApplicationRecord
def sort_diffs(diffs)
Gitlab::Diff::FileCollectionSorter.new(diffs).sort
end
+
+ # Downloads external diff to a temp storage location.
+ def cache_external_diff
+ return unless stored_externally?
+ return if File.exist?(external_diff_cache_filepath)
+
+ Dir.mkdir(external_diff_cache_dir) unless Dir.exist?(external_diff_cache_dir)
+
+ opening_external_diff do |external_diff|
+ File.open(external_diff_cache_filepath, 'wb') do |file|
+ file.write(external_diff.read(EXTERNAL_DIFF_CACHE_CHUNK_SIZE)) until external_diff.eof?
+ end
+ end
+ end
+
+ def external_diff_cache_filepath
+ File.join(external_diff_cache_dir, "diff-#{id}")
+ end
+
+ def external_diff_cache_dir
+ File.join(
+ Dir.tmpdir,
+ EXTERNAL_DIFFS_CACHE_TMPDIR % { project_id: project.id, mr_id: merge_request_id, id: id }
+ )
+ end
end
MergeRequestDiff.prepend_mod_with('MergeRequestDiff')
diff --git a/app/models/merge_request_diff_file.rb b/app/models/merge_request_diff_file.rb
index f7648937c1d..36902e43a77 100644
--- a/app/models/merge_request_diff_file.rb
+++ b/app/models/merge_request_diff_file.rb
@@ -15,7 +15,12 @@ class MergeRequestDiffFile < ApplicationRecord
end
def utf8_diff
- fetched_diff = diff
+ fetched_diff = if Feature.enabled?(:externally_stored_diffs_caching_export) &&
+ merge_request_diff&.stored_externally?
+ diff_export
+ else
+ diff
+ end
return '' if fetched_diff.blank?
@@ -45,4 +50,40 @@ class MergeRequestDiffFile < ApplicationRecord
content
end
end
+
+ private
+
+ # This method is meant to be used during Project Export.
+ # It is identical to the behaviour in #diff with the only
+ # difference of caching externally stored diffs on local disk in
+ # temp storage location in order to improve diff export performance.
+ def diff_export
+ content = merge_request_diff.cached_external_diff do |file|
+ file.seek(external_diff_offset)
+
+ force_encode_utf8(file.read(external_diff_size))
+ end
+
+ # See #diff
+ if binary?
+ content = begin
+ content.unpack1('m0')
+ rescue ArgumentError
+ content
+ end
+ end
+
+ content
+ rescue StandardError => e
+ log_payload = {
+ message: 'Cached external diff export failed',
+ merge_request_diff_file_id: id,
+ merge_request_diff_id: merge_request_diff&.id
+ }
+
+ Gitlab::ExceptionLogFormatter.format!(e, log_payload)
+ Gitlab::AppLogger.warn(log_payload)
+
+ diff
+ end
end
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index 5bb06cdbb4a..f23a859b119 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -74,6 +74,8 @@ class Namespace < ApplicationRecord
has_many :sync_events, class_name: 'Namespaces::SyncEvent'
has_one :cluster_enabled_grant, inverse_of: :namespace, class_name: 'Clusters::ClusterEnabledGrant'
+ has_many :work_items, inverse_of: :namespace
+ has_many :issues, inverse_of: :namespace
validates :owner, presence: true, if: ->(n) { n.owner_required? }
validates :name,
@@ -341,6 +343,10 @@ class Namespace < ApplicationRecord
end
end
+ def emails_enabled?
+ !emails_disabled?
+ end
+
def lfs_enabled?
# User namespace will always default to the global setting
Gitlab.config.lfs.enabled
@@ -450,9 +456,14 @@ class Namespace < ApplicationRecord
end
def pages_virtual_domain
+ cache = if Feature.enabled?(:cache_pages_domain_api, root_ancestor)
+ ::Gitlab::Pages::CacheControl.for_namespace(root_ancestor.id)
+ end
+
Pages::VirtualDomain.new(
- all_projects_with_pages.includes(:route, :project_feature, pages_metadatum: :pages_deployment),
- trim_prefix: full_path
+ projects: all_projects_with_pages.includes(:route, :project_feature, pages_metadatum: :pages_deployment),
+ trim_prefix: full_path,
+ cache: cache
)
end
diff --git a/app/models/namespace_setting.rb b/app/models/namespace_setting.rb
index 504daf2662e..595e34821af 100644
--- a/app/models/namespace_setting.rb
+++ b/app/models/namespace_setting.rb
@@ -24,14 +24,27 @@ class NamespaceSetting < ApplicationRecord
chronic_duration_attr :subgroup_runner_token_expiration_interval_human_readable, :subgroup_runner_token_expiration_interval
chronic_duration_attr :project_runner_token_expiration_interval_human_readable, :project_runner_token_expiration_interval
- NAMESPACE_SETTINGS_PARAMS = [:default_branch_name, :delayed_project_removal,
- :lock_delayed_project_removal, :resource_access_token_creation_allowed,
- :prevent_sharing_groups_outside_hierarchy, :new_user_signups_cap,
- :setup_for_company, :jobs_to_be_done, :runner_token_expiration_interval, :enabled_git_access_protocol,
- :subgroup_runner_token_expiration_interval, :project_runner_token_expiration_interval].freeze
+ NAMESPACE_SETTINGS_PARAMS = %i[
+ default_branch_name
+ delayed_project_removal
+ lock_delayed_project_removal
+ resource_access_token_creation_allowed
+ prevent_sharing_groups_outside_hierarchy
+ new_user_signups_cap
+ setup_for_company
+ jobs_to_be_done
+ runner_token_expiration_interval
+ enabled_git_access_protocol
+ subgroup_runner_token_expiration_interval
+ project_runner_token_expiration_interval
+ ].freeze
self.primary_key = :namespace_id
+ def self.allowed_namespace_settings_params
+ NAMESPACE_SETTINGS_PARAMS
+ end
+
sanitizes! :default_branch_name
def prevent_sharing_groups_outside_hierarchy
diff --git a/app/models/note.rb b/app/models/note.rb
index 41e45a8759f..986a85acac6 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -111,6 +111,7 @@ class Note < ApplicationRecord
end
validate :does_not_exceed_notes_limit?, on: :create, unless: [:system?, :importing?]
+ validate :validate_created_after
# @deprecated attachments are handled by the Upload model.
#
@@ -665,6 +666,25 @@ class Note < ApplicationRecord
)
end
+ def mentioned_users(current_user = nil)
+ users = super
+
+ return users unless confidential?
+
+ Ability.users_that_can_read_internal_notes(users, resource_parent)
+ end
+
+ def mentioned_filtered_user_ids_for(references)
+ return super unless confidential?
+
+ user_ids = references.mentioned_user_ids.presence
+
+ return [] if user_ids.blank?
+
+ users = User.where(id: user_ids)
+ Ability.users_that_can_read_internal_notes(users, resource_parent).pluck(:id)
+ end
+
private
def system_note_viewable_by?(user)
@@ -729,6 +749,13 @@ class Note < ApplicationRecord
errors.add(:base, _('Maximum number of comments exceeded')) if noteable.notes.count >= Noteable::MAX_NOTES_LIMIT
end
+ def validate_created_after
+ return unless created_at
+ return if created_at >= '1970-01-01'
+
+ errors.add(:created_at, s_('Note|The created date provided is too far in the past.'))
+ end
+
def noteable_label_url_method
for_merge_request? ? :project_merge_requests_url : :project_issues_url
end
diff --git a/app/models/notification_recipient.rb b/app/models/notification_recipient.rb
index 79a84231083..b3eaed154e2 100644
--- a/app/models/notification_recipient.rb
+++ b/app/models/notification_recipient.rb
@@ -125,6 +125,10 @@ class NotificationRecipient
@project ? @project.emails_disabled? : @group&.emails_disabled?
end
+ def emails_enabled?
+ !emails_disabled?
+ end
+
def read_ability
return if @skip_read_ability
return @read_ability if instance_variable_defined?(:@read_ability)
diff --git a/app/models/oauth_access_token.rb b/app/models/oauth_access_token.rb
index 9789d8ed62b..20130f01d44 100644
--- a/app/models/oauth_access_token.rb
+++ b/app/models/oauth_access_token.rb
@@ -7,6 +7,8 @@ class OauthAccessToken < Doorkeeper::AccessToken
alias_attribute :user, :resource_owner
scope :distinct_resource_owner_counts, ->(applications) { where(application: applications).distinct.group(:application_id).count(:resource_owner_id) }
+ scope :latest_per_application, -> { select('distinct on(application_id) *').order(application_id: :desc, created_at: :desc) }
+ scope :preload_application, -> { preload(:application) }
def scopes=(value)
if value.is_a?(Array)
diff --git a/app/models/operations/feature_flags_client.rb b/app/models/operations/feature_flags_client.rb
index 1c65c3f096e..e8c237abbc5 100644
--- a/app/models/operations/feature_flags_client.rb
+++ b/app/models/operations/feature_flags_client.rb
@@ -4,6 +4,8 @@ module Operations
class FeatureFlagsClient < ApplicationRecord
include TokenAuthenticatable
+ DEFAULT_UNLEASH_API_VERSION = 1
+
self.table_name = 'operations_feature_flags_clients'
belongs_to :project
@@ -13,6 +15,8 @@ module Operations
add_authentication_token_field :token, encrypted: :required
+ attr_accessor :unleash_app_name
+
before_validation :ensure_token!
def self.find_for_project_and_token(project, token)
@@ -21,5 +25,25 @@ module Operations
where(project_id: project).find_by_token(token)
end
+
+ def self.update_last_feature_flag_updated_at!(project)
+ where(project: project).update_all(last_feature_flag_updated_at: Time.current)
+ end
+
+ def unleash_api_version
+ DEFAULT_UNLEASH_API_VERSION
+ end
+
+ def unleash_api_features
+ return [] unless unleash_app_name.present?
+
+ Operations::FeatureFlag.for_unleash_client(project, unleash_app_name)
+ end
+
+ def unleash_api_cache_key
+ "api_version:#{unleash_api_version}:" \
+ "app_name:#{unleash_app_name}:" \
+ "updated_at:#{last_feature_flag_updated_at.to_i}"
+ end
end
end
diff --git a/app/models/packages/cleanup/policy.rb b/app/models/packages/cleanup/policy.rb
index d7df90a4ce0..35f58f3680d 100644
--- a/app/models/packages/cleanup/policy.rb
+++ b/app/models/packages/cleanup/policy.rb
@@ -23,10 +23,25 @@ module Packages
where.not(keep_n_duplicated_package_files: 'all')
end
+ def self.with_packages
+ exists_select = ::Packages::Package.installable
+ .where('packages_packages.project_id = packages_cleanup_policies.project_id')
+ .select(1)
+ where('EXISTS (?)', exists_select)
+ end
+
+ def self.runnable
+ runnable_schedules.with_packages.order(next_run_at: :asc)
+ end
+
def set_next_run_at
# fixed cadence of 12 hours
self.next_run_at = Time.zone.now + 12.hours
end
+
+ def keep_n_duplicated_package_files_disabled?
+ keep_n_duplicated_package_files == 'all'
+ end
end
end
end
diff --git a/app/models/packages/debian/file_entry.rb b/app/models/packages/debian/file_entry.rb
index eb66f4acfa9..b70b6c460d2 100644
--- a/app/models/packages/debian/file_entry.rb
+++ b/app/models/packages/debian/file_entry.rb
@@ -4,6 +4,7 @@ module Packages
module Debian
class FileEntry
include ActiveModel::Model
+ include ::Packages::FIPS
DIGESTS = %i[md5 sha1 sha256].freeze
FILENAME_REGEX = %r{\A[a-zA-Z0-9][a-zA-Z0-9_.~+-]*\z}.freeze
@@ -31,6 +32,8 @@ module Packages
private
def valid_package_file_digests
+ raise DisabledError, 'Debian registry is not FIPS compliant' if Gitlab::FIPS.enabled?
+
DIGESTS.each do |digest|
package_file_digest = package_file["file_#{digest}"]
sum = public_send("#{digest}sum") # rubocop:disable GitlabSecurity/PublicSend
diff --git a/app/models/pages/virtual_domain.rb b/app/models/pages/virtual_domain.rb
index 497f67993ae..119cc7fc166 100644
--- a/app/models/pages/virtual_domain.rb
+++ b/app/models/pages/virtual_domain.rb
@@ -2,8 +2,9 @@
module Pages
class VirtualDomain
- def initialize(projects, trim_prefix: nil, domain: nil)
+ def initialize(projects:, cache: nil, trim_prefix: nil, domain: nil)
@projects = projects
+ @cache = cache
@trim_prefix = trim_prefix
@domain = domain
end
@@ -27,8 +28,12 @@ module Pages
paths.sort_by(&:prefix).reverse
end
+ def cache_key
+ @cache_key ||= cache&.cache_key
+ end
+
private
- attr_reader :projects, :trim_prefix, :domain
+ attr_reader :projects, :trim_prefix, :domain, :cache
end
end
diff --git a/app/models/pages_domain.rb b/app/models/pages_domain.rb
index 93119bbff1f..9e93bff4acf 100644
--- a/app/models/pages_domain.rb
+++ b/app/models/pages_domain.rb
@@ -209,7 +209,15 @@ class PagesDomain < ApplicationRecord
def pages_virtual_domain
return unless pages_deployed?
- Pages::VirtualDomain.new([project], domain: self)
+ cache = if Feature.enabled?(:cache_pages_domain_api, project.root_namespace)
+ ::Gitlab::Pages::CacheControl.for_project(project.id)
+ end
+
+ Pages::VirtualDomain.new(
+ projects: [project],
+ domain: self,
+ cache: cache
+ )
end
def clear_auto_ssl_failure
diff --git a/app/models/preloaders/users_max_access_level_in_projects_preloader.rb b/app/models/preloaders/users_max_access_level_in_projects_preloader.rb
index b4ce61a869c..99a31a620c5 100644
--- a/app/models/preloaders/users_max_access_level_in_projects_preloader.rb
+++ b/app/models/preloaders/users_max_access_level_in_projects_preloader.rb
@@ -12,6 +12,8 @@ module Preloaders
def execute
return unless @projects.present? && @users.present?
+ preload_users_namespace_bans(@users)
+
access_levels.each do |(project_id, user_id), access_level|
project = projects_by_id[project_id]
@@ -42,5 +44,11 @@ module Preloaders
def projects_by_id
@projects_by_id ||= @projects.index_by(&:id)
end
+
+ def preload_users_namespace_bans(_users)
+ # overridden in EE
+ end
end
end
+
+# Preloaders::UsersMaxAccessLevelInProjectsPreloader.prepend_mod
diff --git a/app/models/project.rb b/app/models/project.rb
index dca47911d20..46e25564eab 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -247,7 +247,6 @@ class Project < ApplicationRecord
has_many :export_jobs, class_name: 'ProjectExportJob'
has_many :bulk_import_exports, class_name: 'BulkImports::Export', inverse_of: :project
has_one :project_repository, inverse_of: :project
- has_one :tracing_setting, class_name: 'ProjectTracingSetting'
has_one :incident_management_setting, inverse_of: :project, class_name: 'IncidentManagement::ProjectIncidentManagementSetting'
has_one :error_tracking_setting, inverse_of: :project, class_name: 'ErrorTracking::ProjectErrorTrackingSetting'
has_one :metrics_setting, inverse_of: :project, class_name: 'ProjectMetricsSetting'
@@ -261,6 +260,7 @@ class Project < ApplicationRecord
has_many :merge_request_metrics, foreign_key: 'target_project', class_name: 'MergeRequest::Metrics', inverse_of: :target_project
has_many :source_of_merge_requests, foreign_key: 'source_project_id', class_name: 'MergeRequest'
has_many :issues
+ has_many :incident_management_issuable_escalation_statuses, through: :issues, inverse_of: :project, class_name: 'IncidentManagement::IssuableEscalationStatus'
has_many :labels, class_name: 'ProjectLabel'
has_many :integrations
has_many :events
@@ -434,7 +434,6 @@ class Project < ApplicationRecord
allow_destroy: true,
reject_if: ->(attrs) { attrs[:id].blank? && attrs[:url].blank? }
- accepts_nested_attributes_for :tracing_setting, update_only: true, allow_destroy: true
accepts_nested_attributes_for :incident_management_setting, update_only: true
accepts_nested_attributes_for :error_tracking_setting, update_only: true
accepts_nested_attributes_for :metrics_setting, update_only: true, allow_destroy: true
@@ -442,33 +441,29 @@ class Project < ApplicationRecord
accepts_nested_attributes_for :prometheus_integration, update_only: true
accepts_nested_attributes_for :alerting_setting, update_only: true
- delegate :feature_available?, :builds_enabled?, :wiki_enabled?,
- :merge_requests_enabled?, :forking_enabled?, :issues_enabled?,
- :pages_enabled?, :analytics_enabled?, :snippets_enabled?, :public_pages?, :private_pages?,
- :merge_requests_access_level, :forking_access_level, :issues_access_level,
- :wiki_access_level, :snippets_access_level, :builds_access_level,
- :repository_access_level, :package_registry_access_level, :pages_access_level, :metrics_dashboard_access_level, :analytics_access_level,
- :operations_enabled?, :operations_access_level, :security_and_compliance_access_level,
- :container_registry_access_level, :container_registry_enabled?,
- to: :project_feature, allow_nil: true
- alias_method :container_registry_enabled, :container_registry_enabled?
- delegate :show_default_award_emojis, :show_default_award_emojis=, :show_default_award_emojis?,
- :enforce_auth_checks_on_uploads, :enforce_auth_checks_on_uploads=, :enforce_auth_checks_on_uploads?,
- :warn_about_potentially_unwanted_characters, :warn_about_potentially_unwanted_characters=, :warn_about_potentially_unwanted_characters?,
- to: :project_setting, allow_nil: true
- delegate :scheduled?, :started?, :in_progress?, :failed?, :finished?,
- prefix: :import, to: :import_state, allow_nil: true
+ delegate :merge_requests_access_level, :forking_access_level, :issues_access_level,
+ :wiki_access_level, :snippets_access_level, :builds_access_level,
+ :repository_access_level, :package_registry_access_level, :pages_access_level,
+ :metrics_dashboard_access_level, :analytics_access_level,
+ :operations_access_level, :security_and_compliance_access_level,
+ :container_registry_access_level,
+ to: :project_feature, allow_nil: true
+
+ delegate :show_default_award_emojis, :show_default_award_emojis=,
+ :enforce_auth_checks_on_uploads, :enforce_auth_checks_on_uploads=,
+ :warn_about_potentially_unwanted_characters, :warn_about_potentially_unwanted_characters=,
+ to: :project_setting, allow_nil: true
+
delegate :squash_always?, :squash_never?, :squash_enabled_by_default?, :squash_readonly?, to: :project_setting
delegate :squash_option, :squash_option=, to: :project_setting
delegate :mr_default_target_self, :mr_default_target_self=, to: :project_setting
delegate :previous_default_branch, :previous_default_branch=, to: :project_setting
- delegate :no_import?, to: :import_state, allow_nil: true
delegate :name, to: :owner, allow_nil: true, prefix: true
delegate :members, to: :team, prefix: true
- delegate :add_user, :add_users, to: :team
+ delegate :add_member, :add_members, to: :team
delegate :add_guest, :add_reporter, :add_developer, :add_maintainer, :add_owner, :add_role, to: :team
delegate :group_runners_enabled, :group_runners_enabled=, to: :ci_cd_settings, allow_nil: true
- delegate :root_ancestor, :certificate_based_clusters_enabled?, to: :namespace, allow_nil: true
+ delegate :root_ancestor, to: :namespace, allow_nil: true
delegate :last_pipeline, to: :commit, allow_nil: true
delegate :external_dashboard_url, to: :metrics_setting, allow_nil: true, prefix: true
delegate :dashboard_timezone, to: :metrics_setting, allow_nil: true, prefix: true
@@ -476,6 +471,7 @@ class Project < ApplicationRecord
delegate :forward_deployment_enabled, :forward_deployment_enabled=, to: :ci_cd_settings, prefix: :ci, allow_nil: true
delegate :job_token_scope_enabled, :job_token_scope_enabled=, to: :ci_cd_settings, prefix: :ci, allow_nil: true
delegate :keep_latest_artifact, :keep_latest_artifact=, to: :ci_cd_settings, allow_nil: true
+ delegate :opt_in_jwt, :opt_in_jwt=, to: :ci_cd_settings, prefix: :ci, allow_nil: true
delegate :restrict_user_defined_variables, :restrict_user_defined_variables=, to: :ci_cd_settings, allow_nil: true
delegate :separated_caches, :separated_caches=, to: :ci_cd_settings, prefix: :ci, allow_nil: true
delegate :runner_token_expiration_interval, :runner_token_expiration_interval=, :runner_token_expiration_interval_human_readable, :runner_token_expiration_interval_human_readable=, to: :ci_cd_settings, allow_nil: true
@@ -483,7 +479,6 @@ class Project < ApplicationRecord
delegate :allow_merge_on_skipped_pipeline, :allow_merge_on_skipped_pipeline?,
:allow_merge_on_skipped_pipeline=, :has_confluence?, :has_shimo?,
to: :project_setting
- delegate :active?, to: :prometheus_integration, allow_nil: true, prefix: true
delegate :merge_commit_template, :merge_commit_template=, to: :project_setting, allow_nil: true
delegate :squash_commit_template, :squash_commit_template=, to: :project_setting, allow_nil: true
@@ -667,7 +662,6 @@ class Project < ApplicationRecord
scope :created_by, -> (user) { where(creator: user) }
scope :imported_from, -> (type) { where(import_type: type) }
scope :imported, -> { where.not(import_type: nil) }
- scope :with_tracing_enabled, -> { joins(:tracing_setting) }
scope :with_enabled_error_tracking, -> { joins(:error_tracking_setting).where(project_error_tracking_settings: { enabled: true }) }
scope :with_service_desk_key, -> (key) do
@@ -676,10 +670,12 @@ class Project < ApplicationRecord
joins(:service_desk_setting).where('service_desk_settings.project_key' => key)
end
- scope :with_topic, ->(topic_name) do
+ scope :with_topic, ->(topic) { where(id: topic.project_topics.select(:project_id)) }
+
+ scope :with_topic_by_name, ->(topic_name) do
topic = Projects::Topic.find_by_name(topic_name)
- topic ? where(id: topic.project_topics.select(:project_id)) : none
+ topic ? with_topic(topic) : none
end
enum auto_cancel_pending_pipelines: { disabled: 0, enabled: 1 }
@@ -917,6 +913,14 @@ class Project < ApplicationRecord
association(:namespace).loaded?
end
+ def certificate_based_clusters_enabled?
+ !!namespace&.certificate_based_clusters_enabled?
+ end
+
+ def prometheus_integration_active?
+ !!prometheus_integration&.active?
+ end
+
def personal_namespace_holder?(user)
return false unless personal?
return false unless user
@@ -933,6 +937,42 @@ class Project < ApplicationRecord
super.presence || build_project_setting
end
+ def show_default_award_emojis?
+ !!project_setting&.show_default_award_emojis?
+ end
+
+ def enforce_auth_checks_on_uploads?
+ !!project_setting&.enforce_auth_checks_on_uploads?
+ end
+
+ def warn_about_potentially_unwanted_characters?
+ !!project_setting&.warn_about_potentially_unwanted_characters?
+ end
+
+ def no_import?
+ !!import_state&.no_import?
+ end
+
+ def import_scheduled?
+ !!import_state&.scheduled?
+ end
+
+ def import_started?
+ !!import_state&.started?
+ end
+
+ def import_in_progress?
+ !!import_state&.in_progress?
+ end
+
+ def import_failed?
+ !!import_state&.failed?
+ end
+
+ def import_finished?
+ !!import_state&.finished?
+ end
+
def all_pipelines
if builds_enabled?
super
@@ -998,6 +1038,9 @@ class Project < ApplicationRecord
end
end
+ def emails_enabled?
+ !emails_disabled?
+ end
override :lfs_enabled?
def lfs_enabled?
return namespace.lfs_enabled? if self[:lfs_enabled].nil?
@@ -1840,6 +1883,59 @@ class Project < ApplicationRecord
end
end
+ def feature_available?(feature, user = nil)
+ !!project_feature&.feature_available?(feature, user)
+ end
+
+ def builds_enabled?
+ !!project_feature&.builds_enabled?
+ end
+
+ def wiki_enabled?
+ !!project_feature&.wiki_enabled?
+ end
+
+ def merge_requests_enabled?
+ !!project_feature&.merge_requests_enabled?
+ end
+
+ def forking_enabled?
+ !!project_feature&.forking_enabled?
+ end
+
+ def issues_enabled?
+ !!project_feature&.issues_enabled?
+ end
+
+ def pages_enabled?
+ !!project_feature&.pages_enabled?
+ end
+
+ def analytics_enabled?
+ !!project_feature&.analytics_enabled?
+ end
+
+ def snippets_enabled?
+ !!project_feature&.snippets_enabled?
+ end
+
+ def public_pages?
+ !!project_feature&.public_pages?
+ end
+
+ def private_pages?
+ !!project_feature&.private_pages?
+ end
+
+ def operations_enabled?
+ !!project_feature&.operations_enabled?
+ end
+
+ def container_registry_enabled?
+ !!project_feature&.container_registry_enabled?
+ end
+ alias_method :container_registry_enabled, :container_registry_enabled?
+
def enable_ci
project_feature.update_attribute(:builds_access_level, ProjectFeature::ENABLED)
end
@@ -2762,10 +2858,6 @@ class Project < ApplicationRecord
instance.token
end
- def tracing_external_url
- tracing_setting&.external_url
- end
-
override :git_garbage_collect_worker_klass
def git_garbage_collect_worker_klass
Projects::GitGarbageCollectWorker
@@ -2907,6 +2999,10 @@ class Project < ApplicationRecord
build_artifacts_size_refresh&.started?
end
+ def group_group_links
+ group&.shared_with_group_links&.of_ancestors_and_self || GroupGroupLink.none
+ end
+
def security_training_available?
licensed_feature_available?(:security_training)
end
diff --git a/app/models/project_export_job.rb b/app/models/project_export_job.rb
index c7fe3d7bc10..decc71ee193 100644
--- a/app/models/project_export_job.rb
+++ b/app/models/project_export_job.rb
@@ -2,6 +2,7 @@
class ProjectExportJob < ApplicationRecord
belongs_to :project
+ has_many :relation_exports, class_name: 'Projects::ImportExport::RelationExport'
validates :project, :jid, :status, presence: true
diff --git a/app/models/project_feature.rb b/app/models/project_feature.rb
index f478af32788..0a30e125c83 100644
--- a/app/models/project_feature.rb
+++ b/app/models/project_feature.rb
@@ -69,6 +69,11 @@ class ProjectFeature < ApplicationRecord
default_value_for :metrics_dashboard_access_level, value: PRIVATE, allows_nil: false
default_value_for :operations_access_level, value: ENABLED, allows_nil: false
default_value_for :security_and_compliance_access_level, value: PRIVATE, allows_nil: false
+ default_value_for :monitor_access_level, value: ENABLED, allows_nil: false
+ default_value_for :infrastructure_access_level, value: ENABLED, allows_nil: false
+ default_value_for :feature_flags_access_level, value: ENABLED, allows_nil: false
+ default_value_for :environments_access_level, value: ENABLED, allows_nil: false
+ default_value_for :releases_access_level, value: ENABLED, allows_nil: false
default_value_for(:pages_access_level, allows_nil: false) do |feature|
if ::Gitlab::Pages.access_control_is_forced?
diff --git a/app/models/project_import_state.rb b/app/models/project_import_state.rb
index b1c1a5b6697..7711c6d604a 100644
--- a/app/models/project_import_state.rb
+++ b/app/models/project_import_state.rb
@@ -31,6 +31,10 @@ class ProjectImportState < ApplicationRecord
transition started: :finished
end
+ event :cancel do
+ transition [:none, :scheduled, :started] => :canceled
+ end
+
event :fail_op do
transition [:scheduled, :started] => :failed
end
@@ -39,6 +43,7 @@ class ProjectImportState < ApplicationRecord
state :started
state :finished
state :failed
+ state :canceled
after_transition [:none, :finished, :failed] => :scheduled do |state, _|
state.run_after_commit do
@@ -51,7 +56,7 @@ class ProjectImportState < ApplicationRecord
end
end
- after_transition any => :finished do |state, _|
+ after_transition any => [:canceled, :finished] do |state, _|
if state.jid.present?
Gitlab::SidekiqStatus.unset(state.jid)
@@ -59,7 +64,7 @@ class ProjectImportState < ApplicationRecord
end
end
- after_transition any => :failed do |state, _|
+ after_transition any => [:canceled, :failed] do |state, _|
state.project.remove_import_data
end
diff --git a/app/models/project_setting.rb b/app/models/project_setting.rb
index e9fd7e4446c..59d2e3deb4f 100644
--- a/app/models/project_setting.rb
+++ b/app/models/project_setting.rb
@@ -5,6 +5,8 @@ class ProjectSetting < ApplicationRecord
belongs_to :project, inverse_of: :project_setting
+ scope :for_projects, ->(projects) { where(project_id: projects) }
+
enum squash_option: {
never: 0,
always: 1,
diff --git a/app/models/project_team.rb b/app/models/project_team.rb
index 97ab5aa2619..5641fbfb867 100644
--- a/app/models/project_team.rb
+++ b/app/models/project_team.rb
@@ -8,23 +8,23 @@ class ProjectTeam
end
def add_guest(user, current_user: nil)
- add_user(user, :guest, current_user: current_user)
+ add_member(user, :guest, current_user: current_user)
end
def add_reporter(user, current_user: nil)
- add_user(user, :reporter, current_user: current_user)
+ add_member(user, :reporter, current_user: current_user)
end
def add_developer(user, current_user: nil)
- add_user(user, :developer, current_user: current_user)
+ add_member(user, :developer, current_user: current_user)
end
def add_maintainer(user, current_user: nil)
- add_user(user, :maintainer, current_user: current_user)
+ add_member(user, :maintainer, current_user: current_user)
end
def add_owner(user, current_user: nil)
- add_user(user, :owner, current_user: current_user)
+ add_member(user, :owner, current_user: current_user)
end
def add_role(user, role, current_user: nil)
@@ -43,8 +43,8 @@ class ProjectTeam
member
end
- def add_users(users, access_level, current_user: nil, expires_at: nil, tasks_to_be_done: [], tasks_project_id: nil)
- Members::Projects::CreatorService.add_users( # rubocop:disable CodeReuse/ServiceClass
+ def add_members(users, access_level, current_user: nil, expires_at: nil, tasks_to_be_done: [], tasks_project_id: nil)
+ Members::Projects::CreatorService.add_members( # rubocop:disable CodeReuse/ServiceClass
project,
users,
access_level,
@@ -55,8 +55,8 @@ class ProjectTeam
)
end
- def add_user(user, access_level, current_user: nil, expires_at: nil)
- Members::Projects::CreatorService.add_user( # rubocop:disable CodeReuse/ServiceClass
+ def add_member(user, access_level, current_user: nil, expires_at: nil)
+ Members::Projects::CreatorService.add_member( # rubocop:disable CodeReuse/ServiceClass
project,
user,
access_level,
diff --git a/app/models/project_tracing_setting.rb b/app/models/project_tracing_setting.rb
deleted file mode 100644
index 93fa80aed67..00000000000
--- a/app/models/project_tracing_setting.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-# frozen_string_literal: true
-
-class ProjectTracingSetting < ApplicationRecord
- belongs_to :project
-
- validates :external_url, length: { maximum: 255 }, public_url: true
-
- before_validation :sanitize_external_url
-
- private
-
- def sanitize_external_url
- self.external_url = Rails::Html::FullSanitizer.new.sanitize(self.external_url)
- end
-end
diff --git a/app/models/projects/import_export/relation_export.rb b/app/models/projects/import_export/relation_export.rb
new file mode 100644
index 00000000000..0a31e525ac2
--- /dev/null
+++ b/app/models/projects/import_export/relation_export.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+module Projects
+ module ImportExport
+ class RelationExport < ApplicationRecord
+ self.table_name = 'project_relation_exports'
+
+ belongs_to :project_export_job
+
+ has_one :upload,
+ class_name: 'Projects::ImportExport::RelationExportUpload',
+ foreign_key: :project_relation_export_id,
+ inverse_of: :relation_export
+
+ validates :export_error, length: { maximum: 300 }
+ validates :jid, length: { maximum: 255 }
+ validates :project_export_job, presence: true
+ validates :relation, presence: true, length: { maximum: 255 }, uniqueness: { scope: :project_export_job_id }
+ validates :status, numericality: { only_integer: true }, presence: true
+ end
+ end
+end
diff --git a/app/models/projects/import_export/relation_export_upload.rb b/app/models/projects/import_export/relation_export_upload.rb
new file mode 100644
index 00000000000..965dc39d19f
--- /dev/null
+++ b/app/models/projects/import_export/relation_export_upload.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module Projects
+ module ImportExport
+ class RelationExportUpload < ApplicationRecord
+ include WithUploads
+ include ObjectStorage::BackgroundMove
+
+ self.table_name = 'project_relation_export_uploads'
+
+ belongs_to :relation_export,
+ class_name: 'Projects::ImportExport::RelationExport',
+ foreign_key: :project_relation_export_id,
+ inverse_of: :upload
+
+ mount_uploader :export_file, ImportExportUploader
+ end
+ end
+end
diff --git a/app/models/protected_branch.rb b/app/models/protected_branch.rb
index 77038d52efe..7cf15439b47 100644
--- a/app/models/protected_branch.rb
+++ b/app/models/protected_branch.rb
@@ -4,6 +4,8 @@ class ProtectedBranch < ApplicationRecord
include ProtectedRef
include Gitlab::SQL::Pattern
+ CACHE_EXPIRE_IN = 1.hour
+
scope :requiring_code_owner_approval,
-> { where(code_owner_approval_required: true) }
@@ -29,7 +31,7 @@ class ProtectedBranch < ApplicationRecord
return true if project.empty_repo? && project.default_branch_protected?
return false if ref_name.blank?
- Rails.cache.fetch(protected_ref_cache_key(project, ref_name)) do
+ Rails.cache.fetch(protected_ref_cache_key(project, ref_name), expires_in: CACHE_EXPIRE_IN) do
self.matching(ref_name, protected_refs: protected_refs(project)).present?
end
end
diff --git a/app/models/remote_mirror.rb b/app/models/remote_mirror.rb
index 7f41f0907d5..f8d500e106b 100644
--- a/app/models/remote_mirror.rb
+++ b/app/models/remote_mirror.rb
@@ -128,7 +128,7 @@ class RemoteMirror < ApplicationRecord
def sync
return unless sync?
- if recently_scheduled?
+ if schedule_with_delay?
RepositoryUpdateRemoteMirrorWorker.perform_in(backoff_delay, self.id, Time.current)
else
RepositoryUpdateRemoteMirrorWorker.perform_async(self.id, Time.current)
@@ -261,7 +261,8 @@ class RemoteMirror < ApplicationRecord
super
end
- def recently_scheduled?
+ def schedule_with_delay?
+ return false if Feature.enabled?(:remote_mirror_no_delay, project, type: :ops)
return false unless self.last_update_started_at
self.last_update_started_at >= Time.current - backoff_delay
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 0135020e586..0da71d87457 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -1049,8 +1049,8 @@ class Repository
blob_data_at(sha, '.lfsconfig')
end
- def changelog_config(ref = 'HEAD')
- blob_data_at(ref, Gitlab::Changelog::Config::FILE_PATH)
+ def changelog_config(ref, path)
+ blob_data_at(ref, path)
end
def fetch_ref(source_repository, source_ref:, target_ref:)
diff --git a/app/models/snippet.rb b/app/models/snippet.rb
index cf4b83d44c2..c813c5cb5b8 100644
--- a/app/models/snippet.rb
+++ b/app/models/snippet.rb
@@ -18,6 +18,7 @@ class Snippet < ApplicationRecord
include CanMoveRepositoryStorage
include AfterCommitQueue
extend ::Gitlab::Utils::Override
+ include CreatedAtFilterable
MAX_FILE_COUNT = 10
diff --git a/app/models/ssh_host_key.rb b/app/models/ssh_host_key.rb
index ac7ba9530dd..daa64f4e087 100644
--- a/app/models/ssh_host_key.rb
+++ b/app/models/ssh_host_key.rb
@@ -12,7 +12,15 @@ class SshHostKey
end
def as_json(*)
- { bits: bits, fingerprint: fingerprint, type: type, index: index }
+ { bits: bits, type: type, index: index }.merge(fingerprint_data)
+ end
+
+ private
+
+ def fingerprint_data
+ data = { fingerprint_sha256: fingerprint_sha256 }
+ data[:fingerprint] = fingerprint unless Gitlab::FIPS.enabled?
+ data
end
end
diff --git a/app/models/terraform/state.rb b/app/models/terraform/state.rb
index 4d17a4d332c..59f7d852ce6 100644
--- a/app/models/terraform/state.rb
+++ b/app/models/terraform/state.rb
@@ -3,6 +3,7 @@
module Terraform
class State < ApplicationRecord
include UsageStatistics
+ include AfterCommitQueue
HEX_REGEXP = %r{\A\h+\z}.freeze
UUID_LENGTH = 32
diff --git a/app/models/todo.rb b/app/models/todo.rb
index 45ab770a0f6..cff7a93f72f 100644
--- a/app/models/todo.rb
+++ b/app/models/todo.rb
@@ -230,6 +230,10 @@ class Todo < ApplicationRecord
target_type == AlertManagement::Alert.name
end
+ def for_issue_or_work_item?
+ [Issue.name, WorkItem.name].any? { |klass_name| target_type == klass_name }
+ end
+
# override to return commits, which are not active record
def target
if for_commit?
diff --git a/app/models/user.rb b/app/models/user.rb
index 40096dfa411..12f434db631 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -9,6 +9,7 @@ class User < ApplicationRecord
include Gitlab::SQL::Pattern
include AfterCommitQueue
include Avatarable
+ include Awareness
include Referable
include Sortable
include CaseSensitivity
@@ -80,7 +81,7 @@ class User < ApplicationRecord
serialize :otp_backup_codes, JSON # rubocop:disable Cop/ActiveRecordSerialize
devise :lockable, :recoverable, :rememberable, :trackable,
- :validatable, :omniauthable, :confirmable, :registerable
+ :validatable, :omniauthable, :confirmable, :registerable, :pbkdf2_encryptable
include AdminChangedPasswordNotifier
@@ -88,6 +89,7 @@ class User < ApplicationRecord
# and should be added after Devise modules are initialized.
include AsyncDeviseEmail
include ForcedEmailConfirmation
+ include RequireEmailVerification
MINIMUM_INACTIVE_DAYS = 90
MINIMUM_DAYS_CREATED = 7
@@ -220,6 +222,7 @@ class User < ApplicationRecord
has_many :custom_attributes, class_name: 'UserCustomAttribute'
has_many :callouts, class_name: 'Users::Callout'
has_many :group_callouts, class_name: 'Users::GroupCallout'
+ has_many :namespace_callouts, class_name: 'Users::NamespaceCallout'
has_many :term_agreements
belongs_to :accepted_term, class_name: 'ApplicationSetting::Term'
@@ -476,8 +479,8 @@ class User < ApplicationRecord
end
scope :order_recent_sign_in, -> { reorder(arel_table[:current_sign_in_at].desc.nulls_last) }
scope :order_oldest_sign_in, -> { reorder(arel_table[:current_sign_in_at].asc.nulls_last) }
- scope :order_recent_last_activity, -> { reorder(arel_table[:last_activity_on].desc.nulls_last) }
- scope :order_oldest_last_activity, -> { reorder(arel_table[:last_activity_on].asc.nulls_first) }
+ scope :order_recent_last_activity, -> { reorder(arel_table[:last_activity_on].desc.nulls_last, arel_table[:id].asc) }
+ scope :order_oldest_last_activity, -> { reorder(arel_table[:last_activity_on].asc.nulls_first, arel_table[:id].desc) }
scope :by_id_and_login, ->(id, login) { where(id: id).where('username = LOWER(:login) OR email = LOWER(:login)', login: login) }
scope :dormant, -> { with_state(:active).human_or_service_user.where('last_activity_on <= ?', MINIMUM_INACTIVE_DAYS.day.ago.to_date) }
scope :with_no_activity, -> { with_state(:active).human_or_service_user.where(last_activity_on: nil).where('created_at <= ?', MINIMUM_DAYS_CREATED.day.ago.to_date) }
@@ -687,7 +690,33 @@ class User < ApplicationRecord
scope = options[:with_private_emails] ? with_primary_or_secondary_email(query) : with_public_email(query)
scope = scope.or(search_by_name_or_username(query, use_minimum_char_limit: options[:use_minimum_char_limit]))
- scope.reorder(sanitized_order_sql, :name)
+ if Feature.enabled?(:use_keyset_aware_user_search_query)
+ order = Gitlab::Pagination::Keyset::Order.build([
+ Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
+ attribute_name: 'users_match_priority',
+ order_expression: sanitized_order_sql.asc,
+ add_to_projections: true,
+ distinct: false
+ ),
+ Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
+ attribute_name: 'users_name',
+ order_expression: arel_table[:name].asc,
+ add_to_projections: true,
+ nullable: :not_nullable,
+ distinct: false
+ ),
+ Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
+ attribute_name: 'users_id',
+ order_expression: arel_table[:id].asc,
+ add_to_projections: true,
+ nullable: :not_nullable,
+ distinct: true
+ )
+ ])
+ scope.reorder(order)
+ else
+ scope.reorder(sanitized_order_sql, :name)
+ end
end
# Limits the result set to users _not_ in the given query/list of IDs.
@@ -894,21 +923,59 @@ class User < ApplicationRecord
reset_password_sent_at.present? && reset_password_sent_at >= 1.minute.ago
end
- # See https://gitlab.com/gitlab-org/security/gitlab/-/issues/638
- DISALLOWED_PASSWORDS = %w[123qweQWE!@#000000000].freeze
+ def authenticatable_salt
+ return encrypted_password[0, 29] unless Feature.enabled?(:pbkdf2_password_encryption)
+ return super if password_strategy == :pbkdf2_sha512
+
+ encrypted_password[0, 29]
+ end
# Overwrites valid_password? from Devise::Models::DatabaseAuthenticatable
# In constant-time, check both that the password isn't on a denylist AND
# that the password is the user's password
def valid_password?(password)
+ return false unless password_allowed?(password)
+ return super if Feature.enabled?(:pbkdf2_password_encryption)
+
+ Devise::Encryptor.compare(self.class, encrypted_password, password)
+ rescue Devise::Pbkdf2Encryptable::Encryptors::InvalidHash
+ validate_and_migrate_bcrypt_password(password)
+ rescue ::BCrypt::Errors::InvalidHash
+ false
+ end
+
+ # This method should be removed once the :pbkdf2_password_encryption feature flag is removed.
+ def password=(new_password)
+ if Feature.enabled?(:pbkdf2_password_encryption) && Feature.enabled?(:pbkdf2_password_encryption_write, self)
+ super
+ else
+ # Copied from Devise DatabaseAuthenticatable.
+ @password = new_password
+ self.encrypted_password = Devise::Encryptor.digest(self.class, new_password) if new_password.present?
+ end
+ end
+
+ def password_strategy
+ super
+ rescue Devise::Pbkdf2Encryptable::Encryptors::InvalidHash
+ begin
+ return :bcrypt if BCrypt::Password.new(encrypted_password)
+ rescue BCrypt::Errors::InvalidHash
+ :unknown
+ end
+ end
+
+ # See https://gitlab.com/gitlab-org/security/gitlab/-/issues/638
+ DISALLOWED_PASSWORDS = %w[123qweQWE!@#000000000].freeze
+
+ def password_allowed?(password)
password_allowed = true
+
DISALLOWED_PASSWORDS.each do |disallowed_password|
password_allowed = false if Devise.secure_compare(password, disallowed_password)
end
- original_result = super
-
- password_allowed && original_result
+ password_allowed
end
def remember_me!
@@ -1570,6 +1637,10 @@ class User < ApplicationRecord
self.followees.exists?(user.id)
end
+ def followed_by?(user)
+ self.followers.include?(user)
+ end
+
def follow(user)
return false if self.id == user.id
@@ -1625,7 +1696,7 @@ class User < ApplicationRecord
end
def oauth_authorized_tokens
- Doorkeeper::AccessToken.where(resource_owner_id: id, revoked_at: nil)
+ OauthAccessToken.where(resource_owner_id: id, revoked_at: nil)
end
# Returns the projects a user contributed to in the last year.
@@ -1899,7 +1970,7 @@ class User < ApplicationRecord
end
# override, from Devise
- def lock_access!
+ def lock_access!(opts = {})
Gitlab::AppLogger.info("Account Locked: username=#{username}")
super
end
@@ -2015,6 +2086,13 @@ class User < ApplicationRecord
callout_dismissed?(callout, ignore_dismissal_earlier_than)
end
+ def dismissed_callout_for_namespace?(feature_name:, namespace:, ignore_dismissal_earlier_than: nil)
+ source_feature_name = "#{feature_name}_#{namespace.id}"
+ callout = namespace_callouts_by_feature_name[source_feature_name]
+
+ callout_dismissed?(callout, ignore_dismissal_earlier_than)
+ end
+
# Load the current highest access by looking directly at the user's memberships
def current_highest_access_level
members.non_request.maximum(:access_level)
@@ -2041,6 +2119,11 @@ class User < ApplicationRecord
.find_or_initialize_by(feature_name: ::Users::GroupCallout.feature_names[feature_name], group_id: group_id)
end
+ def find_or_initialize_namespace_callout(feature_name, namespace_id)
+ namespace_callouts
+ .find_or_initialize_by(feature_name: ::Users::NamespaceCallout.feature_names[feature_name], namespace_id: namespace_id)
+ end
+
def can_trigger_notifications?
confirmed? && !blocked? && !ghost?
end
@@ -2158,6 +2241,10 @@ class User < ApplicationRecord
@group_callouts_by_feature_name ||= group_callouts.index_by(&:source_feature_name)
end
+ def namespace_callouts_by_feature_name
+ @namespace_callouts_by_feature_name ||= namespace_callouts.index_by(&:source_feature_name)
+ end
+
def authorized_groups_without_shared_membership
Group.from_union([
groups.select(*Namespace.cached_column_list),
@@ -2318,6 +2405,15 @@ class User < ApplicationRecord
Ci::NamespaceMirror.contains_traversal_ids(traversal_ids)
end
+
+ def validate_and_migrate_bcrypt_password(password)
+ return false unless Devise::Encryptor.compare(self.class, encrypted_password, password)
+ return true unless Feature.enabled?(:pbkdf2_password_encryption_write, self)
+
+ update_attribute(:password, password)
+ rescue ::BCrypt::Errors::InvalidHash
+ false
+ end
end
User.prepend_mod_with('User')
diff --git a/app/models/users/callout.rb b/app/models/users/callout.rb
index 0ecae4d148a..570e3ae9b3c 100644
--- a/app/models/users/callout.rb
+++ b/app/models/users/callout.rb
@@ -49,11 +49,14 @@ module Users
storage_enforcement_banner_fourth_enforcement_threshold: 46,
attention_requests_top_nav: 47,
attention_requests_side_nav: 48,
- minute_limit_banner: 49,
+ # 49 was removed with https://gitlab.com/gitlab-org/gitlab/-/merge_requests/91533
+ # because the banner was no longer relevant.
+ # Records will be migrated with https://gitlab.com/gitlab-org/gitlab/-/issues/367293
preview_user_over_limit_free_plan_alert: 50, # EE-only
user_reached_limit_free_plan_alert: 51, # EE-only
submit_license_usage_data_banner: 52, # EE-only
- personal_project_limitations_banner: 53 # EE-only
+ personal_project_limitations_banner: 53, # EE-only
+ mr_experience_survey: 54
}
validates :feature_name,
diff --git a/app/models/users/group_callout.rb b/app/models/users/group_callout.rb
index 373bc30889f..0ea7b8199aa 100644
--- a/app/models/users/group_callout.rb
+++ b/app/models/users/group_callout.rb
@@ -16,7 +16,8 @@ module Users
storage_enforcement_banner_third_enforcement_threshold: 5,
storage_enforcement_banner_fourth_enforcement_threshold: 6,
preview_user_over_limit_free_plan_alert: 7, # EE-only
- user_reached_limit_free_plan_alert: 8 # EE-only
+ user_reached_limit_free_plan_alert: 8, # EE-only
+ free_group_limited_alert: 9 # EE-only
}
validates :group, presence: true
diff --git a/app/models/users/in_product_marketing_email.rb b/app/models/users/in_product_marketing_email.rb
index 82c2e336a09..f220cfd17c5 100644
--- a/app/models/users/in_product_marketing_email.rb
+++ b/app/models/users/in_product_marketing_email.rb
@@ -41,7 +41,7 @@ module Users
# Tracks we don't send emails for (e.g. unsuccessful experiment). These
# are kept since we already have DB records that use the enum value.
- INACTIVE_TRACK_NAMES = %w(invite_team).freeze
+ INACTIVE_TRACK_NAMES = %w[invite_team experience].freeze
ACTIVE_TRACKS = tracks.except(*INACTIVE_TRACK_NAMES)
scope :for_user_with_track_and_series, -> (user, track, series) do
diff --git a/app/models/users/namespace_callout.rb b/app/models/users/namespace_callout.rb
new file mode 100644
index 00000000000..a20a196a4ef
--- /dev/null
+++ b/app/models/users/namespace_callout.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+module Users
+ class NamespaceCallout < ApplicationRecord
+ include Users::Calloutable
+
+ self.table_name = 'user_namespace_callouts'
+
+ belongs_to :namespace
+
+ enum feature_name: {
+ invite_members_banner: 1,
+ approaching_seat_count_threshold: 2, # EE-only
+ storage_enforcement_banner_first_enforcement_threshold: 3,
+ storage_enforcement_banner_second_enforcement_threshold: 4,
+ storage_enforcement_banner_third_enforcement_threshold: 5,
+ storage_enforcement_banner_fourth_enforcement_threshold: 6,
+ preview_user_over_limit_free_plan_alert: 7, # EE-only
+ user_reached_limit_free_plan_alert: 8, # EE-only
+ web_hook_disabled: 9
+ }
+
+ validates :namespace, presence: true
+ validates :feature_name,
+ presence: true,
+ uniqueness: { scope: [:user_id, :namespace_id] },
+ inclusion: { in: NamespaceCallout.feature_names.keys }
+
+ def source_feature_name
+ "#{feature_name}_#{namespace_id}"
+ end
+ end
+end
diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb
index 647b4e787c6..63c60f5a89e 100644
--- a/app/models/wiki_page.rb
+++ b/app/models/wiki_page.rb
@@ -316,6 +316,7 @@ class WikiPage
end
def update_front_matter(attrs)
+ return unless Gitlab::WikiPages::FrontMatterParser.enabled?(container)
return unless attrs.has_key?(:front_matter)
fm_yaml = serialize_front_matter(attrs[:front_matter])
@@ -326,7 +327,7 @@ class WikiPage
def parsed_content
strong_memoize(:parsed_content) do
- Gitlab::WikiPages::FrontMatterParser.new(raw_content).parse
+ Gitlab::WikiPages::FrontMatterParser.new(raw_content, container).parse
end
end
diff --git a/app/models/work_item.rb b/app/models/work_item.rb
index bdd9aae90a4..d29df0c31fc 100644
--- a/app/models/work_item.rb
+++ b/app/models/work_item.rb
@@ -1,9 +1,12 @@
# frozen_string_literal: true
class WorkItem < Issue
+ include Gitlab::Utils::StrongMemoize
+
self.table_name = 'issues'
self.inheritance_column = :_type_disabled
+ belongs_to :namespace, inverse_of: :work_items
has_one :parent_link, class_name: '::WorkItems::ParentLink', foreign_key: :work_item_id
has_one :work_item_parent, through: :parent_link, class_name: 'WorkItem'
@@ -22,8 +25,10 @@ class WorkItem < Issue
end
def widgets
- work_item_type.widgets.map do |widget_class|
- widget_class.new(self)
+ strong_memoize(:widgets) do
+ work_item_type.widgets.map do |widget_class|
+ widget_class.new(self)
+ end
end
end
diff --git a/app/models/work_items/parent_link.rb b/app/models/work_items/parent_link.rb
index 3c405dbce3b..f5ebbfa59b8 100644
--- a/app/models/work_items/parent_link.rb
+++ b/app/models/work_items/parent_link.rb
@@ -5,11 +5,13 @@ module WorkItems
self.table_name = 'work_item_parent_links'
MAX_CHILDREN = 100
+ PARENT_TYPES = [:issue, :incident].freeze
belongs_to :work_item
belongs_to :work_item_parent, class_name: 'WorkItem'
- validates :work_item, :work_item_parent, presence: true
+ validates :work_item_parent, presence: true
+ validates :work_item, presence: true, uniqueness: true
validate :validate_child_type
validate :validate_parent_type
validate :validate_same_project
@@ -21,15 +23,20 @@ module WorkItems
return unless work_item
unless work_item.task?
- errors.add :work_item, _('Only Task can be assigned as a child in hierarchy.')
+ errors.add :work_item, _('only Task can be assigned as a child in hierarchy.')
end
end
def validate_parent_type
return unless work_item_parent
- unless work_item_parent.issue?
- errors.add :work_item_parent, _('Only Issue can be parent of Task.')
+ base_type = work_item_parent.work_item_type.base_type.to_sym
+ unless PARENT_TYPES.include?(base_type)
+ parent_names = WorkItems::Type::BASE_TYPES.slice(*WorkItems::ParentLink::PARENT_TYPES)
+ .values.map { |type| type[:name] }
+
+ errors.add :work_item_parent, _('only %{parent_types} can be parent of Task.') %
+ { parent_types: parent_names.to_sentence }
end
end
@@ -37,7 +44,7 @@ module WorkItems
return if work_item.nil? || work_item_parent.nil?
if work_item.resource_parent != work_item_parent.resource_parent
- errors.add :work_item_parent, _('Parent must be in the same project as child.')
+ errors.add :work_item_parent, _('parent must be in the same project as child.')
end
end
@@ -46,7 +53,7 @@ module WorkItems
max = persisted? ? MAX_CHILDREN : MAX_CHILDREN - 1
if work_item_parent.child_links.count > max
- errors.add :work_item_parent, _('Parent already has maximum number of children.')
+ errors.add :work_item_parent, _('parent already has maximum number of children.')
end
end
end
diff --git a/app/models/work_items/type.rb b/app/models/work_items/type.rb
index bf251a3ade5..e38d0ae153a 100644
--- a/app/models/work_items/type.rb
+++ b/app/models/work_items/type.rb
@@ -21,11 +21,11 @@ module WorkItems
}.freeze
WIDGETS_FOR_TYPE = {
- issue: [Widgets::Description, Widgets::Hierarchy],
- incident: [Widgets::Description],
+ issue: [Widgets::Assignees, Widgets::Description, Widgets::Hierarchy, Widgets::Weight],
+ incident: [Widgets::Description, Widgets::Hierarchy],
test_case: [Widgets::Description],
requirement: [Widgets::Description],
- task: [Widgets::Description, Widgets::Hierarchy]
+ task: [Widgets::Assignees, Widgets::Description, Widgets::Hierarchy, Widgets::Weight]
}.freeze
cache_markdown_field :description, pipeline: :single_line
diff --git a/app/models/work_items/widgets/assignees.rb b/app/models/work_items/widgets/assignees.rb
new file mode 100644
index 00000000000..ecbbee1bcfb
--- /dev/null
+++ b/app/models/work_items/widgets/assignees.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+module WorkItems
+ module Widgets
+ class Assignees < Base
+ delegate :assignees, to: :work_item
+ delegate :allows_multiple_assignees?, to: :work_item
+ end
+ end
+end
diff --git a/app/models/work_items/widgets/description.rb b/app/models/work_items/widgets/description.rb
index 35b6d295321..1e84d172bef 100644
--- a/app/models/work_items/widgets/description.rb
+++ b/app/models/work_items/widgets/description.rb
@@ -4,10 +4,6 @@ module WorkItems
module Widgets
class Description < Base
delegate :description, to: :work_item
-
- def update(params:)
- work_item.description = params[:description] if params&.key?(:description)
- end
end
end
end
diff --git a/app/models/work_items/widgets/hierarchy.rb b/app/models/work_items/widgets/hierarchy.rb
index dadd341de83..930aced8ace 100644
--- a/app/models/work_items/widgets/hierarchy.rb
+++ b/app/models/work_items/widgets/hierarchy.rb
@@ -4,13 +4,13 @@ module WorkItems
module Widgets
class Hierarchy < Base
def parent
- return unless Feature.enabled?(:work_items_hierarchy, work_item.project)
+ return unless work_item.project.work_items_feature_flag_enabled?
work_item.work_item_parent
end
def children
- return WorkItem.none unless Feature.enabled?(:work_items_hierarchy, work_item.project)
+ return WorkItem.none unless work_item.project.work_items_feature_flag_enabled?
work_item.work_item_children
end
diff --git a/app/models/work_items/widgets/weight.rb b/app/models/work_items/widgets/weight.rb
new file mode 100644
index 00000000000..f589378f307
--- /dev/null
+++ b/app/models/work_items/widgets/weight.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module WorkItems
+ module Widgets
+ class Weight < Base
+ delegate :weight, to: :work_item
+ end
+ end
+end
diff --git a/app/models/x509_certificate.rb b/app/models/x509_certificate.rb
index 2c1d0110b7c..7c2581b8bb2 100644
--- a/app/models/x509_certificate.rb
+++ b/app/models/x509_certificate.rb
@@ -16,7 +16,7 @@ class X509Certificate < ApplicationRecord
has_many :x509_commit_signatures, class_name: 'CommitSignatures::X509CommitSignature', inverse_of: 'x509_certificate'
# rfc 5280 - 4.2.1.2 Subject Key Identifier
- validates :subject_key_identifier, presence: true, format: { with: /\A(\h{2}:){19}\h{2}\z/ }
+ validates :subject_key_identifier, presence: true, format: { with: Gitlab::Regex.x509_subject_key_identifier_regex }
# rfc 5280 - 4.1.2.6 Subject
validates :subject, presence: true
# rfc 5280 - 4.1.2.6 Subject (subjectAltName contains the email address)
diff --git a/app/models/x509_issuer.rb b/app/models/x509_issuer.rb
index 4b75e38bbde..81491d8e507 100644
--- a/app/models/x509_issuer.rb
+++ b/app/models/x509_issuer.rb
@@ -4,7 +4,7 @@ class X509Issuer < ApplicationRecord
has_many :x509_certificates, inverse_of: 'x509_issuer'
# rfc 5280 - 4.2.1.1 Authority Key Identifier
- validates :subject_key_identifier, presence: true, format: { with: /\A(\h{2}:){19}\h{2}\z/ }
+ validates :subject_key_identifier, presence: true, format: { with: Gitlab::Regex.x509_subject_key_identifier_regex }
# rfc 5280 - 4.1.2.4 Issuer
validates :subject, presence: true
# rfc 5280 - 4.2.1.13 CRL Distribution Points