From e49157717c9d480c7df0377f13f0faeabda2c68a Mon Sep 17 00:00:00 2001 From: Mohammed Bilal Date: Fri, 9 Jun 2023 08:11:10 +0530 Subject: [PATCH] New upstream version 15.10.8+ds1 --- .gitlab/ci/rules.gitlab-ci.yml | 5 + CHANGELOG.md | 27 ++++ GITALY_SERVER_VERSION | 6 +- GITLAB_PAGES_VERSION | 6 +- VERSION | 6 +- app/assets/javascripts/single_file_diff.js | 4 +- app/controllers/abuse_reports_controller.rb | 7 + app/models/concerns/ci/artifactable.rb | 4 + app/models/concerns/exportable.rb | 63 ++++++++ app/models/concerns/issuable.rb | 5 + app/models/label.rb | 15 ++ app/models/project_team.rb | 6 +- app/models/user.rb | 4 +- config/routes.rb | 4 + doc/api/graphql/reference/index.md | 2 +- doc/api/projects.md | 5 + doc/user/project/members/index.md | 5 + lib/api/concerns/packages/npm_endpoints.rb | 10 +- lib/banzai/filter/dollar_math_post_filter.rb | 30 ++++ lib/banzai/filter/dollar_math_pre_filter.rb | 27 ++++ lib/banzai/filter/front_matter_filter.rb | 10 +- lib/banzai/filter/inline_diff_filter.rb | 17 +- lib/extracts_ref.rb | 8 + .../unconfirm_notification_email.html.haml | 6 +- .../unconfirm_notification_email.text.erb | 4 +- lib/gitlab/checks/tag_check.rb | 12 +- .../decompressed_artifact_size_validator.rb | 60 +++++++ .../ci/decompressed_gzip_size_validator.rb | 79 ++++++++++ lib/gitlab/front_matter.rb | 38 +++-- lib/gitlab/hook_data/base_builder.rb | 30 ++-- .../json/streaming_serializer.rb | 7 + lib/gitlab/regex.rb | 42 +++++ lib/gitlab/untrusted_regexp.rb | 25 +++ lib/gitlab/wiki_pages/front_matter_parser.rb | 12 +- spec/controllers/projects_controller_spec.rb | 76 +++++++++ spec/factories/users.rb | 4 + spec/fixtures/markdown.md.erb | 26 +++ spec/frontend/single_file_diff_spec.js | 15 ++ .../banzai/filter/front_matter_filter_spec.rb | 32 ++-- .../banzai/filter/inline_diff_filter_spec.rb | 8 + spec/lib/banzai/filter/math_filter_spec.rb | 21 +++ .../mailers/unconfirm_mailer_spec.rb | 2 +- spec/lib/gitlab/checks/tag_check_spec.rb | 25 ++- ...compressed_artifact_size_validator_spec.rb | 95 +++++++++++ .../decompressed_gzip_size_validator_spec.rb | 127 +++++++++++++++ .../import_export/project/tree_saver_spec.rb | 12 ++ spec/lib/gitlab/regex_spec.rb | 18 +++ spec/lib/gitlab/untrusted_regexp_spec.rb | 35 +++- spec/models/concerns/ci/artifactable_spec.rb | 15 ++ spec/models/concerns/exportable_spec.rb | 74 +++++++++ spec/models/concerns/issuable_spec.rb | 18 +++ spec/models/label_spec.rb | 116 ++++++++++++++ spec/models/members/project_member_spec.rb | 32 ++-- spec/models/project_team_spec.rb | 51 ++++++ spec/models/user_spec.rb | 149 +++++++++++++++--- .../requests/abuse_reports_controller_spec.rb | 71 ++++++++- .../api/npm_instance_packages_spec.rb | 9 ++ .../requests/api/npm_project_packages_spec.rb | 3 + spec/routing/routing_spec.rb | 8 - spec/support/matchers/markdown_matchers.rb | 2 +- .../reportable_note_shared_examples.rb | 1 - .../models/exportable_shared_examples.rb | 65 ++++++++ .../api/npm_packages_shared_examples.rb | 14 ++ 63 files changed, 1603 insertions(+), 112 deletions(-) create mode 100644 lib/gitlab/ci/artifacts/decompressed_artifact_size_validator.rb create mode 100644 lib/gitlab/ci/decompressed_gzip_size_validator.rb create mode 100644 spec/lib/gitlab/ci/artifacts/decompressed_artifact_size_validator_spec.rb create mode 100644 spec/lib/gitlab/ci/decompressed_gzip_size_validator_spec.rb diff --git a/.gitlab/ci/rules.gitlab-ci.yml b/.gitlab/ci/rules.gitlab-ci.yml index f4ca7c9b64..0a7655f7df 100644 --- a/.gitlab/ci/rules.gitlab-ci.yml +++ b/.gitlab/ci/rules.gitlab-ci.yml @@ -1144,6 +1144,11 @@ rules: - <<: *if-not-canonical-namespace when: never +<<<<<<< HEAD +======= + - <<: *if-security-merge-request + when: never +>>>>>>> 68c65fd975 (New upstream version 15.10.8+ds1) - <<: *if-merge-request-targeting-stable-branch when: always diff --git a/CHANGELOG.md b/CHANGELOG.md index 9244a75c3b..9087c73e74 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,33 @@ documentation](doc/development/changelog.md) for instructions on adding your own entry. +<<<<<<< HEAD +======= +## 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)) + +>>>>>>> 68c65fd975 (New upstream version 15.10.8+ds1) ## 15.10.7 (2023-05-10) ### Fixed (1 change) diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index 13aef295bb..15f5c55ae4 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1,5 @@ -15.10.7 \ No newline at end of file +<<<<<<< HEAD +15.10.7 +======= +15.10.8 +>>>>>>> 68c65fd975 (New upstream version 15.10.8+ds1) diff --git a/GITLAB_PAGES_VERSION b/GITLAB_PAGES_VERSION index 13aef295bb..15f5c55ae4 100644 --- a/GITLAB_PAGES_VERSION +++ b/GITLAB_PAGES_VERSION @@ -1 +1,5 @@ -15.10.7 \ No newline at end of file +<<<<<<< HEAD +15.10.7 +======= +15.10.8 +>>>>>>> 68c65fd975 (New upstream version 15.10.8+ds1) diff --git a/VERSION b/VERSION index 13aef295bb..15f5c55ae4 100644 --- a/VERSION +++ b/VERSION @@ -1 +1,5 @@ -15.10.7 \ No newline at end of file +<<<<<<< HEAD +15.10.7 +======= +15.10.8 +>>>>>>> 68c65fd975 (New upstream version 15.10.8+ds1) diff --git a/app/assets/javascripts/single_file_diff.js b/app/assets/javascripts/single_file_diff.js index b613e356a7..cb6c8a78b8 100644 --- a/app/assets/javascripts/single_file_diff.js +++ b/app/assets/javascripts/single_file_diff.js @@ -26,7 +26,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 f6b4fbac8d..8e0afbc0ec 100644 --- a/app/controllers/abuse_reports_controller.rb +++ b/app/controllers/abuse_reports_controller.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true class AbuseReportsController < ApplicationController +<<<<<<< HEAD before_action :set_user, only: [:new, :add_category] feature_category :insider_threat @@ -12,6 +13,12 @@ class AbuseReportsController < ApplicationController ) end +======= + before_action :set_user, only: [:add_category] + + feature_category :insider_threat + +>>>>>>> 68c65fd975 (New upstream version 15.10.8+ds1) def add_category @abuse_report = AbuseReport.new( user_id: @user.id, diff --git a/app/models/concerns/ci/artifactable.rb b/app/models/concerns/ci/artifactable.rb index 3fdbd6a878..974f8213a2 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 066a44912b..c260a37b21 100644 --- a/app/models/concerns/exportable.rb +++ b/app/models/concerns/exportable.rb @@ -3,6 +3,7 @@ module Exportable extend ActiveSupport::Concern +<<<<<<< HEAD def readable_records(association, current_user: nil) association_records = try(association) return unless association_records.present? @@ -16,6 +17,8 @@ module Exportable end end +======= +>>>>>>> 68c65fd975 (New upstream version 15.10.8+ds1) def exportable_association?(association, current_user: nil) return false unless respond_to?(association) return true if has_many_association?(association) @@ -30,8 +33,22 @@ module Exportable exportable_restricted_associations & keys end +<<<<<<< HEAD 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 +>>>>>>> 68c65fd975 (New upstream version 15.10.8+ds1) end private @@ -47,4 +64,50 @@ module Exportable record.readable_by?(user) end end +<<<<<<< HEAD +======= + + 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 +>>>>>>> 68c65fd975 (New upstream version 15.10.8+ds1) end diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index c1c1691e42..258f01d21b 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 @@ -226,6 +227,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/label.rb b/app/models/label.rb index aa53c0e0f3..98c51d38b8 100644 --- a/app/models/label.rb +++ b/app/models/label.rb @@ -13,6 +13,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 @@ -31,6 +32,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]) } @@ -277,6 +282,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 5641fbfb86..e8073c2f90 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_id) + 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 3bd8a03535..15b2e07d5b 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1524,7 +1524,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/routes.rb b/config/routes.rb index 589d44c3de..320f536d37 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -206,7 +206,11 @@ InitializerConnections.raise_if_new_database_connection do end # Spam reports +<<<<<<< HEAD resources :abuse_reports, only: [:new, :create] do +======= + resources :abuse_reports, only: [:create] do +>>>>>>> 68c65fd975 (New upstream version 15.10.8+ds1) collection do post :add_category end diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index f285c872cd..86e3319f20 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -15116,7 +15116,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 3cd7cdd811..6d0fc8b8bd 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -2368,6 +2368,11 @@ curl --request DELETE --header "PRIVATE-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/project/members/index.md b/doc/user/project/members/index.md index 6e20492db0..8b2cacf084 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/lib/api/concerns/packages/npm_endpoints.rb b/lib/api/concerns/packages/npm_endpoints.rb index 703a655ddc..3b8a52fc24 100644 --- a/lib/api/concerns/packages/npm_endpoints.rb +++ b/lib/api/concerns/packages/npm_endpoints.rb @@ -27,6 +27,14 @@ module API end helpers do +<<<<<<< HEAD +======= + params :package_name do + requires :package_name, type: String, file_path: true, desc: 'Package name', + documentation: { example: 'mypackage' } + end + +>>>>>>> 68c65fd975 (New upstream version 15.10.8+ds1) def redirect_or_present_audit_report redirect_registry_request( forward_to_registry: true, @@ -161,7 +169,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 94d1b4bcb4..e253940dd7 100644 --- a/lib/banzai/filter/dollar_math_post_filter.rb +++ b/lib/banzai/filter/dollar_math_post_filter.rb @@ -17,6 +17,7 @@ module Banzai # encoded and will therefore not interfere with the detection of the dollar syntax. # Corresponds to the "$...$" syntax +<<<<<<< HEAD DOLLAR_INLINE_PATTERN = %r{ (?\$(?(?:\S[^$\n]*?\S|[^$\s]))\$)(?:[^\d]|$) }x.freeze @@ -30,6 +31,23 @@ module Banzai DOLLAR_MATH_PIPELINE = [ { pattern: DOLLAR_DISPLAY_INLINE_PATTERN, style: :display }, { pattern: DOLLAR_INLINE_PATTERN, style: :inline } +======= + DOLLAR_INLINE_UNTRUSTED = + '(?P\$(?P(?:\S[^$\n]*?\S|[^$\s]))\$)(?:[^\d]|$)' + DOLLAR_INLINE_UNTRUSTED_REGEX = + Gitlab::UntrustedRegexp.new(DOLLAR_INLINE_UNTRUSTED, multiline: false) + + # Corresponds to the "$$...$$" syntax + DOLLAR_DISPLAY_INLINE_UNTRUSTED = + '(?P\$\$\ *(?P[^$\n]+?)\ *\$\$)' + DOLLAR_DISPLAY_INLINE_UNTRUSTED_REGEX = + Gitlab::UntrustedRegexp.new(DOLLAR_DISPLAY_INLINE_UNTRUSTED, multiline: false) + + # Order dependent. Handle the `$$` syntax before the `$` syntax + DOLLAR_MATH_PIPELINE = [ + { pattern: DOLLAR_DISPLAY_INLINE_UNTRUSTED_REGEX, style: :display }, + { pattern: DOLLAR_INLINE_UNTRUSTED_REGEX, style: :inline } +>>>>>>> 68c65fd975 (New upstream version 15.10.8+ds1) ].freeze # Do not recognize math inside these tags @@ -46,16 +64,28 @@ module Banzai next if has_ancestor?(node, IGNORED_ANCESTOR_TAGS) node_html = node.to_html +<<<<<<< HEAD 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) +>>>>>>> 68c65fd975 (New upstream version 15.10.8+ds1) 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 +<<<<<<< HEAD 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])) +>>>>>>> 68c65fd975 (New upstream version 15.10.8+ds1) end temp_node.replace(html) diff --git a/lib/banzai/filter/dollar_math_pre_filter.rb b/lib/banzai/filter/dollar_math_pre_filter.rb index aaa186f87a..715735a235 100644 --- a/lib/banzai/filter/dollar_math_pre_filter.rb +++ b/lib/banzai/filter/dollar_math_pre_filter.rb @@ -16,6 +16,7 @@ module Banzai # by converting it into the ```math syntax. In this way, we can ensure # that it's considered a code block and will not have any markdown processed inside it. +<<<<<<< HEAD # Corresponds to the "$$\n...\n$$" syntax REGEX = %r{ #{::Gitlab::Regex.markdown_code_or_html_blocks} @@ -41,6 +42,32 @@ module Banzai "```math\n#{$~[:display_math]}\n```" else $~[0] +======= + # Display math block: + # $$ + # latex math + # $$ + REGEX = + "#{::Gitlab::Regex.markdown_code_or_html_blocks_or_html_comments_untrusted}" \ + '|' \ + '^\$\$\ *\n' \ + '(?P' \ + '(?:\n|.)*?' \ + ')' \ + '\n\$\$\ *$' \ + .freeze + + def call + regex = Gitlab::UntrustedRegexp.new(REGEX, multiline: true) + return @text unless regex.match?(@text) + + regex.replace_gsub(@text) do |match| + # change from $$ to ```math + if match[:display_math] + "```math\n#{match[:display_math]}\n```" + else + match.to_s +>>>>>>> 68c65fd975 (New upstream version 15.10.8+ds1) end end end diff --git a/lib/banzai/filter/front_matter_filter.rb b/lib/banzai/filter/front_matter_filter.rb index c788137e12..53683ce07d 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 e47ff15e7b..2a43540934 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) + + INLINE_DIFF_ADDITION_UNTRUSTED = '(?:\[\+(.*?)\+\]|\{\+(.*?)\+\})' + INLINE_DIFF_ADDITION_UNTRUSTED_REGEX = + Gitlab::UntrustedRegexp.new(INLINE_DIFF_ADDITION_UNTRUSTED, multiline: false) + 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(/(?:\[\-(.*?)\-\]|\{\-(.*?)\-\})/, '\1\2') - html_content.gsub(/(?:\[\+(.*?)\+\]|\{\+(.*?)\+\})/, '\1\2') + html_content = INLINE_DIFF_DELETION_UNTRUSTED_REGEX.replace_gsub(text) do |match| + %(#{match[1]}#{match[2]}) + end + + INLINE_DIFF_ADDITION_UNTRUSTED_REGEX.replace_gsub(html_content) do |match| + %(#{match[1]}#{match[2]}) + end end end end diff --git a/lib/extracts_ref.rb b/lib/extracts_ref.rb index 49c9772f76..32ce5b8408 100644 --- a/lib/extracts_ref.rb +++ b/lib/extracts_ref.rb @@ -157,11 +157,19 @@ module ExtractsRef end def ambiguous_ref?(project, ref) +<<<<<<< HEAD return true if project.repository.ambiguous_ref?(ref) return false unless ref&.starts_with?('refs/') unprefixed_ref = ref.sub(%r{^refs/(heads|tags)/}, '') +======= + return false unless ref + return true if project.repository.ambiguous_ref?(ref) + return false unless ref.starts_with?(%r{(refs|heads|tags)/}) + + unprefixed_ref = ref.sub(%r{^(refs/)?(heads|tags)/}, '') +>>>>>>> 68c65fd975 (New upstream version 15.10.8+ds1) 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 d8f7466a1c..53d64fdf48 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 d20af9b980..81ef68e99f 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 007a775eaf..5c43ca946b 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 0000000000..04930da5e3 --- /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/decompressed_gzip_size_validator.rb b/lib/gitlab/ci/decompressed_gzip_size_validator.rb new file mode 100644 index 0000000000..a92f300767 --- /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::Utils.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/front_matter.rb b/lib/gitlab/front_matter.rb index 093501e860..2a759434b8 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(?[^\r\n]*coding:[^\r\n]*\R)? # optional encoding line - (?\s*) - ^(?#{DELIM})[ \t]*(?\S*)\R # opening front matter marker (optional language specifier) - (?.*?) # front matter block content (not greedy) - ^(\k | \.{3}) # closing front matter marker - [^\S\r\n]*(\R|\z) - }mx.freeze + # Original pattern: + # \A(?[^\r\n]*coding:[^\r\n]*\R)? # optional encoding line + # (?\s*) + # ^(?#{DELIM})[ \t]*(?\S*)\R # opening front matter marker (optional language specifier) + # (?.*?) # front matter block content (not greedy) + # ^(\k | \.{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[^\\r\\n]*coding:[^\\r\\n]*#{::Gitlab::UntrustedRegexp::BACKSLASH_R})?" + + '(?P\s*)' + + + # opening front matter marker (optional language specifier) + "^(?P#{DELIM_UNTRUSTED})[ \\t]*(?P\\S*)#{::Gitlab::UntrustedRegexp::BACKSLASH_R}" + + + # front matter block content (not greedy) + '(?P(?:\n|.)*?)' + + + # closing front matter marker + "^((?P#{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) end end diff --git a/lib/gitlab/hook_data/base_builder.rb b/lib/gitlab/hook_data/base_builder.rb index e5bae61ae4..4a81f6b8a0 100644 --- a/lib/gitlab/hook_data/base_builder.rb +++ b/lib/gitlab/hook_data/base_builder.rb @@ -5,15 +5,14 @@ module Gitlab class BaseBuilder attr_accessor :object - MARKDOWN_SIMPLE_IMAGE = %r{ - #{::Gitlab::Regex.markdown_code_or_html_blocks} - | - (? - ! - \[(?[^\n]*?)\] - \((?<url>(?!(https?://|//))[^\n]+?)\) - ) - }mx.freeze + MARKDOWN_SIMPLE_IMAGE = + "#{::Gitlab::Regex.markdown_code_or_html_blocks_untrusted}" \ + '|' \ + '(?P<image>' \ + '!' \ + '\[(?P<title>[^\n]*?)\]' \ + '\((?P<url>(?P<https>(https?://|//)?)[^\n]+?)\)' \ + ')'.freeze def initialize(object) @object = object @@ -37,15 +36,18 @@ module Gitlab def absolute_image_urls(markdown_text) return markdown_text unless markdown_text.present? - markdown_text.gsub(MARKDOWN_SIMPLE_IMAGE) do - if $~[:image] - url = $~[:url] + regex = Gitlab::UntrustedRegexp.new(MARKDOWN_SIMPLE_IMAGE, multiline: false) + return markdown_text unless regex.match?(markdown_text) + + regex.replace_gsub(markdown_text) do |match| + if match[:image] && !match[:https] + url = match[:url] url = "#{uploads_prefix}#{url}" if url.start_with?('/uploads') url = "/#{url}" unless url.start_with?('/') - "![#{$~[:title]}](#{Gitlab.config.gitlab.url}#{url})" + "![#{match[:title]}](#{Gitlab.config.gitlab.url}#{url})" else - $~[0] + match.to_s end end end diff --git a/lib/gitlab/import_export/json/streaming_serializer.rb b/lib/gitlab/import_export/json/streaming_serializer.rb index 389ab8b4c9..712569febc 100644 --- a/lib/gitlab/import_export/json/streaming_serializer.rb +++ b/lib/gitlab/import_export/json/streaming_serializer.rb @@ -121,6 +121,7 @@ module Gitlab def authorized_record_json(record, options) include_keys = options[:include].flat_map(&:keys) keys_to_authorize = record.try(:restricted_associations, include_keys) +<<<<<<< HEAD return record.to_json(options) if keys_to_authorize.blank? record_hash = record.as_json(options).with_indifferent_access @@ -144,6 +145,12 @@ module Gitlab end record_hash +======= + + return record.to_json(options) if keys_to_authorize.blank? + + record.to_authorized_json(keys_to_authorize, current_user, options) +>>>>>>> 68c65fd975 (New upstream version 15.10.8+ds1) end def batch(relation, key) diff --git a/lib/gitlab/regex.rb b/lib/gitlab/regex.rb index de6eba9b9c..7154e5bce8 100644 --- a/lib/gitlab/regex.rb +++ b/lib/gitlab/regex.rb @@ -459,7 +459,11 @@ module Gitlab # ``` MARKDOWN_CODE_BLOCK_REGEX_UNTRUSTED = '(?P<code>' \ +<<<<<<< HEAD '^```\n' \ +======= + '^```.*?\n' \ +>>>>>>> 68c65fd975 (New upstream version 15.10.8+ds1) '(?:\n|.)*?' \ '\n```\ *$' \ ')'.freeze @@ -477,6 +481,20 @@ module Gitlab ) }mx.freeze +<<<<<<< HEAD +======= + # HTML block: + # <tag> + # Anything, including `>>>` blocks which are ignored by this filter + # </tag> + MARKDOWN_HTML_BLOCK_REGEX_UNTRUSTED = + '(?P<html>' \ + '^<[^>]+?>\ *\n' \ + '(?:\n|.)*?' \ + '\n<\/[^>]+?>\ *$' \ + ')'.freeze + +>>>>>>> 68c65fd975 (New upstream version 15.10.8+ds1) # HTML comment line: # <!-- some commented text --> MARKDOWN_HTML_COMMENT_LINE_REGEX_UNTRUSTED = @@ -499,6 +517,16 @@ module Gitlab }mx.freeze end +<<<<<<< HEAD +======= + def markdown_code_or_html_blocks_untrusted + @markdown_code_or_html_blocks_untrusted ||= + "#{MARKDOWN_CODE_BLOCK_REGEX_UNTRUSTED}" \ + "|" \ + "#{MARKDOWN_HTML_BLOCK_REGEX_UNTRUSTED}" + end + +>>>>>>> 68c65fd975 (New upstream version 15.10.8+ds1) def markdown_code_or_html_comments_untrusted @markdown_code_or_html_comments_untrusted ||= "#{MARKDOWN_CODE_BLOCK_REGEX_UNTRUSTED}" \ @@ -506,6 +534,20 @@ module Gitlab "#{MARKDOWN_HTML_COMMENT_LINE_REGEX_UNTRUSTED}" \ "|" \ "#{MARKDOWN_HTML_COMMENT_BLOCK_REGEX_UNTRUSTED}" +<<<<<<< HEAD +======= + end + + def markdown_code_or_html_blocks_or_html_comments_untrusted + @markdown_code_or_html_comments_untrusted ||= + "#{MARKDOWN_CODE_BLOCK_REGEX_UNTRUSTED}" \ + "|" \ + "#{MARKDOWN_HTML_BLOCK_REGEX_UNTRUSTED}" \ + "|" \ + "#{MARKDOWN_HTML_COMMENT_LINE_REGEX_UNTRUSTED}" \ + "|" \ + "#{MARKDOWN_HTML_COMMENT_BLOCK_REGEX_UNTRUSTED}" +>>>>>>> 68c65fd975 (New upstream version 15.10.8+ds1) end # Based on Jira's project key format diff --git a/lib/gitlab/untrusted_regexp.rb b/lib/gitlab/untrusted_regexp.rb index 7c7bda3a8f..7c79c8a592 100644 --- a/lib/gitlab/untrusted_regexp.rb +++ b/lib/gitlab/untrusted_regexp.rb @@ -13,6 +13,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) @@ -29,6 +33,27 @@ module Gitlab RE2.GlobalReplace(text, regexp, rewrite) end + # There is no built-in replace with block support (like `gsub`). We can accomplish + # the same thing by parsing and rebuilding the string with the substitutions. + def replace_gsub(text) + new_text = +'' + remainder = text + + matched = match(remainder) + + until matched.nil? || matched.to_a.compact.empty? + partitioned = remainder.partition(matched.to_s) + new_text << partitioned.first + remainder = partitioned.last + + new_text << yield(matched) + + matched = match(remainder) + end + + new_text << remainder + end + def scan(text) matches = scan_regexp.scan(text).to_a matches.map!(&:first) if regexp.number_of_capturing_groups == 0 diff --git a/lib/gitlab/wiki_pages/front_matter_parser.rb b/lib/gitlab/wiki_pages/front_matter_parser.rb index 071b0dde61..dbe848f0ac 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 5ece9f09e5..2d529dc5e2 100644 --- a/spec/controllers/projects_controller_spec.rb +++ b/spec/controllers/projects_controller_spec.rb @@ -193,6 +193,7 @@ RSpec.describe ProjectsController, feature_category: :projects do end end +<<<<<<< HEAD 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| @@ -225,6 +226,81 @@ RSpec.describe ProjectsController, feature_category: :projects do expect(response).to redirect_to(tree_with_default_branch) end end +======= + 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 + + shared_examples 'ambiguous ref redirects' do + let(:project) { project_with_default_branch } + let(:branch_ref) { "refs/heads/#{ref}" } + let(:repo) { project.repository } + + before do + repo.create_branch(branch_ref, 'master') + repo.change_head(ref) + end + + after do + repo.change_head('master') + repo.delete_branch(branch_ref) + end + + 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 + + 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 +>>>>>>> 68c65fd975 (New upstream version 15.10.8+ds1) end end diff --git a/spec/factories/users.rb b/spec/factories/users.rb index 10de7bc3b5..85841ac18d 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/fixtures/markdown.md.erb b/spec/fixtures/markdown.md.erb index 979e96e6e8..26e5f11068 100644 --- a/spec/fixtures/markdown.md.erb +++ b/spec/fixtures/markdown.md.erb @@ -299,6 +299,32 @@ References should be parseable even inside _<%= merge_request.to_reference %>_ e v^2 + w^2 = x^2 ``` +Parsed correctly when between code blocks + +```ruby +x = 1 +``` + +$$ +a^2+b^2=c^2 +$$ + +``` +plaintext +``` + +Parsed correctly with a mixture of HTML comments and HTML blocks + +<!-- sdf --> + +$$ +a^2+b^2=c^2 +$$ + +<h1> +html +</h1> + ### Gollum Tags - [[linked-resource]] diff --git a/spec/frontend/single_file_diff_spec.js b/spec/frontend/single_file_diff_spec.js index ff2a4e31e0..572fd3810a 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/lib/banzai/filter/front_matter_filter_spec.rb b/spec/lib/banzai/filter/front_matter_filter_spec.rb index b15f3ad2cd..9d25dafb72 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 + 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 + 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 1ef00139db..1388a9053d 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 374983e40a..292b351a1f 100644 --- a/spec/lib/banzai/filter/math_filter_spec.rb +++ b/spec/lib/banzai/filter/math_filter_spec.rb @@ -101,6 +101,10 @@ RSpec.describe Banzai::Filter::MathFilter, feature_category: :team_planning do context 'with valid syntax' do where(:text, :result_template) do "$$\n2+2\n$$" | "<math>2+2\n</math>" +<<<<<<< HEAD +======= + "$$ \n2+2\n$$" | "<math>2+2\n</math>" +>>>>>>> 68c65fd975 (New upstream version 15.10.8+ds1) "$$\n2+2\n3+4\n$$" | "<math>2+2\n3+4\n</math>" end @@ -214,12 +218,29 @@ RSpec.describe Banzai::Filter::MathFilter, feature_category: :team_planning do expect(doc.search('.js-render-math').count).to eq(2) end +<<<<<<< HEAD def pipeline_filter(text) context = { project: nil, no_sourcepos: true } doc = Banzai::Pipeline::PreProcessPipeline.call(text, {}) doc = Banzai::Pipeline::PlainMarkdownPipeline.call(doc[:output], context) doc = Banzai::Filter::SanitizationFilter.call(doc[:output], context, nil) +======= + 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 } + doc = Banzai::Pipeline::PreProcessPipeline.call(text, {}) + doc = Banzai::Pipeline::PlainMarkdownPipeline.call(doc[:output], context) + doc = Banzai::Filter::SanitizationFilter.call(doc[:output], context, nil) + +>>>>>>> 68c65fd975 (New upstream version 15.10.8+ds1) filter(doc) end end 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 f430009989..05bd020d4e 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 50ffa5fad1..e75b045933 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 0000000000..ef39a431d6 --- /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/decompressed_gzip_size_validator_spec.rb b/spec/lib/gitlab/ci/decompressed_gzip_size_validator_spec.rb new file mode 100644 index 0000000000..6ca3f4d415 --- /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 b87992c459..063c03d359 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 |ndjson_enabled| @@ -125,6 +127,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 @@ -512,6 +521,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/regex_spec.rb b/spec/lib/gitlab/regex_spec.rb index e51e62d5f0..13b774683c 100644 --- a/spec/lib/gitlab/regex_spec.rb +++ b/spec/lib/gitlab/regex_spec.rb @@ -1164,10 +1164,28 @@ RSpec.describe Gitlab::Regex, feature_category: :tooling do MARKDOWN end +<<<<<<< HEAD it { is_expected.to match(%(<section>\nsomething\n</section>)) } it { is_expected.not_to match(%(must start in first column <section>\nsomething\n</section>)) } it { is_expected.not_to match(%(<section>must be multi-line</section>)) } it { expect(subject.match(markdown)[:html]).to eq expected } +======= + describe 'normal regular expression' do + it { is_expected.to match(%(<section>\nsomething\n</section>)) } + it { is_expected.not_to match(%(must start in first column <section>\nsomething\n</section>)) } + it { is_expected.not_to match(%(<section>must be multi-line</section>)) } + it { expect(subject.match(markdown)[:html]).to eq expected } + end + + describe 'untrusted regular expression' do + subject { Gitlab::UntrustedRegexp.new(described_class::MARKDOWN_HTML_BLOCK_REGEX_UNTRUSTED, multiline: true) } + + it { is_expected.to match(%(<section>\nsomething\n</section>)) } + it { is_expected.not_to match(%(must start in first column <section>\nsomething\n</section>)) } + it { is_expected.not_to match(%(<section>must be multi-line</section>)) } + it { expect(subject.match(markdown)[:html]).to eq expected } + end +>>>>>>> 68c65fd975 (New upstream version 15.10.8+ds1) end context 'HTML comment lines' do diff --git a/spec/lib/gitlab/untrusted_regexp_spec.rb b/spec/lib/gitlab/untrusted_regexp_spec.rb index 66675b2010..232329a5a1 100644 --- a/spec/lib/gitlab/untrusted_regexp_spec.rb +++ b/spec/lib/gitlab/untrusted_regexp_spec.rb @@ -3,7 +3,7 @@ require 'fast_spec_helper' require 'support/shared_examples/lib/gitlab/malicious_regexp_shared_examples' -RSpec.describe Gitlab::UntrustedRegexp do +RSpec.describe Gitlab::UntrustedRegexp, feature_category: :shared do describe '#initialize' do subject { described_class.new(pattern) } @@ -22,6 +22,39 @@ RSpec.describe Gitlab::UntrustedRegexp do end end + describe '#replace_gsub' do + let(:regex_str) { '(?P<scheme>(ftp))' } + let(:regex) { described_class.new(regex_str, multiline: true) } + + def result(regex, text) + regex.replace_gsub(text) do |match| + if match[:scheme] + "http|#{match[:scheme]}|rss" + else + match.to_s + end + end + end + + it 'replaces all instances of the match in a string' do + text = 'Use only https instead of ftp' + + expect(result(regex, text)).to eq('Use only https instead of http|ftp|rss') + end + + it 'replaces nothing when no match' do + text = 'Use only https instead of gopher' + + expect(result(regex, text)).to eq(text) + end + + it 'handles empty text' do + text = '' + + expect(result(regex, text)).to eq('') + end + end + describe '#replace' do it 'replaces the first instance of the match in a string' do result = described_class.new('foo').replace('foo bar foo', 'oof') diff --git a/spec/models/concerns/ci/artifactable_spec.rb b/spec/models/concerns/ci/artifactable_spec.rb index 6af244a5a0..f61fb74fa4 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 74709b0640..29fe8038cb 100644 --- a/spec/models/concerns/exportable_spec.rb +++ b/spec/models/concerns/exportable_spec.rb @@ -9,6 +9,10 @@ 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) } +<<<<<<< HEAD +======= + let_it_be(:options) { { include: [{ notes: { only: [:note] }, milestone: { only: :title } }] } } +>>>>>>> 68c65fd975 (New upstream version 15.10.8+ds1) let_it_be(:model_klass) do Class.new(ApplicationRecord) do @@ -28,6 +32,7 @@ RSpec.describe Exportable, feature_category: :importers do subject { model_klass.new } +<<<<<<< HEAD describe '.readable_records' do let_it_be(:model_record) { model_klass.new } @@ -41,6 +46,29 @@ RSpec.describe Exportable, feature_category: :importers do context 'when there are no records' do it 'returns nil' do expect(model_record.readable_records(:notes, current_user: user)).to be_nil +======= + describe '.to_authorized_json' do + let_it_be(:model_record) { model_klass.new } + + 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 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 + 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\":[]") +>>>>>>> 68c65fd975 (New upstream version 15.10.8+ds1) end end @@ -57,8 +85,14 @@ RSpec.describe Exportable, feature_category: :importers do end end +<<<<<<< HEAD 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}\"}]") +>>>>>>> 68c65fd975 (New upstream version 15.10.8+ds1) end end @@ -70,8 +104,24 @@ RSpec.describe Exportable, feature_category: :importers do end end +<<<<<<< HEAD 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}\"}]") +>>>>>>> 68c65fd975 (New upstream version 15.10.8+ds1) end end end @@ -87,13 +137,24 @@ RSpec.describe Exportable, feature_category: :importers do it 'calls #readable_by?' do expect(note1).to receive(:readable_by?).with(user) +<<<<<<< HEAD model_record.readable_records(:notes, current_user: user) +======= + record_json +>>>>>>> 68c65fd975 (New upstream version 15.10.8+ds1) end end context 'with single relation' do +<<<<<<< HEAD before do allow(model_record).to receive(:try).with(:milestone).and_return(issue.milestone) +======= + let(:key_to_authorize) { :milestone } + + before do + allow(model_record).to receive(:milestone).and_return(issue.milestone) +>>>>>>> 68c65fd975 (New upstream version 15.10.8+ds1) end context 'when user can read the record' do @@ -101,8 +162,13 @@ RSpec.describe Exportable, feature_category: :importers do allow(milestone).to receive(:readable_by?).with(user).and_return(true) end +<<<<<<< HEAD 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}\"}") +>>>>>>> 68c65fd975 (New upstream version 15.10.8+ds1) end end @@ -111,8 +177,13 @@ RSpec.describe Exportable, feature_category: :importers do allow(milestone).to receive(:readable_by?).with(user).and_return(false) end +<<<<<<< HEAD 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") +>>>>>>> 68c65fd975 (New upstream version 15.10.8+ds1) end end end @@ -211,6 +282,7 @@ RSpec.describe Exportable, feature_category: :importers do end end end +<<<<<<< HEAD describe '.has_many_association?' do let(:model_associations) { [:notes, :labels] } @@ -233,4 +305,6 @@ RSpec.describe Exportable, feature_category: :importers do end end end +======= +>>>>>>> 68c65fd975 (New upstream version 15.10.8+ds1) end diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb index 206b3ae61c..daf8c5a4b7 100644 --- a/spec/models/concerns/issuable_spec.rb +++ b/spec/models/concerns/issuable_spec.rb @@ -1079,4 +1079,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 ff7ac0ebd2..3dd9923842 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 f2bc9b42b7..364c4580f5 100644 --- a/spec/models/members/project_member_spec.rb +++ b/spec/models/members/project_member_spec.rb @@ -87,6 +87,7 @@ RSpec.describe ProjectMember do describe '#holder_of_the_personal_namespace?' do let_it_be(:project_member) { build(:project_member) } +<<<<<<< HEAD using RSpec::Parameterized::TableSyntax @@ -110,29 +111,24 @@ RSpec.describe ProjectMember do before do @project_1 = create(:project) @project_2 = create(:project) +======= +>>>>>>> 68c65fd975 (New upstream version 15.10.8+ds1) - @user_1 = create :user - @user_2 = create :user + using RSpec::Parameterized::TableSyntax - @project_1.add_developer(@user_1) - @project_2.add_reporter(@user_2) - - @status = @project_2.team.import(@project_1) + where(:personal_namespace_holder?, :expected) do + false | false + true | true end - it { expect(@status).to be_truthy } + with_them do + it "returns expected" do + allow(project_member.project).to receive(:personal_namespace_holder?) + .with(project_member.user) + .and_return(personal_namespace_holder?) - 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) } + expect(project_member.holder_of_the_personal_namespace?).to be(expected) + end end end diff --git a/spec/models/project_team_spec.rb b/spec/models/project_team_spec.rb index f4cf3130aa..aca6f7d053 100644 --- a/spec/models/project_team_spec.rb +++ b/spec/models/project_team_spec.rb @@ -164,6 +164,57 @@ RSpec.describe ProjectTeam, feature_category: :subgroups 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 04f1bffce0..e504e16594 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -659,34 +659,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 @@ -3440,15 +3522,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) } + + before do + create(:email, user: user) + end it 'returns only confirmed emails' do - email_confirmed = create :email, user: user, confirmed_at: Time.current - create :email, user: user - 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 934f123e45..b07f01cd42 100644 --- a/spec/requests/abuse_reports_controller_spec.rb +++ b/spec/requests/abuse_reports_controller_spec.rb @@ -18,6 +18,7 @@ RSpec.describe AbuseReportsController, feature_category: :insider_threat do sign_in(reporter) end +<<<<<<< HEAD describe 'GET new' do let(:ref_url) { 'http://example.com' } @@ -32,11 +33,73 @@ RSpec.describe AbuseReportsController, feature_category: :insider_threat do end context 'when the user has already been deleted' do +======= + describe 'POST add_category', :aggregate_failures do + subject(:request) { post add_category_abuse_reports_path, params: request_params } + + context 'when user is reported for abuse' do + let(:ref_url) { 'http://example.com' } + let(:request_params) do + { user_id: user.id, abuse_report: { category: abuse_category, reported_from_url: ref_url } } + end + + it 'renders new template' do + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to render_template(:new) + end + + it 'sets the instance variables' do + subject + + expect(assigns(:abuse_report)).to be_kind_of(AbuseReport) + expect(assigns(:abuse_report)).to have_attributes( + user_id: user.id, + category: abuse_category, + reported_from_url: ref_url + ) + end + + it 'tracks the snowplow event' do + subject + + expect_snowplow_event( + category: 'ReportAbuse', + action: 'select_abuse_category', + property: abuse_category, + user: user + ) + end + end + + context 'when abuse_report is missing in params' do + let(:request_params) { { user_id: user.id } } + + it 'raises an error' do + expect { subject }.to raise_error(ActionController::ParameterMissing) + end + end + + context 'when user_id is missing in params' do + let(:request_params) { { abuse_report: { category: abuse_category } } } + + it 'redirects the reporter to root_path' do + subject + + 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 deleted' do + let(:request_params) { { user_id: user.id, abuse_report: { category: abuse_category } } } + +>>>>>>> 68c65fd975 (New upstream version 15.10.8+ds1) it 'redirects the reporter to root_path' do - user_id = user.id user.destroy! - get new_abuse_report_path(user_id: user_id) + subject expect(response).to redirect_to root_path expect(flash[:alert]).to eq(_('Cannot create the abuse report. The user has been deleted.')) @@ -44,10 +107,12 @@ RSpec.describe AbuseReportsController, feature_category: :insider_threat do end context 'when the user has already been blocked' do + let(:request_params) { { user_id: user.id, abuse_report: { category: abuse_category } } } + it 'redirects the reporter to the user\'s profile' do user.block - get new_abuse_report_path(user_id: user.id) + subject expect(response).to redirect_to user expect(flash[:alert]).to eq(_('Cannot create the abuse report. This user has been blocked.')) diff --git a/spec/requests/api/npm_instance_packages_spec.rb b/spec/requests/api/npm_instance_packages_spec.rb index 591a8ee68d..de146fcf7e 100644 --- a/spec/requests/api/npm_instance_packages_spec.rb +++ b/spec/requests/api/npm_instance_packages_spec.rb @@ -13,11 +13,20 @@ 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}") } +<<<<<<< HEAD it_behaves_like 'handling get metadata requests', scope: :instance context 'with a duplicate package name in another project' do subject { get(url) } +======= + 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 +>>>>>>> 68c65fd975 (New upstream version 15.10.8+ds1) 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 2f67e1e8ee..e92d5cfc61 100644 --- a/spec/requests/api/npm_project_packages_spec.rb +++ b/spec/requests/api/npm_project_packages_spec.rb @@ -21,6 +21,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 2bd23340a8..a59ca5211e 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/matchers/markdown_matchers.rb b/spec/support/matchers/markdown_matchers.rb index a80c269f91..575ae572f2 100644 --- a/spec/support/matchers/markdown_matchers.rb +++ b/spec/support/matchers/markdown_matchers.rb @@ -202,7 +202,7 @@ module MarkdownMatchers match do |actual| expect(actual).to have_selector('[data-math-style="inline"]', count: 4) - expect(actual).to have_selector('[data-math-style="display"]', count: 4) + expect(actual).to have_selector('[data-math-style="display"]', count: 6) end end 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 bb3fab5b23..eaa3deaa44 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 37c3e68fd5..497cf0ea83 100644 --- a/spec/support/shared_examples/models/exportable_shared_examples.rb +++ b/spec/support/shared_examples/models/exportable_shared_examples.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +<<<<<<< HEAD RSpec.shared_examples 'resource with exportable associations' do before do stub_licensed_features(stubbed_features) if stubbed_features.any? @@ -7,21 +8,41 @@ RSpec.shared_examples 'resource with exportable associations' do describe '#exportable_association?' do let(:association) { single_association } +======= +RSpec.shared_examples 'an exportable' do |restricted_association: :project| + let_it_be(:user) { create(:user) } + + describe '#exportable_association?' do + let(:association) { restricted_association } +>>>>>>> 68c65fd975 (New upstream version 15.10.8+ds1) subject { resource.exportable_association?(association, current_user: user) } it { is_expected.to be_falsey } +<<<<<<< HEAD context 'when user can read resource' do before do group.add_developer(user) +======= + context 'when user can only read resource' do + before do + allow(Ability).to receive(:allowed?).and_call_original + allow(Ability).to receive(:allowed?) + .with(user, :"read_#{resource.to_ability_name}", resource) + .and_return(true) +>>>>>>> 68c65fd975 (New upstream version 15.10.8+ds1) end it { is_expected.to be_falsey } context "when user can read resource's association" do before do +<<<<<<< HEAD other_group.add_developer(user) +======= + allow(resource).to receive(:readable_record?).with(anything, user).and_return(true) +>>>>>>> 68c65fd975 (New upstream version 15.10.8+ds1) end it { is_expected.to be_truthy } @@ -31,16 +52,20 @@ RSpec.shared_examples 'resource with exportable associations' do it { is_expected.to be_falsey } end +<<<<<<< HEAD context 'for an unauthenticated user' do let(:user) { nil } it { is_expected.to be_falsey } end +======= +>>>>>>> 68c65fd975 (New upstream version 15.10.8+ds1) end end end +<<<<<<< HEAD describe '#readable_records' do subject { resource.readable_records(association, current_user: user) } @@ -66,6 +91,46 @@ RSpec.shared_examples 'resource with exportable associations' do it 'returns all records' do is_expected.to match_array([readable_note, restricted_note]) +======= + describe '#to_authorized_json' do + let(:options) { { include: [{ notes: { only: [:id] } }] } } + + subject { resource.to_authorized_json(keys, user, options) } + + before do + 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(:keys) { [:foo] } + + it { is_expected.not_to include('foo') } + end + + context 'when association is `:notes`' do + 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') } + + let(:restricted_note_access) { false } + let(:keys) { [:notes] } + + 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 'string includes all notes' do + is_expected.to include("\"notes\":[{\"id\":#{readable_note.id}},{\"id\":#{restricted_note.id}}]") +>>>>>>> 68c65fd975 (New upstream version 15.10.8+ds1) 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 ace76b5ef8..74772a78ec 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 @@ -856,3 +856,17 @@ RSpec.shared_examples 'handling different package names, visibilities and user r it_behaves_like example_name, status: status end end +<<<<<<< HEAD +======= + +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 +>>>>>>> 68c65fd975 (New upstream version 15.10.8+ds1)