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/avatarable.rb1
-rw-r--r--app/models/concerns/boards/listable.rb1
-rw-r--r--app/models/concerns/bulk_member_access_load.rb26
-rw-r--r--app/models/concerns/cache_markdown_field.rb4
-rw-r--r--app/models/concerns/cascading_namespace_setting_attribute.rb241
-rw-r--r--app/models/concerns/ci/artifactable.rb7
-rw-r--r--app/models/concerns/ci/has_status.rb13
-rw-r--r--app/models/concerns/counter_attribute.rb2
-rw-r--r--app/models/concerns/deprecated_assignee.rb2
-rw-r--r--app/models/concerns/enums/ci/commit_status.rb2
-rw-r--r--app/models/concerns/enums/ci/pipeline.rb4
-rw-r--r--app/models/concerns/has_repository.rb9
-rw-r--r--app/models/concerns/has_timelogs_report.rb20
-rw-r--r--app/models/concerns/integration.rb4
-rw-r--r--app/models/concerns/issuable.rb14
-rw-r--r--app/models/concerns/loaded_in_group_list.rb4
-rw-r--r--app/models/concerns/milestoneable.rb4
-rw-r--r--app/models/concerns/milestoneish.rb6
-rw-r--r--app/models/concerns/object_storable.rb10
-rw-r--r--app/models/concerns/participable.rb35
-rw-r--r--app/models/concerns/protected_ref.rb2
-rw-r--r--app/models/concerns/safe_url.rb4
-rw-r--r--app/models/concerns/sidebars/container_with_html_options.rb42
-rw-r--r--app/models/concerns/sidebars/has_active_routes.rb16
-rw-r--r--app/models/concerns/sidebars/has_hint.rb16
-rw-r--r--app/models/concerns/sidebars/has_icon.rb27
-rw-r--r--app/models/concerns/sidebars/has_pill.rb21
-rw-r--r--app/models/concerns/sidebars/positionable_list.rb37
-rw-r--r--app/models/concerns/sidebars/renderable.rb12
-rw-r--r--app/models/concerns/sortable.rb1
-rw-r--r--app/models/concerns/subscribable.rb32
-rw-r--r--app/models/concerns/taskable.rb3
-rw-r--r--app/models/concerns/token_authenticatable_strategies/encrypted.rb17
-rw-r--r--app/models/concerns/token_authenticatable_strategies/encryption_helper.rb26
-rw-r--r--app/models/concerns/vulnerability_finding_helpers.rb7
-rw-r--r--app/models/concerns/vulnerability_finding_signature_helpers.rb7
36 files changed, 632 insertions, 47 deletions
diff --git a/app/models/concerns/avatarable.rb b/app/models/concerns/avatarable.rb
index c106c08c04a..fdc418029be 100644
--- a/app/models/concerns/avatarable.rb
+++ b/app/models/concerns/avatarable.rb
@@ -131,7 +131,6 @@ module Avatarable
def clear_avatar_caches
return unless respond_to?(:verified_emails) && verified_emails.any? && avatar_changed?
- return unless Feature.enabled?(:avatar_cache_for_email, self, type: :development)
Gitlab::AvatarCache.delete_by_email(*verified_emails)
end
diff --git a/app/models/concerns/boards/listable.rb b/app/models/concerns/boards/listable.rb
index d6863e87261..b9827a79422 100644
--- a/app/models/concerns/boards/listable.rb
+++ b/app/models/concerns/boards/listable.rb
@@ -13,6 +13,7 @@ module Boards
scope :ordered, -> { order(:list_type, :position) }
scope :destroyable, -> { where(list_type: list_types.slice(*destroyable_types).values) }
scope :movable, -> { where(list_type: list_types.slice(*movable_types).values) }
+ scope :without_types, ->(list_types) { where.not(list_type: list_types) }
class << self
def preload_preferences_for_user(lists, user)
diff --git a/app/models/concerns/bulk_member_access_load.rb b/app/models/concerns/bulk_member_access_load.rb
index f44ad474cd5..e252ca36629 100644
--- a/app/models/concerns/bulk_member_access_load.rb
+++ b/app/models/concerns/bulk_member_access_load.rb
@@ -13,13 +13,7 @@ module BulkMemberAccessLoad
raise 'Block is mandatory' unless block_given?
resource_ids = resource_ids.uniq
- key = max_member_access_for_resource_key(resource_klass, memoization_index)
- access = {}
-
- if Gitlab::SafeRequestStore.active?
- Gitlab::SafeRequestStore[key] ||= {}
- access = Gitlab::SafeRequestStore[key]
- end
+ access = load_access_hash(resource_klass, memoization_index)
# Look up only the IDs we need
resource_ids -= access.keys
@@ -39,10 +33,28 @@ module BulkMemberAccessLoad
access
end
+ def merge_value_to_request_store(resource_klass, resource_id, memoization_index, value)
+ max_member_access_for_resource_ids(resource_klass, [resource_id], memoization_index) 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, memoization_index)
+ key = max_member_access_for_resource_key(resource_klass, memoization_index)
+
+ access = {}
+ if Gitlab::SafeRequestStore.active?
+ Gitlab::SafeRequestStore[key] ||= {}
+ access = Gitlab::SafeRequestStore[key]
+ end
+
+ access
+ end
end
end
diff --git a/app/models/concerns/cache_markdown_field.rb b/app/models/concerns/cache_markdown_field.rb
index 45944401c2d..34c1b6d25a4 100644
--- a/app/models/concerns/cache_markdown_field.rb
+++ b/app/models/concerns/cache_markdown_field.rb
@@ -56,12 +56,12 @@ module CacheMarkdownField
# Update every applicable column in a row if any one is invalidated, as we only store
# one version per row
def refresh_markdown_cache
- updates = cached_markdown_fields.markdown_fields.map do |markdown_field|
+ updates = cached_markdown_fields.markdown_fields.to_h do |markdown_field|
[
cached_markdown_fields.html_field(markdown_field),
rendered_field_content(markdown_field)
]
- end.to_h
+ end
updates['cached_markdown_version'] = latest_cached_markdown_version
diff --git a/app/models/concerns/cascading_namespace_setting_attribute.rb b/app/models/concerns/cascading_namespace_setting_attribute.rb
new file mode 100644
index 00000000000..2b4a108a9a0
--- /dev/null
+++ b/app/models/concerns/cascading_namespace_setting_attribute.rb
@@ -0,0 +1,241 @@
+# frozen_string_literal: true
+
+#
+# Cascading attributes enables managing settings in a flexible way.
+#
+# - Instance administrator can define an instance-wide default setting, or
+# lock the setting to prevent change by group owners.
+# - Group maintainers/owners can define a default setting for their group, or
+# lock the setting to prevent change by sub-group maintainers/owners.
+#
+# Behavior:
+#
+# - When a group does not have a value (value is `nil`), cascade up the
+# hierarchy to find the first non-nil value.
+# - Settings can be locked at any level to prevent groups/sub-groups from
+# overriding.
+# - If the setting isn't locked, the default can be overridden.
+# - An instance administrator or group maintainer/owner can push settings values
+# to groups/sub-groups to override existing values, even when the setting
+# is not otherwise locked.
+#
+module CascadingNamespaceSettingAttribute
+ extend ActiveSupport::Concern
+ include Gitlab::Utils::StrongMemoize
+
+ class_methods do
+ def cascading_settings_feature_enabled?
+ ::Feature.enabled?(:cascading_namespace_settings, default_enabled: true)
+ end
+
+ private
+
+ # Facilitates the cascading lookup of values and,
+ # similar to Rails' `attr_accessor`, defines convenience methods such as
+ # a reader, writer, and validators.
+ #
+ # Example: `cascading_attr :delayed_project_removal`
+ #
+ # Public methods defined:
+ # - `delayed_project_removal`
+ # - `delayed_project_removal=`
+ # - `delayed_project_removal_locked?`
+ # - `delayed_project_removal_locked_by_ancestor?`
+ # - `delayed_project_removal_locked_by_application_setting?`
+ # - `delayed_project_removal?` (only defined for boolean attributes)
+ # - `delayed_project_removal_locked_ancestor` - Returns locked namespace settings object (only namespace_id)
+ #
+ # Defined validators ensure attribute value cannot be updated if locked by
+ # an ancestor or application settings.
+ #
+ # Requires database columns be present in both `namespace_settings` and
+ # `application_settings`.
+ def cascading_attr(*attributes)
+ attributes.map(&:to_sym).each do |attribute|
+ # public methods
+ define_attr_reader(attribute)
+ define_attr_writer(attribute)
+ define_lock_methods(attribute)
+ alias_boolean(attribute)
+
+ # private methods
+ define_validator_methods(attribute)
+ define_after_update(attribute)
+
+ validate :"#{attribute}_changeable?"
+ validate :"lock_#{attribute}_changeable?"
+
+ after_update :"clear_descendant_#{attribute}_locks", if: -> { saved_change_to_attribute?("lock_#{attribute}", to: true) }
+ end
+ end
+
+ # The cascading attribute reader method handles lookups
+ # with the following criteria:
+ #
+ # 1. Returns the dirty value, if the attribute has changed.
+ # 2. Return locked ancestor value.
+ # 3. Return locked instance-level application settings value.
+ # 4. Return this namespace's attribute, if not nil.
+ # 5. Return value from nearest ancestor where value is not nil.
+ # 6. Return instance-level application setting.
+ def define_attr_reader(attribute)
+ define_method(attribute) do
+ strong_memoize(attribute) do
+ next self[attribute] unless self.class.cascading_settings_feature_enabled?
+
+ next self[attribute] if will_save_change_to_attribute?(attribute)
+ next locked_value(attribute) if cascading_attribute_locked?(attribute)
+ next self[attribute] unless self[attribute].nil?
+
+ cascaded_value = cascaded_ancestor_value(attribute)
+ next cascaded_value unless cascaded_value.nil?
+
+ application_setting_value(attribute)
+ end
+ end
+ end
+
+ def define_attr_writer(attribute)
+ define_method("#{attribute}=") do |value|
+ clear_memoization(attribute)
+
+ super(value)
+ end
+ end
+
+ def define_lock_methods(attribute)
+ define_method("#{attribute}_locked?") do
+ cascading_attribute_locked?(attribute)
+ end
+
+ define_method("#{attribute}_locked_by_ancestor?") do
+ locked_by_ancestor?(attribute)
+ end
+
+ define_method("#{attribute}_locked_by_application_setting?") do
+ locked_by_application_setting?(attribute)
+ end
+
+ define_method("#{attribute}_locked_ancestor") do
+ locked_ancestor(attribute)
+ end
+ end
+
+ def alias_boolean(attribute)
+ return unless Gitlab::Database.exists? && type_for_attribute(attribute).type == :boolean
+
+ alias_method :"#{attribute}?", attribute
+ end
+
+ # Defines two validations - one for the cascadable attribute itself and one
+ # for the lock attribute. Only allows the respective value to change if
+ # an ancestor has not already locked the value.
+ def define_validator_methods(attribute)
+ define_method("#{attribute}_changeable?") do
+ return unless cascading_attribute_changed?(attribute)
+ return unless cascading_attribute_locked?(attribute)
+
+ errors.add(attribute, s_('CascadingSettings|cannot be changed because it is locked by an ancestor'))
+ end
+
+ define_method("lock_#{attribute}_changeable?") do
+ return unless cascading_attribute_changed?("lock_#{attribute}")
+
+ if cascading_attribute_locked?(attribute)
+ return errors.add(:"lock_#{attribute}", s_('CascadingSettings|cannot be changed because it is locked by an ancestor'))
+ end
+
+ # Don't allow locking a `nil` attribute.
+ # Even if the value being locked is currently cascaded from an ancestor,
+ # it should be copied to this record to avoid the ancestor changing the
+ # value unexpectedly later.
+ return unless self[attribute].nil? && public_send("lock_#{attribute}?") # rubocop:disable GitlabSecurity/PublicSend
+
+ errors.add(attribute, s_('CascadingSettings|cannot be nil when locking the attribute'))
+ end
+
+ private :"#{attribute}_changeable?", :"lock_#{attribute}_changeable?"
+ end
+
+ # When a particular group locks the attribute, clear all sub-group locks
+ # since the higher lock takes priority.
+ def define_after_update(attribute)
+ define_method("clear_descendant_#{attribute}_locks") do
+ self.class.where(namespace_id: descendants).update_all("lock_#{attribute}" => false)
+ end
+
+ private :"clear_descendant_#{attribute}_locks"
+ end
+ end
+
+ private
+
+ def locked_value(attribute)
+ ancestor = locked_ancestor(attribute)
+ return ancestor.read_attribute(attribute) if ancestor
+
+ Gitlab::CurrentSettings.public_send(attribute) # rubocop:disable GitlabSecurity/PublicSend
+ end
+
+ def locked_ancestor(attribute)
+ return unless self.class.cascading_settings_feature_enabled?
+ return unless namespace.has_parent?
+
+ strong_memoize(:"#{attribute}_locked_ancestor") do
+ self.class
+ .select(:namespace_id, "lock_#{attribute}", attribute)
+ .where(namespace_id: namespace_ancestor_ids)
+ .where(self.class.arel_table["lock_#{attribute}"].eq(true))
+ .limit(1).load.first
+ end
+ end
+
+ def locked_by_ancestor?(attribute)
+ return false unless self.class.cascading_settings_feature_enabled?
+
+ locked_ancestor(attribute).present?
+ end
+
+ def locked_by_application_setting?(attribute)
+ return false unless self.class.cascading_settings_feature_enabled?
+
+ Gitlab::CurrentSettings.public_send("lock_#{attribute}") # rubocop:disable GitlabSecurity/PublicSend
+ end
+
+ def cascading_attribute_locked?(attribute)
+ locked_by_ancestor?(attribute) || locked_by_application_setting?(attribute)
+ end
+
+ def cascading_attribute_changed?(attribute)
+ public_send("#{attribute}_changed?") # rubocop:disable GitlabSecurity/PublicSend
+ end
+
+ def cascaded_ancestor_value(attribute)
+ return unless namespace.has_parent?
+
+ # rubocop:disable GitlabSecurity/SqlInjection
+ self.class
+ .select(attribute)
+ .joins("join unnest(ARRAY[#{namespace_ancestor_ids.join(',')}]) with ordinality t(namespace_id, ord) USING (namespace_id)")
+ .where("#{attribute} IS NOT NULL")
+ .order('t.ord')
+ .limit(1).first&.read_attribute(attribute)
+ # rubocop:enable GitlabSecurity/SqlInjection
+ end
+
+ def application_setting_value(attribute)
+ Gitlab::CurrentSettings.public_send(attribute) # rubocop:disable GitlabSecurity/PublicSend
+ end
+
+ def namespace_ancestor_ids
+ strong_memoize(:namespace_ancestor_ids) do
+ namespace.self_and_ancestors(hierarchy_order: :asc).pluck(:id).reject { |id| id == namespace_id }
+ end
+ end
+
+ def descendants
+ strong_memoize(:descendants) do
+ namespace.descendants.pluck(:id)
+ end
+ end
+end
diff --git a/app/models/concerns/ci/artifactable.rb b/app/models/concerns/ci/artifactable.rb
index cbe7d3b6abb..0d29955268f 100644
--- a/app/models/concerns/ci/artifactable.rb
+++ b/app/models/concerns/ci/artifactable.rb
@@ -4,8 +4,10 @@ module Ci
module Artifactable
extend ActiveSupport::Concern
- NotSupportedAdapterError = Class.new(StandardError)
+ include ObjectStorable
+ STORE_COLUMN = :file_store
+ NotSupportedAdapterError = Class.new(StandardError)
FILE_FORMAT_ADAPTERS = {
gzip: Gitlab::Ci::Build::Artifacts::Adapters::GzipStream,
raw: Gitlab::Ci::Build::Artifacts::Adapters::RawStream
@@ -20,6 +22,7 @@ module Ci
scope :expired_before, -> (timestamp) { where(arel_table[:expire_at].lt(timestamp)) }
scope :expired, -> (limit) { expired_before(Time.current).limit(limit) }
+ scope :project_id_in, ->(ids) { where(project_id: ids) }
end
def each_blob(&blk)
@@ -39,3 +42,5 @@ module Ci
end
end
end
+
+Ci::Artifactable.prepend_ee_mod
diff --git a/app/models/concerns/ci/has_status.rb b/app/models/concerns/ci/has_status.rb
index 0412f7a072b..c990da5873a 100644
--- a/app/models/concerns/ci/has_status.rb
+++ b/app/models/concerns/ci/has_status.rb
@@ -16,6 +16,19 @@ module Ci
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
+ STATUSES_DESCRIPTION = {
+ created: 'Pipeline has been created',
+ waiting_for_resource: 'A resource (for example, a runner) that the pipeline requires to run is unavailable',
+ preparing: 'Pipeline is preparing to run',
+ pending: 'Pipeline has not started running yet',
+ running: 'Pipeline is running',
+ failed: 'At least one stage of the pipeline failed',
+ success: 'Pipeline completed successfully',
+ canceled: 'Pipeline was canceled before completion',
+ skipped: 'Pipeline was skipped',
+ manual: 'Pipeline needs to be manually started',
+ scheduled: 'Pipeline is scheduled to run'
+ }.freeze
UnknownStatusError = Class.new(StandardError)
diff --git a/app/models/concerns/counter_attribute.rb b/app/models/concerns/counter_attribute.rb
index b468415c4c7..829b2a6ef21 100644
--- a/app/models/concerns/counter_attribute.rb
+++ b/app/models/concerns/counter_attribute.rb
@@ -33,7 +33,7 @@ module CounterAttribute
extend AfterCommitQueue
include Gitlab::ExclusiveLeaseHelpers
- LUA_STEAL_INCREMENT_SCRIPT = <<~EOS.freeze
+ LUA_STEAL_INCREMENT_SCRIPT = <<~EOS
local increment_key, flushed_key = KEYS[1], KEYS[2]
local increment_value = redis.call("get", increment_key) or 0
local flushed_value = redis.call("incrby", flushed_key, increment_value)
diff --git a/app/models/concerns/deprecated_assignee.rb b/app/models/concerns/deprecated_assignee.rb
index 7f12ce39c96..3f557ee9b48 100644
--- a/app/models/concerns/deprecated_assignee.rb
+++ b/app/models/concerns/deprecated_assignee.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-# This module handles backward compatibility for import/export of Merge Requests after
+# This module handles backward compatibility for import/export of merge requests after
# multiple assignees feature was introduced. Also, it handles the scenarios where
# the #26496 background migration hasn't finished yet.
# Ideally, most of this code should be removed at #59457.
diff --git a/app/models/concerns/enums/ci/commit_status.rb b/app/models/concerns/enums/ci/commit_status.rb
index 48b4a402974..de17f50cd29 100644
--- a/app/models/concerns/enums/ci/commit_status.rb
+++ b/app/models/concerns/enums/ci/commit_status.rb
@@ -20,6 +20,8 @@ module Enums
scheduler_failure: 11,
data_integrity_failure: 12,
forward_deployment_failure: 13,
+ user_blocked: 14,
+ project_deleted: 15,
insufficient_bridge_permissions: 1_001,
downstream_bridge_project_not_found: 1_002,
invalid_bridge_trigger: 1_003,
diff --git a/app/models/concerns/enums/ci/pipeline.rb b/app/models/concerns/enums/ci/pipeline.rb
index f8314d8b429..fdc48d09db2 100644
--- a/app/models/concerns/enums/ci/pipeline.rb
+++ b/app/models/concerns/enums/ci/pipeline.rb
@@ -13,7 +13,9 @@ module Enums
activity_limit_exceeded: 20,
size_limit_exceeded: 21,
job_activity_limit_exceeded: 22,
- deployments_limit_exceeded: 23
+ deployments_limit_exceeded: 23,
+ user_blocked: 24,
+ project_deleted: 25
}
end
diff --git a/app/models/concerns/has_repository.rb b/app/models/concerns/has_repository.rb
index b9ad78c14fd..774cda2c3e8 100644
--- a/app/models/concerns/has_repository.rb
+++ b/app/models/concerns/has_repository.rb
@@ -77,9 +77,14 @@ module HasRepository
def default_branch_from_preferences
return unless empty_repo?
- group_branch_default_name = group&.default_branch_name if respond_to?(:group)
+ (default_branch_from_group_preferences || Gitlab::CurrentSettings.default_branch_name).presence
+ end
+
+ def default_branch_from_group_preferences
+ return unless respond_to?(:group)
+ return unless group
- (group_branch_default_name || Gitlab::CurrentSettings.default_branch_name).presence
+ group.default_branch_name || group.root_ancestor.default_branch_name
end
def reload_default_branch
diff --git a/app/models/concerns/has_timelogs_report.rb b/app/models/concerns/has_timelogs_report.rb
new file mode 100644
index 00000000000..90f9876de95
--- /dev/null
+++ b/app/models/concerns/has_timelogs_report.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module HasTimelogsReport
+ extend ActiveSupport::Concern
+ include Gitlab::Utils::StrongMemoize
+
+ def timelogs(start_time, end_time)
+ strong_memoize(:timelogs) { timelogs_for(start_time, end_time) }
+ end
+
+ def user_can_access_group_timelogs?(current_user)
+ Ability.allowed?(current_user, :read_group_timelogs, self)
+ end
+
+ private
+
+ def timelogs_for(start_time, end_time)
+ Timelog.between_times(start_time, end_time).for_issues_in_group(self)
+ end
+end
diff --git a/app/models/concerns/integration.rb b/app/models/concerns/integration.rb
index 9d446841a9f..5e53f13be95 100644
--- a/app/models/concerns/integration.rb
+++ b/app/models/concerns/integration.rb
@@ -6,12 +6,12 @@ module Integration
class_methods do
def with_custom_integration_for(integration, page = nil, per = nil)
custom_integration_project_ids = Service
+ .select(:project_id)
.where(type: integration.type)
.where(inherit_from_id: nil)
- .distinct # Required until https://gitlab.com/gitlab-org/gitlab/-/issues/207385
+ .where.not(project_id: nil)
.page(page)
.per(per)
- .pluck(:project_id)
Project.where(id: custom_integration_project_ids)
end
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index e1be0665452..1e44321e148 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -65,7 +65,7 @@ module Issuable
has_many :label_links, as: :target, dependent: :destroy, inverse_of: :target # rubocop:disable Cop/ActiveRecordDependent
has_many :labels, through: :label_links
- has_many :todos, as: :target, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
+ has_many :todos, as: :target
has_one :metrics, inverse_of: model_name.singular.to_sym, autosave: true
@@ -137,6 +137,14 @@ module Issuable
scope :references_project, -> { references(:project) }
scope :non_archived, -> { join_project.where(projects: { archived: false }) }
+ scope :includes_for_bulk_update, -> do
+ associations = %i[author assignees epic group labels metrics project source_project target_project].select do |association|
+ reflect_on_association(association)
+ end
+
+ includes(*associations)
+ end
+
attr_mentionable :title, pipeline: :single_line
attr_mentionable :description
@@ -324,7 +332,7 @@ module Issuable
# This prevents errors when ignored columns are present in the database.
issuable_columns = with_cte ? issue_grouping_columns(use_cte: with_cte) : "#{table_name}.*"
- extra_select_columns = extra_select_columns.unshift("(#{highest_priority}) AS highest_priority")
+ extra_select_columns.unshift("(#{highest_priority}) AS highest_priority")
select(issuable_columns)
.select(extra_select_columns)
@@ -437,7 +445,7 @@ module Issuable
end
def subscribed_without_subscriptions?(user, project)
- participants(user).include?(user)
+ participant?(user)
end
def can_assign_epic?(user)
diff --git a/app/models/concerns/loaded_in_group_list.rb b/app/models/concerns/loaded_in_group_list.rb
index e624b9aa356..59e0ed75d2d 100644
--- a/app/models/concerns/loaded_in_group_list.rb
+++ b/app/models/concerns/loaded_in_group_list.rb
@@ -73,6 +73,10 @@ module LoadedInGroupList
def member_count
@member_count ||= try(:preloaded_member_count) || members.count
end
+
+ def guest_count
+ @guest_count ||= members.guests.count
+ end
end
LoadedInGroupList::ClassMethods.prepend_if_ee('EE::LoadedInGroupList::ClassMethods')
diff --git a/app/models/concerns/milestoneable.rb b/app/models/concerns/milestoneable.rb
index ccb334343ff..d42417bb6c1 100644
--- a/app/models/concerns/milestoneable.rb
+++ b/app/models/concerns/milestoneable.rb
@@ -39,11 +39,13 @@ module Milestoneable
private
def milestone_is_valid
- errors.add(:milestone_id, 'is invalid') if respond_to?(:milestone_id) && milestone_id.present? && !milestone_available?
+ errors.add(:milestone_id, 'is invalid') if respond_to?(:milestone_id) && !milestone_available?
end
end
def milestone_available?
+ return true if milestone_id.blank?
+
project_id == milestone&.project_id || project.ancestors_upto.compact.include?(milestone&.group)
end
diff --git a/app/models/concerns/milestoneish.rb b/app/models/concerns/milestoneish.rb
index 5f24564dc56..eaf64f2541d 100644
--- a/app/models/concerns/milestoneish.rb
+++ b/app/models/concerns/milestoneish.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module Milestoneish
- DISPLAY_ISSUES_LIMIT = 3000
+ DISPLAY_ISSUES_LIMIT = 500
def total_issues_count
@total_issues_count ||= Milestones::IssuesCountService.new(self).count
@@ -15,6 +15,10 @@ module Milestoneish
total_issues_count - closed_issues_count
end
+ def total_merge_requests_count
+ @total_merge_request_count ||= Milestones::MergeRequestsCountService.new(self).count
+ end
+
def complete?
total_issues_count > 0 && total_issues_count == closed_issues_count
end
diff --git a/app/models/concerns/object_storable.rb b/app/models/concerns/object_storable.rb
new file mode 100644
index 00000000000..c13dddc0b88
--- /dev/null
+++ b/app/models/concerns/object_storable.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+module ObjectStorable
+ extend ActiveSupport::Concern
+
+ included do
+ scope :with_files_stored_locally, -> { where(klass::STORE_COLUMN => ObjectStorage::Store::LOCAL) }
+ scope :with_files_stored_remotely, -> { where(klass::STORE_COLUMN => ObjectStorage::Store::REMOTE) }
+ end
+end
diff --git a/app/models/concerns/participable.rb b/app/models/concerns/participable.rb
index af105629398..acd654bd229 100644
--- a/app/models/concerns/participable.rb
+++ b/app/models/concerns/participable.rb
@@ -56,18 +56,34 @@ module Participable
# This method processes attributes of objects in breadth-first order.
#
# Returns an Array of User instances.
- def participants(current_user = nil)
- all_participants[current_user]
+ def participants(user = nil)
+ filtered_participants_hash[user]
+ end
+
+ # Checks if the user is a participant in a discussion.
+ #
+ # This method processes attributes of objects in breadth-first order.
+ #
+ # Returns a Boolean.
+ def participant?(user)
+ can_read_participable?(user) &&
+ all_participants_hash[user].include?(user)
end
private
- def all_participants
- @all_participants ||= Hash.new do |hash, user|
+ def all_participants_hash
+ @all_participants_hash ||= Hash.new do |hash, user|
hash[user] = raw_participants(user)
end
end
+ def filtered_participants_hash
+ @filtered_participants_hash ||= Hash.new do |hash, user|
+ hash[user] = filter_by_ability(all_participants_hash[user])
+ end
+ end
+
def raw_participants(current_user = nil)
current_user ||= author
ext = Gitlab::ReferenceExtractor.new(project, current_user)
@@ -98,8 +114,6 @@ module Participable
end
participants.merge(ext.users)
-
- filter_by_ability(participants)
end
def filter_by_ability(participants)
@@ -110,6 +124,15 @@ module Participable
Ability.users_that_can_read_project(participants.to_a, project)
end
end
+
+ def can_read_participable?(participant)
+ case self
+ when PersonalSnippet
+ participant.can?(:read_snippet, self)
+ else
+ participant.can?(:read_project, project)
+ end
+ end
end
Participable.prepend_if_ee('EE::Participable')
diff --git a/app/models/concerns/protected_ref.rb b/app/models/concerns/protected_ref.rb
index 65195a8d5aa..2828ae4a3a9 100644
--- a/app/models/concerns/protected_ref.rb
+++ b/app/models/concerns/protected_ref.rb
@@ -4,7 +4,7 @@ module ProtectedRef
extend ActiveSupport::Concern
included do
- belongs_to :project
+ belongs_to :project, touch: true
validates :name, presence: true
validates :project, presence: true
diff --git a/app/models/concerns/safe_url.rb b/app/models/concerns/safe_url.rb
index febca7d241f..7dce05bddba 100644
--- a/app/models/concerns/safe_url.rb
+++ b/app/models/concerns/safe_url.rb
@@ -3,12 +3,12 @@
module SafeUrl
extend ActiveSupport::Concern
- def safe_url(usernames_whitelist: [])
+ def safe_url(allowed_usernames: [])
return if url.nil?
uri = URI.parse(url)
uri.password = '*****' if uri.password
- uri.user = '*****' if uri.user && !usernames_whitelist.include?(uri.user)
+ uri.user = '*****' if uri.user && allowed_usernames.exclude?(uri.user)
uri.to_s
rescue URI::Error
end
diff --git a/app/models/concerns/sidebars/container_with_html_options.rb b/app/models/concerns/sidebars/container_with_html_options.rb
new file mode 100644
index 00000000000..12ea366c66a
--- /dev/null
+++ b/app/models/concerns/sidebars/container_with_html_options.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+module Sidebars
+ module ContainerWithHtmlOptions
+ # The attributes returned from this method
+ # will be applied to helper methods like
+ # `link_to` or the div containing the container.
+ def container_html_options
+ {
+ aria: { label: title }
+ }.merge(extra_container_html_options)
+ end
+
+ # Classes will override mostly this method
+ # and not `container_html_options`.
+ def extra_container_html_options
+ {}
+ end
+
+ # Attributes to pass to the html_options attribute
+ # in the helper method that sets the active class
+ # on each element.
+ def nav_link_html_options
+ {}
+ end
+
+ def title
+ raise NotImplementedError
+ end
+
+ # The attributes returned from this method
+ # will be applied right next to the title,
+ # for example in the span that renders the title.
+ def title_html_options
+ {}
+ end
+
+ def link
+ raise NotImplementedError
+ end
+ end
+end
diff --git a/app/models/concerns/sidebars/has_active_routes.rb b/app/models/concerns/sidebars/has_active_routes.rb
new file mode 100644
index 00000000000..e7a153f067a
--- /dev/null
+++ b/app/models/concerns/sidebars/has_active_routes.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module Sidebars
+ module HasActiveRoutes
+ # This method will indicate for which paths or
+ # controllers, the menu or menu item should
+ # be set as active.
+ #
+ # The returned values are passed to the `nav_link` helper method,
+ # so the params can be either `path`, `page`, `controller`.
+ # Param 'action' is not supported.
+ def active_routes
+ {}
+ end
+ end
+end
diff --git a/app/models/concerns/sidebars/has_hint.rb b/app/models/concerns/sidebars/has_hint.rb
new file mode 100644
index 00000000000..21dca39dca0
--- /dev/null
+++ b/app/models/concerns/sidebars/has_hint.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+# This module has the necessary methods to store
+# hints for menus. Hints are elements displayed
+# when the user hover the menu item.
+module Sidebars
+ module HasHint
+ def show_hint?
+ false
+ end
+
+ def hint_html_options
+ {}
+ end
+ end
+end
diff --git a/app/models/concerns/sidebars/has_icon.rb b/app/models/concerns/sidebars/has_icon.rb
new file mode 100644
index 00000000000..d1a87918285
--- /dev/null
+++ b/app/models/concerns/sidebars/has_icon.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+# This module has the necessary methods to show
+# sprites or images next to the menu item.
+module Sidebars
+ module HasIcon
+ def sprite_icon
+ nil
+ end
+
+ def sprite_icon_html_options
+ {}
+ end
+
+ def image_path
+ nil
+ end
+
+ def image_html_options
+ {}
+ end
+
+ def icon_or_image?
+ sprite_icon || image_path
+ end
+ end
+end
diff --git a/app/models/concerns/sidebars/has_pill.rb b/app/models/concerns/sidebars/has_pill.rb
new file mode 100644
index 00000000000..ad7064fe63d
--- /dev/null
+++ b/app/models/concerns/sidebars/has_pill.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+# This module introduces the logic to show the "pill" element
+# next to the menu item, indicating the a count.
+module Sidebars
+ module HasPill
+ def has_pill?
+ false
+ end
+
+ # In this method we will need to provide the query
+ # to retrieve the elements count
+ def pill_count
+ raise NotImplementedError
+ end
+
+ def pill_html_options
+ {}
+ end
+ end
+end
diff --git a/app/models/concerns/sidebars/positionable_list.rb b/app/models/concerns/sidebars/positionable_list.rb
new file mode 100644
index 00000000000..30830d547f3
--- /dev/null
+++ b/app/models/concerns/sidebars/positionable_list.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+# This module handles elements in a list. All elements
+# must have a different class
+module Sidebars
+ module PositionableList
+ def add_element(list, element)
+ list << element
+ end
+
+ def insert_element_before(list, before_element, new_element)
+ index = index_of(list, before_element)
+
+ if index
+ list.insert(index, new_element)
+ else
+ list.unshift(new_element)
+ end
+ end
+
+ def insert_element_after(list, after_element, new_element)
+ index = index_of(list, after_element)
+
+ if index
+ list.insert(index + 1, new_element)
+ else
+ add_element(list, new_element)
+ end
+ end
+
+ private
+
+ def index_of(list, element)
+ list.index { |e| e.is_a?(element) }
+ end
+ end
+end
diff --git a/app/models/concerns/sidebars/renderable.rb b/app/models/concerns/sidebars/renderable.rb
new file mode 100644
index 00000000000..a3976af8515
--- /dev/null
+++ b/app/models/concerns/sidebars/renderable.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+module Sidebars
+ module Renderable
+ # This method will control whether the menu or menu_item
+ # should be rendered. It will be overriden by specific
+ # classes.
+ def render?
+ true
+ end
+ end
+end
diff --git a/app/models/concerns/sortable.rb b/app/models/concerns/sortable.rb
index 4fe2a0e1827..9f5e9b2bb57 100644
--- a/app/models/concerns/sortable.rb
+++ b/app/models/concerns/sortable.rb
@@ -9,6 +9,7 @@ module Sortable
included do
scope :with_order_id_desc, -> { order(self.arel_table['id'].desc) }
+ scope :with_order_id_asc, -> { order(self.arel_table['id'].asc) }
scope :order_id_desc, -> { reorder(self.arel_table['id'].desc) }
scope :order_id_asc, -> { reorder(self.arel_table['id'].asc) }
scope :order_created_desc, -> { reorder(self.arel_table['created_at'].desc) }
diff --git a/app/models/concerns/subscribable.rb b/app/models/concerns/subscribable.rb
index 33e9e0e38fb..5a10ea7a248 100644
--- a/app/models/concerns/subscribable.rb
+++ b/app/models/concerns/subscribable.rb
@@ -17,13 +17,37 @@ module Subscribable
def subscribed?(user, project = nil)
return false unless user
- if subscription = subscriptions.find_by(user: user, project: project)
+ if (subscription = lazy_subscription(user, project)&.itself)
subscription.subscribed
else
subscribed_without_subscriptions?(user, project)
end
end
+ def lazy_subscription(user, project = nil)
+ return unless user
+
+ # handle project and group labels as well as issuable subscriptions
+ subscribable_type = self.class.ancestors.include?(Label) ? 'Label' : self.class.name
+ BatchLoader.for(id: id, subscribable_type: subscribable_type, project_id: project&.id).batch do |items, loader|
+ values = items.each_with_object({ ids: Set.new, subscribable_types: Set.new, project_ids: Set.new }) do |item, result|
+ result[:ids] << item[:id]
+ result[:subscribable_types] << item[:subscribable_type]
+ result[:project_ids] << item[:project_id]
+ end
+
+ subscriptions = Subscription.where(subscribable_id: values[:ids], subscribable_type: values[:subscribable_types], project_id: values[:project_ids], user: user)
+
+ subscriptions.each do |subscription|
+ loader.call({
+ id: subscription.subscribable_id,
+ subscribable_type: subscription.subscribable_type,
+ project_id: subscription.project_id
+ }, subscription)
+ end
+ end
+ end
+
# Override this method to define custom logic to consider a subscribable as
# subscribed without an explicit subscription record.
def subscribed_without_subscriptions?(user, project)
@@ -41,8 +65,10 @@ module Subscribable
def toggle_subscription(user, project = nil)
unsubscribe_from_other_levels(user, project)
+ new_value = !subscribed?(user, project)
+
find_or_initialize_subscription(user, project)
- .update(subscribed: !subscribed?(user, project))
+ .update(subscribed: new_value)
end
def subscribe(user, project = nil)
@@ -83,6 +109,8 @@ module Subscribable
end
def find_or_initialize_subscription(user, project)
+ BatchLoader::Executor.clear_current
+
subscriptions
.find_or_initialize_by(user_id: user.id, project_id: project.try(:id))
end
diff --git a/app/models/concerns/taskable.rb b/app/models/concerns/taskable.rb
index 5debfa6f834..d8867177059 100644
--- a/app/models/concerns/taskable.rb
+++ b/app/models/concerns/taskable.rb
@@ -30,7 +30,8 @@ module Taskable
end
def self.get_updated_tasks(old_content:, new_content:)
- old_tasks, new_tasks = get_tasks(old_content), get_tasks(new_content)
+ old_tasks = get_tasks(old_content)
+ new_tasks = get_tasks(new_content)
new_tasks.select.with_index do |new_task, i|
old_task = old_tasks[i]
diff --git a/app/models/concerns/token_authenticatable_strategies/encrypted.rb b/app/models/concerns/token_authenticatable_strategies/encrypted.rb
index 672402ee4d6..50a2613bb10 100644
--- a/app/models/concerns/token_authenticatable_strategies/encrypted.rb
+++ b/app/models/concerns/token_authenticatable_strategies/encrypted.rb
@@ -42,14 +42,14 @@ module TokenAuthenticatableStrategies
return insecure_strategy.get_token(instance) if migrating?
encrypted_token = instance.read_attribute(encrypted_field)
- token = Gitlab::CryptoHelper.aes256_gcm_decrypt(encrypted_token)
+ token = EncryptionHelper.decrypt_token(encrypted_token)
token || (insecure_strategy.get_token(instance) if optional?)
end
def set_token(instance, token)
raise ArgumentError unless token.present?
- instance[encrypted_field] = Gitlab::CryptoHelper.aes256_gcm_encrypt(token)
+ instance[encrypted_field] = EncryptionHelper.encrypt_token(token)
instance[token_field] = token if migrating?
instance[token_field] = nil if optional?
token
@@ -85,16 +85,9 @@ module TokenAuthenticatableStrategies
end
def find_by_encrypted_token(token, unscoped)
- nonce = Feature.enabled?(:dynamic_nonce_creation) ? find_hashed_iv(token) : Gitlab::CryptoHelper::AES256_GCM_IV_STATIC
- encrypted_value = Gitlab::CryptoHelper.aes256_gcm_encrypt(token, nonce: nonce)
-
- relation(unscoped).find_by(encrypted_field => encrypted_value)
- end
-
- def find_hashed_iv(token)
- token_record = TokenWithIv.find_by_plaintext_token(token)
-
- token_record&.iv || Gitlab::CryptoHelper::AES256_GCM_IV_STATIC
+ encrypted_value = EncryptionHelper.encrypt_token(token)
+ token_encrypted_with_static_iv = Gitlab::CryptoHelper.aes256_gcm_encrypt(token)
+ relation(unscoped).find_by(encrypted_field => [encrypted_value, token_encrypted_with_static_iv])
end
def insecure_strategy
diff --git a/app/models/concerns/token_authenticatable_strategies/encryption_helper.rb b/app/models/concerns/token_authenticatable_strategies/encryption_helper.rb
new file mode 100644
index 00000000000..25c050820d6
--- /dev/null
+++ b/app/models/concerns/token_authenticatable_strategies/encryption_helper.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+module TokenAuthenticatableStrategies
+ class EncryptionHelper
+ DYNAMIC_NONCE_IDENTIFIER = "|"
+ NONCE_SIZE = 12
+
+ def self.encrypt_token(plaintext_token)
+ Gitlab::CryptoHelper.aes256_gcm_encrypt(plaintext_token)
+ end
+
+ def self.decrypt_token(token)
+ return unless token
+
+ # The pattern of the token is "#{DYNAMIC_NONCE_IDENTIFIER}#{token}#{iv_of_12_characters}"
+ if token.start_with?(DYNAMIC_NONCE_IDENTIFIER) && token.size > NONCE_SIZE + DYNAMIC_NONCE_IDENTIFIER.size
+ token_to_decrypt = token[1...-NONCE_SIZE]
+ iv = token[-NONCE_SIZE..-1]
+
+ Gitlab::CryptoHelper.aes256_gcm_decrypt(token_to_decrypt, nonce: iv)
+ else
+ Gitlab::CryptoHelper.aes256_gcm_decrypt(token)
+ end
+ end
+ end
+end
diff --git a/app/models/concerns/vulnerability_finding_helpers.rb b/app/models/concerns/vulnerability_finding_helpers.rb
new file mode 100644
index 00000000000..cf50305faab
--- /dev/null
+++ b/app/models/concerns/vulnerability_finding_helpers.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+module VulnerabilityFindingHelpers
+ extend ActiveSupport::Concern
+end
+
+VulnerabilityFindingHelpers.prepend_if_ee('EE::VulnerabilityFindingHelpers')
diff --git a/app/models/concerns/vulnerability_finding_signature_helpers.rb b/app/models/concerns/vulnerability_finding_signature_helpers.rb
new file mode 100644
index 00000000000..f57e3cb0bfb
--- /dev/null
+++ b/app/models/concerns/vulnerability_finding_signature_helpers.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+module VulnerabilityFindingSignatureHelpers
+ extend ActiveSupport::Concern
+end
+
+VulnerabilityFindingSignatureHelpers.prepend_if_ee('EE::VulnerabilityFindingSignatureHelpers')