Refactor Gitlab::Checks::ChangeAccess and EE::Gitlab::Checks::ChangeAccess

This class handles the validations of git pushes. It is
quite big and all the logic is inside one file. This commit
refactors the code and split it into different files.
parent d390f8f7
# frozen_string_literal: true
module EE
module Gitlab
module Checks
module BaseChecker
extend ActiveSupport::Concern
include ::Gitlab::Utils::StrongMemoize
private
def push_rule
strong_memoize(:push_rule) do
project.push_rule
end
end
end
end
end
end
# frozen_string_literal: true
module EE module EE
module Gitlab module Gitlab
module Checks module Checks
module ChangeAccess module ChangeAccess
extend ActiveSupport::Concern extend ActiveSupport::Concern
extend ::Gitlab::Utils::Override extend ::Gitlab::Utils::Override
include PathLocksHelper
include ::Gitlab::Utils::StrongMemoize
ERROR_MESSAGES = {
push_rule_branch_name: "Branch name does not follow the pattern '%{branch_name_regex}'",
push_rule_committer_not_verified: "Committer email '%{committer_email}' is not verified.",
push_rule_committer_not_allowed: "You cannot push commits for '%{committer_email}'. You can only push commits that were committed with one of your own verified emails."
}.freeze
LOG_MESSAGES = {
push_rule_tag_check: "Checking if you are allowed to delete a tag...",
push_rule_branch_check: "Checking if branch follows the naming patterns defined by the project...",
push_rule_commits_check: "Checking if commits follow defined push rules...",
file_size_check: "Checking if any files are larger than the allowed size..."
}.freeze
override :exec
def exec
return true if skip_authorization
super(skip_commits_check: true)
push_rule_check
file_size_check
# Check of commits should happen as the last step
# given they're expensive in terms of performance
commits_check
true
end
private
def file_size_check
return if push_rule.nil? || push_rule.max_file_size.zero?
logger.log_timed(LOG_MESSAGES[__method__]) do
max_file_size = push_rule.max_file_size
blobs = project.repository.new_blobs(newrev, dynamic_timeout: logger.time_left)
large_blob = blobs.find do |blob|
::Gitlab::Utils.bytes_to_megabytes(blob.size) > max_file_size
end
if large_blob
raise ::Gitlab::GitAccess::UnauthorizedError, %Q{File "#{large_blob.path}" is larger than the allowed size of #{max_file_size} MB}
end
end
end
def push_rule
project.push_rule
end
def push_rule_check
return unless newrev && oldrev && project.feature_available?(:push_rules)
if tag_name
push_rule_tag_check
else
push_rule_branch_check
end
end
def push_rule_tag_check override :ref_level_checks
logger.log_timed(LOG_MESSAGES[__method__]) do def ref_level_checks
if tag_deletion_denied_by_push_rule? super
raise ::Gitlab::GitAccess::UnauthorizedError, 'You cannot delete a tag'
end
end
end
def push_rule_branch_check
logger.log_timed(LOG_MESSAGES[__method__]) do
unless branch_name_allowed_by_push_rule?
message = ERROR_MESSAGES[:push_rule_branch_name] % { branch_name_regex: push_rule.branch_name_regex }
raise ::Gitlab::GitAccess::UnauthorizedError.new(message)
end
end
commit_validation = push_rule.try(:commit_validation?)
# if newrev is blank, the branch was deleted
return if deletion? || !commit_validation
logger.log_timed(LOG_MESSAGES[:push_rule_commits_check]) do
commits.each do |commit|
logger.check_timeout_reached
push_rule_commit_check(commit)
end
end
rescue ::PushRule::MatchError => e
raise ::Gitlab::GitAccess::UnauthorizedError, e.message
end
def branch_name_allowed_by_push_rule?
return true if skip_branch_name_push_rule?
push_rule.branch_name_allowed?(branch_name)
end
def skip_branch_name_push_rule?
push_rule.nil? ||
deletion? ||
branch_name.blank? ||
branch_name == project.default_branch
end
def tag_deletion_denied_by_push_rule?
push_rule.try(:deny_delete_tag) &&
!updated_from_web? &&
deletion? &&
tag_exists?
end
def push_rule_commit_check(commit)
if push_rule.try(:commit_validation?)
error = check_commit(commit)
raise ::Gitlab::GitAccess::UnauthorizedError, error if error
end
end
# If commit does not pass push rule validation the whole push should be rejected.
# This method should return nil if no error found or a string if error.
# In case of errors - all other checks will be canceled and push will be rejected.
def check_commit(commit)
unless push_rule.commit_message_allowed?(commit.safe_message)
return "Commit message does not follow the pattern '#{push_rule.commit_message_regex}'"
end
if push_rule.commit_message_blocked?(commit.safe_message) PushRuleCheck.new(self).validate!
return "Commit message contains the forbidden pattern '#{push_rule.commit_message_negative_regex}'"
end
unless push_rule.author_email_allowed?(commit.committer_email)
return "Committer's email '#{commit.committer_email}' does not follow the pattern '#{push_rule.author_email_regex}'"
end
unless push_rule.author_email_allowed?(commit.author_email)
return "Author's email '#{commit.author_email}' does not follow the pattern '#{push_rule.author_email_regex}'"
end
committer_error_message = committer_check(commit)
return committer_error_message if committer_error_message
if !updated_from_web? && !push_rule.commit_signature_allowed?(commit)
return "Commit must be signed with a GPG key"
end
# Check whether author is a GitLab member
if push_rule.member_check
unless ::User.find_by_any_email(commit.author_email).present?
return "Author '#{commit.author_email}' is not a member of team"
end
if commit.author_email.casecmp(commit.committer_email) == -1
unless ::User.find_by_any_email(commit.committer_email).present?
return "Committer '#{commit.committer_email}' is not a member of team"
end
end
end
nil
end
def committer_check(commit)
unless push_rule.committer_allowed?(commit.committer_email, user_access.user)
committer_is_current_user = commit.committer == user_access.user
if committer_is_current_user && !commit.committer.verified_email?(commit.committer_email)
ERROR_MESSAGES[:push_rule_committer_not_verified] % { committer_email: commit.committer_email }
else
ERROR_MESSAGES[:push_rule_committer_not_allowed] % { committer_email: commit.committer_email }
end
end
end
override :should_run_commit_validations?
def should_run_commit_validations?
super || validate_path_locks? || push_rule_checks_commit?
end
def push_rule_checks_commit?
return false unless push_rule
push_rule.file_name_regex.present? || push_rule.prevent_secrets
end
override :validations_for_commit
def validations_for_commit(commit)
validations = super
validations.push(path_locks_validation) if validate_path_locks?
validations.concat(push_rule_commit_validations(commit))
end
def push_rule_commit_validations(commit)
return [] unless push_rule
[file_name_validation]
end
def validate_path_locks?
strong_memoize(:validate_path_locks) do
project.feature_available?(:file_locks) &&
newrev && oldrev && project.any_path_locks? &&
project.default_branch == branch_name # locks protect default branch only
end
end
def path_locks_validation
lambda do |diff|
path = diff.new_path || diff.old_path
lock_info = project.find_path_lock(path)
if lock_info && lock_info.user != user_access.user
return "The path '#{lock_info.path}' is locked by #{lock_info.user.name}"
end
end
end
def file_name_validation
lambda do |diff|
begin
if (diff.renamed_file || diff.new_file) && blacklisted_regex = push_rule.filename_blacklisted?(diff.new_path)
return nil unless blacklisted_regex.present?
"File name #{diff.new_path} was blacklisted by the pattern #{blacklisted_regex}."
end
rescue ::PushRule::MatchError => e
raise ::Gitlab::GitAccess::UnauthorizedError, e.message
end
end
end end
end end
end end
......
# frozen_string_literal: true
module EE
module Gitlab
module Checks
module DiffCheck
extend ActiveSupport::Concern
extend ::Gitlab::Utils::Override
private
override :should_run_diff_validations?
def should_run_diff_validations?
super || validate_path_locks? || push_rule_checks_commit?
end
def validate_path_locks?
strong_memoize(:validate_path_locks) do
project.feature_available?(:file_locks) &&
project.any_path_locks? &&
project.default_branch == branch_name # locks protect default branch only
end
end
def push_rule_checks_commit?
return false unless push_rule
push_rule.file_name_regex.present? || push_rule.prevent_secrets
end
override :validations_for_diff
def validations_for_diff
super.tap do |validations|
validations.push(path_locks_validation) if validate_path_locks?
validations.push(file_name_validation) if push_rule
end
end
def path_locks_validation
lambda do |diff|
path = diff.new_path || diff.old_path
lock_info = project.find_path_lock(path)
if lock_info && lock_info.user != user_access.user
return "The path '#{lock_info.path}' is locked by #{lock_info.user.name}"
end
end
end
def file_name_validation
lambda do |diff|
begin
if (diff.renamed_file || diff.new_file) && blacklisted_regex = push_rule.filename_blacklisted?(diff.new_path)
return nil unless blacklisted_regex.present?
"File name #{diff.new_path} was blacklisted by the pattern #{blacklisted_regex}."
end
rescue ::PushRule::MatchError => e
raise ::Gitlab::GitAccess::UnauthorizedError, e.message
end
end
end
end
end
end
end
# frozen_string_literal: true
module EE
module Gitlab
module Checks
class PushRuleCheck < ::Gitlab::Checks::BaseChecker
def validate!
return unless push_rule
if tag_name
PushRules::TagCheck.new(change_access).validate!
else
PushRules::BranchCheck.new(change_access).validate!
end
PushRules::FileSizeCheck.new(change_access).validate!
end
end
end
end
end
# frozen_string_literal: true
module EE
module Gitlab
module Checks
module PushRules
class BranchCheck < ::Gitlab::Checks::BaseChecker
ERROR_MESSAGE = "Branch name does not follow the pattern '%{branch_name_regex}'".freeze
LOG_MESSAGE = "Checking if branch follows the naming patterns defined by the project...".freeze
def validate!
return unless newrev && oldrev && push_rule
logger.log_timed(LOG_MESSAGE) do
unless branch_name_allowed_by_push_rule?
message = ERROR_MESSAGE % { branch_name_regex: push_rule.branch_name_regex }
raise ::Gitlab::GitAccess::UnauthorizedError.new(message)
end
end
PushRules::CommitCheck.new(change_access).validate!
rescue ::PushRule::MatchError => e
raise ::Gitlab::GitAccess::UnauthorizedError, e.message
end
private
def branch_name_allowed_by_push_rule?
return true if skip_branch_name_push_rule?
push_rule.branch_name_allowed?(branch_name)
end
def skip_branch_name_push_rule?
deletion? ||
branch_name.blank? ||
branch_name == project.default_branch
end
end
end
end
end
end
# frozen_string_literal: true
module EE
module Gitlab
module Checks
module PushRules
class CommitCheck < ::Gitlab::Checks::BaseChecker
ERROR_MESSAGES = {
committer_not_verified: "Committer email '%{committer_email}' is not verified.",
committer_not_allowed: "You cannot push commits for '%{committer_email}'. You can only push commits that were committed with one of your own verified emails."
}.freeze
LOG_MESSAGE = "Checking if commits follow defined push rules...".freeze
def validate!
return unless newrev && oldrev && push_rule
commit_validation = push_rule.commit_validation?
# if newrev is blank, the branch was deleted
return if deletion? || !commit_validation
logger.log_timed(LOG_MESSAGE) do
commits.each do |commit|
logger.check_timeout_reached
push_rule_commit_check(commit)
end
end
rescue ::PushRule::MatchError => e
raise ::Gitlab::GitAccess::UnauthorizedError, e.message
end
private
def push_rule_commit_check(commit)
error = check_commit(commit)
raise ::Gitlab::GitAccess::UnauthorizedError, error if error
end
# If commit does not pass push rule validation the whole push should be rejected.
# This method should return nil if no error found or a string if error.
# In case of errors - all other checks will be canceled and push will be rejected.
def check_commit(commit)
unless push_rule.commit_message_allowed?(commit.safe_message)
return "Commit message does not follow the pattern '#{push_rule.commit_message_regex}'"
end
if push_rule.commit_message_blocked?(commit.safe_message)
return "Commit message contains the forbidden pattern '#{push_rule.commit_message_negative_regex}'"
end
unless push_rule.author_email_allowed?(commit.committer_email)
return "Committer's email '#{commit.committer_email}' does not follow the pattern '#{push_rule.author_email_regex}'"
end
unless push_rule.author_email_allowed?(commit.author_email)
return "Author's email '#{commit.author_email}' does not follow the pattern '#{push_rule.author_email_regex}'"
end
committer_error_message = committer_check(commit)
return committer_error_message if committer_error_message
if !updated_from_web? && !push_rule.commit_signature_allowed?(commit)
return "Commit must be signed with a GPG key"
end
# Check whether author is a GitLab member
if push_rule.member_check
unless ::User.find_by_any_email(commit.author_email).present?
return "Author '#{commit.author_email}' is not a member of team"
end
if commit.author_email.casecmp(commit.committer_email) == -1
unless ::User.find_by_any_email(commit.committer_email).present?
return "Committer '#{commit.committer_email}' is not a member of team"
end
end
end
nil
end
def committer_check(commit)
unless push_rule.committer_allowed?(commit.committer_email, user_access.user)
committer_is_current_user = commit.committer == user_access.user
if committer_is_current_user && !commit.committer.verified_email?(commit.committer_email)
ERROR_MESSAGES[:committer_not_verified] % { committer_email: commit.committer_email }
else
ERROR_MESSAGES[:committer_not_allowed] % { committer_email: commit.committer_email }
end
end
end
end
end
end
end
end
# frozen_string_literal: true
module EE
module Gitlab
module Checks
module PushRules
class FileSizeCheck < ::Gitlab::Checks::BaseChecker
LOG_MESSAGE = "Checking if any files are larger than the allowed size...".freeze
def validate!
return if push_rule.nil? || push_rule.max_file_size.zero?
logger.log_timed(LOG_MESSAGE) do
max_file_size = push_rule.max_file_size
blobs = project.repository.new_blobs(newrev, dynamic_timeout: logger.time_left)
large_blob = blobs.find do |blob|
::Gitlab::Utils.bytes_to_megabytes(blob.size) > max_file_size
end
if large_blob
raise ::Gitlab::GitAccess::UnauthorizedError, %Q{File "#{large_blob.path}" is larger than the allowed size of #{max_file_size} MB}
end
end
end
end
end
end
end
end
# frozen_string_literal: true
module EE
module Gitlab
module Checks
module PushRules
class TagCheck < ::Gitlab::Checks::BaseChecker
def validate!
return unless newrev && oldrev && push_rule
logger.log_timed("Checking if you are allowed to delete a tag...") do
if tag_deletion_denied_by_push_rule?
raise ::Gitlab::GitAccess::UnauthorizedError, 'You cannot delete a tag'
end
end
end
private
def tag_deletion_denied_by_push_rule?
push_rule.deny_delete_tag &&
!updated_from_web? &&
deletion? &&
tag_exists?
end
end
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe EE::Gitlab::Checks::PushRuleCheck do
include_context 'push rules checks context'
let(:push_rule) { create(:push_rule, :commit_message) }
describe '#validate!' do
before do
expect_any_instance_of(EE::Gitlab::Checks::PushRules::FileSizeCheck)
.to receive(:validate!)
end
context 'when tag name exists' do
before do
allow(change_access).to receive(:tag_name).and_return(true)
end
it 'validates tags push rules' do
expect_any_instance_of(EE::Gitlab::Checks::PushRules::TagCheck)
.to receive(:validate!)
expect_any_instance_of(EE::Gitlab::Checks::PushRules::BranchCheck)
.not_to receive(:validate!)
subject.validate!
end
end
context 'when tag name does not exists' do
before do
allow(change_access).to receive(:tag_name).and_return(false)
end
it 'validates branches push rules' do
expect_any_instance_of(EE::Gitlab::Checks::PushRules::TagCheck)
.not_to receive(:validate!)
expect_any_instance_of(EE::Gitlab::Checks::PushRules::BranchCheck)
.to receive(:validate!)
subject.validate!
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe EE::Gitlab::Checks::PushRules::BranchCheck do
include_context 'push rules checks context'
describe '#validate!' do
let!(:push_rule) { create(:push_rule, branch_name_regex: '^(w*)$') }
let(:ref) { 'refs/heads/a-branch-that-is-not-allowed' }
it_behaves_like 'check ignored when push rule unlicensed'
it 'rejects the branch that is not allowed' do
expect { subject.validate! }.to raise_error(Gitlab::GitAccess::UnauthorizedError, "Branch name does not follow the pattern '^(w*)$'")
end
it 'returns an error if the regex is invalid' do
push_rule.branch_name_regex = '+'
expect { subject.validate! }.to raise_error(Gitlab::GitAccess::UnauthorizedError, /\ARegular expression '\+' is invalid/)
end
context 'when the ref is not a branch ref' do
let(:ref) { 'a/ref/thats/not/abranch' }
it 'allows the creation' do
expect { subject.validate! }.not_to raise_error
end
end
context 'when no commits are present' do
before do
allow(project.repository).to receive(:new_commits) { [] }
end
it 'rejects the branch that is not allowed' do
expect { subject.validate! }.to raise_error(Gitlab::GitAccess::UnauthorizedError, "Branch name does not follow the pattern '^(w*)$'")
end
end
context 'when the default branch does not match the push rules' do
let(:push_rule) { create(:push_rule, branch_name_regex: 'not-master') }
let(:ref) { "refs/heads/#{project.default_branch}" }
it 'allows the default branch even if it does not match push rule' do
expect { subject.validate! }.not_to raise_error
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe EE::Gitlab::Checks::PushRules::CommitCheck do
include_context 'push rules checks context'
describe '#validate!' do
context 'commit message rules' do
let!(:push_rule) { create(:push_rule, :commit_message) }
it_behaves_like 'check ignored when push rule unlicensed'
it 'returns an error if the rule fails due to missing required characters' do
expect { subject.validate! }.to raise_error(Gitlab::GitAccess::UnauthorizedError, "Commit message does not follow the pattern '#{push_rule.commit_message_regex}'")
end
it 'returns an error if the rule fails due to forbidden characters' do
push_rule.commit_message_regex = nil
push_rule.commit_message_negative_regex = '.*'
expect { subject.validate! }.to raise_error(Gitlab::GitAccess::UnauthorizedError, "Commit message contains the forbidden pattern '#{push_rule.commit_message_negative_regex}'")
end
it 'returns an error if the regex is invalid' do
push_rule.commit_message_regex = '+'
expect { subject.validate! }.to raise_error(Gitlab::GitAccess::UnauthorizedError, /\ARegular expression '\+' is invalid/)
end
it 'returns an error if the negative regex is invalid' do
push_rule.commit_message_regex = nil
push_rule.commit_message_negative_regex = '+'
expect { subject.validate! }.to raise_error(Gitlab::GitAccess::UnauthorizedError, /\ARegular expression '\+' is invalid/)
end
end
context 'author email rules' do
let!(:push_rule) { create(:push_rule, author_email_regex: '.*@valid.com') }
before do
allow_any_instance_of(Commit).to receive(:committer_email).and_return('mike@valid.com')
allow_any_instance_of(Commit).to receive(:author_email).and_return('mike@valid.com')
end
it_behaves_like 'check ignored when push rule unlicensed'
it 'returns an error if the rule fails for the committer' do
allow_any_instance_of(Commit).to receive(:committer_email).and_return('ana@invalid.com')
expect { subject.validate! }.to raise_error(Gitlab::GitAccess::UnauthorizedError, "Committer's email 'ana@invalid.com' does not follow the pattern '.*@valid.com'")
end
it 'returns an error if the rule fails for the author' do
allow_any_instance_of(Commit).to receive(:author_email).and_return('joan@invalid.com')
expect { subject.validate! }.to raise_error(Gitlab::GitAccess::UnauthorizedError, "Author's email 'joan@invalid.com' does not follow the pattern '.*@valid.com'")
end
it 'returns an error if the regex is invalid' do
push_rule.author_email_regex = '+'
expect { subject.validate! }.to raise_error(Gitlab::GitAccess::UnauthorizedError, /\ARegular expression '\+' is invalid/)
end
end
context 'existing member rules' do
let(:push_rule) { create(:push_rule, member_check: true) }
context 'with private commit email' do
it 'returns error if private commit email was not associated to a user' do
user_email = "#{User.maximum(:id).next}-foo@#{::Gitlab::CurrentSettings.current_application_settings.commit_email_hostname}"
allow_any_instance_of(Commit).to receive(:author_email).and_return(user_email)
expect { subject.validate! }.to raise_error(Gitlab::GitAccess::UnauthorizedError, "Author '#{user_email}' is not a member of team")
end
it 'returns true when private commit email was associated to a user' do
allow_any_instance_of(Commit).to receive(:committer_email).and_return(user.private_commit_email)
allow_any_instance_of(Commit).to receive(:author_email).and_return(user.private_commit_email)
expect { subject.validate! }.not_to raise_error
end
end
context 'without private commit email' do
before do
allow_any_instance_of(Commit).to receive(:author_email).and_return('some@mail.com')
end
it_behaves_like 'check ignored when push rule unlicensed'
it 'returns an error if the commit author is not a GitLab member' do
expect { subject.validate! }.to raise_error(Gitlab::GitAccess::UnauthorizedError, "Author 'some@mail.com' is not a member of team")
end
end
end
context 'GPG sign rules' do
let(:push_rule) { create(:push_rule, reject_unsigned_commits: true) }
before do
stub_licensed_features(reject_unsigned_commits: true)
end
it_behaves_like 'check ignored when push rule unlicensed'
context 'when it is only enabled in Global settings' do
before do
project.push_rule.update_column(:reject_unsigned_commits, nil)
create(:push_rule_sample, reject_unsigned_commits: true)
end
context 'and commit is not signed' do
before do
allow_any_instance_of(Commit).to receive(:has_signature?).and_return(false)
end
it 'returns an error' do
expect { subject.validate! }.to raise_error(Gitlab::GitAccess::UnauthorizedError, "Commit must be signed with a GPG key")
end
end
end
context 'when enabled in Project' do
context 'and commit is not signed' do
before do
allow_any_instance_of(Commit).to receive(:has_signature?).and_return(false)
end
it 'returns an error' do
expect { subject.validate! }.to raise_error(Gitlab::GitAccess::UnauthorizedError, "Commit must be signed with a GPG key")
end
context 'but the change is made in the web application' do
let(:protocol) { 'web' }
it 'does not return an error' do
expect { subject.validate! }.not_to raise_error
end
end
end
context 'and commit is signed' do
before do
allow_any_instance_of(Commit).to receive(:has_signature?).and_return(true)
end
it 'does not return an error' do
expect { subject.validate! }.not_to raise_error
end
end
end
context 'when disabled in Project' do
let(:push_rule) { create(:push_rule, reject_unsigned_commits: false) }
context 'and commit is not signed' do
before do
allow_any_instance_of(Commit).to receive(:has_signature?).and_return(false)
end
it 'does not return an error' do
expect { subject.validate! }.not_to raise_error
end
end
end
end
context 'Check commit author rules' do
let(:push_rule) { create(:push_rule, commit_committer_check: true) }
before do
stub_licensed_features(commit_committer_check: true)
end
context 'with a commit from the authenticated user' do
context 'with private commit email' do
it 'allows the commit when they were done with private commit email of the current user' do
allow_any_instance_of(Commit).to receive(:committer_email).and_return(user.private_commit_email)
expect { subject.validate! }.not_to raise_error
end
it 'raises an error when using an unknown private commit email' do
user_email = "#{User.maximum(:id).next}-foobar@users.noreply.gitlab.com"
allow_any_instance_of(Commit).to receive(:committer_email).and_return(user_email)
expect { subject.validate! }
.to raise_error(Gitlab::GitAccess::UnauthorizedError,
"You cannot push commits for '#{user_email}'. You can only push commits that were committed with one of your own verified emails.")
end
end
context 'without private commit email' do
before do
allow_any_instance_of(Commit).to receive(:committer_email).and_return(user.email)
end
it 'does not return an error' do
expect { subject.validate! }.not_to raise_error
end
it 'allows the commit when they were done with another email that belongs to the current user' do
user.emails.create(email: 'secondary_email@user.com', confirmed_at: Time.now)
allow_any_instance_of(Commit).to receive(:committer_email).and_return('secondary_email@user.com')
expect { subject.validate! }.not_to raise_error
end
it 'raises an error when the commit was done with an unverified email' do
user.emails.create(email: 'secondary_email@user.com')
allow_any_instance_of(Commit).to receive(:committer_email).and_return('secondary_email@user.com')
expect { subject.validate! }
.to raise_error(Gitlab::GitAccess::UnauthorizedError,
"Committer email 'secondary_email@user.com' is not verified.")
end
it 'raises an error when using an unknown email' do
allow_any_instance_of(Commit).to receive(:committer_email).and_return('some@mail.com')
expect { subject.validate! }
.to raise_error(Gitlab::GitAccess::UnauthorizedError,
"You cannot push commits for 'some@mail.com'. You can only push commits that were committed with one of your own verified emails.")
end
end
end
context 'for an ff merge request' do
# the signed-commits branch fast-forwards onto master
let(:newrev) { "2d1096e3a0ecf1d2baf6dee036cc80775d4940ba" }
before do
allow(project.repository).to receive(:new_commits).and_call_original
end
it 'does not raise errors for a fast forward' do
expect(subject).not_to receive(:committer_check)
expect { subject.validate! }.not_to raise_error
end
end
context 'for a normal merge' do
# This creates a merge commit without adding it to a target branch
# that is what the repository would look like during the `pre-receive` hook.
#
# That means only the merge commit should be validated.
let(:newrev) do
rugged = rugged_repo(project.repository)
base = oldrev
to_merge = '2d1096e3a0ecf1d2baf6dee036cc80775d4940ba'
merge_index = rugged.merge_commits(base, to_merge)
options = {
parents: [base, to_merge],
tree: merge_index.write_tree(rugged),
message: 'The merge commit',
author: { name: user.name, email: user.email, time: Time.now },
committer: { name: user.name, email: user.email, time: Time.now }
}
Rugged::Commit.create(rugged, options)
end
before do
allow(project.repository).to receive(:new_commits).and_call_original
end
it 'does not raise errors for a merge commit' do
expect(subject).to receive(:committer_check).once
.and_call_original
expect { subject.validate! }.not_to raise_error
end
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe EE::Gitlab::Checks::PushRules::FileSizeCheck do
include_context 'push rules checks context'
describe '#validate!' do
let(:push_rule) { create(:push_rule, max_file_size: 1) }
# SHA of the 2-mb-file branch
let(:newrev) { 'bf12d2567099e26f59692896f73ac819bae45b00' }
let(:ref) { 'my-branch' }
before do
# Delete branch so Repository#new_blobs can return results
project.repository.delete_branch('2-mb-file')
end
it_behaves_like 'check ignored when push rule unlicensed'
it 'returns an error if file exceeds the maximum file size' do
expect { subject.validate! }.to raise_error(Gitlab::GitAccess::UnauthorizedError, "File \"file.bin\" is larger than the allowed size of 1 MB")
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe EE::Gitlab::Checks::PushRules::TagCheck do
include_context 'push rules checks context'
describe '#validate!' do
let(:push_rule) { create(:push_rule, deny_delete_tag: true) }
let(:oldrev) { 'be93687618e4b132087f430a4d8fc3a609c9b77c' }
let(:newrev) { '0000000000000000000000000000000000000000' }
let(:ref) { 'refs/tags/v1.0.0' }
it_behaves_like 'check ignored when push rule unlicensed'
it 'returns an error if the rule denies tag deletion' do
expect { subject.validate! }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You cannot delete a tag')
end
context 'when tag is deleted in web UI' do
let(:protocol) { 'web' }
it 'ignores the push rule' do
expect(subject.validate!).to be_truthy
end
end
end
end
# frozen_string_literal: true
require 'spec_helper' require 'spec_helper'
describe Gitlab::Checks::ChangeAccess do describe Gitlab::Checks::ChangeAccess do
include GitHelpers
describe '#exec' do describe '#exec' do
let(:user) { create(:user) } include_context 'push rules checks context'
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) }
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
context 'push rules checks' do
shared_examples 'check ignored when push rule unlicensed' do
before do
stub_licensed_features(push_rules: false)
end
it { is_expected.to be_truthy }
end
let(:project) { create(:project, :public, :repository, push_rule: push_rule) }
before do
allow(project.repository).to receive(:new_commits).and_return(
project.repository.commits_between('be93687618e4b132087f430a4d8fc3a609c9b77c', '54fcc214b94e78d7a41a9a8fe6d87a5e59500e51')
)
end
context 'tag deletion' do
let(:push_rule) { create(:push_rule, deny_delete_tag: true) } let(:push_rule) { create(:push_rule, deny_delete_tag: true) }
let(:oldrev) { 'be93687618e4b132087f430a4d8fc3a609c9b77c' }
let(:newrev) { '0000000000000000000000000000000000000000' }
let(:ref) { 'refs/tags/v1.0.0' }
before do
project.add_maintainer(user)
end
it_behaves_like 'check ignored when push rule unlicensed'
it 'returns an error if the rule denies tag deletion' do
expect { subject.exec }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You cannot delete a tag')
end
context 'when tag is deleted in web UI' do
let(:protocol) { 'web' }
it 'ignores the push rule' do
expect(subject.exec).to be_truthy
end
end
end
context 'commit message rules' do
let!(:push_rule) { create(:push_rule, :commit_message) }
it_behaves_like 'check ignored when push rule unlicensed'
it 'returns an error if the rule fails due to missing required characters' do
expect { subject.exec }.to raise_error(Gitlab::GitAccess::UnauthorizedError, "Commit message does not follow the pattern '#{push_rule.commit_message_regex}'")
end
it 'returns an error if the rule fails due to forbidden characters' do
push_rule.commit_message_regex = nil
push_rule.commit_message_negative_regex = '.*'
expect { subject.exec }.to raise_error(Gitlab::GitAccess::UnauthorizedError, "Commit message contains the forbidden pattern '#{push_rule.commit_message_negative_regex}'")
end
it 'returns an error if the regex is invalid' do
push_rule.commit_message_regex = '+'
expect { subject.exec }.to raise_error(Gitlab::GitAccess::UnauthorizedError, /\ARegular expression '\+' is invalid/)
end
it 'returns an error if the negative regex is invalid' do
push_rule.commit_message_regex = nil
push_rule.commit_message_negative_regex = '+'
expect { subject.exec }.to raise_error(Gitlab::GitAccess::UnauthorizedError, /\ARegular expression '\+' is invalid/)
end
end
context 'author email rules' do
let!(:push_rule) { create(:push_rule, author_email_regex: '.*@valid.com') }
before do
allow_any_instance_of(Commit).to receive(:committer_email).and_return('mike@valid.com')
allow_any_instance_of(Commit).to receive(:author_email).and_return('mike@valid.com')
end
it_behaves_like 'check ignored when push rule unlicensed'
it 'returns an error if the rule fails for the committer' do
allow_any_instance_of(Commit).to receive(:committer_email).and_return('ana@invalid.com')
expect { subject.exec }.to raise_error(Gitlab::GitAccess::UnauthorizedError, "Committer's email 'ana@invalid.com' does not follow the pattern '.*@valid.com'")
end
it 'returns an error if the rule fails for the author' do
allow_any_instance_of(Commit).to receive(:author_email).and_return('joan@invalid.com')
expect { subject.exec }.to raise_error(Gitlab::GitAccess::UnauthorizedError, "Author's email 'joan@invalid.com' does not follow the pattern '.*@valid.com'")
end
it 'returns an error if the regex is invalid' do
push_rule.author_email_regex = '+'
expect { subject.exec }.to raise_error(Gitlab::GitAccess::UnauthorizedError, /\ARegular expression '\+' is invalid/)
end
end
context 'branch name rules' do
let!(:push_rule) { create(:push_rule, branch_name_regex: '^(w*)$') }
let(:ref) { 'refs/heads/a-branch-that-is-not-allowed' }
it_behaves_like 'check ignored when push rule unlicensed'
it 'rejects the branch that is not allowed' do
expect { subject.exec }.to raise_error(Gitlab::GitAccess::UnauthorizedError, "Branch name does not follow the pattern '^(w*)$'")
end
it 'returns an error if the regex is invalid' do
push_rule.branch_name_regex = '+'
expect { subject.exec }.to raise_error(Gitlab::GitAccess::UnauthorizedError, /\ARegular expression '\+' is invalid/)
end
context 'when the ref is not a branch ref' do
let(:ref) { 'a/ref/thats/not/abranch' }
it 'allows the creation' do
expect { subject.exec }.not_to raise_error
end
end
context 'when no commits are present' do
before do
allow(project.repository).to receive(:new_commits) { [] }
end
it 'rejects the branch that is not allowed' do
expect { subject.exec }.to raise_error(Gitlab::GitAccess::UnauthorizedError, "Branch name does not follow the pattern '^(w*)$'")
end
end
context 'when the default branch does not match the push rules' do
let(:push_rule) { create(:push_rule, branch_name_regex: 'not-master') }
let(:ref) { "refs/heads/#{project.default_branch}" }
it 'allows the default branch even if it does not match push rule' do
expect { subject.exec }.not_to raise_error
end
it 'memoizes the validate_path_locks? call' do
expect(project.path_locks).to receive(:any?).once.and_call_original
2.times { subject.exec }
end
end
end
context 'existing member rules' do
let(:push_rule) { create(:push_rule, member_check: true) }
context 'with private commit email' do
it 'returns error if private commit email was not associated to a user' do
user_email = "#{User.maximum(:id).next}-foo@#{::Gitlab::CurrentSettings.current_application_settings.commit_email_hostname}"
allow_any_instance_of(Commit).to receive(:author_email).and_return(user_email)
expect { subject.exec }.to raise_error(Gitlab::GitAccess::UnauthorizedError, "Author '#{user_email}' is not a member of team")
end
it 'returns true when private commit email was associated to a user' do
allow_any_instance_of(Commit).to receive(:committer_email).and_return(user.private_commit_email)
allow_any_instance_of(Commit).to receive(:author_email).and_return(user.private_commit_email)
expect { subject.exec }.not_to raise_error
end
end
context 'without private commit email' do
before do
allow_any_instance_of(Commit).to receive(:author_email).and_return('some@mail.com')
end
it_behaves_like 'check ignored when push rule unlicensed'
it 'returns an error if the commit author is not a GitLab member' do
expect { subject.exec }.to raise_error(Gitlab::GitAccess::UnauthorizedError, "Author 'some@mail.com' is not a member of team")
end
end
end
context 'file name rules' do
# Notice that the commit used creates a file named 'README'
context 'file name regex check' do
let!(:push_rule) { create(:push_rule, file_name_regex: 'READ*') }
it_behaves_like 'check ignored when push rule unlicensed'
it "returns an error if a new or renamed filed doesn't match the file name regex" do
expect { subject.exec }.to raise_error(Gitlab::GitAccess::UnauthorizedError, "File name README was blacklisted by the pattern READ*.")
end
it 'returns an error if the regex is invalid' do
push_rule.file_name_regex = '+'
expect { subject.exec }.to raise_error(Gitlab::GitAccess::UnauthorizedError, /\ARegular expression '\+' is invalid/)
end
end
context 'blacklisted files check' do subject { change_access }
let(:push_rule) { create(:push_rule, prevent_secrets: true) }
it_behaves_like 'check ignored when push rule unlicensed' it_behaves_like 'check ignored when push rule unlicensed'
it "returns true if there is no blacklisted files" do it 'calls push rules validators' do
new_rev = nil expect_any_instance_of(EE::Gitlab::Checks::PushRuleCheck).to receive(:validate!)
white_listed = subject.exec
[
'readme.txt', 'any/ida_rsa.pub', 'any/id_dsa.pub', 'any_2/id_ed25519.pub',
'random_file.pdf', 'folder/id_ecdsa.pub', 'docs/aws/credentials.md', 'ending_withhistory'
]
white_listed.each do |file_path|
old_rev = 'be93687618e4b132087f430a4d8fc3a609c9b77c'
old_rev = new_rev if new_rev
new_rev = project.repository.create_file(user, file_path, "commit #{file_path}", message: "commit #{file_path}", branch_name: "master")
allow(project.repository).to receive(:new_commits).and_return(
project.repository.commits_between(old_rev, new_rev)
)
expect(subject.exec).to be_truthy
end
end
it "returns an error if a new or renamed filed doesn't match the file name regex" do
new_rev = nil
black_listed =
[
'aws/credentials', '.ssh/personal_rsa', 'config/server_rsa', '.ssh/id_rsa', '.ssh/id_dsa',
'.ssh/personal_dsa', 'config/server_ed25519', 'any/id_ed25519', '.ssh/personal_ecdsa', 'config/server_ecdsa',
'any_place/id_ecdsa', 'some_pLace/file.key', 'other_PlAcE/other_file.pem', 'bye_bug.history', 'pg_sql_history'
]
black_listed.each do |file_path|
old_rev = 'be93687618e4b132087f430a4d8fc3a609c9b77c'
old_rev = new_rev if new_rev
new_rev = project.repository.create_file(user, file_path, "commit #{file_path}", message: "commit #{file_path}", branch_name: "master")
allow(subject).to receive(:commits).and_return(
project.repository.commits_between(old_rev, new_rev)
)
expect { subject.exec }.to raise_error(Gitlab::GitAccess::UnauthorizedError, /File name #{file_path} was blacklisted by the pattern/)
end
end
end
end
context 'max file size rules' do
let(:push_rule) { create(:push_rule, max_file_size: 1) }
# SHA of the 2-mb-file branch
let(:newrev) { 'bf12d2567099e26f59692896f73ac819bae45b00' }
let(:ref) { 'my-branch' }
before do
# Delete branch so Repository#new_blobs can return results
project.repository.delete_branch('2-mb-file')
end
it_behaves_like 'check ignored when push rule unlicensed'
it 'returns an error if file exceeds the maximum file size' do
expect { subject.exec }.to raise_error(Gitlab::GitAccess::UnauthorizedError, "File \"file.bin\" is larger than the allowed size of 1 MB")
end
end
context 'GPG sign rules' do
before do
stub_licensed_features(reject_unsigned_commits: true)
end
let(:push_rule) { create(:push_rule, reject_unsigned_commits: true) }
it_behaves_like 'check ignored when push rule unlicensed'
context 'when it is only enabled in Global settings' do
before do
project.push_rule.update_column(:reject_unsigned_commits, nil)
create(:push_rule_sample, reject_unsigned_commits: true)
end
context 'and commit is not signed' do
before do
allow_any_instance_of(Commit).to receive(:has_signature?).and_return(false)
end
it 'returns an error' do
expect { subject.exec }.to raise_error(Gitlab::GitAccess::UnauthorizedError, "Commit must be signed with a GPG key")
end
end
end
context 'when enabled in Project' do
context 'and commit is not signed' do
before do
allow_any_instance_of(Commit).to receive(:has_signature?).and_return(false)
end
it 'returns an error' do
expect { subject.exec }.to raise_error(Gitlab::GitAccess::UnauthorizedError, "Commit must be signed with a GPG key")
end
context 'but the change is made in the web application' do
let(:protocol) { 'web' }
it 'does not return an error' do
expect { subject.exec }.not_to raise_error
end
end
end
context 'and commit is signed' do
before do
allow_any_instance_of(Commit).to receive(:has_signature?).and_return(true)
end
it 'does not return an error' do
expect { subject.exec }.not_to raise_error
end
end
end
context 'when disabled in Project' do
let(:push_rule) { create(:push_rule, reject_unsigned_commits: false) }
context 'and commit is not signed' do
before do
allow_any_instance_of(Commit).to receive(:has_signature?).and_return(false)
end
it 'does not return an error' do
expect { subject.exec }.not_to raise_error
end
end
end
end
end
context 'file lock rules' do
let!(:path_lock) { create(:path_lock, path: 'README', project: project) }
before do
allow(project.repository).to receive(:new_commits).and_return(
project.repository.commits_between('be93687618e4b132087f430a4d8fc3a609c9b77c', '54fcc214b94e78d7a41a9a8fe6d87a5e59500e51')
)
end
it 'returns an error if the changes update a path locked by another user' do
expect { subject.exec }.to raise_error(Gitlab::GitAccess::UnauthorizedError, "The path 'README' is locked by #{path_lock.user.name}")
end
end
context 'Check commit author rules' do
before do
stub_licensed_features(commit_committer_check: true)
end
let(:push_rule) { create(:push_rule, commit_committer_check: true) }
let(:project) { create(:project, :public, :repository, push_rule: push_rule) }
context 'with a commit from the authenticated user' do
before do
allow(project.repository).to receive(:new_commits).and_return(
project.repository.commits_between('be93687618e4b132087f430a4d8fc3a609c9b77c', '54fcc214b94e78d7a41a9a8fe6d87a5e59500e51')
)
end
context 'with private commit email' do
it 'allows the commit when they were done with private commit email of the current user' do
allow_any_instance_of(Commit).to receive(:committer_email).and_return(user.private_commit_email)
expect { subject.exec }.not_to raise_error
end
it 'raises an error when using an unknown private commit email' do
user_email = "#{User.maximum(:id).next}-foobar@users.noreply.gitlab.com"
allow_any_instance_of(Commit).to receive(:committer_email).and_return(user_email)
expect { subject.exec }
.to raise_error(Gitlab::GitAccess::UnauthorizedError,
"You cannot push commits for '#{user_email}'. You can only push commits that were committed with one of your own verified emails.")
end
end
context 'without private commit email' do
before do
allow_any_instance_of(Commit).to receive(:committer_email).and_return(user.email)
end
it 'does not return an error' do
expect { subject.exec }.not_to raise_error
end
it 'allows the commit when they were done with another email that belongs to the current user' do
user.emails.create(email: 'secondary_email@user.com', confirmed_at: Time.now)
allow_any_instance_of(Commit).to receive(:committer_email).and_return('secondary_email@user.com')
expect { subject.exec }.not_to raise_error
end
it 'raises an error when the commit was done with an unverified email' do
user.emails.create(email: 'secondary_email@user.com')
allow_any_instance_of(Commit).to receive(:committer_email).and_return('secondary_email@user.com')
expect { subject.exec }
.to raise_error(Gitlab::GitAccess::UnauthorizedError,
"Committer email 'secondary_email@user.com' is not verified.")
end
it 'raises an error when using an unknown email' do
allow_any_instance_of(Commit).to receive(:committer_email).and_return('some@mail.com')
expect { subject.exec }
.to raise_error(Gitlab::GitAccess::UnauthorizedError,
"You cannot push commits for 'some@mail.com'. You can only push commits that were committed with one of your own verified emails.")
end
end
end
context 'for an ff merge request' do
# the signed-commits branch fast-forwards onto master
let(:newrev) { "2d1096e3a0ecf1d2baf6dee036cc80775d4940ba" }
it 'does not raise errors for a fast forward' do
expect(change_access).not_to receive(:committer_check)
expect { subject.exec }.not_to raise_error
end
end
context 'for a normal merge' do
# This creates a merge commit without adding it to a target branch
# that is what the repository would look like during the `pre-receive` hook.
#
# That means only the merge commit should be validated.
let(:newrev) do
rugged = rugged_repo(project.repository)
base = oldrev
to_merge = '2d1096e3a0ecf1d2baf6dee036cc80775d4940ba'
merge_index = rugged.merge_commits(base, to_merge)
options = {
parents: [base, to_merge],
tree: merge_index.write_tree(rugged),
message: 'The merge commit',
author: { name: user.name, email: user.email, time: Time.now },
committer: { name: user.name, email: user.email, time: Time.now }
}
Rugged::Commit.create(rugged, options)
end
it 'does not raise errors for a merge commit' do
expect(change_access).to receive(:committer_check).once
.and_call_original
expect { subject.exec }.not_to raise_error
end
end
end end
end end
end end
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::Checks::DiffCheck do
include_context 'push rules checks context'
describe '#validate!' do
context 'file name rules' do
# Notice that the commit used creates a file named 'README'
context 'file name regex check' do
let!(:push_rule) { create(:push_rule, file_name_regex: 'READ*') }
it_behaves_like 'check ignored when push rule unlicensed'
it "returns an error if a new or renamed filed doesn't match the file name regex" do
expect { subject.validate! }.to raise_error(Gitlab::GitAccess::UnauthorizedError, "File name README was blacklisted by the pattern READ*.")
end
it 'returns an error if the regex is invalid' do
push_rule.file_name_regex = '+'
expect { subject.validate! }.to raise_error(Gitlab::GitAccess::UnauthorizedError, /\ARegular expression '\+' is invalid/)
end
end
context 'blacklisted files check' do
let(:push_rule) { create(:push_rule, prevent_secrets: true) }
it_behaves_like 'check ignored when push rule unlicensed'
it "returns true if there is no blacklisted files" do
new_rev = nil
white_listed =
[
'readme.txt', 'any/ida_rsa.pub', 'any/id_dsa.pub', 'any_2/id_ed25519.pub',
'random_file.pdf', 'folder/id_ecdsa.pub', 'docs/aws/credentials.md', 'ending_withhistory'
]
white_listed.each do |file_path|
old_rev = 'be93687618e4b132087f430a4d8fc3a609c9b77c'
old_rev = new_rev if new_rev
new_rev = project.repository.create_file(user, file_path, "commit #{file_path}", message: "commit #{file_path}", branch_name: "master")
allow(project.repository).to receive(:new_commits).and_return(
project.repository.commits_between(old_rev, new_rev)
)
expect(subject.validate!).to be_truthy
end
end
it "returns an error if a new or renamed filed doesn't match the file name regex" do
new_rev = nil
black_listed =
[
'aws/credentials', '.ssh/personal_rsa', 'config/server_rsa', '.ssh/id_rsa', '.ssh/id_dsa',
'.ssh/personal_dsa', 'config/server_ed25519', 'any/id_ed25519', '.ssh/personal_ecdsa', 'config/server_ecdsa',
'any_place/id_ecdsa', 'some_pLace/file.key', 'other_PlAcE/other_file.pem', 'bye_bug.history', 'pg_sql_history'
]
black_listed.each do |file_path|
old_rev = 'be93687618e4b132087f430a4d8fc3a609c9b77c'
old_rev = new_rev if new_rev
new_rev = project.repository.create_file(user, file_path, "commit #{file_path}", message: "commit #{file_path}", branch_name: "master")
allow(subject).to receive(:commits).and_return(
project.repository.commits_between(old_rev, new_rev)
)
expect { subject.validate! }.to raise_error(Gitlab::GitAccess::UnauthorizedError, /File name #{file_path} was blacklisted by the pattern/)
end
end
end
end
context 'file lock rules' do
let(:project) { create(:project, :repository) }
it 'returns an error if the changes update a path locked by another user' do
path_lock = create(:path_lock, path: 'README', project: project)
expect { subject.validate! }.to raise_error(Gitlab::GitAccess::UnauthorizedError, "The path 'README' is locked by #{path_lock.user.name}")
end
it 'memoizes the validate_path_locks? call' do
expect(project).to receive(:any_path_locks?).once.and_call_original
2.times { subject.validate! }
end
end
end
end
# frozen_string_literal: true
shared_context 'push rules checks context' do
include_context 'change access checks context'
let(:project) { create(:project, :public, :repository, push_rule: push_rule) }
before do
allow(project.repository).to receive(:new_commits).and_return(
project.repository.commits_between('be93687618e4b132087f430a4d8fc3a609c9b77c', '54fcc214b94e78d7a41a9a8fe6d87a5e59500e51')
)
end
end
# frozen_string_literal: true
shared_examples 'check ignored when push rule unlicensed' do
before do
stub_licensed_features(push_rules: false)
end
it { is_expected.to be_truthy }
end
# frozen_string_literal: true
module Gitlab
module Checks
class BaseChecker
prepend EE::Gitlab::Checks::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
# 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
...@@ -5,35 +5,11 @@ module Gitlab ...@@ -5,35 +5,11 @@ module Gitlab
class ChangeAccess class ChangeAccess
prepend EE::Gitlab::Checks::ChangeAccess prepend EE::Gitlab::Checks::ChangeAccess
ERROR_MESSAGES = { ATTRIBUTES = %i[user_access project skip_authorization
push_code: 'You are not allowed to push code to this project.', skip_lfs_integrity_check protocol oldrev newrev ref
delete_default_branch: 'The default branch of a project cannot be deleted.', branch_name tag_name logger commits].freeze
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
LOG_MESSAGES = { attr_reader(*ATTRIBUTES)
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
def initialize( def initialize(
change, user_access:, project:, skip_authorization: false, change, user_access:, project:, skip_authorization: false,
...@@ -52,206 +28,32 @@ module Gitlab ...@@ -52,206 +28,32 @@ module Gitlab
@logger.append_message("Running checks for ref: #{@branch_name || @tag_name}") @logger.append_message("Running checks for ref: #{@branch_name || @tag_name}")
end end
def exec(skip_commits_check: false) def exec
return true if skip_authorization return true if skip_authorization
push_checks ref_level_checks
branch_checks # Check of commits should happen as the last step
tag_checks # given they're expensive in terms of performance
lfs_objects_exist_check unless skip_lfs_integrity_check commits_check
commits_check unless skip_commits_check
true true
end end
protected def commits
@commits ||= project.repository.new_commits(newrev)
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
end end
def protected_tag_checks protected
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) def ref_level_checks
raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:create_protected_tag] Gitlab::Checks::PushCheck.new(self).validate!
end Gitlab::Checks::BranchCheck.new(self).validate!
end Gitlab::Checks::TagCheck.new(self).validate!
Gitlab::Checks::LfsCheck.new(self).validate!
end end
def commits_check def commits_check
return if deletion? || newrev.nil? Gitlab::Checks::DiffCheck.new(self).validate!
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)
end end
end end
end end
......
...@@ -2,53 +2,92 @@ ...@@ -2,53 +2,92 @@
module Gitlab module Gitlab
module Checks module Checks
class CommitCheck class DiffCheck < BaseChecker
include Gitlab::Utils::StrongMemoize include Gitlab::Utils::StrongMemoize
prepend EE::Gitlab::Checks::DiffCheck
attr_reader :project, :user, :newrev, :oldrev LOG_MESSAGES = {
validate_file_paths: "Validating diffs' file paths...",
diff_content_check: "Validating diff contents..."
}.freeze
def initialize(project, user, newrev, oldrev) def validate!
@project = project return unless should_run_diff_validations?
@user = user return if commits.empty?
@newrev = newrev return unless uses_raw_delta_validations?
@oldrev = oldrev
@file_paths = [] file_paths = []
process_raw_deltas do |diff|
file_paths << (diff.new_path || diff.old_path)
validate_diff(diff)
end end
def validate(commit, validations) validate_file_paths(file_paths)
return if validations.empty? && path_validations.empty? end
commit.raw_deltas.each do |diff| private
@file_paths << (diff.new_path || diff.old_path)
validations.each do |validation| def should_run_diff_validations?
if error = validation.call(diff) newrev && oldrev && !deletion? && validate_lfs_file_locks?
raise ::Gitlab::GitAccess::UnauthorizedError, error
end end
def validate_lfs_file_locks?
strong_memoize(:validate_lfs_file_locks) do
project.lfs_enabled? && project.any_lfs_file_locks?
end end
end end
def uses_raw_delta_validations?
validations_for_diff.present? || path_validations.present?
end end
def validate_file_paths def validate_diff(diff)
path_validations.each do |validation| validations_for_diff.each do |validation|
if error = validation.call(@file_paths) if error = validation.call(diff)
raise ::Gitlab::GitAccess::UnauthorizedError, error raise ::Gitlab::GitAccess::UnauthorizedError, error
end end
end end
end end
def validate_lfs_file_locks? # Method overwritten in EE to inject custom validations
strong_memoize(:validate_lfs_file_locks) do def validations_for_diff
project.lfs_enabled? && newrev && oldrev && project.any_lfs_file_locks? []
end end
def path_validations
validate_lfs_file_locks? ? [lfs_file_locks_validation] : []
end end
private 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 # rubocop: disable CodeReuse/ActiveRecord
def lfs_file_locks_validation def lfs_file_locks_validation
lambda do |paths| lambda do |paths|
lfs_lock = project.lfs_file_locks.where(path: paths).where.not(user_id: user.id).first lfs_lock = project.lfs_file_locks.where(path: paths).where.not(user_id: user_access.user.id).take
if lfs_lock if lfs_lock
return "The path '#{lfs_lock.path}' is locked in Git LFS by #{lfs_lock.user.name}" return "The path '#{lfs_lock.path}' is locked in Git LFS by #{lfs_lock.user.name}"
...@@ -56,10 +95,6 @@ module Gitlab ...@@ -56,10 +95,6 @@ module Gitlab
end end
end end
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
def path_validations
validate_lfs_file_locks? ? [lfs_file_locks_validation] : []
end
end end
end end
end end
# 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
# 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
# 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
# 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
...@@ -2,315 +2,56 @@ require 'spec_helper' ...@@ -2,315 +2,56 @@ require 'spec_helper'
describe Gitlab::Checks::ChangeAccess do describe Gitlab::Checks::ChangeAccess do
describe '#exec' do describe '#exec' do
let(:user) { create(:user) } include_context 'change access checks context'
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) }
subject(:change_access) do subject { change_access }
described_class.new(
changes,
project: project,
user_access: user_access,
protocol: protocol,
logger: logger
)
end
before do
project.add_developer(user)
end
context 'without failed checks' do context 'without failed checks' do
it "doesn't raise an error" do it "doesn't raise an error" do
expect { subject.exec }.not_to raise_error expect { subject.exec }.not_to raise_error
end 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)
expect { access.exec }.to raise_error(Gitlab::Checks::TimedLogger::TimeoutError)
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)
expect { subject.exec }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You are not allowed to push code to this project.')
end
end
context 'tags check' do
let(:ref) { 'refs/tags/v1.0.0' }
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.')
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.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
end
end
context 'branches check' do
context 'trying to delete the default branch' do
let(:newrev) { '0000000000000000000000000000000000000000' }
let(:ref) { 'refs/heads/master' }
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 'calls pushes checks' do
it 'raises an error' do expect_any_instance_of(Gitlab::Checks::PushCheck).to receive(:validate!)
expect { subject.exec }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You can only delete protected branches using the web interface.')
end
end
end
end
end
end
context 'LFS integrity check' do
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
context 'with LFS not enabled' do
it 'skips integrity check' do
expect_any_instance_of(Gitlab::Git::LfsChanges).not_to receive(:new_pointers)
subject.exec subject.exec
end 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 it 'calls branches checks' do
expect(project.repository).not_to receive(:new_objects) expect_any_instance_of(Gitlab::Checks::BranchCheck).to receive(:validate!)
subject.exec subject.exec
end end
end
it 'fails if any LFS blobs are missing' do
expect { subject.exec }.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.exec }.not_to raise_error
end
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 it 'calls tags checks' do
allow(project.repository).to receive(:new_commits).and_return( expect_any_instance_of(Gitlab::Checks::TagCheck).to receive(:validate!)
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 subject.exec
end 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 it 'calls lfs checks' do
let(:user) { owner } expect_any_instance_of(Gitlab::Checks::LfsCheck).to receive(:validate!)
it "doesn't raise any error" do
expect { subject.exec }.not_to raise_error
end
end
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 subject.exec
end end
end
context 'with LFS enabled' do it 'calls diff checks' do
before do expect_any_instance_of(Gitlab::Checks::DiffCheck).to receive(:validate!)
allow(project).to receive(:lfs_enabled?).and_return(true)
end
context 'when change is sent by a different user' do subject.exec
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
end end
context 'when change is sent by the author of the lock' do context 'when time limit was reached' do
let(:user) { owner } 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 { access.exec }.to raise_error(Gitlab::Checks::TimedLogger::TimeoutError)
expect { subject.exec }.not_to raise_error
end
end
end end
end end
end end
......
# 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
# 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
# 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
# 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
...@@ -302,7 +302,7 @@ describe 'Git HTTP requests' do ...@@ -302,7 +302,7 @@ describe 'Git HTTP requests' do
it 'rejects pushes with 403 Forbidden' do it 'rejects pushes with 403 Forbidden' do
upload(path, env) do |response| upload(path, env) do |response|
expect(response).to have_gitlab_http_status(:forbidden) 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
end end
......
...@@ -60,9 +60,4 @@ module GitHttpHelpers ...@@ -60,9 +60,4 @@ module GitHttpHelpers
message = Gitlab::GitAccessWiki::ERROR_MESSAGES[error_key] message = Gitlab::GitAccessWiki::ERROR_MESSAGES[error_key]
message || raise("GitAccessWiki error message key '#{error_key}' not found") message || raise("GitAccessWiki error message key '#{error_key}' not found")
end 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 end
# 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
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment