Commit c597b7e0 authored by Fabio Pitino's avatar Fabio Pitino

Merge branch 'id-move-approvals-check-in-service' into 'master'

Add validation checks for approvals services

See merge request gitlab-org/gitlab!36279
parents 1a8c124f 080d9920
...@@ -10,6 +10,10 @@ class MergeRequestPolicy < IssuablePolicy ...@@ -10,6 +10,10 @@ class MergeRequestPolicy < IssuablePolicy
# it would not be safe to prevent :create_note there, since # it would not be safe to prevent :create_note there, since
# note permissions are shared, and this would apply too broadly. # note permissions are shared, and this would apply too broadly.
rule { ~can?(:read_merge_request) }.prevent :create_note rule { ~can?(:read_merge_request) }.prevent :create_note
rule { can?(:update_merge_request) }.policy do
enable :approve_merge_request
end
end end
MergeRequestPolicy.prepend_if_ee('EE::MergeRequestPolicy') MergeRequestPolicy.prepend_if_ee('EE::MergeRequestPolicy')
...@@ -3,19 +3,27 @@ ...@@ -3,19 +3,27 @@
module MergeRequests module MergeRequests
class ApprovalService < MergeRequests::BaseService class ApprovalService < MergeRequests::BaseService
def execute(merge_request) def execute(merge_request)
return unless can_be_approved?(merge_request)
approval = merge_request.approvals.new(user: current_user) approval = merge_request.approvals.new(user: current_user)
return unless save_approval(approval) return success unless save_approval(approval)
reset_approvals_cache(merge_request) reset_approvals_cache(merge_request)
create_event(merge_request) create_event(merge_request)
create_approval_note(merge_request) create_approval_note(merge_request)
mark_pending_todos_as_done(merge_request) mark_pending_todos_as_done(merge_request)
execute_approval_hooks(merge_request, current_user) execute_approval_hooks(merge_request, current_user)
success
end end
private private
def can_be_approved?(merge_request)
current_user.can?(:approve_merge_request, merge_request)
end
def reset_approvals_cache(merge_request) def reset_approvals_cache(merge_request)
merge_request.approvals.reset merge_request.approvals.reset
end end
......
...@@ -4,6 +4,8 @@ module MergeRequests ...@@ -4,6 +4,8 @@ module MergeRequests
class RemoveApprovalService < MergeRequests::BaseService class RemoveApprovalService < MergeRequests::BaseService
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
def execute(merge_request) def execute(merge_request)
return unless approved_by_user?(merge_request)
# paranoid protection against running wrong deletes # paranoid protection against running wrong deletes
return unless merge_request.id && current_user.id return unless merge_request.id && current_user.id
...@@ -15,11 +17,17 @@ module MergeRequests ...@@ -15,11 +17,17 @@ module MergeRequests
reset_approvals_cache(merge_request) reset_approvals_cache(merge_request)
create_note(merge_request) create_note(merge_request)
end end
success
end end
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
private private
def approved_by_user?(merge_request)
merge_request.approved_by_users.include?(current_user)
end
def reset_approvals_cache(merge_request) def reset_approvals_cache(merge_request)
merge_request.approvals.reset merge_request.approvals.reset
end end
......
...@@ -13,7 +13,6 @@ module EE ...@@ -13,7 +13,6 @@ module EE
rule { ~can_override_approvers }.prevent :update_approvers rule { ~can_override_approvers }.prevent :update_approvers
rule { can?(:update_merge_request) }.policy do rule { can?(:update_merge_request) }.policy do
enable :update_approvers enable :update_approvers
enable :approve_merge_request
end end
end end
end end
......
...@@ -9,15 +9,18 @@ module EE ...@@ -9,15 +9,18 @@ module EE
override :execute override :execute
def execute(merge_request) def execute(merge_request)
if incorrect_approval_password?(merge_request) return if incorrect_approval_password?(merge_request)
raise IncorrectApprovalPasswordError
end
super super
end end
private private
override :can_be_approved?
def can_be_approved?(merge_request)
merge_request.can_approve?(current_user)
end
override :reset_approvals_cache override :reset_approvals_cache
def reset_approvals_cache(merge_request) def reset_approvals_cache(merge_request)
merge_request.reset_approval_cache! merge_request.reset_approval_cache!
......
...@@ -7,6 +7,11 @@ module EE ...@@ -7,6 +7,11 @@ module EE
private private
override :approved_by_user?
def approved_by_user?(merge_request)
merge_request.has_approved?(current_user)
end
override :reset_approvals_cache override :reset_approvals_cache
def reset_approvals_cache(merge_request) def reset_approvals_cache(merge_request)
merge_request.reset_approval_cache! merge_request.reset_approval_cache!
......
...@@ -144,17 +144,15 @@ module API ...@@ -144,17 +144,15 @@ module API
post 'approve' do post 'approve' do
merge_request = find_project_merge_request(params[:merge_request_iid]) merge_request = find_project_merge_request(params[:merge_request_iid])
unauthorized! unless merge_request.can_approve?(current_user)
check_sha_param!(params, merge_request) check_sha_param!(params, merge_request)
begin success =
::MergeRequests::ApprovalService ::MergeRequests::ApprovalService
.new(user_project, current_user, params) .new(user_project, current_user, params)
.execute(merge_request) .execute(merge_request)
rescue ::MergeRequests::ApprovalService::IncorrectApprovalPasswordError
unauthorized! unauthorized! unless success
end
present_approval(merge_request) present_approval(merge_request)
end end
...@@ -164,12 +162,12 @@ module API ...@@ -164,12 +162,12 @@ module API
post 'unapprove' do post 'unapprove' do
merge_request = find_project_merge_request(params[:merge_request_iid]) merge_request = find_project_merge_request(params[:merge_request_iid])
not_found! unless merge_request.has_approved?(current_user) success = ::MergeRequests::RemoveApprovalService
::MergeRequests::RemoveApprovalService
.new(user_project, current_user) .new(user_project, current_user)
.execute(merge_request) .execute(merge_request)
not_found! unless success
present_approval(merge_request) present_approval(merge_request)
end end
end end
......
...@@ -11,6 +11,10 @@ RSpec.describe MergeRequests::ApprovalService do ...@@ -11,6 +11,10 @@ RSpec.describe MergeRequests::ApprovalService do
subject(:service) { described_class.new(project, user) } subject(:service) { described_class.new(project, user) }
before do
project.add_developer(user)
end
context 'with invalid approval' do context 'with invalid approval' do
before do before do
allow(merge_request.approvals).to receive(:new).and_return(double(save: false)) allow(merge_request.approvals).to receive(:new).and_return(double(save: false))
...@@ -133,8 +137,8 @@ RSpec.describe MergeRequests::ApprovalService do ...@@ -133,8 +137,8 @@ RSpec.describe MergeRequests::ApprovalService do
user.update(password: 'password') user.update(password: 'password')
end end
context 'when password not specified' do context 'when password not specified' do
it 'raises an error' do it 'does not update the approvals' do
expect { service.execute(merge_request) }.to raise_error(::MergeRequests::ApprovalService::IncorrectApprovalPasswordError) expect { service.execute(merge_request) }.not_to change { merge_request.approvals.size }
end end
end end
...@@ -143,9 +147,10 @@ RSpec.describe MergeRequests::ApprovalService do ...@@ -143,9 +147,10 @@ RSpec.describe MergeRequests::ApprovalService do
{ approval_password: 'incorrect' } { approval_password: 'incorrect' }
end end
it 'raises an error' do it 'does not update the approvals' do
service_with_params = described_class.new(project, user, params) service_with_params = described_class.new(project, user, params)
expect { service_with_params.execute(merge_request) }.to raise_error(::MergeRequests::ApprovalService::IncorrectApprovalPasswordError)
expect { service_with_params.execute(merge_request) }.not_to change { merge_request.approvals.size }
end end
end end
...@@ -154,9 +159,10 @@ RSpec.describe MergeRequests::ApprovalService do ...@@ -154,9 +159,10 @@ RSpec.describe MergeRequests::ApprovalService do
{ approval_password: 'password' } { approval_password: 'password' }
end end
it 'does not raise an error' do it 'approves the merge request' do
service_with_params = described_class.new(project, user, params) service_with_params = described_class.new(project, user, params)
expect { service_with_params.execute(merge_request) }.not_to raise_error(::MergeRequests::ApprovalService::IncorrectApprovalPasswordError)
expect { service_with_params.execute(merge_request) }.to change { merge_request.approvals.size }
end end
end end
end end
......
...@@ -24,6 +24,7 @@ RSpec.describe MergeRequestPolicy do ...@@ -24,6 +24,7 @@ RSpec.describe MergeRequestPolicy do
mr_perms = %i[create_merge_request_in mr_perms = %i[create_merge_request_in
create_merge_request_from create_merge_request_from
read_merge_request read_merge_request
approve_merge_request
create_note].freeze create_note].freeze
shared_examples_for 'a denied user' do shared_examples_for 'a denied user' do
......
...@@ -11,6 +11,10 @@ RSpec.describe MergeRequests::ApprovalService do ...@@ -11,6 +11,10 @@ RSpec.describe MergeRequests::ApprovalService do
subject(:service) { described_class.new(project, user) } subject(:service) { described_class.new(project, user) }
before do
project.add_developer(user)
end
context 'with invalid approval' do context 'with invalid approval' do
before do before do
allow(merge_request.approvals).to receive(:new).and_return(double(save: false)) allow(merge_request.approvals).to receive(:new).and_return(double(save: false))
...@@ -56,5 +60,15 @@ RSpec.describe MergeRequests::ApprovalService do ...@@ -56,5 +60,15 @@ RSpec.describe MergeRequests::ApprovalService do
end end
end end
end end
context 'user cannot update the merge request' do
before do
project.add_guest(user)
end
it 'does not update approvals' do
expect { service.execute(merge_request) }.not_to change { merge_request.approvals.size }
end
end
end end
end end
...@@ -33,5 +33,14 @@ RSpec.describe MergeRequests::RemoveApprovalService do ...@@ -33,5 +33,14 @@ RSpec.describe MergeRequests::RemoveApprovalService do
execute! execute!
end end
end end
context 'with a user who has not approved' do
it 'does not create an unapproval note and triggers web hook' do
expect(service).not_to receive(:execute_hooks)
expect(SystemNoteService).not_to receive(:unapprove_mr)
execute!
end
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