diff --git a/.gitlab/ci/rules.gitlab-ci.yml b/.gitlab/ci/rules.gitlab-ci.yml index 13189f8812..710856fe13 100644 --- a/.gitlab/ci/rules.gitlab-ci.yml +++ b/.gitlab/ci/rules.gitlab-ci.yml @@ -1254,6 +1254,8 @@ rules: - <<: *if-not-canonical-namespace when: never + - <<: *if-security-merge-request + when: never - <<: *if-merge-request-targeting-stable-branch when: always diff --git a/.rubocop_todo/gitlab/strong_memoize_attr.yml b/.rubocop_todo/gitlab/strong_memoize_attr.yml index 21b8a2c9de..c9ed5041ab 100644 --- a/.rubocop_todo/gitlab/strong_memoize_attr.yml +++ b/.rubocop_todo/gitlab/strong_memoize_attr.yml @@ -363,7 +363,6 @@ Gitlab/StrongMemoizeAttr: - 'ee/app/models/vulnerabilities/finding.rb' - 'ee/app/presenters/approval_rule_presenter.rb' - 'ee/app/presenters/ci/minutes/usage_presenter.rb' - - 'ee/app/presenters/merge_request_approver_presenter.rb' - 'ee/app/serializers/dashboard_operations_project_entity.rb' - 'ee/app/serializers/ee/member_user_entity.rb' - 'ee/app/services/app_sec/dast/pipelines/find_latest_service.rb' diff --git a/.rubocop_todo/rspec/missing_feature_category.yml b/.rubocop_todo/rspec/missing_feature_category.yml index c94541ad8a..8018d9ab05 100644 --- a/.rubocop_todo/rspec/missing_feature_category.yml +++ b/.rubocop_todo/rspec/missing_feature_category.yml @@ -1069,7 +1069,6 @@ RSpec/MissingFeatureCategory: - 'ee/spec/models/approval_merge_request_rule_spec.rb' - 'ee/spec/models/approval_state_spec.rb' - 'ee/spec/models/approval_wrapped_any_approver_rule_spec.rb' - - 'ee/spec/models/approval_wrapped_code_owner_rule_spec.rb' - 'ee/spec/models/approval_wrapped_rule_spec.rb' - 'ee/spec/models/approvals/scan_finding_wrapped_rule_set_spec.rb' - 'ee/spec/models/approvals/wrapped_rule_set_spec.rb' diff --git a/CHANGELOG.md b/CHANGELOG.md index 170766e5f0..722a2406cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,69 @@ documentation](doc/development/changelog.md) for instructions on adding your own entry. +## 15.11.11 (2023-07-04) + +### Security (1 change) + +- [Add authorization to the subscriptions group controller](gitlab-org/security/gitlab@a6cb78e56a0e6b08ee7bbabd1687fb43a9f8703b) ([merge request](gitlab-org/security/gitlab!3381)) + +## 15.11.10 (2023-06-28) + +### Security (10 changes) + +- [Revert 'security-leaked-ci-job-token-permission-15-11' from '15-11'"](gitlab-org/security/gitlab@19f73bf5494d34b43eb8c807f860d545acae0c32) ([merge request](gitlab-org/security/gitlab!3375)) +- [Use fully qualified ref when loading code owner file](gitlab-org/security/gitlab@d7ffb4cca68373bff38bd05f0b8afc868cda9e04) ([merge request](gitlab-org/security/gitlab!3354)) +- [Maintainer can leak masked webhook secrets by manipulating URL masking](gitlab-org/security/gitlab@3a7ccdac5e41870fdce362c38d0a1d1437906fbd) ([merge request](gitlab-org/security/gitlab!3361)) +- [Remove approvals when the only commit gets amended](gitlab-org/security/gitlab@f8a4ad8be7e5fdf752f525ed58b94b1ce625b9a1) ([merge request](gitlab-org/security/gitlab!3368)) +- [Fix for fork permissions check in compare controller](gitlab-org/security/gitlab@8edf44b13e55ffe0c912f98134d0341a5a6bcd28) ([merge request](gitlab-org/security/gitlab!3344)) +- [Webhook token leaked in Sidekiq logs if log format is 'default'](gitlab-org/security/gitlab@02b58237085930c62ee277c9ebd89a0560f44a98) ([merge request](gitlab-org/security/gitlab!3347)) +- [Mitigate epic reference filter ReDOS](gitlab-org/security/gitlab@4c2cd6e5f7c994aca554be37d9ea9e5e114341f1) ([merge request](gitlab-org/security/gitlab!3339)) +- [Increasing security for CI_JOB_TOKEN on public and internal projects](gitlab-org/security/gitlab@4f8a00b2499e876df5b65eca921812fbb3215800) ([merge request](gitlab-org/security/gitlab!3319)) +- [Sanitize user email addresses in admin confirm user dialog](gitlab-org/security/gitlab@608c8001c349b0a62aae81850de669d3af02ab60) ([merge request](gitlab-org/security/gitlab!3332)) +- [Obfuscate email of service desk issue creator in issue REST API](gitlab-org/security/gitlab@a092ebc54cce4492f87f8ed2bf67c31793b0bd0e) ([merge request](gitlab-org/security/gitlab!3316)) + +## 15.11.9 (2023-06-15) + +### Changed (1 change) + +- [Make MigrateSharedVulnerabilityIdentifiers use slow iteration](gitlab-org/gitlab@1d91c7b295b22e844b81fb665748c447028525cd) ([merge request](gitlab-org/gitlab!122856)) **GitLab Enterprise Edition** + +## 15.11.8 (2023-06-06) + +### Fixed (2 changes) + +- [Fix memory leak in CI config includes entry](gitlab-org/gitlab@3e367e614c855352295e3bcab25bf5af4ec66bf5) ([merge request](gitlab-org/gitlab!122540)) +- [Fix serialization of pull_requests in Bitbucket Server Import](gitlab-org/gitlab@201ad93dfdf8f4acfb6d6eee32e2bd6a4ff68157) ([merge request](gitlab-org/gitlab!122396)) + +### Security (1 change) + +- [Validate description length in labels](gitlab-org/gitlab@2c821ee0823e37a57a6dc049591097232c933713) ([merge request](gitlab-org/gitlab!122697)) + +### Performance (1 change) + +- [LFS: Serve pre-signed URLs in `/lfs/objects/batch`](gitlab-org/gitlab@df3a9655a0e8f0bdab3433cefcbd37acfb8ebcff) ([merge request](gitlab-org/gitlab!122348)) + +## 15.11.7 (2023-06-05) + +### Security (16 changes) + +- [Fix DoS on test report artifacts](gitlab-org/security/gitlab@76133e75ad38326bf971e2d913263349781aecbe) ([merge request](gitlab-org/security/gitlab!3200)) +- [Fix XSS in Abuse Reports form action](gitlab-org/security/gitlab@e9f9b656b34bb30a7bd66ce82a9d8f6ac43c1ba8) ([merge request](gitlab-org/security/gitlab!3290)) +- [Escape the source branch link correctly](gitlab-org/security/gitlab@77ed3e8c2ef51e7bcc89ad1c8c549424a69e3478) ([merge request](gitlab-org/security/gitlab!3288)) +- [Import source owners with maintainer access if importer is a maintainer](gitlab-org/security/gitlab@98f939c9ba3efd5e51807adbaee189f180131544) ([merge request](gitlab-org/security/gitlab!3283)) +- [Filter inaccessible issuable notes when exporting project](gitlab-org/security/gitlab@29fe6582dd81855cdb263e118459aba370a7c7eb) ([merge request](gitlab-org/security/gitlab!3274)) +- [Block tag names that are prepended with refs/tags/, due to conflicts](gitlab-org/security/gitlab@2b39d58eb21cf2ecc581bc5e1bd4dd48dcfd20bc) ([merge request](gitlab-org/security/gitlab!3262)) +- [Set IP in ActionContoller filter before IP enforcement is evaluated](gitlab-org/security/gitlab@8d6e83ff72564f3fa3b24e2040072024b715a073) ([merge request](gitlab-org/security/gitlab!3279)) +- [Prevent primary email returned as verified on unsaved change](gitlab-org/security/gitlab@8ac9a3e3efea62d9b3e6d758ab3f1c43f2354ea5) ([merge request](gitlab-org/security/gitlab!3223)) +- [Use UntrustedRegexp to protect FrontMatter filter](gitlab-org/security/gitlab@5d300c3af9c37a7607d795868ae2b4e51b8802c5) ([merge request](gitlab-org/security/gitlab!3257)) +- [Improve ambiguous_ref? logic to include heads and tags](gitlab-org/security/gitlab@f478b7673efd183971e9375be84ad06af641893f) ([merge request](gitlab-org/security/gitlab!3247)) +- [Use UntrustedRegexp to protect InlineDiff filter](gitlab-org/security/gitlab@4056d6ccc5b592029ea92ebb90b6e6a66c6eb157) ([merge request](gitlab-org/security/gitlab!3254)) +- [Ignore user-defined diff paths in diff notes](gitlab-org/security/gitlab@b21208f4c10e8bd4e6754f9bfd2cc5fc96c8be20) ([merge request](gitlab-org/security/gitlab!3267)) +- [Reject NPM metadata requests with invalid package_name](gitlab-org/security/gitlab@c4d0f6256bae18d9bb9f6afc87afeeb3ff971335) ([merge request](gitlab-org/security/gitlab!3285)) +- [Use UntrustedRegexp to protect MathFilter regex](gitlab-org/security/gitlab@fd8298b140dba65ac77ed340a5f78e1fc8032db6) ([merge request](gitlab-org/security/gitlab!3251)) +- [Resolve Overall Project Vulnerability Disclosure](gitlab-org/security/gitlab@199048eb1c61063409e25d3433e7276faf95709b) ([merge request](gitlab-org/security/gitlab!3230)) +- [Validate description length in labels](gitlab-org/security/gitlab@208342903aabd7c4b78c24c0b9b173dfbd62e405) ([merge request](gitlab-org/security/gitlab!3242)) + ## 15.11.6 (2023-05-24) ### Changed (1 change) diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index 7b10f109c5..c694fe833b 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -15.11.6 \ No newline at end of file +15.11.11 \ No newline at end of file diff --git a/GITLAB_PAGES_VERSION b/GITLAB_PAGES_VERSION index 7b10f109c5..c694fe833b 100644 --- a/GITLAB_PAGES_VERSION +++ b/GITLAB_PAGES_VERSION @@ -1 +1 @@ -15.11.6 \ No newline at end of file +15.11.11 \ No newline at end of file diff --git a/VERSION b/VERSION index 7b10f109c5..c694fe833b 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -15.11.6 \ No newline at end of file +15.11.11 \ No newline at end of file diff --git a/app/assets/javascripts/single_file_diff.js b/app/assets/javascripts/single_file_diff.js index bab167bb7e..11896a7579 100644 --- a/app/assets/javascripts/single_file_diff.js +++ b/app/assets/javascripts/single_file_diff.js @@ -24,7 +24,9 @@ export default class SingleFileDiff { this.content = $('.diff-content', this.file); this.$chevronRightIcon = $('.diff-toggle-caret .chevron-right', this.file); this.$chevronDownIcon = $('.diff-toggle-caret .chevron-down', this.file); - this.diffForPath = this.content.find('[data-diff-for-path]').data('diffForPath'); + this.diffForPath = this.content + .find('div:not(.note-text)[data-diff-for-path]') + .data('diffForPath'); this.isOpen = !this.diffForPath; if (this.diffForPath) { this.collapsedContent = this.content; diff --git a/app/controllers/abuse_reports_controller.rb b/app/controllers/abuse_reports_controller.rb index edeac57bc4..5a4f80fcb3 100644 --- a/app/controllers/abuse_reports_controller.rb +++ b/app/controllers/abuse_reports_controller.rb @@ -1,17 +1,10 @@ # frozen_string_literal: true class AbuseReportsController < ApplicationController - before_action :set_user, only: [:new, :add_category] + before_action :set_user, only: [:add_category] feature_category :insider_threat - def new - @abuse_report = AbuseReport.new( - user_id: @user.id, - reported_from_url: params.fetch(:ref_url, '') - ) - end - def add_category @abuse_report = AbuseReport.new( user_id: @user.id, diff --git a/app/controllers/projects/compare_controller.rb b/app/controllers/projects/compare_controller.rb index 266edd506d..599bfd75e1 100644 --- a/app/controllers/projects/compare_controller.rb +++ b/app/controllers/projects/compare_controller.rb @@ -89,10 +89,14 @@ class Projects::CompareController < Projects::ApplicationController # target == start_ref == from def target_project strong_memoize(:target_project) do - next source_project.default_merge_request_target unless compare_params.key?(:from_project_id) - next source_project if compare_params[:from_project_id].to_i == source_project.id - - target_project = target_projects(source_project).find_by_id(compare_params[:from_project_id]) + target_project = + if !compare_params.key?(:from_project_id) + source_project.default_merge_request_target + elsif compare_params[:from_project_id].to_i == source_project.id + source_project + else + target_projects(source_project).find_by_id(compare_params[:from_project_id]) + end # Just ignore the field if it points at a non-existent or hidden project next source_project unless target_project && can?(current_user, :read_code, target_project) diff --git a/app/controllers/repositories/lfs_api_controller.rb b/app/controllers/repositories/lfs_api_controller.rb index d52ae723ee..32119ddf89 100644 --- a/app/controllers/repositories/lfs_api_controller.rb +++ b/app/controllers/repositories/lfs_api_controller.rb @@ -6,6 +6,10 @@ module Repositories include Gitlab::Utils::StrongMemoize LFS_TRANSFER_CONTENT_TYPE = 'application/octet-stream' + # Downloading directly with presigned URLs via batch requests + # require longer expire time. + # The 1h should be enough to download 100 objects. + LFS_DIRECT_BATCH_EXPIRE_IN = 3600.seconds skip_before_action :lfs_check_access!, only: [:deprecated] before_action :lfs_check_batch_operation!, only: [:batch] @@ -22,7 +26,11 @@ module Repositories end if download_request? - render json: { objects: download_objects! }, content_type: LfsRequest::CONTENT_TYPE + if Feature.enabled?(:lfs_batch_direct_downloads, project) + render json: { objects: download_objects! }, content_type: LfsRequest::CONTENT_TYPE + else + render json: { objects: legacy_download_objects! }, content_type: LfsRequest::CONTENT_TYPE + end elsif upload_request? render json: { objects: upload_objects! }, content_type: LfsRequest::CONTENT_TYPE else @@ -52,11 +60,34 @@ module Repositories end def download_objects! + existing_oids = project.lfs_objects + .for_oids(objects_oids) + .index_by(&:oid) + + objects.each do |object| + if lfs_object = existing_oids[object[:oid]] + object[:actions] = download_actions(object, lfs_object) + + if Guest.can?(:download_code, project) + object[:authenticated] = true + end + else + object[:error] = { + code: 404, + message: _("Object does not exist on the server or you don't have permissions to access it") + } + end + end + + objects + end + + def legacy_download_objects! existing_oids = project.lfs_objects_oids(oids: objects_oids) objects.each do |object| if existing_oids.include?(object[:oid]) - object[:actions] = download_actions(object) + object[:actions] = proxy_download_actions(object) if Guest.can?(:download_code, project) object[:authenticated] = true @@ -85,7 +116,26 @@ module Repositories objects end - def download_actions(object) + def download_actions(object, lfs_object) + if lfs_object.file.file_storage? || lfs_object.file.class.proxy_download_enabled? + proxy_download_actions(object) + else + direct_download_actions(lfs_object) + end + end + + def direct_download_actions(lfs_object) + { + download: { + href: lfs_object.file.url( + content_type: "application/octet-stream", + expire_at: LFS_DIRECT_BATCH_EXPIRE_IN.since + ) + } + } + end + + def proxy_download_actions(object) { download: { href: "#{project.http_url_to_repo}/gitlab-lfs/objects/#{object[:oid]}", diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb index 833f2874a9..26cfeb9380 100644 --- a/app/helpers/merge_requests_helper.rb +++ b/app/helpers/merge_requests_helper.rb @@ -243,13 +243,13 @@ module MergeRequestsHelper end branch = if merge_request.for_fork? - _('%{fork_icon} %{source_project_path}:%{source_branch}').html_safe % { fork_icon: fork_icon.html_safe, source_project_path: merge_request.source_project_path.html_safe, source_branch: merge_request.source_branch.html_safe } + html_escape(_('%{fork_icon} %{source_project_path}:%{source_branch}')) % { fork_icon: fork_icon.html_safe, source_project_path: merge_request.source_project_path, source_branch: merge_request.source_branch } else merge_request.source_branch end branch_title = if merge_request.for_fork? - _('%{source_project_path}:%{source_branch}').html_safe % { source_project_path: merge_request.source_project_path.html_safe, source_branch: merge_request.source_branch.html_safe } + html_escape(_('%{source_project_path}:%{source_branch}')) % { source_project_path: merge_request.source_project_path, source_branch: merge_request.source_branch } else merge_request.source_branch end diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb index a137ff4d6f..3a47fb3c96 100644 --- a/app/helpers/users_helper.rb +++ b/app/helpers/users_helper.rb @@ -136,7 +136,7 @@ module UsersHelper def confirm_user_data(user) message = if user.unconfirmed_email.present? - _('This user has an unconfirmed email address (%{email}). You may force a confirmation.') % { email: user.unconfirmed_email } + _('This user has an unconfirmed email address (%{email}). You may force a confirmation.').html_safe % { email: user.unconfirmed_email } else _('This user has an unconfirmed email address. You may force a confirmation.') end 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..c305d272a1 100644 --- a/app/models/concerns/exportable.rb +++ b/app/models/concerns/exportable.rb @@ -3,19 +3,6 @@ module Exportable extend ActiveSupport::Concern - def readable_records(association, current_user: nil) - association_records = try(association) - return unless association_records.present? - - if has_many_association?(association) - DeclarativePolicy.user_scope do - association_records.select { |record| readable_record?(record, current_user) } - end - else - readable_record?(association_records, current_user) ? association_records : nil - end - end - def exportable_association?(association, current_user: nil) return false unless respond_to?(association) return true if has_many_association?(association) @@ -30,8 +17,17 @@ module Exportable exportable_restricted_associations & keys end - def has_many_association?(association_name) - self.class.reflect_on_association(association_name)&.macro == :has_many + def to_authorized_json(keys_to_authorize, current_user, options) + modified_options = filtered_associations_opts(options, keys_to_authorize) + record_hash = as_json(modified_options).with_indifferent_access + + keys_to_authorize.each do |key| + next unless record_hash.key?(key) + + record_hash[key] = authorized_association_records(key, current_user, options) + end + + record_hash.to_json end private @@ -47,4 +43,47 @@ module Exportable record.readable_by?(user) end end + + def has_many_association?(association_name) + self.class.reflect_on_association(association_name)&.macro == :has_many + end + + def readable_records(association, current_user: nil) + association_records = try(association) + return unless association_records.present? + + if has_many_association?(association) + DeclarativePolicy.user_scope do + association_records.select { |record| readable_record?(record, current_user) } + end + else + readable_record?(association_records, current_user) ? association_records : nil + end + end + + def authorized_association_records(key, current_user, options) + records = readable_records(key, current_user: current_user) + empty_assoc = has_many_association?(key) ? [] : nil + return empty_assoc unless records.present? + + assoc_opts = association_options(key, options)&.dig(key) + records.as_json(assoc_opts) + end + + def filtered_associations_opts(options, associations) + options_copy = options.deep_dup + + associations.each do |key| + assoc_opts = association_options(key, options_copy) + next unless assoc_opts + + assoc_opts[key] = { only: [:id] } + end + + options_copy + end + + def association_options(key, options) + options[:include].find { |assoc| assoc.key?(key) } + end end diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 6594884ca0..f5227169f6 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/hooks/web_hook.rb b/app/models/hooks/web_hook.rb index 5ccbc926a7..1ee47bc63c 100644 --- a/app/models/hooks/web_hook.rb +++ b/app/models/hooks/web_hook.rb @@ -135,6 +135,7 @@ class WebHook < ApplicationRecord return if url_variables_were.blank? || interpolated_url_was == interpolated_url + self.url_variables = {} if url_variables_were.keys.intersection(url_variables.keys).any? self.url_variables = {} if url_changed? && url_variables_were.to_a.intersection(url_variables.to_a).any? end diff --git a/app/models/issue.rb b/app/models/issue.rb index 8ace5dfff5..5db024590f 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -25,7 +25,6 @@ class Issue < ApplicationRecord include FromUnion include EachBatch include PgFullTextSearchable - include Exportable extend ::Gitlab::Utils::Override diff --git a/app/models/label.rb b/app/models/label.rb index 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 dd200aec80..ca1064997a 100644 --- a/app/models/project_team.rb +++ b/app/models/project_team.rb @@ -117,12 +117,14 @@ class ProjectTeam owners.include?(user) end - def import(source_project, current_user = nil) + def import(source_project, current_user) target_project = project source_members = source_project.project_members.to_a target_user_ids = target_project.project_members.pluck_user_ids + importer_access_level = max_member_access(current_user.id) + source_members.reject! do |member| # Skip if user already present in team !member.invite? && target_user_ids.include?(member.user_id) @@ -132,6 +134,8 @@ class ProjectTeam new_member = member.dup new_member.id = nil new_member.source = target_project + # So that a maintainer cannot import a member with owner access + new_member.access_level = [new_member.access_level, importer_access_level].min new_member.created_by = current_user new_member end diff --git a/app/models/user.rb b/app/models/user.rb index 96223ac502..3b0fd23989 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1538,7 +1538,9 @@ class User < ApplicationRecord # rubocop: enable CodeReuse/ServiceClass def primary_email_verified? - confirmed? && !temp_oauth_email? + return false unless confirmed? && !temp_oauth_email? + + !email_changed? || new_record? end def accept_pending_invitations! diff --git a/config/feature_flags/development/lfs_batch_direct_downloads.yml b/config/feature_flags/development/lfs_batch_direct_downloads.yml new file mode 100644 index 0000000000..71634fc1d6 --- /dev/null +++ b/config/feature_flags/development/lfs_batch_direct_downloads.yml @@ -0,0 +1,8 @@ +--- +name: lfs_batch_direct_downloads +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/122221 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/413684 +milestone: '16.1' +type: development +group: group::tenant scale +default_enabled: false diff --git a/config/routes.rb b/config/routes.rb index ebb0984a00..b40076ed71 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -207,7 +207,7 @@ InitializerConnections.raise_if_new_database_connection do end # Spam reports - resources :abuse_reports, only: [:new, :create] do + resources :abuse_reports, only: [:create] do collection do post :add_category end diff --git a/db/15_11_migration_fixes.txt b/db/15_11_migration_fixes.txt new file mode 100644 index 0000000000..7fa291f7b2 --- /dev/null +++ b/db/15_11_migration_fixes.txt @@ -0,0 +1,299 @@ +20211202041233 +20211202094944 +20211202135508 +20211202145237 +20211203091642 +20211203160952 +20211203161149 +20211203161840 +20211203161942 +20211204010826 +20211206073851 +20211206074547 +20211206161271 +20211207081708 +20211207090503 +20211207125331 +20211207135331 +20211207154413 +20211207154414 +20211207165508 +20211207173510 +20211207173511 +20211208111425 +20211208122200 +20211208122201 +20211208171402 +20211209093636 +20211209093828 +20211209093923 +20211209094222 +20211209103048 +20211209203820 +20211209203821 +20211209230042 +20211210025754 +20211210031721 +20211210140000 +20211210140629 +20211210173137 +20211213064821 +20211213102111 +20211213130324 +20211213142344 +20211213154259 +20211213154704 +20211214012507 +20211214110307 +20211215090620 +20211215182006 +20211216133107 +20211216134134 +20211216135651 +20211216220939 +20211217050753 +20211217120000 +20211217145923 +20211217174331 +20211220064757 +20211220120402 +20211220123956 +20211220174504 +20211223125921 +20211224112937 +20211224114539 +20211229023654 +20211230112517 +20211230113031 +20220104060049 +20220104174445 +20220105020514 +20220105082217 +20220105121325 +20220105152547 +20220105153149 +20220106111958 +20220106112043 +20220106112085 +20220106141756 +20220106163326 +20220106185033 +20220106230629 +20220106230712 +20220106231518 +20220106233459 +20220106235626 +20220107064845 +20220107091629 +20220107165036 +20220109133006 +20220109134455 +20220110170953 +20220110171049 +20220110224913 +20220110231420 +20220110233155 +20220111002756 +20220111023852 +20220111093534 +20220111095006 +20220111095007 +20220111101421 +20220111102314 +20220111154950 +20220111154951 +20220111200254 +20220111221516 +20220112015940 +20220112090556 +20220112115413 +20220112205111 +20220112230642 +20220112232037 +20220112232605 +20220112232723 +20220113013319 +20220113014438 +20220113015830 +20220113035519 +20220113040447 +20220113111440 +20220113125401 +20220113135449 +20220113135924 +20220113164801 +20220113164901 +20220114105525 +20220114131950 +20220116175851 +20220117034056 +20220117082611 +20220117225936 +20220118015633 +20220118020026 +20220118141950 +20220118155846 +20220118155847 +20220118155848 +20220118204039 +20220119094023 +20220119094503 +20220119141407 +20220119141736 +20220119143130 +20220119144253 +20220119144458 +20220119151221 +20220119153706 +20220119154442 +20220119170426 +20220119193130 +20220119201340 +20220119203119 +20220119220620 +20220120033115 +20220120085655 +20220120094340 +20220120123700 +20220120123800 +20220120160625 +20220120211831 +20220120211832 +20220121214752 +20220121214753 +20220121221651 +20220124130028 +20220124145019 +20220124151456 +20220124151949 +20220124152824 +20220124153233 +20220124153234 +20220124180704 +20220124180705 +20220124184338 +20220124200927 +20220124204046 +20220124214131 +20220124215857 +20220124221521 +20220125083520 +20220125084127 +20220125084348 +20220125122228 +20220125122640 +20220125122725 +20220125230538 +20220126191624 +20220126201752 +20220126202654 +20220126203421 +20220126210021 +20220126210022 +20220126210657 +20220127112243 +20220127112412 +20220127132200 +20220127132201 +20220128093756 +20220128103042 +20220128155251 +20220128155814 +20220128194722 +20220131000000 +20220131000001 +20220131135725 +20220131192643 +20220201034731 +20220201141705 +20220201173212 +20220201193033 +20220201205300 +20220202034409 +20220202105733 +20220202115350 +20220203074916 +20220203091304 +20220203123333 +20220203133652 +20220203134942 +20220204053655 +20220204093120 +20220204095121 +20220204110725 +20220204154220 +20220204193000 +20220204194347 +20220207080758 +20220207083129 +20220208080921 +20220208115439 +20220208170445 +20220208171826 +20220209111007 +20220211090920 +20220211125954 +20220211214605 +20220212120735 +20220213100000 +20220213103859 +20220213104531 +20220215164709 +20220215190020 +20220216110023 +20220216201949 +20220217100008 +20220217113058 +20220217135229 +20220221102333 +20220221214928 +20220222072536 +20220222191845 +20220222192524 +20220222192525 +20220223112304 +20220223124428 +20220224000000 +20220224204415 +20220225133705 +20220301002101 +20220301003502 +20220301091503 +20220301093434 +20220301175104 +20220301175426 +20220302110724 +20220302114046 +20220302203410 +20220303190555 +20220303191047 +20220304052335 +20220304061631 +20220304062107 +20220304152729 +20220304165107 +20220304201847 +20220305223212 +20220307192534 +20220307192610 +20220307192645 +20220307192725 +20220307203458 +20220307203459 +20220308000205 +20220308115219 +20220308115502 +20220309084838 +20220309084954 +20220309100648 +20220309154855 +20220310011530 +20220310011613 +20220310095341 +20220310101118 +20220310134207 +20220310141349 +20220311010352 +20220314094841 +20220314154235 +20220314162342 \ No newline at end of file diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 2aa42018fc..a031d53347 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -15434,7 +15434,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 35400b0d54..03af850db9 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -2381,6 +2381,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 afbde29616..74ad3bb296 100644 --- a/lib/api/concerns/packages/npm_endpoints.rb +++ b/lib/api/concerns/packages/npm_endpoints.rb @@ -27,6 +27,11 @@ module API end helpers do + params :package_name do + requires :package_name, type: String, file_path: true, desc: 'Package name', + documentation: { example: 'mypackage' } + end + def redirect_or_present_audit_report redirect_registry_request( forward_to_registry: true, @@ -172,7 +177,7 @@ module API tags %w[npm_packages] end params do - requires :package_name, type: String, desc: 'Package name' + use :package_name end route_setting :authentication, job_token_allowed: true, deploy_token_allowed: true get '*package_name', format: false, requirements: ::API::Helpers::Packages::Npm::NPM_ENDPOINT_REQUIREMENTS do diff --git a/lib/api/entities/issue.rb b/lib/api/entities/issue.rb index 56e942a038..79adad92d6 100644 --- a/lib/api/entities/issue.rb +++ b/lib/api/entities/issue.rb @@ -55,7 +55,14 @@ module API end expose :moved_to_id - expose :service_desk_reply_to + expose :service_desk_reply_to do |issue| + issue.present( + current_user: options[:current_user], + # We need to pass it explicitly to account for the case where `issue` + # is a `WorkItem` which doesn't have a presenter yet. + presenter_class: IssuePresenter + ).service_desk_reply_to + end end end end diff --git a/lib/banzai/filter/dollar_math_post_filter.rb b/lib/banzai/filter/dollar_math_post_filter.rb index 94d1b4bcb4..76f69a66e8 100644 --- a/lib/banzai/filter/dollar_math_post_filter.rb +++ b/lib/banzai/filter/dollar_math_post_filter.rb @@ -17,19 +17,21 @@ module Banzai # encoded and will therefore not interfere with the detection of the dollar syntax. # Corresponds to the "$...$" syntax - DOLLAR_INLINE_PATTERN = %r{ - (?\$(?(?:\S[^$\n]*?\S|[^$\s]))\$)(?:[^\d]|$) - }x.freeze + 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_PATTERN = %r{ - (?\$\$\ *(?[^$\n]+?)\ *\$\$) - }x.freeze + 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_PATTERN, style: :display }, - { pattern: DOLLAR_INLINE_PATTERN, style: :inline } + { pattern: DOLLAR_DISPLAY_INLINE_UNTRUSTED_REGEX, style: :display }, + { pattern: DOLLAR_INLINE_UNTRUSTED_REGEX, style: :inline } ].freeze # Do not recognize math inside these tags @@ -46,16 +48,18 @@ module Banzai next if has_ancestor?(node, IGNORED_ANCESTOR_TAGS) node_html = node.to_html - next unless node_html.match?(DOLLAR_INLINE_PATTERN) || - node_html.match?(DOLLAR_DISPLAY_INLINE_PATTERN) + next unless DOLLAR_INLINE_UNTRUSTED_REGEX.match?(node_html) || + DOLLAR_DISPLAY_INLINE_UNTRUSTED_REGEX.match?(node_html) temp_doc = Nokogiri::HTML.fragment(node_html) DOLLAR_MATH_PIPELINE.each do |pipeline| temp_doc.xpath('child::text()').each do |temp_node| html = temp_node.to_html - temp_node.content.scan(pipeline[:pattern]).each do |matched, math| - html.sub!(matched, math_html(math: math, style: pipeline[:style])) + + pipeline[:pattern].scan(temp_node.content).each do |match| + math = pipeline[:pattern].extract_named_group(:math, match) + html.sub!(match.first, math_html(math: math, style: pipeline[:style])) end temp_node.replace(html) diff --git a/lib/banzai/filter/front_matter_filter.rb b/lib/banzai/filter/front_matter_filter.rb index 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/banzai/filter/references/reference_cache.rb b/lib/banzai/filter/references/reference_cache.rb index c8370d5f9c..759c34ab7e 100644 --- a/lib/banzai/filter/references/reference_cache.rb +++ b/lib/banzai/filter/references/reference_cache.rb @@ -29,15 +29,19 @@ module Banzai @references_per_parent[parent_type] ||= begin refs = Hash.new { |hash, key| hash[key] = Set.new } - prepare_doc_for_scan.to_enum(:scan, regex).each do - parent_path = if parent_type == :project - full_project_path($~[:namespace], $~[:project]) - else - full_group_path($~[:group]) - end + [filter.object_class.link_reference_pattern, filter.object_class.reference_pattern].each do |pattern| + next unless pattern - ident = filter.identifier($~) - refs[parent_path] << ident if ident + prepare_doc_for_scan.to_enum(:scan, pattern).each do + parent_path = if parent_type == :project + full_project_path($~[:namespace], $~[:project]) + else + full_group_path($~[:group]) + end + + ident = filter.identifier($~) + refs[parent_path] << ident if ident + end end refs @@ -171,15 +175,6 @@ module Banzai delegate :project, :group, :parent, :parent_type, to: :filter - def regex - strong_memoize(:regex) do - [ - filter.object_class.link_reference_pattern, - filter.object_class.reference_pattern - ].compact.reduce { |a, b| Regexp.union(a, b) } - end - end - def refs_cache Gitlab::SafeRequestStore["banzai_#{parent_type}_refs".to_sym] ||= {} end diff --git a/lib/bitbucket_server/representation/pull_request.rb b/lib/bitbucket_server/representation/pull_request.rb index 9a8eec67bd..acbc44f611 100644 --- a/lib/bitbucket_server/representation/pull_request.rb +++ b/lib/bitbucket_server/representation/pull_request.rb @@ -80,6 +80,7 @@ module BitbucketServer state: state, title: title, source_branch_name: source_branch_name, + source_branch_sha: source_branch_sha, target_branch_name: target_branch_name, target_branch_sha: target_branch_sha } diff --git a/lib/extracts_ref.rb b/lib/extracts_ref.rb index 49c9772f76..5f73b47495 100644 --- a/lib/extracts_ref.rb +++ b/lib/extracts_ref.rb @@ -157,11 +157,11 @@ module ExtractsRef end def ambiguous_ref?(project, ref) + return false unless ref return true if project.repository.ambiguous_ref?(ref) + return false unless ref.starts_with?(%r{(refs|heads|tags)/}) - return false unless ref&.starts_with?('refs/') - - unprefixed_ref = ref.sub(%r{^refs/(heads|tags)/}, '') + unprefixed_ref = ref.sub(%r{^(refs/)?(heads|tags)/}, '') project.repository.commit(unprefixed_ref).present? end end diff --git a/lib/gitlab/background_migration/mailers/views/unconfirm_mailer/unconfirm_notification_email.html.haml b/lib/gitlab/background_migration/mailers/views/unconfirm_mailer/unconfirm_notification_email.html.haml index 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/config/entry/includes.rb b/lib/gitlab/ci/config/entry/includes.rb index 24d0e27e3a..671ecf4eac 100644 --- a/lib/gitlab/ci/config/entry/includes.rb +++ b/lib/gitlab/ci/config/entry/includes.rb @@ -7,7 +7,7 @@ module Gitlab ## # Entry that represents a list of include. # - class Includes < ::Gitlab::Config::Entry::Node + class Includes < ::Gitlab::Config::Entry::ComposableArray include ::Gitlab::Config::Entry::Validatable validations do @@ -23,16 +23,8 @@ module Gitlab end end - def self.aspects - super.append -> do - @config = Array.wrap(@config) - - @config.each_with_index do |config, i| - @entries[i] = ::Gitlab::Config::Entry::Factory.new(Entry::Include) - .value(config || {}) - .create! - end - end + def composable_class + Entry::Include 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/import_export/json/streaming_serializer.rb b/lib/gitlab/import_export/json/streaming_serializer.rb index 389ab8b4c9..72d230ea37 100644 --- a/lib/gitlab/import_export/json/streaming_serializer.rb +++ b/lib/gitlab/import_export/json/streaming_serializer.rb @@ -121,29 +121,10 @@ module Gitlab def authorized_record_json(record, options) include_keys = options[:include].flat_map(&:keys) keys_to_authorize = record.try(:restricted_associations, include_keys) + return record.to_json(options) if keys_to_authorize.blank? - record_hash = record.as_json(options).with_indifferent_access - filtered_record_hash(record, keys_to_authorize, record_hash).to_json(options) - end - - def filtered_record_hash(record, keys_to_authorize, record_hash) - keys_to_authorize.each do |key| - next unless record_hash[key].present? - - readable = record.try(:readable_records, key, current_user: current_user) - if record.has_many_association?(key) - readable_ids = readable.pluck(:id) - - record_hash[key].keep_if do |association_record| - readable_ids.include?(association_record[:id]) - end - else - record_hash[key] = nil unless readable.present? - end - end - - record_hash + record.to_authorized_json(keys_to_authorize, current_user, options) end def batch(relation, key) diff --git a/lib/gitlab/sidekiq_middleware/arguments_logger.rb b/lib/gitlab/sidekiq_middleware/arguments_logger.rb index 2c506786d8..a743663d66 100644 --- a/lib/gitlab/sidekiq_middleware/arguments_logger.rb +++ b/lib/gitlab/sidekiq_middleware/arguments_logger.rb @@ -6,7 +6,8 @@ module Gitlab include Sidekiq::ServerMiddleware def call(worker, job, queue) - logger.info "arguments: #{Gitlab::Json.dump(job['args'])}" + loggable_args = Gitlab::ErrorTracking::Processor::SidekiqProcessor.loggable_arguments(job['args'], job['class']) + logger.info "arguments: #{Gitlab::Json.dump(loggable_args)}" yield end end diff --git a/lib/gitlab/untrusted_regexp.rb b/lib/gitlab/untrusted_regexp.rb index b7817a0c14..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) 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/lib/tasks/gitlab/db/migration_fix_15_11.rake b/lib/tasks/gitlab/db/migration_fix_15_11.rake new file mode 100644 index 0000000000..fbfee856ab --- /dev/null +++ b/lib/tasks/gitlab/db/migration_fix_15_11.rake @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +desc 'db | migration_fix_15_11' +task migration_fix_15_11: [:environment] do + next if Gitlab.com? + + only_on = %i[main ci].select { |db| Gitlab::Database.has_database?(db) } + Gitlab::Database::EachDatabase.each_database_connection(only: only_on) do |conn, database| + begin + first_migration = conn.execute('SELECT * FROM schema_migrations ORDER BY version ASC LIMIT 1') + rescue ActiveRecord::StatementInvalid + # Uninitialized DB, skip + next + end + next if first_migration.none? # No migrations have been run yet + # If we are affected, the first migration in the schema_migrations table + # will be 20220314184009 + next unless first_migration.first['version'] == '20220314184009' + + puts "Running 15.11 migration fix for #{database}" + fixes = File.readlines(Rails.root.join('db/15_11_migration_fixes.txt')).map(&:chomp) + conn.transaction do + fixes.each do |version| + conn.execute("INSERT INTO schema_migrations (version) VALUES ('#{version}')") + end + end + end +end + +Rake::Task['db:migrate'].enhance(['migration_fix_15_11']) diff --git a/spec/controllers/admin/hooks_controller_spec.rb b/spec/controllers/admin/hooks_controller_spec.rb index 4e68ffdda2..86c3405863 100644 --- a/spec/controllers/admin/hooks_controller_spec.rb +++ b/spec/controllers/admin/hooks_controller_spec.rb @@ -55,12 +55,13 @@ RSpec.describe Admin::HooksController do hook.update!(url_variables: { 'foo' => 'bar', 'baz' => 'woo' }) hook_params = { - url: 'http://example.com/{baz}?token={token}', + url: 'http://example.com/{bar}?token={token}', enable_ssl_verification: false, url_variables: [ { key: 'token', value: 'some secret value' }, - { key: 'baz', value: 'qux' }, - { key: 'foo', value: nil } + { key: 'baz', value: nil }, + { key: 'foo', value: nil }, + { key: 'bar', value: 'qux' } ] } @@ -72,7 +73,7 @@ RSpec.describe Admin::HooksController do expect(flash[:notice]).to include('was updated') expect(hook).to have_attributes(hook_params.except(:url_variables)) expect(hook).to have_attributes( - url_variables: { 'token' => 'some secret value', 'baz' => 'qux' } + url_variables: { 'token' => 'some secret value', 'bar' => 'qux' } ) end end diff --git a/spec/controllers/projects/compare_controller_spec.rb b/spec/controllers/projects/compare_controller_spec.rb index a49f8b51c1..7dc9bcd967 100644 --- a/spec/controllers/projects/compare_controller_spec.rb +++ b/spec/controllers/projects/compare_controller_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Projects::CompareController do +RSpec.describe Projects::CompareController, feature_category: :source_code_management do include ProjectForksHelper using RSpec::Parameterized::TableSyntax @@ -211,6 +211,36 @@ RSpec.describe Projects::CompareController do end end + context 'when the target project is the default source but hidden to the user' do + let(:project) { create(:project, :repository, :private) } + let(:from_ref) { 'improve%2Fmore-awesome' } + let(:to_ref) { 'feature' } + let(:whitespace) { nil } + + let(:request_params) do + { + namespace_id: project.namespace, + project_id: project, + from: from_ref, + to: to_ref, + w: whitespace, + page: page, + straight: straight + } + end + + it 'does not show the diff' do + allow(controller).to receive(:source_project).and_return(project) + expect(project).to receive(:default_merge_request_target).and_return(private_fork) + + show_request + + expect(response).to be_successful + expect(assigns(:diffs)).to be_empty + expect(assigns(:commits)).to be_empty + end + end + context 'when the source ref does not exist' do let(:from_project_id) { nil } let(:from_ref) { 'non-existent-source-ref' } diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb index b652aba1ff..577f10b961 100644 --- a/spec/controllers/projects_controller_spec.rb +++ b/spec/controllers/projects_controller_spec.rb @@ -193,37 +193,78 @@ RSpec.describe ProjectsController, feature_category: :projects do end end - context 'when the default branch name can resolve to another ref' do - let!(:project_with_default_branch) do - create(:project, :public, :custom_repo, files: ['somefile']).tap do |p| - p.repository.create_branch("refs/heads/refs/heads/#{other_ref}", 'master') - p.change_head("refs/heads/#{other_ref}") - end.reload + context 'when the default branch name is ambiguous' do + let_it_be(:project_with_default_branch) do + create(:project, :public, :custom_repo, files: ['somefile']) end - let(:other_ref) { 'branch-name' } - - context 'but there is no other ref' do - it 'responds with ok' do - get :show, params: { namespace_id: project_with_default_branch.namespace, id: project_with_default_branch } - expect(response).to be_ok - end - end - - context 'and that other ref exists' do - let(:tree_with_default_branch) do - branch = project_with_default_branch.repository.find_branch(project_with_default_branch.default_branch) - project_tree_path(project_with_default_branch, branch.target) - 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 - project_with_default_branch.repository.create_branch(other_ref, 'master') + repo.create_branch(branch_ref, 'master') + repo.change_head(ref) end - it 'redirects to tree view for the default branch' do - get :show, params: { namespace_id: project_with_default_branch.namespace, id: project_with_default_branch } - expect(response).to redirect_to(tree_with_default_branch) + 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 end end diff --git a/spec/factories/users.rb b/spec/factories/users.rb index 368623b9af..e2baf18b28 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/features/admin/users/user_spec.rb b/spec/features/admin/users/user_spec.rb index 403fd49fc6..485653d2ec 100644 --- a/spec/features/admin/users/user_spec.rb +++ b/spec/features/admin/users/user_spec.rb @@ -501,7 +501,9 @@ RSpec.describe 'Admin::Users::User', feature_category: :user_management do end context 'when user has an unconfirmed email', :js do - let(:unconfirmed_user) { create(:user, :unconfirmed) } + # Email address contains HTML to ensure email address is displayed in an HTML safe way. + let_it_be(:unconfirmed_email) { "#{generate(:email)}

