diff options
author | Ben Bodenmiller <bbodenmiller@hotmail.com> | 2019-08-31 10:21:29 +0000 |
---|---|---|
committer | Ben Bodenmiller <bbodenmiller@hotmail.com> | 2019-08-31 10:21:29 +0000 |
commit | 8898ea87bae5b171ec2d22772e3b0d10a2f73975 (patch) | |
tree | d66514b646191d79f95346886be9481ec0cb2d64 /app/models | |
parent | cc70c6a54aa64425dfa8a3896625b308a83536c3 (diff) | |
parent | 195ac30514e98b0f97bd191183124f06d1d221fc (diff) | |
download | gitlab-ce-patch-73-move-storage-shards.tar.gz |
Merge branch 'master' into 'patch-73-move-storage-shards'patch-73-move-storage-shards
# Conflicts:
# app/models/project.rb
# spec/models/project_spec.rb
Diffstat (limited to 'app/models')
41 files changed, 393 insertions, 142 deletions
diff --git a/app/models/analytics/cycle_analytics/project_stage.rb b/app/models/analytics/cycle_analytics/project_stage.rb index 88c8cb40ccb..a312bd24e78 100644 --- a/app/models/analytics/cycle_analytics/project_stage.rb +++ b/app/models/analytics/cycle_analytics/project_stage.rb @@ -3,7 +3,12 @@ module Analytics module CycleAnalytics class ProjectStage < ApplicationRecord + include Analytics::CycleAnalytics::Stage + + validates :project, presence: true belongs_to :project + + alias_attribute :parent, :project end end end diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 2a99c6e5c59..e39d655325f 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -4,7 +4,6 @@ class ApplicationSetting < ApplicationRecord include CacheableAttributes include CacheMarkdownField include TokenAuthenticatable - include IgnorableColumn include ChronicDurationAttribute add_authentication_token_field :runners_registration_token, encrypted: -> { Feature.enabled?(:application_settings_tokens_optional_encryption, default_enabled: true) ? :optional : :required } @@ -18,19 +17,28 @@ class ApplicationSetting < ApplicationRecord # fix a lot of tests using allow_any_instance_of include ApplicationSettingImplementation + attr_encrypted :asset_proxy_secret_key, + mode: :per_attribute_iv, + insecure_mode: true, + key: Settings.attr_encrypted_db_key_base_truncated, + algorithm: 'aes-256-cbc' + serialize :restricted_visibility_levels # rubocop:disable Cop/ActiveRecordSerialize serialize :import_sources # rubocop:disable Cop/ActiveRecordSerialize serialize :disabled_oauth_sign_in_sources, Array # rubocop:disable Cop/ActiveRecordSerialize serialize :domain_whitelist, Array # rubocop:disable Cop/ActiveRecordSerialize serialize :domain_blacklist, Array # rubocop:disable Cop/ActiveRecordSerialize serialize :repository_storages # rubocop:disable Cop/ActiveRecordSerialize + serialize :asset_proxy_whitelist, Array # rubocop:disable Cop/ActiveRecordSerialize - ignore_column :koding_url - ignore_column :koding_enabled - ignore_column :sentry_enabled - ignore_column :sentry_dsn - ignore_column :clientside_sentry_enabled - ignore_column :clientside_sentry_dsn + self.ignored_columns += %i[ + clientside_sentry_dsn + clientside_sentry_enabled + koding_enabled + koding_url + sentry_dsn + sentry_enabled + ] cache_markdown_field :sign_in_text cache_markdown_field :help_page_text @@ -75,11 +83,11 @@ class ApplicationSetting < ApplicationRecord validates :recaptcha_site_key, presence: true, - if: :recaptcha_enabled + if: :recaptcha_or_login_protection_enabled validates :recaptcha_private_key, presence: true, - if: :recaptcha_enabled + if: :recaptcha_or_login_protection_enabled validates :akismet_api_key, presence: true, @@ -192,6 +200,17 @@ class ApplicationSetting < ApplicationRecord allow_nil: true, numericality: { only_integer: true, greater_than_or_equal_to: 0, less_than: 65536 } + validates :asset_proxy_url, + presence: true, + allow_blank: false, + url: true, + if: :asset_proxy_enabled? + + validates :asset_proxy_secret_key, + presence: true, + allow_blank: false, + if: :asset_proxy_enabled? + SUPPORTED_KEY_TYPES.each do |type| validates :"#{type}_key_restriction", presence: true, key_restriction: { type: type } end @@ -292,4 +311,8 @@ class ApplicationSetting < ApplicationRecord def self.cache_backend Gitlab::ThreadMemoryCache.cache_backend end + + def recaptcha_or_login_protection_enabled + recaptcha_enabled || login_recaptcha_protection_enabled + end end diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb index 55ac1e129cf..f402c0e2775 100644 --- a/app/models/application_setting_implementation.rb +++ b/app/models/application_setting_implementation.rb @@ -23,8 +23,9 @@ module ApplicationSettingImplementation akismet_enabled: false, allow_local_requests_from_web_hooks_and_services: false, allow_local_requests_from_system_hooks: true, - dns_rebinding_protection_enabled: true, + asset_proxy_enabled: false, authorized_keys_enabled: true, # TODO default to false if the instance is configured to use AuthorizedKeysCommand + commit_email_hostname: default_commit_email_hostname, container_registry_token_expire_delay: 5, default_artifacts_expire_in: '30 days', default_branch_protection: Settings.gitlab['default_branch_protection'], @@ -33,7 +34,9 @@ module ApplicationSettingImplementation default_project_visibility: Settings.gitlab.default_projects_features['visibility_level'], default_projects_limit: Settings.gitlab['default_projects_limit'], default_snippet_visibility: Settings.gitlab.default_projects_features['visibility_level'], + diff_max_patch_bytes: Gitlab::Git::Diff::DEFAULT_MAX_PATCH_BYTES, disabled_oauth_sign_in_sources: [], + dns_rebinding_protection_enabled: true, domain_whitelist: Settings.gitlab['domain_whitelist'], dsa_key_restriction: 0, ecdsa_key_restriction: 0, @@ -52,9 +55,11 @@ module ApplicationSettingImplementation housekeeping_gc_period: 200, housekeeping_incremental_repack_period: 10, import_sources: Settings.gitlab['import_sources'], + local_markdown_version: 0, max_artifacts_size: Settings.artifacts['max_size'], max_attachment_size: Settings.gitlab['max_attachment_size'], mirror_available: true, + outbound_local_requests_whitelist: [], password_authentication_enabled_for_git: true, password_authentication_enabled_for_web: Settings.gitlab['signin_enabled'], performance_bar_allowed_group_id: nil, @@ -63,7 +68,10 @@ module ApplicationSettingImplementation plantuml_url: nil, polling_interval_multiplier: 1, project_export_enabled: true, + protected_ci_variables: false, + raw_blob_request_limit: 300, recaptcha_enabled: false, + login_recaptcha_protection_enabled: false, repository_checks_enabled: true, repository_storages: ['default'], require_two_factor_authentication: false, @@ -95,16 +103,10 @@ module ApplicationSettingImplementation user_default_internal_regex: nil, user_show_add_ssh_key_message: true, usage_stats_set_by_user_id: nil, - diff_max_patch_bytes: Gitlab::Git::Diff::DEFAULT_MAX_PATCH_BYTES, - commit_email_hostname: default_commit_email_hostname, snowplow_collector_hostname: nil, snowplow_cookie_domain: nil, snowplow_enabled: false, - snowplow_site_id: nil, - protected_ci_variables: false, - local_markdown_version: 0, - outbound_local_requests_whitelist: [], - raw_blob_request_limit: 300 + snowplow_site_id: nil } end @@ -198,6 +200,15 @@ module ApplicationSettingImplementation end end + def asset_proxy_whitelist=(values) + values = domain_strings_to_array(values) if values.is_a?(String) + + # make sure we always whitelist the running host + values << Gitlab.config.gitlab.host unless values.include?(Gitlab.config.gitlab.host) + + self[:asset_proxy_whitelist] = values + end + def repository_storages Array(read_attribute(:repository_storages)) end @@ -306,6 +317,7 @@ module ApplicationSettingImplementation values .split(DOMAIN_LIST_SEPARATOR) + .map(&:strip) .reject(&:empty?) .uniq end diff --git a/app/models/award_emoji.rb b/app/models/award_emoji.rb index e26162f6151..0ab302a0f3e 100644 --- a/app/models/award_emoji.rb +++ b/app/models/award_emoji.rb @@ -16,8 +16,10 @@ class AwardEmoji < ApplicationRecord participant :user - scope :downvotes, -> { where(name: DOWNVOTE_NAME) } - scope :upvotes, -> { where(name: UPVOTE_NAME) } + scope :downvotes, -> { named(DOWNVOTE_NAME) } + scope :upvotes, -> { named(UPVOTE_NAME) } + scope :named, -> (names) { where(name: names) } + scope :awarded_by, -> (users) { where(user: users) } after_save :expire_etag_cache after_destroy :expire_etag_cache diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 3c0efca31db..79a2d5e6e9d 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -11,19 +11,20 @@ module Ci include ObjectStorage::BackgroundMove include Presentable include Importable - include IgnorableColumn include Gitlab::Utils::StrongMemoize include Deployable include HasRef BuildArchivedError = Class.new(StandardError) - ignore_column :commands - ignore_column :artifacts_file - ignore_column :artifacts_metadata - ignore_column :artifacts_file_store - ignore_column :artifacts_metadata_store - ignore_column :artifacts_size + self.ignored_columns += %i[ + artifacts_file + artifacts_file_store + artifacts_metadata + artifacts_metadata_store + artifacts_size + commands + ] belongs_to :project, inverse_of: :builds belongs_to :runner @@ -121,6 +122,8 @@ module Ci scope :scheduled_actions, ->() { where(when: :delayed, status: COMPLETED_STATUSES + %i[scheduled]) } scope :ref_protected, -> { where(protected: true) } scope :with_live_trace, -> { where('EXISTS (?)', Ci::BuildTraceChunk.where('ci_builds.id = ci_build_trace_chunks.build_id').select(1)) } + scope :with_stale_live_trace, -> { with_live_trace.finished_before(12.hours.ago) } + scope :finished_before, -> (date) { finished.where('finished_at < ?', date) } scope :matches_tag_ids, -> (tag_ids) do matcher = ::ActsAsTaggableOn::Tagging diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb index e132cb045e2..b4497d8af09 100644 --- a/app/models/ci/job_artifact.rb +++ b/app/models/ci/job_artifact.rb @@ -87,6 +87,8 @@ module Ci scope :expired, -> (limit) { where('expire_at < ?', Time.now).limit(limit) } + scope :scoped_project, -> { where('ci_job_artifacts.project_id = projects.id') } + delegate :filename, :exists?, :open, to: :file enum file_type: { diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 0a943a33bbb..64e372878e6 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -203,6 +203,7 @@ module Ci scope :for_sha, -> (sha) { where(sha: sha) } scope :for_source_sha, -> (source_sha) { where(source_sha: source_sha) } scope :for_sha_or_source_sha, -> (sha) { for_sha(sha).or(for_source_sha(sha)) } + scope :created_after, -> (time) { where('ci_pipelines.created_at > ?', time) } scope :triggered_by_merge_request, -> (merge_request) do where(source: :merge_request_event, merge_request: merge_request) diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb index 43ff874ac23..e0e905ebfa8 100644 --- a/app/models/ci/runner.rb +++ b/app/models/ci/runner.rb @@ -4,7 +4,6 @@ module Ci class Runner < ApplicationRecord extend Gitlab::Ci::Model include Gitlab::SQL::Pattern - include IgnorableColumn include RedisCacheable include ChronicDurationAttribute include FromUnion @@ -23,16 +22,20 @@ module Ci project_type: 3 } - RUNNER_QUEUE_EXPIRY_TIME = 60.minutes ONLINE_CONTACT_TIMEOUT = 1.hour - UPDATE_DB_RUNNER_INFO_EVERY = 40.minutes + RUNNER_QUEUE_EXPIRY_TIME = 1.hour + + # This needs to be less than `ONLINE_CONTACT_TIMEOUT` + UPDATE_CONTACT_COLUMN_EVERY = (40.minutes..55.minutes).freeze + AVAILABLE_TYPES_LEGACY = %w[specific shared].freeze AVAILABLE_TYPES = runner_types.keys.freeze AVAILABLE_STATUSES = %w[active paused online offline].freeze AVAILABLE_SCOPES = (AVAILABLE_TYPES_LEGACY + AVAILABLE_TYPES + AVAILABLE_STATUSES).freeze + FORM_EDITABLE = %i[description tag_list active run_untagged locked access_level maximum_timeout_human_readable].freeze - ignore_column :is_shared + self.ignored_columns = %i[is_shared] has_many :builds has_many :runner_projects, inverse_of: :runner, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent @@ -46,7 +49,7 @@ module Ci scope :active, -> { where(active: true) } scope :paused, -> { where(active: false) } - scope :online, -> { where('contacted_at > ?', contact_time_deadline) } + scope :online, -> { where('contacted_at > ?', online_contact_time_deadline) } # The following query using negation is cheaper than using `contacted_at <= ?` # because there are less runners online than have been created. The # resulting query is quickly finding online ones and then uses the regular @@ -56,6 +59,8 @@ module Ci scope :offline, -> { where.not(id: online) } scope :ordered, -> { order(id: :desc) } + scope :with_recent_runner_queue, -> { where('contacted_at > ?', recent_queue_deadline) } + # BACKWARD COMPATIBILITY: There are needed to maintain compatibility with `AVAILABLE_SCOPES` used by `lib/api/runners.rb` scope :deprecated_shared, -> { instance_type } scope :deprecated_specific, -> { project_type.or(group_type) } @@ -137,10 +142,18 @@ module Ci fuzzy_search(query, [:token, :description]) end - def self.contact_time_deadline + def self.online_contact_time_deadline ONLINE_CONTACT_TIMEOUT.ago end + def self.recent_queue_deadline + # we add queue expiry + online + # - contacted_at can be updated at any time within this interval + # we have always accurate `contacted_at` but it is stored in Redis + # and not persisted in database + (ONLINE_CONTACT_TIMEOUT + RUNNER_QUEUE_EXPIRY_TIME).ago + end + def self.order_by(order) if order == 'contacted_asc' order_contacted_at_asc @@ -174,7 +187,7 @@ module Ci end def online? - contacted_at && contacted_at > self.class.contact_time_deadline + contacted_at && contacted_at > self.class.online_contact_time_deadline end def status @@ -275,9 +288,7 @@ module Ci def persist_cached_data? # Use a random threshold to prevent beating DB updates. - # It generates a distribution between [40m, 80m]. - - contacted_at_max_age = UPDATE_DB_RUNNER_INFO_EVERY + Random.rand(UPDATE_DB_RUNNER_INFO_EVERY) + contacted_at_max_age = Random.rand(UPDATE_CONTACT_COLUMN_EVERY) real_contacted_at = read_attribute(:contacted_at) real_contacted_at.nil? || diff --git a/app/models/clusters/applications/cert_manager.rb b/app/models/clusters/applications/cert_manager.rb index 6bd7473c8ff..27d4180e5b9 100644 --- a/app/models/clusters/applications/cert_manager.rb +++ b/app/models/clusters/applications/cert_manager.rb @@ -3,7 +3,8 @@ module Clusters module Applications class CertManager < ApplicationRecord - VERSION = 'v0.5.2'.freeze + VERSION = 'v0.9.1' + CRD_VERSION = '0.9' self.table_name = 'clusters_applications_cert_managers' @@ -21,16 +22,22 @@ module Clusters validates :email, presence: true def chart - 'stable/cert-manager' + 'certmanager/cert-manager' + end + + def repository + 'https://charts.jetstack.io' end def install_command Gitlab::Kubernetes::Helm::InstallCommand.new( name: 'certmanager', + repository: repository, version: VERSION, rbac: cluster.platform_kubernetes_rbac?, chart: chart, files: files.merge(cluster_issuer_file), + preinstall: pre_install_script, postinstall: post_install_script ) end @@ -46,16 +53,30 @@ module Clusters private + def pre_install_script + [ + apply_file("https://raw.githubusercontent.com/jetstack/cert-manager/release-#{CRD_VERSION}/deploy/manifests/00-crds.yaml"), + "kubectl label --overwrite namespace #{Gitlab::Kubernetes::Helm::NAMESPACE} certmanager.k8s.io/disable-validation=true" + ] + end + def post_install_script - ["kubectl create -f /data/helm/certmanager/config/cluster_issuer.yaml"] + [retry_command(apply_file('/data/helm/certmanager/config/cluster_issuer.yaml'))] + end + + def retry_command(command) + "for i in $(seq 1 30); do #{command} && break; sleep 1s; echo \"Retrying ($i)...\"; done" end def post_delete_script [ delete_private_key, delete_crd('certificates.certmanager.k8s.io'), + delete_crd('certificaterequests.certmanager.k8s.io'), + delete_crd('challenges.certmanager.k8s.io'), delete_crd('clusterissuers.certmanager.k8s.io'), - delete_crd('issuers.certmanager.k8s.io') + delete_crd('issuers.certmanager.k8s.io'), + delete_crd('orders.certmanager.k8s.io') ].compact end @@ -75,6 +96,10 @@ module Clusters Gitlab::Kubernetes::KubectlCmd.delete("crd", definition, "--ignore-not-found") end + def apply_file(filename) + Gitlab::Kubernetes::KubectlCmd.apply_file(filename) + end + def cluster_issuer_file { 'cluster_issuer.yaml': cluster_issuer_yaml_content diff --git a/app/models/clusters/applications/runner.rb b/app/models/clusters/applications/runner.rb index 6533b7a186e..329250255fd 100644 --- a/app/models/clusters/applications/runner.rb +++ b/app/models/clusters/applications/runner.rb @@ -3,7 +3,7 @@ module Clusters module Applications class Runner < ApplicationRecord - VERSION = '0.7.0'.freeze + VERSION = '0.8.0'.freeze self.table_name = 'clusters_applications_runners' diff --git a/app/models/commit.rb b/app/models/commit.rb index 0889ce7e287..1470b50f396 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -35,6 +35,7 @@ class Commit MIN_SHA_LENGTH = Gitlab::Git::Commit::MIN_SHA_LENGTH COMMIT_SHA_PATTERN = /\h{#{MIN_SHA_LENGTH},40}/.freeze + EXACT_COMMIT_SHA_PATTERN = /\A#{COMMIT_SHA_PATTERN}\z/.freeze # Used by GFM to match and present link extensions on node texts and hrefs. LINK_EXTENSION_PATTERN = /(patch)/.freeze @@ -90,7 +91,7 @@ class Commit end def valid_hash?(key) - !!(/\A#{COMMIT_SHA_PATTERN}\z/ =~ key) + !!(EXACT_COMMIT_SHA_PATTERN =~ key) end def lazy(project, oid) @@ -139,6 +140,10 @@ class Commit '@' end + def self.reference_valid?(reference) + !!(reference =~ EXACT_COMMIT_SHA_PATTERN) + end + # Pattern used to extract commit references from text # # This pattern supports cross-project references. diff --git a/app/models/concerns/analytics/cycle_analytics/stage.rb b/app/models/concerns/analytics/cycle_analytics/stage.rb new file mode 100644 index 00000000000..0c603c2d5e6 --- /dev/null +++ b/app/models/concerns/analytics/cycle_analytics/stage.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true + +module Analytics + module CycleAnalytics + module Stage + extend ActiveSupport::Concern + + included do + validates :name, presence: true + validates :start_event_identifier, presence: true + validates :end_event_identifier, presence: true + validate :validate_stage_event_pairs + + enum start_event_identifier: Gitlab::Analytics::CycleAnalytics::StageEvents.to_enum, _prefix: :start_event_identifier + enum end_event_identifier: Gitlab::Analytics::CycleAnalytics::StageEvents.to_enum, _prefix: :end_event_identifier + + alias_attribute :custom_stage?, :custom + end + + def parent=(_) + raise NotImplementedError + end + + def parent + raise NotImplementedError + end + + def start_event + Gitlab::Analytics::CycleAnalytics::StageEvents[start_event_identifier].new(params_for_start_event) + end + + def end_event + Gitlab::Analytics::CycleAnalytics::StageEvents[end_event_identifier].new(params_for_end_event) + end + + def params_for_start_event + {} + end + + def params_for_end_event + {} + end + + def default_stage? + !custom + end + + # The model that is going to be queried, Issue or MergeRequest + def subject_model + start_event.object_type + end + + private + + def validate_stage_event_pairs + return if start_event_identifier.nil? || end_event_identifier.nil? + + unless pairing_rules.fetch(start_event.class, []).include?(end_event.class) + errors.add(:end_event, :not_allowed_for_the_given_start_event) + end + end + + def pairing_rules + Gitlab::Analytics::CycleAnalytics::StageEvents.pairing_rules + end + end + end +end diff --git a/app/models/concerns/awardable.rb b/app/models/concerns/awardable.rb index 14bc56f0eee..f229b42ade6 100644 --- a/app/models/concerns/awardable.rb +++ b/app/models/concerns/awardable.rb @@ -106,30 +106,6 @@ module Awardable end def awarded_emoji?(emoji_name, current_user) - award_emoji.where(name: emoji_name, user: current_user).exists? - end - - def create_award_emoji(name, current_user) - return unless emoji_awardable? - - award_emoji.create(name: normalize_name(name), user: current_user) - end - - def remove_award_emoji(name, current_user) - award_emoji.where(name: name, user: current_user).destroy_all # rubocop: disable DestroyAll - end - - def toggle_award_emoji(emoji_name, current_user) - if awarded_emoji?(emoji_name, current_user) - remove_award_emoji(emoji_name, current_user) - else - create_award_emoji(emoji_name, current_user) - end - end - - private - - def normalize_name(name) - Gitlab::Emoji.normalize_emoji_name(name) + award_emoji.named(emoji_name).awarded_by(current_user).exists? end end diff --git a/app/models/concerns/ignorable_column.rb b/app/models/concerns/ignorable_column.rb deleted file mode 100644 index 3bec44dc79b..00000000000 --- a/app/models/concerns/ignorable_column.rb +++ /dev/null @@ -1,30 +0,0 @@ -# frozen_string_literal: true - -# Module that can be included into a model to make it easier to ignore database -# columns. -# -# Example: -# -# class User < ApplicationRecord -# include IgnorableColumn -# -# ignore_column :updated_at -# end -# -module IgnorableColumn - extend ActiveSupport::Concern - - class_methods do - def columns - super.reject { |column| ignored_columns.include?(column.name) } - end - - def ignored_columns - @ignored_columns ||= Set.new - end - - def ignore_column(*names) - ignored_columns.merge(names.map(&:to_s)) - end - end -end diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index e60b6497cb7..eefe9f00836 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -73,6 +73,7 @@ module Issuable validates :author, presence: true validates :title, presence: true, length: { maximum: 255 } + validates :description, length: { maximum: Gitlab::Database::MAX_TEXT_SIZE_LIMIT }, allow_blank: true validate :milestone_is_valid scope :authored, ->(user) { where(author_id: user) } @@ -186,16 +187,15 @@ module Issuable def sort_by_attribute(method, excluded_labels: []) sorted = case method.to_s - when 'downvotes_desc' then order_downvotes_desc - when 'label_priority' then order_labels_priority(excluded_labels: excluded_labels) - when 'label_priority_desc' then order_labels_priority('DESC', excluded_labels: excluded_labels) - when 'milestone', 'milestone_due_asc' then order_milestone_due_asc - when 'milestone_due_desc' then order_milestone_due_desc - when 'popularity', 'popularity_desc' then order_upvotes_desc - when 'popularity_asc' then order_upvotes_asc - 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 'upvotes_desc' then order_upvotes_desc + when 'downvotes_desc' then order_downvotes_desc + when 'label_priority', 'label_priority_asc' then order_labels_priority(excluded_labels: excluded_labels) + when 'label_priority_desc' then order_labels_priority('DESC', excluded_labels: excluded_labels) + when 'milestone', 'milestone_due_asc' then order_milestone_due_asc + when 'milestone_due_desc' then order_milestone_due_desc + when 'popularity_asc' then order_upvotes_asc + 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) else order_by(method) end diff --git a/app/models/concerns/noteable.rb b/app/models/concerns/noteable.rb index 4b428b0af83..6a44bc7c401 100644 --- a/app/models/concerns/noteable.rb +++ b/app/models/concerns/noteable.rb @@ -73,6 +73,10 @@ module Noteable .discussions(self) end + def capped_notes_count(max) + notes.limit(max).count + end + def grouped_diff_discussions(*args) # Doesn't use `discussion_notes`, because this may include commit diff notes # besides MR diff notes, that we do not want to display on the MR Changes tab. diff --git a/app/models/concerns/routable.rb b/app/models/concerns/routable.rb index 116e8967651..3a486632800 100644 --- a/app/models/concerns/routable.rb +++ b/app/models/concerns/routable.rb @@ -33,8 +33,17 @@ module Routable # # Returns a single object, or nil. def find_by_full_path(path, follow_redirects: false) - order_sql = Arel.sql("(CASE WHEN routes.path = #{connection.quote(path)} THEN 0 ELSE 1 END)") - found = where_full_path_in([path]).reorder(order_sql).take + increment_counter(:routable_find_by_full_path, 'Number of calls to Routable.find_by_full_path') + + if Feature.enabled?(:routable_two_step_lookup) + # Case sensitive match first (it's cheaper and the usual case) + # If we didn't have an exact match, we perform a case insensitive search + found = joins(:route).find_by(routes: { path: path }) || where_full_path_in([path]).take + else + order_sql = Arel.sql("(CASE WHEN routes.path = #{connection.quote(path)} THEN 0 ELSE 1 END)") + found = where_full_path_in([path]).reorder(order_sql).take + end + return found if found if follow_redirects @@ -52,12 +61,23 @@ module Routable def where_full_path_in(paths) return none if paths.empty? + increment_counter(:routable_where_full_path_in, 'Number of calls to Routable.where_full_path_in') + wheres = paths.map do |path| "(LOWER(routes.path) = LOWER(#{connection.quote(path)}))" end joins(:route).where(wheres.join(' OR ')) end + + # Temporary instrumentation of method calls + def increment_counter(counter, description) + @counters[counter] ||= Gitlab::Metrics.counter(counter, description) + + @counters[counter].increment + rescue + # ignore the error + end end def full_name diff --git a/app/models/concerns/sortable.rb b/app/models/concerns/sortable.rb index df1a9e3fe6e..c4af1b1fab2 100644 --- a/app/models/concerns/sortable.rb +++ b/app/models/concerns/sortable.rb @@ -27,14 +27,18 @@ module Sortable def simple_sorts { 'created_asc' => -> { order_created_asc }, + 'created_at_asc' => -> { order_created_asc }, 'created_date' => -> { order_created_desc }, 'created_desc' => -> { order_created_desc }, + 'created_at_desc' => -> { order_created_desc }, 'id_asc' => -> { order_id_asc }, 'id_desc' => -> { order_id_desc }, 'name_asc' => -> { order_name_asc }, 'name_desc' => -> { order_name_desc }, 'updated_asc' => -> { order_updated_asc }, - 'updated_desc' => -> { order_updated_desc } + 'updated_at_asc' => -> { order_updated_asc }, + 'updated_desc' => -> { order_updated_desc }, + 'updated_at_desc' => -> { order_updated_desc } } end diff --git a/app/models/deploy_key.rb b/app/models/deploy_key.rb index 0bd90bd28e3..22ab326a0ab 100644 --- a/app/models/deploy_key.rb +++ b/app/models/deploy_key.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class DeployKey < Key - include IgnorableColumn include FromUnion has_many :deploy_keys_projects, inverse_of: :deploy_key, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent @@ -11,7 +10,7 @@ class DeployKey < Key scope :are_public, -> { where(public: true) } scope :with_projects, -> { includes(deploy_keys_projects: { project: [:route, :namespace] }) } - ignore_column :can_push + self.ignored_columns += %i[can_push] accepts_nested_attributes_for :deploy_keys_projects diff --git a/app/models/deploy_token.rb b/app/models/deploy_token.rb index 33f0be91632..85f5a2040c0 100644 --- a/app/models/deploy_token.rb +++ b/app/models/deploy_token.rb @@ -5,7 +5,7 @@ class DeployToken < ApplicationRecord include TokenAuthenticatable include PolicyActor include Gitlab::Utils::StrongMemoize - add_authentication_token_field :token + add_authentication_token_field :token, encrypted: :optional AVAILABLE_SCOPES = %i(read_repository read_registry).freeze GITLAB_DEPLOY_TOKEN_NAME = 'gitlab-deploy-token'.freeze diff --git a/app/models/deployment.rb b/app/models/deployment.rb index 68586e7a1fd..bff5d348ca0 100644 --- a/app/models/deployment.rb +++ b/app/models/deployment.rb @@ -162,6 +162,14 @@ class Deployment < ApplicationRecord deployed_at&.to_time&.in_time_zone&.to_s(:medium) end + def deployed_by + # We use deployable's user if available because Ci::PlayBuildService + # does not update the deployment's user, just the one for the deployable. + # TODO: use deployment's user once https://gitlab.com/gitlab-org/gitlab-ce/issues/66442 + # is completed. + deployable&.user || user + end + private def ref_path diff --git a/app/models/event.rb b/app/models/event.rb index 738080eb584..392d7368033 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -2,7 +2,6 @@ class Event < ApplicationRecord include Sortable - include IgnorableColumn include FromUnion default_scope { reorder(nil) } diff --git a/app/models/group.rb b/app/models/group.rb index 6c868b1d1f0..abe93cf3c84 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -15,6 +15,8 @@ class Group < Namespace include WithUploads include Gitlab::Utils::StrongMemoize + ACCESS_REQUEST_APPROVERS_TO_BE_NOTIFIED_LIMIT = 10 + has_many :group_members, -> { where(requested_at: nil) }, dependent: :destroy, as: :source # rubocop:disable Cop/ActiveRecordDependent alias_method :members, :group_members has_many :users, through: :group_members @@ -365,6 +367,8 @@ class Group < Namespace end def max_member_access_for_user(user) + return GroupMember::NO_ACCESS unless user + return GroupMember::OWNER if user.admin? members_with_parents @@ -427,6 +431,10 @@ class Group < Namespace super || ::Gitlab::Access::OWNER_SUBGROUP_ACCESS end + def access_request_approvers_to_be_notified + members.owners.order_recent_sign_in.limit(ACCESS_REQUEST_APPROVERS_TO_BE_NOTIFIED_LIMIT) + end + private def update_two_factor_requirement diff --git a/app/models/issue.rb b/app/models/issue.rb index c5a18f0af0f..75d4fc8c1c5 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -128,11 +128,10 @@ class Issue < ApplicationRecord def self.sort_by_attribute(method, excluded_labels: []) case method.to_s - when 'closest_future_date' then order_closest_future_date - when 'due_date' then order_due_date_asc - when 'due_date_asc' then order_due_date_asc - when 'due_date_desc' then order_due_date_desc - when 'relative_position' then order_relative_position_asc.with_order_id_desc + when 'closest_future_date', 'closest_future_date_asc' then order_closest_future_date + when 'due_date', 'due_date_asc' then order_due_date_asc + when 'due_date_desc' then order_due_date_desc + when 'relative_position', 'relative_position_asc' then order_relative_position_asc.with_order_id_desc else super end @@ -179,7 +178,7 @@ class Issue < ApplicationRecord end def moved? - !moved_to.nil? + !moved_to_id.nil? end def can_move?(user, to_project = nil) diff --git a/app/models/label.rb b/app/models/label.rb index d9455b36242..dc9f0a3d1a9 100644 --- a/app/models/label.rb +++ b/app/models/label.rb @@ -199,7 +199,11 @@ class Label < ApplicationRecord end def title=(value) - write_attribute(:title, sanitize_title(value)) if value.present? + write_attribute(:title, sanitize_value(value)) if value.present? + end + + def description=(value) + write_attribute(:description, sanitize_value(value)) if value.present? end ## @@ -260,7 +264,7 @@ class Label < ApplicationRecord end end - def sanitize_title(value) + def sanitize_value(value) CGI.unescapeHTML(Sanitize.clean(value.to_s)) end diff --git a/app/models/lfs_object.rb b/app/models/lfs_object.rb index 79a376ff0fd..40695a97d97 100644 --- a/app/models/lfs_object.rb +++ b/app/models/lfs_object.rb @@ -2,6 +2,7 @@ class LfsObject < ApplicationRecord include AfterCommitQueue + include EachBatch include ObjectStorage::BackgroundMove has_many :lfs_objects_projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent diff --git a/app/models/list.rb b/app/models/list.rb index ccadd39bda2..ae7085f05a7 100644 --- a/app/models/list.rb +++ b/app/models/list.rb @@ -1,9 +1,11 @@ # frozen_string_literal: true class List < ApplicationRecord + include Importable + belongs_to :board belongs_to :label - include Importable + has_many :list_user_preferences enum list_type: { backlog: 0, label: 1, closed: 2, assignee: 3, milestone: 4 } @@ -16,9 +18,24 @@ class List < ApplicationRecord scope :destroyable, -> { where(list_type: list_types.slice(*destroyable_types).values) } scope :movable, -> { where(list_type: list_types.slice(*movable_types).values) } - scope :preload_associations, -> { preload(:board, :label) } + + scope :preload_associations, -> (user) do + preload(:board, label: :priorities) + .with_preferences_for(user) + end + scope :ordered, -> { order(:list_type, :position) } + # Loads list with preferences for given user + # if preferences exists for user or not + scope :with_preferences_for, -> (user) do + return unless user + + includes(:list_user_preferences).where(list_user_preferences: { user_id: [user.id, nil] }) + end + + alias_method :preferences, :list_user_preferences + class << self def destroyable_types [:label] @@ -29,6 +46,31 @@ class List < ApplicationRecord end end + def preferences_for(user) + return preferences.build unless user + + if preferences.loaded? + preloaded_preferences_for(user) + else + preferences.find_or_initialize_by(user: user) + end + end + + def preloaded_preferences_for(user) + user_preferences = + preferences.find do |preference| + preference.user_id == user.id + end + + user_preferences || preferences.build(user: user) + end + + def update_preferences_for(user, preferences = {}) + return unless user + + preferences_for(user).update(preferences) + end + def destroyable? self.class.destroyable_types.include?(list_type&.to_sym) end @@ -43,6 +85,14 @@ class List < ApplicationRecord def as_json(options = {}) super(options).tap do |json| + json[:collapsed] = false + + if options.key?(:collapsed) + preferences = preferences_for(options[:current_user]) + + json[:collapsed] = preferences.collapsed? + end + if options.key?(:label) json[:label] = label.as_json( project: board.project, diff --git a/app/models/list_user_preference.rb b/app/models/list_user_preference.rb new file mode 100644 index 00000000000..fe1cc7d5425 --- /dev/null +++ b/app/models/list_user_preference.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +class ListUserPreference < ApplicationRecord + belongs_to :user + belongs_to :list + + validates :user, presence: true + validates :list, presence: true + validates :user_id, uniqueness: { scope: :list_id, message: "should have only one list preference per user" } +end diff --git a/app/models/members/group_member.rb b/app/models/members/group_member.rb index f6b19317c50..3d6f397e599 100644 --- a/app/models/members/group_member.rb +++ b/app/models/members/group_member.rb @@ -15,8 +15,8 @@ class GroupMember < Member default_scope { where(source_type: SOURCE_TYPE) } scope :of_groups, ->(groups) { where(source_id: groups.select(:id)) } - scope :count_users_by_group_id, -> { joins(:user).group(:source_id).count } + scope :of_ldap_type, -> { where(ldap: true) } after_create :update_two_factor_requirement, unless: :invite? after_destroy :update_two_factor_requirement, unless: :invite? diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index 2c9dbf2585c..2402fa8e38f 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -4,7 +4,6 @@ class MergeRequestDiff < ApplicationRecord include Sortable include Importable include ManualInverseAssociation - include IgnorableColumn include EachBatch include Gitlab::Utils::StrongMemoize include ObjectStorage::BackgroundMove diff --git a/app/models/namespace/root_storage_statistics.rb b/app/models/namespace/root_storage_statistics.rb index 56c430013ee..ae9b2f14343 100644 --- a/app/models/namespace/root_storage_statistics.rb +++ b/app/models/namespace/root_storage_statistics.rb @@ -8,6 +8,8 @@ class Namespace::RootStorageStatistics < ApplicationRecord belongs_to :namespace has_one :route, through: :namespace + scope :for_namespace_ids, ->(namespace_ids) { where(namespace_id: namespace_ids) } + delegate :all_projects, to: :namespace def recalculate! diff --git a/app/models/note.rb b/app/models/note.rb index a12d1eb7243..ebd13675dc9 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -14,7 +14,6 @@ class Note < ApplicationRecord include CacheMarkdownField include AfterCommitQueue include ResolvableNote - include IgnorableColumn include Editable include Gitlab::SQL::Pattern include ThrottledTouch @@ -34,7 +33,7 @@ class Note < ApplicationRecord end end - ignore_column :original_discussion_id + self.ignored_columns += %i[original_discussion_id] cache_markdown_field :note, pipeline: :note, issuable_state_filter_enabled: true @@ -89,6 +88,7 @@ class Note < ApplicationRecord delegate :title, to: :noteable, allow_nil: true validates :note, presence: true + validates :note, length: { maximum: Gitlab::Database::MAX_TEXT_SIZE_LIMIT } validates :project, presence: true, if: :for_project_noteable? # Attachments are deprecated and are handled by Markdown uploader @@ -331,6 +331,10 @@ class Note < ApplicationRecord cross_reference? && !all_referenced_mentionables_allowed?(user) end + def visible_for?(user) + !cross_reference_not_visible_for?(user) + end + def award_emoji? can_be_award_emoji? && contains_emoji_only? end diff --git a/app/models/notification_setting.rb b/app/models/notification_setting.rb index 8306b11a7b6..637c017a342 100644 --- a/app/models/notification_setting.rb +++ b/app/models/notification_setting.rb @@ -1,9 +1,7 @@ # frozen_string_literal: true class NotificationSetting < ApplicationRecord - include IgnorableColumn - - ignore_column :events + self.ignored_columns += %i[events] enum level: { global: 3, watch: 2, participating: 1, mention: 4, disabled: 0, custom: 5 } diff --git a/app/models/project.rb b/app/models/project.rb index 4fa486da760..51d26b764fc 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -55,10 +55,16 @@ class Project < ApplicationRecord VALID_MIRROR_PORTS = [22, 80, 443].freeze VALID_MIRROR_PROTOCOLS = %w(http https ssh git).freeze + ACCESS_REQUEST_APPROVERS_TO_BE_NOTIFIED_LIMIT = 10 + + SORTING_PREFERENCE_FIELD = :projects_sort + cache_markdown_field :description, pipeline: :description delegate :feature_available?, :builds_enabled?, :wiki_enabled?, :merge_requests_enabled?, :issues_enabled?, :pages_enabled?, :public_pages?, + :merge_requests_access_level, :issues_access_level, :wiki_access_level, + :snippets_access_level, :builds_access_level, :repository_access_level, to: :project_feature, allow_nil: true delegate :base_dir, :disk_path, :ensure_storage_path_exists, to: :storage @@ -495,6 +501,7 @@ class Project < ApplicationRecord # We require an alias to the project_mirror_data_table in order to use import_state in our queries scope :joins_import_state, -> { joins("INNER JOIN project_mirror_data import_state ON import_state.project_id = projects.id") } scope :for_group, -> (group) { where(group: group) } + scope :for_group_and_its_subgroups, ->(group) { where(namespace_id: group.self_and_descendants.select(:id)) } class << self # Searches for a list of projects based on the query given in `query`. @@ -2173,8 +2180,7 @@ class Project < ApplicationRecord hashed_storage?(:repository) && public? && repository_exists? && - Gitlab::CurrentSettings.hashed_storage_enabled && - Feature.enabled?(:object_pools, self, default_enabled: true) + Gitlab::CurrentSettings.hashed_storage_enabled end def leave_pool_repository @@ -2199,6 +2205,10 @@ class Project < ApplicationRecord self.repository_read_only = true end + def access_request_approvers_to_be_notified + members.maintainers.order_recent_sign_in.limit(ACCESS_REQUEST_APPROVERS_TO_BE_NOTIFIED_LIMIT) + end + private def merge_requests_allowing_collaboration(source_branch = nil) diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb index d08fcd8954d..0728c83005e 100644 --- a/app/models/project_services/jira_service.rb +++ b/app/models/project_services/jira_service.rb @@ -64,7 +64,12 @@ class JiraService < IssueTrackerService end def client - @client ||= JIRA::Client.new(options) + @client ||= begin + JIRA::Client.new(options).tap do |client| + # Replaces JIRA default http client with our implementation + client.request_client = Gitlab::Jira::HttpClient.new(client.options) + end + end end def help diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb index c91add6439f..4a19e05bf76 100644 --- a/app/models/project_wiki.rb +++ b/app/models/project_wiki.rb @@ -85,6 +85,10 @@ class ProjectWiki list_pages(limit: 1).empty? end + def exists? + !empty? + end + # Lists wiki pages of the repository. # # limit - max number of pages returned by the method. diff --git a/app/models/remote_mirror.rb b/app/models/remote_mirror.rb index c9ee0653d86..41e63986286 100644 --- a/app/models/remote_mirror.rb +++ b/app/models/remote_mirror.rb @@ -200,6 +200,7 @@ class RemoteMirror < ApplicationRecord result.password = '*****' if result.password result.user = '*****' if result.user && result.user != 'git' # tokens or other data may be saved as user result.to_s + rescue URI::Error end def ensure_remote! diff --git a/app/models/system_note_metadata.rb b/app/models/system_note_metadata.rb index 9a2640db9ca..a19755d286a 100644 --- a/app/models/system_note_metadata.rb +++ b/app/models/system_note_metadata.rb @@ -9,7 +9,7 @@ class SystemNoteMetadata < ApplicationRecord TYPES_WITH_CROSS_REFERENCES = %w[ commit cross_reference close duplicate - moved + moved merge ].freeze ICON_TYPES = %w[ diff --git a/app/models/todo.rb b/app/models/todo.rb index 240c91da5b6..1ec04189482 100644 --- a/app/models/todo.rb +++ b/app/models/todo.rb @@ -186,9 +186,9 @@ class Todo < ApplicationRecord def target_reference if for_commit? - target.reference_link_text(full: true) + target.reference_link_text else - target.to_reference(full: true) + target.to_reference end end diff --git a/app/models/user.rb b/app/models/user.rb index 6131a8dc710..3ca84ba612a 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -13,7 +13,6 @@ class User < ApplicationRecord include Sortable include CaseSensitivity include TokenAuthenticatable - include IgnorableColumn include FeatureGate include CreatedAtFilterable include BulkMemberAccessLoad @@ -24,9 +23,11 @@ class User < ApplicationRecord DEFAULT_NOTIFICATION_LEVEL = :participating - ignore_column :external_email - ignore_column :email_provider - ignore_column :authentication_token + self.ignored_columns += %i[ + authentication_token + email_provider + external_email + ] add_authentication_token_field :incoming_email_token, token_generator: -> { SecureRandom.hex.to_i(16).to_s(36) } add_authentication_token_field :feed_token @@ -161,6 +162,8 @@ class User < ApplicationRecord # # Note: devise :validatable above adds validations for :email and :password validates :name, presence: true, length: { maximum: 128 } + validates :first_name, length: { maximum: 255 } + validates :last_name, length: { maximum: 255 } validates :email, confirmation: true validates :notification_email, presence: true validates :notification_email, devise_email: true, if: ->(user) { user.notification_email != user.email } @@ -643,6 +646,13 @@ class User < ApplicationRecord end end + # will_save_change_to_attribute? is used by Devise to check if it is necessary + # to clear any existing reset_password_tokens before updating an authentication_key + # and login in our case is a virtual attribute to allow login by username or email. + def will_save_change_to_login? + will_save_change_to_username? || will_save_change_to_email? + end + def unique_email if !emails.exists?(email: email) && Email.exists?(email: email) errors.add(:email, _('has already been taken')) @@ -881,7 +891,15 @@ class User < ApplicationRecord end def first_name - name.split.first unless name.blank? + read_attribute(:first_name) || begin + name.split(' ').first unless name.blank? + end + end + + def last_name + read_attribute(:last_name) || begin + name.split(' ').drop(1).join(' ') unless name.blank? + end end def projects_limit_left diff --git a/app/models/users_star_project.rb b/app/models/users_star_project.rb index 3c7a805cc5c..c633e2d8b3d 100644 --- a/app/models/users_star_project.rb +++ b/app/models/users_star_project.rb @@ -17,6 +17,7 @@ class UsersStarProject < ApplicationRecord scope :by_project, -> (project) { where(project_id: project.id) } scope :with_visible_profile, -> (user) { joins(:user).merge(User.with_visible_profile(user)) } scope :with_public_profile, -> { joins(:user).merge(User.with_public_profile) } + scope :preload_users, -> { preload(:user) } class << self def sort_by_attribute(method) |