Merge branch 'dm-push-rule-regexp-error' into 'master'

Capture push rule regex errors and present them to user

Closes #4585

See merge request gitlab-org/gitlab-ee!4102
parents 1003c04e 37e58a74
......@@ -56,6 +56,9 @@ module MergeRequests
end
true
rescue PushRule::MatchError => e
handle_merge_error(log_message: e.message, save_message_on_model: true)
false
end
private
......
---
title: Capture push rule regex errors and present them to user
merge_request: 4102
author:
type: fixed
class PushRule < ActiveRecord::Base
MatchError = Class.new(StandardError)
belongs_to :project
validates :project, presence: true, unless: "is_sample?"
......@@ -98,6 +100,8 @@ class PushRule < ActiveRecord::Base
else
true
end
rescue RegexpError => e
raise MatchError, "Regular expression '#{regex}' is invalid: #{e.message}"
end
def read_setting_with_global_default(setting)
......
module EE
module Gitlab
module Checks
module ChangeAccess
extend ActiveSupport::Concern
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: "Comitter email '%{commiter_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
override :exec
def exec
return true if skip_authorization
super
push_rule_check
true
end
private
def push_rule_check
return unless newrev && oldrev && project.feature_available?(:push_rules)
push_rule = project.push_rule
if tag_name
push_rule_tag_check(push_rule)
else
push_rule_branch_check(push_rule)
end
end
def push_rule_tag_check(push_rule)
if tag_deletion_denied_by_push_rule?(push_rule)
raise ::Gitlab::GitAccess::UnauthorizedError, 'You cannot delete a tag'
end
end
def push_rule_branch_check(push_rule)
unless branch_name_allowed_by_push_rule?(push_rule)
message = ERROR_MESSAGES[:push_rule_branch_name] % { branch_name_regex: push_rule.branch_name_regex }
raise ::Gitlab::GitAccess::UnauthorizedError.new(message)
end
commit_validation = push_rule.try(:commit_validation?)
# if newrev is blank, the branch was deleted
return if deletion? || !(commit_validation || validate_path_locks?)
# n+1: https://gitlab.com/gitlab-org/gitlab-ee/issues/3593
::Gitlab::GitalyClient.allow_n_plus_1_calls do
commits.each do |commit|
push_rule_commit_check(commit, push_rule)
end
end
rescue ::PushRule::MatchError => e
raise ::Gitlab::GitAccess::UnauthorizedError, e.message
end
def branch_name_allowed_by_push_rule?(push_rule)
return true if skip_branch_name_push_rule?(push_rule)
push_rule.branch_name_allowed?(branch_name)
end
def skip_branch_name_push_rule?(push_rule)
push_rule.nil? ||
deletion? ||
branch_name.blank? ||
branch_name == project.default_branch
end
def tag_deletion_denied_by_push_rule?(push_rule)
push_rule.try(:deny_delete_tag) &&
!updated_from_web? &&
deletion? &&
tag_exists?
end
def push_rule_commit_check(commit, push_rule)
if push_rule.try(:commit_validation?)
error = check_commit(commit, push_rule)
raise ::Gitlab::GitAccess::UnauthorizedError, error if error
end
if error = check_commit_diff(commit, push_rule)
raise ::Gitlab::GitAccess::UnauthorizedError, 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, push_rule)
unless push_rule.commit_message_allowed?(commit.safe_message)
return "Commit message does not follow the pattern '#{push_rule.commit_message_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, push_rule)
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.existing_member?(commit.author_email.downcase)
return "Author '#{commit.author_email}' is not a member of team"
end
if commit.author_email.casecmp(commit.committer_email) == -1
unless ::User.existing_member?(commit.committer_email.downcase)
return "Committer '#{commit.committer_email}' is not a member of team"
end
end
end
nil
end
def committer_check(commit, push_rule)
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
def check_commit_diff(commit, push_rule)
validations = validations_for_commit(commit, push_rule)
return if validations.empty?
commit.raw_deltas.each do |diff|
validations.each do |validation|
if error = validation.call(diff)
return error
end
end
end
nil
end
def validations_for_commit(commit, push_rule)
validations = base_validations
return validations unless push_rule
validations << file_name_validation(push_rule)
if push_rule.max_file_size > 0
validations << file_size_validation(commit, push_rule.max_file_size)
end
validations
end
def base_validations
validate_path_locks? ? [path_locks_validation] : []
end
def validate_path_locks?
strong_memoize(:validate_path_locks) do
project.feature_available?(:file_locks) &&
project.path_locks.any? && newrev && oldrev &&
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(push_rule)
lambda do |diff|
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
end
end
def file_size_validation(commit, max_file_size)
lambda do |diff|
return if diff.deleted_file
blob = project.repository.blob_at(commit.id, diff.new_path)
if blob && blob.size && blob.size > max_file_size.megabytes
return "File #{diff.new_path.inspect} is larger than the allowed size of #{max_file_size} MB"
end
end
end
def commits
project.repository.new_commits(newrev)
end
end
end
end
end
module Gitlab
module Checks
class ChangeAccess
include PathLocksHelper
include Gitlab::Utils::StrongMemoize
prepend EE::Gitlab::Checks::ChangeAccess
ERROR_MESSAGES = {
push_code: 'You are not allowed to push code to this project.',
......@@ -16,14 +15,10 @@ module Gitlab
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".',
push_rule_branch_name: "Branch name does not follow the pattern '%{branch_name_regex}'",
push_rule_committer_not_verified: "Comitter email '%{commiter_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."
lfs_objects_missing: 'LFS objects are missing. Ensure LFS is properly set up or try a manual "git lfs push --all".'
}.freeze
# protocol is currently used only in EE
attr_reader :user_access, :project, :skip_authorization, :protocol
attr_reader :user_access, :project, :skip_authorization, :protocol, :oldrev, :newrev, :ref, :branch_name, :tag_name
def initialize(
change, user_access:, project:, skip_authorization: false,
......@@ -45,7 +40,6 @@ module Gitlab
branch_checks
tag_checks
lfs_objects_exist_check
push_rule_check
true
end
......@@ -59,9 +53,9 @@ module Gitlab
end
def branch_checks
return unless @branch_name
return unless branch_name
if deletion? && @branch_name == project.default_branch
if deletion? && branch_name == project.default_branch
raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:delete_default_branch]
end
......@@ -69,7 +63,7 @@ module Gitlab
end
def protected_branch_checks
return unless ProtectedBranch.protected?(project, @branch_name)
return unless ProtectedBranch.protected?(project, branch_name)
if forced_push?
raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:force_push_protected_branch]
......@@ -83,7 +77,7 @@ module Gitlab
end
def protected_branch_deletion_checks
unless user_access.can_delete_branch?(@branch_name)
unless user_access.can_delete_branch?(branch_name)
raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:non_master_delete_protected_branch]
end
......@@ -94,18 +88,18 @@ module Gitlab
def protected_branch_push_checks
if matching_merge_request?
unless user_access.can_merge_to_branch?(@branch_name) || user_access.can_push_to_branch?(@branch_name)
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)
unless user_access.can_push_to_branch?(branch_name)
raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:push_protected_branch]
end
end
end
def tag_checks
return unless @tag_name
return unless tag_name
if tag_exists? && user_access.cannot_do_action?(:admin_project)
raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:change_existing_tags]
......@@ -115,12 +109,12 @@ module Gitlab
end
def protected_tag_checks
return unless ProtectedTag.protected?(project, @tag_name)
return unless ProtectedTag.protected?(project, tag_name)
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)
unless user_access.can_create_tag?(tag_name)
raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:create_protected_tag]
end
end
......@@ -132,218 +126,32 @@ module Gitlab
end
def tag_exists?
project.repository.tag_exists?(@tag_name)
project.repository.tag_exists?(tag_name)
end
def forced_push?
Gitlab::Checks::ForcePush.force_push?(@project, @oldrev, @newrev)
Gitlab::Checks::ForcePush.force_push?(project, oldrev, newrev)
end
def update?
!Gitlab::Git.blank_ref?(@oldrev) && !deletion?
!Gitlab::Git.blank_ref?(oldrev) && !deletion?
end
def deletion?
Gitlab::Git.blank_ref?(@newrev)
Gitlab::Git.blank_ref?(newrev)
end
def matching_merge_request?
Checks::MatchingMergeRequest.new(@newrev, @branch_name, @project).match?
Checks::MatchingMergeRequest.new(newrev, branch_name, project).match?
end
def lfs_objects_exist_check
lfs_check = Checks::LfsIntegrity.new(project, @newrev)
lfs_check = Checks::LfsIntegrity.new(project, newrev)
if lfs_check.objects_missing?
raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:lfs_objects_missing]
end
end
def push_rule_check
return unless @newrev && @oldrev && project.feature_available?(:push_rules)
push_rule = project.push_rule
# Prevent tag removal
if @tag_name
if tag_deletion_denied_by_push_rule?(push_rule)
raise GitAccess::UnauthorizedError, 'You cannot delete a tag'
end
else
unless branch_name_allowed_by_push_rule?(push_rule)
message = ERROR_MESSAGES[:push_rule_branch_name] % { branch_name_regex: push_rule.branch_name_regex }
raise GitAccess::UnauthorizedError.new(message)
end
commit_validation = push_rule.try(:commit_validation?)
# if newrev is blank, the branch was deleted
return if deletion? || !(commit_validation || validate_path_locks?)
# n+1: https://gitlab.com/gitlab-org/gitlab-ee/issues/3593
Gitlab::GitalyClient.allow_n_plus_1_calls do
commits.each do |commit|
if commit_validation
error = check_commit(commit, push_rule)
raise GitAccess::UnauthorizedError, error if error
end
if error = check_commit_diff(commit, push_rule)
raise GitAccess::UnauthorizedError, error
end
end
end
end
end
def branch_name_allowed_by_push_rule?(push_rule)
return true if skip_branch_name_push_rule?(push_rule)
push_rule.branch_name_allowed?(@branch_name)
end
def skip_branch_name_push_rule?(push_rule)
push_rule.nil? ||
deletion? ||
@branch_name.blank? ||
@branch_name == @project.default_branch
end
def tag_deletion_denied_by_push_rule?(push_rule)
push_rule.try(:deny_delete_tag) &&
!updated_from_web? &&
deletion? &&
tag_exists?
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, push_rule)
unless push_rule.commit_message_allowed?(commit.safe_message)
return "Commit message does not follow the pattern '#{push_rule.commit_message_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, push_rule)
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.existing_member?(commit.author_email.downcase)
return "Author '#{commit.author_email}' is not a member of team"
end
if commit.author_email.casecmp(commit.committer_email) == -1
unless User.existing_member?(commit.committer_email.downcase)
return "Committer '#{commit.committer_email}' is not a member of team"
end
end
end
nil
end
def committer_check(commit, push_rule)
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
def check_commit_diff(commit, push_rule)
validations = validations_for_commit(commit, push_rule)
return if validations.empty?
commit.raw_deltas.each do |diff|
validations.each do |validation|
if error = validation.call(diff)
return error
end
end
end
nil
end
def validations_for_commit(commit, push_rule)
validations = base_validations
return validations unless push_rule
validations << file_name_validation(push_rule)
if push_rule.max_file_size > 0
validations << file_size_validation(commit, push_rule.max_file_size)
end
validations
end
def base_validations
validate_path_locks? ? [path_locks_validation] : []
end
def validate_path_locks?
strong_memoize(:validate_path_locks) do
@project.feature_available?(:file_locks) &&
project.path_locks.any? && @newrev && @oldrev &&
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(push_rule)
lambda do |diff|
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
end
end
def file_size_validation(commit, max_file_size)
lambda do |diff|
return if diff.deleted_file
blob = project.repository.blob_at(commit.id, diff.new_path)
if blob && blob.size && blob.size > max_file_size.megabytes
return "File #{diff.new_path.inspect} is larger than the allowed size of #{max_file_size} MB"
end
end
end
def commits
project.repository.new_commits(@newrev)
end
end
end
end
......@@ -49,6 +49,24 @@ describe PushRule do
end
end
methods_and_regexes = {
commit_message_allowed?: :commit_message_regex,
branch_name_allowed?: :branch_name_regex,
author_email_allowed?: :author_email_regex,
filename_blacklisted?: :file_name_regex
}
methods_and_regexes.each do |method_name, regex_attr|
describe "##{method_name}" do
it 'raises a MatchError when the regex is invalid' do
push_rule[regex_attr] = '+'
expect { push_rule.public_send(method_name, 'foo') } # rubocop:disable GitlabSecurity/PublicSend
.to raise_error(PushRule::MatchError, /\ARegular expression '\+' is invalid/)
end
end
end
describe '#commit_signature_allowed?' do
let!(:premium_license) { create(:license, plan: License::PREMIUM_PLAN) }
let(:signed_commit) { double(has_signature?: true) }
......
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' }
subject(:change_access) do
described_class.new(
changes,
project: project,
user_access: user_access,
protocol: protocol
)
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(:oldrev) { 'be93687618e4b132087f430a4d8fc3a609c9b77c' }
let(:newrev) { '0000000000000000000000000000000000000000' }
let(:ref) { 'refs/tags/v1.0.0' }
before do
project.add_master(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' 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 regex is invalid' do
push_rule.commit_message_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) }
before do
allow(User).to receive(:existing_member?).and_return(false)
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
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
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.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(project.repository).to receive(:new_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) }
before do
allow_any_instance_of(Blob).to receive(:size).and_return(2.megabytes)
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 \"README\" 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')
)
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,
"Comitter email '%{commiter_email}' 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
context 'for an ff merge request' do
# the signed-commits branch fast-forwards onto master
let(:newrev) { '2d1096e3' }
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 = project.repository.raw_repository.rugged
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
......@@ -208,387 +208,5 @@ describe Gitlab::Checks::ChangeAccess do
end
end
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(:oldrev) { 'be93687618e4b132087f430a4d8fc3a609c9b77c' }
let(:newrev) { '0000000000000000000000000000000000000000' }
let(:ref) { 'refs/tags/v1.0.0' }
before do
project.add_master(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' do
expect { subject.exec }.to raise_error(Gitlab::GitAccess::UnauthorizedError, "Commit message does not follow the pattern '#{push_rule.commit_message_regex}'")
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
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
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) }
before do
allow(User).to receive(:existing_member?).and_return(false)
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
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
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.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(project.repository).to receive(:new_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) }
before do
allow_any_instance_of(Blob).to receive(:size).and_return(2.megabytes)
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 \"README\" 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')
)
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,
"Comitter email '%{commiter_email}' 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
context 'for an ff merge request' do
# the signed-commits branch fast-forwards onto master
let(:newrev) { '2d1096e3' }
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 = project.repository.raw_repository.rugged
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
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