testing" } + let_it_be(:unconfirmed_user) { create(:user, :unconfirmed, unconfirmed_email: unconfirmed_email) } where(:path_helper) do [ @@ -521,7 +523,9 @@ RSpec.describe 'Admin::Users::User', feature_category: :user_management do within_modal do expect(page).to have_content("Confirm user #{unconfirmed_user.name}?") - expect(page).to have_content('This user has an unconfirmed email address. You may force a confirmation.') + expect(page).to have_content( + "This user has an unconfirmed email address (#{unconfirmed_email}). You may force a confirmation." + ) click_button 'Confirm user' end diff --git a/spec/fixtures/emails/valid_reply_signed_smime.eml b/spec/fixtures/emails/valid_reply_signed_smime.eml index 965d922c95..0c5e2c439a 100644 --- a/spec/fixtures/emails/valid_reply_signed_smime.eml +++ b/spec/fixtures/emails/valid_reply_signed_smime.eml @@ -1,294 +1,294 @@ -User-Agent: Microsoft-MacOutlook/10.22.0.200209 -Date: Mon, 17 Feb 2020 22:56:47 +0100 -Subject: Re: htmltest | test issue (#1) -From: "Louzan Martinez, Diego (ext) (SI BP R&D ZG)" - -To: Administrator / htmltest - -Message-ID: <012E37D9-2A3F-4AC8-B79A-871F42914D86@siemens.com> -Thread-Topic: htmltest | test issue (#1) -References: - - -In-Reply-To: -Content-type: multipart/signed; - protocol="application/pkcs7-signature"; - micalg=sha256; - boundary="B_3664825007_1904734766" -MIME-Version: 1.0 - ---B_3664825007_1904734766 -Content-type: multipart/mixed; - boundary="B_3664825007_384940722" - - ---B_3664825007_384940722 -Content-type: multipart/alternative; - boundary="B_3664825007_1519466360" - - ---B_3664825007_1519466360 -Content-type: text/plain; - charset="UTF-8" -Content-transfer-encoding: quoted-printable - -Me too, with an attachment - -=20 - -From: Administrator -Reply to: Administrator / htmltest -Date: Monday, 17 February 2020 at 22:55 -To: "Louzan Martinez, Diego (ext) (SOP IT STG XS)" -Subject: Re: htmltest | test issue (#1) - -=20 - -Administrator commented:=20 - -I pity the foo !!! - -=E2=80=94=20 -Reply to this email directly or view it on GitLab.=20 -You're receiving this email because of your account on 169.254.169.254. If = -you'd like to receive fewer emails, you can unsubscribe from this thread or = -adjust your notification settings.=20 - - ---B_3664825007_1519466360 -Content-type: text/html; - charset="UTF-8" -Content-transfer-encoding: quoted-printable - -GitLab

