Commit dd5d6c9d authored by Max Woolf's avatar Max Woolf

Do not allow status checks to exist with external protected branches

Previously it was possible to create or update an external
status check with a protected branch of any project, including
a private one.

Adds a validation to external status check to enforce
all external status checks to only contain protected
branches from their projects.

Changelog: security
EE: true
parent d2167efa
...@@ -19,6 +19,7 @@ module MergeRequests ...@@ -19,6 +19,7 @@ module MergeRequests
validates :external_url, presence: true, uniqueness: { scope: :project_id }, addressable_url: true validates :external_url, presence: true, uniqueness: { scope: :project_id }, addressable_url: true
validates :name, uniqueness: { scope: :project_id }, presence: true validates :name, uniqueness: { scope: :project_id }, presence: true
validate :protected_branches_must_belong_to_project
def async_execute(data) def async_execute(data)
return unless protected_branches.none? || protected_branches.by_name(data[:object_attributes][:target_branch]).any? return unless protected_branches.none? || protected_branches.by_name(data[:object_attributes][:target_branch]).any?
...@@ -43,5 +44,9 @@ module MergeRequests ...@@ -43,5 +44,9 @@ module MergeRequests
def payload_data(merge_request_hook_data) def payload_data(merge_request_hook_data)
merge_request_hook_data.merge(external_approval_rule: self.to_h) merge_request_hook_data.merge(external_approval_rule: self.to_h)
end end
def protected_branches_must_belong_to_project
errors.add(:base, 'all protected branches must exist within the project') unless protected_branches.all? { |b| project.protected_branches.include?(b) }
end
end end
end end
...@@ -14,6 +14,15 @@ RSpec.describe MergeRequests::ExternalStatusCheck, type: :model do ...@@ -14,6 +14,15 @@ RSpec.describe MergeRequests::ExternalStatusCheck, type: :model do
it { is_expected.to validate_presence_of(:external_url) } it { is_expected.to validate_presence_of(:external_url) }
it { is_expected.to validate_uniqueness_of(:name).scoped_to(:project_id) } it { is_expected.to validate_uniqueness_of(:name).scoped_to(:project_id) }
it { is_expected.to validate_uniqueness_of(:external_url).scoped_to(:project_id) } it { is_expected.to validate_uniqueness_of(:external_url).scoped_to(:project_id) }
describe 'protected_branches_must_belong_to_project' do
let(:check) { build(:external_status_check, protected_branches: [create(:protected_branch)]) }
it 'is invalid' do
expect(check).to be_invalid
expect(check.errors.messages[:base]).to eq ['all protected branches must exist within the project']
end
end
end end
describe 'to_h' do describe 'to_h' do
...@@ -26,8 +35,8 @@ RSpec.describe MergeRequests::ExternalStatusCheck, type: :model do ...@@ -26,8 +35,8 @@ RSpec.describe MergeRequests::ExternalStatusCheck, type: :model do
let_it_be(:merge_request) { create(:merge_request) } let_it_be(:merge_request) { create(:merge_request) }
let_it_be(:check_belonging_to_different_project) { create(:external_status_check) } let_it_be(:check_belonging_to_different_project) { create(:external_status_check) }
let_it_be(:check_with_no_protected_branches) { create(:external_status_check, project: merge_request.project, protected_branches: []) } let_it_be(:check_with_no_protected_branches) { create(:external_status_check, project: merge_request.project, protected_branches: []) }
let_it_be(:check_with_applicable_protected_branches) { create(:external_status_check, project: merge_request.project, protected_branches: [create(:protected_branch, name: merge_request.target_branch)]) } let_it_be(:check_with_applicable_protected_branches) { create(:external_status_check, project: merge_request.project, protected_branches: [create(:protected_branch, name: merge_request.target_branch, project: merge_request.project)]) }
let_it_be(:check_with_non_applicable_protected_branches) { create(:external_status_check, project: merge_request.project, protected_branches: [create(:protected_branch, name: 'testbranch')]) } let_it_be(:check_with_non_applicable_protected_branches) { create(:external_status_check, project: merge_request.project, protected_branches: [create(:protected_branch, name: 'testbranch', project: merge_request.project)]) }
it 'returns the correct collection of checks' do it 'returns the correct collection of checks' do
expect(merge_request.project.external_status_checks.applicable_to_branch(merge_request.target_branch)).to contain_exactly(check_with_no_protected_branches, check_with_applicable_protected_branches) expect(merge_request.project.external_status_checks.applicable_to_branch(merge_request.target_branch)).to contain_exactly(check_with_no_protected_branches, check_with_applicable_protected_branches)
...@@ -58,7 +67,7 @@ RSpec.describe MergeRequests::ExternalStatusCheck, type: :model do ...@@ -58,7 +67,7 @@ RSpec.describe MergeRequests::ExternalStatusCheck, type: :model do
end end
context 'when data target branch matches a protected branch' do context 'when data target branch matches a protected branch' do
let_it_be(:check) { create(:external_status_check, project: merge_request.project, protected_branches: [create(:protected_branch, name: 'test')]) } let_it_be(:check) { create(:external_status_check, project: merge_request.project, protected_branches: [create(:protected_branch, name: 'test', project: merge_request.project)]) }
it 'enqueues the status check' do it 'enqueues the status check' do
expect(ApprovalRules::ExternalApprovalRulePayloadWorker).to receive(:perform_async).once expect(ApprovalRules::ExternalApprovalRulePayloadWorker).to receive(:perform_async).once
...@@ -68,7 +77,7 @@ RSpec.describe MergeRequests::ExternalStatusCheck, type: :model do ...@@ -68,7 +77,7 @@ RSpec.describe MergeRequests::ExternalStatusCheck, type: :model do
end end
context 'when data target branch does not match a protected branch' do context 'when data target branch does not match a protected branch' do
let_it_be(:check) { create(:external_status_check, project: merge_request.project, protected_branches: [create(:protected_branch, name: 'new-branch')]) } let_it_be(:check) { create(:external_status_check, project: merge_request.project, protected_branches: [create(:protected_branch, name: 'new-branch', project: merge_request.project)]) }
it 'does not enqueue the status check' do it 'does not enqueue the status check' do
expect(ApprovalRules::ExternalApprovalRulePayloadWorker).to receive(:perform_async).never expect(ApprovalRules::ExternalApprovalRulePayloadWorker).to receive(:perform_async).never
......
...@@ -211,7 +211,7 @@ RSpec.describe MergeRequestPresenter do ...@@ -211,7 +211,7 @@ RSpec.describe MergeRequestPresenter do
context 'without applicable branches' do context 'without applicable branches' do
before do before do
create(:external_status_check, project: project, protected_branches: [create(:protected_branch, name: 'testbranch')]) create(:external_status_check, project: project, protected_branches: [create(:protected_branch, name: 'testbranch', project: project)])
end end
it { is_expected.to eq(nil) } it { is_expected.to eq(nil) }
...@@ -227,7 +227,7 @@ RSpec.describe MergeRequestPresenter do ...@@ -227,7 +227,7 @@ RSpec.describe MergeRequestPresenter do
context 'with applicable branches' do context 'with applicable branches' do
before do before do
create(:external_status_check, project: project, protected_branches: [create(:protected_branch, name: merge_request.target_branch)]) create(:external_status_check, project: project, protected_branches: [create(:protected_branch, name: merge_request.target_branch, project: project)])
end end
it { is_expected.to eq(exposed_path) } it { is_expected.to eq(exposed_path) }
......
...@@ -39,8 +39,8 @@ RSpec.describe API::StatusChecks do ...@@ -39,8 +39,8 @@ RSpec.describe API::StatusChecks do
end end
context 'when merge request has received status check responses' do context 'when merge request has received status check responses' do
let!(:non_applicable_check) { create(:external_status_check, project: project, protected_branches: [create(:protected_branch, name: 'different-branch')]) } let!(:non_applicable_check) { create(:external_status_check, project: project, protected_branches: [create(:protected_branch, name: 'different-branch', project: project)]) }
let!(:branch_specific_check) { create(:external_status_check, project: project, protected_branches: [create(:protected_branch, name: merge_request.target_branch)]) } let!(:branch_specific_check) { create(:external_status_check, project: project, protected_branches: [create(:protected_branch, name: merge_request.target_branch, project: project)]) }
let!(:status_check_response) { create(:status_check_response, external_status_check: rule, merge_request: merge_request, sha: sha) } let!(:status_check_response) { create(:status_check_response, external_status_check: rule, merge_request: merge_request, sha: sha) }
it 'returns a 200' do it 'returns a 200' do
...@@ -288,6 +288,24 @@ RSpec.describe API::StatusChecks do ...@@ -288,6 +288,24 @@ RSpec.describe API::StatusChecks do
expect(response).to have_gitlab_http_status(:success) expect(response).to have_gitlab_http_status(:success)
end end
context 'when referencing a protected branch outside of the project' do
let_it_be(:protected_branch) { create(:protected_branch) }
let(:params) do
{ name: 'New rule', external_url: 'https://gitlab.com/test/example.json', protected_branch_ids: protected_branch.id }
end
subject do
put api(single_object_url, project.owner), params: params
end
it 'is invalid' do
subject
expect(response).to have_gitlab_http_status(:unprocessable_entity)
end
end
context 'with protected branches' do context 'with protected branches' do
let_it_be(:protected_branch) { create(:protected_branch, project: project) } let_it_be(:protected_branch) { create(:protected_branch, project: project) }
......
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