diff options
-rw-r--r-- | lib/gitlab/checks/base_checker.rb | 38 | ||||
-rw-r--r-- | lib/gitlab/checks/branch_check.rb | 110 | ||||
-rw-r--r-- | lib/gitlab/checks/change_access.rb | 234 | ||||
-rw-r--r-- | lib/gitlab/checks/commit_check.rb | 65 | ||||
-rw-r--r-- | lib/gitlab/checks/diff_check.rb | 99 | ||||
-rw-r--r-- | lib/gitlab/checks/lfs_check.rb | 22 | ||||
-rw-r--r-- | lib/gitlab/checks/push_check.rb | 22 | ||||
-rw-r--r-- | lib/gitlab/checks/tag_check.rb | 46 | ||||
-rw-r--r-- | spec/lib/gitlab/checks/branch_check_spec.rb | 90 | ||||
-rw-r--r-- | spec/lib/gitlab/checks/change_access_spec.rb | 241 | ||||
-rw-r--r-- | spec/lib/gitlab/checks/diff_check_spec.rb | 51 | ||||
-rw-r--r-- | spec/lib/gitlab/checks/lfs_check_spec.rb | 52 | ||||
-rw-r--r-- | spec/lib/gitlab/checks/push_check_spec.rb | 22 | ||||
-rw-r--r-- | spec/lib/gitlab/checks/tag_check_spec.rb | 64 | ||||
-rw-r--r-- | spec/requests/git_http_spec.rb | 2 | ||||
-rw-r--r-- | spec/support/helpers/git_http_helpers.rb | 5 | ||||
-rw-r--r-- | spec/support/shared_contexts/change_access_checks_shared_context.rb | 29 |
17 files changed, 690 insertions, 502 deletions
diff --git a/lib/gitlab/checks/base_checker.rb b/lib/gitlab/checks/base_checker.rb new file mode 100644 index 00000000000..f8cda0382fe --- /dev/null +++ b/lib/gitlab/checks/base_checker.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +module Gitlab + module Checks + class BaseChecker + include Gitlab::Utils::StrongMemoize + + attr_reader :change_access + delegate(*ChangeAccess::ATTRIBUTES, to: :change_access) + + def initialize(change_access) + @change_access = change_access + end + + def validate! + raise NotImplementedError + end + + private + + def deletion? + Gitlab::Git.blank_ref?(newrev) + end + + def update? + !Gitlab::Git.blank_ref?(oldrev) && !deletion? + end + + def updated_from_web? + protocol == 'web' + end + + def tag_exists? + project.repository.tag_exists?(tag_name) + end + end + end +end diff --git a/lib/gitlab/checks/branch_check.rb b/lib/gitlab/checks/branch_check.rb new file mode 100644 index 00000000000..d06b2df36f2 --- /dev/null +++ b/lib/gitlab/checks/branch_check.rb @@ -0,0 +1,110 @@ +# frozen_string_literal: true + +module Gitlab + module Checks + class BranchCheck < BaseChecker + ERROR_MESSAGES = { + delete_default_branch: 'The default branch of a project cannot be deleted.', + force_push_protected_branch: 'You are not allowed to force push code to a protected branch on this project.', + non_master_delete_protected_branch: 'You are not allowed to delete protected branches from this project. Only a project maintainer or owner can delete a protected branch.', + non_web_delete_protected_branch: 'You can only delete protected branches using the web interface.', + merge_protected_branch: 'You are not allowed to merge code into protected branches on this project.', + push_protected_branch: 'You are not allowed to push code to protected branches on this project.' + }.freeze + + LOG_MESSAGES = { + delete_default_branch_check: "Checking if default branch is being deleted...", + protected_branch_checks: "Checking if you are force pushing to a protected branch...", + protected_branch_push_checks: "Checking if you are allowed to push to the protected branch...", + protected_branch_deletion_checks: "Checking if you are allowed to delete the protected branch..." + }.freeze + + def validate! + return unless branch_name + + logger.log_timed(LOG_MESSAGES[:delete_default_branch_check]) do + if deletion? && branch_name == project.default_branch + raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:delete_default_branch] + end + end + + protected_branch_checks + end + + private + + def protected_branch_checks + logger.log_timed(LOG_MESSAGES[:protected_branch_checks]) do + return unless ProtectedBranch.protected?(project, branch_name) # rubocop:disable Cop/AvoidReturnFromBlocks + + if forced_push? + raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:force_push_protected_branch] + end + end + + if deletion? + protected_branch_deletion_checks + else + protected_branch_push_checks + end + end + + def protected_branch_deletion_checks + logger.log_timed(LOG_MESSAGES[:protected_branch_deletion_checks]) do + unless user_access.can_delete_branch?(branch_name) + raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:non_master_delete_protected_branch] + end + + unless updated_from_web? + raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:non_web_delete_protected_branch] + end + end + end + + def protected_branch_push_checks + logger.log_timed(LOG_MESSAGES[:protected_branch_push_checks]) do + if matching_merge_request? + unless user_access.can_merge_to_branch?(branch_name) || user_access.can_push_to_branch?(branch_name) + raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:merge_protected_branch] + end + else + unless user_access.can_push_to_branch?(branch_name) + raise GitAccess::UnauthorizedError, push_to_protected_branch_rejected_message + end + end + end + end + + def push_to_protected_branch_rejected_message + if project.empty_repo? + empty_project_push_message + else + ERROR_MESSAGES[:push_protected_branch] + end + end + + def empty_project_push_message + <<~MESSAGE + + A default branch (e.g. master) does not yet exist for #{project.full_path} + Ask a project Owner or Maintainer to create a default branch: + + #{project_members_url} + + MESSAGE + end + + def project_members_url + Gitlab::Routing.url_helpers.project_project_members_url(project) + end + + def matching_merge_request? + Checks::MatchingMergeRequest.new(newrev, branch_name, project).match? + end + + def forced_push? + Gitlab::Checks::ForcePush.force_push?(project, oldrev, newrev) + end + end + end +end diff --git a/lib/gitlab/checks/change_access.rb b/lib/gitlab/checks/change_access.rb index 074afe9c412..7778d3068cc 100644 --- a/lib/gitlab/checks/change_access.rb +++ b/lib/gitlab/checks/change_access.rb @@ -3,35 +3,11 @@ module Gitlab module Checks class ChangeAccess - ERROR_MESSAGES = { - push_code: 'You are not allowed to push code to this project.', - delete_default_branch: 'The default branch of a project cannot be deleted.', - force_push_protected_branch: 'You are not allowed to force push code to a protected branch on this project.', - non_master_delete_protected_branch: 'You are not allowed to delete protected branches from this project. Only a project maintainer or owner can delete a protected branch.', - non_web_delete_protected_branch: 'You can only delete protected branches using the web interface.', - merge_protected_branch: 'You are not allowed to merge code into protected branches on this project.', - push_protected_branch: 'You are not allowed to push code to protected branches on this project.', - change_existing_tags: 'You are not allowed to change existing tags on this project.', - update_protected_tag: 'Protected tags cannot be updated.', - delete_protected_tag: 'Protected tags cannot be deleted.', - create_protected_tag: 'You are not allowed to create this tag as it is protected.', - lfs_objects_missing: 'LFS objects are missing. Ensure LFS is properly set up or try a manual "git lfs push --all".' - }.freeze + ATTRIBUTES = %i[user_access project skip_authorization + skip_lfs_integrity_check protocol oldrev newrev ref + branch_name tag_name logger commits].freeze - LOG_MESSAGES = { - push_checks: "Checking if you are allowed to push...", - delete_default_branch_check: "Checking if default branch is being deleted...", - protected_branch_checks: "Checking if you are force pushing to a protected branch...", - protected_branch_push_checks: "Checking if you are allowed to push to the protected branch...", - protected_branch_deletion_checks: "Checking if you are allowed to delete the protected branch...", - tag_checks: "Checking if you are allowed to change existing tags...", - protected_tag_checks: "Checking if you are creating, updating or deleting a protected tag...", - lfs_objects_exist_check: "Scanning repository for blobs stored in LFS and verifying their files have been uploaded to GitLab...", - commits_check_file_paths_validation: "Validating commits' file paths...", - commits_check: "Validating commit contents..." - }.freeze - - attr_reader :user_access, :project, :skip_authorization, :skip_lfs_integrity_check, :protocol, :oldrev, :newrev, :ref, :branch_name, :tag_name, :logger + attr_reader(*ATTRIBUTES) def initialize( change, user_access:, project:, skip_authorization: false, @@ -50,206 +26,32 @@ module Gitlab @logger.append_message("Running checks for ref: #{@branch_name || @tag_name}") end - def exec(skip_commits_check: false) + def exec return true if skip_authorization - push_checks - branch_checks - tag_checks - lfs_objects_exist_check unless skip_lfs_integrity_check - commits_check unless skip_commits_check + ref_level_checks + # Check of commits should happen as the last step + # given they're expensive in terms of performance + commits_check true end - protected - - def push_checks - logger.log_timed(LOG_MESSAGES[__method__]) do - unless can_push? - raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:push_code] - end - end - end - - def branch_checks - return unless branch_name - - logger.log_timed(LOG_MESSAGES[:delete_default_branch_check]) do - if deletion? && branch_name == project.default_branch - raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:delete_default_branch] - end - end - - protected_branch_checks - end - - def protected_branch_checks - logger.log_timed(LOG_MESSAGES[__method__]) do - return unless ProtectedBranch.protected?(project, branch_name) # rubocop:disable Cop/AvoidReturnFromBlocks - - if forced_push? - raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:force_push_protected_branch] - end - end - - if deletion? - protected_branch_deletion_checks - else - protected_branch_push_checks - end - end - - def protected_branch_deletion_checks - logger.log_timed(LOG_MESSAGES[__method__]) do - unless user_access.can_delete_branch?(branch_name) - raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:non_master_delete_protected_branch] - end - - unless updated_from_web? - raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:non_web_delete_protected_branch] - end - end - end - - def protected_branch_push_checks - logger.log_timed(LOG_MESSAGES[__method__]) do - if matching_merge_request? - unless user_access.can_merge_to_branch?(branch_name) || user_access.can_push_to_branch?(branch_name) - raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:merge_protected_branch] - end - else - unless user_access.can_push_to_branch?(branch_name) - raise GitAccess::UnauthorizedError, push_to_protected_branch_rejected_message - end - end - end - end - - def tag_checks - return unless tag_name - - logger.log_timed(LOG_MESSAGES[__method__]) do - if tag_exists? && user_access.cannot_do_action?(:admin_project) - raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:change_existing_tags] - end - end - - protected_tag_checks + def commits + @commits ||= project.repository.new_commits(newrev) end - def protected_tag_checks - logger.log_timed(LOG_MESSAGES[__method__]) do - return unless ProtectedTag.protected?(project, tag_name) # rubocop:disable Cop/AvoidReturnFromBlocks - - raise(GitAccess::UnauthorizedError, ERROR_MESSAGES[:update_protected_tag]) if update? - raise(GitAccess::UnauthorizedError, ERROR_MESSAGES[:delete_protected_tag]) if deletion? + protected - unless user_access.can_create_tag?(tag_name) - raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:create_protected_tag] - end - end + def ref_level_checks + Gitlab::Checks::PushCheck.new(self).validate! + Gitlab::Checks::BranchCheck.new(self).validate! + Gitlab::Checks::TagCheck.new(self).validate! + Gitlab::Checks::LfsCheck.new(self).validate! end def commits_check - return if deletion? || newrev.nil? - return unless should_run_commit_validations? - - logger.log_timed(LOG_MESSAGES[__method__]) do - # n+1: https://gitlab.com/gitlab-org/gitlab-ee/issues/3593 - ::Gitlab::GitalyClient.allow_n_plus_1_calls do - commits.each do |commit| - logger.check_timeout_reached - - commit_check.validate(commit, validations_for_commit(commit)) - end - end - end - - logger.log_timed(LOG_MESSAGES[:commits_check_file_paths_validation]) do - commit_check.validate_file_paths - end - end - - # Method overwritten in EE to inject custom validations - def validations_for_commit(_) - [] - end - - private - - def push_to_protected_branch_rejected_message - if project.empty_repo? - empty_project_push_message - else - ERROR_MESSAGES[:push_protected_branch] - end - end - - def empty_project_push_message - <<~MESSAGE - - A default branch (e.g. master) does not yet exist for #{project.full_path} - Ask a project Owner or Maintainer to create a default branch: - - #{project_members_url} - - MESSAGE - end - - def project_members_url - Gitlab::Routing.url_helpers.project_project_members_url(project) - end - - def should_run_commit_validations? - commit_check.validate_lfs_file_locks? - end - - def updated_from_web? - protocol == 'web' - end - - def tag_exists? - project.repository.tag_exists?(tag_name) - end - - def forced_push? - Gitlab::Checks::ForcePush.force_push?(project, oldrev, newrev) - end - - def update? - !Gitlab::Git.blank_ref?(oldrev) && !deletion? - end - - def deletion? - Gitlab::Git.blank_ref?(newrev) - end - - def matching_merge_request? - Checks::MatchingMergeRequest.new(newrev, branch_name, project).match? - end - - def lfs_objects_exist_check - logger.log_timed(LOG_MESSAGES[__method__]) do - lfs_check = Checks::LfsIntegrity.new(project, newrev, logger.time_left) - - if lfs_check.objects_missing? - raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:lfs_objects_missing] - end - end - end - - def commit_check - @commit_check ||= Gitlab::Checks::CommitCheck.new(project, user_access.user, newrev, oldrev) - end - - def commits - @commits ||= project.repository.new_commits(newrev) - end - - def can_push? - user_access.can_do_action?(:push_code) || - user_access.can_push_to_branch?(branch_name) + Gitlab::Checks::DiffCheck.new(self).validate! end end end diff --git a/lib/gitlab/checks/commit_check.rb b/lib/gitlab/checks/commit_check.rb deleted file mode 100644 index 58267b6752f..00000000000 --- a/lib/gitlab/checks/commit_check.rb +++ /dev/null @@ -1,65 +0,0 @@ -# frozen_string_literal: true - -module Gitlab - module Checks - class CommitCheck - include Gitlab::Utils::StrongMemoize - - attr_reader :project, :user, :newrev, :oldrev - - def initialize(project, user, newrev, oldrev) - @project = project - @user = user - @newrev = newrev - @oldrev = oldrev - @file_paths = [] - end - - def validate(commit, validations) - return if validations.empty? && path_validations.empty? - - commit.raw_deltas.each do |diff| - @file_paths << (diff.new_path || diff.old_path) - - validations.each do |validation| - if error = validation.call(diff) - raise ::Gitlab::GitAccess::UnauthorizedError, error - end - end - end - end - - def validate_file_paths - path_validations.each do |validation| - if error = validation.call(@file_paths) - raise ::Gitlab::GitAccess::UnauthorizedError, error - end - end - end - - def validate_lfs_file_locks? - strong_memoize(:validate_lfs_file_locks) do - project.lfs_enabled? && newrev && oldrev && project.any_lfs_file_locks? - end - end - - private - - # rubocop: disable CodeReuse/ActiveRecord - def lfs_file_locks_validation - lambda do |paths| - lfs_lock = project.lfs_file_locks.where(path: paths).where.not(user_id: user.id).first - - if lfs_lock - return "The path '#{lfs_lock.path}' is locked in Git LFS by #{lfs_lock.user.name}" - end - end - end - # rubocop: enable CodeReuse/ActiveRecord - - def path_validations - validate_lfs_file_locks? ? [lfs_file_locks_validation] : [] - end - end - end -end diff --git a/lib/gitlab/checks/diff_check.rb b/lib/gitlab/checks/diff_check.rb new file mode 100644 index 00000000000..49d361fcef7 --- /dev/null +++ b/lib/gitlab/checks/diff_check.rb @@ -0,0 +1,99 @@ +# frozen_string_literal: true + +module Gitlab + module Checks + class DiffCheck < BaseChecker + include Gitlab::Utils::StrongMemoize + + LOG_MESSAGES = { + validate_file_paths: "Validating diffs' file paths...", + diff_content_check: "Validating diff contents..." + }.freeze + + def validate! + return unless should_run_diff_validations? + return if commits.empty? + return unless uses_raw_delta_validations? + + file_paths = [] + process_raw_deltas do |diff| + file_paths << (diff.new_path || diff.old_path) + + validate_diff(diff) + end + + validate_file_paths(file_paths) + end + + private + + def should_run_diff_validations? + newrev && oldrev && !deletion? && validate_lfs_file_locks? + end + + def validate_lfs_file_locks? + strong_memoize(:validate_lfs_file_locks) do + project.lfs_enabled? && project.any_lfs_file_locks? + end + end + + def uses_raw_delta_validations? + validations_for_diff.present? || path_validations.present? + end + + def validate_diff(diff) + validations_for_diff.each do |validation| + if error = validation.call(diff) + raise ::Gitlab::GitAccess::UnauthorizedError, error + end + end + end + + # Method overwritten in EE to inject custom validations + def validations_for_diff + [] + end + + def path_validations + validate_lfs_file_locks? ? [lfs_file_locks_validation] : [] + end + + def process_raw_deltas + logger.log_timed(LOG_MESSAGES[:diff_content_check]) do + # n+1: https://gitlab.com/gitlab-org/gitlab-ee/issues/3593 + ::Gitlab::GitalyClient.allow_n_plus_1_calls do + commits.each do |commit| + logger.check_timeout_reached + + commit.raw_deltas.each do |diff| + yield(diff) + end + end + end + end + end + + def validate_file_paths(file_paths) + logger.log_timed(LOG_MESSAGES[__method__]) do + path_validations.each do |validation| + if error = validation.call(file_paths) + raise ::Gitlab::GitAccess::UnauthorizedError, error + end + end + end + end + + # rubocop: disable CodeReuse/ActiveRecord + def lfs_file_locks_validation + lambda do |paths| + lfs_lock = project.lfs_file_locks.where(path: paths).where.not(user_id: user_access.user.id).take + + if lfs_lock + return "The path '#{lfs_lock.path}' is locked in Git LFS by #{lfs_lock.user.name}" + end + end + end + # rubocop: enable CodeReuse/ActiveRecord + end + end +end diff --git a/lib/gitlab/checks/lfs_check.rb b/lib/gitlab/checks/lfs_check.rb new file mode 100644 index 00000000000..e42684e679a --- /dev/null +++ b/lib/gitlab/checks/lfs_check.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Gitlab + module Checks + class LfsCheck < BaseChecker + LOG_MESSAGE = "Scanning repository for blobs stored in LFS and verifying their files have been uploaded to GitLab...".freeze + ERROR_MESSAGE = 'LFS objects are missing. Ensure LFS is properly set up or try a manual "git lfs push --all".'.freeze + + def validate! + return if skip_lfs_integrity_check + + logger.log_timed(LOG_MESSAGE) do + lfs_check = Checks::LfsIntegrity.new(project, newrev, logger.time_left) + + if lfs_check.objects_missing? + raise GitAccess::UnauthorizedError, ERROR_MESSAGE + end + end + end + end + end +end diff --git a/lib/gitlab/checks/push_check.rb b/lib/gitlab/checks/push_check.rb new file mode 100644 index 00000000000..f3a52f09868 --- /dev/null +++ b/lib/gitlab/checks/push_check.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Gitlab + module Checks + class PushCheck < BaseChecker + def validate! + logger.log_timed("Checking if you are allowed to push...") do + unless can_push? + raise GitAccess::UnauthorizedError, 'You are not allowed to push code to this project.' + end + end + end + + private + + def can_push? + user_access.can_do_action?(:push_code) || + user_access.can_push_to_branch?(branch_name) + end + end + end +end diff --git a/lib/gitlab/checks/tag_check.rb b/lib/gitlab/checks/tag_check.rb new file mode 100644 index 00000000000..2a75c8059bd --- /dev/null +++ b/lib/gitlab/checks/tag_check.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +module Gitlab + module Checks + class TagCheck < BaseChecker + ERROR_MESSAGES = { + change_existing_tags: 'You are not allowed to change existing tags on this project.', + update_protected_tag: 'Protected tags cannot be updated.', + delete_protected_tag: 'Protected tags cannot be deleted.', + create_protected_tag: 'You are not allowed to create this tag as it is protected.' + }.freeze + + LOG_MESSAGES = { + tag_checks: "Checking if you are allowed to change existing tags...", + protected_tag_checks: "Checking if you are creating, updating or deleting a protected tag..." + }.freeze + + def validate! + return unless tag_name + + logger.log_timed(LOG_MESSAGES[:tag_checks]) do + if tag_exists? && user_access.cannot_do_action?(:admin_project) + raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:change_existing_tags] + end + end + + protected_tag_checks + end + + private + + def protected_tag_checks + logger.log_timed(LOG_MESSAGES[__method__]) do + return unless ProtectedTag.protected?(project, tag_name) # rubocop:disable Cop/AvoidReturnFromBlocks + + raise(GitAccess::UnauthorizedError, ERROR_MESSAGES[:update_protected_tag]) if update? + raise(GitAccess::UnauthorizedError, ERROR_MESSAGES[:delete_protected_tag]) if deletion? + + unless user_access.can_create_tag?(tag_name) + raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:create_protected_tag] + end + end + end + end + end +end diff --git a/spec/lib/gitlab/checks/branch_check_spec.rb b/spec/lib/gitlab/checks/branch_check_spec.rb new file mode 100644 index 00000000000..77366e91dca --- /dev/null +++ b/spec/lib/gitlab/checks/branch_check_spec.rb @@ -0,0 +1,90 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Checks::BranchCheck do + include_context 'change access checks context' + + describe '#validate!' do + it 'does not raise any error' do + expect { subject.validate! }.not_to raise_error + end + + context 'trying to delete the default branch' do + let(:newrev) { '0000000000000000000000000000000000000000' } + let(:ref) { 'refs/heads/master' } + + it 'raises an error' do + expect { subject.validate! }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'The default branch of a project cannot be deleted.') + end + end + + context 'protected branches check' do + before do + allow(ProtectedBranch).to receive(:protected?).with(project, 'master').and_return(true) + allow(ProtectedBranch).to receive(:protected?).with(project, 'feature').and_return(true) + end + + it 'raises an error if the user is not allowed to do forced pushes to protected branches' do + expect(Gitlab::Checks::ForcePush).to receive(:force_push?).and_return(true) + + expect { subject.validate! }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You are not allowed to force push code to a protected branch on this project.') + end + + it 'raises an error if the user is not allowed to merge to protected branches' do + expect_any_instance_of(Gitlab::Checks::MatchingMergeRequest).to receive(:match?).and_return(true) + expect(user_access).to receive(:can_merge_to_branch?).and_return(false) + expect(user_access).to receive(:can_push_to_branch?).and_return(false) + + expect { subject.validate! }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You are not allowed to merge code into protected branches on this project.') + end + + it 'raises an error if the user is not allowed to push to protected branches' do + expect(user_access).to receive(:can_push_to_branch?).and_return(false) + + expect { subject.validate! }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You are not allowed to push code to protected branches on this project.') + end + + context 'when project repository is empty' do + let(:project) { create(:project) } + + it 'raises an error if the user is not allowed to push to protected branches' do + expect(user_access).to receive(:can_push_to_branch?).and_return(false) + + expect { subject.validate! }.to raise_error(Gitlab::GitAccess::UnauthorizedError, /Ask a project Owner or Maintainer to create a default branch/) + end + end + + context 'branch deletion' do + let(:newrev) { '0000000000000000000000000000000000000000' } + let(:ref) { 'refs/heads/feature' } + + context 'if the user is not allowed to delete protected branches' do + it 'raises an error' do + expect { subject.validate! }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You are not allowed to delete protected branches from this project. Only a project maintainer or owner can delete a protected branch.') + end + end + + context 'if the user is allowed to delete protected branches' do + before do + project.add_maintainer(user) + end + + context 'through the web interface' do + let(:protocol) { 'web' } + + it 'allows branch deletion' do + expect { subject.validate! }.not_to raise_error + end + end + + context 'over SSH or HTTP' do + it 'raises an error' do + expect { subject.validate! }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You can only delete protected branches using the web interface.') + end + end + end + end + end + end +end diff --git a/spec/lib/gitlab/checks/change_access_spec.rb b/spec/lib/gitlab/checks/change_access_spec.rb index 81804ba5c76..45fb33e9e4a 100644 --- a/spec/lib/gitlab/checks/change_access_spec.rb +++ b/spec/lib/gitlab/checks/change_access_spec.rb @@ -2,245 +2,56 @@ require 'spec_helper' describe Gitlab::Checks::ChangeAccess do describe '#exec' do - let(:user) { create(:user) } - let(:project) { create(:project, :repository) } - let(:user_access) { Gitlab::UserAccess.new(user, project: project) } - let(:oldrev) { 'be93687618e4b132087f430a4d8fc3a609c9b77c' } - let(:newrev) { '54fcc214b94e78d7a41a9a8fe6d87a5e59500e51' } - let(:ref) { 'refs/heads/master' } - let(:changes) { { oldrev: oldrev, newrev: newrev, ref: ref } } - let(:protocol) { 'ssh' } - let(:timeout) { Gitlab::GitAccess::INTERNAL_TIMEOUT } - let(:logger) { Gitlab::Checks::TimedLogger.new(timeout: timeout) } + include_context 'change access checks context' - subject(:change_access) do - described_class.new( - changes, - project: project, - user_access: user_access, - protocol: protocol, - logger: logger - ) - end - - before do - project.add_developer(user) - end + subject { change_access } context 'without failed checks' do it "doesn't raise an error" do expect { subject.exec }.not_to raise_error end - end - context 'when time limit was reached' do - it 'raises a TimeoutError' do - logger = Gitlab::Checks::TimedLogger.new(start_time: timeout.ago, timeout: timeout) - access = described_class.new(changes, - project: project, - user_access: user_access, - protocol: protocol, - logger: logger) + it 'calls pushes checks' do + expect_any_instance_of(Gitlab::Checks::PushCheck).to receive(:validate!) - expect { access.exec }.to raise_error(Gitlab::Checks::TimedLogger::TimeoutError) + subject.exec end - end - context 'when the user is not allowed to push to the repo' do - it 'raises an error' do - expect(user_access).to receive(:can_do_action?).with(:push_code).and_return(false) - expect(user_access).to receive(:can_push_to_branch?).with('master').and_return(false) + it 'calls branches checks' do + expect_any_instance_of(Gitlab::Checks::BranchCheck).to receive(:validate!) - expect { subject.exec }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You are not allowed to push code to this project.') + subject.exec end - end - context 'tags check' do - let(:ref) { 'refs/tags/v1.0.0' } + it 'calls tags checks' do + expect_any_instance_of(Gitlab::Checks::TagCheck).to receive(:validate!) - it 'raises an error if the user is not allowed to update tags' do - allow(user_access).to receive(:can_do_action?).with(:push_code).and_return(true) - expect(user_access).to receive(:can_do_action?).with(:admin_project).and_return(false) - - expect { subject.exec }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You are not allowed to change existing tags on this project.') + subject.exec end - context 'with protected tag' do - let!(:protected_tag) { create(:protected_tag, project: project, name: 'v*') } - - context 'as maintainer' do - before do - project.add_maintainer(user) - end + it 'calls lfs checks' do + expect_any_instance_of(Gitlab::Checks::LfsCheck).to receive(:validate!) - context 'deletion' do - let(:oldrev) { 'be93687618e4b132087f430a4d8fc3a609c9b77c' } - let(:newrev) { '0000000000000000000000000000000000000000' } - - it 'is prevented' do - expect { subject.exec }.to raise_error(Gitlab::GitAccess::UnauthorizedError, /cannot be deleted/) - end - end - - context 'update' do - let(:oldrev) { 'be93687618e4b132087f430a4d8fc3a609c9b77c' } - let(:newrev) { '54fcc214b94e78d7a41a9a8fe6d87a5e59500e51' } - - it 'is prevented' do - expect { subject.exec }.to raise_error(Gitlab::GitAccess::UnauthorizedError, /cannot be updated/) - end - end - end - - context 'creation' do - let(:oldrev) { '0000000000000000000000000000000000000000' } - let(:newrev) { '54fcc214b94e78d7a41a9a8fe6d87a5e59500e51' } - let(:ref) { 'refs/tags/v9.1.0' } - - it 'prevents creation below access level' do - expect { subject.exec }.to raise_error(Gitlab::GitAccess::UnauthorizedError, /allowed to create this tag as it is protected/) - end - - context 'when user has access' do - let!(:protected_tag) { create(:protected_tag, :developers_can_create, project: project, name: 'v*') } - - it 'allows tag creation' do - expect { subject.exec }.not_to raise_error - end - end - end + subject.exec end - end - context 'branches check' do - context 'trying to delete the default branch' do - let(:newrev) { '0000000000000000000000000000000000000000' } - let(:ref) { 'refs/heads/master' } + it 'calls diff checks' do + expect_any_instance_of(Gitlab::Checks::DiffCheck).to receive(:validate!) - it 'raises an error' do - expect { subject.exec }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'The default branch of a project cannot be deleted.') - end - end - - context 'protected branches check' do - before do - allow(ProtectedBranch).to receive(:protected?).with(project, 'master').and_return(true) - allow(ProtectedBranch).to receive(:protected?).with(project, 'feature').and_return(true) - end - - it 'raises an error if the user is not allowed to do forced pushes to protected branches' do - expect(Gitlab::Checks::ForcePush).to receive(:force_push?).and_return(true) - - expect { subject.exec }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You are not allowed to force push code to a protected branch on this project.') - end - - it 'raises an error if the user is not allowed to merge to protected branches' do - expect_any_instance_of(Gitlab::Checks::MatchingMergeRequest).to receive(:match?).and_return(true) - expect(user_access).to receive(:can_merge_to_branch?).and_return(false) - expect(user_access).to receive(:can_push_to_branch?).and_return(false) - - expect { subject.exec }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You are not allowed to merge code into protected branches on this project.') - end - - it 'raises an error if the user is not allowed to push to protected branches' do - expect(user_access).to receive(:can_push_to_branch?).and_return(false) - - expect { subject.exec }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You are not allowed to push code to protected branches on this project.') - end - - context 'when project repository is empty' do - let(:project) { create(:project) } - - it 'raises an error if the user is not allowed to push to protected branches' do - expect(user_access).to receive(:can_push_to_branch?).and_return(false) - - expect { subject.exec }.to raise_error(Gitlab::GitAccess::UnauthorizedError, /Ask a project Owner or Maintainer to create a default branch/) - end - end - - context 'branch deletion' do - let(:newrev) { '0000000000000000000000000000000000000000' } - let(:ref) { 'refs/heads/feature' } - - context 'if the user is not allowed to delete protected branches' do - it 'raises an error' do - expect { subject.exec }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You are not allowed to delete protected branches from this project. Only a project maintainer or owner can delete a protected branch.') - end - end - - context 'if the user is allowed to delete protected branches' do - before do - project.add_maintainer(user) - end - - context 'through the web interface' do - let(:protocol) { 'web' } - - it 'allows branch deletion' do - expect { subject.exec }.not_to raise_error - end - end - - context 'over SSH or HTTP' do - it 'raises an error' do - expect { subject.exec }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You can only delete protected branches using the web interface.') - end - end - end - end + subject.exec end end - context 'LFS integrity check' do - it 'fails if any LFS blobs are missing' do - allow_any_instance_of(Gitlab::Checks::LfsIntegrity).to receive(:objects_missing?).and_return(true) - - expect { subject.exec }.to raise_error(Gitlab::GitAccess::UnauthorizedError, /LFS objects are missing/) - end - - it 'succeeds if LFS objects have already been uploaded' do - allow_any_instance_of(Gitlab::Checks::LfsIntegrity).to receive(:objects_missing?).and_return(false) - - expect { subject.exec }.not_to raise_error - end - end - - context 'LFS file lock check' do - let(:owner) { create(:user) } - let!(:lock) { create(:lfs_file_lock, user: owner, project: project, path: 'README') } - - before do - allow(project.repository).to receive(:new_commits).and_return( - project.repository.commits_between('be93687618e4b132087f430a4d8fc3a609c9b77c', '54fcc214b94e78d7a41a9a8fe6d87a5e59500e51') - ) - end - - context 'with LFS not enabled' do - it 'skips the validation' do - expect_any_instance_of(Gitlab::Checks::CommitCheck).not_to receive(:validate) - - subject.exec - end - end - - context 'with LFS enabled' do - before do - allow(project).to receive(:lfs_enabled?).and_return(true) - end - - context 'when change is sent by a different user' do - it 'raises an error if the user is not allowed to update the file' do - expect { subject.exec }.to raise_error(Gitlab::GitAccess::UnauthorizedError, "The path 'README' is locked in Git LFS by #{lock.user.name}") - end - end - - context 'when change is sent by the author of the lock' do - let(:user) { owner } + context 'when time limit was reached' do + it 'raises a TimeoutError' do + logger = Gitlab::Checks::TimedLogger.new(start_time: timeout.ago, timeout: timeout) + access = described_class.new(changes, + project: project, + user_access: user_access, + protocol: protocol, + logger: logger) - it "doesn't raise any error" do - expect { subject.exec }.not_to raise_error - end - end + expect { access.exec }.to raise_error(Gitlab::Checks::TimedLogger::TimeoutError) end end end diff --git a/spec/lib/gitlab/checks/diff_check_spec.rb b/spec/lib/gitlab/checks/diff_check_spec.rb new file mode 100644 index 00000000000..eeec1e83179 --- /dev/null +++ b/spec/lib/gitlab/checks/diff_check_spec.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Checks::DiffCheck do + include_context 'change access checks context' + + describe '#validate!' do + let(:owner) { create(:user) } + let!(:lock) { create(:lfs_file_lock, user: owner, project: project, path: 'README') } + + before do + allow(project.repository).to receive(:new_commits).and_return( + project.repository.commits_between('be93687618e4b132087f430a4d8fc3a609c9b77c', '54fcc214b94e78d7a41a9a8fe6d87a5e59500e51') + ) + end + + context 'with LFS not enabled' do + before do + allow(project).to receive(:lfs_enabled?).and_return(false) + end + + it 'skips the validation' do + expect(subject).not_to receive(:validate_diff) + expect(subject).not_to receive(:validate_file_paths) + + subject.validate! + end + end + + context 'with LFS enabled' do + before do + allow(project).to receive(:lfs_enabled?).and_return(true) + end + + context 'when change is sent by a different user' do + it 'raises an error if the user is not allowed to update the file' do + expect { subject.validate! }.to raise_error(Gitlab::GitAccess::UnauthorizedError, "The path 'README' is locked in Git LFS by #{lock.user.name}") + end + end + + context 'when change is sent by the author of the lock' do + let(:user) { owner } + + it "doesn't raise any error" do + expect { subject.validate! }.not_to raise_error + end + end + end + end +end diff --git a/spec/lib/gitlab/checks/lfs_check_spec.rb b/spec/lib/gitlab/checks/lfs_check_spec.rb new file mode 100644 index 00000000000..35f8069c8a4 --- /dev/null +++ b/spec/lib/gitlab/checks/lfs_check_spec.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Checks::LfsCheck do + include_context 'change access checks context' + + let(:blob_object) { project.repository.blob_at_branch('lfs', 'files/lfs/lfs_object.iso') } + + before do + allow_any_instance_of(Gitlab::Git::LfsChanges).to receive(:new_pointers) do + [blob_object] + end + end + + describe '#validate!' do + context 'with LFS not enabled' do + it 'skips integrity check' do + expect_any_instance_of(Gitlab::Git::LfsChanges).not_to receive(:new_pointers) + + subject.validate! + end + end + + context 'with LFS enabled' do + before do + allow(project).to receive(:lfs_enabled?).and_return(true) + end + + context 'deletion' do + let(:changes) { { oldrev: oldrev, ref: ref } } + + it 'skips integrity check' do + expect(project.repository).not_to receive(:new_objects) + + subject.validate! + end + end + + it 'fails if any LFS blobs are missing' do + expect { subject.validate! }.to raise_error(Gitlab::GitAccess::UnauthorizedError, /LFS objects are missing/) + end + + it 'succeeds if LFS objects have already been uploaded' do + lfs_object = create(:lfs_object, oid: blob_object.lfs_oid) + create(:lfs_objects_project, project: project, lfs_object: lfs_object) + + expect { subject.validate! }.not_to raise_error + end + end + end +end diff --git a/spec/lib/gitlab/checks/push_check_spec.rb b/spec/lib/gitlab/checks/push_check_spec.rb new file mode 100644 index 00000000000..25f0d428cb9 --- /dev/null +++ b/spec/lib/gitlab/checks/push_check_spec.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Checks::PushCheck do + include_context 'change access checks context' + + describe '#validate!' do + it 'does not raise any error' do + expect { subject.validate! }.not_to raise_error + end + + context 'when the user is not allowed to push to the repo' do + it 'raises an error' do + expect(user_access).to receive(:can_do_action?).with(:push_code).and_return(false) + expect(user_access).to receive(:can_push_to_branch?).with('master').and_return(false) + + expect { subject.validate! }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You are not allowed to push code to this project.') + end + end + end +end diff --git a/spec/lib/gitlab/checks/tag_check_spec.rb b/spec/lib/gitlab/checks/tag_check_spec.rb new file mode 100644 index 00000000000..b1258270611 --- /dev/null +++ b/spec/lib/gitlab/checks/tag_check_spec.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Checks::TagCheck do + include_context 'change access checks context' + + describe '#validate!' do + let(:ref) { 'refs/tags/v1.0.0' } + + it 'raises an error' do + allow(user_access).to receive(:can_do_action?).with(:push_code).and_return(true) + expect(user_access).to receive(:can_do_action?).with(:admin_project).and_return(false) + + expect { subject.validate! }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You are not allowed to change existing tags on this project.') + end + + context 'with protected tag' do + let!(:protected_tag) { create(:protected_tag, project: project, name: 'v*') } + + context 'as maintainer' do + before do + project.add_maintainer(user) + end + + context 'deletion' do + let(:oldrev) { 'be93687618e4b132087f430a4d8fc3a609c9b77c' } + let(:newrev) { '0000000000000000000000000000000000000000' } + + it 'is prevented' do + expect { subject.validate! }.to raise_error(Gitlab::GitAccess::UnauthorizedError, /cannot be deleted/) + end + end + + context 'update' do + let(:oldrev) { 'be93687618e4b132087f430a4d8fc3a609c9b77c' } + let(:newrev) { '54fcc214b94e78d7a41a9a8fe6d87a5e59500e51' } + + it 'is prevented' do + expect { subject.validate! }.to raise_error(Gitlab::GitAccess::UnauthorizedError, /cannot be updated/) + end + end + end + + context 'creation' do + let(:oldrev) { '0000000000000000000000000000000000000000' } + let(:newrev) { '54fcc214b94e78d7a41a9a8fe6d87a5e59500e51' } + let(:ref) { 'refs/tags/v9.1.0' } + + it 'prevents creation below access level' do + expect { subject.validate! }.to raise_error(Gitlab::GitAccess::UnauthorizedError, /allowed to create this tag as it is protected/) + end + + context 'when user has access' do + let!(:protected_tag) { create(:protected_tag, :developers_can_create, project: project, name: 'v*') } + + it 'allows tag creation' do + expect { subject.validate! }.not_to raise_error + end + end + end + end + end +end diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb index c71eae9164a..0dc459d9b5a 100644 --- a/spec/requests/git_http_spec.rb +++ b/spec/requests/git_http_spec.rb @@ -302,7 +302,7 @@ describe 'Git HTTP requests' do it 'rejects pushes with 403 Forbidden' do upload(path, env) do |response| expect(response).to have_gitlab_http_status(:forbidden) - expect(response.body).to eq(change_access_error(:push_code)) + expect(response.body).to eq('You are not allowed to push code to this project.') end end diff --git a/spec/support/helpers/git_http_helpers.rb b/spec/support/helpers/git_http_helpers.rb index b8289e6c5f1..9a5845af90c 100644 --- a/spec/support/helpers/git_http_helpers.rb +++ b/spec/support/helpers/git_http_helpers.rb @@ -60,9 +60,4 @@ module GitHttpHelpers message = Gitlab::GitAccessWiki::ERROR_MESSAGES[error_key] message || raise("GitAccessWiki error message key '#{error_key}' not found") end - - def change_access_error(error_key) - message = Gitlab::Checks::ChangeAccess::ERROR_MESSAGES[error_key] - message || raise("ChangeAccess error message key '#{error_key}' not found") - end end diff --git a/spec/support/shared_contexts/change_access_checks_shared_context.rb b/spec/support/shared_contexts/change_access_checks_shared_context.rb new file mode 100644 index 00000000000..aca18b0c73b --- /dev/null +++ b/spec/support/shared_contexts/change_access_checks_shared_context.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +shared_context 'change access checks context' do + let(:user) { create(:user) } + let(:project) { create(:project, :repository) } + let(:user_access) { Gitlab::UserAccess.new(user, project: project) } + let(:oldrev) { 'be93687618e4b132087f430a4d8fc3a609c9b77c' } + let(:newrev) { '54fcc214b94e78d7a41a9a8fe6d87a5e59500e51' } + let(:ref) { 'refs/heads/master' } + let(:changes) { { oldrev: oldrev, newrev: newrev, ref: ref } } + let(:protocol) { 'ssh' } + let(:timeout) { Gitlab::GitAccess::INTERNAL_TIMEOUT } + let(:logger) { Gitlab::Checks::TimedLogger.new(timeout: timeout) } + let(:change_access) do + Gitlab::Checks::ChangeAccess.new( + changes, + project: project, + user_access: user_access, + protocol: protocol, + logger: logger + ) + end + + subject { described_class.new(change_access) } + + before do + project.add_developer(user) + end +end |