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:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-06-06 03:08:24 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-06-06 03:08:24 +0300
commitbdad4dd5dabec7a0a611d5241fa2d56359884420 (patch)
tree333f5147c450cf0849823c7c42c7e52d70a0af74
parent96e23b2017cbe56969771960f6c274c5d3599397 (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--CHANGELOG.md74
-rw-r--r--app/assets/javascripts/single_file_diff.js4
-rw-r--r--app/controllers/abuse_reports_controller.rb9
-rw-r--r--app/helpers/merge_requests_helper.rb4
-rw-r--r--app/models/concerns/ci/artifactable.rb4
-rw-r--r--app/models/concerns/exportable.rb69
-rw-r--r--app/models/concerns/issuable.rb5
-rw-r--r--app/models/issue.rb1
-rw-r--r--app/models/label.rb15
-rw-r--r--app/models/project_team.rb6
-rw-r--r--app/models/user.rb4
-rw-r--r--config/feature_flags/development/search_projects_hide_archived.yml (renamed from config/feature_flags/development/ci_multi_doc_yaml.yml)12
-rw-r--r--config/routes.rb2
-rw-r--r--doc/api/graphql/reference/index.md2
-rw-r--r--doc/api/projects.md5
-rw-r--r--doc/user/ai_features.md20
-rw-r--r--doc/user/group/reporting/git_abuse_rate_limit.md4
-rw-r--r--doc/user/project/members/index.md5
-rw-r--r--doc/user/search/index.md6
-rw-r--r--lib/api/concerns/packages/npm_endpoints.rb7
-rw-r--r--lib/banzai/filter/dollar_math_post_filter.rb28
-rw-r--r--lib/banzai/filter/front_matter_filter.rb10
-rw-r--r--lib/banzai/filter/inline_diff_filter.rb17
-rw-r--r--lib/extracts_ref.rb6
-rw-r--r--lib/gitlab/background_migration/mailers/views/unconfirm_mailer/unconfirm_notification_email.html.haml6
-rw-r--r--lib/gitlab/background_migration/mailers/views/unconfirm_mailer/unconfirm_notification_email.text.erb4
-rw-r--r--lib/gitlab/checks/tag_check.rb12
-rw-r--r--lib/gitlab/ci/artifacts/decompressed_artifact_size_validator.rb60
-rw-r--r--lib/gitlab/ci/config/yaml/loader.rb18
-rw-r--r--lib/gitlab/ci/config/yaml/result.rb2
-rw-r--r--lib/gitlab/ci/decompressed_gzip_size_validator.rb79
-rw-r--r--lib/gitlab/ci/templates/Security/DAST.gitlab-ci.yml8
-rw-r--r--lib/gitlab/ci/templates/Security/DAST.latest.gitlab-ci.yml15
-rw-r--r--lib/gitlab/front_matter.rb38
-rw-r--r--lib/gitlab/import_export/json/streaming_serializer.rb23
-rw-r--r--lib/gitlab/search_results.rb2
-rw-r--r--lib/gitlab/untrusted_regexp.rb4
-rw-r--r--lib/gitlab/wiki_pages/front_matter_parser.rb12
-rw-r--r--spec/controllers/projects_controller_spec.rb83
-rw-r--r--spec/factories/users.rb4
-rw-r--r--spec/frontend/single_file_diff_spec.js15
-rw-r--r--spec/helpers/merge_requests_helper_spec.rb18
-rw-r--r--spec/lib/banzai/filter/front_matter_filter_spec.rb32
-rw-r--r--spec/lib/banzai/filter/inline_diff_filter_spec.rb8
-rw-r--r--spec/lib/banzai/filter/math_filter_spec.rb8
-rw-r--r--spec/lib/gitlab/background_migration/mailers/unconfirm_mailer_spec.rb2
-rw-r--r--spec/lib/gitlab/checks/tag_check_spec.rb25
-rw-r--r--spec/lib/gitlab/ci/artifacts/decompressed_artifact_size_validator_spec.rb95
-rw-r--r--spec/lib/gitlab/ci/config/yaml/result_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/config/yaml_spec.rb78
-rw-r--r--spec/lib/gitlab/ci/decompressed_gzip_size_validator_spec.rb127
-rw-r--r--spec/lib/gitlab/import_export/project/tree_saver_spec.rb12
-rw-r--r--spec/lib/gitlab/search_results_spec.rb15
-rw-r--r--spec/models/concerns/ci/artifactable_spec.rb15
-rw-r--r--spec/models/concerns/exportable_spec.rb79
-rw-r--r--spec/models/concerns/issuable_spec.rb18
-rw-r--r--spec/models/label_spec.rb116
-rw-r--r--spec/models/members/project_member_spec.rb30
-rw-r--r--spec/models/project_team_spec.rb51
-rw-r--r--spec/models/user_spec.rb149
-rw-r--r--spec/requests/abuse_reports_controller_spec.rb37
-rw-r--r--spec/requests/api/npm_group_packages_spec.rb2
-rw-r--r--spec/requests/api/npm_instance_packages_spec.rb5
-rw-r--r--spec/requests/api/npm_project_packages_spec.rb3
-rw-r--r--spec/routing/routing_spec.rb8
-rw-r--r--spec/support/shared_examples/features/reportable_note_shared_examples.rb1
-rw-r--r--spec/support/shared_examples/models/exportable_shared_examples.rb62
-rw-r--r--spec/support/shared_examples/requests/api/npm_packages_shared_examples.rb11
68 files changed, 1318 insertions, 395 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b834564d3c3..483f4dc7acd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,35 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
+## 16.0.2 (2023-06-05)
+
+### Fixed (1 change)
+
+- [Fix Sidekiq crash when gitlab.yml contains UTF-8 characters](gitlab-org/security/gitlab@bb3f94f25f14068e11ff242e00841ec6ad4952a5)
+
+### Changed (1 change)
+
+- [Introduce parallelised BitBucket Server Importer](gitlab-org/security/gitlab@6aa9cf0799d3fa30d7498a9d119dbfb52a839247)
+
+### Security (16 changes)
+
+- [Fix DoS on test report artifacts](gitlab-org/security/gitlab@1058018d0f40165150742f2703e3feb0e0810799) ([merge request](gitlab-org/security/gitlab!3271))
+- [Fix XSS in Abuse Reports form action](gitlab-org/security/gitlab@c5ab6568c8d9d084876b8680394cfe18d6c58ba1) ([merge request](gitlab-org/security/gitlab!3289))
+- [Escape the source branch link correctly](gitlab-org/security/gitlab@40a3d27c0293190fb094216a8d7a193751e61acd) ([merge request](gitlab-org/security/gitlab!3286))
+- [Import source owners with maintainer access if importer is a maintainer](gitlab-org/security/gitlab@2d580f269e4285834f478df6601132f7cdc3130a) ([merge request](gitlab-org/security/gitlab!3282))
+- [Filter inaccessible issuable notes when exporting project](gitlab-org/security/gitlab@ca46e35908072748b5ae68d099acf46f8f667cfc) ([merge request](gitlab-org/security/gitlab!3275))
+- [Block tag names that are prepended with refs/tags/, due to conflicts](gitlab-org/security/gitlab@80e86cec6ea8a87c6b869eeeaa8c9a317f53d26d) ([merge request](gitlab-org/security/gitlab!3269))
+- [Set IP in ActionContoller filter before IP enforcement is evaluated](gitlab-org/security/gitlab@121f1270bed598cf1ca249fc5fd27c9806765421) ([merge request](gitlab-org/security/gitlab!3278))
+- [Prevent primary email returned as verified on unsaved change](gitlab-org/security/gitlab@506533fc3a1928361ea451dbb7a702da2912f16f) ([merge request](gitlab-org/security/gitlab!3259))
+- [Use UntrustedRegexp to protect FrontMatter filter](gitlab-org/security/gitlab@50e26a058850e907970ea12d94a72e8be4c6b296) ([merge request](gitlab-org/security/gitlab!3258))
+- [Improve ambiguous_ref? logic to include heads and tags](gitlab-org/security/gitlab@0d24382ab68543cafb08e88e25ff973cd7fbd17e) ([merge request](gitlab-org/security/gitlab!3294))
+- [Use UntrustedRegexp to protect InlineDiff filter](gitlab-org/security/gitlab@2dc1ecf6a4f89b43a6e75f07580ee4076d9073dc) ([merge request](gitlab-org/security/gitlab!3253))
+- [Ignore user-defined diff paths in diff notes](gitlab-org/security/gitlab@5f4dc756d0aa396e808d556d82e74a1832728b09) ([merge request](gitlab-org/security/gitlab!3266))
+- [Reject NPM metadata requests with invalid package_name](gitlab-org/security/gitlab@28e13218559dc1fe3e82400fe438d09d20f210c1) ([merge request](gitlab-org/security/gitlab!3281))
+- [Use UntrustedRegexp to protect MathFilter regex](gitlab-org/security/gitlab@d686081bc5a93b7e55bca0fde388bca7cc8d3626) ([merge request](gitlab-org/security/gitlab!3252))
+- [Resolve Overall Project Vulnerability Disclosure](gitlab-org/security/gitlab@0b2770814ad2f6c741b146c25025773678efbcb1) ([merge request](gitlab-org/security/gitlab!3270))
+- [Validate description length in labels](gitlab-org/security/gitlab@688225aba7e485de365b1ad914049acbc6bd07d2) ([merge request](gitlab-org/security/gitlab!3244))
+
## 16.0.1 (2023-05-22)
### Security (1 change)
@@ -780,6 +809,27 @@ entry.
- [Add index to group_group_links table](gitlab-org/gitlab@9a3f2c1a90b54074e61d0abf07101ce664198e81) ([merge request](gitlab-org/gitlab!117386))
- [Validate the projects.creator_id foregin key synchronously](gitlab-org/gitlab@ed9351984a16f20506babf6eab6706b917904ed1) ([merge request](gitlab-org/gitlab!117147))
+## 15.11.7 (2023-06-05)
+
+### Security (16 changes)
+
+- [Fix DoS on test report artifacts](gitlab-org/security/gitlab@76133e75ad38326bf971e2d913263349781aecbe) ([merge request](gitlab-org/security/gitlab!3200))
+- [Fix XSS in Abuse Reports form action](gitlab-org/security/gitlab@e9f9b656b34bb30a7bd66ce82a9d8f6ac43c1ba8) ([merge request](gitlab-org/security/gitlab!3290))
+- [Escape the source branch link correctly](gitlab-org/security/gitlab@77ed3e8c2ef51e7bcc89ad1c8c549424a69e3478) ([merge request](gitlab-org/security/gitlab!3288))
+- [Import source owners with maintainer access if importer is a maintainer](gitlab-org/security/gitlab@98f939c9ba3efd5e51807adbaee189f180131544) ([merge request](gitlab-org/security/gitlab!3283))
+- [Filter inaccessible issuable notes when exporting project](gitlab-org/security/gitlab@29fe6582dd81855cdb263e118459aba370a7c7eb) ([merge request](gitlab-org/security/gitlab!3274))
+- [Block tag names that are prepended with refs/tags/, due to conflicts](gitlab-org/security/gitlab@2b39d58eb21cf2ecc581bc5e1bd4dd48dcfd20bc) ([merge request](gitlab-org/security/gitlab!3262))
+- [Set IP in ActionContoller filter before IP enforcement is evaluated](gitlab-org/security/gitlab@8d6e83ff72564f3fa3b24e2040072024b715a073) ([merge request](gitlab-org/security/gitlab!3279))
+- [Prevent primary email returned as verified on unsaved change](gitlab-org/security/gitlab@8ac9a3e3efea62d9b3e6d758ab3f1c43f2354ea5) ([merge request](gitlab-org/security/gitlab!3223))
+- [Use UntrustedRegexp to protect FrontMatter filter](gitlab-org/security/gitlab@5d300c3af9c37a7607d795868ae2b4e51b8802c5) ([merge request](gitlab-org/security/gitlab!3257))
+- [Improve ambiguous_ref? logic to include heads and tags](gitlab-org/security/gitlab@f478b7673efd183971e9375be84ad06af641893f) ([merge request](gitlab-org/security/gitlab!3247))
+- [Use UntrustedRegexp to protect InlineDiff filter](gitlab-org/security/gitlab@4056d6ccc5b592029ea92ebb90b6e6a66c6eb157) ([merge request](gitlab-org/security/gitlab!3254))
+- [Ignore user-defined diff paths in diff notes](gitlab-org/security/gitlab@b21208f4c10e8bd4e6754f9bfd2cc5fc96c8be20) ([merge request](gitlab-org/security/gitlab!3267))
+- [Reject NPM metadata requests with invalid package_name](gitlab-org/security/gitlab@c4d0f6256bae18d9bb9f6afc87afeeb3ff971335) ([merge request](gitlab-org/security/gitlab!3285))
+- [Use UntrustedRegexp to protect MathFilter regex](gitlab-org/security/gitlab@fd8298b140dba65ac77ed340a5f78e1fc8032db6) ([merge request](gitlab-org/security/gitlab!3251))
+- [Resolve Overall Project Vulnerability Disclosure](gitlab-org/security/gitlab@199048eb1c61063409e25d3433e7276faf95709b) ([merge request](gitlab-org/security/gitlab!3230))
+- [Validate description length in labels](gitlab-org/security/gitlab@208342903aabd7c4b78c24c0b9b173dfbd62e405) ([merge request](gitlab-org/security/gitlab!3242))
+
## 15.11.6 (2023-05-24)
### Changed (1 change)
@@ -1659,6 +1709,30 @@ entry.
- [Update header section](gitlab-org/gitlab@cf4ab283267d84fa1c0dc90fefb1b6ddd2617b5c) ([merge request](gitlab-org/gitlab!114102)) **GitLab Enterprise Edition**
- [Swap merge_request_user_mentions.note_id to bigint](gitlab-org/gitlab@96baed47326db4f0cc9f60b2e74215211effd814) ([merge request](gitlab-org/gitlab!113928))
+## 15.10.8 (2023-06-05)
+
+### Fixed (1 change)
+
+- [Convert some regex to use Gitlab::UntrustedRegexp](gitlab-org/security/gitlab@251e0f30177cf458f4384662bdfc14d404c5b98d)
+
+### Security (15 changes)
+
+- [Fix DoS on test report artifacts](gitlab-org/security/gitlab@5893c3c3311052744175051c8393e451771ea100) ([merge request](gitlab-org/security/gitlab!3201))
+- [Fix XSS in Abuse Reports form action](gitlab-org/security/gitlab@da5ecc94a6db6d3e2180d7bd7e2b32e903f7f5c6) ([merge request](gitlab-org/security/gitlab!3291))
+- [Import source owners with maintainer access if importer is a maintainer](gitlab-org/security/gitlab@9995ef153a96621da0d0f2469734dd895485a4d7) ([merge request](gitlab-org/security/gitlab!3284))
+- [Filter inaccessible issuable notes when exporting project](gitlab-org/security/gitlab@cf73c05b31cf466011fbb3492495a7acbcd78d5f) ([merge request](gitlab-org/security/gitlab!3276))
+- [Block tag names that are prepended with refs/tags/, due to conflicts](gitlab-org/security/gitlab@eb4e906ecd8d56ef71c97ab74a32c06c0a9bd7b6) ([merge request](gitlab-org/security/gitlab!3263))
+- [Set IP in ActionContoller filter before IP enforcement is evaluated](gitlab-org/security/gitlab@d10133feff8201b45c8a4c29681db4f167e23d59) ([merge request](gitlab-org/security/gitlab!3280))
+- [Prevent primary email returned as verified on unsaved change](gitlab-org/security/gitlab@ca0f866a5663af8ffa094b0ffd152e5031beecd5) ([merge request](gitlab-org/security/gitlab!3224))
+- [Use UntrustedRegexp to protect FrontMatter filter](gitlab-org/security/gitlab@f66129126262d000c77f36ea2b1b0f5e88f1be13) ([merge request](gitlab-org/security/gitlab!3256))
+- [Improve ambiguous_ref? logic to include heads and tags](gitlab-org/security/gitlab@7fb2dfc1135d74ea261e633ec0a828fa8a8c7ef0) ([merge request](gitlab-org/security/gitlab!3248))
+- [Use UntrustedRegexp to protect InlineDiff filter](gitlab-org/security/gitlab@2a50fd1fd3c4610871644237edc22bbdc9cbcb1d) ([merge request](gitlab-org/security/gitlab!3255))
+- [Ignore user-defined diff paths in diff notes](gitlab-org/security/gitlab@2e969309ad7b3fff551857ee481a154cb3be73f4) ([merge request](gitlab-org/security/gitlab!3268))
+- [Reject NPM metadata requests with invalid package_name](gitlab-org/security/gitlab@7ec6ab8c11d3732b53c7adc951d3da9972695bff) ([merge request](gitlab-org/security/gitlab!3287))
+- [Use UntrustedRegexp to protect MathFilter regex](gitlab-org/security/gitlab@2a2035520eab7263d157b312f5fb7d3d82440ccf) ([merge request](gitlab-org/security/gitlab!3250))
+- [Resolve Overall Project Vulnerability Disclosure](gitlab-org/security/gitlab@457cd1086688b1a44f1f771c407e8d1eaa8f2951) ([merge request](gitlab-org/security/gitlab!3231))
+- [Validate description length in labels](gitlab-org/security/gitlab@c6f95221685f4475a8b91190c61ee4208e257844) ([merge request](gitlab-org/security/gitlab!3243))
+
## 15.10.7 (2023-05-10)
### Fixed (1 change)
diff --git a/app/assets/javascripts/single_file_diff.js b/app/assets/javascripts/single_file_diff.js
index bab167bb7e4..11896a75798 100644
--- a/app/assets/javascripts/single_file_diff.js
+++ b/app/assets/javascripts/single_file_diff.js
@@ -24,7 +24,9 @@ export default class SingleFileDiff {
this.content = $('.diff-content', this.file);
this.$chevronRightIcon = $('.diff-toggle-caret .chevron-right', this.file);
this.$chevronDownIcon = $('.diff-toggle-caret .chevron-down', this.file);
- this.diffForPath = this.content.find('[data-diff-for-path]').data('diffForPath');
+ this.diffForPath = this.content
+ .find('div:not(.note-text)[data-diff-for-path]')
+ .data('diffForPath');
this.isOpen = !this.diffForPath;
if (this.diffForPath) {
this.collapsedContent = this.content;
diff --git a/app/controllers/abuse_reports_controller.rb b/app/controllers/abuse_reports_controller.rb
index 55aef945702..4cd76311b27 100644
--- a/app/controllers/abuse_reports_controller.rb
+++ b/app/controllers/abuse_reports_controller.rb
@@ -1,17 +1,10 @@
# frozen_string_literal: true
class AbuseReportsController < ApplicationController
- before_action :set_user, only: [:new, :add_category]
+ before_action :set_user, only: [:add_category]
feature_category :insider_threat
- def new
- @abuse_report = AbuseReport.new(
- user_id: @user.id,
- reported_from_url: params.fetch(:ref_url, '')
- )
- end
-
def add_category
@abuse_report = AbuseReport.new(
user_id: @user.id,
diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb
index 15901e13c1a..af1c85532c3 100644
--- a/app/helpers/merge_requests_helper.rb
+++ b/app/helpers/merge_requests_helper.rb
@@ -247,13 +247,13 @@ module MergeRequestsHelper
end
branch = if merge_request.for_fork?
- _('%{fork_icon} %{source_project_path}:%{source_branch}').html_safe % { fork_icon: fork_icon.html_safe, source_project_path: merge_request.source_project_path.html_safe, source_branch: merge_request.source_branch.html_safe }
+ html_escape(_('%{fork_icon} %{source_project_path}:%{source_branch}')) % { fork_icon: fork_icon.html_safe, source_project_path: merge_request.source_project_path, source_branch: merge_request.source_branch }
else
merge_request.source_branch
end
branch_title = if merge_request.for_fork?
- _('%{source_project_path}:%{source_branch}').html_safe % { source_project_path: merge_request.source_project_path.html_safe, source_branch: merge_request.source_branch.html_safe }
+ html_escape(_('%{source_project_path}:%{source_branch}')) % { source_project_path: merge_request.source_project_path, source_branch: merge_request.source_branch }
else
merge_request.source_branch
end
diff --git a/app/models/concerns/ci/artifactable.rb b/app/models/concerns/ci/artifactable.rb
index 3fdbd6a8789..974f8213a29 100644
--- a/app/models/concerns/ci/artifactable.rb
+++ b/app/models/concerns/ci/artifactable.rb
@@ -9,6 +9,7 @@ module Ci
STORE_COLUMN = :file_store
NotSupportedAdapterError = Class.new(StandardError)
+
FILE_FORMAT_ADAPTERS = {
# While zip is a streamable file format, performing streaming
# reads requires that each entry in the zip has certain headers
@@ -41,6 +42,9 @@ module Ci
raise NotSupportedAdapterError, 'This file format requires a dedicated adapter'
end
+ ::Gitlab::Ci::Artifacts::DecompressedArtifactSizeValidator
+ .new(file: file, file_format: file_format.to_sym).validate!
+
log_artifacts_filesize(file.model)
file.open do |stream|
diff --git a/app/models/concerns/exportable.rb b/app/models/concerns/exportable.rb
index 066a44912be..c305d272a14 100644
--- a/app/models/concerns/exportable.rb
+++ b/app/models/concerns/exportable.rb
@@ -3,19 +3,6 @@
module Exportable
extend ActiveSupport::Concern
- def readable_records(association, current_user: nil)
- association_records = try(association)
- return unless association_records.present?
-
- if has_many_association?(association)
- DeclarativePolicy.user_scope do
- association_records.select { |record| readable_record?(record, current_user) }
- end
- else
- readable_record?(association_records, current_user) ? association_records : nil
- end
- end
-
def exportable_association?(association, current_user: nil)
return false unless respond_to?(association)
return true if has_many_association?(association)
@@ -30,8 +17,17 @@ module Exportable
exportable_restricted_associations & keys
end
- def has_many_association?(association_name)
- self.class.reflect_on_association(association_name)&.macro == :has_many
+ def to_authorized_json(keys_to_authorize, current_user, options)
+ modified_options = filtered_associations_opts(options, keys_to_authorize)
+ record_hash = as_json(modified_options).with_indifferent_access
+
+ keys_to_authorize.each do |key|
+ next unless record_hash.key?(key)
+
+ record_hash[key] = authorized_association_records(key, current_user, options)
+ end
+
+ record_hash.to_json
end
private
@@ -47,4 +43,47 @@ module Exportable
record.readable_by?(user)
end
end
+
+ def has_many_association?(association_name)
+ self.class.reflect_on_association(association_name)&.macro == :has_many
+ end
+
+ def readable_records(association, current_user: nil)
+ association_records = try(association)
+ return unless association_records.present?
+
+ if has_many_association?(association)
+ DeclarativePolicy.user_scope do
+ association_records.select { |record| readable_record?(record, current_user) }
+ end
+ else
+ readable_record?(association_records, current_user) ? association_records : nil
+ end
+ end
+
+ def authorized_association_records(key, current_user, options)
+ records = readable_records(key, current_user: current_user)
+ empty_assoc = has_many_association?(key) ? [] : nil
+ return empty_assoc unless records.present?
+
+ assoc_opts = association_options(key, options)&.dig(key)
+ records.as_json(assoc_opts)
+ end
+
+ def filtered_associations_opts(options, associations)
+ options_copy = options.deep_dup
+
+ associations.each do |key|
+ assoc_opts = association_options(key, options_copy)
+ next unless assoc_opts
+
+ assoc_opts[key] = { only: [:id] }
+ end
+
+ options_copy
+ end
+
+ def association_options(key, options)
+ options[:include].find { |assoc| assoc.key?(key) }
+ end
end
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index 94cc417af22..9a513ea0e5b 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -27,6 +27,7 @@ module Issuable
include ClosedAtFilterable
include VersionedDescription
include SortableTitle
+ include Exportable
TITLE_LENGTH_MAX = 255
TITLE_HTML_LENGTH_MAX = 800
@@ -230,6 +231,10 @@ module Issuable
issuable_severity&.severity || IssuableSeverity::DEFAULT
end
+ def exportable_restricted_associations
+ super + [:notes]
+ end
+
private
def validate_description_length?
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 66c62ddf0b5..860c9ee8cde 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -25,7 +25,6 @@ class Issue < ApplicationRecord
include FromUnion
include EachBatch
include PgFullTextSearchable
- include Exportable
extend ::Gitlab::Utils::Override
diff --git a/app/models/label.rb b/app/models/label.rb
index 32b399ac461..0831ba40536 100644
--- a/app/models/label.rb
+++ b/app/models/label.rb
@@ -14,6 +14,7 @@ class Label < ApplicationRecord
cache_markdown_field :description, pipeline: :single_line
DEFAULT_COLOR = ::Gitlab::Color.of('#6699cc')
+ DESCRIPTION_LENGTH_MAX = 512.kilobytes
attribute :color, ::Gitlab::Database::Type::Color.new, default: DEFAULT_COLOR
@@ -32,6 +33,10 @@ class Label < ApplicationRecord
validates :title, uniqueness: { scope: [:group_id, :project_id] }
validates :title, length: { maximum: 255 }
+ # we validate the description against DESCRIPTION_LENGTH_MAX only for labels being created and on updates if
+ # the description changes to avoid breaking the existing labels which may have their descriptions longer
+ validates :description, bytesize: { maximum: -> { DESCRIPTION_LENGTH_MAX } }, if: :validate_description_length?
+
default_scope { order(title: :asc) } # rubocop:disable Cop/DefaultScope
scope :templates, -> { where(template: true, type: [Label.name, nil]) }
@@ -282,6 +287,16 @@ class Label < ApplicationRecord
private
+ def validate_description_length?
+ return false unless description_changed?
+
+ previous_description = changes['description'].first
+ # previous_description will be nil for new records
+ return true if previous_description.blank?
+
+ previous_description.bytesize <= DESCRIPTION_LENGTH_MAX || description.bytesize > previous_description.bytesize
+ end
+
def issues_count(user, params = {})
params.merge!(subject_foreign_key => subject.id, label_name: title, scope: 'all')
IssuesFinder.new(user, params.with_indifferent_access).execute.count
diff --git a/app/models/project_team.rb b/app/models/project_team.rb
index 631797ee812..fbdc88e7b76 100644
--- a/app/models/project_team.rb
+++ b/app/models/project_team.rb
@@ -117,12 +117,14 @@ class ProjectTeam
owners.include?(user)
end
- def import(source_project, current_user = nil)
+ def import(source_project, current_user)
target_project = project
source_members = source_project.project_members.to_a
target_user_ids = target_project.project_members.pluck_user_ids
+ importer_access_level = max_member_access(current_user.id)
+
source_members.reject! do |member|
# Skip if user already present in team
!member.invite? && target_user_ids.include?(member.user_id)
@@ -132,6 +134,8 @@ class ProjectTeam
new_member = member.dup
new_member.id = nil
new_member.source = target_project
+ # So that a maintainer cannot import a member with owner access
+ new_member.access_level = [new_member.access_level, importer_access_level].min
new_member.created_by = current_user
new_member
end
diff --git a/app/models/user.rb b/app/models/user.rb
index 714c449ffd3..9516d162066 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -1553,7 +1553,9 @@ class User < ApplicationRecord
# rubocop: enable CodeReuse/ServiceClass
def primary_email_verified?
- confirmed? && !temp_oauth_email?
+ return false unless confirmed? && !temp_oauth_email?
+
+ !email_changed? || new_record?
end
def accept_pending_invitations!
diff --git a/config/feature_flags/development/ci_multi_doc_yaml.yml b/config/feature_flags/development/search_projects_hide_archived.yml
index ec86ecd95c2..9d2e5b1a74c 100644
--- a/config/feature_flags/development/ci_multi_doc_yaml.yml
+++ b/config/feature_flags/development/search_projects_hide_archived.yml
@@ -1,8 +1,8 @@
---
-name: ci_multi_doc_yaml
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/109137
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/388836
-milestone: '15.9'
+name: search_projects_hide_archived
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/122332
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/413821
+milestone: '16.1'
type: development
-group: group::pipeline authoring
-default_enabled: true
+group: group::global search
+default_enabled: false
diff --git a/config/routes.rb b/config/routes.rb
index cf5476b0c77..ff3ca48501a 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -204,7 +204,7 @@ InitializerConnections.raise_if_new_database_connection do
end
# Spam reports
- resources :abuse_reports, only: [:new, :create] do
+ resources :abuse_reports, only: [:create] do
collection do
post :add_category
end
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 5ee56680756..39aba86b8c0 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -16388,7 +16388,7 @@ four standard [pagination arguments](#connection-pagination-arguments):
Represents vulnerable project counts for each grade.
-Returns [`[VulnerableProjectsByGrade!]!`](#vulnerableprojectsbygrade).
+Returns [`[VulnerableProjectsByGrade!]`](#vulnerableprojectsbygrade).
###### Arguments
diff --git a/doc/api/projects.md b/doc/api/projects.md
index 4d1ca656b29..5547546e6cc 100644
--- a/doc/api/projects.md
+++ b/doc/api/projects.md
@@ -2377,6 +2377,11 @@ curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" "https://git
Import members from another project.
+If the importing member's role in the target project is:
+
+- Maintainer, then members with the Owner role in the source project are imported with the Maintainer role.
+- Owner, then members with the Owner role in the source project are imported with the Owner role.
+
```plaintext
POST /projects/:id/import_project_members/:project_id
```
diff --git a/doc/user/ai_features.md b/doc/user/ai_features.md
index f5541297d58..8d8eaf7bfdc 100644
--- a/doc/user/ai_features.md
+++ b/doc/user/ai_features.md
@@ -197,6 +197,26 @@ Feedback on this experimental feature can be provided in [issue 408995](https://
- Contents of the file
- The file name
+### Summarize issue discussions **(ULTIMATE SAAS)**
+
+> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/10344) in GitLab 16.0 as an [Experiment](../policy/alpha-beta-support.md#experiment).
+
+This feature is an [Experiment](../policy/alpha-beta-support.md) on GitLab.com that is powered by OpenAI's
+GPT-3. It requires the [group-level third-party AI features setting](group/manage.md#enable-third-party-ai-features) to be enabled.
+
+You can generate a summary of discussions on an issue:
+
+1. In an issue, scroll to the **Activity** section.
+1. Select **View summary** (**{comment}**).
+
+The comments in the issue are summarized in as many as 10 list items.
+The summary is displayed only for you.
+
+Provide feedback on this experimental feature in [issue 407779](https://gitlab.com/gitlab-org/gitlab/-/issues/407779).
+
+**Data usage**: When you use this feature, the text of public comments on the issue are sent to the large
+language model referenced above.
+
## Data Usage
GitLab AI features leverage generative AI to help increase velocity and aim to help make you more productive. Each feature operates independently of other features and is not required for other features to function.
diff --git a/doc/user/group/reporting/git_abuse_rate_limit.md b/doc/user/group/reporting/git_abuse_rate_limit.md
index d5c44f4e245..ad176fdd384 100644
--- a/doc/user/group/reporting/git_abuse_rate_limit.md
+++ b/doc/user/group/reporting/git_abuse_rate_limit.md
@@ -17,6 +17,10 @@ Git abuse rate limiting is a feature to automatically ban users who download, cl
Git abuse rate limiting does not apply to top-level group owners, [deploy tokens](../../../user/project/deploy_tokens/index.md), or [deploy keys](../../../user/project/deploy_keys/index.md).
+How GitLab determines a user's rate limit is under development.
+For more information, see the [confidential epic](../../project/issues/confidential_issues.md)
+`https://gitlab.com/groups/gitlab-org/modelops/anti-abuse/-/epics/14`.
+
## Automatic ban notifications
If the `limit_unique_project_downloads_per_namespace_user` feature flag is enabled, selected users receive an email when a user is about to be banned.
diff --git a/doc/user/project/members/index.md b/doc/user/project/members/index.md
index 6e20492db05..8b2cacf084e 100644
--- a/doc/user/project/members/index.md
+++ b/doc/user/project/members/index.md
@@ -200,6 +200,11 @@ Prerequisite:
- You must have the Maintainer or Owner role.
+If the importing member's role in the target project is:
+
+- Maintainer, then members with the Owner role in the source project are imported with the Maintainer role.
+- Owner, then members with the Owner role in the source project are imported with the Owner role.
+
To import users:
1. On the top bar, select **Main menu > Projects** and find your project.
diff --git a/doc/user/search/index.md b/doc/user/search/index.md
index 24bc31a103f..2d0d1fa72bf 100644
--- a/doc/user/search/index.md
+++ b/doc/user/search/index.md
@@ -108,7 +108,11 @@ For example, the search query:
### Search archived projects
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/121981) in GitLab 16.1.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/121981) in GitLab 16.1 [with a flag](../../administration/feature_flags.md) named `search_projects_hide_archived`. Disabled by default.
+
+FLAG:
+On self-managed GitLab, by default this feature is not available. To make it available,
+ask an administrator to [enable the feature flag](../../administration/feature_flags.md) named `search_projects_hide_archived`. On GitLab.com, this feature is not available.
By default, archived projects are excluded from the search results. To include archived projects, add the parameter `include_archived=true` to the URL.
diff --git a/lib/api/concerns/packages/npm_endpoints.rb b/lib/api/concerns/packages/npm_endpoints.rb
index afbde296161..74ad3bb296f 100644
--- a/lib/api/concerns/packages/npm_endpoints.rb
+++ b/lib/api/concerns/packages/npm_endpoints.rb
@@ -27,6 +27,11 @@ module API
end
helpers do
+ params :package_name do
+ requires :package_name, type: String, file_path: true, desc: 'Package name',
+ documentation: { example: 'mypackage' }
+ end
+
def redirect_or_present_audit_report
redirect_registry_request(
forward_to_registry: true,
@@ -172,7 +177,7 @@ module API
tags %w[npm_packages]
end
params do
- requires :package_name, type: String, desc: 'Package name'
+ use :package_name
end
route_setting :authentication, job_token_allowed: true, deploy_token_allowed: true
get '*package_name', format: false, requirements: ::API::Helpers::Packages::Npm::NPM_ENDPOINT_REQUIREMENTS do
diff --git a/lib/banzai/filter/dollar_math_post_filter.rb b/lib/banzai/filter/dollar_math_post_filter.rb
index 94d1b4bcb48..b3c230131e4 100644
--- a/lib/banzai/filter/dollar_math_post_filter.rb
+++ b/lib/banzai/filter/dollar_math_post_filter.rb
@@ -17,19 +17,21 @@ module Banzai
# encoded and will therefore not interfere with the detection of the dollar syntax.
# Corresponds to the "$...$" syntax
- DOLLAR_INLINE_PATTERN = %r{
- (?<matched>\$(?<math>(?:\S[^$\n]*?\S|[^$\s]))\$)(?:[^\d]|$)
- }x.freeze
+ DOLLAR_INLINE_UNTRUSTED =
+ '(?P<matched>\$(?P<math>(?:\S[^$\n]*?\S|[^$\s]))\$)(?:[^\d]|$)'
+ DOLLAR_INLINE_UNTRUSTED_REGEX =
+ Gitlab::UntrustedRegexp.new(DOLLAR_INLINE_UNTRUSTED, multiline: false).freeze
# Corresponds to the "$$...$$" syntax
- DOLLAR_DISPLAY_INLINE_PATTERN = %r{
- (?<matched>\$\$\ *(?<math>[^$\n]+?)\ *\$\$)
- }x.freeze
+ DOLLAR_DISPLAY_INLINE_UNTRUSTED =
+ '(?P<matched>\$\$\ *(?P<math>[^$\n]+?)\ *\$\$)'
+ DOLLAR_DISPLAY_INLINE_UNTRUSTED_REGEX =
+ Gitlab::UntrustedRegexp.new(DOLLAR_DISPLAY_INLINE_UNTRUSTED, multiline: false).freeze
# Order dependent. Handle the `$$` syntax before the `$` syntax
DOLLAR_MATH_PIPELINE = [
- { pattern: DOLLAR_DISPLAY_INLINE_PATTERN, style: :display },
- { pattern: DOLLAR_INLINE_PATTERN, style: :inline }
+ { pattern: DOLLAR_DISPLAY_INLINE_UNTRUSTED_REGEX, style: :display },
+ { pattern: DOLLAR_INLINE_UNTRUSTED_REGEX, style: :inline }
].freeze
# Do not recognize math inside these tags
@@ -46,16 +48,18 @@ module Banzai
next if has_ancestor?(node, IGNORED_ANCESTOR_TAGS)
node_html = node.to_html
- next unless node_html.match?(DOLLAR_INLINE_PATTERN) ||
- node_html.match?(DOLLAR_DISPLAY_INLINE_PATTERN)
+ next unless DOLLAR_INLINE_UNTRUSTED_REGEX.match?(node_html) ||
+ DOLLAR_DISPLAY_INLINE_UNTRUSTED_REGEX.match?(node_html)
temp_doc = Nokogiri::HTML.fragment(node_html)
DOLLAR_MATH_PIPELINE.each do |pipeline|
temp_doc.xpath('child::text()').each do |temp_node|
html = temp_node.to_html
- temp_node.content.scan(pipeline[:pattern]).each do |matched, math|
- html.sub!(matched, math_html(math: math, style: pipeline[:style]))
+
+ pipeline[:pattern].scan(temp_node.content).each do |match|
+ math = pipeline[:pattern].extract_named_group(:math, match)
+ html.sub!(match.first, math_html(math: math, style: pipeline[:style]))
end
temp_node.replace(html)
diff --git a/lib/banzai/filter/front_matter_filter.rb b/lib/banzai/filter/front_matter_filter.rb
index c788137e122..53683ce07d9 100644
--- a/lib/banzai/filter/front_matter_filter.rb
+++ b/lib/banzai/filter/front_matter_filter.rb
@@ -6,13 +6,13 @@ module Banzai
def call
lang_mapping = Gitlab::FrontMatter::DELIM_LANG
- html.sub(Gitlab::FrontMatter::PATTERN) do |_match|
- lang = $~[:lang].presence || lang_mapping[$~[:delim]]
+ Gitlab::FrontMatter::PATTERN_UNTRUSTED_REGEX.replace_gsub(html) do |match|
+ lang = match[:lang].presence || lang_mapping[match[:delim]]
- before = $~[:before]
- before = "\n#{before}" if $~[:encoding].presence
+ before = match[:before]
+ before = "\n#{before}" if match[:encoding].presence
- "#{before}```#{lang}:frontmatter\n#{$~[:front_matter]}```\n"
+ "#{before}```#{lang}:frontmatter\n#{match[:front_matter]}```\n"
end
end
end
diff --git a/lib/banzai/filter/inline_diff_filter.rb b/lib/banzai/filter/inline_diff_filter.rb
index e47ff15e7b7..fc77984f135 100644
--- a/lib/banzai/filter/inline_diff_filter.rb
+++ b/lib/banzai/filter/inline_diff_filter.rb
@@ -6,6 +6,14 @@ module Banzai
class InlineDiffFilter < HTML::Pipeline::Filter
IGNORED_ANCESTOR_TAGS = %w(pre code tt).to_set
+ INLINE_DIFF_DELETION_UNTRUSTED = '(?:\[\-(.*?)\-\]|\{\-(.*?)\-\})'
+ INLINE_DIFF_DELETION_UNTRUSTED_REGEX =
+ Gitlab::UntrustedRegexp.new(INLINE_DIFF_DELETION_UNTRUSTED, multiline: false).freeze
+
+ INLINE_DIFF_ADDITION_UNTRUSTED = '(?:\[\+(.*?)\+\]|\{\+(.*?)\+\})'
+ INLINE_DIFF_ADDITION_UNTRUSTED_REGEX =
+ Gitlab::UntrustedRegexp.new(INLINE_DIFF_ADDITION_UNTRUSTED, multiline: false).freeze
+
def call
doc.xpath('descendant-or-self::text()').each do |node|
next if has_ancestor?(node, IGNORED_ANCESTOR_TAGS)
@@ -21,8 +29,13 @@ module Banzai
end
def inline_diff_filter(text)
- html_content = text.gsub(/(?:\[\-(.*?)\-\]|\{\-(.*?)\-\})/, '<span class="idiff left right deletion">\1\2</span>')
- html_content.gsub(/(?:\[\+(.*?)\+\]|\{\+(.*?)\+\})/, '<span class="idiff left right addition">\1\2</span>')
+ html_content = INLINE_DIFF_DELETION_UNTRUSTED_REGEX.replace_gsub(text) do |match|
+ %(<span class="idiff left right deletion">#{match[1]}#{match[2]}</span>)
+ end
+
+ INLINE_DIFF_ADDITION_UNTRUSTED_REGEX.replace_gsub(html_content) do |match|
+ %(<span class="idiff left right addition">#{match[1]}#{match[2]}</span>)
+ end
end
end
end
diff --git a/lib/extracts_ref.rb b/lib/extracts_ref.rb
index 49c9772f760..5f73b474956 100644
--- a/lib/extracts_ref.rb
+++ b/lib/extracts_ref.rb
@@ -157,11 +157,11 @@ module ExtractsRef
end
def ambiguous_ref?(project, ref)
+ return false unless ref
return true if project.repository.ambiguous_ref?(ref)
+ return false unless ref.starts_with?(%r{(refs|heads|tags)/})
- return false unless ref&.starts_with?('refs/')
-
- unprefixed_ref = ref.sub(%r{^refs/(heads|tags)/}, '')
+ unprefixed_ref = ref.sub(%r{^(refs/)?(heads|tags)/}, '')
project.repository.commit(unprefixed_ref).present?
end
end
diff --git a/lib/gitlab/background_migration/mailers/views/unconfirm_mailer/unconfirm_notification_email.html.haml b/lib/gitlab/background_migration/mailers/views/unconfirm_mailer/unconfirm_notification_email.html.haml
index d8f7466a1ca..53d64fdf488 100644
--- a/lib/gitlab/background_migration/mailers/views/unconfirm_mailer/unconfirm_notification_email.html.haml
+++ b/lib/gitlab/background_migration/mailers/views/unconfirm_mailer/unconfirm_notification_email.html.haml
@@ -3,16 +3,16 @@
Dear GitLab user,
%p
- As part of our commitment to keeping GitLab secure, we have identified and addressed a vulnerability in GitLab that allowed some users to bypass the email verification process in a #{link_to("recent security release", "https://about.gitlab.com/releases/2020/05/27/security-release-13-0-1-released", target: '_blank')}.
+ As part of our commitment to keeping GitLab secure, we have identified and addressed a vulnerability in GitLab that allowed some users to bypass the email verification process in a #{link_to('recent security release', 'https://about.gitlab.com/releases/2020/05/27/security-release-13-0-1-released', target: '_blank')}.
%p
As a precautionary measure, you will need to re-verify some of your account's email addresses before continuing to use GitLab. Sorry for the inconvenience!
%p
- We have already sent the re-verification email with a subject line of "Confirmation instructions" from #{@verification_from_mail}. Please feel free to contribute any questions or comments to #{link_to("this issue", "https://gitlab.com/gitlab-com/www-gitlab-com/-/issues/7942", target: '_blank')}.
+ We have already sent the re-verification email with a subject line of 'Confirmation instructions' from #{@verification_from_mail}. Please feel free to contribute any questions or comments to #{link_to('this issue', 'https://gitlab.com/gitlab-com/www-gitlab-com/-/issues/7942', target: '_blank')}.
%p
- If you are not "#{@user.username}", please #{link_to 'report this to our administrator', new_abuse_report_url(user_id: @user.id)}
+ If you are not "#{@user.username}", please report abuse from the user's #{link_to('profile page', user_url(@user.id), target: '_blank', rel: 'noopener noreferrer')}. #{link_to('Learn more.', help_page_url('user/report_abuse', anchor: 'report-abuse-from-the-users-profile-page', target: '_blank', rel: 'noopener noreferrer'))}
%p
Thank you for being a GitLab user!
diff --git a/lib/gitlab/background_migration/mailers/views/unconfirm_mailer/unconfirm_notification_email.text.erb b/lib/gitlab/background_migration/mailers/views/unconfirm_mailer/unconfirm_notification_email.text.erb
index d20af9b9803..81ef68e99f0 100644
--- a/lib/gitlab/background_migration/mailers/views/unconfirm_mailer/unconfirm_notification_email.text.erb
+++ b/lib/gitlab/background_migration/mailers/views/unconfirm_mailer/unconfirm_notification_email.text.erb
@@ -9,6 +9,8 @@ As a precautionary measure, you will need to re-verify some of your account's em
We have already sent the re-verification email with a subject line of "Confirmation instructions" from <%= @verification_from_mail %>.
Please feel free to contribute any questions or comments to this issue: https://gitlab.com/gitlab-com/www-gitlab-com/-/issues/7942
-If you are not "<%= @user.username %>", please report this to our administrator. Report link: <%= new_abuse_report_url(user_id: @user.id) %>
+If you are not "<%= @user.username %>", please report abuse from the user's profile page: <%= user_url(@user.id) %>.
+
+Learn more: <%= help_page_url('user/report_abuse', anchor: 'report-abuse-from-the-users-profile-page') %>
Thank you for being a GitLab user!
diff --git a/lib/gitlab/checks/tag_check.rb b/lib/gitlab/checks/tag_check.rb
index 007a775eaf5..5c43ca946b5 100644
--- a/lib/gitlab/checks/tag_check.rb
+++ b/lib/gitlab/checks/tag_check.rb
@@ -10,7 +10,8 @@ module Gitlab
'Only a project maintainer or owner can delete a protected tag.',
delete_protected_tag_non_web: 'You can only delete protected tags using the web interface.',
create_protected_tag: 'You are not allowed to create this tag as it is protected.',
- default_branch_collision: 'You cannot use default branch name to create a tag'
+ default_branch_collision: 'You cannot use default branch name to create a tag',
+ prohibited_tag_name: 'You cannot create a tag with a prohibited pattern.'
}.freeze
LOG_MESSAGES = {
@@ -29,11 +30,20 @@ module Gitlab
end
default_branch_collision_check
+ prohibited_tag_checks
protected_tag_checks
end
private
+ def prohibited_tag_checks
+ return if deletion?
+
+ if tag_name.start_with?("refs/tags/") # rubocop: disable Style/GuardClause
+ raise GitAccess::ForbiddenError, ERROR_MESSAGES[:prohibited_tag_name]
+ end
+ end
+
def protected_tag_checks
logger.log_timed(LOG_MESSAGES[__method__]) do
return unless ProtectedTag.protected?(project, tag_name) # rubocop:disable Cop/AvoidReturnFromBlocks
diff --git a/lib/gitlab/ci/artifacts/decompressed_artifact_size_validator.rb b/lib/gitlab/ci/artifacts/decompressed_artifact_size_validator.rb
new file mode 100644
index 00000000000..04930da5e30
--- /dev/null
+++ b/lib/gitlab/ci/artifacts/decompressed_artifact_size_validator.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ module Artifacts
+ class DecompressedArtifactSizeValidator
+ DEFAULT_MAX_BYTES = 4.gigabytes.freeze
+
+ FILE_FORMAT_VALIDATORS = {
+ gzip: ::Gitlab::Ci::DecompressedGzipSizeValidator
+ }.freeze
+
+ FileDecompressionError = Class.new(StandardError)
+
+ def initialize(file:, file_format:, max_bytes: DEFAULT_MAX_BYTES)
+ @file = file
+ @file_path = file&.path
+ @file_format = file_format
+ @max_bytes = max_bytes
+ end
+
+ def validate!
+ validator_class = FILE_FORMAT_VALIDATORS[file_format.to_sym]
+
+ return if file_path.nil?
+ return if validator_class.nil?
+
+ if file.respond_to?(:object_store) && file.object_store == ObjectStorage::Store::REMOTE
+ return if valid_on_storage?(validator_class)
+ elsif validator_class.new(archive_path: file_path, max_bytes: max_bytes).valid?
+ return
+ end
+
+ raise(FileDecompressionError, 'File decompression error')
+ end
+
+ private
+
+ attr_reader :file_path, :file, :file_format, :max_bytes
+
+ def valid_on_storage?(validator_class)
+ temp_filename = "#{SecureRandom.uuid}.gz"
+
+ is_valid = false
+ Tempfile.open(temp_filename, '/tmp') do |tempfile|
+ tempfile.binmode
+ ::Faraday.get(file.url) do |req|
+ req.options.on_data = proc { |chunk, _| tempfile.write(chunk) }
+ end
+
+ is_valid = validator_class.new(archive_path: tempfile.path, max_bytes: max_bytes).valid?
+ tempfile.unlink
+ end
+
+ is_valid
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/yaml/loader.rb b/lib/gitlab/ci/config/yaml/loader.rb
index 7277981f1e3..924a1f2e46b 100644
--- a/lib/gitlab/ci/config/yaml/loader.rb
+++ b/lib/gitlab/ci/config/yaml/loader.rb
@@ -34,18 +34,12 @@ module Gitlab
def load!
ensure_custom_tags
- if project.present? && ::Feature.enabled?(:ci_multi_doc_yaml, project)
- ::Gitlab::Config::Loader::MultiDocYaml.new(
- content,
- max_documents: MAX_DOCUMENTS,
- additional_permitted_classes: AVAILABLE_TAGS,
- reject_empty: true
- ).load!
- else
- ::Gitlab::Config::Loader::Yaml
- .new(content, additional_permitted_classes: AVAILABLE_TAGS)
- .load!
- end
+ ::Gitlab::Config::Loader::MultiDocYaml.new(
+ content,
+ max_documents: MAX_DOCUMENTS,
+ additional_permitted_classes: AVAILABLE_TAGS,
+ reject_empty: true
+ ).load!
end
end
end
diff --git a/lib/gitlab/ci/config/yaml/result.rb b/lib/gitlab/ci/config/yaml/result.rb
index 33f9a454106..6b53adc3a57 100644
--- a/lib/gitlab/ci/config/yaml/result.rb
+++ b/lib/gitlab/ci/config/yaml/result.rb
@@ -31,7 +31,7 @@ module Gitlab
def content
return @config.last if has_header?
- @config.first
+ @config.first || {}
end
end
end
diff --git a/lib/gitlab/ci/decompressed_gzip_size_validator.rb b/lib/gitlab/ci/decompressed_gzip_size_validator.rb
new file mode 100644
index 00000000000..9b7b5f0dd66
--- /dev/null
+++ b/lib/gitlab/ci/decompressed_gzip_size_validator.rb
@@ -0,0 +1,79 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ class DecompressedGzipSizeValidator
+ DEFAULT_MAX_BYTES = 4.gigabytes.freeze
+ TIMEOUT_LIMIT = 210.seconds
+
+ ServiceError = Class.new(StandardError)
+
+ def initialize(archive_path:, max_bytes: DEFAULT_MAX_BYTES)
+ @archive_path = archive_path
+ @max_bytes = max_bytes
+ end
+
+ def valid?
+ validate
+ end
+
+ private
+
+ def validate
+ pgrps = nil
+ valid_archive = true
+
+ validate_archive_path
+
+ Timeout.timeout(TIMEOUT_LIMIT) do
+ stderr_r, stderr_w = IO.pipe
+ stdout, wait_threads = Open3.pipeline_r(*command, pgroup: true, err: stderr_w)
+
+ # When validation is performed on a small archive (e.g. 100 bytes)
+ # `wait_thr` finishes before we can get process group id. Do not
+ # raise exception in this scenario.
+ pgrps = wait_threads.map do |wait_thr|
+ Process.getpgid(wait_thr[:pid])
+ rescue Errno::ESRCH
+ nil
+ end
+ pgrps.compact!
+
+ status = wait_threads.last.value
+
+ if status.success?
+ result = stdout.readline
+
+ valid_archive = false if result.to_i > max_bytes
+ else
+ valid_archive = false
+ end
+
+ ensure
+ stdout.close
+ stderr_w.close
+ stderr_r.close
+ end
+
+ valid_archive
+ rescue StandardError
+ pgrps.each { |pgrp| Process.kill(-1, pgrp) } if pgrps
+
+ false
+ end
+
+ def validate_archive_path
+ Gitlab::PathTraversal.check_path_traversal!(archive_path)
+
+ raise(ServiceError, 'Archive path is a symlink') if File.lstat(archive_path).symlink?
+ raise(ServiceError, 'Archive path is not a file') unless File.file?(archive_path)
+ end
+
+ def command
+ [['gzip', '-dc', archive_path], ['wc', '-c']]
+ end
+
+ attr_reader :archive_path, :max_bytes
+ end
+ end
+end
diff --git a/lib/gitlab/ci/templates/Security/DAST.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/DAST.gitlab-ci.yml
index 792bd7f666b..f10011ab23b 100644
--- a/lib/gitlab/ci/templates/Security/DAST.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Security/DAST.gitlab-ci.yml
@@ -26,11 +26,12 @@ variables:
# Setting this variable will affect all Security templates
# (SAST, Dependency Scanning, ...)
SECURE_ANALYZERS_PREFIX: "$CI_TEMPLATE_REGISTRY_HOST/security-products"
+ DAST_IMAGE_SUFFIX: ""
dast:
stage: dast
image:
- name: "$SECURE_ANALYZERS_PREFIX/dast:$DAST_VERSION"
+ name: "$SECURE_ANALYZERS_PREFIX/dast:$DAST_VERSION$DAST_IMAGE_SUFFIX"
variables:
GIT_STRATEGY: none
allow_failure: true
@@ -57,6 +58,11 @@ dast:
$REVIEW_DISABLED == '1'
when: never
- if: $CI_COMMIT_BRANCH &&
+ $CI_GITLAB_FIPS_MODE == "true" &&
+ $GITLAB_FEATURES =~ /\bdast\b/
+ variables:
+ DAST_IMAGE_SUFFIX: "-fips"
+ - if: $CI_COMMIT_BRANCH &&
$GITLAB_FEATURES =~ /\bdast\b/
after_script:
# Remove any debug.log files because they might contain secrets.
diff --git a/lib/gitlab/ci/templates/Security/DAST.latest.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/DAST.latest.gitlab-ci.yml
index d1d1c4d7e52..989f9caf601 100644
--- a/lib/gitlab/ci/templates/Security/DAST.latest.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Security/DAST.latest.gitlab-ci.yml
@@ -26,11 +26,12 @@ variables:
# Setting this variable will affect all Security templates
# (SAST, Dependency Scanning, ...)
SECURE_ANALYZERS_PREFIX: "$CI_TEMPLATE_REGISTRY_HOST/security-products"
+ DAST_IMAGE_SUFFIX: ""
dast:
stage: dast
image:
- name: "$SECURE_ANALYZERS_PREFIX/dast:$DAST_VERSION"
+ name: "$SECURE_ANALYZERS_PREFIX/dast:$DAST_VERSION$DAST_IMAGE_SUFFIX"
variables:
GIT_STRATEGY: none
allow_failure: true
@@ -59,6 +60,12 @@ dast:
$REVIEW_DISABLED == '1'
when: never
+ # Add the job to merge request pipelines if there's an open merge request. (FIPS)
+ - if: $CI_PIPELINE_SOURCE == "merge_request_event" &&
+ $CI_GITLAB_FIPS_MODE == "true" &&
+ $GITLAB_FEATURES =~ /\bdast\b/
+ variables:
+ DAST_IMAGE_SUFFIX: "-fips"
# Add the job to merge request pipelines if there's an open merge request.
- if: $CI_PIPELINE_SOURCE == "merge_request_event" &&
$GITLAB_FEATURES =~ /\bdast\b/
@@ -67,6 +74,12 @@ dast:
- if: $CI_OPEN_MERGE_REQUESTS
when: never
+ # Add the job to branch pipelines. (FIPS)
+ - if: $CI_COMMIT_BRANCH &&
+ $CI_GITLAB_FIPS_MODE == "true" &&
+ $GITLAB_FEATURES =~ /\bdast\b/
+ variables:
+ DAST_IMAGE_SUFFIX: "-fips"
# Add the job to branch pipelines.
- if: $CI_COMMIT_BRANCH &&
$GITLAB_FEATURES =~ /\bdast\b/
diff --git a/lib/gitlab/front_matter.rb b/lib/gitlab/front_matter.rb
index 093501e860b..d215a77f4d7 100644
--- a/lib/gitlab/front_matter.rb
+++ b/lib/gitlab/front_matter.rb
@@ -8,15 +8,35 @@ module Gitlab
';;;' => 'json'
}.freeze
- DELIM = Regexp.union(DELIM_LANG.keys)
+ DELIM_UNTRUSTED = "(?:#{Gitlab::FrontMatter::DELIM_LANG.keys.map { |x| RE2::Regexp.escape(x) }.join('|')})".freeze
- PATTERN = %r{
- \A(?<encoding>[^\r\n]*coding:[^\r\n]*\R)? # optional encoding line
- (?<before>\s*)
- ^(?<delim>#{DELIM})[ \t]*(?<lang>\S*)\R # opening front matter marker (optional language specifier)
- (?<front_matter>.*?) # front matter block content (not greedy)
- ^(\k<delim> | \.{3}) # closing front matter marker
- [^\S\r\n]*(\R|\z)
- }mx.freeze
+ # Original pattern:
+ # \A(?<encoding>[^\r\n]*coding:[^\r\n]*\R)? # optional encoding line
+ # (?<before>\s*)
+ # ^(?<delim>#{DELIM})[ \t]*(?<lang>\S*)\R # opening front matter marker (optional language specifier)
+ # (?<front_matter>.*?) # front matter block content (not greedy)
+ # ^(\k<delim> | \.{3}) # closing front matter marker
+ # [^\S\r\n]*(\R|\z)
+ # rubocop:disable Style/StringConcatenation
+ # rubocop:disable Style/LineEndConcatenation
+ PATTERN_UNTRUSTED =
+ # optional encoding line
+ "\\A(?P<encoding>[^\\r\\n]*coding:[^\\r\\n]*#{::Gitlab::UntrustedRegexp::BACKSLASH_R})?" +
+ '(?P<before>\s*)' +
+
+ # opening front matter marker (optional language specifier)
+ "^(?P<delim>#{DELIM_UNTRUSTED})[ \\t]*(?P<lang>\\S*)#{::Gitlab::UntrustedRegexp::BACKSLASH_R}" +
+
+ # front matter block content (not greedy)
+ '(?P<front_matter>(?:\n|.)*?)' +
+
+ # closing front matter marker
+ "^((?P<delim_closing>#{DELIM_UNTRUSTED})|\\.{3})" +
+ "[^\\S\\r\\n]*(#{::Gitlab::UntrustedRegexp::BACKSLASH_R}|\\z)"
+ # rubocop:enable Style/LineEndConcatenation
+ # rubocop:enable Style/StringConcatenation
+
+ PATTERN_UNTRUSTED_REGEX =
+ Gitlab::UntrustedRegexp.new(PATTERN_UNTRUSTED, multiline: true).freeze
end
end
diff --git a/lib/gitlab/import_export/json/streaming_serializer.rb b/lib/gitlab/import_export/json/streaming_serializer.rb
index 9bb0770dc90..2c64ca53f76 100644
--- a/lib/gitlab/import_export/json/streaming_serializer.rb
+++ b/lib/gitlab/import_export/json/streaming_serializer.rb
@@ -133,29 +133,10 @@ module Gitlab
def authorized_record_json(record, options)
include_keys = options[:include].flat_map(&:keys)
keys_to_authorize = record.try(:restricted_associations, include_keys)
- return record.to_json(options) if keys_to_authorize.blank?
-
- record_hash = record.as_json(options).with_indifferent_access
- filtered_record_hash(record, keys_to_authorize, record_hash).to_json(options)
- end
-
- def filtered_record_hash(record, keys_to_authorize, record_hash)
- keys_to_authorize.each do |key|
- next unless record_hash[key].present?
-
- readable = record.try(:readable_records, key, current_user: current_user)
- if record.has_many_association?(key)
- readable_ids = readable.pluck(:id)
- record_hash[key].keep_if do |association_record|
- readable_ids.include?(association_record[:id])
- end
- else
- record_hash[key] = nil unless readable.present?
- end
- end
+ return record.to_json(options) if keys_to_authorize.blank?
- record_hash
+ record.to_authorized_json(keys_to_authorize, current_user, options)
end
def batch(relation, key)
diff --git a/lib/gitlab/search_results.rb b/lib/gitlab/search_results.rb
index 5d2dc2f7d19..16a03f2356c 100644
--- a/lib/gitlab/search_results.rb
+++ b/lib/gitlab/search_results.rb
@@ -175,7 +175,7 @@ module Gitlab
def projects
scope = limit_projects
- scope = filters[:include_archived] ? scope : scope.non_archived
+ scope = scope.non_archived if Feature.enabled?(:search_projects_hide_archived) && !filters[:include_archived]
scope.search(query)
end
diff --git a/lib/gitlab/untrusted_regexp.rb b/lib/gitlab/untrusted_regexp.rb
index fe3377dae68..150f50e4da6 100644
--- a/lib/gitlab/untrusted_regexp.rb
+++ b/lib/gitlab/untrusted_regexp.rb
@@ -16,6 +16,10 @@ module Gitlab
class UntrustedRegexp
require_dependency 're2'
+ # recreate Ruby's \R metacharacter
+ # https://ruby-doc.org/3.2.2/Regexp.html#class-Regexp-label-Character+Classes
+ BACKSLASH_R = '(\n|\v|\f|\r|\x{0085}|\x{2028}|\x{2029}|\r\n)'
+
delegate :===, :source, to: :regexp
def initialize(pattern, multiline: false)
diff --git a/lib/gitlab/wiki_pages/front_matter_parser.rb b/lib/gitlab/wiki_pages/front_matter_parser.rb
index 071b0dde619..dbe848f0acf 100644
--- a/lib/gitlab/wiki_pages/front_matter_parser.rb
+++ b/lib/gitlab/wiki_pages/front_matter_parser.rb
@@ -53,7 +53,7 @@ module Gitlab
include Gitlab::Utils::StrongMemoize
def initialize(delim = nil, lang = '', text = nil)
- @lang = lang.downcase.presence || Gitlab::FrontMatter::DELIM_LANG[delim]
+ @lang = lang&.downcase.presence || Gitlab::FrontMatter::DELIM_LANG[delim]
@text = text&.strip!
end
@@ -109,11 +109,17 @@ module Gitlab
end
def parse_front_matter_block
- wiki_content.match(Gitlab::FrontMatter::PATTERN) { |m| Block.new(m[:delim], m[:lang], m[:front_matter]) } || Block.new
+ if match = Gitlab::FrontMatter::PATTERN_UNTRUSTED_REGEX.match(wiki_content)
+ Block.new(match[:delim], match[:lang], match[:front_matter])
+ else
+ Block.new
+ end
end
def strip_front_matter_block
- wiki_content.gsub(Gitlab::FrontMatter::PATTERN, '')
+ Gitlab::FrontMatter::PATTERN_UNTRUSTED_REGEX.replace_gsub(wiki_content) do
+ ''
+ end
end
end
end
diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb
index c6bedcc95a7..6adddccfda7 100644
--- a/spec/controllers/projects_controller_spec.rb
+++ b/spec/controllers/projects_controller_spec.rb
@@ -193,38 +193,79 @@ RSpec.describe ProjectsController, feature_category: :groups_and_projects do
end
end
- context 'when the default branch name can resolve to another ref' do
- let!(:project_with_default_branch) do
- create(:project, :public, :custom_repo, files: ['somefile']).tap do |p|
- p.repository.create_branch("refs/heads/refs/heads/#{other_ref}", 'master')
- p.change_head("refs/heads/#{other_ref}")
- end.reload
+ context 'when the default branch name is ambiguous' do
+ let_it_be(:project_with_default_branch) do
+ create(:project, :public, :custom_repo, files: ['somefile'])
end
- let(:other_ref) { 'branch-name' }
+ shared_examples 'ambiguous ref redirects' do
+ let(:project) { project_with_default_branch }
+ let(:branch_ref) { "refs/heads/#{ref}" }
+ let(:repo) { project.repository }
- context 'but there is no other ref' do
- it 'responds with ok' do
- get :show, params: { namespace_id: project_with_default_branch.namespace, id: project_with_default_branch }
- expect(response).to be_ok
+ before do
+ repo.create_branch(branch_ref, 'master')
+ repo.change_head(ref)
end
- end
- context 'and that other ref exists' do
- let(:tree_with_default_branch) do
- branch = project_with_default_branch.repository.find_branch(project_with_default_branch.default_branch)
- project_tree_path(project_with_default_branch, branch.target)
+ after do
+ repo.change_head('master')
+ repo.delete_branch(branch_ref)
end
- before do
- project_with_default_branch.repository.create_branch(other_ref, 'master')
+ subject do
+ get(
+ :show,
+ params: {
+ namespace_id: project.namespace,
+ id: project
+ }
+ )
+ end
+
+ context 'when there is no conflicting ref' do
+ let(:other_ref) { 'non-existent-ref' }
+
+ it { is_expected.to have_gitlab_http_status(:ok) }
end
- it 'redirects to tree view for the default branch' do
- get :show, params: { namespace_id: project_with_default_branch.namespace, id: project_with_default_branch }
- expect(response).to redirect_to(tree_with_default_branch)
+ context 'and that other ref exists' do
+ let(:other_ref) { 'master' }
+
+ let(:project_default_root_tree_path) do
+ sha = repo.find_branch(project.default_branch).target
+ project_tree_path(project, sha)
+ end
+
+ it 'redirects to tree view for the default branch' do
+ is_expected.to redirect_to(project_default_root_tree_path)
+ end
end
end
+
+ context 'when ref starts with ref/heads/' do
+ let(:ref) { "refs/heads/#{other_ref}" }
+
+ include_examples 'ambiguous ref redirects'
+ end
+
+ context 'when ref starts with ref/tags/' do
+ let(:ref) { "refs/tags/#{other_ref}" }
+
+ include_examples 'ambiguous ref redirects'
+ end
+
+ context 'when ref starts with heads/' do
+ let(:ref) { "heads/#{other_ref}" }
+
+ include_examples 'ambiguous ref redirects'
+ end
+
+ context 'when ref starts with tags/' do
+ let(:ref) { "tags/#{other_ref}" }
+
+ include_examples 'ambiguous ref redirects'
+ end
end
end
diff --git a/spec/factories/users.rb b/spec/factories/users.rb
index 448d937f30e..9040cd754b3 100644
--- a/spec/factories/users.rb
+++ b/spec/factories/users.rb
@@ -20,6 +20,10 @@ FactoryBot.define do
public_email { email }
end
+ trait :notification_email do
+ notification_email { email }
+ end
+
trait :private_profile do
private_profile { true }
end
diff --git a/spec/frontend/single_file_diff_spec.js b/spec/frontend/single_file_diff_spec.js
index ff2a4e31e0b..572fd3810ab 100644
--- a/spec/frontend/single_file_diff_spec.js
+++ b/spec/frontend/single_file_diff_spec.js
@@ -67,6 +67,21 @@ describe('SingleFileDiff', () => {
expect(mock.history.get.length).toBe(1);
});
+ it('ignores user-defined diff path attributes', () => {
+ setHTMLFixture(`
+ <div class="diff-file">
+ <div class="diff-content">
+ <div class="diff-viewer" data-type="simple">
+ <div class="note-text"><a data-diff-for-path="test/note/path">Test note</a></div>
+ <div data-diff-for-path="${blobDiffPath}">MOCK CONTENT</div>
+ </div>
+ </div>
+ </div>
+`);
+ const { diffForPath } = new SingleFileDiff(document.querySelector('.diff-file'));
+ expect(diffForPath).toEqual(blobDiffPath);
+ });
+
it('does not load diffs via axios for already expanded diffs', async () => {
setHTMLFixture(`
<div class="diff-file">
diff --git a/spec/helpers/merge_requests_helper_spec.rb b/spec/helpers/merge_requests_helper_spec.rb
index 93df9d5f94b..b6c8653a563 100644
--- a/spec/helpers/merge_requests_helper_spec.rb
+++ b/spec/helpers/merge_requests_helper_spec.rb
@@ -183,10 +183,18 @@ RSpec.describe MergeRequestsHelper, feature_category: :code_review_workflow do
end
describe '#merge_request_source_branch' do
- let_it_be(:project) { create(:project) }
- let(:forked_project) { fork_project(project) }
- let(:merge_request_forked) { create(:merge_request, source_project: forked_project, target_project: project) }
+ let(:malicious_branch_name) { 'name<script>test</script>' }
+ let(:project) { create(:project) }
let(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
+ let(:forked_project) { fork_project(project) }
+ let(:merge_request_forked) do
+ create(
+ :merge_request,
+ source_project: forked_project,
+ source_branch: malicious_branch_name,
+ target_project: project
+ )
+ end
context 'when merge request is a fork' do
subject { merge_request_source_branch(merge_request_forked) }
@@ -194,6 +202,10 @@ RSpec.describe MergeRequestsHelper, feature_category: :code_review_workflow do
it 'does show the fork icon' do
expect(subject).to match(/fork/)
end
+
+ it 'escapes properly' do
+ expect(subject).to include(html_escape(malicious_branch_name))
+ end
end
context 'when merge request is not a fork' do
diff --git a/spec/lib/banzai/filter/front_matter_filter_spec.rb b/spec/lib/banzai/filter/front_matter_filter_spec.rb
index b15f3ad2cd6..9d25dafb727 100644
--- a/spec/lib/banzai/filter/front_matter_filter_spec.rb
+++ b/spec/lib/banzai/filter/front_matter_filter_spec.rb
@@ -189,19 +189,29 @@ RSpec.describe Banzai::Filter::FrontMatterFilter, feature_category: :team_planni
end
end
- it 'fails fast for strings with many spaces' do
- content = "coding:" + " " * 50_000 + ";"
+ describe 'protects against malicious backtracking' do
+ it 'fails fast for strings with many spaces' do
+ content = "coding:" + " " * 50_000 + ";"
- expect do
- Timeout.timeout(3.seconds) { filter(content) }
- end.not_to raise_error
- end
+ expect do
+ Timeout.timeout(3.seconds) { filter(content) }
+ end.not_to raise_error
+ end
+
+ it 'fails fast for strings with many newlines' do
+ content = "coding:\n" + ";;;" + "\n" * 10_000 + "x"
- it 'fails fast for strings with many newlines' do
- content = "coding:\n" + ";;;" + "\n" * 10_000 + "x"
+ expect do
+ Timeout.timeout(3.seconds) { filter(content) }
+ end.not_to raise_error
+ end
+
+ it 'fails fast for strings with many `coding:`' do
+ content = "coding:" * 120_000 + "\n" * 80_000 + ";"
- expect do
- Timeout.timeout(3.seconds) { filter(content) }
- end.not_to raise_error
+ expect do
+ Timeout.timeout(3.seconds) { filter(content) }
+ end.not_to raise_error
+ end
end
end
diff --git a/spec/lib/banzai/filter/inline_diff_filter_spec.rb b/spec/lib/banzai/filter/inline_diff_filter_spec.rb
index 1ef00139db2..1388a9053d9 100644
--- a/spec/lib/banzai/filter/inline_diff_filter_spec.rb
+++ b/spec/lib/banzai/filter/inline_diff_filter_spec.rb
@@ -67,4 +67,12 @@ RSpec.describe Banzai::Filter::InlineDiffFilter do
doc = "<tt>START {+something added+} END</tt>"
expect(filter(doc).to_html).to eq(doc)
end
+
+ it 'protects against malicious backtracking' do
+ doc = '[-{-' * 250_000
+
+ expect do
+ Timeout.timeout(3.seconds) { filter(doc) }
+ end.not_to raise_error
+ end
end
diff --git a/spec/lib/banzai/filter/math_filter_spec.rb b/spec/lib/banzai/filter/math_filter_spec.rb
index ded94dd6ce5..e4ebebc0fde 100644
--- a/spec/lib/banzai/filter/math_filter_spec.rb
+++ b/spec/lib/banzai/filter/math_filter_spec.rb
@@ -215,6 +215,14 @@ RSpec.describe Banzai::Filter::MathFilter, feature_category: :team_planning do
expect(doc.search('.js-render-math').count).to eq(2)
end
+ it 'protects against malicious backtracking' do
+ doc = pipeline_filter("$$#{' ' * 1_000_000}$")
+
+ expect do
+ Timeout.timeout(3.seconds) { filter(doc) }
+ end.not_to raise_error
+ end
+
def pipeline_filter(text)
context = { project: nil, no_sourcepos: true }
diff --git a/spec/lib/gitlab/background_migration/mailers/unconfirm_mailer_spec.rb b/spec/lib/gitlab/background_migration/mailers/unconfirm_mailer_spec.rb
index f430009989b..05bd020d4e2 100644
--- a/spec/lib/gitlab/background_migration/mailers/unconfirm_mailer_spec.rb
+++ b/spec/lib/gitlab/background_migration/mailers/unconfirm_mailer_spec.rb
@@ -7,6 +7,6 @@ RSpec.describe Gitlab::BackgroundMigration::Mailers::UnconfirmMailer do
let(:subject) { described_class.unconfirm_notification_email(user) }
it 'contains abuse report url' do
- expect(subject.body.encoded).to include(Rails.application.routes.url_helpers.new_abuse_report_url(user_id: user.id))
+ expect(subject.body.encoded).to include(Gitlab::Routing.url_helpers.user_url(user.id))
end
end
diff --git a/spec/lib/gitlab/checks/tag_check_spec.rb b/spec/lib/gitlab/checks/tag_check_spec.rb
index 50ffa5fad10..e75b0459337 100644
--- a/spec/lib/gitlab/checks/tag_check_spec.rb
+++ b/spec/lib/gitlab/checks/tag_check_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Checks::TagCheck do
+RSpec.describe Gitlab::Checks::TagCheck, feature_category: :source_code_management do
include_context 'change access checks context'
describe '#validate!' do
@@ -14,6 +14,29 @@ RSpec.describe Gitlab::Checks::TagCheck do
expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError, 'You are not allowed to change existing tags on this project.')
end
+ context "prohibited tags check" do
+ it "prohibits tag names that include refs/tags/ at the head" do
+ allow(subject).to receive(:tag_name).and_return("refs/tags/foo")
+
+ expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError, "You cannot create a tag with a prohibited pattern.")
+ end
+
+ it "doesn't prohibit a nested refs/tags/ string in a tag name" do
+ allow(subject).to receive(:tag_name).and_return("fix-for-refs/tags/foo")
+
+ expect { subject.validate! }.not_to raise_error
+ end
+
+ context "deleting a refs/tags headed tag" do
+ let(:newrev) { "0000000000000000000000000000000000000000" }
+ let(:ref) { "refs/tags/refs/tags/267208abfe40e546f5e847444276f7d43a39503e" }
+
+ it "doesn't prohibit the deletion of a refs/tags/ tag name" do
+ expect { subject.validate! }.not_to raise_error
+ end
+ end
+ end
+
context 'with protected tag' do
let!(:protected_tag) { create(:protected_tag, project: project, name: 'v*') }
diff --git a/spec/lib/gitlab/ci/artifacts/decompressed_artifact_size_validator_spec.rb b/spec/lib/gitlab/ci/artifacts/decompressed_artifact_size_validator_spec.rb
new file mode 100644
index 00000000000..ef39a431d63
--- /dev/null
+++ b/spec/lib/gitlab/ci/artifacts/decompressed_artifact_size_validator_spec.rb
@@ -0,0 +1,95 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Ci::Artifacts::DecompressedArtifactSizeValidator, feature_category: :build_artifacts do
+ include WorkhorseHelpers
+
+ let_it_be(:file_path) { File.join(Dir.tmpdir, 'decompressed_archive_size_validator_spec.gz') }
+ let(:file) { File.open(file_path) }
+ let(:file_format) { :gzip }
+ let(:max_bytes) { 20 }
+ let(:gzip_valid?) { true }
+ let(:validator) { instance_double(::Gitlab::Ci::DecompressedGzipSizeValidator, valid?: gzip_valid?) }
+
+ before(:all) do
+ Zlib::GzipWriter.open(file_path) do |gz|
+ gz.write('Hello World!')
+ end
+ end
+
+ after(:all) do
+ FileUtils.rm(file_path)
+ end
+
+ before do
+ allow(::Gitlab::Ci::DecompressedGzipSizeValidator)
+ .to receive(:new)
+ .and_return(validator)
+ end
+
+ subject { described_class.new(file: file, file_format: file_format, max_bytes: max_bytes) }
+
+ shared_examples 'when file does not exceed allowed compressed size' do
+ let(:gzip_valid?) { true }
+
+ it 'passes validation' do
+ expect { subject.validate! }.not_to raise_error
+ end
+ end
+
+ shared_examples 'when file exceeds allowed decompressed size' do
+ let(:gzip_valid?) { false }
+
+ it 'raises an exception' do
+ expect { subject.validate! }
+ .to raise_error(Gitlab::Ci::Artifacts::DecompressedArtifactSizeValidator::FileDecompressionError)
+ end
+ end
+
+ describe '#validate!' do
+ it_behaves_like 'when file does not exceed allowed compressed size'
+
+ it_behaves_like 'when file exceeds allowed decompressed size'
+ end
+
+ context 'when file is not provided' do
+ let(:file) { nil }
+
+ it 'passes validation' do
+ expect { subject.validate! }.not_to raise_error
+ end
+ end
+
+ context 'when the file is located in the cloud' do
+ let(:remote_path) { File.join(remote_store_path, remote_id) }
+
+ let(:file_url) { "http://s3.amazonaws.com/#{remote_path}" }
+ let(:file) do
+ instance_double(JobArtifactUploader,
+ path: file_path,
+ url: file_url,
+ object_store: ObjectStorage::Store::REMOTE)
+ end
+
+ let(:remote_id) { 'generated-remote-id-12345' }
+ let(:remote_store_path) { ObjectStorage::TMP_UPLOAD_PATH }
+
+ before do
+ stub_request(:get, %r{s3.amazonaws.com/#{remote_path}})
+ .to_return(status: 200, body: File.read('spec/fixtures/build.env.gz'))
+ end
+
+ it_behaves_like 'when file does not exceed allowed compressed size'
+
+ it_behaves_like 'when file exceeds allowed decompressed size'
+ end
+
+ context 'when file_format is not on the list' do
+ let_it_be(:file_format) { 'rar' }
+
+ it 'passes validation' do
+ expect { subject.validate! }.not_to raise_error
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/yaml/result_spec.rb b/spec/lib/gitlab/ci/config/yaml/result_spec.rb
index 72d96349668..d17e0609ef6 100644
--- a/spec/lib/gitlab/ci/config/yaml/result_spec.rb
+++ b/spec/lib/gitlab/ci/config/yaml/result_spec.rb
@@ -35,7 +35,7 @@ RSpec.describe Gitlab::Ci::Config::Yaml::Result, feature_category: :pipeline_com
result = described_class.new(config: [nil, { a: 1 }])
expect(result).not_to have_header
- expect(result.content).to be_nil
+ expect(result.content).to be_empty
end
end
diff --git a/spec/lib/gitlab/ci/config/yaml_spec.rb b/spec/lib/gitlab/ci/config/yaml_spec.rb
index beb872071d2..3576dd481c6 100644
--- a/spec/lib/gitlab/ci/config/yaml_spec.rb
+++ b/spec/lib/gitlab/ci/config/yaml_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe Gitlab::Ci::Config::Yaml, feature_category: :pipeline_composition do
describe '.load!' do
- it 'loads a single-doc YAML file' do
+ it 'loads a YAML file' do
yaml = <<~YAML
image: 'image:1.0'
texts:
@@ -26,30 +26,6 @@ RSpec.describe Gitlab::Ci::Config::Yaml, feature_category: :pipeline_composition
})
end
- it 'loads the first document from a multi-doc YAML file' do
- yaml = <<~YAML
- spec:
- inputs:
- test_input:
- ---
- image: 'image:1.0'
- texts:
- nested_key: 'value1'
- more_text:
- more_nested_key: 'value2'
- YAML
-
- config = described_class.load!(yaml)
-
- expect(config).to eq({
- spec: {
- inputs: {
- test_input: nil
- }
- }
- })
- end
-
context 'when YAML is invalid' do
let(:yaml) { 'some: invalid: syntax' }
@@ -58,58 +34,6 @@ RSpec.describe Gitlab::Ci::Config::Yaml, feature_category: :pipeline_composition
.to raise_error ::Gitlab::Config::Loader::FormatError, /mapping values are not allowed in this context/
end
end
-
- context 'when ci_multi_doc_yaml is disabled' do
- before do
- stub_feature_flags(ci_multi_doc_yaml: false)
- end
-
- it 'loads a single-doc YAML file' do
- yaml = <<~YAML
- image: 'image:1.0'
- texts:
- nested_key: 'value1'
- more_text:
- more_nested_key: 'value2'
- YAML
-
- config = described_class.load!(yaml)
-
- expect(config).to eq({
- image: 'image:1.0',
- texts: {
- nested_key: 'value1',
- more_text: {
- more_nested_key: 'value2'
- }
- }
- })
- end
-
- it 'loads the first document from a multi-doc YAML file' do
- yaml = <<~YAML
- spec:
- inputs:
- test_input:
- ---
- image: 'image:1.0'
- texts:
- nested_key: 'value1'
- more_text:
- more_nested_key: 'value2'
- YAML
-
- config = described_class.load!(yaml)
-
- expect(config).to eq({
- spec: {
- inputs: {
- test_input: nil
- }
- }
- })
- end
- end
end
describe '.load_result!' do
diff --git a/spec/lib/gitlab/ci/decompressed_gzip_size_validator_spec.rb b/spec/lib/gitlab/ci/decompressed_gzip_size_validator_spec.rb
new file mode 100644
index 00000000000..6ca3f4d415e
--- /dev/null
+++ b/spec/lib/gitlab/ci/decompressed_gzip_size_validator_spec.rb
@@ -0,0 +1,127 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Ci::DecompressedGzipSizeValidator, feature_category: :importers do
+ let_it_be(:filepath) { File.join(Dir.tmpdir, 'decompressed_gzip_size_validator_spec.gz') }
+
+ before(:all) do
+ create_compressed_file
+ end
+
+ after(:all) do
+ FileUtils.rm(filepath)
+ end
+
+ subject { described_class.new(archive_path: filepath, max_bytes: max_bytes) }
+
+ describe '#valid?' do
+ let(:max_bytes) { 20 }
+
+ context 'when file does not exceed allowed decompressed size' do
+ it 'returns true' do
+ expect(subject.valid?).to eq(true)
+ end
+
+ context 'when the waiter thread no longer exists due to being terminated or crashing' do
+ it 'gracefully handles the absence of the waiter without raising exception' do
+ allow(Process).to receive(:getpgid).and_raise(Errno::ESRCH)
+
+ expect(subject.valid?).to eq(true)
+ end
+ end
+ end
+
+ context 'when file exceeds allowed decompressed size' do
+ let(:max_bytes) { 1 }
+
+ it 'returns false' do
+ expect(subject.valid?).to eq(false)
+ end
+ end
+
+ context 'when exception occurs during header readings' do
+ shared_examples 'raises exception and terminates validator process group' do
+ let(:std) { instance_double(IO, close: nil) }
+ let(:wait_thr) { double }
+ let(:wait_threads) { [wait_thr, wait_thr] }
+
+ before do
+ allow(Process).to receive(:getpgid).and_return(2)
+ allow(Open3).to receive(:pipeline_r).and_return([std, wait_threads])
+ allow(wait_thr).to receive(:[]).with(:pid).and_return(1)
+ allow(wait_thr).to receive(:value).and_raise(exception)
+ end
+
+ it 'terminates validator process group' do
+ expect(Process).to receive(:kill).with(-1, 2).twice
+ expect(subject.valid?).to eq(false)
+ end
+ end
+
+ context 'when timeout occurs' do
+ let(:exception) { Timeout::Error }
+
+ include_examples 'raises exception and terminates validator process group'
+ end
+
+ context 'when exception occurs' do
+ let(:error_message) { 'Error!' }
+ let(:exception) { StandardError.new(error_message) }
+
+ include_examples 'raises exception and terminates validator process group'
+ end
+ end
+
+ describe 'archive path validation' do
+ let(:filesize) { nil }
+
+ context 'when archive path is traversed' do
+ let(:filepath) { '/foo/../bar' }
+
+ it 'does not pass validation' do
+ expect(subject.valid?).to eq(false)
+ end
+ end
+ end
+
+ context 'when archive path is not a string' do
+ let(:filepath) { 123 }
+
+ it 'returns false' do
+ expect(subject.valid?).to eq(false)
+ end
+ end
+
+ context 'when archive path is a symlink' do
+ let(:filepath) { File.join(Dir.tmpdir, 'symlink') }
+
+ before do
+ FileUtils.ln_s(filepath, filepath, force: true)
+ end
+
+ it 'returns false' do
+ expect(subject.valid?).to eq(false)
+ end
+ end
+
+ context 'when archive path is not a file' do
+ let(:filepath) { Dir.mktmpdir }
+ let(:filesize) { File.size(filepath) }
+
+ after do
+ FileUtils.rm_rf(filepath)
+ end
+
+ it 'returns false' do
+ expect(subject.valid?).to eq(false)
+ end
+ end
+ end
+
+ def create_compressed_file
+ Zlib::GzipWriter.open(filepath) do |gz|
+ gz.write('Hello World!')
+ end
+ end
+end
diff --git a/spec/lib/gitlab/import_export/project/tree_saver_spec.rb b/spec/lib/gitlab/import_export/project/tree_saver_spec.rb
index 4166eba4e8e..d8b8903c8ca 100644
--- a/spec/lib/gitlab/import_export/project/tree_saver_spec.rb
+++ b/spec/lib/gitlab/import_export/project/tree_saver_spec.rb
@@ -7,6 +7,8 @@ RSpec.describe Gitlab::ImportExport::Project::TreeSaver, :with_license, feature_
let_it_be(:exportable_path) { 'project' }
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
+ let_it_be(:private_project) { create(:project, :private, group: group) }
+ let_it_be(:private_mr) { create(:merge_request, source_project: private_project, project: private_project) }
let_it_be(:project) { setup_project }
shared_examples 'saves project tree successfully' do
@@ -118,6 +120,13 @@ RSpec.describe Gitlab::ImportExport::Project::TreeSaver, :with_license, feature_
expect(reviewer).not_to be_nil
expect(reviewer['user_id']).to eq(user.id)
end
+
+ it 'has merge requests system notes' do
+ system_notes = subject.first['notes'].select { |note| note['system'] }
+
+ expect(system_notes.size).to eq(1)
+ expect(system_notes.first['note']).to eq('merged')
+ end
end
context 'with snippets' do
@@ -492,6 +501,9 @@ RSpec.describe Gitlab::ImportExport::Project::TreeSaver, :with_license, feature_
create(:milestone, project: project)
discussion_note = create(:discussion_note, noteable: issue, project: project)
mr_note = create(:note, noteable: merge_request, project: project)
+ create(:system_note, noteable: merge_request, project: project, author: user, note: 'merged')
+ private_system_note = "mentioned in merge request #{private_mr.to_reference(project)}"
+ create(:system_note, noteable: merge_request, project: project, author: user, note: private_system_note)
create(:note, noteable: snippet, project: project)
create(:note_on_commit,
author: user,
diff --git a/spec/lib/gitlab/search_results_spec.rb b/spec/lib/gitlab/search_results_spec.rb
index 18b42da659b..0d4411a5d41 100644
--- a/spec/lib/gitlab/search_results_spec.rb
+++ b/spec/lib/gitlab/search_results_spec.rb
@@ -270,6 +270,21 @@ RSpec.describe Gitlab::SearchResults, feature_category: :global_search do
let_it_be(:archived_project) { create(:project, :archived, :public, group: group, name: 'Test2') }
it_behaves_like 'search results filtered by archived'
+
+ context 'when the search_projects_hide_archived feature flag is disabled' do
+ before do
+ stub_feature_flags(search_projects_hide_archived: false)
+ end
+
+ context 'when filter not provided' do
+ let(:filters) { {} }
+
+ it 'returns archived and unarchived results', :aggregate_failures do
+ expect(results.objects('projects')).to include unarchived_project
+ expect(results.objects('projects')).to include archived_project
+ end
+ end
+ end
end
end
diff --git a/spec/models/concerns/ci/artifactable_spec.rb b/spec/models/concerns/ci/artifactable_spec.rb
index 6af244a5a0f..f61fb74fa4e 100644
--- a/spec/models/concerns/ci/artifactable_spec.rb
+++ b/spec/models/concerns/ci/artifactable_spec.rb
@@ -36,6 +36,21 @@ RSpec.describe Ci::Artifactable do
expect { |b| artifact.each_blob(&b) }.to yield_control.exactly(3).times
end
end
+
+ context 'when decompressed artifact size validator fails' do
+ let(:artifact) { build(:ci_job_artifact, :junit) }
+
+ before do
+ allow_next_instance_of(Gitlab::Ci::DecompressedGzipSizeValidator) do |instance|
+ allow(instance).to receive(:valid?).and_return(false)
+ end
+ end
+
+ it 'fails on blob' do
+ expect { |b| artifact.each_blob(&b) }
+ .to raise_error(::Gitlab::Ci::Artifacts::DecompressedArtifactSizeValidator::FileDecompressionError)
+ end
+ end
end
context 'when file format is raw' do
diff --git a/spec/models/concerns/exportable_spec.rb b/spec/models/concerns/exportable_spec.rb
index 74709b06403..4f36fa52c6b 100644
--- a/spec/models/concerns/exportable_spec.rb
+++ b/spec/models/concerns/exportable_spec.rb
@@ -9,6 +9,7 @@ RSpec.describe Exportable, feature_category: :importers do
let_it_be(:issue) { create(:issue, project: project, milestone: milestone) }
let_it_be(:note1) { create(:system_note, project: project, noteable: issue) }
let_it_be(:note2) { create(:system_note, project: project, noteable: issue) }
+ let_it_be(:options) { { include: [{ notes: { only: [:note] }, milestone: { only: :title } }] } }
let_it_be(:model_klass) do
Class.new(ApplicationRecord) do
@@ -28,19 +29,27 @@ RSpec.describe Exportable, feature_category: :importers do
subject { model_klass.new }
- describe '.readable_records' do
+ describe '.to_authorized_json' do
let_it_be(:model_record) { model_klass.new }
- context 'when model does not respond to association name' do
- it 'returns nil' do
- expect(subject.readable_records(:foo, current_user: user)).to be_nil
+ context 'when key to authorize is not an association name' do
+ it 'returns string without given key' do
+ expect(subject.to_authorized_json([:foo], user, options)).not_to include('foo')
end
end
- context 'when model does respond to association name' do
+ context 'when key to authorize is an association name' do
+ let(:key_to_authorize) { :notes }
+
+ subject(:record_json) { model_record.to_authorized_json([key_to_authorize], user, options) }
+
context 'when there are no records' do
- it 'returns nil' do
- expect(model_record.readable_records(:notes, current_user: user)).to be_nil
+ before do
+ allow(model_record).to receive(:notes).and_return(Note.none)
+ end
+
+ it 'returns string including the empty association' do
+ expect(record_json).to include("\"notes\":[]")
end
end
@@ -57,8 +66,9 @@ RSpec.describe Exportable, feature_category: :importers do
end
end
- it 'returns collection of readable records' do
- expect(model_record.readable_records(:notes, current_user: user)).to contain_exactly(note1, note2)
+ it 'returns string containing all records' do
+ expect(record_json)
+ .to include("\"notes\":[{\"note\":\"#{note1.note}\"},{\"note\":\"#{note2.note}\"}]")
end
end
@@ -70,8 +80,19 @@ RSpec.describe Exportable, feature_category: :importers do
end
end
- it 'returns collection of readable records' do
- expect(model_record.readable_records(:notes, current_user: user)).to eq([])
+ it 'returns string including the empty association' do
+ expect(record_json).to include("\"notes\":[]")
+ end
+ end
+
+ context 'when user can read some records' do
+ before do
+ allow(model_record).to receive(:readable_records).with(:notes, current_user: user)
+ .and_return([note1])
+ end
+
+ it 'returns string containing readable records only' do
+ expect(record_json).to include("\"notes\":[{\"note\":\"#{note1.note}\"}]")
end
end
end
@@ -87,13 +108,15 @@ RSpec.describe Exportable, feature_category: :importers do
it 'calls #readable_by?' do
expect(note1).to receive(:readable_by?).with(user)
- model_record.readable_records(:notes, current_user: user)
+ record_json
end
end
context 'with single relation' do
+ let(:key_to_authorize) { :milestone }
+
before do
- allow(model_record).to receive(:try).with(:milestone).and_return(issue.milestone)
+ allow(model_record).to receive(:milestone).and_return(issue.milestone)
end
context 'when user can read the record' do
@@ -101,8 +124,8 @@ RSpec.describe Exportable, feature_category: :importers do
allow(milestone).to receive(:readable_by?).with(user).and_return(true)
end
- it 'returns collection of readable records' do
- expect(model_record.readable_records(:milestone, current_user: user)).to eq(milestone)
+ it 'returns string including association' do
+ expect(record_json).to include("\"milestone\":{\"title\":\"#{milestone.title}\"}")
end
end
@@ -111,8 +134,8 @@ RSpec.describe Exportable, feature_category: :importers do
allow(milestone).to receive(:readable_by?).with(user).and_return(false)
end
- it 'returns collection of readable records' do
- expect(model_record.readable_records(:milestone, current_user: user)).to be_nil
+ it 'returns string with null association' do
+ expect(record_json).to include("\"milestone\":null")
end
end
end
@@ -211,26 +234,4 @@ RSpec.describe Exportable, feature_category: :importers do
end
end
end
-
- describe '.has_many_association?' do
- let(:model_associations) { [:notes, :labels] }
-
- context 'when association type is `has_many`' do
- it 'returns true' do
- expect(subject.has_many_association?(:notes)).to eq(true)
- end
- end
-
- context 'when association type is `has_one`' do
- it 'returns true' do
- expect(subject.has_many_association?(:milestone)).to eq(false)
- end
- end
-
- context 'when association type is `belongs_to`' do
- it 'returns true' do
- expect(subject.has_many_association?(:project)).to eq(false)
- end
- end
- end
end
diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb
index e11c7e98287..42b70cbb858 100644
--- a/spec/models/concerns/issuable_spec.rb
+++ b/spec/models/concerns/issuable_spec.rb
@@ -1057,4 +1057,22 @@ RSpec.describe Issuable do
end
end
end
+
+ context 'with exportable associations' do
+ let_it_be(:project) { create(:project, group: create(:group, :private)) }
+
+ context 'for issues' do
+ let_it_be_with_reload(:resource) { create(:issue, project: project) }
+
+ it_behaves_like 'an exportable'
+ end
+
+ context 'for merge requests' do
+ let_it_be_with_reload(:resource) do
+ create(:merge_request, source_project: project, project: project)
+ end
+
+ it_behaves_like 'an exportable'
+ end
+ end
end
diff --git a/spec/models/label_spec.rb b/spec/models/label_spec.rb
index 26a1edcbcff..65e02da2b5d 100644
--- a/spec/models/label_spec.rb
+++ b/spec/models/label_spec.rb
@@ -44,6 +44,122 @@ RSpec.describe Label do
is_expected.to allow_value("customer's request").for(:title)
is_expected.to allow_value('s' * 255).for(:title)
end
+
+ describe 'description length' do
+ let(:invalid_description) { 'x' * (::Label::DESCRIPTION_LENGTH_MAX + 1) }
+ let(:valid_description) { 'short description' }
+ let(:label) { build(:label, project: project, description: description) }
+
+ let(:error_message) do
+ format(
+ _('is too long (%{size}). The maximum size is %{max_size}.'),
+ size: ActiveSupport::NumberHelper.number_to_human_size(invalid_description.bytesize),
+ max_size: ActiveSupport::NumberHelper.number_to_human_size(::Label::DESCRIPTION_LENGTH_MAX)
+ )
+ end
+
+ subject(:validate) { label.validate }
+
+ context 'when label is a new record' do
+ context 'when description exceeds the maximum size' do
+ let(:description) { invalid_description }
+
+ it 'adds a description too long error' do
+ validate
+
+ expect(label.errors[:description]).to contain_exactly(error_message)
+ end
+ end
+
+ context 'when description is within the allowed limits' do
+ let(:description) { valid_description }
+
+ it 'does not add a validation error' do
+ validate
+
+ expect(label.errors).not_to have_key(:description)
+ end
+ end
+ end
+
+ context 'when label is an existing record' do
+ before do
+ label.description = existing_description
+ label.save!(validate: false)
+ label.description = description
+ end
+
+ context 'when record already had a valid description' do
+ let(:existing_description) { 'small difference so it triggers description_changed?' }
+
+ context 'when new description exceeds the maximum size' do
+ let(:description) { invalid_description }
+
+ it 'adds a description too long error' do
+ validate
+
+ expect(label.errors[:description]).to contain_exactly(error_message)
+ end
+ end
+
+ context 'when new description is within the allowed limits' do
+ let(:description) { valid_description }
+
+ it 'does not add a validation error' do
+ validate
+
+ expect(label.errors).not_to have_key(:description)
+ end
+ end
+ end
+
+ context 'when record existed with an invalid description' do
+ let(:existing_description) { "#{invalid_description} small difference so it triggers description_changed?" }
+
+ context 'when description is not changed' do
+ let(:description) { existing_description }
+
+ it 'does not add a validation error' do
+ validate
+
+ expect(label.errors).not_to have_key(:description)
+ end
+ end
+
+ context 'when new description exceeds the maximum size' do
+ context 'when new description is shorter than existing description' do
+ let(:description) { invalid_description }
+
+ it 'allows updating descriptions that already existed above the limit' do
+ validate
+
+ expect(label.errors).not_to have_key(:description)
+ end
+ end
+
+ context 'when new description is longer than existing description' do
+ let(:description) { "#{existing_description}1" }
+
+ it 'adds a description too long error' do
+ validate
+
+ expect(label.errors[:description]).to contain_exactly(error_message)
+ end
+ end
+ end
+
+ context 'when new description is within the allowed limits' do
+ let(:description) { valid_description }
+
+ it 'does not add a validation error' do
+ validate
+
+ expect(label.errors).not_to have_key(:description)
+ end
+ end
+ end
+ end
+ end
end
describe 'scopes' do
diff --git a/spec/models/members/project_member_spec.rb b/spec/models/members/project_member_spec.rb
index 20dae056646..67af7a049cb 100644
--- a/spec/models/members/project_member_spec.rb
+++ b/spec/models/members/project_member_spec.rb
@@ -106,36 +106,6 @@ RSpec.describe ProjectMember do
end
end
- describe '.import_team' do
- before do
- @project_1 = create(:project)
- @project_2 = create(:project)
-
- @user_1 = create :user
- @user_2 = create :user
-
- @project_1.add_developer(@user_1)
- @project_2.add_reporter(@user_2)
-
- @status = @project_2.team.import(@project_1)
- end
-
- it { expect(@status).to be_truthy }
-
- describe 'project 2 should get user 1 as developer. user_2 should not be changed' do
- it { expect(@project_2.users).to include(@user_1) }
- it { expect(@project_2.users).to include(@user_2) }
-
- it { expect(Ability.allowed?(@user_1, :create_project, @project_2)).to be_truthy }
- it { expect(Ability.allowed?(@user_2, :read_project, @project_2)).to be_truthy }
- end
-
- describe 'project 1 should not be changed' do
- it { expect(@project_1.users).to include(@user_1) }
- it { expect(@project_1.users).not_to include(@user_2) }
- end
- end
-
describe '.truncate_teams' do
before do
@project_1 = create(:project)
diff --git a/spec/models/project_team_spec.rb b/spec/models/project_team_spec.rb
index 8fec2eb3386..4ff77faf5bf 100644
--- a/spec/models/project_team_spec.rb
+++ b/spec/models/project_team_spec.rb
@@ -164,6 +164,57 @@ RSpec.describe ProjectTeam, feature_category: :groups_and_projects do
end
end
+ describe '#import_team' do
+ let_it_be(:source_project) { create(:project) }
+ let_it_be(:target_project) { create(:project) }
+ let_it_be(:source_project_owner) { source_project.first_owner }
+ let_it_be(:source_project_developer) { create(:user) { |user| source_project.add_developer(user) } }
+ let_it_be(:current_user) { create(:user) { |user| target_project.add_maintainer(user) } }
+
+ subject(:import) { target_project.team.import(source_project, current_user) }
+
+ it { is_expected.to be_truthy }
+
+ it 'target project includes source member with the same access' do
+ import
+
+ imported_member_access = target_project.members.find_by!(user: source_project_developer).access_level
+ expect(imported_member_access).to eq(Gitlab::Access::DEVELOPER)
+ end
+
+ it 'does not change the source project members' do
+ import
+
+ expect(source_project.users).to include(source_project_developer)
+ expect(source_project.users).not_to include(current_user)
+ end
+
+ shared_examples 'imports source owners with correct access' do
+ specify do
+ import
+
+ source_owner_access_in_target = target_project.members.find_by!(user: source_project_owner).access_level
+ expect(source_owner_access_in_target).to eq(target_access_level)
+ end
+ end
+
+ context 'when importer is a maintainer in target project' do
+ it_behaves_like 'imports source owners with correct access' do
+ let(:target_access_level) { Gitlab::Access::MAINTAINER }
+ end
+ end
+
+ context 'when importer is an owner in target project' do
+ before do
+ target_project.add_owner(current_user)
+ end
+
+ it_behaves_like 'imports source owners with correct access' do
+ let(:target_access_level) { Gitlab::Access::OWNER }
+ end
+ end
+ end
+
describe '#find_member' do
context 'personal project' do
let(:project) do
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 9ca98354b9c..d10a8afc51e 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -690,34 +690,116 @@ RSpec.describe User, feature_category: :user_profile do
end
end
- describe '#commit_email=' do
- subject(:user) { create(:user) }
+ shared_examples 'for user notification, public, and commit emails' do
+ context 'when confirmed primary email' do
+ let(:user) { create(:user) }
+ let(:email) { user.email }
- it 'can be set to a confirmed email' do
- confirmed = create(:email, :confirmed, user: user)
- user.commit_email = confirmed.email
+ it 'can be set' do
+ set_email
- expect(user).to be_valid
+ expect(user).to be_valid
+ end
+
+ context 'when primary email is changed' do
+ before do
+ user.email = generate(:email)
+ end
+
+ it 'can not be set' do
+ set_email
+
+ expect(user).not_to be_valid
+ end
+ end
+
+ context 'when confirmed secondary email' do
+ let(:email) { create(:email, :confirmed, user: user).email }
+
+ it 'can be set' do
+ set_email
+
+ expect(user).to be_valid
+ end
+ end
+
+ context 'when unconfirmed secondary email' do
+ let(:email) { create(:email, user: user).email }
+
+ it 'can not be set' do
+ set_email
+
+ expect(user).not_to be_valid
+ end
+ end
+
+ context 'when invalid confirmed secondary email' do
+ let(:email) { create(:email, :confirmed, :skip_validate, user: user, email: 'invalid') }
+
+ it 'can not be set' do
+ set_email
+
+ expect(user).not_to be_valid
+ end
+ end
end
- it 'can not be set to an unconfirmed email' do
- unconfirmed = create(:email, user: user)
- user.commit_email = unconfirmed.email
+ context 'when unconfirmed primary email ' do
+ let(:user) { create(:user, :unconfirmed) }
+ let(:email) { user.email }
- expect(user).not_to be_valid
+ it 'can not be set' do
+ set_email
+
+ expect(user).not_to be_valid
+ end
end
- it 'can not be set to a non-existent email' do
- user.commit_email = 'non-existent-email@nonexistent.nonexistent'
+ context 'when new record' do
+ let(:user) { build(:user, :unconfirmed) }
+ let(:email) { user.email }
- expect(user).not_to be_valid
+ it 'can not be set' do
+ set_email
+
+ expect(user).not_to be_valid
+ end
+
+ context 'when skipping confirmation' do
+ before do
+ user.skip_confirmation = true
+ end
+
+ it 'can be set' do
+ set_email
+
+ expect(user).to be_valid
+ end
+ end
end
+ end
- it 'can not be set to an invalid email, even if confirmed' do
- confirmed = create(:email, :confirmed, :skip_validate, user: user, email: 'invalid')
- user.commit_email = confirmed.email
+ describe 'notification_email' do
+ include_examples 'for user notification, public, and commit emails' do
+ subject(:set_email) do
+ user.notification_email = email
+ end
+ end
+ end
- expect(user).not_to be_valid
+ describe 'public_email' do
+ include_examples 'for user notification, public, and commit emails' do
+ subject(:set_email) do
+ user.public_email = email
+ end
+ end
+ end
+
+ describe 'commit_email' do
+ include_examples 'for user notification, public, and commit emails' do
+ subject(:set_email) do
+ user.commit_email = email
+ end
end
end
@@ -3588,15 +3670,40 @@ RSpec.describe User, feature_category: :user_profile do
describe '#verified_emails' do
let(:user) { create(:user) }
+ let!(:confirmed_email) { create(:email, :confirmed, user: user) }
- it 'returns only confirmed emails' do
- email_confirmed = create :email, user: user, confirmed_at: Time.current
- create :email, user: user
+ before do
+ create(:email, user: user)
+ end
+ it 'returns only confirmed emails' do
expect(user.verified_emails).to contain_exactly(
user.email,
user.private_commit_email,
- email_confirmed.email
+ confirmed_email.email
+ )
+ end
+
+ it 'does not return primary email when primary email is changed' do
+ original_email = user.email
+ user.email = generate(:email)
+
+ expect(user.verified_emails).to contain_exactly(
+ user.private_commit_email,
+ confirmed_email.email,
+ original_email
+ )
+ end
+
+ it 'does not return unsaved primary email even if skip_confirmation is enabled' do
+ original_email = user.email
+ user.skip_confirmation = true
+ user.email = generate(:email)
+
+ expect(user.verified_emails).to contain_exactly(
+ user.private_commit_email,
+ confirmed_email.email,
+ original_email
)
end
end
diff --git a/spec/requests/abuse_reports_controller_spec.rb b/spec/requests/abuse_reports_controller_spec.rb
index 69b0fb41330..622d133158f 100644
--- a/spec/requests/abuse_reports_controller_spec.rb
+++ b/spec/requests/abuse_reports_controller_spec.rb
@@ -19,43 +19,6 @@ RSpec.describe AbuseReportsController, feature_category: :insider_threat do
sign_in(reporter)
end
- describe 'GET new' do
- let(:ref_url) { 'http://example.com' }
-
- it 'sets the instance variables' do
- get new_abuse_report_path(user_id: user.id, ref_url: ref_url)
-
- expect(assigns(:abuse_report)).to be_kind_of(AbuseReport)
- expect(assigns(:abuse_report)).to have_attributes(
- user_id: user.id,
- reported_from_url: ref_url
- )
- end
-
- context 'when the user has already been deleted' do
- it 'redirects the reporter to root_path' do
- user_id = user.id
- user.destroy!
-
- get new_abuse_report_path(user_id: user_id)
-
- expect(response).to redirect_to root_path
- expect(flash[:alert]).to eq(_('Cannot create the abuse report. The user has been deleted.'))
- end
- end
-
- context 'when the user has already been banned' do
- it 'redirects the reporter to the user\'s profile' do
- user.ban
-
- get new_abuse_report_path(user_id: user.id)
-
- expect(response).to redirect_to user
- expect(flash[:alert]).to eq(_('Cannot create the abuse report. This user has been banned.'))
- end
- end
- end
-
describe 'POST add_category', :aggregate_failures do
subject(:request) { post add_category_abuse_reports_path, params: request_params }
diff --git a/spec/requests/api/npm_group_packages_spec.rb b/spec/requests/api/npm_group_packages_spec.rb
index 1a862382d44..d97c7682b4b 100644
--- a/spec/requests/api/npm_group_packages_spec.rb
+++ b/spec/requests/api/npm_group_packages_spec.rb
@@ -27,6 +27,8 @@ RSpec.describe API::NpmGroupPackages, feature_category: :package_registry do
version: '1.2.0')
end
+ it_behaves_like 'rejects invalid package names'
+
it 'includes all matching package versions in the response' do
subject
diff --git a/spec/requests/api/npm_instance_packages_spec.rb b/spec/requests/api/npm_instance_packages_spec.rb
index 591a8ee68dc..97de7fa9e52 100644
--- a/spec/requests/api/npm_instance_packages_spec.rb
+++ b/spec/requests/api/npm_instance_packages_spec.rb
@@ -13,11 +13,12 @@ RSpec.describe API::NpmInstancePackages, feature_category: :package_registry do
describe 'GET /api/v4/packages/npm/*package_name' do
let(:url) { api("/packages/npm/#{package_name}") }
+ subject { get(url) }
+
it_behaves_like 'handling get metadata requests', scope: :instance
+ it_behaves_like 'rejects invalid package names'
context 'with a duplicate package name in another project' do
- subject { get(url) }
-
let_it_be(:project2) { create(:project, :public, namespace: namespace) }
let_it_be(:package2) do
create(:npm_package,
diff --git a/spec/requests/api/npm_project_packages_spec.rb b/spec/requests/api/npm_project_packages_spec.rb
index 6fede7e5267..60d4bddc502 100644
--- a/spec/requests/api/npm_project_packages_spec.rb
+++ b/spec/requests/api/npm_project_packages_spec.rb
@@ -23,6 +23,9 @@ RSpec.describe API::NpmProjectPackages, feature_category: :package_registry do
it_behaves_like 'handling get metadata requests', scope: :project
it_behaves_like 'accept get request on private project with access to package registry for everyone'
+ it_behaves_like 'rejects invalid package names' do
+ subject { get(url) }
+ end
end
describe 'GET /api/v4/projects/:id/packages/npm/-/package/*package_name/dist-tags' do
diff --git a/spec/routing/routing_spec.rb b/spec/routing/routing_spec.rb
index 2bd23340a88..a59ca5211ed 100644
--- a/spec/routing/routing_spec.rb
+++ b/spec/routing/routing_spec.rb
@@ -351,14 +351,6 @@ RSpec.describe InvitesController, 'routing' do
end
end
-RSpec.describe AbuseReportsController, 'routing' do
- let_it_be(:user) { create(:user) }
-
- it 'to #new' do
- expect(get("/-/abuse_reports/new?user_id=#{user.id}")).to route_to('abuse_reports#new', user_id: user.id.to_s)
- end
-end
-
RSpec.describe SentNotificationsController, 'routing' do
it 'to #unsubscribe' do
expect(get("/-/sent_notifications/4bee17d4a63ed60cf5db53417e9aeb4c/unsubscribe"))
diff --git a/spec/support/shared_examples/features/reportable_note_shared_examples.rb b/spec/support/shared_examples/features/reportable_note_shared_examples.rb
index 133da230bed..264cc9c798b 100644
--- a/spec/support/shared_examples/features/reportable_note_shared_examples.rb
+++ b/spec/support/shared_examples/features/reportable_note_shared_examples.rb
@@ -6,7 +6,6 @@ RSpec.shared_examples 'reportable note' do |type|
let(:comment) { find("##{ActionView::RecordIdentifier.dom_id(note)}") }
let(:more_actions_selector) { '.more-actions.dropdown' }
- let(:abuse_report_path) { new_abuse_report_path(user_id: note.author.id, ref_url: noteable_note_url(note)) }
it 'has an edit button' do
expect(comment).to have_selector('.js-note-edit')
diff --git a/spec/support/shared_examples/models/exportable_shared_examples.rb b/spec/support/shared_examples/models/exportable_shared_examples.rb
index 37c3e68fd5f..57e231e4a6e 100644
--- a/spec/support/shared_examples/models/exportable_shared_examples.rb
+++ b/spec/support/shared_examples/models/exportable_shared_examples.rb
@@ -1,27 +1,28 @@
# frozen_string_literal: true
-RSpec.shared_examples 'resource with exportable associations' do
- before do
- stub_licensed_features(stubbed_features) if stubbed_features.any?
- end
+RSpec.shared_examples 'an exportable' do |restricted_association: :project|
+ let_it_be(:user) { create(:user) }
describe '#exportable_association?' do
- let(:association) { single_association }
+ let(:association) { restricted_association }
subject { resource.exportable_association?(association, current_user: user) }
it { is_expected.to be_falsey }
- context 'when user can read resource' do
+ context 'when user can only read resource' do
before do
- group.add_developer(user)
+ allow(Ability).to receive(:allowed?).and_call_original
+ allow(Ability).to receive(:allowed?)
+ .with(user, :"read_#{resource.to_ability_name}", resource)
+ .and_return(true)
end
it { is_expected.to be_falsey }
context "when user can read resource's association" do
before do
- other_group.add_developer(user)
+ allow(resource).to receive(:readable_record?).with(anything, user).and_return(true)
end
it { is_expected.to be_truthy }
@@ -31,41 +32,48 @@ RSpec.shared_examples 'resource with exportable associations' do
it { is_expected.to be_falsey }
end
-
- context 'for an unauthenticated user' do
- let(:user) { nil }
-
- it { is_expected.to be_falsey }
- end
end
end
end
- describe '#readable_records' do
- subject { resource.readable_records(association, current_user: user) }
+ describe '#to_authorized_json' do
+ let(:options) { { include: [{ notes: { only: [:id] } }] } }
+
+ subject { resource.to_authorized_json(keys, user, options) }
before do
- group.add_developer(user)
+ allow(Ability).to receive(:allowed?).and_call_original
+ allow(Ability).to receive(:allowed?)
+ .with(user, :"read_#{resource.to_ability_name}", resource)
+ .and_return(true)
end
context 'when association not supported' do
- let(:association) { :foo }
+ let(:keys) { [:foo] }
- it { is_expected.to be_nil }
+ it { is_expected.not_to include('foo') }
end
context 'when association is `:notes`' do
- let(:association) { :notes }
+ let_it_be(:readable_note) { create(:system_note, noteable: resource, project: project, note: 'readable') }
+ let_it_be(:restricted_note) { create(:system_note, noteable: resource, project: project, note: 'restricted') }
- it { is_expected.to match_array([readable_note]) }
+ let(:restricted_note_access) { false }
+ let(:keys) { [:notes] }
- context 'when user have access' do
- before do
- other_group.add_developer(user)
- end
+ before do
+ allow(Ability).to receive(:allowed?).and_call_original
+ allow(Ability).to receive(:allowed?).with(user, :read_note, readable_note).and_return(true)
+ allow(Ability).to receive(:allowed?).with(user, :read_note, restricted_note).and_return(restricted_note_access)
+ end
+
+ it { is_expected.to include("\"notes\":[{\"id\":#{readable_note.id}}]") }
+
+ context 'when user have access to all notes' do
+ let(:restricted_note_access) { true }
- it 'returns all records' do
- is_expected.to match_array([readable_note, restricted_note])
+ it 'string includes all notes' do
+ is_expected.to include("\"notes\":[{\"id\":#{readable_note.id}},{\"id\":#{restricted_note.id}}]")
end
end
end
diff --git a/spec/support/shared_examples/requests/api/npm_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/npm_packages_shared_examples.rb
index 7b9c110536d..5284ed2de21 100644
--- a/spec/support/shared_examples/requests/api/npm_packages_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/npm_packages_shared_examples.rb
@@ -861,3 +861,14 @@ RSpec.shared_examples 'handling different package names, visibilities and user r
it_behaves_like example_name, status: status
end
end
+
+RSpec.shared_examples 'rejects invalid package names' do
+ let(:package_name) { "%0d%0ahttp:/%2fexample.com" }
+
+ it do
+ subject
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(Gitlab::Json.parse(response.body)).to eq({ 'error' => 'package_name should be a valid file path' })
+ end
+end