Me too, with an attachment

 

From: Administrator <dlouzan.dummy@gma= -il.com>
Reply to: Administrator / htmltest <dlouzan.dummy+c0= -34670b1623e617e15a3df64223d363@gmail.com>
Date: Monday, 17 Febr= -uary 2020 at 22:55
To: "Louzan Martinez, Diego (ext) (SOP IT = -STG XS)" <diego.louzan.ext@siemens.com>
Subject: Re: ht= -mltest | test issue (#1)

= - 

Administrator commented:

<= -div>

I pity the foo !!!

=E2=80=94
Reply to this = -email directly or view it on GitLab.
You're receiving this email because of you= -r account on 169.254.169.254. If you'd like to receive fewer emails, you can= - unsubscribe from this thread or adjust your notific= -ation settings.

- ---B_3664825007_1519466360-- - - ---B_3664825007_384940722 -Content-type: image/png; name="gitlab_logo.png"; - x-mac-creator="4F50494D"; - x-mac-type="504E4766" -Content-disposition: attachment; - filename="gitlab_logo.png" -Content-transfer-encoding: base64 - - -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 ---B_3664825007_384940722-- - ---B_3664825007_1904734766 -Content-type: application/pkcs7-signature; name="smime.p7s" -Content-transfer-encoding: base64 -Content-disposition: attachment; - filename="smime.p7s" - -MIIRpwYJKoZIhvcNAQcCoIIRmDCCEZQCAQExDzANBglghkgBZQMEAgEFADALBgkqhkiG9w0B -BwGggg8VMIIHojCCBYqgAwIBAgIEZ5a6PTANBgkqhkiG9w0BAQsFADCBtjELMAkGA1UEBhMC -REUxDzANBgNVBAgMBkJheWVybjERMA8GA1UEBwwITXVlbmNoZW4xEDAOBgNVBAoMB1NpZW1l -bnMxETAPBgNVBAUTCFpaWlpaWkE2MR0wGwYDVQQLDBRTaWVtZW5zIFRydXN0IENlbnRlcjE/ -MD0GA1UEAww2U2llbWVucyBJc3N1aW5nIENBIE1lZGl1bSBTdHJlbmd0aCBBdXRoZW50aWNh -dGlvbiAyMDE2MB4XDTE5MTEyMTE0NDQ0N1oXDTIwMTEyMTE0NDQ0N1owdzERMA8GA1UEBRMI -WjAwM0gwOFQxDjAMBgNVBCoMBURpZWdvMRgwFgYDVQQEDA9Mb3V6YW4gTWFydGluZXoxGDAW -BgNVBAoMD1NpZW1lbnMtUGFydG5lcjEeMBwGA1UEAwwVTG91emFuIE1hcnRpbmV6IERpZWdv -MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuInpNaC7NRYD+0pOpHDz2pk9xmPt -JGj860SF6Nmn6Eu9EMYKEDfneC6z5QcH+mPS2d0VWgqVVGbRXSPsxJtbi9TCWjQUZdHglEZK -z9zxoFDh2dvW5/+TOT5Jf78FXyqak0YtY6+oMjQ/i9RUqPL7sIlyXLrBYrILzQ9Afo+7bXZg -v3ypp6xtqAV2ctHzQWFi0onJzxLVYguiVb7fFF9rBEMvSZonuw5tvOwJIhbe5FDFOrDcfbyU -ofZ/wikIZ+A+CE5GryXuuQmGxJaC2QqOkRAWQDzLDx9nG+rKiEs5OvlfEZC7EV1PyjZ93coM -faCVdlAgcFZ5fvd37CjyjKl+1QIDAQABo4IC9DCCAvAwggEEBggrBgEFBQcBAQSB9zCB9DAy -BggrBgEFBQcwAoYmaHR0cDovL2FoLnNpZW1lbnMuY29tL3BraT9aWlpaWlpBNi5jcnQwQQYI -KwYBBQUHMAKGNWxkYXA6Ly9hbC5zaWVtZW5zLm5ldC9DTj1aWlpaWlpBNixMPVBLST9jQUNl -cnRpZmljYXRlMEkGCCsGAQUFBzAChj1sZGFwOi8vYWwuc2llbWVucy5jb20vQ049WlpaWlpa -QTYsbz1UcnVzdGNlbnRlcj9jQUNlcnRpZmljYXRlMDAGCCsGAQUFBzABhiRodHRwOi8vb2Nz -cC5wa2ktc2VydmljZXMuc2llbWVucy5jb20wHwYDVR0jBBgwFoAU+BVdRwxsd3tyxAIXkWii -tvdqCUQwDAYDVR0TAQH/BAIwADBFBgNVHSAEPjA8MDoGDSsGAQQBoWkHAgIEAQMwKTAnBggr -BgEFBQcCARYbaHR0cDovL3d3dy5zaWVtZW5zLmNvbS9wa2kvMIHKBgNVHR8EgcIwgb8wgbyg -gbmggbaGJmh0dHA6Ly9jaC5zaWVtZW5zLmNvbS9wa2k/WlpaWlpaQTYuY3JshkFsZGFwOi8v -Y2wuc2llbWVucy5uZXQvQ049WlpaWlpaQTYsTD1QS0k/Y2VydGlmaWNhdGVSZXZvY2F0aW9u -TGlzdIZJbGRhcDovL2NsLnNpZW1lbnMuY29tL0NOPVpaWlpaWkE2LG89VHJ1c3RjZW50ZXI/ -Y2VydGlmaWNhdGVSZXZvY2F0aW9uTGlzdDAdBgNVHSUEFjAUBggrBgEFBQcDAgYIKwYBBQUH -AwQwDgYDVR0PAQH/BAQDAgeAMFUGA1UdEQROMEygLAYKKwYBBAGCNxQCA6AeDBxkaWVnby5s -b3V6YW4uZXh0QHNpZW1lbnMuY29tgRxkaWVnby5sb3V6YW4uZXh0QHNpZW1lbnMuY29tMB0G -A1UdDgQWBBQj8k8aqZey68w8ALYKGJSGMt5hZDANBgkqhkiG9w0BAQsFAAOCAgEAFDHqxpb1 -R9cB4noC9vx09bkNbmXCpVfl3XCQUmAWTznC0nwEssTTjo0PWuIV4C3jnsp0MRUeHZ6lsyhZ -OzS1ETwYgvj6wzjb8RF3wgn7N/JOvFGaErMz5HZpKOfzGiNpW6/Rmd4hsRDjAwOVQOXUTqc/ -0Bj3FMoLRCSWSnTp5HdyvrY2xOKHfTrTjzmcLdFaKE2F5n7+dBkwCKVfzut8CqfVq/I7ks4m -D1IHk93/P6l9U34R2FHPt6zRTNZcWmDirRSlMH4L18CnfiNPuDN/PtRYlt3Vng5EdYN0VCg2 -NM/uees0U4ingCb0NFjg66uQ/tjfPQk55MN4Wpls4N6TkMoTCWLiqZzYTGdmVQexzroL6940 -tmMr8LoN3TpPf0OdvdKEpyH7fzsx5QlmQyywIWec6X+Fx6+l0g91VJnPEtqACpfZIBZtviHl -gfX298w+SsvBK8C48Pqs8Ijh7tLrCxx7VMLVHZqwWWPK53ga+CDWmjoSQPxi+CPZF7kao6N5 -4GrJWwSHlHh6WzTbLyLvTJZZ775Utp4W8s8xMUsQJ413iYzEaC8FcSeNjSk5UiDDiHrKmzpM -tbApD3pUXStblUMKYGTG1Mj9BcEBFkCdoGlw/ulszIrKFfOyRNDG3Ay+Dj/oMjoKsJphu3px -wyft82rTer7UW/I7o0h0DAG4lkMwggdrMIIFU6ADAgECAgR5nlqfMA0GCSqGSIb3DQEBCwUA -MIGeMQswCQYDVQQGEwJERTEPMA0GA1UECAwGQmF5ZXJuMREwDwYDVQQHDAhNdWVuY2hlbjEQ -MA4GA1UECgwHU2llbWVuczERMA8GA1UEBRMIWlpaWlpaQTMxHTAbBgNVBAsMFFNpZW1lbnMg -VHJ1c3QgQ2VudGVyMScwJQYDVQQDDB5TaWVtZW5zIElzc3VpbmcgQ0EgRUUgRW5jIDIwMTYw -HhcNMTkwOTI3MDgwMTM5WhcNMjAwOTI3MDgwMTM3WjB3MREwDwYDVQQFEwhaMDAzSDA4VDEO -MAwGA1UEKgwFRGllZ28xGDAWBgNVBAQMD0xvdXphbiBNYXJ0aW5lejEYMBYGA1UECgwPU2ll -bWVucy1QYXJ0bmVyMR4wHAYDVQQDDBVMb3V6YW4gTWFydGluZXogRGllZ28wggEiMA0GCSqG -SIb3DQEBAQUAA4IBDwAwggEKAoIBAQCyby5qKzZIrGYWRqxnaAyMt/a/uc0uMk0F3MjwxvPM -vh5DllUpqx0l8ZDakDjPhlEXTeoL4DHNgmh+CDCs76CppM3cNG/1W1Ajo/L2iwMoXaxYuQ/F -q7ED+02KEkWX2DDVVG3fhrUGP20QAq77xPDptmVWZnUnuobZBNYkC49Xfl9HJvkJL8P0+Jqb -Eae7p4roiEr7wNkGriwrVXgA3oPNF/W+OuI76JTNTajS/6PAK/GeqIvLjfuBXpdBZTY031nE -Cztca8vI1jUjQzVhS+0dWpvpfhkVumbvOnid8DI9lapYsX8dpZFsa3ya+T3tjUdGSOOKi0kg -lWf/XYyyfhmDAgMBAAGjggLVMIIC0TAdBgNVHQ4EFgQUprhTCDwNLfPImpSfWdq+QvPTo9Mw -JwYDVR0RBCAwHoEcZGllZ28ubG91emFuLmV4dEBzaWVtZW5zLmNvbTAOBgNVHQ8BAf8EBAMC -BDAwLAYDVR0lBCUwIwYIKwYBBQUHAwQGCisGAQQBgjcKAwQGCysGAQQBgjcKAwQBMIHKBgNV -HR8EgcIwgb8wgbyggbmggbaGJmh0dHA6Ly9jaC5zaWVtZW5zLmNvbS9wa2k/WlpaWlpaQTMu -Y3JshkFsZGFwOi8vY2wuc2llbWVucy5uZXQvQ049WlpaWlpaQTMsTD1QS0k/Y2VydGlmaWNh -dGVSZXZvY2F0aW9uTGlzdIZJbGRhcDovL2NsLnNpZW1lbnMuY29tL0NOPVpaWlpaWkEzLG89 -VHJ1c3RjZW50ZXI/Y2VydGlmaWNhdGVSZXZvY2F0aW9uTGlzdDBFBgNVHSAEPjA8MDoGDSsG -AQQBoWkHAgIEAQMwKTAnBggrBgEFBQcCARYbaHR0cDovL3d3dy5zaWVtZW5zLmNvbS9wa2kv -MAwGA1UdEwEB/wQCMAAwHwYDVR0jBBgwFoAUoassbqB68NPCTeof8R4hivwMre8wggEEBggr -BgEFBQcBAQSB9zCB9DAyBggrBgEFBQcwAoYmaHR0cDovL2FoLnNpZW1lbnMuY29tL3BraT9a -WlpaWlpBMy5jcnQwQQYIKwYBBQUHMAKGNWxkYXA6Ly9hbC5zaWVtZW5zLm5ldC9DTj1aWlpa -WlpBMyxMPVBLST9jQUNlcnRpZmljYXRlMEkGCCsGAQUFBzAChj1sZGFwOi8vYWwuc2llbWVu -cy5jb20vQ049WlpaWlpaQTMsbz1UcnVzdGNlbnRlcj9jQUNlcnRpZmljYXRlMDAGCCsGAQUF -BzABhiRodHRwOi8vb2NzcC5wa2ktc2VydmljZXMuc2llbWVucy5jb20wDQYJKoZIhvcNAQEL -BQADggIBAF98ZMNg28LgkwdjOdvOGbC1QitsWjZTyotmQESF0nClDLUhb0O5675vVixntbrf -eB8xy1+KRiadk40GnAIJ0YzmNl4Tav6hPYv9VBWe5olsWG7C4qB3Q/SwhvW/e+owxv1cBra8 -R3oRudiN81eTZQHyNghRephVqQG/dpPYqydoANfIhEpHa79QlpaCAeYl4896AZOS8HYbkDFs -hLdv7sEHtl79YuSWI1wBjbJl70c0Sb4wLRgCPuHyQj2Uw/vQ5xJlEvBDZAIXXe1TP/nqiuY6 -7nweJbbeqfFE6ZP3kCe+mEIWGSaO0iThZyLGer8fHs1XiEmhhPgvC7P7KodzpXU6+hX+ZzbD -DxEjFfetV5sh0aNSXG9xx4hZmS9bpImBGR8MvZ7cgxqItvLtY2xvfUbYW244d4RcWesaCDq3 -ZEIo6uCIzOzJAwjUdLIac+lLV0rxiHmb7O3cQ19kjpWDB31hmfrus/TKJ55pBKVWBX5m/mFv -K8Ep5USpGrNS0EzOP7I1kQZv2VsvAhSxk/m5FMLpDy8T0O8YgbLypTXoeJFWCF6RduSjVsaZ -lkAtTQYud683pjyOMxJXaQUYGU1PmEYSOonMkVsT9aBcxYkXLp+Ln/+8G0OCYu7dRdwnj+Ut -7yR/ltxtgDcaFApCb0qBTKbgbqZk1fASmkOp+kbdYmoUMYICVjCCAlICAQEwgb8wgbYxCzAJ -BgNVBAYTAkRFMQ8wDQYDVQQIDAZCYXllcm4xETAPBgNVBAcMCE11ZW5jaGVuMRAwDgYDVQQK -DAdTaWVtZW5zMREwDwYDVQQFEwhaWlpaWlpBNjEdMBsGA1UECwwUU2llbWVucyBUcnVzdCBD -ZW50ZXIxPzA9BgNVBAMMNlNpZW1lbnMgSXNzdWluZyBDQSBNZWRpdW0gU3RyZW5ndGggQXV0 -aGVudGljYXRpb24gMjAxNgIEZ5a6PTANBglghkgBZQMEAgEFAKBpMC8GCSqGSIb3DQEJBDEi -BCAOR58AbNfSrI+vtMs+dgAQtn3IVZ3RjYC5hz3j9k+6TTAYBgkqhkiG9w0BCQMxCwYJKoZI -hvcNAQcBMBwGCSqGSIb3DQEJBTEPFw0yMDAyMTcyMTU2NDdaMA0GCSqGSIb3DQEBAQUABIIB -AHLSBcFHhNHPevbwqvA2ecuVb/aKnj45CFF6l8esP1H5DRm1ee5qMKuIS84NFuFC9RUENNhW -DBzsB+BVGz64o1f8QgIklYVrIJ4JZ0q1abNG7NbkVKWIpS3CQo//YWShUTYg+JpKx4YbahGR -sP5zbufbU4eagrrqBChjPTLy+njdjwCNu0XPykBTKOOf6BMjnS33AYjHJyh83JOY7rw3IDLx -8POQH4g5EMRpl9354s0rEkIezMt7pfUAsqY3QnQ8hvlE4KTikPQ+tvLMK1l/ffcLAP8BdBNI -YA3ikb3qCoGNSLKieYzNnBPhNOIJELUtEEaljAFZYMQzMKCbI4JdiDs= - ---B_3664825007_1904734766-- +User-Agent: Microsoft-MacOutlook/10.22.0.200209 +Date: Mon, 17 Feb 2020 22:56:47 +0100 +Subject: Re: htmltest | test issue (#1) +From: "Louzan Martinez, Diego (ext) (SI BP R&D ZG)" + +To: Administrator / htmltest + +Message-ID: <012E37D9-2A3F-4AC8-B79A-871F42914D86@siemens.com> +Thread-Topic: htmltest | test issue (#1) +References: + + +In-Reply-To: +Content-type: multipart/signed; + protocol="application/pkcs7-signature"; + micalg=sha256; + boundary="B_3664825007_1904734766" +MIME-Version: 1.0 + +--B_3664825007_1904734766 +Content-type: multipart/mixed; + boundary="B_3664825007_384940722" + + +--B_3664825007_384940722 +Content-type: multipart/alternative; + boundary="B_3664825007_1519466360" + + +--B_3664825007_1519466360 +Content-type: text/plain; + charset="UTF-8" +Content-transfer-encoding: quoted-printable + +Me too, with an attachment + +=20 + +From: Administrator +Reply to: Administrator / htmltest +Date: Monday, 17 February 2020 at 22:55 +To: "Louzan Martinez, Diego (ext) (SOP IT STG XS)" +Subject: Re: htmltest | test issue (#1) + +=20 + +Administrator commented:=20 + +I pity the foo !!! + +=E2=80=94=20 +Reply to this email directly or view it on GitLab.=20 +You're receiving this email because of your account on 169.254.169.254. If = +you'd like to receive fewer emails, you can unsubscribe from this thread or = +adjust your notification settings.=20 + + +--B_3664825007_1519466360 +Content-type: text/html; + charset="UTF-8" +Content-transfer-encoding: quoted-printable + +GitLab

Me too, with an attachment

 

From: Administrator <dlouzan.dummy@gma= +il.com>
Reply to: Administrator / htmltest <dlouzan.dummy+c0= +34670b1623e617e15a3df64223d363@gmail.com>
Date: Monday, 17 Febr= +uary 2020 at 22:55
To: "Louzan Martinez, Diego (ext) (SOP IT = +STG XS)" <diego.louzan.ext@siemens.com>
Subject: Re: ht= +mltest | test issue (#1)

= + 

Administrator commented:

<= +div>

I pity the foo !!!

=E2=80=94
Reply to this = +email directly or view it on GitLab.
You're receiving this email because of you= +r account on 169.254.169.254. If you'd like to receive fewer emails, you can= + unsubscribe from this thread or adjust your notific= +ation settings.

+ +--B_3664825007_1519466360-- + + +--B_3664825007_384940722 +Content-type: image/png; name="gitlab_logo.png"; + x-mac-creator="4F50494D"; + x-mac-type="504E4766" +Content-disposition: attachment; + filename="gitlab_logo.png" +Content-transfer-encoding: base64 + + +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 +--B_3664825007_384940722-- + +--B_3664825007_1904734766 +Content-type: application/pkcs7-signature; name="smime.p7s" +Content-transfer-encoding: base64 +Content-disposition: attachment; + filename="smime.p7s" + +MIIRpwYJKoZIhvcNAQcCoIIRmDCCEZQCAQExDzANBglghkgBZQMEAgEFADALBgkqhkiG9w0B +BwGggg8VMIIHojCCBYqgAwIBAgIEZ5a6PTANBgkqhkiG9w0BAQsFADCBtjELMAkGA1UEBhMC +REUxDzANBgNVBAgMBkJheWVybjERMA8GA1UEBwwITXVlbmNoZW4xEDAOBgNVBAoMB1NpZW1l +bnMxETAPBgNVBAUTCFpaWlpaWkE2MR0wGwYDVQQLDBRTaWVtZW5zIFRydXN0IENlbnRlcjE/ +MD0GA1UEAww2U2llbWVucyBJc3N1aW5nIENBIE1lZGl1bSBTdHJlbmd0aCBBdXRoZW50aWNh +dGlvbiAyMDE2MB4XDTE5MTEyMTE0NDQ0N1oXDTIwMTEyMTE0NDQ0N1owdzERMA8GA1UEBRMI +WjAwM0gwOFQxDjAMBgNVBCoMBURpZWdvMRgwFgYDVQQEDA9Mb3V6YW4gTWFydGluZXoxGDAW +BgNVBAoMD1NpZW1lbnMtUGFydG5lcjEeMBwGA1UEAwwVTG91emFuIE1hcnRpbmV6IERpZWdv +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuInpNaC7NRYD+0pOpHDz2pk9xmPt +JGj860SF6Nmn6Eu9EMYKEDfneC6z5QcH+mPS2d0VWgqVVGbRXSPsxJtbi9TCWjQUZdHglEZK +z9zxoFDh2dvW5/+TOT5Jf78FXyqak0YtY6+oMjQ/i9RUqPL7sIlyXLrBYrILzQ9Afo+7bXZg +v3ypp6xtqAV2ctHzQWFi0onJzxLVYguiVb7fFF9rBEMvSZonuw5tvOwJIhbe5FDFOrDcfbyU +ofZ/wikIZ+A+CE5GryXuuQmGxJaC2QqOkRAWQDzLDx9nG+rKiEs5OvlfEZC7EV1PyjZ93coM +faCVdlAgcFZ5fvd37CjyjKl+1QIDAQABo4IC9DCCAvAwggEEBggrBgEFBQcBAQSB9zCB9DAy +BggrBgEFBQcwAoYmaHR0cDovL2FoLnNpZW1lbnMuY29tL3BraT9aWlpaWlpBNi5jcnQwQQYI +KwYBBQUHMAKGNWxkYXA6Ly9hbC5zaWVtZW5zLm5ldC9DTj1aWlpaWlpBNixMPVBLST9jQUNl +cnRpZmljYXRlMEkGCCsGAQUFBzAChj1sZGFwOi8vYWwuc2llbWVucy5jb20vQ049WlpaWlpa +QTYsbz1UcnVzdGNlbnRlcj9jQUNlcnRpZmljYXRlMDAGCCsGAQUFBzABhiRodHRwOi8vb2Nz +cC5wa2ktc2VydmljZXMuc2llbWVucy5jb20wHwYDVR0jBBgwFoAU+BVdRwxsd3tyxAIXkWii +tvdqCUQwDAYDVR0TAQH/BAIwADBFBgNVHSAEPjA8MDoGDSsGAQQBoWkHAgIEAQMwKTAnBggr +BgEFBQcCARYbaHR0cDovL3d3dy5zaWVtZW5zLmNvbS9wa2kvMIHKBgNVHR8EgcIwgb8wgbyg +gbmggbaGJmh0dHA6Ly9jaC5zaWVtZW5zLmNvbS9wa2k/WlpaWlpaQTYuY3JshkFsZGFwOi8v +Y2wuc2llbWVucy5uZXQvQ049WlpaWlpaQTYsTD1QS0k/Y2VydGlmaWNhdGVSZXZvY2F0aW9u +TGlzdIZJbGRhcDovL2NsLnNpZW1lbnMuY29tL0NOPVpaWlpaWkE2LG89VHJ1c3RjZW50ZXI/ +Y2VydGlmaWNhdGVSZXZvY2F0aW9uTGlzdDAdBgNVHSUEFjAUBggrBgEFBQcDAgYIKwYBBQUH +AwQwDgYDVR0PAQH/BAQDAgeAMFUGA1UdEQROMEygLAYKKwYBBAGCNxQCA6AeDBxkaWVnby5s +b3V6YW4uZXh0QHNpZW1lbnMuY29tgRxkaWVnby5sb3V6YW4uZXh0QHNpZW1lbnMuY29tMB0G +A1UdDgQWBBQj8k8aqZey68w8ALYKGJSGMt5hZDANBgkqhkiG9w0BAQsFAAOCAgEAFDHqxpb1 +R9cB4noC9vx09bkNbmXCpVfl3XCQUmAWTznC0nwEssTTjo0PWuIV4C3jnsp0MRUeHZ6lsyhZ +OzS1ETwYgvj6wzjb8RF3wgn7N/JOvFGaErMz5HZpKOfzGiNpW6/Rmd4hsRDjAwOVQOXUTqc/ +0Bj3FMoLRCSWSnTp5HdyvrY2xOKHfTrTjzmcLdFaKE2F5n7+dBkwCKVfzut8CqfVq/I7ks4m +D1IHk93/P6l9U34R2FHPt6zRTNZcWmDirRSlMH4L18CnfiNPuDN/PtRYlt3Vng5EdYN0VCg2 +NM/uees0U4ingCb0NFjg66uQ/tjfPQk55MN4Wpls4N6TkMoTCWLiqZzYTGdmVQexzroL6940 +tmMr8LoN3TpPf0OdvdKEpyH7fzsx5QlmQyywIWec6X+Fx6+l0g91VJnPEtqACpfZIBZtviHl +gfX298w+SsvBK8C48Pqs8Ijh7tLrCxx7VMLVHZqwWWPK53ga+CDWmjoSQPxi+CPZF7kao6N5 +4GrJWwSHlHh6WzTbLyLvTJZZ775Utp4W8s8xMUsQJ413iYzEaC8FcSeNjSk5UiDDiHrKmzpM +tbApD3pUXStblUMKYGTG1Mj9BcEBFkCdoGlw/ulszIrKFfOyRNDG3Ay+Dj/oMjoKsJphu3px +wyft82rTer7UW/I7o0h0DAG4lkMwggdrMIIFU6ADAgECAgR5nlqfMA0GCSqGSIb3DQEBCwUA +MIGeMQswCQYDVQQGEwJERTEPMA0GA1UECAwGQmF5ZXJuMREwDwYDVQQHDAhNdWVuY2hlbjEQ +MA4GA1UECgwHU2llbWVuczERMA8GA1UEBRMIWlpaWlpaQTMxHTAbBgNVBAsMFFNpZW1lbnMg +VHJ1c3QgQ2VudGVyMScwJQYDVQQDDB5TaWVtZW5zIElzc3VpbmcgQ0EgRUUgRW5jIDIwMTYw +HhcNMTkwOTI3MDgwMTM5WhcNMjAwOTI3MDgwMTM3WjB3MREwDwYDVQQFEwhaMDAzSDA4VDEO +MAwGA1UEKgwFRGllZ28xGDAWBgNVBAQMD0xvdXphbiBNYXJ0aW5lejEYMBYGA1UECgwPU2ll +bWVucy1QYXJ0bmVyMR4wHAYDVQQDDBVMb3V6YW4gTWFydGluZXogRGllZ28wggEiMA0GCSqG +SIb3DQEBAQUAA4IBDwAwggEKAoIBAQCyby5qKzZIrGYWRqxnaAyMt/a/uc0uMk0F3MjwxvPM +vh5DllUpqx0l8ZDakDjPhlEXTeoL4DHNgmh+CDCs76CppM3cNG/1W1Ajo/L2iwMoXaxYuQ/F +q7ED+02KEkWX2DDVVG3fhrUGP20QAq77xPDptmVWZnUnuobZBNYkC49Xfl9HJvkJL8P0+Jqb +Eae7p4roiEr7wNkGriwrVXgA3oPNF/W+OuI76JTNTajS/6PAK/GeqIvLjfuBXpdBZTY031nE +Cztca8vI1jUjQzVhS+0dWpvpfhkVumbvOnid8DI9lapYsX8dpZFsa3ya+T3tjUdGSOOKi0kg +lWf/XYyyfhmDAgMBAAGjggLVMIIC0TAdBgNVHQ4EFgQUprhTCDwNLfPImpSfWdq+QvPTo9Mw +JwYDVR0RBCAwHoEcZGllZ28ubG91emFuLmV4dEBzaWVtZW5zLmNvbTAOBgNVHQ8BAf8EBAMC +BDAwLAYDVR0lBCUwIwYIKwYBBQUHAwQGCisGAQQBgjcKAwQGCysGAQQBgjcKAwQBMIHKBgNV +HR8EgcIwgb8wgbyggbmggbaGJmh0dHA6Ly9jaC5zaWVtZW5zLmNvbS9wa2k/WlpaWlpaQTMu +Y3JshkFsZGFwOi8vY2wuc2llbWVucy5uZXQvQ049WlpaWlpaQTMsTD1QS0k/Y2VydGlmaWNh +dGVSZXZvY2F0aW9uTGlzdIZJbGRhcDovL2NsLnNpZW1lbnMuY29tL0NOPVpaWlpaWkEzLG89 +VHJ1c3RjZW50ZXI/Y2VydGlmaWNhdGVSZXZvY2F0aW9uTGlzdDBFBgNVHSAEPjA8MDoGDSsG +AQQBoWkHAgIEAQMwKTAnBggrBgEFBQcCARYbaHR0cDovL3d3dy5zaWVtZW5zLmNvbS9wa2kv +MAwGA1UdEwEB/wQCMAAwHwYDVR0jBBgwFoAUoassbqB68NPCTeof8R4hivwMre8wggEEBggr +BgEFBQcBAQSB9zCB9DAyBggrBgEFBQcwAoYmaHR0cDovL2FoLnNpZW1lbnMuY29tL3BraT9a +WlpaWlpBMy5jcnQwQQYIKwYBBQUHMAKGNWxkYXA6Ly9hbC5zaWVtZW5zLm5ldC9DTj1aWlpa +WlpBMyxMPVBLST9jQUNlcnRpZmljYXRlMEkGCCsGAQUFBzAChj1sZGFwOi8vYWwuc2llbWVu +cy5jb20vQ049WlpaWlpaQTMsbz1UcnVzdGNlbnRlcj9jQUNlcnRpZmljYXRlMDAGCCsGAQUF +BzABhiRodHRwOi8vb2NzcC5wa2ktc2VydmljZXMuc2llbWVucy5jb20wDQYJKoZIhvcNAQEL +BQADggIBAF98ZMNg28LgkwdjOdvOGbC1QitsWjZTyotmQESF0nClDLUhb0O5675vVixntbrf +eB8xy1+KRiadk40GnAIJ0YzmNl4Tav6hPYv9VBWe5olsWG7C4qB3Q/SwhvW/e+owxv1cBra8 +R3oRudiN81eTZQHyNghRephVqQG/dpPYqydoANfIhEpHa79QlpaCAeYl4896AZOS8HYbkDFs +hLdv7sEHtl79YuSWI1wBjbJl70c0Sb4wLRgCPuHyQj2Uw/vQ5xJlEvBDZAIXXe1TP/nqiuY6 +7nweJbbeqfFE6ZP3kCe+mEIWGSaO0iThZyLGer8fHs1XiEmhhPgvC7P7KodzpXU6+hX+ZzbD +DxEjFfetV5sh0aNSXG9xx4hZmS9bpImBGR8MvZ7cgxqItvLtY2xvfUbYW244d4RcWesaCDq3 +ZEIo6uCIzOzJAwjUdLIac+lLV0rxiHmb7O3cQ19kjpWDB31hmfrus/TKJ55pBKVWBX5m/mFv +K8Ep5USpGrNS0EzOP7I1kQZv2VsvAhSxk/m5FMLpDy8T0O8YgbLypTXoeJFWCF6RduSjVsaZ +lkAtTQYud683pjyOMxJXaQUYGU1PmEYSOonMkVsT9aBcxYkXLp+Ln/+8G0OCYu7dRdwnj+Ut +7yR/ltxtgDcaFApCb0qBTKbgbqZk1fASmkOp+kbdYmoUMYICVjCCAlICAQEwgb8wgbYxCzAJ +BgNVBAYTAkRFMQ8wDQYDVQQIDAZCYXllcm4xETAPBgNVBAcMCE11ZW5jaGVuMRAwDgYDVQQK +DAdTaWVtZW5zMREwDwYDVQQFEwhaWlpaWlpBNjEdMBsGA1UECwwUU2llbWVucyBUcnVzdCBD +ZW50ZXIxPzA9BgNVBAMMNlNpZW1lbnMgSXNzdWluZyBDQSBNZWRpdW0gU3RyZW5ndGggQXV0 +aGVudGljYXRpb24gMjAxNgIEZ5a6PTANBglghkgBZQMEAgEFAKBpMC8GCSqGSIb3DQEJBDEi +BCAOR58AbNfSrI+vtMs+dgAQtn3IVZ3RjYC5hz3j9k+6TTAYBgkqhkiG9w0BCQMxCwYJKoZI +hvcNAQcBMBwGCSqGSIb3DQEJBTEPFw0yMDAyMTcyMTU2NDdaMA0GCSqGSIb3DQEBAQUABIIB +AHLSBcFHhNHPevbwqvA2ecuVb/aKnj45CFF6l8esP1H5DRm1ee5qMKuIS84NFuFC9RUENNhW +DBzsB+BVGz64o1f8QgIklYVrIJ4JZ0q1abNG7NbkVKWIpS3CQo//YWShUTYg+JpKx4YbahGR +sP5zbufbU4eagrrqBChjPTLy+njdjwCNu0XPykBTKOOf6BMjnS33AYjHJyh83JOY7rw3IDLx +8POQH4g5EMRpl9354s0rEkIezMt7pfUAsqY3QnQ8hvlE4KTikPQ+tvLMK1l/ffcLAP8BdBNI +YA3ikb3qCoGNSLKieYzNnBPhNOIJELUtEEaljAFZYMQzMKCbI4JdiDs= + +--B_3664825007_1904734766-- 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(` +
+
+
+ +
MOCK CONTENT
+
+
+
+`); + 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(`
diff --git a/spec/helpers/merge_requests_helper_spec.rb b/spec/helpers/merge_requests_helper_spec.rb index 3a06e7ad08..55ca6a95d3 100644 --- a/spec/helpers/merge_requests_helper_spec.rb +++ b/spec/helpers/merge_requests_helper_spec.rb @@ -153,10 +153,18 @@ RSpec.describe MergeRequestsHelper, feature_category: :code_review_workflow do end describe '#merge_request_source_branch' do - let_it_be(:project) { create(:project) } - let(:forked_project) { fork_project(project) } - let(:merge_request_forked) { create(:merge_request, source_project: forked_project, target_project: project) } + let(:malicious_branch_name) { 'name' } + let(:project) { create(:project) } let(:merge_request) { create(:merge_request, source_project: project, target_project: project) } + let(:forked_project) { fork_project(project) } + let(:merge_request_forked) do + create( + :merge_request, + source_project: forked_project, + source_branch: malicious_branch_name, + target_project: project + ) + end context 'when merge request is a fork' do subject { merge_request_source_branch(merge_request_forked) } @@ -164,6 +172,10 @@ RSpec.describe MergeRequestsHelper, feature_category: :code_review_workflow do it 'does show the fork icon' do expect(subject).to match(/fork/) end + + it 'escapes properly' do + expect(subject).to include(html_escape(malicious_branch_name)) + end end context 'when merge request is not a fork' do diff --git a/spec/lib/api/entities/issue_spec.rb b/spec/lib/api/entities/issue_spec.rb new file mode 100644 index 0000000000..1588654257 --- /dev/null +++ b/spec/lib/api/entities/issue_spec.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ::API::Entities::Issue, feature_category: :team_planning do + let_it_be(:project) { create(:project) } + let(:issue) { build_stubbed(:issue, project: project) } + let(:current_user) { build_stubbed(:user) } + let(:options) { { current_user: current_user }.merge(option_addons) } + let(:option_addons) { {} } + let(:entity) { described_class.new(issue, options) } + + subject(:json) { entity.as_json } + + describe '#service_desk_reply_to', feature_category: :service_desk do + # Setting to true (default) doesn't play nice with stubs + let(:option_addons) { { include_subscribed: false } } + let(:issue) { build_stubbed(:issue, project: project, service_desk_reply_to: email) } + let(:email) { 'creator@example.com' } + let(:role) { :developer } + + subject { json[:service_desk_reply_to] } + + context 'as developer' do + before do + stub_member_access_level(issue.project, developer: current_user) + end + + it { is_expected.to eq(email) } + end + + context 'as guest' do + before do + stub_member_access_level(issue.project, guest: current_user) + end + + it { is_expected.to eq('cr*****@e*****.c**') } + end + + context 'without email' do + let(:email) { nil } + + specify { expect(json).to have_key(:service_desk_reply_to) } + it { is_expected.to eq(nil) } + end + end +end 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 = "START {+something added+} END" 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 50784d3e42..f59529364c 100644 --- a/spec/lib/banzai/filter/math_filter_spec.rb +++ b/spec/lib/banzai/filter/math_filter_spec.rb @@ -215,6 +215,14 @@ RSpec.describe Banzai::Filter::MathFilter, feature_category: :team_planning do expect(doc.search('.js-render-math').count).to eq(2) end + it 'protects against malicious backtracking' do + doc = pipeline_filter("$$#{' ' * 1_000_000}$") + + expect do + Timeout.timeout(3.seconds) { filter(doc) } + end.not_to raise_error + end + def pipeline_filter(text) context = { project: nil, no_sourcepos: true } doc = Banzai::Pipeline::PreProcessPipeline.call(text, {}) diff --git a/spec/lib/bitbucket_server/representation/pull_request_spec.rb b/spec/lib/bitbucket_server/representation/pull_request_spec.rb index 5312bc1d71..c5d0ee8908 100644 --- a/spec/lib/bitbucket_server/representation/pull_request_spec.rb +++ b/spec/lib/bitbucket_server/representation/pull_request_spec.rb @@ -115,6 +115,7 @@ RSpec.describe BitbucketServer::Representation::PullRequest, feature_category: : author: "root", description: "Test", source_branch_name: "refs/heads/root/CODE_OF_CONDUCTmd-1530600625006", + source_branch_sha: "074e2b4dddc5b99df1bf9d4a3f66cfc15481fdc8", target_branch_name: "refs/heads/master", target_branch_sha: "839fa9a2d434eb697815b8fcafaecc51accfdbbc", title: "Added a new line" 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/config/entry/includes_spec.rb b/spec/lib/gitlab/ci/config/entry/includes_spec.rb new file mode 100644 index 0000000000..f1f28c24e7 --- /dev/null +++ b/spec/lib/gitlab/ci/config/entry/includes_spec.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' +require_dependency 'active_model' + +RSpec.describe ::Gitlab::Ci::Config::Entry::Includes, feature_category: :pipeline_composition do + subject(:include_entry) { described_class.new(config) } + + describe '#initialize' do + let(:config) { 'test.yml' } + + it 'does not increase aspects' do + 2.times { expect { described_class.new(config) }.not_to change { described_class.aspects.count } } + 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 4166eba4e8..d8b8903c8c 100644 --- a/spec/lib/gitlab/import_export/project/tree_saver_spec.rb +++ b/spec/lib/gitlab/import_export/project/tree_saver_spec.rb @@ -7,6 +7,8 @@ RSpec.describe Gitlab::ImportExport::Project::TreeSaver, :with_license, feature_ let_it_be(:exportable_path) { 'project' } let_it_be(:user) { create(:user) } let_it_be(:group) { create(:group) } + let_it_be(:private_project) { create(:project, :private, group: group) } + let_it_be(:private_mr) { create(:merge_request, source_project: private_project, project: private_project) } let_it_be(:project) { setup_project } shared_examples 'saves project tree successfully' do @@ -118,6 +120,13 @@ RSpec.describe Gitlab::ImportExport::Project::TreeSaver, :with_license, feature_ expect(reviewer).not_to be_nil expect(reviewer['user_id']).to eq(user.id) end + + it 'has merge requests system notes' do + system_notes = subject.first['notes'].select { |note| note['system'] } + + expect(system_notes.size).to eq(1) + expect(system_notes.first['note']).to eq('merged') + end end context 'with snippets' do @@ -492,6 +501,9 @@ RSpec.describe Gitlab::ImportExport::Project::TreeSaver, :with_license, feature_ create(:milestone, project: project) discussion_note = create(:discussion_note, noteable: issue, project: project) mr_note = create(:note, noteable: merge_request, project: project) + create(:system_note, noteable: merge_request, project: project, author: user, note: 'merged') + private_system_note = "mentioned in merge request #{private_mr.to_reference(project)}" + create(:system_note, noteable: merge_request, project: project, author: user, note: private_system_note) create(:note, noteable: snippet, project: project) create(:note_on_commit, author: user, diff --git a/spec/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..4f36fa52c6 100644 --- a/spec/models/concerns/exportable_spec.rb +++ b/spec/models/concerns/exportable_spec.rb @@ -9,6 +9,7 @@ RSpec.describe Exportable, feature_category: :importers do let_it_be(:issue) { create(:issue, project: project, milestone: milestone) } let_it_be(:note1) { create(:system_note, project: project, noteable: issue) } let_it_be(:note2) { create(:system_note, project: project, noteable: issue) } + let_it_be(:options) { { include: [{ notes: { only: [:note] }, milestone: { only: :title } }] } } let_it_be(:model_klass) do Class.new(ApplicationRecord) do @@ -28,19 +29,27 @@ RSpec.describe Exportable, feature_category: :importers do subject { model_klass.new } - describe '.readable_records' do + describe '.to_authorized_json' do let_it_be(:model_record) { model_klass.new } - context 'when model does not respond to association name' do - it 'returns nil' do - expect(subject.readable_records(:foo, current_user: user)).to be_nil + context 'when key to authorize is not an association name' do + it 'returns string without given key' do + expect(subject.to_authorized_json([:foo], user, options)).not_to include('foo') end end - context 'when model does respond to association name' do + context 'when key to authorize is an association name' do + let(:key_to_authorize) { :notes } + + subject(:record_json) { model_record.to_authorized_json([key_to_authorize], user, options) } + context 'when there are no records' do - it 'returns nil' do - expect(model_record.readable_records(:notes, current_user: user)).to be_nil + before do + allow(model_record).to receive(:notes).and_return(Note.none) + end + + it 'returns string including the empty association' do + expect(record_json).to include("\"notes\":[]") end end @@ -57,8 +66,9 @@ RSpec.describe Exportable, feature_category: :importers do end end - it 'returns collection of readable records' do - expect(model_record.readable_records(:notes, current_user: user)).to contain_exactly(note1, note2) + it 'returns string containing all records' do + expect(record_json) + .to include("\"notes\":[{\"note\":\"#{note1.note}\"},{\"note\":\"#{note2.note}\"}]") end end @@ -70,8 +80,19 @@ RSpec.describe Exportable, feature_category: :importers do end end - it 'returns collection of readable records' do - expect(model_record.readable_records(:notes, current_user: user)).to eq([]) + it 'returns string including the empty association' do + expect(record_json).to include("\"notes\":[]") + end + end + + context 'when user can read some records' do + before do + allow(model_record).to receive(:readable_records).with(:notes, current_user: user) + .and_return([note1]) + end + + it 'returns string containing readable records only' do + expect(record_json).to include("\"notes\":[{\"note\":\"#{note1.note}\"}]") end end end @@ -87,13 +108,15 @@ RSpec.describe Exportable, feature_category: :importers do it 'calls #readable_by?' do expect(note1).to receive(:readable_by?).with(user) - model_record.readable_records(:notes, current_user: user) + record_json end end context 'with single relation' do + let(:key_to_authorize) { :milestone } + before do - allow(model_record).to receive(:try).with(:milestone).and_return(issue.milestone) + allow(model_record).to receive(:milestone).and_return(issue.milestone) end context 'when user can read the record' do @@ -101,8 +124,8 @@ RSpec.describe Exportable, feature_category: :importers do allow(milestone).to receive(:readable_by?).with(user).and_return(true) end - it 'returns collection of readable records' do - expect(model_record.readable_records(:milestone, current_user: user)).to eq(milestone) + it 'returns string including association' do + expect(record_json).to include("\"milestone\":{\"title\":\"#{milestone.title}\"}") end end @@ -111,8 +134,8 @@ RSpec.describe Exportable, feature_category: :importers do allow(milestone).to receive(:readable_by?).with(user).and_return(false) end - it 'returns collection of readable records' do - expect(model_record.readable_records(:milestone, current_user: user)).to be_nil + it 'returns string with null association' do + expect(record_json).to include("\"milestone\":null") end end end @@ -211,26 +234,4 @@ RSpec.describe Exportable, feature_category: :importers do end end end - - describe '.has_many_association?' do - let(:model_associations) { [:notes, :labels] } - - context 'when association type is `has_many`' do - it 'returns true' do - expect(subject.has_many_association?(:notes)).to eq(true) - end - end - - context 'when association type is `has_one`' do - it 'returns true' do - expect(subject.has_many_association?(:milestone)).to eq(false) - end - end - - context 'when association type is `belongs_to`' do - it 'returns true' do - expect(subject.has_many_association?(:project)).to eq(false) - end - end - end end diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb index ea6baac455..5330279e7a 100644 --- a/spec/models/concerns/issuable_spec.rb +++ b/spec/models/concerns/issuable_spec.rb @@ -1081,4 +1081,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/hooks/web_hook_spec.rb b/spec/models/hooks/web_hook_spec.rb index 254b8c2520..4c7317de90 100644 --- a/spec/models/hooks/web_hook_spec.rb +++ b/spec/models/hooks/web_hook_spec.rb @@ -258,6 +258,13 @@ RSpec.describe WebHook, feature_category: :integrations do expect(hook.url_variables).to eq({}) end + it 'resets url variables if url variables are overwritten' do + hook.url_variables = hook.url_variables.merge('abc' => 'baz') + + expect(hook).not_to be_valid + expect(hook.url_variables).to eq({}) + end + it 'does not reset url variables if both url and url variables are changed' do hook.url = 'http://example.com/{one}/{two}' hook.url_variables = { 'one' => 'foo', 'two' => 'bar' } 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 20dae05664..67af7a049c 100644 --- a/spec/models/members/project_member_spec.rb +++ b/spec/models/members/project_member_spec.rb @@ -106,36 +106,6 @@ RSpec.describe ProjectMember do end end - describe '.import_team' do - before do - @project_1 = create(:project) - @project_2 = create(:project) - - @user_1 = create :user - @user_2 = create :user - - @project_1.add_developer(@user_1) - @project_2.add_reporter(@user_2) - - @status = @project_2.team.import(@project_1) - end - - it { expect(@status).to be_truthy } - - describe 'project 2 should get user 1 as developer. user_2 should not be changed' do - it { expect(@project_2.users).to include(@user_1) } - it { expect(@project_2.users).to include(@user_2) } - - it { expect(Ability.allowed?(@user_1, :create_project, @project_2)).to be_truthy } - it { expect(Ability.allowed?(@user_2, :read_project, @project_2)).to be_truthy } - end - - describe 'project 1 should not be changed' do - it { expect(@project_1.users).to include(@user_1) } - it { expect(@project_1.users).not_to include(@user_2) } - end - end - describe '.truncate_teams' do before do @project_1 = create(:project) diff --git a/spec/models/project_team_spec.rb b/spec/models/project_team_spec.rb index 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 bc677aca0f..89f0a48c73 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -668,34 +668,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 @@ -3461,15 +3543,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 4b81394aea..0b9cf24230 100644 --- a/spec/requests/abuse_reports_controller_spec.rb +++ b/spec/requests/abuse_reports_controller_spec.rb @@ -19,43 +19,6 @@ RSpec.describe AbuseReportsController, feature_category: :insider_threat do sign_in(reporter) end - describe 'GET new' do - let(:ref_url) { 'http://example.com' } - - it 'sets the instance variables' do - get new_abuse_report_path(user_id: user.id, ref_url: ref_url) - - expect(assigns(:abuse_report)).to be_kind_of(AbuseReport) - expect(assigns(:abuse_report)).to have_attributes( - user_id: user.id, - reported_from_url: ref_url - ) - end - - context 'when the user has already been deleted' do - it 'redirects the reporter to root_path' do - user_id = user.id - user.destroy! - - get new_abuse_report_path(user_id: user_id) - - expect(response).to redirect_to root_path - expect(flash[:alert]).to eq(_('Cannot create the abuse report. The user has been deleted.')) - end - end - - context 'when the user has already been blocked' do - it 'redirects the reporter to the user\'s profile' do - user.block - - get new_abuse_report_path(user_id: user.id) - - expect(response).to redirect_to user - expect(flash[:alert]).to eq(_('Cannot create the abuse report. This user has been blocked.')) - end - end - end - describe 'POST add_category', :aggregate_failures do subject(:request) { post add_category_abuse_reports_path, params: request_params } diff --git a/spec/requests/api/npm_instance_packages_spec.rb b/spec/requests/api/npm_instance_packages_spec.rb index 591a8ee68d..97de7fa9e5 100644 --- a/spec/requests/api/npm_instance_packages_spec.rb +++ b/spec/requests/api/npm_instance_packages_spec.rb @@ -13,11 +13,12 @@ RSpec.describe API::NpmInstancePackages, feature_category: :package_registry do describe 'GET /api/v4/packages/npm/*package_name' do let(:url) { api("/packages/npm/#{package_name}") } + subject { get(url) } + it_behaves_like 'handling get metadata requests', scope: :instance + it_behaves_like 'rejects invalid package names' context 'with a duplicate package name in another project' do - subject { get(url) } - let_it_be(:project2) { create(:project, :public, namespace: namespace) } let_it_be(:package2) do create(:npm_package, diff --git a/spec/requests/api/npm_project_packages_spec.rb b/spec/requests/api/npm_project_packages_spec.rb index f621af5d96..91912e75ff 100644 --- a/spec/requests/api/npm_project_packages_spec.rb +++ b/spec/requests/api/npm_project_packages_spec.rb @@ -23,6 +23,9 @@ RSpec.describe API::NpmProjectPackages, feature_category: :package_registry do it_behaves_like 'handling get metadata requests', scope: :project it_behaves_like 'accept get request on private project with access to package registry for everyone' + it_behaves_like 'rejects invalid package names' do + subject { get(url) } + end end describe 'GET /api/v4/projects/:id/packages/npm/-/package/*package_name/dist-tags' do diff --git a/spec/requests/lfs_http_spec.rb b/spec/requests/lfs_http_spec.rb index e5f03d16dd..b07296a0df 100644 --- a/spec/requests/lfs_http_spec.rb +++ b/spec/requests/lfs_http_spec.rb @@ -323,6 +323,62 @@ RSpec.describe 'Git LFS API and storage', feature_category: :source_code_managem it_behaves_like 'process authorization header', renew_authorization: renew_authorization end + + context 'when downloading an LFS object that is stored on object storage' do + before do + stub_lfs_object_storage + lfs_object.file.migrate!(LfsObjectUploader::Store::REMOTE) + end + + context 'when lfs.object_store.proxy_download=true' do + before do + stub_lfs_object_storage(proxy_download: true) + end + + it_behaves_like 'LFS http 200 response' + + it 'does return proxied address URL' do + expect(json_response['objects'].first).to include(sample_object) + expect(json_response['objects'].first['actions']['download']['href']).to eq(objects_url(project, sample_oid)) + end + end + + context 'when "lfs.object_store.proxy_download" is "false"' do + before do + stub_lfs_object_storage(proxy_download: false) + end + + it_behaves_like 'LFS http 200 response' + + it 'does return direct object storage URL' do + expect(json_response['objects'].first).to include(sample_object) + expect(json_response['objects'].first['actions']['download']['href']).to start_with("https://lfs-objects.s3.amazonaws.com/") + expect(json_response['objects'].first['actions']['download']['href']).to include("X-Amz-Expires=3600&") + end + + context 'when feature flag "lfs_batch_direct_downloads" is "false"' do + before do + stub_feature_flags(lfs_batch_direct_downloads: false) + end + + it_behaves_like 'LFS http 200 response' + + it 'does return proxied address URL' do + expect(json_response['objects'].first).to include(sample_object) + expect(json_response['objects'].first['actions']['download']['href']).to eq(objects_url(project, sample_oid)) + end + end + end + end + + context 'when sending objects=[]' do + let(:body) { download_body([]) } + + it_behaves_like 'LFS http expected response code and message' do + let(:response_code) { 404 } + let(:message) { 'Not found.' } + end + end end context 'when user is authenticated' 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/shared_examples/features/reportable_note_shared_examples.rb b/spec/support/shared_examples/features/reportable_note_shared_examples.rb index 45ad4d5cf7..06568bf0ac 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..57e231e4a6 100644 --- a/spec/support/shared_examples/models/exportable_shared_examples.rb +++ b/spec/support/shared_examples/models/exportable_shared_examples.rb @@ -1,27 +1,28 @@ # frozen_string_literal: true -RSpec.shared_examples 'resource with exportable associations' do - before do - stub_licensed_features(stubbed_features) if stubbed_features.any? - end +RSpec.shared_examples 'an exportable' do |restricted_association: :project| + let_it_be(:user) { create(:user) } describe '#exportable_association?' do - let(:association) { single_association } + let(:association) { restricted_association } subject { resource.exportable_association?(association, current_user: user) } it { is_expected.to be_falsey } - context 'when user can read resource' do + context 'when user can only read resource' do before do - group.add_developer(user) + allow(Ability).to receive(:allowed?).and_call_original + allow(Ability).to receive(:allowed?) + .with(user, :"read_#{resource.to_ability_name}", resource) + .and_return(true) end it { is_expected.to be_falsey } context "when user can read resource's association" do before do - other_group.add_developer(user) + allow(resource).to receive(:readable_record?).with(anything, user).and_return(true) end it { is_expected.to be_truthy } @@ -31,41 +32,48 @@ RSpec.shared_examples 'resource with exportable associations' do it { is_expected.to be_falsey } end - - context 'for an unauthenticated user' do - let(:user) { nil } - - it { is_expected.to be_falsey } - end end end end - describe '#readable_records' do - subject { resource.readable_records(association, current_user: user) } + describe '#to_authorized_json' do + let(:options) { { include: [{ notes: { only: [:id] } }] } } + + subject { resource.to_authorized_json(keys, user, options) } before do - group.add_developer(user) + allow(Ability).to receive(:allowed?).and_call_original + allow(Ability).to receive(:allowed?) + .with(user, :"read_#{resource.to_ability_name}", resource) + .and_return(true) end context 'when association not supported' do - let(:association) { :foo } + let(:keys) { [:foo] } - it { is_expected.to be_nil } + it { is_expected.not_to include('foo') } end context 'when association is `:notes`' do - let(:association) { :notes } + let_it_be(:readable_note) { create(:system_note, noteable: resource, project: project, note: 'readable') } + let_it_be(:restricted_note) { create(:system_note, noteable: resource, project: project, note: 'restricted') } - it { is_expected.to match_array([readable_note]) } + let(:restricted_note_access) { false } + let(:keys) { [:notes] } - context 'when user have access' do - before do - other_group.add_developer(user) - end + before do + allow(Ability).to receive(:allowed?).and_call_original + allow(Ability).to receive(:allowed?).with(user, :read_note, readable_note).and_return(true) + allow(Ability).to receive(:allowed?).with(user, :read_note, restricted_note).and_return(restricted_note_access) + end - it 'returns all records' do - is_expected.to match_array([readable_note, restricted_note]) + 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}}]") 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 f53532d00d..f430db6197 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 @@ -849,3 +849,14 @@ RSpec.shared_examples 'handling different package names, visibilities and user r it_behaves_like example_name, status: status end end + +RSpec.shared_examples 'rejects invalid package names' do + let(:package_name) { "%0d%0ahttp:/%2fexample.com" } + + it do + subject + + expect(response).to have_gitlab_http_status(:bad_request) + expect(Gitlab::Json.parse(response.body)).to eq({ 'error' => 'package_name should be a valid file path' }) + end +end diff --git a/spec/tasks/gitlab/db/migration_fix_15_11_rake_spec.rb b/spec/tasks/gitlab/db/migration_fix_15_11_rake_spec.rb new file mode 100644 index 0000000000..3ff07698ad --- /dev/null +++ b/spec/tasks/gitlab/db/migration_fix_15_11_rake_spec.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +require 'rake_helper' + +RSpec.describe 'migration_fix_15_11', :reestablished_active_record_base, feature_category: :database do + let(:db) { ApplicationRecord.connection } + let(:target_init_schema) { '20220314184009' } + let(:earlier_init_schema) { '20210101010101' } + + before :all do + Rake.application.rake_require 'active_record/railties/databases' + Rake.application.rake_require 'tasks/gitlab/db/migration_fix_15_11' + + Rake::Task.define_task :environment + end + + describe 'migration_fix_15_11' do + context 'when fix is needed' do + it 'patches init_schema' do + db.execute('DELETE FROM schema_migrations') + db.execute("INSERT INTO schema_migrations (version) VALUES ('#{target_init_schema}')") + run_rake_task(:migration_fix_15_11) + result = db.execute('SELECT * FROM schema_migrations') + expect(result.count).to eq(300) + end + end + + context 'when fix is not needed because no migrations have been run' do + it 'does nothing' do + db.execute('DELETE FROM schema_migrations') + run_rake_task(:migration_fix_15_11) + result = db.execute('SELECT * FROM schema_migrations') + expect(result.count).to eq(0) + end + end + + context 'when fix is not needed because DB has not been initialized' do + it 'does nothing' do + db.execute('DROP TABLE schema_migrations') + expect { run_rake_task(:migration_fix_15_11) }.not_to raise_error + end + end + + context 'when fix is not needed because there is an earlier init_schema' do + it 'does nothing' do + db.execute('DELETE FROM schema_migrations') + db.execute("INSERT INTO schema_migrations (version) VALUES ('#{earlier_init_schema}')") + run_rake_task(:migration_fix_15_11) + result = db.execute('SELECT * FROM schema_migrations') + expect(result.pluck('version')).to match_array [earlier_init_schema] + end + end + + context 'when fix is not needed because the fix has been run already' do + it 'does not affect the schema_migrations table' do + db.execute('DELETE FROM schema_migrations') + db.execute("INSERT INTO schema_migrations (version) VALUES ('#{target_init_schema}')") + run_rake_task(:migration_fix_15_11) + fixed_table = db.execute('SELECT version FROM schema_migrations').pluck('version') + run_rake_task(:migration_fix_15_11) + test_fixed_table = db.execute('SELECT version FROM schema_migrations').pluck('version') + expect(fixed_table).to match_array test_fixed_table + end + end + end +end