diff options
author | GitLab Release Tools Bot <delivery-team+release-tools@gitlab.com> | 2023-08-01 19:49:37 +0300 |
---|---|---|
committer | GitLab Release Tools Bot <delivery-team+release-tools@gitlab.com> | 2023-08-01 19:49:37 +0300 |
commit | 806bc80d8a8ca536ad4cd598eb6f47b2d396ee04 (patch) | |
tree | c2d8dbedb20c847aee04aff7abbf5a759ccba32a | |
parent | 920cbcb38960331cd5e9315e63b2d808fd1a1e48 (diff) | |
parent | 9a102b1b08b83bb415912be95f8264804680ade2 (diff) |
Merge remote-tracking branch 'dev/16-2-stable' into 16-2-stable
69 files changed, 1136 insertions, 252 deletions
diff --git a/.rubocop_todo/rspec/missing_feature_category.yml b/.rubocop_todo/rspec/missing_feature_category.yml index 0a559874217..cdd58e0c046 100644 --- a/.rubocop_todo/rspec/missing_feature_category.yml +++ b/.rubocop_todo/rspec/missing_feature_category.yml @@ -5088,7 +5088,6 @@ RSpec/MissingFeatureCategory: - 'spec/policies/ci/bridge_policy_spec.rb' - 'spec/policies/ci/build_policy_spec.rb' - 'spec/policies/ci/pipeline_policy_spec.rb' - - 'spec/policies/ci/pipeline_schedule_policy_spec.rb' - 'spec/policies/ci/trigger_policy_spec.rb' - 'spec/policies/clusters/agent_policy_spec.rb' - 'spec/policies/clusters/agent_token_policy_spec.rb' diff --git a/CHANGELOG.md b/CHANGELOG.md index f3636bb8a6d..27b39717ea2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,37 @@ documentation](doc/development/changelog.md) for instructions on adding your own entry. +## 16.2.2 (2023-08-01) + +### Added (1 change) + +- [Add MR reviewers to BitBucketServer import to 16-2](gitlab-org/security/gitlab@aeb33292029aae649352dea089d9e86933e01a80) + +### Fixed (2 changes) + +- [Disable IAT verification by default](gitlab-org/security/gitlab@b3a0c02721101596c644443d412ca13e8f4ce000) +- [Enable descendant_security_scans by default](gitlab-org/security/gitlab@66eaaabed118b3b4b75fca17ef13e56b64e4eb4b) **GitLab Enterprise Edition** + +### Security (17 changes) + +- [Fix undefined method licenses for nil:NilClass bug](gitlab-org/security/gitlab@aa4c4dc26a239e7799f9e9aa14d893c7a696d112) ([merge request](gitlab-org/security/gitlab!3471)) +- [Fix undefined method page error in list dependencies](gitlab-org/security/gitlab@08acd6aa91d34de2403e1d2a28b437c58af107c1) ([merge request](gitlab-org/security/gitlab!3470)) +- [Add pagination for license scanning](gitlab-org/security/gitlab@b58ed3a7c40dd08ab0fe48c0cc4386e1cb7fa48a) ([merge request](gitlab-org/security/gitlab!3467)) +- [Prevent leaking emails of newly created users](gitlab-org/security/gitlab@25d75bb2494dffb7e2b55f3b9d190a7302461fe1) ([merge request](gitlab-org/security/gitlab!3449)) +- [Added redirect to filtered params](gitlab-org/security/gitlab@a72a1d48e871716ebeae3a4082078d4626cab8a0) ([merge request](gitlab-org/security/gitlab!3441)) +- [Relocate PlantUML config and disable SVG support](gitlab-org/security/gitlab@6aac3a3e7223cbb85a62d6e95cf096e8a582cfcf) ([merge request](gitlab-org/security/gitlab!3438)) +- [Sanitize multiple hardlinks from import archives](gitlab-org/security/gitlab@286c5b4e79f1a94554ee20b2535376d7c1c329a8) ([merge request](gitlab-org/security/gitlab!3435)) +- [Validates project path availability](gitlab-org/security/gitlab@d970c230a0dc1113de469e4636bac020fa7cdeac) ([merge request](gitlab-org/security/gitlab!3426)) +- [Fix policy project assign](gitlab-org/security/gitlab@5564547ac37f5f80c58f444778bdaf3e3a491ff7) ([merge request](gitlab-org/security/gitlab!3423)) +- [Fix bug where comments on files with incorrect sha breaks UI](gitlab-org/security/gitlab@da3ff8cc07ec7449f8bf1092ad1219fc2debbe53) ([merge request](gitlab-org/security/gitlab!3446)) +- [Fix pipeline schedule authorization for protected branch/tag](gitlab-org/security/gitlab@0d9a0ab46faa06cc33144096382f1375f95e91bd) ([merge request](gitlab-org/security/gitlab!3413)) +- [Mitigate autolink filter ReDOS](gitlab-org/security/gitlab@f8d9f5fa2617f1e372c6e477bc3840fd2161d7a7) ([merge request](gitlab-org/security/gitlab!3434)) +- [Fix XSS vector in Web IDE](gitlab-org/security/gitlab@530b97903782b2108284d47e33421e21f576ec8e) ([merge request](gitlab-org/security/gitlab!3409)) +- [Mitigate project reference filter ReDOS](gitlab-org/security/gitlab@93f7983d73955244994a131f7093ab8699347eb6) ([merge request](gitlab-org/security/gitlab!3431)) +- [Add a stricter regex for the Harbor search param](gitlab-org/security/gitlab@988280e72ac7570825e7451c528b718d38be681c) ([merge request](gitlab-org/security/gitlab!3408)) +- [Update pipeline user to the last policy MR author](gitlab-org/security/gitlab@85f3797d29aa957df88b4f8eaf47ec102021dd1a) ([merge request](gitlab-org/security/gitlab!3421)) +- [Prohibit 40 character hex plus a hyphen if branch name is path](gitlab-org/security/gitlab@14fabf71a196dcfa6af566084f0b5266be4bfe2d) ([merge request](gitlab-org/security/gitlab!3405)) + ## 16.2.1 (2023-07-25) ### Fixed (1 change) diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index e9c8486a0ff..c70abaa3d70 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -16.2.1
\ No newline at end of file +16.2.2
\ No newline at end of file diff --git a/GITLAB_PAGES_VERSION b/GITLAB_PAGES_VERSION index e9c8486a0ff..c70abaa3d70 100644 --- a/GITLAB_PAGES_VERSION +++ b/GITLAB_PAGES_VERSION @@ -1 +1 @@ -16.2.1
\ No newline at end of file +16.2.2
\ No newline at end of file @@ -1 +1 @@ -16.2.1
\ No newline at end of file +16.2.2
\ No newline at end of file diff --git a/app/assets/javascripts/diffs/store/mutations.js b/app/assets/javascripts/diffs/store/mutations.js index 4855ca87e91..f90e0a24d0e 100644 --- a/app/assets/javascripts/diffs/store/mutations.js +++ b/app/assets/javascripts/diffs/store/mutations.js @@ -167,7 +167,7 @@ export default { originalStartLineCode, ...(discussion.line_codes || []), ]; - const fileHash = discussion.diff_file.file_hash; + const fileHash = discussion.diff_file?.file_hash; const lineCheck = (line) => discussionLineCodes.some( (discussionLineCode) => diff --git a/app/assets/javascripts/diffs/utils/diff_file.js b/app/assets/javascripts/diffs/utils/diff_file.js index f2a3224d332..98e1c1cc849 100644 --- a/app/assets/javascripts/diffs/utils/diff_file.js +++ b/app/assets/javascripts/diffs/utils/diff_file.js @@ -77,7 +77,7 @@ export function prepareRawDiffFile({ file, allFiles, meta = false, index = -1 }) } export function collapsedType(file) { - const isManual = typeof file.viewer?.manuallyCollapsed === 'boolean'; + const isManual = typeof file?.viewer?.manuallyCollapsed === 'boolean'; return isManual ? DIFF_FILE_MANUAL_COLLAPSE : DIFF_FILE_AUTOMATIC_COLLAPSE; } @@ -85,8 +85,8 @@ export function collapsedType(file) { export function isCollapsed(file) { const type = collapsedType(file); const collapsedStates = { - [DIFF_FILE_AUTOMATIC_COLLAPSE]: file.viewer?.automaticallyCollapsed || false, - [DIFF_FILE_MANUAL_COLLAPSE]: file.viewer?.manuallyCollapsed, + [DIFF_FILE_AUTOMATIC_COLLAPSE]: file?.viewer?.automaticallyCollapsed || false, + [DIFF_FILE_MANUAL_COLLAPSE]: file?.viewer?.manuallyCollapsed, }; return collapsedStates[type]; diff --git a/app/assets/javascripts/notes/components/diff_with_note.vue b/app/assets/javascripts/notes/components/diff_with_note.vue index db32079e6b9..b1a2ab77fa8 100644 --- a/app/assets/javascripts/notes/components/diff_with_note.vue +++ b/app/assets/javascripts/notes/components/diff_with_note.vue @@ -41,7 +41,7 @@ export default { return getDiffMode(this.discussion.diff_file); }, diffViewerMode() { - return this.discussion.diff_file.viewer.name; + return this.discussion.diff_file?.viewer.name; }, fileDiffRefs() { return this.discussion.diff_file.diff_refs; @@ -96,6 +96,7 @@ export default { <template> <div :class="{ 'text-file': isTextFile }" class="diff-file file-holder"> <diff-file-header + v-if="discussion.diff_file" :discussion-path="discussion.discussion_path" :diff-file="discussion.diff_file" :can-current-user-fork="false" diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue index a5939e1023c..7e79edfea15 100644 --- a/app/assets/javascripts/notes/components/noteable_discussion.vue +++ b/app/assets/javascripts/notes/components/noteable_discussion.vue @@ -169,7 +169,7 @@ export default { return !this.discussionResolved ? this.discussion.resolve_with_issue_path : ''; }, canShowReplyActions() { - if (this.shouldRenderDiffs && !this.discussion.diff_file.diff_refs) { + if (this.shouldRenderDiffs && !this.discussion.diff_file?.diff_refs) { return false; } diff --git a/app/controllers/projects/pipeline_schedules_controller.rb b/app/controllers/projects/pipeline_schedules_controller.rb index 4fd307b5105..96c9aa89953 100644 --- a/app/controllers/projects/pipeline_schedules_controller.rb +++ b/app/controllers/projects/pipeline_schedules_controller.rb @@ -21,7 +21,6 @@ class Projects::PipelineSchedulesController < Projects::ApplicationController end def new - @schedule = project.pipeline_schedules.new end def create @@ -113,6 +112,15 @@ class Projects::PipelineSchedulesController < Projects::ApplicationController variables_attributes: [:id, :variable_type, :key, :secret_value, :_destroy]) end + def new_schedule + # We need the `ref` here for `authorize_create_pipeline_schedule!` + @schedule ||= project.pipeline_schedules.new(ref: params.dig(:schedule, :ref)) + end + + def authorize_create_pipeline_schedule! + return access_denied! unless can?(current_user, :create_pipeline_schedule, new_schedule) + end + def authorize_play_pipeline_schedule! return access_denied! unless can?(current_user, :play_pipeline_schedule, schedule) end diff --git a/app/models/project.rb b/app/models/project.rb index 931f4db3a54..8959eccbd1f 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -579,6 +579,8 @@ class Project < ApplicationRecord validates :max_artifacts_size, numericality: { only_integer: true, greater_than: 0, allow_nil: true } validates :suggestion_commit_message, length: { maximum: MAX_SUGGESTIONS_TEMPLATE_LENGTH } + validate :path_availability, if: :path_changed? + # Scopes scope :pending_delete, -> { where(pending_delete: true) } scope :without_deleted, -> { where(pending_delete: false) } @@ -3221,6 +3223,15 @@ class Project < ApplicationRecord group.crm_enabled? end + def path_availability + base, _, host = path.partition('.') + + return unless host == Gitlab.config.pages&.dig('host') + return unless ProjectSetting.where(pages_unique_domain: base).exists? + + errors.add(:path, s_('Project|already in use')) + end + private # overridden in EE diff --git a/app/models/project_setting.rb b/app/models/project_setting.rb index 7ca74d4e970..aeefa5c8dcd 100644 --- a/app/models/project_setting.rb +++ b/app/models/project_setting.rb @@ -59,6 +59,8 @@ class ProjectSetting < ApplicationRecord validate :validates_mr_default_target_self + validate :pages_unique_domain_availability, if: :pages_unique_domain_changed? + attribute :legacy_open_source_license_available, default: -> do Feature.enabled?(:legacy_open_source_license_available, type: :ops) end @@ -109,6 +111,15 @@ class ProjectSetting < ApplicationRecord pages_unique_domain_enabled || pages_unique_domain_in_database.present? end + + def pages_unique_domain_availability + host = Gitlab.config.pages&.dig('host') + + return if host.blank? + return unless Project.where(path: "#{pages_unique_domain}.#{host}").exists? + + errors.add(:pages_unique_domain, s_('ProjectSetting|already in use')) + end end ProjectSetting.prepend_mod diff --git a/app/policies/ci/pipeline_schedule_policy.rb b/app/policies/ci/pipeline_schedule_policy.rb index 7b0d484f9f7..cbc60c4a30a 100644 --- a/app/policies/ci/pipeline_schedule_policy.rb +++ b/app/policies/ci/pipeline_schedule_policy.rb @@ -5,7 +5,18 @@ module Ci alias_method :pipeline_schedule, :subject condition(:protected_ref) do - ref_protected?(@user, @subject.project, @subject.project.repository.tag_exists?(@subject.ref), @subject.ref) + if full_ref?(@subject.ref) + is_tag = Gitlab::Git.tag_ref?(@subject.ref) + ref_name = Gitlab::Git.ref_name(@subject.ref) + else + # NOTE: this block should not be removed + # until the full ref validation is in place + # and all old refs are updated and validated + is_tag = @subject.project.repository.tag_exists?(@subject.ref) + ref_name = @subject.ref + end + + ref_protected?(@user, @subject.project, is_tag, ref_name) end condition(:owner_of_schedule) do @@ -31,6 +42,15 @@ module Ci enable :take_ownership_pipeline_schedule end - rule { protected_ref }.prevent :play_pipeline_schedule + rule { protected_ref }.policy do + prevent :play_pipeline_schedule + prevent :create_pipeline_schedule + end + + private + + def full_ref?(ref) + Gitlab::Git.tag_ref?(ref) || Gitlab::Git.branch_ref?(ref) + end end end diff --git a/app/services/bulk_imports/archive_extraction_service.rb b/app/services/bulk_imports/archive_extraction_service.rb index 4485b19035b..bce2a67218a 100644 --- a/app/services/bulk_imports/archive_extraction_service.rb +++ b/app/services/bulk_imports/archive_extraction_service.rb @@ -49,11 +49,7 @@ module BulkImports end def validate_symlink - raise(BulkImports::Error, 'Invalid file') if symlink?(filepath) - end - - def symlink?(filepath) - File.lstat(filepath).symlink? + raise(BulkImports::Error, 'Invalid file') if Gitlab::Utils::FileInfo.linked?(filepath) end def extract_archive diff --git a/app/services/bulk_imports/file_decompression_service.rb b/app/services/bulk_imports/file_decompression_service.rb index 94573f6bb13..77638f10f54 100644 --- a/app/services/bulk_imports/file_decompression_service.rb +++ b/app/services/bulk_imports/file_decompression_service.rb @@ -53,7 +53,7 @@ module BulkImports end def validate_symlink(filepath) - raise(ServiceError, 'Invalid file') if File.lstat(filepath).symlink? + raise(ServiceError, 'Invalid file') if Gitlab::Utils::FileInfo.linked?(filepath) end def decompress_file diff --git a/app/services/discussions/capture_diff_note_positions_service.rb b/app/services/discussions/capture_diff_note_positions_service.rb index 3684a3f679a..f9b31e0f2f1 100644 --- a/app/services/discussions/capture_diff_note_positions_service.rb +++ b/app/services/discussions/capture_diff_note_positions_service.rb @@ -26,7 +26,7 @@ module Discussions active_diff_discussions = merge_request.notes.new_diff_notes.discussions.select do |discussion| discussion.active?(merge_request.diff_refs) end - paths = active_diff_discussions.flat_map { |n| n.diff_file.paths } + paths = active_diff_discussions.flat_map { |n| n.diff_file&.paths } [active_diff_discussions, paths] end diff --git a/config/application.rb b/config/application.rb index 1e2a6a69dc8..1b96e34bfcb 100644 --- a/config/application.rb +++ b/config/application.rb @@ -186,6 +186,7 @@ module Gitlab # - Any parameter containing `password` # - Any parameter containing `secret` # - Any parameter ending with `key` + # - Any parameter named `redirect`, filtered for security concerns of exposing sensitive information # - Two-factor tokens (:otp_attempt) # - Repo/Project Import URLs (:import_url) # - Build traces (:trace) @@ -228,6 +229,7 @@ module Gitlab variables content sharedSecret + redirect ) # This config option can be removed after Rails 7.1 by https://gitlab.com/gitlab-org/gitlab/-/issues/416270 diff --git a/lib/banzai/filter/autolink_filter.rb b/lib/banzai/filter/autolink_filter.rb index 336d60055e2..bbddaa37380 100644 --- a/lib/banzai/filter/autolink_filter.rb +++ b/lib/banzai/filter/autolink_filter.rb @@ -34,8 +34,13 @@ module Banzai # https://github.com/vmg/rinku/blob/v2.0.1/ext/rinku/autolink.c#L65 # # Rubular: http://rubular.com/r/nrL3r9yUiq + # Note that it's not possible to use Gitlab::UntrustedRegexp for LINK_PATTERN, + # as `(?<!` is unsupported in `re2`, see https://github.com/google/re2/wiki/Syntax LINK_PATTERN = %r{([a-z][a-z0-9\+\.-]+://[^\s>]+)(?<!\?|!|\.|,|:)}.freeze + ENTITY_UNTRUSTED = '((?:&[\w#]+;)+)\z' + ENTITY_UNTRUSTED_REGEX = Gitlab::UntrustedRegexp.new(ENTITY_UNTRUSTED, multiline: false) + # Text matching LINK_PATTERN inside these elements will not be linked IGNORE_PARENTS = %w(a code kbd pre script style).to_set @@ -85,10 +90,14 @@ module Banzai # Remove any trailing HTML entities and store them for appending # outside the link element. The entity must be marked HTML safe in # order to be output literally rather than escaped. - match.gsub!(/((?:&[\w#]+;)+)\z/, '') - dropped = (Regexp.last_match(1) || '').html_safe + dropped = '' + match = ENTITY_UNTRUSTED_REGEX.replace_gsub(match) do |entities| + dropped = entities[1].html_safe + + '' + end - # To match the behaviour of Rinku, if the matched link ends with a + # To match the behavior of Rinku, if the matched link ends with a # closing part of a matched pair of punctuation, we remove that trailing # character unless there are an equal number of closing and opening # characters in the link. diff --git a/lib/banzai/filter/plantuml_filter.rb b/lib/banzai/filter/plantuml_filter.rb index 2e5f1b29c52..7e6535b86fd 100644 --- a/lib/banzai/filter/plantuml_filter.rb +++ b/lib/banzai/filter/plantuml_filter.rb @@ -11,7 +11,7 @@ module Banzai def call return doc unless settings.plantuml_enabled? && doc.at_xpath(lang_tag) - plantuml_setup + Gitlab::Plantuml.configure doc.xpath(lang_tag).each do |node| img_tag = Nokogiri::HTML::DocumentFragment.parse( @@ -38,15 +38,6 @@ module Banzai def settings Gitlab::CurrentSettings.current_application_settings end - - def plantuml_setup - Asciidoctor::PlantUml.configure do |conf| - conf.url = settings.plantuml_url - conf.png_enable = settings.plantuml_enabled - conf.svg_enable = false - conf.txt_enable = false - end - end end end end diff --git a/lib/bulk_imports/common/pipelines/lfs_objects_pipeline.rb b/lib/bulk_imports/common/pipelines/lfs_objects_pipeline.rb index 0bf4d341aad..bd09b6add00 100644 --- a/lib/bulk_imports/common/pipelines/lfs_objects_pipeline.rb +++ b/lib/bulk_imports/common/pipelines/lfs_objects_pipeline.rb @@ -26,7 +26,7 @@ module BulkImports return if tar_filepath?(file_path) return if lfs_json_filepath?(file_path) return if File.directory?(file_path) - return if File.lstat(file_path).symlink? + return if Gitlab::Utils::FileInfo.linked?(file_path) size = File.size(file_path) oid = LfsObject.calculate_oid(file_path) diff --git a/lib/bulk_imports/common/pipelines/uploads_pipeline.rb b/lib/bulk_imports/common/pipelines/uploads_pipeline.rb index 81ce20db9ab..ea17af36c9a 100644 --- a/lib/bulk_imports/common/pipelines/uploads_pipeline.rb +++ b/lib/bulk_imports/common/pipelines/uploads_pipeline.rb @@ -26,7 +26,7 @@ module BulkImports # Validate that the path is OK to load Gitlab::PathTraversal.check_allowed_absolute_path_and_path_traversal!(file_path, [Dir.tmpdir]) return if File.directory?(file_path) - return if File.lstat(file_path).symlink? + return if Gitlab::Utils::FileInfo.linked?(file_path) avatar_path = AVATAR_PATTERN.match(file_path) return save_avatar(file_path) if avatar_path diff --git a/lib/bulk_imports/file_downloads/validations.rb b/lib/bulk_imports/file_downloads/validations.rb index b852a50c888..e1844843408 100644 --- a/lib/bulk_imports/file_downloads/validations.rb +++ b/lib/bulk_imports/file_downloads/validations.rb @@ -32,7 +32,7 @@ module BulkImports end def validate_symlink - return unless File.lstat(filepath).symlink? + return unless Gitlab::Utils::FileInfo.linked?(filepath) File.delete(filepath) raise_error 'Invalid downloaded file' diff --git a/lib/bulk_imports/projects/pipelines/design_bundle_pipeline.rb b/lib/bulk_imports/projects/pipelines/design_bundle_pipeline.rb index 373cd2bd75a..235d2629b9e 100644 --- a/lib/bulk_imports/projects/pipelines/design_bundle_pipeline.rb +++ b/lib/bulk_imports/projects/pipelines/design_bundle_pipeline.rb @@ -26,7 +26,7 @@ module BulkImports return unless portable.lfs_enabled? return unless File.exist?(bundle_path) return if File.directory?(bundle_path) - return if File.lstat(bundle_path).symlink? + return if Gitlab::Utils::FileInfo.linked?(bundle_path) portable.design_repository.create_from_bundle(bundle_path) end diff --git a/lib/bulk_imports/projects/pipelines/repository_bundle_pipeline.rb b/lib/bulk_imports/projects/pipelines/repository_bundle_pipeline.rb index f19d8931f4a..4307cb2bafd 100644 --- a/lib/bulk_imports/projects/pipelines/repository_bundle_pipeline.rb +++ b/lib/bulk_imports/projects/pipelines/repository_bundle_pipeline.rb @@ -26,7 +26,7 @@ module BulkImports return unless File.exist?(bundle_path) return if File.directory?(bundle_path) - return if File.lstat(bundle_path).symlink? + return if Gitlab::Utils::FileInfo.linked?(bundle_path) portable.repository.create_from_bundle(bundle_path) end diff --git a/lib/gitlab/asciidoc.rb b/lib/gitlab/asciidoc.rb index 955cb14594f..31e8dcd84b7 100644 --- a/lib/gitlab/asciidoc.rb +++ b/lib/gitlab/asciidoc.rb @@ -78,20 +78,11 @@ module Gitlab context[:pipeline] = :ascii_doc context[:max_includes] = [MAX_INCLUDES, context[:max_includes]].compact.min - plantuml_setup + Gitlab::Plantuml.configure html = ::Asciidoctor.convert(input, asciidoc_opts) html = Banzai.render(html, context) html.html_safe end - - def self.plantuml_setup - Asciidoctor::PlantUml.configure do |conf| - conf.url = Gitlab::CurrentSettings.plantuml_url - conf.svg_enable = Gitlab::CurrentSettings.plantuml_enabled - conf.png_enable = Gitlab::CurrentSettings.plantuml_enabled - conf.txt_enable = false - end - end end end diff --git a/lib/gitlab/checks/branch_check.rb b/lib/gitlab/checks/branch_check.rb index 8be1e1716ec..aa89c2711f9 100644 --- a/lib/gitlab/checks/branch_check.rb +++ b/lib/gitlab/checks/branch_check.rb @@ -43,7 +43,7 @@ module Gitlab def prohibited_branch_checks return if deletion? - if branch_name =~ %r{\A\h{40}(/|\z)} + if branch_name =~ %r{\A\h{40}(-/|/|\z)} raise GitAccess::ForbiddenError, ERROR_MESSAGES[:prohibited_hex_branch_name] end diff --git a/lib/gitlab/ci/decompressed_gzip_size_validator.rb b/lib/gitlab/ci/decompressed_gzip_size_validator.rb index 9b7b5f0dd66..b386e400423 100644 --- a/lib/gitlab/ci/decompressed_gzip_size_validator.rb +++ b/lib/gitlab/ci/decompressed_gzip_size_validator.rb @@ -65,7 +65,7 @@ module Gitlab def validate_archive_path Gitlab::PathTraversal.check_path_traversal!(archive_path) - raise(ServiceError, 'Archive path is a symlink') if File.lstat(archive_path).symlink? + raise(ServiceError, 'Archive path is a symlink or hard link') if Gitlab::Utils::FileInfo.linked?(archive_path) raise(ServiceError, 'Archive path is not a file') unless File.file?(archive_path) end diff --git a/lib/gitlab/harbor/query.rb b/lib/gitlab/harbor/query.rb index fcd984b01ce..fc0ac539e07 100644 --- a/lib/gitlab/harbor/query.rb +++ b/lib/gitlab/harbor/query.rb @@ -25,7 +25,7 @@ module Gitlab message: 'params invalid' }, allow_blank: true validates :search, format: { - with: /\A([a-z\_]*=[a-zA-Z0-9\- :]*,*)*\z/, + with: /\A(name=[a-zA-Z0-9\-:]+(?:,name=[a-zA-Z0-9\-:]+)*)\z/, message: 'params invalid' }, allow_blank: true diff --git a/lib/gitlab/import_export/command_line_util.rb b/lib/gitlab/import_export/command_line_util.rb index d681f39f00b..e2f365fcbf8 100644 --- a/lib/gitlab/import_export/command_line_util.rb +++ b/lib/gitlab/import_export/command_line_util.rb @@ -5,8 +5,11 @@ module Gitlab module CommandLineUtil UNTAR_MASK = 'u+rwX,go+rX,go-w' DEFAULT_DIR_MODE = 0700 + CLEAN_DIR_IGNORE_FILE_NAMES = %w[. ..].freeze - FileOversizedError = Class.new(StandardError) + CommandLineUtilError = Class.new(StandardError) + FileOversizedError = Class.new(CommandLineUtilError) + HardLinkError = Class.new(CommandLineUtilError) def tar_czf(archive:, dir:) tar_with_options(archive: archive, dir: dir, options: 'czf') @@ -90,7 +93,7 @@ module Gitlab def untar_with_options(archive:, dir:, options:) execute_cmd(%W(tar -#{options} #{archive} -C #{dir})) execute_cmd(%W(chmod -R #{UNTAR_MASK} #{dir})) - remove_symlinks(dir) + clean_extraction_dir!(dir) end # rubocop:disable Gitlab/ModuleWithInstanceVariables @@ -122,17 +125,27 @@ module Gitlab true end - def remove_symlinks(dir) - ignore_file_names = %w[. ..] - + # Scans and cleans the directory tree. + # Symlinks are considered legal but are removed. + # Files sharing hard links are considered illegal and the directory will be removed + # and a `HardLinkError` exception will be raised. + # + # @raise [HardLinkError] if there multiple hard links to the same file detected. + # @return [Boolean] true + def clean_extraction_dir!(dir) # Using File::FNM_DOTMATCH to also delete symlinks starting with "." - Dir.glob("#{dir}/**/*", File::FNM_DOTMATCH) - .reject { |f| ignore_file_names.include?(File.basename(f)) } - .each do |filepath| - FileUtils.rm(filepath) if File.lstat(filepath).symlink? - end + Dir.glob("#{dir}/**/*", File::FNM_DOTMATCH).each do |filepath| + next if CLEAN_DIR_IGNORE_FILE_NAMES.include?(File.basename(filepath)) + + raise HardLinkError, 'File shares hard link' if Gitlab::Utils::FileInfo.shares_hard_link?(filepath) + + FileUtils.rm(filepath) if Gitlab::Utils::FileInfo.linked?(filepath) + end true + rescue HardLinkError + FileUtils.remove_dir(dir) + raise end end end diff --git a/lib/gitlab/import_export/decompressed_archive_size_validator.rb b/lib/gitlab/import_export/decompressed_archive_size_validator.rb index 104c9e6c456..2e39f3f38c2 100644 --- a/lib/gitlab/import_export/decompressed_archive_size_validator.rb +++ b/lib/gitlab/import_export/decompressed_archive_size_validator.rb @@ -87,7 +87,7 @@ module Gitlab def validate_archive_path Gitlab::PathTraversal.check_path_traversal!(@archive_path) - raise(ServiceError, 'Archive path is a symlink') if File.lstat(@archive_path).symlink? + raise(ServiceError, 'Archive path is a symlink or hard link') if Gitlab::Utils::FileInfo.linked?(@archive_path) raise(ServiceError, 'Archive path is not a file') unless File.file?(@archive_path) end diff --git a/lib/gitlab/import_export/file_importer.rb b/lib/gitlab/import_export/file_importer.rb index d2593289c23..37c83e88ef2 100644 --- a/lib/gitlab/import_export/file_importer.rb +++ b/lib/gitlab/import_export/file_importer.rb @@ -23,7 +23,7 @@ module Gitlab mkdir_p(@shared.export_path) mkdir_p(@shared.archive_path) - remove_symlinks(@shared.export_path) + clean_extraction_dir!(@shared.export_path) copy_archive wait_for_archived_file do @@ -35,7 +35,7 @@ module Gitlab false ensure remove_import_file - remove_symlinks(@shared.export_path) + clean_extraction_dir!(@shared.export_path) end private diff --git a/lib/gitlab/import_export/json/ndjson_reader.rb b/lib/gitlab/import_export/json/ndjson_reader.rb index 3de56aacf18..93a94716f8d 100644 --- a/lib/gitlab/import_export/json/ndjson_reader.rb +++ b/lib/gitlab/import_export/json/ndjson_reader.rb @@ -21,7 +21,9 @@ module Gitlab # This reads from `tree/project.json` path = file_path("#{importable_path}.json") - raise Gitlab::ImportExport::Error, 'Invalid file' if !File.exist?(path) || File.symlink?(path) + if !File.exist?(path) || Gitlab::Utils::FileInfo.linked?(path) + raise Gitlab::ImportExport::Error, 'Invalid file' + end data = File.read(path, MAX_JSON_DOCUMENT_SIZE) json_decode(data) @@ -34,7 +36,7 @@ module Gitlab # This reads from `tree/project/merge_requests.ndjson` path = file_path(importable_path, "#{key}.ndjson") - next if !File.exist?(path) || File.symlink?(path) + next if !File.exist?(path) || Gitlab::Utils::FileInfo.linked?(path) File.foreach(path, MAX_JSON_DOCUMENT_SIZE).with_index do |line, line_num| documents << [json_decode(line), line_num] diff --git a/lib/gitlab/import_export/recursive_merge_folders.rb b/lib/gitlab/import_export/recursive_merge_folders.rb index 827385d4daf..e6eba60db93 100644 --- a/lib/gitlab/import_export/recursive_merge_folders.rb +++ b/lib/gitlab/import_export/recursive_merge_folders.rb @@ -57,7 +57,7 @@ module Gitlab source_child = File.join(source_path, child) target_child = File.join(target_path, child) - next if File.lstat(source_child).symlink? + next if Gitlab::Utils::FileInfo.linked?(source_child) if File.directory?(source_child) FileUtils.mkdir_p(target_child, mode: DEFAULT_DIR_MODE) unless File.exist?(target_child) diff --git a/lib/gitlab/pages/virtual_host_finder.rb b/lib/gitlab/pages/virtual_host_finder.rb index 5fec60188f8..d5e2159fb52 100644 --- a/lib/gitlab/pages/virtual_host_finder.rb +++ b/lib/gitlab/pages/virtual_host_finder.rb @@ -10,13 +10,12 @@ module Gitlab def execute return if host.blank? - gitlab_host = ::Settings.pages.host.downcase.prepend(".") + gitlab_host = ::Gitlab.config.pages.host.downcase.prepend(".") if host.ends_with?(gitlab_host) name = host.delete_suffix(gitlab_host) - by_namespace_domain(name) || - by_unique_domain(name) + by_unique_domain(name) || by_namespace_domain(name) else by_custom_domain(host) end diff --git a/lib/gitlab/path_regex.rb b/lib/gitlab/path_regex.rb index e112423f167..8afcf682d5d 100644 --- a/lib/gitlab/path_regex.rb +++ b/lib/gitlab/path_regex.rb @@ -131,7 +131,7 @@ module Gitlab # `NAMESPACE_FORMAT_REGEX`, with the negative lookbehind assertion removed. This means that the client-side validation # will pass for usernames ending in `.atom` and `.git`, but will be caught by the server-side validation. PATH_START_CHAR = '[a-zA-Z0-9_\.]' - PATH_REGEX_STR = PATH_START_CHAR + '[a-zA-Z0-9_\-\.]*' + PATH_REGEX_STR = PATH_START_CHAR + '[a-zA-Z0-9_\-\.]' + "{0,#{Namespace::URL_MAX_LENGTH - 1}}" NAMESPACE_FORMAT_REGEX_JS = PATH_REGEX_STR + '[a-zA-Z0-9_\-]|[a-zA-Z0-9_]' NO_SUFFIX_REGEX = /(?<!\.git|\.atom)/.freeze diff --git a/lib/gitlab/plantuml.rb b/lib/gitlab/plantuml.rb new file mode 100644 index 00000000000..9ec544452fd --- /dev/null +++ b/lib/gitlab/plantuml.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +require "asciidoctor_plantuml/plantuml" + +module Gitlab + module Plantuml + class << self + def configure + Asciidoctor::PlantUml.configure do |conf| + conf.url = Gitlab::CurrentSettings.plantuml_url + conf.png_enable = Gitlab::CurrentSettings.plantuml_enabled + conf.svg_enable = false + conf.txt_enable = false + + conf + end + end + end + end +end diff --git a/lib/gitlab/utils/file_info.rb b/lib/gitlab/utils/file_info.rb new file mode 100644 index 00000000000..a0ec370e225 --- /dev/null +++ b/lib/gitlab/utils/file_info.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +module Gitlab + module Utils + module FileInfo + class << self + # Returns true if: + # - File or directory is a symlink. + # - File shares a hard link. + def linked?(file) + stat = to_file_stat(file) + + stat.symlink? || shares_hard_link?(stat) + end + + # Returns: + # - true if file shares a hard link with another file. + # - false if file is a directory, as directories cannot be hard linked. + def shares_hard_link?(file) + stat = to_file_stat(file) + + stat.file? && stat.nlink > 1 + end + + private + + def to_file_stat(filepath_or_stat) + return filepath_or_stat if filepath_or_stat.is_a?(File::Stat) + + File.lstat(filepath_or_stat) + end + end + end + end +end diff --git a/lib/sbom/purl_type/converter.rb b/lib/sbom/purl_type/converter.rb new file mode 100644 index 00000000000..e02d6932167 --- /dev/null +++ b/lib/sbom/purl_type/converter.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +module Sbom + module PurlType + class Converter + PACKAGE_MANAGER_TO_PURL_TYPE_MAP = { + 'bundler' => 'gem', + 'yarn' => 'npm', + 'npm' => 'npm', + 'pnpm' => 'npm', + 'maven' => 'maven', + 'sbt' => 'maven', + 'gradle' => 'maven', + 'composer' => 'composer', + 'conan' => 'conan', + 'go' => 'golang', + 'nuget' => 'nuget', + 'pip' => 'pypi', + 'pipenv' => 'pypi', + 'setuptools' => 'pypi' + }.with_indifferent_access.freeze + + def self.purl_type_for_pkg_manager(package_manager) + PACKAGE_MANAGER_TO_PURL_TYPE_MAP[package_manager] + end + end + end +end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 5a15c526b7e..50694b8fc5c 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -36590,6 +36590,9 @@ msgstr "" msgid "ProjectSettings|Your project is set up. %{linkStart}View instrumentation instructions%{linkEnd}." msgstr "" +msgid "ProjectSetting|already in use" +msgstr "" + msgid "ProjectTemplates|.NET Core" msgstr "" @@ -36887,6 +36890,9 @@ msgstr "" msgid "ProjectsNew|Your project will be created at:" msgstr "" +msgid "Project|already in use" +msgstr "" + msgid "PrometheusAlerts|exceeded" msgstr "" @@ -54482,9 +54488,6 @@ msgstr "" msgid "eligible users" msgstr "" -msgid "email '%{email}' is not a verified email." -msgstr "" - msgid "email address settings" msgstr "" @@ -54772,6 +54775,9 @@ msgstr "" msgid "is not valid. The iteration group has to match the iteration cadence group." msgstr "" +msgid "is not verified." +msgstr "" + msgid "is one of" msgstr "" diff --git a/package.json b/package.json index 7677d108133..ac838559367 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,7 @@ "@gitlab/svgs": "3.55.0", "@gitlab/ui": "64.20.1", "@gitlab/visual-review-tools": "1.7.3", - "@gitlab/web-ide": "0.0.1-dev-20230713160749", + "@gitlab/web-ide": "0.0.1-dev-20230713160749-patch-1", "@mattiasbuelens/web-streams-adapter": "^0.1.0", "@popperjs/core": "^2.11.2", "@rails/actioncable": "7.0.6", diff --git a/spec/controllers/projects/pipeline_schedules_controller_spec.rb b/spec/controllers/projects/pipeline_schedules_controller_spec.rb index 486062fe52b..e15f07a4e22 100644 --- a/spec/controllers/projects/pipeline_schedules_controller_spec.rb +++ b/spec/controllers/projects/pipeline_schedules_controller_spec.rb @@ -4,6 +4,7 @@ require 'spec_helper' RSpec.describe Projects::PipelineSchedulesController, feature_category: :continuous_integration do include AccessMatchersForController + using RSpec::Parameterized::TableSyntax let_it_be(:user) { create(:user) } let_it_be(:project) { create(:project, :public, :repository) } @@ -45,6 +46,43 @@ RSpec.describe Projects::PipelineSchedulesController, feature_category: :continu end end + shared_examples 'protecting ref' do + where(:branch_access_levels, :tag_access_level, :maintainer_accessible, :developer_accessible) do + [:no_one_can_push, :no_one_can_merge] | :no_one_can_create | \ + :be_denied_for | :be_denied_for + [:maintainers_can_push, :maintainers_can_merge] | :maintainers_can_create | \ + :be_allowed_for | :be_denied_for + [:developers_can_push, :developers_can_merge] | :developers_can_create | \ + :be_allowed_for | :be_allowed_for + end + + with_them do + context 'when branch is protected' do + let(:ref_prefix) { 'heads' } + let(:ref_name) { 'master' } + + before do + create(:protected_branch, *branch_access_levels, name: ref_name, project: project) + end + + it { expect { go }.to try(maintainer_accessible, :maintainer).of(project) } + it { expect { go }.to try(developer_accessible, :developer).of(project) } + end + + context 'when tag is protected' do + let(:ref_prefix) { 'tags' } + let(:ref_name) { 'v1.0.0' } + + before do + create(:protected_tag, tag_access_level, name: ref_name, project: project) + end + + it { expect { go }.to try(maintainer_accessible, :maintainer).of(project) } + it { expect { go }.to try(developer_accessible, :developer).of(project) } + end + end + end + describe 'GET #index' do render_views @@ -159,7 +197,9 @@ RSpec.describe Projects::PipelineSchedulesController, feature_category: :continu end describe 'security' do - let(:schedule) { attributes_for(:ci_pipeline_schedule) } + let(:schedule) { attributes_for(:ci_pipeline_schedule, ref: "refs/#{ref_prefix}/#{ref_name}") } + let(:ref_prefix) { 'heads' } + let(:ref_name) { "master" } it 'is allowed for admin when admin mode enabled', :enable_admin_mode do expect { go }.to be_allowed_for(:admin) @@ -178,6 +218,8 @@ RSpec.describe Projects::PipelineSchedulesController, feature_category: :continu it { expect { go }.to be_denied_for(:user) } it { expect { go }.to be_denied_for(:external) } it { expect { go }.to be_denied_for(:visitor) } + + it_behaves_like 'protecting ref' end def go @@ -438,7 +480,7 @@ RSpec.describe Projects::PipelineSchedulesController, feature_category: :continu end describe 'POST #play', :clean_gitlab_redis_rate_limiting do - let(:ref) { 'master' } + let(:ref_name) { 'master' } before do project.add_developer(user) @@ -454,7 +496,7 @@ RSpec.describe Projects::PipelineSchedulesController, feature_category: :continu it 'does not allow pipeline to be executed' do expect(RunPipelineScheduleWorker).not_to receive(:perform_async) - post :play, params: { namespace_id: project.namespace.to_param, project_id: project, id: pipeline_schedule.id } + go expect(response).to have_gitlab_http_status(:not_found) end @@ -464,16 +506,14 @@ RSpec.describe Projects::PipelineSchedulesController, feature_category: :continu it 'executes a new pipeline' do expect(RunPipelineScheduleWorker).to receive(:perform_async).with(pipeline_schedule.id, user.id).and_return('job-123') - post :play, params: { namespace_id: project.namespace.to_param, project_id: project, id: pipeline_schedule.id } + go expect(flash[:notice]).to start_with 'Successfully scheduled a pipeline to run' expect(response).to have_gitlab_http_status(:found) end it 'prevents users from scheduling the same pipeline repeatedly' do - 2.times do - post :play, params: { namespace_id: project.namespace.to_param, project_id: project, id: pipeline_schedule.id } - end + 2.times { go } expect(flash.to_a.size).to eq(2) expect(flash[:alert]).to eq _('You cannot play this scheduled pipeline at the moment. Please wait a minute.') @@ -481,17 +521,14 @@ RSpec.describe Projects::PipelineSchedulesController, feature_category: :continu end end - context 'when a developer attempts to schedule a protected ref' do - it 'does not allow pipeline to be executed' do - create(:protected_branch, project: project, name: ref) - protected_schedule = create(:ci_pipeline_schedule, project: project, ref: ref) - - expect(RunPipelineScheduleWorker).not_to receive(:perform_async) + describe 'security' do + let!(:pipeline_schedule) { create(:ci_pipeline_schedule, project: project, ref: "refs/#{ref_prefix}/#{ref_name}") } - post :play, params: { namespace_id: project.namespace.to_param, project_id: project, id: protected_schedule.id } + it_behaves_like 'protecting ref' + end - expect(response).to have_gitlab_http_status(:not_found) - end + def go + post :play, params: { namespace_id: project.namespace.to_param, project_id: project, id: pipeline_schedule.id } end end diff --git a/spec/factories/diff_position.rb b/spec/factories/diff_position.rb index bd248452de8..87875e0fa4d 100644 --- a/spec/factories/diff_position.rb +++ b/spec/factories/diff_position.rb @@ -54,6 +54,10 @@ FactoryBot.define do end end + factory :file_diff_position do + position_type { 'file' } + end + factory :image_diff_position do position_type { 'image' } x { 1 } diff --git a/spec/features/merge_request/user_views_comment_on_diff_file_spec.rb b/spec/features/merge_request/user_views_comment_on_diff_file_spec.rb new file mode 100644 index 00000000000..26dbf5605ec --- /dev/null +++ b/spec/features/merge_request/user_views_comment_on_diff_file_spec.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'User views comment on a diff file', :js, feature_category: :code_review_workflow do + include MergeRequestDiffHelpers + include RepoHelpers + + let_it_be(:project) { create(:project, :repository) } + let_it_be(:merge_request) do + create(:merge_request_with_diffs, source_project: project, target_project: project, source_branch: 'merge-test') + end + + let_it_be(:user) { create(:user) } + + before_all do + project.add_maintainer(user) + end + + before do + sign_in(user) + + visit(diffs_project_merge_request_path(project, merge_request)) + end + + context 'with invalid start_sha position' do + before do + diff_refs = Gitlab::Diff::DiffRefs.new( + base_sha: merge_request.diff_refs.base_sha, + start_sha: 'fakesha', + head_sha: merge_request.diff_refs.head_sha + ) + position = build(:file_diff_position, file: 'files/ruby/popen.rb', diff_refs: diff_refs) + create(:diff_note_on_merge_request, noteable: merge_request, project: project, position: position) + end + + it 'renders diffs' do + visit diffs_project_merge_request_path(project, merge_request) + + expect(page).to have_selector('.diff-file') + end + + it 'renders discussion on overview tab' do + visit project_merge_request_path(project, merge_request) + + expect(page).to have_selector('.note-discussion') + end + end +end diff --git a/spec/lib/banzai/filter/autolink_filter_spec.rb b/spec/lib/banzai/filter/autolink_filter_spec.rb index c8b5a9ffa0b..fe642855f3b 100644 --- a/spec/lib/banzai/filter/autolink_filter_spec.rb +++ b/spec/lib/banzai/filter/autolink_filter_spec.rb @@ -226,6 +226,22 @@ RSpec.describe Banzai::Filter::AutolinkFilter, feature_category: :team_planning end end + it 'protects against malicious backtracking' do + doc = "http://#{'&' * 1_000_000}x" + + expect do + Timeout.timeout(30.seconds) { filter(doc) } + end.not_to raise_error + end + + it 'does not timeout with excessively long scheme' do + doc = "#{'h' * 1_000_000}://example.com" + + expect do + Timeout.timeout(30.seconds) { filter(doc) } + end.not_to raise_error + end + # Rinku does not escape these characters in HTML attributes, but content_tag # does. We don't care about that difference for these specs, though. def unescape(html) diff --git a/spec/lib/banzai/filter/references/project_reference_filter_spec.rb b/spec/lib/banzai/filter/references/project_reference_filter_spec.rb index 49c71d08364..b6d6ff2309a 100644 --- a/spec/lib/banzai/filter/references/project_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/references/project_reference_filter_spec.rb @@ -39,6 +39,7 @@ RSpec.describe Banzai::Filter::References::ProjectReferenceFilter, feature_categ it_behaves_like 'fails fast', 'A' * 50000 it_behaves_like 'fails fast', '/a' * 50000 + it_behaves_like 'fails fast', "mailto:#{'a-' * 499_000}@aaaaaaaa..aaaaaaaa.example.com" end it 'allows references with text after the > character' do diff --git a/spec/lib/bulk_imports/common/pipelines/lfs_objects_pipeline_spec.rb b/spec/lib/bulk_imports/common/pipelines/lfs_objects_pipeline_spec.rb index 297ac0ca0ba..50640e1e4ba 100644 --- a/spec/lib/bulk_imports/common/pipelines/lfs_objects_pipeline_spec.rb +++ b/spec/lib/bulk_imports/common/pipelines/lfs_objects_pipeline_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe BulkImports::Common::Pipelines::LfsObjectsPipeline do +RSpec.describe BulkImports::Common::Pipelines::LfsObjectsPipeline, feature_category: :importers do let_it_be(:portable) { create(:project) } let_it_be(:oid) { 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855' } @@ -118,13 +118,22 @@ RSpec.describe BulkImports::Common::Pipelines::LfsObjectsPipeline do context 'when file path is symlink' do it 'returns' do symlink = File.join(tmpdir, 'symlink') + FileUtils.ln_s(lfs_file_path, symlink) - FileUtils.ln_s(File.join(tmpdir, lfs_file_path), symlink) - + expect(Gitlab::Utils::FileInfo).to receive(:linked?).with(symlink).and_call_original expect { pipeline.load(context, symlink) }.not_to change { portable.lfs_objects.count } end end + context 'when file path shares multiple hard links' do + it 'returns' do + FileUtils.link(lfs_file_path, File.join(tmpdir, 'hard_link')) + + expect(Gitlab::Utils::FileInfo).to receive(:linked?).with(lfs_file_path).and_call_original + expect { pipeline.load(context, lfs_file_path) }.not_to change { portable.lfs_objects.count } + end + end + context 'when path is a directory' do it 'returns' do expect { pipeline.load(context, Dir.tmpdir) }.not_to change { portable.lfs_objects.count } diff --git a/spec/lib/bulk_imports/common/pipelines/uploads_pipeline_spec.rb b/spec/lib/bulk_imports/common/pipelines/uploads_pipeline_spec.rb index bc6d36452b4..09c8c7b92c2 100644 --- a/spec/lib/bulk_imports/common/pipelines/uploads_pipeline_spec.rb +++ b/spec/lib/bulk_imports/common/pipelines/uploads_pipeline_spec.rb @@ -105,6 +105,7 @@ RSpec.describe BulkImports::Common::Pipelines::UploadsPipeline, feature_category it 'returns' do path = File.join(tmpdir, 'test') FileUtils.touch(path) + expect { pipeline.load(context, path) }.not_to change { portable.uploads.count } end end @@ -118,13 +119,22 @@ RSpec.describe BulkImports::Common::Pipelines::UploadsPipeline, feature_category context 'when path is a symlink' do it 'does not upload the file' do symlink = File.join(tmpdir, 'symlink') + FileUtils.ln_s(upload_file_path, symlink) - FileUtils.ln_s(File.join(tmpdir, upload_file_path), symlink) - + expect(Gitlab::Utils::FileInfo).to receive(:linked?).with(symlink).and_call_original expect { pipeline.load(context, symlink) }.not_to change { portable.uploads.count } end end + context 'when path has multiple hard links' do + it 'does not upload the file' do + FileUtils.link(upload_file_path, File.join(tmpdir, 'hard_link')) + + expect(Gitlab::Utils::FileInfo).to receive(:linked?).with(upload_file_path).and_call_original + expect { pipeline.load(context, upload_file_path) }.not_to change { portable.uploads.count } + end + end + context 'when path traverses' do it 'does not upload the file' do path_traversal = "#{uploads_dir_path}/avatar/../../../../etc/passwd" diff --git a/spec/lib/bulk_imports/projects/pipelines/design_bundle_pipeline_spec.rb b/spec/lib/bulk_imports/projects/pipelines/design_bundle_pipeline_spec.rb index 5b7309b09f5..87efad92131 100644 --- a/spec/lib/bulk_imports/projects/pipelines/design_bundle_pipeline_spec.rb +++ b/spec/lib/bulk_imports/projects/pipelines/design_bundle_pipeline_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe BulkImports::Projects::Pipelines::DesignBundlePipeline do +RSpec.describe BulkImports::Projects::Pipelines::DesignBundlePipeline, feature_category: :importers do let_it_be(:design) { create(:design, :with_file) } let(:portable) { create(:project) } @@ -125,9 +125,9 @@ RSpec.describe BulkImports::Projects::Pipelines::DesignBundlePipeline do context 'when path is symlink' do it 'returns' do symlink = File.join(tmpdir, 'symlink') + FileUtils.ln_s(design_bundle_path, symlink) - FileUtils.ln_s(File.join(tmpdir, design_bundle_path), symlink) - + expect(Gitlab::Utils::FileInfo).to receive(:linked?).with(symlink).and_call_original expect(portable.design_repository).not_to receive(:create_from_bundle) pipeline.load(context, symlink) @@ -136,6 +136,19 @@ RSpec.describe BulkImports::Projects::Pipelines::DesignBundlePipeline do end end + context 'when path has multiple hard links' do + it 'returns' do + FileUtils.link(design_bundle_path, File.join(tmpdir, 'hard_link')) + + expect(Gitlab::Utils::FileInfo).to receive(:linked?).with(design_bundle_path).and_call_original + expect(portable.design_repository).not_to receive(:create_from_bundle) + + pipeline.load(context, design_bundle_path) + + expect(portable.design_repository.exists?).to eq(false) + end + end + context 'when path is not under tmpdir' do it 'returns' do expect { pipeline.load(context, '/home/test.txt') } diff --git a/spec/lib/bulk_imports/projects/pipelines/repository_bundle_pipeline_spec.rb b/spec/lib/bulk_imports/projects/pipelines/repository_bundle_pipeline_spec.rb index 07fafc19026..5aae8c959fa 100644 --- a/spec/lib/bulk_imports/projects/pipelines/repository_bundle_pipeline_spec.rb +++ b/spec/lib/bulk_imports/projects/pipelines/repository_bundle_pipeline_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe BulkImports::Projects::Pipelines::RepositoryBundlePipeline do +RSpec.describe BulkImports::Projects::Pipelines::RepositoryBundlePipeline, feature_category: :importers do let_it_be(:source) { create(:project, :repository) } let(:portable) { create(:project) } @@ -123,9 +123,9 @@ RSpec.describe BulkImports::Projects::Pipelines::RepositoryBundlePipeline do context 'when path is symlink' do it 'returns' do symlink = File.join(tmpdir, 'symlink') + FileUtils.ln_s(bundle_path, symlink) - FileUtils.ln_s(File.join(tmpdir, bundle_path), symlink) - + expect(Gitlab::Utils::FileInfo).to receive(:linked?).with(symlink).and_call_original expect(portable.repository).not_to receive(:create_from_bundle) pipeline.load(context, symlink) @@ -134,6 +134,19 @@ RSpec.describe BulkImports::Projects::Pipelines::RepositoryBundlePipeline do end end + context 'when path has mutiple hard links' do + it 'returns' do + FileUtils.link(bundle_path, File.join(tmpdir, 'hard_link')) + + expect(Gitlab::Utils::FileInfo).to receive(:linked?).with(bundle_path).and_call_original + expect(portable.repository).not_to receive(:create_from_bundle) + + pipeline.load(context, bundle_path) + + expect(portable.repository.exists?).to eq(false) + end + end + context 'when path is not under tmpdir' do it 'returns' do expect { pipeline.load(context, '/home/test.txt') } diff --git a/spec/lib/gitlab/checks/branch_check_spec.rb b/spec/lib/gitlab/checks/branch_check_spec.rb index 7ce267c535f..9950d4dbd12 100644 --- a/spec/lib/gitlab/checks/branch_check_spec.rb +++ b/spec/lib/gitlab/checks/branch_check_spec.rb @@ -32,6 +32,12 @@ RSpec.describe Gitlab::Checks::BranchCheck, feature_category: :source_code_manag expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError, "You cannot create a branch with a 40-character hexadecimal branch name.") end + it "prohibits 40-character hexadecimal branch names followed by a dash as the start of a path" do + allow(subject).to receive(:branch_name).and_return("267208abfe40e546f5e847444276f7d43a39503e-/test") + + expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError, "You cannot create a branch with a 40-character hexadecimal branch name.") + end + it "doesn't prohibit a nested hexadecimal in a branch name" do allow(subject).to receive(:branch_name).and_return("267208abfe40e546f5e847444276f7d43a39503e-fix") diff --git a/spec/lib/gitlab/ci/decompressed_gzip_size_validator_spec.rb b/spec/lib/gitlab/ci/decompressed_gzip_size_validator_spec.rb index 6ca3f4d415e..dad5bd2548b 100644 --- a/spec/lib/gitlab/ci/decompressed_gzip_size_validator_spec.rb +++ b/spec/lib/gitlab/ci/decompressed_gzip_size_validator_spec.rb @@ -105,6 +105,16 @@ RSpec.describe Gitlab::Ci::DecompressedGzipSizeValidator, feature_category: :imp end end + context 'when archive path has multiple hard links' do + before do + FileUtils.link(filepath, File.join(Dir.mktmpdir, 'hard_link')) + end + + it 'returns false' do + expect(subject).not_to be_valid + end + end + context 'when archive path is not a file' do let(:filepath) { Dir.mktmpdir } let(:filesize) { File.size(filepath) } diff --git a/spec/lib/gitlab/github_import/attachments_downloader_spec.rb b/spec/lib/gitlab/github_import/attachments_downloader_spec.rb index 84d6713efdb..086aa4be17e 100644 --- a/spec/lib/gitlab/github_import/attachments_downloader_spec.rb +++ b/spec/lib/gitlab/github_import/attachments_downloader_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::GithubImport::AttachmentsDownloader do +RSpec.describe Gitlab::GithubImport::AttachmentsDownloader, feature_category: :importers do subject(:downloader) { described_class.new(file_url) } let_it_be(:file_url) { 'https://example.com/avatar.png' } @@ -39,6 +39,26 @@ RSpec.describe Gitlab::GithubImport::AttachmentsDownloader do end end + context 'when file shares multiple hard links' do + let(:tmpdir) { Dir.mktmpdir } + let(:hard_link) { File.join(tmpdir, 'hard_link') } + + before do + existing_file = File.join(tmpdir, 'file.txt') + FileUtils.touch(existing_file) + FileUtils.link(existing_file, hard_link) + allow(downloader).to receive(:filepath).and_return(hard_link) + end + + it 'raises expected exception' do + expect(Gitlab::Utils::FileInfo).to receive(:linked?).with(hard_link).and_call_original + expect { downloader.perform }.to raise_exception( + described_class::DownloadError, + 'Invalid downloaded file' + ) + end + end + context 'when filename is malicious' do let_it_be(:file_url) { 'https://example.com/ava%2F..%2Ftar.png' } diff --git a/spec/lib/gitlab/harbor/query_spec.rb b/spec/lib/gitlab/harbor/query_spec.rb index dcb9a16b27b..fe05643e04a 100644 --- a/spec/lib/gitlab/harbor/query_spec.rb +++ b/spec/lib/gitlab/harbor/query_spec.rb @@ -3,6 +3,8 @@ require 'spec_helper' RSpec.describe Gitlab::Harbor::Query do + using RSpec::Parameterized::TableSyntax + let_it_be(:harbor_integration) { create(:harbor_integration) } let(:params) { {} } @@ -111,19 +113,20 @@ RSpec.describe Gitlab::Harbor::Query do end context 'search' do - context 'with valid search' do - let(:params) { { search: 'name=desc' } } - - it 'initialize successfully' do - expect(query.valid?).to eq(true) - end + where(:search_param, :is_valid) do + "name=desc" | true + "name=value1,name=value-2" | true + "name=value1,name=value_2" | false + "name=desc,key=value" | false + "name=value1, name=value2" | false + "name" | false end - context 'with invalid search' do - let(:params) { { search: 'blabla' } } + with_them do + let(:params) { { search: search_param } } - it 'initialize failed' do - expect(query.valid?).to eq(false) + it "validates according to the regex" do + expect(query.valid?).to eq(is_valid) end end end diff --git a/spec/lib/gitlab/import_export/command_line_util_spec.rb b/spec/lib/gitlab/import_export/command_line_util_spec.rb index 91cfab1688a..b2e047f5621 100644 --- a/spec/lib/gitlab/import_export/command_line_util_spec.rb +++ b/spec/lib/gitlab/import_export/command_line_util_spec.rb @@ -5,13 +5,16 @@ require 'spec_helper' RSpec.describe Gitlab::ImportExport::CommandLineUtil, feature_category: :importers do include ExportFileHelper - let(:path) { "#{Dir.tmpdir}/symlink_test" } - let(:archive) { 'spec/fixtures/symlink_export.tar.gz' } let(:shared) { Gitlab::ImportExport::Shared.new(nil) } - let(:tmpdir) { Dir.mktmpdir } + # Separate where files are written during this test by their kind, to avoid them interfering with each other: + # - `source_dir` Dir to compress files from. + # - `target_dir` Dir to decompress archived files into. + # - `archive_dir` Dir to write any archive files to. + let(:source_dir) { Dir.mktmpdir } + let(:target_dir) { Dir.mktmpdir } let(:archive_dir) { Dir.mktmpdir } - subject do + subject(:mock_class) do Class.new do include Gitlab::ImportExport::CommandLineUtil @@ -25,38 +28,59 @@ RSpec.describe Gitlab::ImportExport::CommandLineUtil, feature_category: :importe end before do - FileUtils.mkdir_p(path) + FileUtils.mkdir_p(source_dir) end after do - FileUtils.rm_rf(path) + FileUtils.rm_rf(source_dir) + FileUtils.rm_rf(target_dir) FileUtils.rm_rf(archive_dir) - FileUtils.remove_entry(tmpdir) end shared_examples 'deletes symlinks' do |compression, decompression| it 'deletes the symlinks', :aggregate_failures do - Dir.mkdir("#{tmpdir}/.git") - Dir.mkdir("#{tmpdir}/folder") - FileUtils.touch("#{tmpdir}/file.txt") - FileUtils.touch("#{tmpdir}/folder/file.txt") - FileUtils.touch("#{tmpdir}/.gitignore") - FileUtils.touch("#{tmpdir}/.git/config") - File.symlink('file.txt', "#{tmpdir}/.symlink") - File.symlink('file.txt', "#{tmpdir}/.git/.symlink") - File.symlink('file.txt', "#{tmpdir}/folder/.symlink") - archive = File.join(archive_dir, 'archive') - subject.public_send(compression, archive: archive, dir: tmpdir) - - subject.public_send(decompression, archive: archive, dir: archive_dir) - - expect(File.exist?("#{archive_dir}/file.txt")).to eq(true) - expect(File.exist?("#{archive_dir}/folder/file.txt")).to eq(true) - expect(File.exist?("#{archive_dir}/.gitignore")).to eq(true) - expect(File.exist?("#{archive_dir}/.git/config")).to eq(true) - expect(File.exist?("#{archive_dir}/.symlink")).to eq(false) - expect(File.exist?("#{archive_dir}/.git/.symlink")).to eq(false) - expect(File.exist?("#{archive_dir}/folder/.symlink")).to eq(false) + Dir.mkdir("#{source_dir}/.git") + Dir.mkdir("#{source_dir}/folder") + FileUtils.touch("#{source_dir}/file.txt") + FileUtils.touch("#{source_dir}/folder/file.txt") + FileUtils.touch("#{source_dir}/.gitignore") + FileUtils.touch("#{source_dir}/.git/config") + File.symlink('file.txt', "#{source_dir}/.symlink") + File.symlink('file.txt', "#{source_dir}/.git/.symlink") + File.symlink('file.txt', "#{source_dir}/folder/.symlink") + archive_file = File.join(archive_dir, 'symlink_archive.tar.gz') + subject.public_send(compression, archive: archive_file, dir: source_dir) + subject.public_send(decompression, archive: archive_file, dir: target_dir) + + expect(File).to exist("#{target_dir}/file.txt") + expect(File).to exist("#{target_dir}/folder/file.txt") + expect(File).to exist("#{target_dir}/.gitignore") + expect(File).to exist("#{target_dir}/.git/config") + expect(File).not_to exist("#{target_dir}/.symlink") + expect(File).not_to exist("#{target_dir}/.git/.symlink") + expect(File).not_to exist("#{target_dir}/folder/.symlink") + end + end + + shared_examples 'handles shared hard links' do |compression, decompression| + let(:archive_file) { File.join(archive_dir, 'hard_link_archive.tar.gz') } + + subject(:decompress) { mock_class.public_send(decompression, archive: archive_file, dir: target_dir) } + + before do + Dir.mkdir("#{source_dir}/dir") + FileUtils.touch("#{source_dir}/file.txt") + FileUtils.touch("#{source_dir}/dir/.file.txt") + FileUtils.link("#{source_dir}/file.txt", "#{source_dir}/.hard_linked_file.txt") + + mock_class.public_send(compression, archive: archive_file, dir: source_dir) + end + + it 'raises an exception and deletes the extraction dir', :aggregate_failures do + expect(FileUtils).to receive(:remove_dir).with(target_dir).and_call_original + expect(Dir).to exist(target_dir) + expect { decompress }.to raise_error(described_class::HardLinkError) + expect(Dir).not_to exist(target_dir) end end @@ -212,6 +236,8 @@ RSpec.describe Gitlab::ImportExport::CommandLineUtil, feature_category: :importe end describe '#gzip' do + let(:path) { source_dir } + it 'compresses specified file' do tempfile = Tempfile.new('test', path) filename = File.basename(tempfile.path) @@ -229,14 +255,16 @@ RSpec.describe Gitlab::ImportExport::CommandLineUtil, feature_category: :importe end describe '#gunzip' do + let(:path) { source_dir } + it 'decompresses specified file' do filename = 'labels.ndjson.gz' gz_filepath = "spec/fixtures/bulk_imports/gz/#{filename}" - FileUtils.copy_file(gz_filepath, File.join(tmpdir, filename)) + FileUtils.copy_file(gz_filepath, File.join(path, filename)) - subject.gunzip(dir: tmpdir, filename: filename) + subject.gunzip(dir: path, filename: filename) - expect(File.exist?(File.join(tmpdir, 'labels.ndjson'))).to eq(true) + expect(File.exist?(File.join(path, 'labels.ndjson'))).to eq(true) end context 'when exception occurs' do @@ -250,7 +278,7 @@ RSpec.describe Gitlab::ImportExport::CommandLineUtil, feature_category: :importe it 'archives a folder without compression' do archive_file = File.join(archive_dir, 'archive.tar') - result = subject.tar_cf(archive: archive_file, dir: tmpdir) + result = subject.tar_cf(archive: archive_file, dir: source_dir) expect(result).to eq(true) expect(File.exist?(archive_file)).to eq(true) @@ -270,29 +298,35 @@ RSpec.describe Gitlab::ImportExport::CommandLineUtil, feature_category: :importe end describe '#untar_zxf' do + let(:tar_archive_fixture) { 'spec/fixtures/symlink_export.tar.gz' } + it_behaves_like 'deletes symlinks', :tar_czf, :untar_zxf + it_behaves_like 'handles shared hard links', :tar_czf, :untar_zxf it 'has the right mask for project.json' do - subject.untar_zxf(archive: archive, dir: path) + subject.untar_zxf(archive: tar_archive_fixture, dir: target_dir) - expect(file_permissions("#{path}/project.json")).to eq(0755) # originally 777 + expect(file_permissions("#{target_dir}/project.json")).to eq(0755) # originally 777 end it 'has the right mask for uploads' do - subject.untar_zxf(archive: archive, dir: path) + subject.untar_zxf(archive: tar_archive_fixture, dir: target_dir) - expect(file_permissions("#{path}/uploads")).to eq(0755) # originally 555 + expect(file_permissions("#{target_dir}/uploads")).to eq(0755) # originally 555 end end describe '#untar_xf' do + let(:tar_archive_fixture) { 'spec/fixtures/symlink_export.tar.gz' } + it_behaves_like 'deletes symlinks', :tar_cf, :untar_xf + it_behaves_like 'handles shared hard links', :tar_cf, :untar_xf it 'extracts archive without decompression' do filename = 'archive.tar.gz' archive_file = File.join(archive_dir, 'archive.tar') - FileUtils.copy_file(archive, File.join(archive_dir, filename)) + FileUtils.copy_file(tar_archive_fixture, File.join(archive_dir, filename)) subject.gunzip(dir: archive_dir, filename: filename) result = subject.untar_xf(archive: archive_file, dir: archive_dir) diff --git a/spec/lib/gitlab/import_export/decompressed_archive_size_validator_spec.rb b/spec/lib/gitlab/import_export/decompressed_archive_size_validator_spec.rb index a6cb74c3c9f..8c5823edc51 100644 --- a/spec/lib/gitlab/import_export/decompressed_archive_size_validator_spec.rb +++ b/spec/lib/gitlab/import_export/decompressed_archive_size_validator_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::ImportExport::DecompressedArchiveSizeValidator do +RSpec.describe Gitlab::ImportExport::DecompressedArchiveSizeValidator, feature_category: :importers do let_it_be(:filepath) { File.join(Dir.tmpdir, 'decompressed_archive_size_validator_spec.gz') } before(:all) do @@ -121,7 +121,7 @@ RSpec.describe Gitlab::ImportExport::DecompressedArchiveSizeValidator do context 'which archive path is a symlink' do let(:filepath) { File.join(Dir.tmpdir, 'symlink') } - let(:error_message) { 'Archive path is a symlink' } + let(:error_message) { 'Archive path is a symlink or hard link' } before do FileUtils.ln_s(filepath, filepath, force: true) @@ -132,6 +132,19 @@ RSpec.describe Gitlab::ImportExport::DecompressedArchiveSizeValidator do end end + context 'when archive path shares multiple hard links' do + let(:filesize) { 32 } + let(:error_message) { 'Archive path is a symlink or hard link' } + + before do + FileUtils.link(filepath, File.join(Dir.mktmpdir, 'hard_link')) + end + + it 'returns false' do + expect(subject).not_to be_valid + end + end + context 'when archive path is not a file' do let(:filepath) { Dir.mktmpdir } let(:filesize) { File.size(filepath) } diff --git a/spec/lib/gitlab/import_export/file_importer_spec.rb b/spec/lib/gitlab/import_export/file_importer_spec.rb index 5a75631ec4d..aff11f7ac30 100644 --- a/spec/lib/gitlab/import_export/file_importer_spec.rb +++ b/spec/lib/gitlab/import_export/file_importer_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::ImportExport::FileImporter do +RSpec.describe Gitlab::ImportExport::FileImporter, feature_category: :importers do include ExportFileHelper let(:shared) { Gitlab::ImportExport::Shared.new(nil) } @@ -113,28 +113,73 @@ RSpec.describe Gitlab::ImportExport::FileImporter do end context 'error' do + subject(:import) { described_class.import(importable: build(:project), archive_file: '', shared: shared) } + before do allow_next_instance_of(described_class) do |instance| - allow(instance).to receive(:wait_for_archived_file).and_raise(StandardError) + allow(instance).to receive(:wait_for_archived_file).and_raise(StandardError, 'foo') end - described_class.import(importable: build(:project), archive_file: '', shared: shared) end it 'removes symlinks in root folder' do + import + expect(File.exist?(symlink_file)).to be false end it 'removes hidden symlinks in root folder' do + import + expect(File.exist?(hidden_symlink_file)).to be false end it 'removes symlinks in subfolders' do + import + expect(File.exist?(subfolder_symlink_file)).to be false end it 'does not remove a valid file' do + import + expect(File.exist?(valid_file)).to be true end + + it 'returns false and sets an error on shared' do + result = import + + expect(result).to eq(false) + expect(shared.errors.join).to eq('foo') + end + + context 'when files in the archive share hard links' do + let(:hard_link_file) { "#{shared.export_path}/hard_link_file.txt" } + + before do + FileUtils.link(valid_file, hard_link_file) + end + + it 'returns false and sets an error on shared' do + result = import + + expect(result).to eq(false) + expect(shared.errors.join).to eq('File shares hard link') + end + + it 'removes all files in export path' do + expect(Dir).to exist(shared.export_path) + expect(File).to exist(symlink_file) + expect(File).to exist(hard_link_file) + expect(File).to exist(valid_file) + + import + + expect(File).not_to exist(symlink_file) + expect(File).not_to exist(hard_link_file) + expect(File).not_to exist(valid_file) + expect(Dir).not_to exist(shared.export_path) + end + end end context 'when file exceeds acceptable decompressed size' do @@ -157,8 +202,10 @@ RSpec.describe Gitlab::ImportExport::FileImporter do allow(Gitlab::ImportExport::DecompressedArchiveSizeValidator).to receive(:max_bytes).and_return(1) end - it 'returns false' do - expect(subject.import).to eq(false) + it 'returns false and sets an error on shared' do + result = subject.import + + expect(result).to eq(false) expect(shared.errors.join).to eq('Decompressed archive size validation failed.') end end diff --git a/spec/lib/gitlab/import_export/json/ndjson_reader_spec.rb b/spec/lib/gitlab/import_export/json/ndjson_reader_spec.rb index 98afe01c08b..1f98a9efb7d 100644 --- a/spec/lib/gitlab/import_export/json/ndjson_reader_spec.rb +++ b/spec/lib/gitlab/import_export/json/ndjson_reader_spec.rb @@ -35,16 +35,22 @@ RSpec.describe Gitlab::ImportExport::Json::NdjsonReader, feature_category: :impo expect(subject).to eq(root_tree) end - context 'when project.json is symlink' do - it 'raises error an error' do - Dir.mktmpdir do |tmpdir| - FileUtils.touch(File.join(tmpdir, 'passwd')) - File.symlink(File.join(tmpdir, 'passwd'), File.join(tmpdir, 'project.json')) + context 'when project.json is symlink or hard link' do + using RSpec::Parameterized::TableSyntax - ndjson_reader = described_class.new(tmpdir) + where(:link_method) { [:link, :symlink] } - expect { ndjson_reader.consume_attributes(importable_path) } - .to raise_error(Gitlab::ImportExport::Error, 'Invalid file') + with_them do + it 'raises an error' do + Dir.mktmpdir do |tmpdir| + FileUtils.touch(File.join(tmpdir, 'passwd')) + FileUtils.send(link_method, File.join(tmpdir, 'passwd'), File.join(tmpdir, 'project.json')) + + ndjson_reader = described_class.new(tmpdir) + + expect { ndjson_reader.consume_attributes(importable_path) } + .to raise_error(Gitlab::ImportExport::Error, 'Invalid file') + end end end end @@ -97,18 +103,24 @@ RSpec.describe Gitlab::ImportExport::Json::NdjsonReader, feature_category: :impo end end - context 'when relation file is a symlink' do - it 'yields nothing to the Enumerator' do - Dir.mktmpdir do |tmpdir| - Dir.mkdir(File.join(tmpdir, 'project')) - File.write(File.join(tmpdir, 'passwd'), "{}\n{}") - File.symlink(File.join(tmpdir, 'passwd'), File.join(tmpdir, 'project', 'issues.ndjson')) + context 'when relation file is a symlink or hard link' do + using RSpec::Parameterized::TableSyntax - ndjson_reader = described_class.new(tmpdir) + where(:link_method) { [:link, :symlink] } + + with_them do + it 'yields nothing to the Enumerator' do + Dir.mktmpdir do |tmpdir| + Dir.mkdir(File.join(tmpdir, 'project')) + File.write(File.join(tmpdir, 'passwd'), "{}\n{}") + FileUtils.send(link_method, File.join(tmpdir, 'passwd'), File.join(tmpdir, 'project', 'issues.ndjson')) + + ndjson_reader = described_class.new(tmpdir) - result = ndjson_reader.consume_relation(importable_path, 'issues') + result = ndjson_reader.consume_relation(importable_path, 'issues') - expect(result.to_a).to eq([]) + expect(result.to_a).to eq([]) + end end end end diff --git a/spec/lib/gitlab/import_export/recursive_merge_folders_spec.rb b/spec/lib/gitlab/import_export/recursive_merge_folders_spec.rb index cb8ac088493..2b9590da421 100644 --- a/spec/lib/gitlab/import_export/recursive_merge_folders_spec.rb +++ b/spec/lib/gitlab/import_export/recursive_merge_folders_spec.rb @@ -4,15 +4,17 @@ require 'spec_helper' RSpec.describe Gitlab::ImportExport::RecursiveMergeFolders do describe '.merge' do - it 'merge folder and ignore symlinks' do + it 'merges folder and ignores symlinks and files that share hard links' do Dir.mktmpdir do |tmpdir| source = "#{tmpdir}/source" FileUtils.mkdir_p("#{source}/folder/folder") FileUtils.touch("#{source}/file1.txt") + FileUtils.touch("#{source}/file_that_shares_hard_links.txt") FileUtils.touch("#{source}/folder/file2.txt") FileUtils.touch("#{source}/folder/folder/file3.txt") FileUtils.ln_s("#{source}/file1.txt", "#{source}/symlink-file1.txt") FileUtils.ln_s("#{source}/folder", "#{source}/symlink-folder") + FileUtils.link("#{source}/file_that_shares_hard_links.txt", "#{source}/hard_link.txt") target = "#{tmpdir}/target" FileUtils.mkdir_p("#{target}/folder/folder") diff --git a/spec/lib/gitlab/pages/virtual_host_finder_spec.rb b/spec/lib/gitlab/pages/virtual_host_finder_spec.rb index 4b584a45503..49eee772f8d 100644 --- a/spec/lib/gitlab/pages/virtual_host_finder_spec.rb +++ b/spec/lib/gitlab/pages/virtual_host_finder_spec.rb @@ -9,6 +9,10 @@ RSpec.describe Gitlab::Pages::VirtualHostFinder, feature_category: :pages do project.update_pages_deployment!(create(:pages_deployment, project: project)) end + before do + stub_pages_setting(host: 'example.com') + end + it 'returns nil when host is empty' do expect(described_class.new(nil).execute).to be_nil expect(described_class.new('').execute).to be_nil @@ -69,7 +73,7 @@ RSpec.describe Gitlab::Pages::VirtualHostFinder, feature_category: :pages do end it 'returns the virual domain with no lookup_paths' do - virtual_domain = described_class.new("#{project.namespace.path}.#{Settings.pages.host}").execute + virtual_domain = described_class.new("#{project.namespace.path}.example.com").execute expect(virtual_domain).to be_an_instance_of(Pages::VirtualDomain) expect(virtual_domain.cache_key).to match(/pages_domain_for_namespace_#{project.namespace.id}_/) @@ -82,7 +86,7 @@ RSpec.describe Gitlab::Pages::VirtualHostFinder, feature_category: :pages do end it 'returns the virual domain with no lookup_paths' do - virtual_domain = described_class.new("#{project.namespace.path}.#{Settings.pages.host}".downcase).execute + virtual_domain = described_class.new("#{project.namespace.path}.example.com".downcase).execute expect(virtual_domain).to be_an_instance_of(Pages::VirtualDomain) expect(virtual_domain.cache_key).to be_nil @@ -104,7 +108,7 @@ RSpec.describe Gitlab::Pages::VirtualHostFinder, feature_category: :pages do end it 'returns the virual domain when there are pages deployed for the project' do - virtual_domain = described_class.new("#{project.namespace.path}.#{Settings.pages.host}").execute + virtual_domain = described_class.new("#{project.namespace.path}.example.com").execute expect(virtual_domain).to be_an_instance_of(Pages::VirtualDomain) expect(virtual_domain.cache_key).to match(/pages_domain_for_namespace_#{project.namespace.id}_/) @@ -113,7 +117,7 @@ RSpec.describe Gitlab::Pages::VirtualHostFinder, feature_category: :pages do end it 'finds domain with case-insensitive' do - virtual_domain = described_class.new("#{project.namespace.path}.#{Settings.pages.host.upcase}").execute + virtual_domain = described_class.new("#{project.namespace.path}.Example.com").execute expect(virtual_domain).to be_an_instance_of(Pages::VirtualDomain) expect(virtual_domain.cache_key).to match(/pages_domain_for_namespace_#{project.namespace.id}_/) @@ -127,7 +131,7 @@ RSpec.describe Gitlab::Pages::VirtualHostFinder, feature_category: :pages do end it 'returns the virual domain when there are pages deployed for the project' do - virtual_domain = described_class.new("#{project.namespace.path}.#{Settings.pages.host}").execute + virtual_domain = described_class.new("#{project.namespace.path}.example.com").execute expect(virtual_domain).to be_an_instance_of(Pages::VirtualDomain) expect(virtual_domain.cache_key).to be_nil @@ -143,7 +147,7 @@ RSpec.describe Gitlab::Pages::VirtualHostFinder, feature_category: :pages do project.project_setting.update!(pages_unique_domain: 'unique-domain') end - subject(:virtual_domain) { described_class.new("unique-domain.#{Settings.pages.host.upcase}").execute } + subject(:virtual_domain) { described_class.new('unique-domain.example.com').execute } context 'when pages unique domain is enabled' do before_all do @@ -171,6 +175,19 @@ RSpec.describe Gitlab::Pages::VirtualHostFinder, feature_category: :pages do expect(virtual_domain.lookup_paths.first.project_id).to eq(project.id) end + context 'when a project path conflicts with a unique domain' do + it 'prioritizes the unique domain project' do + group = create(:group, path: 'unique-domain') + other_project = build(:project, path: 'unique-domain.example.com', group: group) + other_project.save!(validate: false) + other_project.update_pages_deployment!(create(:pages_deployment, project: other_project)) + other_project.mark_pages_as_deployed + + expect(virtual_domain).to be_an_instance_of(Pages::VirtualDomain) + expect(virtual_domain.lookup_paths.first.project_id).to eq(project.id) + end + end + context 'when :cache_pages_domain_api is disabled' do before do stub_feature_flags(cache_pages_domain_api: false) diff --git a/spec/lib/gitlab/plantuml_spec.rb b/spec/lib/gitlab/plantuml_spec.rb new file mode 100644 index 00000000000..c783dd66c48 --- /dev/null +++ b/spec/lib/gitlab/plantuml_spec.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe Gitlab::Plantuml, feature_category: :shared do + describe ".configure" do + subject { described_class.configure } + + let(:plantuml_url) { "http://plantuml.foo.bar" } + + before do + allow(Gitlab::CurrentSettings).to receive(:plantuml_url).and_return(plantuml_url) + end + + context "when PlantUML is enabled" do + before do + allow(Gitlab::CurrentSettings).to receive(:plantuml_enabled).and_return(true) + end + + it "configures the endpoint URL" do + expect(subject.url).to eq(plantuml_url) + end + + it "enables PNG support" do + expect(subject.png_enable).to be_truthy + end + + it "disables SVG support" do + expect(subject.svg_enable).to be_falsey + end + + it "disables TXT support" do + expect(subject.txt_enable).to be_falsey + end + end + + context "when PlantUML is disabled" do + before do + allow(Gitlab::CurrentSettings).to receive(:plantuml_enabled).and_return(false) + end + + it "configures the endpoint URL" do + expect(subject.url).to eq(plantuml_url) + end + + it "enables PNG support" do + expect(subject.png_enable).to be_falsey + end + + it "disables SVG support" do + expect(subject.svg_enable).to be_falsey + end + + it "disables TXT support" do + expect(subject.txt_enable).to be_falsey + end + end + end +end diff --git a/spec/lib/gitlab/utils/file_info_spec.rb b/spec/lib/gitlab/utils/file_info_spec.rb new file mode 100644 index 00000000000..480036b2fd0 --- /dev/null +++ b/spec/lib/gitlab/utils/file_info_spec.rb @@ -0,0 +1,88 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' + +RSpec.describe Gitlab::Utils::FileInfo, feature_category: :shared do + let(:tmpdir) { Dir.mktmpdir } + let(:file_path) { "#{tmpdir}/test.txt" } + + before do + FileUtils.touch(file_path) + end + + after do + FileUtils.rm_rf(tmpdir) + end + + describe '.linked?' do + it 'raises an error when file does not exist' do + expect { subject.linked?('foo') }.to raise_error(Errno::ENOENT) + end + + shared_examples 'identifies a linked file' do + it 'returns false when file or dir is not a link' do + expect(subject.linked?(tmpdir)).to eq(false) + expect(subject.linked?(file)).to eq(false) + end + + it 'returns true when file or dir is symlinked' do + FileUtils.symlink(tmpdir, "#{tmpdir}/symlinked_dir") + FileUtils.symlink(file_path, "#{tmpdir}/symlinked_file.txt") + + expect(subject.linked?("#{tmpdir}/symlinked_dir")).to eq(true) + expect(subject.linked?("#{tmpdir}/symlinked_file.txt")).to eq(true) + end + + it 'returns true when file has more than one hard link' do + FileUtils.link(file_path, "#{tmpdir}/hardlinked_file.txt") + + expect(subject.linked?(file)).to eq(true) + expect(subject.linked?("#{tmpdir}/hardlinked_file.txt")).to eq(true) + end + end + + context 'when file is a File::Stat' do + let(:file) { File.lstat(file_path) } + + it_behaves_like 'identifies a linked file' + end + + context 'when file is path' do + let(:file) { file_path } + + it_behaves_like 'identifies a linked file' + end + end + + describe '.shares_hard_link?' do + it 'raises an error when file does not exist' do + expect { subject.shares_hard_link?('foo') }.to raise_error(Errno::ENOENT) + end + + shared_examples 'identifies a file that shares a hard link' do + it 'returns false when file or dir does not share hard links' do + expect(subject.shares_hard_link?(tmpdir)).to eq(false) + expect(subject.shares_hard_link?(file)).to eq(false) + end + + it 'returns true when file has more than one hard link' do + FileUtils.link(file_path, "#{tmpdir}/hardlinked_file.txt") + + expect(subject.shares_hard_link?(file)).to eq(true) + expect(subject.shares_hard_link?("#{tmpdir}/hardlinked_file.txt")).to eq(true) + end + end + + context 'when file is a File::Stat' do + let(:file) { File.lstat(file_path) } + + it_behaves_like 'identifies a file that shares a hard link' + end + + context 'when file is path' do + let(:file) { file_path } + + it_behaves_like 'identifies a file that shares a hard link' + end + end +end diff --git a/spec/models/project_setting_spec.rb b/spec/models/project_setting_spec.rb index 6928cc8ba08..5d06b30a529 100644 --- a/spec/models/project_setting_spec.rb +++ b/spec/models/project_setting_spec.rb @@ -76,12 +76,36 @@ RSpec.describe ProjectSetting, type: :model, feature_category: :groups_and_proje expect(project_setting).not_to be_valid expect(project_setting.errors.full_messages).to include("Pages unique domain has already been taken") end + + it "validates if the pages_unique_domain already exist as a project path" do + stub_pages_setting(host: 'example.com') + + create(:project, path: "random-unique-domain.example.com") + project_setting = build(:project_setting, pages_unique_domain: "random-unique-domain") + + expect(project_setting).not_to be_valid + expect(project_setting.errors.full_messages_for(:pages_unique_domain)) + .to match(["Pages unique domain already in use"]) + end + + context "when updating" do + it "validates if the pages_unique_domain already exist as a project path" do + stub_pages_setting(host: 'example.com') + project_setting = create(:project_setting) + + create(:project, path: "random-unique-domain.example.com") + + expect(project_setting.update(pages_unique_domain: "random-unique-domain")).to eq(false) + expect(project_setting.errors.full_messages_for(:pages_unique_domain)) + .to match(["Pages unique domain already in use"]) + end + end end describe 'target_platforms=' do it 'stringifies and sorts' do project_setting = build(:project_setting, target_platforms: [:watchos, :ios]) - expect(project_setting.target_platforms).to eq %w(ios watchos) + expect(project_setting.target_platforms).to eq %w[ios watchos] end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 044408e86e9..538f6b363e9 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -827,6 +827,37 @@ RSpec.describe Project, factory_default: :keep, feature_category: :groups_and_pr expect(project).to be_valid end + context 'when validating if path already exist as pages unique domain' do + before do + stub_pages_setting(host: 'example.com') + end + + it 'rejects paths that match pages unique domain' do + create(:project_setting, pages_unique_domain: 'some-unique-domain') + + project = build(:project, path: 'some-unique-domain.example.com') + + expect(project).not_to be_valid + expect(project.errors.full_messages_for(:path)).to match(['Path already in use']) + end + + it 'accepts path when the host does not match' do + create(:project_setting, pages_unique_domain: 'some-unique-domain') + + project = build(:project, path: 'some-unique-domain.another-example.com') + + expect(project).to be_valid + end + + it 'accepts path when the domain does not match' do + create(:project_setting, pages_unique_domain: 'another-unique-domain') + + project = build(:project, path: 'some-unique-domain.example.com') + + expect(project).to be_valid + end + end + context 'path is unchanged' do let_it_be(:invalid_path_project) do project = create(:project, :repository, :public) @@ -4729,6 +4760,33 @@ RSpec.describe Project, factory_default: :keep, feature_category: :groups_and_pr project.update!(visibility_level: Gitlab::VisibilityLevel::INTERNAL) end + context 'when validating if path already exist as pages unique domain' do + before do + stub_pages_setting(host: 'example.com') + end + + it 'rejects paths that match pages unique domain' do + stub_pages_setting(host: 'example.com') + create(:project_setting, pages_unique_domain: 'some-unique-domain') + + expect(project.update(path: 'some-unique-domain.example.com')).to eq(false) + expect(project.errors.full_messages_for(:path)).to match(['Path already in use']) + end + + it 'accepts path when the host does not match' do + create(:project_setting, pages_unique_domain: 'some-unique-domain') + + expect(project.update(path: 'some-unique-domain.another-example.com')).to eq(true) + end + + it 'accepts path when the domain does not match' do + stub_pages_setting(host: 'example.com') + create(:project_setting, pages_unique_domain: 'another-unique-domain') + + expect(project.update(path: 'some-unique-domain.example.com')).to eq(true) + end + end + it 'does not validate the visibility' do expect(project).not_to receive(:visibility_level_allowed_as_fork).and_call_original expect(project).not_to receive(:visibility_level_allowed_by_group).and_call_original diff --git a/spec/policies/ci/pipeline_schedule_policy_spec.rb b/spec/policies/ci/pipeline_schedule_policy_spec.rb index 7025eda1ba1..8fc5c6ca296 100644 --- a/spec/policies/ci/pipeline_schedule_policy_spec.rb +++ b/spec/policies/ci/pipeline_schedule_policy_spec.rb @@ -2,10 +2,13 @@ require 'spec_helper' -RSpec.describe Ci::PipelineSchedulePolicy, :models, :clean_gitlab_redis_cache do +RSpec.describe Ci::PipelineSchedulePolicy, :models, :clean_gitlab_redis_cache, feature_category: :continuous_integration do + using RSpec::Parameterized::TableSyntax + let_it_be(:user) { create(:user) } - let_it_be(:project) { create(:project, :repository) } - let_it_be(:pipeline_schedule, reload: true) { create(:ci_pipeline_schedule, :nightly, project: project) } + let_it_be_with_reload(:project) { create(:project, :repository, create_tag: tag_ref_name) } + let_it_be_with_reload(:pipeline_schedule) { create(:ci_pipeline_schedule, :nightly, project: project) } + let_it_be(:tag_ref_name) { "v1.0.0" } let(:policy) do described_class.new(user, pipeline_schedule) @@ -13,51 +16,143 @@ RSpec.describe Ci::PipelineSchedulePolicy, :models, :clean_gitlab_redis_cache do describe 'rules' do describe 'rules for protected ref' do - before do - project.add_developer(user) - end - - context 'when no one can push or merge to the branch' do - before do - create(:protected_branch, :no_one_can_push, name: pipeline_schedule.ref, project: project) - end - - it 'does not include ability to play pipeline schedule' do - expect(policy).to be_disallowed :play_pipeline_schedule + context 'for branch' do + %w[refs/heads/master master].each do |branch_ref| + context "with #{branch_ref}" do + let_it_be(:branch_ref_name) { "master" } + let_it_be(:branch_pipeline_schedule) do + create(:ci_pipeline_schedule, :nightly, project: project, ref: branch_ref) + end + + where(:push_access_level, :merge_access_level, :project_role, :accessible) do + :no_one_can_push | :no_one_can_merge | :owner | :be_disallowed + :no_one_can_push | :no_one_can_merge | :maintainer | :be_disallowed + :no_one_can_push | :no_one_can_merge | :developer | :be_disallowed + :no_one_can_push | :no_one_can_merge | :reporter | :be_disallowed + :no_one_can_push | :no_one_can_merge | :guest | :be_disallowed + + :maintainers_can_push | :no_one_can_merge | :owner | :be_allowed + :maintainers_can_push | :no_one_can_merge | :maintainer | :be_allowed + :maintainers_can_push | :no_one_can_merge | :developer | :be_disallowed + :maintainers_can_push | :no_one_can_merge | :reporter | :be_disallowed + :maintainers_can_push | :no_one_can_merge | :guest | :be_disallowed + + :developers_can_push | :no_one_can_merge | :owner | :be_allowed + :developers_can_push | :no_one_can_merge | :maintainer | :be_allowed + :developers_can_push | :no_one_can_merge | :developer | :be_allowed + :developers_can_push | :no_one_can_merge | :reporter | :be_disallowed + :developers_can_push | :no_one_can_merge | :guest | :be_disallowed + + :no_one_can_push | :maintainers_can_merge | :owner | :be_allowed + :no_one_can_push | :maintainers_can_merge | :maintainer | :be_allowed + :no_one_can_push | :maintainers_can_merge | :developer | :be_disallowed + :no_one_can_push | :maintainers_can_merge | :reporter | :be_disallowed + :no_one_can_push | :maintainers_can_merge | :guest | :be_disallowed + + :maintainers_can_push | :maintainers_can_merge | :owner | :be_allowed + :maintainers_can_push | :maintainers_can_merge | :maintainer | :be_allowed + :maintainers_can_push | :maintainers_can_merge | :developer | :be_disallowed + :maintainers_can_push | :maintainers_can_merge | :reporter | :be_disallowed + :maintainers_can_push | :maintainers_can_merge | :guest | :be_disallowed + + :developers_can_push | :maintainers_can_merge | :owner | :be_allowed + :developers_can_push | :maintainers_can_merge | :maintainer | :be_allowed + :developers_can_push | :maintainers_can_merge | :developer | :be_allowed + :developers_can_push | :maintainers_can_merge | :reporter | :be_disallowed + :developers_can_push | :maintainers_can_merge | :guest | :be_disallowed + + :no_one_can_push | :developers_can_merge | :owner | :be_allowed + :no_one_can_push | :developers_can_merge | :maintainer | :be_allowed + :no_one_can_push | :developers_can_merge | :developer | :be_allowed + :no_one_can_push | :developers_can_merge | :reporter | :be_disallowed + :no_one_can_push | :developers_can_merge | :guest | :be_disallowed + + :maintainers_can_push | :developers_can_merge | :owner | :be_allowed + :maintainers_can_push | :developers_can_merge | :maintainer | :be_allowed + :maintainers_can_push | :developers_can_merge | :developer | :be_allowed + :maintainers_can_push | :developers_can_merge | :reporter | :be_disallowed + :maintainers_can_push | :developers_can_merge | :guest | :be_disallowed + + :developers_can_push | :developers_can_merge | :owner | :be_allowed + :developers_can_push | :developers_can_merge | :maintainer | :be_allowed + :developers_can_push | :developers_can_merge | :developer | :be_allowed + :developers_can_push | :developers_can_merge | :reporter | :be_disallowed + :developers_can_push | :developers_can_merge | :guest | :be_disallowed + end + + with_them do + before do + create(:protected_branch, push_access_level, merge_access_level, name: branch_ref_name, + project: project) + project.add_role(user, project_role) + end + + context 'for create_pipeline_schedule' do + subject(:policy) { described_class.new(user, new_branch_pipeline_schedule) } + + let(:new_branch_pipeline_schedule) { project.pipeline_schedules.new(ref: branch_ref) } + + it { expect(policy).to try(accessible, :create_pipeline_schedule) } + end + + context 'for play_pipeline_schedule' do + subject(:policy) { described_class.new(user, branch_pipeline_schedule) } + + it { expect(policy).to try(accessible, :play_pipeline_schedule) } + end + end + end end end - context 'when developers can push to the branch' do - before do - create(:protected_branch, :developers_can_merge, name: pipeline_schedule.ref, project: project) - end - - it 'includes ability to update pipeline' do - expect(policy).to be_allowed :play_pipeline_schedule - end - end - - context 'when no one can create the tag' do - let(:tag) { 'v1.0.0' } - - before do - pipeline_schedule.update!(ref: tag) - - create(:protected_tag, :no_one_can_create, name: pipeline_schedule.ref, project: project) - end - - it 'does not include ability to play pipeline schedule' do - expect(policy).to be_disallowed :play_pipeline_schedule - end - end - - context 'when no one can create the tag but it is not a tag' do - before do - create(:protected_tag, :no_one_can_create, name: pipeline_schedule.ref, project: project) - end - - it 'includes ability to play pipeline schedule' do - expect(policy).to be_allowed :play_pipeline_schedule + context 'for tag' do + %w[refs/tags/v1.0.0 v1.0.0].each do |tag_ref| + context "with #{tag_ref}" do + let_it_be(:tag_pipeline_schedule) do + create(:ci_pipeline_schedule, :nightly, project: project, ref: tag_ref) + end + + where(:access_level, :project_role, :accessible) do + :no_one_can_create | :owner | :be_disallowed + :no_one_can_create | :maintainer | :be_disallowed + :no_one_can_create | :developer | :be_disallowed + :no_one_can_create | :reporter | :be_disallowed + :no_one_can_create | :guest | :be_disallowed + + :maintainers_can_create | :owner | :be_allowed + :maintainers_can_create | :maintainer | :be_allowed + :maintainers_can_create | :developer | :be_disallowed + :maintainers_can_create | :reporter | :be_disallowed + :maintainers_can_create | :guest | :be_disallowed + + :developers_can_create | :owner | :be_allowed + :developers_can_create | :maintainer | :be_allowed + :developers_can_create | :developer | :be_allowed + :developers_can_create | :reporter | :be_disallowed + :developers_can_create | :guest | :be_disallowed + end + + with_them do + before do + create(:protected_tag, access_level, name: tag_ref_name, project: project) + project.add_role(user, project_role) + end + + context 'for create_pipeline_schedule' do + subject(:policy) { described_class.new(user, new_tag_pipeline_schedule) } + + let(:new_tag_pipeline_schedule) { project.pipeline_schedules.new(ref: tag_ref) } + + it { expect(policy).to try(accessible, :create_pipeline_schedule) } + end + + context 'for play_pipeline_schedule' do + subject(:policy) { described_class.new(user, tag_pipeline_schedule) } + + it { expect(policy).to try(accessible, :play_pipeline_schedule) } + end + end + end end end end diff --git a/spec/services/bulk_imports/archive_extraction_service_spec.rb b/spec/services/bulk_imports/archive_extraction_service_spec.rb index 5593218c259..f76ac722ad7 100644 --- a/spec/services/bulk_imports/archive_extraction_service_spec.rb +++ b/spec/services/bulk_imports/archive_extraction_service_spec.rb @@ -43,13 +43,21 @@ RSpec.describe BulkImports::ArchiveExtractionService, feature_category: :importe context 'when archive file is a symlink' do it 'raises an error' do - FileUtils.ln_s(File.join(tmpdir, filename), File.join(tmpdir, 'symlink')) + FileUtils.ln_s(filepath, File.join(tmpdir, 'symlink')) expect { described_class.new(tmpdir: tmpdir, filename: 'symlink').execute } .to raise_error(BulkImports::Error, 'Invalid file') end end + context 'when archive file shares multiple hard links' do + it 'raises an error' do + FileUtils.link(filepath, File.join(tmpdir, 'hard_link')) + + expect { subject.execute }.to raise_error(BulkImports::Error, 'Invalid file') + end + end + context 'when filepath is being traversed' do it 'raises an error' do expect { described_class.new(tmpdir: File.join(Dir.mktmpdir, 'test', '..'), filename: 'name').execute } diff --git a/spec/services/bulk_imports/file_decompression_service_spec.rb b/spec/services/bulk_imports/file_decompression_service_spec.rb index 9d80ab3cd8f..e6d919c3499 100644 --- a/spec/services/bulk_imports/file_decompression_service_spec.rb +++ b/spec/services/bulk_imports/file_decompression_service_spec.rb @@ -3,6 +3,8 @@ require 'spec_helper' RSpec.describe BulkImports::FileDecompressionService, feature_category: :importers do + using RSpec::Parameterized::TableSyntax + let_it_be(:tmpdir) { Dir.mktmpdir } let_it_be(:ndjson_filename) { 'labels.ndjson' } let_it_be(:ndjson_filepath) { File.join(tmpdir, ndjson_filename) } @@ -70,39 +72,68 @@ RSpec.describe BulkImports::FileDecompressionService, feature_category: :importe end end - context 'when compressed file is a symlink' do - let_it_be(:symlink) { File.join(tmpdir, 'symlink.gz') } + shared_examples 'raises an error and removes the file' do |error_message:| + specify do + expect { subject.execute } + .to raise_error(BulkImports::FileDecompressionService::ServiceError, error_message) + expect(File).not_to exist(file) + end + end + + shared_context 'when compressed file' do + let_it_be(:file) { File.join(tmpdir, 'file.gz') } + + subject { described_class.new(tmpdir: tmpdir, filename: 'file.gz') } before do - FileUtils.ln_s(File.join(tmpdir, gz_filename), symlink) + FileUtils.send(link_method, File.join(tmpdir, gz_filename), file) end + end - subject { described_class.new(tmpdir: tmpdir, filename: 'symlink.gz') } + shared_context 'when decompressed file' do + let_it_be(:file) { File.join(tmpdir, 'file.txt') } - it 'raises an error and removes the file' do - expect { subject.execute } - .to raise_error(BulkImports::FileDecompressionService::ServiceError, 'File decompression error') + subject { described_class.new(tmpdir: tmpdir, filename: gz_filename) } - expect(File.exist?(symlink)).to eq(false) + before do + original_file = File.join(tmpdir, 'original_file.txt') + FileUtils.touch(original_file) + FileUtils.send(link_method, original_file, file) + + subject.instance_variable_set(:@decompressed_filepath, file) end end + context 'when compressed file is a symlink' do + let(:link_method) { :symlink } + + include_context 'when compressed file' + + include_examples 'raises an error and removes the file', error_message: 'File decompression error' + end + + context 'when compressed file shares multiple hard links' do + let(:link_method) { :link } + + include_context 'when compressed file' + + include_examples 'raises an error and removes the file', error_message: 'File decompression error' + end + context 'when decompressed file is a symlink' do - let_it_be(:symlink) { File.join(tmpdir, 'symlink') } + let(:link_method) { :symlink } - before do - FileUtils.ln_s(File.join(tmpdir, ndjson_filename), symlink) + include_context 'when decompressed file' - subject.instance_variable_set(:@decompressed_filepath, symlink) - end + include_examples 'raises an error and removes the file', error_message: 'Invalid file' + end - subject { described_class.new(tmpdir: tmpdir, filename: gz_filename) } + context 'when decompressed file shares multiple hard links' do + let(:link_method) { :link } - it 'raises an error and removes the file' do - expect { subject.execute }.to raise_error(described_class::ServiceError, 'Invalid file') + include_context 'when decompressed file' - expect(File.exist?(symlink)).to eq(false) - end + include_examples 'raises an error and removes the file', error_message: 'Invalid file' end end end diff --git a/spec/services/bulk_imports/file_download_service_spec.rb b/spec/services/bulk_imports/file_download_service_spec.rb index cbeea5b0f46..c035eabf767 100644 --- a/spec/services/bulk_imports/file_download_service_spec.rb +++ b/spec/services/bulk_imports/file_download_service_spec.rb @@ -10,7 +10,7 @@ RSpec.describe BulkImports::FileDownloadService, feature_category: :importers do let_it_be(:content_type) { 'application/octet-stream' } let_it_be(:content_disposition) { nil } let_it_be(:filename) { 'file_download_service_spec' } - let_it_be(:tmpdir) { Dir.tmpdir } + let_it_be(:tmpdir) { Dir.mktmpdir } let_it_be(:filepath) { File.join(tmpdir, filename) } let_it_be(:content_length) { 1000 } @@ -247,6 +247,36 @@ RSpec.describe BulkImports::FileDownloadService, feature_category: :importers do end end + context 'when file shares multiple hard links' do + let_it_be(:hard_link) { File.join(tmpdir, 'hard_link') } + + before do + existing_file = File.join(Dir.mktmpdir, filename) + FileUtils.touch(existing_file) + FileUtils.link(existing_file, hard_link) + end + + subject do + described_class.new( + configuration: config, + relative_url: '/test', + tmpdir: tmpdir, + filename: 'hard_link', + file_size_limit: file_size_limit, + allowed_content_types: allowed_content_types + ) + end + + it 'raises an error and removes the file' do + expect { subject.execute }.to raise_error( + described_class::ServiceError, + 'Invalid downloaded file' + ) + + expect(File.exist?(hard_link)).to eq(false) + end + end + context 'when dir is not in tmpdir' do subject do described_class.new( diff --git a/spec/services/ci/pipeline_schedules/update_service_spec.rb b/spec/services/ci/pipeline_schedules/update_service_spec.rb index c31a652ed93..5c1354bd5aa 100644 --- a/spec/services/ci/pipeline_schedules/update_service_spec.rb +++ b/spec/services/ci/pipeline_schedules/update_service_spec.rb @@ -6,7 +6,9 @@ RSpec.describe Ci::PipelineSchedules::UpdateService, feature_category: :continuo let_it_be(:user) { create(:user) } let_it_be(:reporter) { create(:user) } let_it_be(:project) { create(:project, :public, :repository) } - let_it_be(:pipeline_schedule) { create(:ci_pipeline_schedule, project: project, owner: user) } + let_it_be(:pipeline_schedule) do + create(:ci_pipeline_schedule, project: project, owner: user, ref: 'master') + end let_it_be(:pipeline_schedule_variable) do create(:ci_pipeline_schedule_variable, diff --git a/yarn.lock b/yarn.lock index 2282ff0ddfc..123d6a0bda1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1151,10 +1151,10 @@ resolved "https://registry.yarnpkg.com/@gitlab/visual-review-tools/-/visual-review-tools-1.7.3.tgz#9ea641146436da388ffbad25d7f2abe0df52c235" integrity sha512-NMV++7Ew1FSBDN1xiZaauU9tfeSfgDHcOLpn+8bGpP+O5orUPm2Eu66R5eC5gkjBPaXosNAxNWtriee+aFk4+g== -"@gitlab/web-ide@0.0.1-dev-20230713160749": - version "0.0.1-dev-20230713160749" - resolved "https://registry.yarnpkg.com/@gitlab/web-ide/-/web-ide-0.0.1-dev-20230713160749.tgz#427aed5e9fa6be1d1f58dde282500759df204b6c" - integrity sha512-QySxWa5dzuZw1XGtkFDuTX9CF/kvBMGoiM9PySH2cvx6mGvC9dphis06eeliNKkNZG3arjwNSC6/8JRkg96B5A== +"@gitlab/web-ide@0.0.1-dev-20230713160749-patch-1": + version "0.0.1-dev-20230713160749-patch-1" + resolved "https://registry.yarnpkg.com/@gitlab/web-ide/-/web-ide-0.0.1-dev-20230713160749-patch-1.tgz#6420b55aae444533f9a4bd6269503d98a72aaa2e" + integrity sha512-Dh8XQyPwDY6fkd/A+hTHCqrD23u5qnlaxKu5myyxDEgBNGgu4SGblFU9B6NHNm8eGUZk6Cs5MuMk+NUvWRKbmA== "@graphql-eslint/eslint-plugin@3.20.0": version "3.20.0" |