diff options
Diffstat (limited to 'app/models/concerns')
-rw-r--r-- | app/models/concerns/approvable_base.rb | 4 | ||||
-rw-r--r-- | app/models/concerns/cache_markdown_field.rb | 33 | ||||
-rw-r--r-- | app/models/concerns/calloutable.rb | 15 | ||||
-rw-r--r-- | app/models/concerns/ci/contextable.rb | 4 | ||||
-rw-r--r-- | app/models/concerns/cron_schedulable.rb | 8 | ||||
-rw-r--r-- | app/models/concerns/enums/ci/commit_status.rb | 1 | ||||
-rw-r--r-- | app/models/concerns/featurable.rb | 21 | ||||
-rw-r--r-- | app/models/concerns/has_repository.rb | 4 | ||||
-rw-r--r-- | app/models/concerns/integrations/has_data_fields.rb | 1 | ||||
-rw-r--r-- | app/models/concerns/issuable.rb | 17 | ||||
-rw-r--r-- | app/models/concerns/loose_foreign_key.rb | 95 | ||||
-rw-r--r-- | app/models/concerns/mentionable.rb | 11 | ||||
-rw-r--r-- | app/models/concerns/optimized_issuable_label_filter.rb | 121 | ||||
-rw-r--r-- | app/models/concerns/partitioned_table.rb | 2 | ||||
-rw-r--r-- | app/models/concerns/relative_positioning.rb | 12 | ||||
-rw-r--r-- | app/models/concerns/sanitizable.rb | 52 | ||||
-rw-r--r-- | app/models/concerns/sortable_title.rb | 21 | ||||
-rw-r--r-- | app/models/concerns/taggable_queries.rb | 10 |
18 files changed, 235 insertions, 197 deletions
diff --git a/app/models/concerns/approvable_base.rb b/app/models/concerns/approvable_base.rb index ef7ba7b1089..8240f9bd6ea 100644 --- a/app/models/concerns/approvable_base.rb +++ b/app/models/concerns/approvable_base.rb @@ -54,4 +54,8 @@ module ApprovableBase def can_be_approved_by?(user) user && !approved_by?(user) && user.can?(:approve_merge_request, self) end + + def can_be_unapproved_by?(user) + user && approved_by?(user) && user.can?(:approve_merge_request, self) + end end diff --git a/app/models/concerns/cache_markdown_field.rb b/app/models/concerns/cache_markdown_field.rb index 44d9beff27e..9414d16beef 100644 --- a/app/models/concerns/cache_markdown_field.rb +++ b/app/models/concerns/cache_markdown_field.rb @@ -160,39 +160,6 @@ module CacheMarkdownField # We can only store mentions if the mentionable is a database object return unless self.is_a?(ApplicationRecord) - return store_mentions_without_subtransaction! if Feature.enabled?(:store_mentions_without_subtransaction, default_enabled: :yaml) - - refs = all_references(self.author) - - references = {} - references[:mentioned_users_ids] = refs.mentioned_user_ids.presence - references[:mentioned_groups_ids] = refs.mentioned_group_ids.presence - references[:mentioned_projects_ids] = refs.mentioned_project_ids.presence - - # One retry is enough as next time `model_user_mention` should return the existing mention record, - # that threw the `ActiveRecord::RecordNotUnique` exception in first place. - self.class.safe_ensure_unique(retries: 1) do - user_mention = model_user_mention - - # this may happen due to notes polymorphism, so noteable_id may point to a record - # that no longer exists as we cannot have FK on noteable_id - break if user_mention.blank? - - user_mention.mentioned_users_ids = references[:mentioned_users_ids] - user_mention.mentioned_groups_ids = references[:mentioned_groups_ids] - user_mention.mentioned_projects_ids = references[:mentioned_projects_ids] - - if user_mention.has_mentions? - user_mention.save! - else - user_mention.destroy! - end - end - - true - end - - def store_mentions_without_subtransaction! identifier = user_mention_identifier # this may happen due to notes polymorphism, so noteable_id may point to a record diff --git a/app/models/concerns/calloutable.rb b/app/models/concerns/calloutable.rb new file mode 100644 index 00000000000..8b9cfae6a32 --- /dev/null +++ b/app/models/concerns/calloutable.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Calloutable + extend ActiveSupport::Concern + + included do + belongs_to :user + + validates :user, presence: true + end + + def dismissed_after?(dismissed_after) + dismissed_at > dismissed_after + end +end diff --git a/app/models/concerns/ci/contextable.rb b/app/models/concerns/ci/contextable.rb index bdba2d3e251..27a704c1de0 100644 --- a/app/models/concerns/ci/contextable.rb +++ b/app/models/concerns/ci/contextable.rb @@ -33,13 +33,13 @@ module Ci # def simple_variables strong_memoize(:simple_variables) do - scoped_variables(environment: nil).to_runner_variables + scoped_variables(environment: nil) end end def simple_variables_without_dependencies strong_memoize(:variables_without_dependencies) do - scoped_variables(environment: nil, dependencies: false).to_runner_variables + scoped_variables(environment: nil, dependencies: false) end end diff --git a/app/models/concerns/cron_schedulable.rb b/app/models/concerns/cron_schedulable.rb index 48605ecc3d7..d5b86db2640 100644 --- a/app/models/concerns/cron_schedulable.rb +++ b/app/models/concerns/cron_schedulable.rb @@ -14,12 +14,10 @@ module CronSchedulable # The `next_run_at` column is set to the actual execution date of worker that # triggers the schedule. This way, a schedule like `*/1 * * * *` won't be triggered # in a short interval when the worker runs irregularly by Sidekiq Memory Killer. - def calculate_next_run_at - now = Time.zone.now + def calculate_next_run_at(start_time = Time.zone.now) + ideal_next_run = ideal_next_run_from(start_time) - ideal_next_run = ideal_next_run_from(now) - - if ideal_next_run == cron_worker_next_run_from(now) + if ideal_next_run == cron_worker_next_run_from(start_time) ideal_next_run else cron_worker_next_run_from(ideal_next_run) diff --git a/app/models/concerns/enums/ci/commit_status.rb b/app/models/concerns/enums/ci/commit_status.rb index 16dec5fb081..7f46e44697e 100644 --- a/app/models/concerns/enums/ci/commit_status.rb +++ b/app/models/concerns/enums/ci/commit_status.rb @@ -26,6 +26,7 @@ module Enums pipeline_loop_detected: 17, no_matching_runner: 18, # not used anymore, but cannot be deleted because of old data trace_size_exceeded: 19, + builds_disabled: 20, insufficient_bridge_permissions: 1_001, downstream_bridge_project_not_found: 1_002, invalid_bridge_trigger: 1_003, diff --git a/app/models/concerns/featurable.rb b/app/models/concerns/featurable.rb index ed9bce87da1..70d67fc7559 100644 --- a/app/models/concerns/featurable.rb +++ b/app/models/concerns/featurable.rb @@ -83,6 +83,10 @@ module Featurable end end + included do + validate :allowed_access_levels + end + def access_level(feature) public_send(self.class.access_level_attribute(feature)) # rubocop:disable GitlabSecurity/PublicSend end @@ -94,4 +98,21 @@ module Featurable def string_access_level(feature) self.class.str_from_access_level(access_level(feature)) end + + private + + def allowed_access_levels + validator = lambda do |field| + level = public_send(field) || ENABLED # rubocop:disable GitlabSecurity/PublicSend + not_allowed = level > ENABLED + self.errors.add(field, "cannot have public visibility level") if not_allowed + end + + (self.class.available_features - feature_validation_exclusion).each {|f| validator.call("#{f}_access_level")} + end + + # Features that we should exclude from the validation + def feature_validation_exclusion + [] + end end diff --git a/app/models/concerns/has_repository.rb b/app/models/concerns/has_repository.rb index 1b4c590694a..9218ba47d20 100644 --- a/app/models/concerns/has_repository.rb +++ b/app/models/concerns/has_repository.rb @@ -122,4 +122,8 @@ module HasRepository def after_repository_change_head reload_default_branch end + + def after_change_head_branch_does_not_exist(branch) + # No-op (by default) + end end diff --git a/app/models/concerns/integrations/has_data_fields.rb b/app/models/concerns/integrations/has_data_fields.rb index e9aaaac8226..1709b56080e 100644 --- a/app/models/concerns/integrations/has_data_fields.rb +++ b/app/models/concerns/integrations/has_data_fields.rb @@ -46,6 +46,7 @@ module Integrations has_one :issue_tracker_data, autosave: true, inverse_of: :integration, foreign_key: :service_id, class_name: 'Integrations::IssueTrackerData' has_one :jira_tracker_data, autosave: true, inverse_of: :integration, foreign_key: :service_id, class_name: 'Integrations::JiraTrackerData' has_one :open_project_tracker_data, autosave: true, inverse_of: :integration, foreign_key: :service_id, class_name: 'Integrations::OpenProjectTrackerData' + has_one :zentao_tracker_data, autosave: true, inverse_of: :integration, foreign_key: :integration_id, class_name: 'Integrations::ZentaoTrackerData' def data_fields raise NotImplementedError diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 8d0f8b01d64..5c307158a9a 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -26,6 +26,7 @@ module Issuable include UpdatedAtFilterable include ClosedAtFilterable include VersionedDescription + include SortableTitle TITLE_LENGTH_MAX = 255 TITLE_HTML_LENGTH_MAX = 800 @@ -116,20 +117,6 @@ module Issuable end # rubocop:enable GitlabSecurity/SqlInjection - scope :without_particular_labels, ->(label_names) do - labels_table = Label.arel_table - label_links_table = LabelLink.arel_table - issuables_table = klass.arel_table - inner_query = label_links_table.project('true') - .join(labels_table, Arel::Nodes::InnerJoin).on(labels_table[:id].eq(label_links_table[:label_id])) - .where(label_links_table[:target_type].eq(name) - .and(label_links_table[:target_id].eq(issuables_table[:id])) - .and(labels_table[:title].in(label_names))) - .exists.not - - where(inner_query) - end - scope :without_label, -> { joins("LEFT OUTER JOIN label_links ON label_links.target_type = '#{name}' AND label_links.target_id = #{table_name}.id").where(label_links: { id: nil }) } scope :with_label_ids, ->(label_ids) { joins(:label_links).where(label_links: { label_id: label_ids }) } scope :join_project, -> { joins(:project) } @@ -293,6 +280,8 @@ module Issuable when 'popularity', 'popularity_desc', 'upvotes_desc' then order_upvotes_desc when 'priority', 'priority_asc' then order_due_date_and_labels_priority(excluded_labels: excluded_labels) when 'priority_desc' then order_due_date_and_labels_priority('DESC', excluded_labels: excluded_labels) + when 'title_asc' then order_title_asc.with_order_id_desc + when 'title_desc' then order_title_desc.with_order_id_desc else order_by(method) end diff --git a/app/models/concerns/loose_foreign_key.rb b/app/models/concerns/loose_foreign_key.rb new file mode 100644 index 00000000000..4e822a04869 --- /dev/null +++ b/app/models/concerns/loose_foreign_key.rb @@ -0,0 +1,95 @@ +# frozen_string_literal: true + +module LooseForeignKey + extend ActiveSupport::Concern + + # This concern adds loose foreign key support to ActiveRecord models. + # Loose foreign keys allow delayed processing of associated database records + # with similar guarantees than a database foreign key. + # + # TODO: finalize this later once the async job is in place + # + # Prerequisites: + # + # To start using the concern, you'll need to install a database trigger to the parent + # table in a standard DB migration (not post-migration). + # + # > add_loose_foreign_key_support(:projects, :gitlab_main) + # + # Usage: + # + # > class Ci::Build < ApplicationRecord + # > + # > loose_foreign_key :security_scans, :build_id, on_delete: :async_delete, gitlab_schema: :gitlab_main + # > + # > # associations can be still defined, the dependent options is no longer necessary: + # > has_many :security_scans, class_name: 'Security::Scan' + # > + # > end + # + # Options for on_delete: + # + # - :async_delete - deletes the children rows via an asynchronous process. + # - :async_nullify - sets the foreign key column to null via an asynchronous process. + # + # Options for gitlab_schema: + # + # - :gitlab_ci + # - :gitlab_main + # + # The value can be determined by calling `Model.gitlab_schema` where the Model represents + # the model for the child table. + # + # How it works: + # + # When adding loose foreign key support to the table, a DELETE trigger is installed + # which tracks the record deletions (stores primary key value of the deleted row) in + # a database table. + # + # These deletion records are processed asynchronously and records are cleaned up + # according to the loose foreign key definitions described in the model. + # + # The cleanup happens in batches, which reduces the likelyhood of statement timeouts. + # + # When all associations related to the deleted record are cleaned up, the record itself + # is deleted. + included do + class_attribute :loose_foreign_key_definitions, default: [] + end + + class_methods do + def loose_foreign_key(to_table, column, options) + symbolized_options = options.symbolize_keys + + unless base_class? + raise <<~MSG + loose_foreign_key can be only used on base classes, inherited classes are not supported. + Please define the loose_foreign_key on the #{base_class.name} class. + MSG + end + + on_delete_options = %i[async_delete async_nullify] + gitlab_schema_options = [ApplicationRecord.gitlab_schema, Ci::ApplicationRecord.gitlab_schema] + + unless on_delete_options.include?(symbolized_options[:on_delete]&.to_sym) + raise "Invalid on_delete option given: #{symbolized_options[:on_delete]}. Valid options: #{on_delete_options.join(', ')}" + end + + unless gitlab_schema_options.include?(symbolized_options[:gitlab_schema]&.to_sym) + raise "Invalid gitlab_schema option given: #{symbolized_options[:gitlab_schema]}. Valid options: #{gitlab_schema_options.join(', ')}" + end + + definition = ActiveRecord::ConnectionAdapters::ForeignKeyDefinition.new( + table_name.to_s, + to_table.to_s, + { + column: column.to_s, + on_delete: symbolized_options[:on_delete].to_sym, + gitlab_schema: symbolized_options[:gitlab_schema].to_sym + } + ) + + self.loose_foreign_key_definitions += [definition] + end + end +end diff --git a/app/models/concerns/mentionable.rb b/app/models/concerns/mentionable.rb index 4df9e32d8ec..a0ea5ac8012 100644 --- a/app/models/concerns/mentionable.rb +++ b/app/models/concerns/mentionable.rb @@ -217,17 +217,6 @@ module Mentionable def user_mention_association association(:user_mentions).reflection end - - # User mention that is parsed from model description rather then its related notes. - # Models that have a description attribute like Issue, MergeRequest, Epic, Snippet may have such a user mention. - # Other mentionable models like Commit, DesignManagement::Design, will never have such record as those do not have - # a description attribute. - # - # Using this method followed by a call to *save* may result in *ActiveRecord::RecordNotUnique* exception - # in a multi-threaded environment. Make sure to use it within a *safe_ensure_unique* block. - def model_user_mention - user_mentions.where(note_id: nil).first_or_initialize - end end Mentionable.prepend_mod_with('Mentionable') diff --git a/app/models/concerns/optimized_issuable_label_filter.rb b/app/models/concerns/optimized_issuable_label_filter.rb deleted file mode 100644 index 19d2ac620f3..00000000000 --- a/app/models/concerns/optimized_issuable_label_filter.rb +++ /dev/null @@ -1,121 +0,0 @@ -# frozen_string_literal: true - -module OptimizedIssuableLabelFilter - extend ActiveSupport::Concern - - prepended do - extend Gitlab::Cache::RequestCache - - # Avoid repeating label queries times when the finder is instantiated multiple times during the request. - request_cache(:find_label_ids) { [root_namespace.id, params.label_names] } - end - - def by_label(items) - return items unless params.labels? - - return super if Feature.disabled?(:optimized_issuable_label_filter, default_enabled: :yaml) - - target_model = items.model - - if params.filter_by_no_label? - items.where('NOT EXISTS (?)', optimized_any_label_query(target_model)) - elsif params.filter_by_any_label? - items.where('EXISTS (?)', optimized_any_label_query(target_model)) - else - issuables_with_selected_labels(items, target_model) - end - end - - # Taken from IssuableFinder - def count_by_state - return super if Feature.disabled?(:optimized_issuable_label_filter, default_enabled: :yaml) - - count_params = params.merge(state: nil, sort: nil, force_cte: true) - finder = self.class.new(current_user, count_params) - - state_counts = finder - .execute - .reorder(nil) - .group(:state_id) - .count - - counts = Hash.new(0) - - state_counts.each do |key, value| - counts[count_key(key)] += value - end - - counts[:all] = counts.values.sum - counts.with_indifferent_access - end - - private - - def issuables_with_selected_labels(items, target_model) - if root_namespace - all_label_ids = find_label_ids - # Found less labels in the DB than we were searching for. Return nothing. - return items.none if all_label_ids.size != params.label_names.size - - all_label_ids.each do |label_ids| - items = items.where('EXISTS (?)', optimized_label_query_by_label_ids(target_model, label_ids)) - end - else - params.label_names.each do |label_name| - items = items.where('EXISTS (?)', optimized_label_query_by_label_name(target_model, label_name)) - end - end - - items - end - - def find_label_ids - group_labels = Label - .where(project_id: nil) - .where(title: params.label_names) - .where(group_id: root_namespace.self_and_descendants.select(:id)) - - project_labels = Label - .where(group_id: nil) - .where(title: params.label_names) - .where(project_id: Project.select(:id).where(namespace_id: root_namespace.self_and_descendants.select(:id))) - - Label - .from_union([group_labels, project_labels], remove_duplicates: false) - .reorder(nil) - .pluck(:title, :id) - .group_by(&:first) - .values - .map { |labels| labels.map(&:last) } - end - - def root_namespace - strong_memoize(:root_namespace) do - (params.project || params.group)&.root_ancestor - end - end - - def optimized_any_label_query(target_model) - LabelLink - .where(target_type: target_model.name) - .where(LabelLink.arel_table['target_id'].eq(target_model.arel_table['id'])) - .limit(1) - end - - def optimized_label_query_by_label_ids(target_model, label_ids) - LabelLink - .where(target_type: target_model.name) - .where(LabelLink.arel_table['target_id'].eq(target_model.arel_table['id'])) - .where(label_id: label_ids) - .limit(1) - end - - def optimized_label_query_by_label_name(target_model, label_name) - LabelLink - .joins(:label) - .where(target_type: target_model.name) - .where(LabelLink.arel_table['target_id'].eq(target_model.arel_table['id'])) - .where(labels: { name: label_name }) - .limit(1) - end -end diff --git a/app/models/concerns/partitioned_table.rb b/app/models/concerns/partitioned_table.rb index eab5d4c35bb..23d2d00b346 100644 --- a/app/models/concerns/partitioned_table.rb +++ b/app/models/concerns/partitioned_table.rb @@ -14,8 +14,6 @@ module PartitionedTable strategy_class = PARTITIONING_STRATEGIES[strategy.to_sym] || raise(ArgumentError, "Unknown partitioning strategy: #{strategy}") @partitioning_strategy = strategy_class.new(self, partitioning_key, **kwargs) - - Gitlab::Database::Partitioning::PartitionManager.register(self) end end end diff --git a/app/models/concerns/relative_positioning.rb b/app/models/concerns/relative_positioning.rb index 75dfed6d58f..c32e499c329 100644 --- a/app/models/concerns/relative_positioning.rb +++ b/app/models/concerns/relative_positioning.rb @@ -135,21 +135,21 @@ module RelativePositioning before, after = [before, after].sort_by(&:relative_position) if before && after RelativePositioning.mover.move(self, before, after) - rescue ActiveRecord::QueryCanceled, NoSpaceLeft => e + rescue NoSpaceLeft => e could_not_move(e) raise e end def move_after(before = self) RelativePositioning.mover.move(self, before, nil) - rescue ActiveRecord::QueryCanceled, NoSpaceLeft => e + rescue NoSpaceLeft => e could_not_move(e) raise e end def move_before(after = self) RelativePositioning.mover.move(self, nil, after) - rescue ActiveRecord::QueryCanceled, NoSpaceLeft => e + rescue NoSpaceLeft => e could_not_move(e) raise e end @@ -159,9 +159,6 @@ module RelativePositioning rescue NoSpaceLeft => e could_not_move(e) self.relative_position = MAX_POSITION - rescue ActiveRecord::QueryCanceled => e - could_not_move(e) - raise e end def move_to_start @@ -169,9 +166,6 @@ module RelativePositioning rescue NoSpaceLeft => e could_not_move(e) self.relative_position = MIN_POSITION - rescue ActiveRecord::QueryCanceled => e - could_not_move(e) - raise e end # This method is used during rebalancing - override it to customise the update diff --git a/app/models/concerns/sanitizable.rb b/app/models/concerns/sanitizable.rb new file mode 100644 index 00000000000..05756beb404 --- /dev/null +++ b/app/models/concerns/sanitizable.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +# == Sanitizable concern +# +# This concern adds HTML sanitization and validation to models. The intention is +# to help prevent XSS attacks in the event of a by-pass in the frontend +# sanitizer due to a configuration issue or a vulnerability in the sanitizer. +# This approach is commonly referred to as defense-in-depth. +# +# Example: +# +# module Dast +# class Profile < ApplicationRecord +# include Sanitizable +# +# sanitizes! :name, :description + +module Sanitizable + extend ActiveSupport::Concern + + class_methods do + def sanitize(input) + return unless input + + # We return the input unchanged to avoid escaping pre-escaped HTML fragments. + # Please see gitlab-org/gitlab#293634 for an example. + return input unless input == CGI.unescapeHTML(input.to_s) + + CGI.unescapeHTML(Sanitize.fragment(input)) + end + + def sanitizes!(*attrs) + instance_eval do + before_validation do + attrs.each do |attr| + input = public_send(attr) # rubocop: disable GitlabSecurity/PublicSend + + public_send("#{attr}=", self.class.sanitize(input)) # rubocop: disable GitlabSecurity/PublicSend + end + end + + validates_each(*attrs) do |record, attr, input| + # We reject pre-escaped HTML fragments as invalid to avoid saving them + # to the database. + unless input.to_s == CGI.unescapeHTML(input.to_s) + record.errors.add(attr, 'cannot contain escaped HTML entities') + end + end + end + end + end +end diff --git a/app/models/concerns/sortable_title.rb b/app/models/concerns/sortable_title.rb new file mode 100644 index 00000000000..7c5cad17f4c --- /dev/null +++ b/app/models/concerns/sortable_title.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module SortableTitle + extend ActiveSupport::Concern + + included do + scope :order_title_asc, -> { reorder(Arel::Nodes::Ascending.new(arel_table[:title].lower)) } + scope :order_title_desc, -> { reorder(Arel::Nodes::Descending.new(arel_table[:title].lower)) } + end + + class_methods do + def simple_sorts + super.merge( + { + 'title_asc' => -> { order_title_asc }, + 'title_desc' => -> { order_title_desc } + } + ) + end + end +end diff --git a/app/models/concerns/taggable_queries.rb b/app/models/concerns/taggable_queries.rb index cba2e93a86d..06799f0a9f4 100644 --- a/app/models/concerns/taggable_queries.rb +++ b/app/models/concerns/taggable_queries.rb @@ -3,6 +3,10 @@ module TaggableQueries extend ActiveSupport::Concern + MAX_TAGS_IDS = 50 + + TooManyTagsError = Class.new(StandardError) + class_methods do # context is a name `acts_as_taggable context` def arel_tag_names_array(context = :tags) @@ -34,4 +38,10 @@ module TaggableQueries where("EXISTS (?)", matcher) end end + + def tags_ids + tags.limit(MAX_TAGS_IDS).order('id ASC').pluck(:id).tap do |ids| + raise TooManyTagsError if ids.size >= MAX_TAGS_IDS + end + end end |