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/concerns')
-rw-r--r--app/models/concerns/blocks_json_serialization.rb18
-rw-r--r--app/models/concerns/blocks_unsafe_serialization.rb32
-rw-r--r--app/models/concerns/bulk_member_access_load.rb52
-rw-r--r--app/models/concerns/ci/has_deployment_name.rb15
-rw-r--r--app/models/concerns/ci/has_status.rb6
-rw-r--r--app/models/concerns/counter_attribute.rb26
-rw-r--r--app/models/concerns/deployment_platform.rb2
-rw-r--r--app/models/concerns/has_user_type.rb13
-rw-r--r--app/models/concerns/integrations/has_issue_tracker_fields.rb30
-rw-r--r--app/models/concerns/issuable.rb60
-rw-r--r--app/models/concerns/issuable_link.rb55
-rw-r--r--app/models/concerns/issue_resource_event.rb6
-rw-r--r--app/models/concerns/merge_request_reviewer_state.rb8
-rw-r--r--app/models/concerns/pg_full_text_searchable.rb111
-rw-r--r--app/models/concerns/runners_token_prefixable.rb6
-rw-r--r--app/models/concerns/select_for_project_authorization.rb6
-rw-r--r--app/models/concerns/sensitive_serializable_hash.rb46
-rw-r--r--app/models/concerns/spammable.rb11
-rw-r--r--app/models/concerns/timebox.rb1
-rw-r--r--app/models/concerns/token_authenticatable.rb10
-rw-r--r--app/models/concerns/token_authenticatable_strategies/base.rb8
-rw-r--r--app/models/concerns/token_authenticatable_strategies/digest.rb4
-rw-r--r--app/models/concerns/token_authenticatable_strategies/encrypted.rb4
-rw-r--r--app/models/concerns/update_namespace_statistics.rb54
24 files changed, 477 insertions, 107 deletions
diff --git a/app/models/concerns/blocks_json_serialization.rb b/app/models/concerns/blocks_json_serialization.rb
deleted file mode 100644
index 18c00532d78..00000000000
--- a/app/models/concerns/blocks_json_serialization.rb
+++ /dev/null
@@ -1,18 +0,0 @@
-# frozen_string_literal: true
-
-# Overrides `as_json` and `to_json` to raise an exception when called in order
-# to prevent accidentally exposing attributes
-#
-# Not that would ever happen... but just in case.
-module BlocksJsonSerialization
- extend ActiveSupport::Concern
-
- JsonSerializationError = Class.new(StandardError)
-
- def to_json(*)
- raise JsonSerializationError,
- "JSON serialization has been disabled on #{self.class.name}"
- end
-
- alias_method :as_json, :to_json
-end
diff --git a/app/models/concerns/blocks_unsafe_serialization.rb b/app/models/concerns/blocks_unsafe_serialization.rb
new file mode 100644
index 00000000000..72adbe70f15
--- /dev/null
+++ b/app/models/concerns/blocks_unsafe_serialization.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+# Overrides `#serializable_hash` to raise an exception when called without the `only` option
+# in order to prevent accidentally exposing attributes.
+#
+# An `unsafe: true` option can also be passed in to bypass this check.
+#
+# `#serializable_hash` is used by ActiveModel serializers like `ActiveModel::Serializers::JSON`
+# which overrides `#as_json` and `#to_json`.
+#
+module BlocksUnsafeSerialization
+ extend ActiveSupport::Concern
+ extend ::Gitlab::Utils::Override
+
+ UnsafeSerializationError = Class.new(StandardError)
+
+ override :serializable_hash
+ def serializable_hash(options = nil)
+ return super if allow_serialization?(options)
+
+ raise UnsafeSerializationError,
+ "Serialization has been disabled on #{self.class.name}"
+ end
+
+ private
+
+ def allow_serialization?(options = nil)
+ return false unless options
+
+ !!(options[:only] || options[:unsafe])
+ end
+end
diff --git a/app/models/concerns/bulk_member_access_load.rb b/app/models/concerns/bulk_member_access_load.rb
index 927d6ccb28f..efc65e55e40 100644
--- a/app/models/concerns/bulk_member_access_load.rb
+++ b/app/models/concerns/bulk_member_access_load.rb
@@ -1,61 +1,19 @@
# frozen_string_literal: true
-# Returns and caches in thread max member access for a resource
-#
module BulkMemberAccessLoad
extend ActiveSupport::Concern
included do
- # Determine the maximum access level for a group of resources in bulk.
- #
- # Returns a Hash mapping resource ID -> maximum access level.
- def max_member_access_for_resource_ids(resource_klass, resource_ids, &block)
- raise 'Block is mandatory' unless block_given?
-
- memoization_index = self.id
- memoization_class = self.class
-
- resource_ids = resource_ids.uniq
- memo_id = "#{memoization_class}:#{memoization_index}"
- access = load_access_hash(resource_klass, memo_id)
-
- # Look up only the IDs we need
- resource_ids -= access.keys
-
- return access if resource_ids.empty?
-
- resource_access = yield(resource_ids)
-
- access.merge!(resource_access)
-
- missing_resource_ids = resource_ids - resource_access.keys
-
- missing_resource_ids.each do |resource_id|
- access[resource_id] = Gitlab::Access::NO_ACCESS
- end
-
- access
- end
-
def merge_value_to_request_store(resource_klass, resource_id, value)
- max_member_access_for_resource_ids(resource_klass, [resource_id]) do
+ Gitlab::SafeRequestLoader.execute(resource_key: max_member_access_for_resource_key(resource_klass),
+ resource_ids: [resource_id],
+ default_value: Gitlab::Access::NO_ACCESS) do
{ resource_id => value }
end
end
- private
-
- def max_member_access_for_resource_key(klass, memoization_index)
- "max_member_access_for_#{klass.name.underscore.pluralize}:#{memoization_index}"
- end
-
- def load_access_hash(resource_klass, memo_id)
- return {} unless Gitlab::SafeRequestStore.active?
-
- key = max_member_access_for_resource_key(resource_klass, memo_id)
- Gitlab::SafeRequestStore[key] ||= {}
-
- Gitlab::SafeRequestStore[key]
+ def max_member_access_for_resource_key(klass)
+ "max_member_access_for_#{klass.name.underscore.pluralize}:#{self.class}:#{self.id}"
end
end
end
diff --git a/app/models/concerns/ci/has_deployment_name.rb b/app/models/concerns/ci/has_deployment_name.rb
new file mode 100644
index 00000000000..fe288134872
--- /dev/null
+++ b/app/models/concerns/ci/has_deployment_name.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module Ci
+ module HasDeploymentName
+ extend ActiveSupport::Concern
+
+ def count_user_deployment?
+ Feature.enabled?(:job_deployment_count) && deployment_name?
+ end
+
+ def deployment_name?
+ self.class::DEPLOYMENT_NAMES.any? { |n| name.downcase.include?(n) }
+ end
+ end
+end
diff --git a/app/models/concerns/ci/has_status.rb b/app/models/concerns/ci/has_status.rb
index ccaccec3b6b..313c767e59f 100644
--- a/app/models/concerns/ci/has_status.rb
+++ b/app/models/concerns/ci/has_status.rb
@@ -7,12 +7,16 @@ module Ci
DEFAULT_STATUS = 'created'
BLOCKED_STATUS = %w[manual scheduled].freeze
AVAILABLE_STATUSES = %w[created waiting_for_resource preparing pending running success failed canceled skipped manual scheduled].freeze
+ # TODO: replace STARTED_STATUSES with data from BUILD_STARTED_RUNNING_STATUSES in https://gitlab.com/gitlab-org/gitlab/-/issues/273378
+ # see https://gitlab.com/gitlab-org/gitlab/-/merge_requests/82149#note_865508501
+ BUILD_STARTED_RUNNING_STATUSES = %w[running success failed].freeze
STARTED_STATUSES = %w[running success failed skipped manual scheduled].freeze
ACTIVE_STATUSES = %w[waiting_for_resource preparing pending running].freeze
COMPLETED_STATUSES = %w[success failed canceled skipped].freeze
ORDERED_STATUSES = %w[failed preparing pending running waiting_for_resource manual scheduled canceled success skipped created].freeze
PASSED_WITH_WARNINGS_STATUSES = %w[failed canceled].to_set.freeze
EXCLUDE_IGNORED_STATUSES = %w[manual failed canceled].to_set.freeze
+ CANCELABLE_STATUSES = %w[running waiting_for_resource preparing pending created scheduled].freeze
STATUSES_ENUM = { created: 0, pending: 1, running: 2, success: 3,
failed: 4, canceled: 5, skipped: 6, manual: 7,
scheduled: 8, preparing: 9, waiting_for_resource: 10 }.freeze
@@ -85,7 +89,7 @@ module Ci
scope :waiting_for_resource_or_upcoming, -> { with_status(:created, :scheduled, :waiting_for_resource) }
scope :cancelable, -> do
- where(status: [:running, :waiting_for_resource, :preparing, :pending, :created, :scheduled])
+ where(status: klass::CANCELABLE_STATUSES)
end
scope :without_statuses, -> (names) do
diff --git a/app/models/concerns/counter_attribute.rb b/app/models/concerns/counter_attribute.rb
index 4bfeba338d2..b41b1ba6008 100644
--- a/app/models/concerns/counter_attribute.rb
+++ b/app/models/concerns/counter_attribute.rb
@@ -102,9 +102,7 @@ module CounterAttribute
run_after_commit_or_now do
if counter_attribute_enabled?(attribute)
- redis_state do |redis|
- redis.incrby(counter_key(attribute), increment)
- end
+ increment_counter(attribute, increment)
FlushCounterIncrementsWorker.perform_in(WORKER_DELAY, self.class.name, self.id, attribute)
else
@@ -115,6 +113,28 @@ module CounterAttribute
true
end
+ def increment_counter(attribute, increment)
+ if counter_attribute_enabled?(attribute)
+ redis_state do |redis|
+ redis.incrby(counter_key(attribute), increment)
+ end
+ end
+ end
+
+ def clear_counter!(attribute)
+ if counter_attribute_enabled?(attribute)
+ redis_state { |redis| redis.del(counter_key(attribute)) }
+ end
+ end
+
+ def get_counter_value(attribute)
+ if counter_attribute_enabled?(attribute)
+ redis_state do |redis|
+ redis.get(counter_key(attribute)).to_i
+ end
+ end
+ end
+
def counter_key(attribute)
"project:{#{project_id}}:counters:#{self.class}:#{id}:#{attribute}"
end
diff --git a/app/models/concerns/deployment_platform.rb b/app/models/concerns/deployment_platform.rb
index b6245e29746..d9c622f247a 100644
--- a/app/models/concerns/deployment_platform.rb
+++ b/app/models/concerns/deployment_platform.rb
@@ -3,6 +3,8 @@
module DeploymentPlatform
# rubocop:disable Gitlab/ModuleWithInstanceVariables
def deployment_platform(environment: nil)
+ return if Feature.disabled?(:certificate_based_clusters, default_enabled: :yaml, type: :ops)
+
@deployment_platform ||= {}
@deployment_platform[environment] ||= find_deployment_platform(environment)
diff --git a/app/models/concerns/has_user_type.rb b/app/models/concerns/has_user_type.rb
index 28ee54afaa9..ad070090dd5 100644
--- a/app/models/concerns/has_user_type.rb
+++ b/app/models/concerns/has_user_type.rb
@@ -46,4 +46,17 @@ module HasUserType
def internal?
ghost? || (bot? && !project_bot?)
end
+
+ def redacted_name(viewing_user)
+ return self.name unless self.project_bot?
+
+ return self.name if self.groups.any? && viewing_user&.can?(:read_group, self.groups.first)
+
+ return self.name if viewing_user&.can?(:read_project, self.projects.first)
+
+ # If the requester does not have permission to read the project bot name,
+ # the API returns an arbitrary string. UI changes will be addressed in a follow up issue:
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/346058
+ '****'
+ end
end
diff --git a/app/models/concerns/integrations/has_issue_tracker_fields.rb b/app/models/concerns/integrations/has_issue_tracker_fields.rb
new file mode 100644
index 00000000000..b1def38d019
--- /dev/null
+++ b/app/models/concerns/integrations/has_issue_tracker_fields.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+module Integrations
+ module HasIssueTrackerFields
+ extend ActiveSupport::Concern
+
+ included do
+ field :project_url,
+ required: true,
+ storage: :data_fields,
+ title: -> { _('Project URL') },
+ help: -> { s_('IssueTracker|The URL to the project in the external issue tracker.') }
+
+ 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}.'),
+ 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.') }
+ end
+ end
+end
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index 0138c0ad20f..1eb30e88f16 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -74,6 +74,7 @@ module Issuable
end
has_many :note_authors, -> { distinct }, through: :notes, source: :author
+ has_many :user_note_authors, -> { distinct.where("notes.system = false") }, through: :notes, source: :author
has_many :label_links, as: :target, inverse_of: :target
has_many :labels, through: :label_links
@@ -464,37 +465,54 @@ module Issuable
false
end
- def to_hook_data(user, old_associations: {})
- changes = previous_changes
+ def hook_association_changes(old_associations)
+ changes = {}
- if old_associations
- old_labels = old_associations.fetch(:labels, labels)
- old_assignees = old_associations.fetch(:assignees, assignees)
- old_severity = old_associations.fetch(:severity, severity)
+ old_labels = old_associations.fetch(:labels, labels)
+ old_assignees = old_associations.fetch(:assignees, assignees)
+ old_severity = old_associations.fetch(:severity, severity)
- if old_labels != labels
- changes[:labels] = [old_labels.map(&:hook_attrs), labels.map(&:hook_attrs)]
- end
+ if old_labels != labels
+ changes[:labels] = [old_labels.map(&:hook_attrs), labels.map(&:hook_attrs)]
+ end
- if old_assignees != assignees
- changes[:assignees] = [old_assignees.map(&:hook_attrs), assignees.map(&:hook_attrs)]
- end
+ if old_assignees != assignees
+ changes[:assignees] = [old_assignees.map(&:hook_attrs), assignees.map(&:hook_attrs)]
+ end
+
+ if supports_severity? && old_severity != severity
+ changes[:severity] = [old_severity, severity]
+ end
+
+ if supports_escalation? && escalation_status
+ current_escalation_status = escalation_status.status_name
+ old_escalation_status = old_associations.fetch(:escalation_status, current_escalation_status)
- if supports_severity? && old_severity != severity
- changes[:severity] = [old_severity, severity]
+ if old_escalation_status != current_escalation_status
+ changes[:escalation_status] = [old_escalation_status, current_escalation_status]
end
+ end
- if self.respond_to?(:total_time_spent)
- old_total_time_spent = old_associations.fetch(:total_time_spent, total_time_spent)
- old_time_change = old_associations.fetch(:time_change, time_change)
+ if self.respond_to?(:total_time_spent)
+ old_total_time_spent = old_associations.fetch(:total_time_spent, total_time_spent)
+ old_time_change = old_associations.fetch(:time_change, time_change)
- if old_total_time_spent != total_time_spent
- changes[:total_time_spent] = [old_total_time_spent, total_time_spent]
- changes[:time_change] = [old_time_change, time_change]
- end
+ if old_total_time_spent != total_time_spent
+ changes[:total_time_spent] = [old_total_time_spent, total_time_spent]
+ changes[:time_change] = [old_time_change, time_change]
end
end
+ changes
+ end
+
+ def to_hook_data(user, old_associations: {})
+ changes = previous_changes
+
+ if old_associations.present?
+ changes.merge!(hook_association_changes(old_associations))
+ end
+
Gitlab::HookData::IssuableBuilder.new(self).build(user: user, changes: changes)
end
diff --git a/app/models/concerns/issuable_link.rb b/app/models/concerns/issuable_link.rb
new file mode 100644
index 00000000000..3e14507bc70
--- /dev/null
+++ b/app/models/concerns/issuable_link.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+# == IssuableLink concern
+#
+# Contains common functionality shared between related Issues and related Epics
+#
+# Used by IssueLink, Epic::RelatedEpicLink
+#
+module IssuableLink
+ extend ActiveSupport::Concern
+
+ TYPE_RELATES_TO = 'relates_to'
+ TYPE_BLOCKS = 'blocks' ## EE-only. Kept here to be used on link_type enum.
+
+ class_methods do
+ def inverse_link_type(type)
+ type
+ end
+
+ def issuable_type
+ raise NotImplementedError
+ end
+ end
+
+ included do
+ validates :source, presence: true
+ validates :target, presence: true
+ validates :source, uniqueness: { scope: :target_id, message: 'is already related' }
+ validate :check_self_relation
+ validate :check_opposite_relation
+
+ enum link_type: { TYPE_RELATES_TO => 0, TYPE_BLOCKS => 1 }
+
+ private
+
+ def check_self_relation
+ return unless source && target
+
+ if source == target
+ errors.add(:source, 'cannot be related to itself')
+ end
+ end
+
+ def check_opposite_relation
+ return unless source && target
+
+ if self.class.base_class.find_by(source: target, target: source)
+ errors.add(:source, "is already related to this #{self.class.issuable_type}")
+ end
+ end
+ end
+end
+
+IssuableLink.prepend_mod_with('IssuableLink')
+IssuableLink::ClassMethods.prepend_mod_with('IssuableLink::ClassMethods')
diff --git a/app/models/concerns/issue_resource_event.rb b/app/models/concerns/issue_resource_event.rb
index 1c24032dbbb..5cbc937e465 100644
--- a/app/models/concerns/issue_resource_event.rb
+++ b/app/models/concerns/issue_resource_event.rb
@@ -8,6 +8,10 @@ module IssueResourceEvent
scope :by_issue, ->(issue) { where(issue_id: issue.id) }
- scope :by_issue_ids_and_created_at_earlier_or_equal_to, ->(issue_ids, time) { where(issue_id: issue_ids).where('created_at <= ?', time) }
+ scope :by_created_at_earlier_or_equal_to, ->(time) { where('created_at <= ?', time) }
+ scope :by_issue_ids, ->(issue_ids) do
+ table = self.klass.arel_table
+ where(table[:issue_id].in(issue_ids))
+ end
end
end
diff --git a/app/models/concerns/merge_request_reviewer_state.rb b/app/models/concerns/merge_request_reviewer_state.rb
index 5859f43a70c..893d06b4da8 100644
--- a/app/models/concerns/merge_request_reviewer_state.rb
+++ b/app/models/concerns/merge_request_reviewer_state.rb
@@ -14,6 +14,14 @@ module MergeRequestReviewerState
presence: true,
inclusion: { in: self.states.keys }
+ belongs_to :updated_state_by, class_name: 'User', foreign_key: :updated_state_by_user_id
+
after_initialize :set_state, unless: :persisted?
+
+ def attention_requested_by
+ return unless attention_requested?
+
+ updated_state_by
+ end
end
end
diff --git a/app/models/concerns/pg_full_text_searchable.rb b/app/models/concerns/pg_full_text_searchable.rb
new file mode 100644
index 00000000000..68357c44300
--- /dev/null
+++ b/app/models/concerns/pg_full_text_searchable.rb
@@ -0,0 +1,111 @@
+# frozen_string_literal: true
+
+# This module adds PG full-text search capabilities to a model.
+# A `search_data` association with a `search_vector` column is required.
+#
+# Declare the fields that will be part of the search vector with their
+# corresponding weights. Possible values for weight are A, B, C, or D.
+# For example:
+#
+# include PgFullTextSearchable
+# pg_full_text_searchable columns: [{ name: 'title', weight: 'A' }, { name: 'description', weight: 'B' }]
+#
+# This module sets up an after_commit hook that updates the search data
+# when the searchable columns are changed. You will need to implement the
+# `#persist_pg_full_text_search_vector` method that does the actual insert or update.
+#
+# This also adds a `pg_full_text_search` scope so you can do:
+#
+# Model.pg_full_text_search("some search term")
+
+module PgFullTextSearchable
+ extend ActiveSupport::Concern
+
+ LONG_WORDS_REGEX = %r([A-Za-z0-9+/@]{50,}).freeze
+ TSVECTOR_MAX_LENGTH = 1.megabyte.freeze
+ TEXT_SEARCH_DICTIONARY = 'english'
+
+ def update_search_data!
+ tsvector_sql_nodes = self.class.pg_full_text_searchable_columns.map do |column, weight|
+ tsvector_arel_node(column, weight)&.to_sql
+ end
+
+ persist_pg_full_text_search_vector(Arel.sql(tsvector_sql_nodes.compact.join(' || ')))
+ rescue ActiveRecord::StatementInvalid => e
+ raise unless e.cause.is_a?(PG::ProgramLimitExceeded) && e.message.include?('string is too long for tsvector')
+
+ Gitlab::AppJsonLogger.error(
+ message: 'Error updating search data: string is too long for tsvector',
+ class: self.class.name,
+ model_id: self.id
+ )
+ end
+
+ private
+
+ def persist_pg_full_text_search_vector(search_vector)
+ raise NotImplementedError
+ end
+
+ def tsvector_arel_node(column, weight)
+ return if self[column].blank?
+
+ column_text = self[column].gsub(LONG_WORDS_REGEX, ' ')
+ column_text = column_text[0..(TSVECTOR_MAX_LENGTH - 1)]
+ column_text = ActiveSupport::Inflector.transliterate(column_text)
+
+ Arel::Nodes::NamedFunction.new(
+ 'setweight',
+ [
+ Arel::Nodes::NamedFunction.new(
+ 'to_tsvector',
+ [Arel::Nodes.build_quoted(TEXT_SEARCH_DICTIONARY), Arel::Nodes.build_quoted(column_text)]
+ ),
+ Arel::Nodes.build_quoted(weight)
+ ]
+ )
+ end
+
+ included do
+ cattr_reader :pg_full_text_searchable_columns do
+ {}
+ end
+ end
+
+ class_methods do
+ def pg_full_text_searchable(columns:)
+ raise 'Full text search columns already defined!' if pg_full_text_searchable_columns.present?
+
+ columns.each do |column|
+ pg_full_text_searchable_columns[column[:name]] = column[:weight]
+ end
+
+ # We update this outside the transaction because this could raise an error if the resulting tsvector
+ # is too long. When that happens, we still persist the create / update but the model will not have a
+ # search data record. This is fine in most cases because this is a very rare occurrence and only happens
+ # with strings that are most likely unsearchable anyway.
+ #
+ # We also do not want to use a subtransaction here due to: https://gitlab.com/groups/gitlab-org/-/epics/6540
+ after_save_commit do
+ next unless pg_full_text_searchable_columns.keys.any? { |f| saved_changes.has_key?(f) }
+
+ update_search_data!
+ end
+ end
+
+ def pg_full_text_search(search_term)
+ search_data_table = reflect_on_association(:search_data).klass.arel_table
+
+ joins(:search_data).where(
+ Arel::Nodes::InfixOperation.new(
+ '@@',
+ search_data_table[:search_vector],
+ Arel::Nodes::NamedFunction.new(
+ 'websearch_to_tsquery',
+ [Arel::Nodes.build_quoted(TEXT_SEARCH_DICTIONARY), Arel::Nodes.build_quoted(search_term)]
+ )
+ )
+ )
+ end
+ end
+end
diff --git a/app/models/concerns/runners_token_prefixable.rb b/app/models/concerns/runners_token_prefixable.rb
index 1aea874337e..99bbbece7c7 100644
--- a/app/models/concerns/runners_token_prefixable.rb
+++ b/app/models/concerns/runners_token_prefixable.rb
@@ -1,14 +1,8 @@
# frozen_string_literal: true
module RunnersTokenPrefixable
- extend ActiveSupport::Concern
-
# Prefix for runners_token which can be used to invalidate existing tokens.
# The value chosen here is GR (for Gitlab Runner) combined with the rotation
# date (20220225) decimal to hex encoded.
RUNNERS_TOKEN_PREFIX = 'GR1348941'
-
- def runners_token_prefix
- RUNNERS_TOKEN_PREFIX
- end
end
diff --git a/app/models/concerns/select_for_project_authorization.rb b/app/models/concerns/select_for_project_authorization.rb
index 49342e30db6..5a7e16eb2c4 100644
--- a/app/models/concerns/select_for_project_authorization.rb
+++ b/app/models/concerns/select_for_project_authorization.rb
@@ -8,8 +8,10 @@ module SelectForProjectAuthorization
select("projects.id AS project_id", "members.access_level")
end
- def select_as_maintainer_for_project_authorization
- select(["projects.id AS project_id", "#{Gitlab::Access::MAINTAINER} AS access_level"])
+ # workaround until we migrate Project#owners to have membership with
+ # OWNER access level
+ def select_project_owner_for_project_authorization
+ select(["projects.id AS project_id", "#{Gitlab::Access::OWNER} AS access_level"])
end
end
end
diff --git a/app/models/concerns/sensitive_serializable_hash.rb b/app/models/concerns/sensitive_serializable_hash.rb
new file mode 100644
index 00000000000..725ec60e9b6
--- /dev/null
+++ b/app/models/concerns/sensitive_serializable_hash.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+module SensitiveSerializableHash
+ extend ActiveSupport::Concern
+
+ included do
+ class_attribute :attributes_exempt_from_serializable_hash, default: []
+ end
+
+ class_methods do
+ def prevent_from_serialization(*keys)
+ self.attributes_exempt_from_serializable_hash ||= []
+ self.attributes_exempt_from_serializable_hash.concat keys
+ end
+ end
+
+ # Override serializable_hash to exclude sensitive attributes by default
+ #
+ # In general, prefer NOT to use serializable_hash / to_json / as_json in favor
+ # of serializers / entities instead which has an allowlist of attributes
+ def serializable_hash(options = nil)
+ return super unless prevent_sensitive_fields_from_serializable_hash?
+ return super if options && options[:unsafe_serialization_hash]
+
+ options = options.try(:dup) || {}
+ options[:except] = Array(options[:except]).dup
+
+ options[:except].concat self.class.attributes_exempt_from_serializable_hash
+
+ if self.class.respond_to?(:encrypted_attributes)
+ options[:except].concat self.class.encrypted_attributes.keys
+
+ # Per https://github.com/attr-encrypted/attr_encrypted/blob/a96693e9a2a25f4f910bf915e29b0f364f277032/lib/attr_encrypted.rb#L413
+ options[:except].concat self.class.encrypted_attributes.values.map { |v| v[:attribute] }
+ options[:except].concat self.class.encrypted_attributes.values.map { |v| "#{v[:attribute]}_iv" }
+ end
+
+ super(options)
+ end
+
+ private
+
+ def prevent_sensitive_fields_from_serializable_hash?
+ Feature.enabled?(:prevent_sensitive_fields_from_serializable_hash, default_enabled: :yaml)
+ end
+end
diff --git a/app/models/concerns/spammable.rb b/app/models/concerns/spammable.rb
index 4901cd832ff..b475eb79aa3 100644
--- a/app/models/concerns/spammable.rb
+++ b/app/models/concerns/spammable.rb
@@ -12,7 +12,7 @@ module Spammable
included do
has_one :user_agent_detail, as: :subject, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
- attr_accessor :spam
+ attr_writer :spam
attr_accessor :needs_recaptcha
attr_accessor :spam_log
@@ -29,6 +29,10 @@ module Spammable
delegate :ip_address, :user_agent, to: :user_agent_detail, allow_nil: true
end
+ def spam
+ !!@spam # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ end
+
def submittable_as_spam_by?(current_user)
current_user && current_user.admin? && submittable_as_spam?
end
@@ -74,8 +78,9 @@ module Spammable
end
def recaptcha_error!
- self.errors.add(:base, "Your #{spammable_entity_type} has been recognized as spam. "\
- "Please, change the content or solve the reCAPTCHA to proceed.")
+ self.errors.add(:base, _("Your %{spammable_entity_type} has been recognized as spam. "\
+ "Please, change the content or solve the reCAPTCHA to proceed.") \
+ % { spammable_entity_type: spammable_entity_type })
end
def unrecoverable_spam_error!
diff --git a/app/models/concerns/timebox.rb b/app/models/concerns/timebox.rb
index 943ef3fa59f..d53594eb5af 100644
--- a/app/models/concerns/timebox.rb
+++ b/app/models/concerns/timebox.rb
@@ -44,7 +44,6 @@ module Timebox
validates :group, presence: true, unless: :project
validates :project, presence: true, unless: :group
- validates :title, presence: true
validate :timebox_type_check
validate :start_date_should_be_less_than_due_date, if: proc { |m| m.start_date.present? && m.due_date.present? }
diff --git a/app/models/concerns/token_authenticatable.rb b/app/models/concerns/token_authenticatable.rb
index f44ad8ebe90..d91ec161b84 100644
--- a/app/models/concerns/token_authenticatable.rb
+++ b/app/models/concerns/token_authenticatable.rb
@@ -8,6 +8,10 @@ module TokenAuthenticatable
@encrypted_token_authenticatable_fields ||= []
end
+ def token_authenticatable_fields
+ @token_authenticatable_fields ||= []
+ end
+
private
def add_authentication_token_field(token_field, options = {})
@@ -23,6 +27,8 @@ module TokenAuthenticatable
strategy = TokenAuthenticatableStrategies::Base
.fabricate(self, token_field, options)
+ prevent_from_serialization(*strategy.token_fields) if respond_to?(:prevent_from_serialization)
+
if options.fetch(:unique, true)
define_singleton_method("find_by_#{token_field}") do |token|
strategy.find_token_authenticatable(token)
@@ -82,9 +88,5 @@ module TokenAuthenticatable
@token_authenticatable_module ||=
const_set(:TokenAuthenticatable, Module.new).tap(&method(:include))
end
-
- def token_authenticatable_fields
- @token_authenticatable_fields ||= []
- end
end
end
diff --git a/app/models/concerns/token_authenticatable_strategies/base.rb b/app/models/concerns/token_authenticatable_strategies/base.rb
index 2cec4ab460e..2b677f37c89 100644
--- a/app/models/concerns/token_authenticatable_strategies/base.rb
+++ b/app/models/concerns/token_authenticatable_strategies/base.rb
@@ -23,6 +23,14 @@ module TokenAuthenticatableStrategies
raise NotImplementedError
end
+ def token_fields
+ result = [token_field]
+
+ result << @expires_at_field if expirable?
+
+ result
+ end
+
# Default implementation returns the token as-is
def format_token(instance, token)
instance.send("format_#{@token_field}", token) # rubocop:disable GitlabSecurity/PublicSend
diff --git a/app/models/concerns/token_authenticatable_strategies/digest.rb b/app/models/concerns/token_authenticatable_strategies/digest.rb
index 9926662ed66..5c94f25949f 100644
--- a/app/models/concerns/token_authenticatable_strategies/digest.rb
+++ b/app/models/concerns/token_authenticatable_strategies/digest.rb
@@ -2,6 +2,10 @@
module TokenAuthenticatableStrategies
class Digest < Base
+ def token_fields
+ super + [token_field_name]
+ end
+
def find_token_authenticatable(token, unscoped = false)
return unless token
diff --git a/app/models/concerns/token_authenticatable_strategies/encrypted.rb b/app/models/concerns/token_authenticatable_strategies/encrypted.rb
index e957d09fbc6..1db88c27181 100644
--- a/app/models/concerns/token_authenticatable_strategies/encrypted.rb
+++ b/app/models/concerns/token_authenticatable_strategies/encrypted.rb
@@ -2,6 +2,10 @@
module TokenAuthenticatableStrategies
class Encrypted < Base
+ def token_fields
+ super + [encrypted_field]
+ end
+
def find_token_authenticatable(token, unscoped = false)
return if token.blank?
diff --git a/app/models/concerns/update_namespace_statistics.rb b/app/models/concerns/update_namespace_statistics.rb
new file mode 100644
index 00000000000..26d6fc10228
--- /dev/null
+++ b/app/models/concerns/update_namespace_statistics.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+# This module provides helpers for updating `NamespaceStatistics` with `after_save` and
+# `after_destroy` hooks.
+#
+# Models including this module must respond to and return a `namespace`
+#
+# Example:
+#
+# class DependencyProxy::Manifest
+# include UpdateNamespaceStatistics
+#
+# belongs_to :group
+# alias_attribute :namespace, :group
+#
+# update_namespace_statistics namespace_statistics_name: :dependency_proxy_size
+# end
+module UpdateNamespaceStatistics
+ extend ActiveSupport::Concern
+ include AfterCommitQueue
+
+ class_methods do
+ attr_reader :namespace_statistics_name, :statistic_attribute
+
+ # Configure the model to update `namespace_statistics_name` on NamespaceStatistics,
+ # when `statistic_attribute` changes
+ #
+ # - namespace_statistics_name: A column of `NamespaceStatistics` to update
+ # - statistic_attribute: An attribute of the current model, default to `size`
+ def update_namespace_statistics(namespace_statistics_name:, statistic_attribute: :size)
+ @namespace_statistics_name = namespace_statistics_name
+ @statistic_attribute = statistic_attribute
+
+ after_save(:schedule_namespace_statistics_refresh, if: :update_namespace_statistics?)
+ after_destroy(:schedule_namespace_statistics_refresh)
+ end
+
+ private :update_namespace_statistics
+ end
+
+ included do
+ private
+
+ def update_namespace_statistics?
+ saved_change_to_attribute?(self.class.statistic_attribute)
+ end
+
+ def schedule_namespace_statistics_refresh
+ run_after_commit do
+ Groups::UpdateStatisticsWorker.perform_async(namespace.id, [self.class.namespace_statistics_name])
+ end
+ end
+ end
+end