Commit b0343dde authored by Douwe Maan's avatar Douwe Maan

Merge branch '3845-committers-cannot-approve-mr' into 'master'

Resolve "Prevent commit authors from self approvaling merge requests"

Closes #3845

See merge request gitlab-org/gitlab-ee!9007
parents 2a299bf9 7503a526
...@@ -19,6 +19,12 @@ class CommitCollection ...@@ -19,6 +19,12 @@ class CommitCollection
commits.each(&block) commits.each(&block)
end end
def committers
emails = commits.reject(&:merge_commit?).map(&:committer_email).uniq
User.by_any_email(emails)
end
# Sets the pipeline status for every commit. # Sets the pipeline status for every commit.
# #
# Setting this status ahead of time removes the need for running a query for # Setting this status ahead of time removes the need for running a query for
......
...@@ -286,6 +286,14 @@ class MergeRequest < ActiveRecord::Base ...@@ -286,6 +286,14 @@ class MergeRequest < ActiveRecord::Base
work_in_progress?(title) ? title : "WIP: #{title}" work_in_progress?(title) ? title : "WIP: #{title}"
end end
def committers
@committers ||= commits.committers
end
def authors
User.from_union([committers, User.where(id: self.author_id)])
end
# Verifies if title has changed not taking into account WIP prefix # Verifies if title has changed not taking into account WIP prefix
# for merge requests. # for merge requests.
def wipless_title_changed(old_title) def wipless_title_changed(old_title)
...@@ -329,13 +337,15 @@ class MergeRequest < ActiveRecord::Base ...@@ -329,13 +337,15 @@ class MergeRequest < ActiveRecord::Base
end end
def commits def commits
if persisted? return merge_request_diff.commits if persisted?
merge_request_diff.commits
elsif compare_commits commits_arr = if compare_commits
compare_commits.reverse compare_commits.reverse
else else
[] []
end end
CommitCollection.new(source_project, commits_arr, source_branch)
end end
def commits_count def commits_count
......
...@@ -52,8 +52,9 @@ A group can also be added as an approver. [In the future](https://gitlab.com/git ...@@ -52,8 +52,9 @@ A group can also be added as an approver. [In the future](https://gitlab.com/git
group approvers will be restricted. group approvers will be restricted.
If a user is added as an individual approver and is also part of a group approver, If a user is added as an individual approver and is also part of a group approver,
then that user is just counted once. The merge request author does not count as then that user is just counted once. The merge request author and users that have committed
an eligible approver, unless [self-approval] is explicitly enabled on the project settings. to the merge request do not count as eligible approvers,
unless [self-approval] is explicitly enabled on the project settings.
### Implicit approvers ### Implicit approvers
......
...@@ -58,7 +58,7 @@ module Approvable ...@@ -58,7 +58,7 @@ module Approvable
# they're included/excluded from that list accordingly. # they're included/excluded from that list accordingly.
return true if approvers_left.include?(user) return true if approvers_left.include?(user)
# We can safely unauthorize authors if it reaches this guard clause. # We can safely unauthorize authors if it reaches this guard clause.
return false if user == author return false if authors.include?(user)
return false unless user.can?(:update_merge_request, self) return false unless user.can?(:update_merge_request, self)
any_approver_allowed? && approvals.where(user: user).empty? any_approver_allowed? && approvals.where(user: user).empty?
......
...@@ -37,8 +37,8 @@ module VisibleApprovable ...@@ -37,8 +37,8 @@ module VisibleApprovable
approvers_relation = target_project.approvers approvers_relation = target_project.approvers
end end
if author && !authors_can_approve? if authors.any? && !authors_can_approve?
approvers_relation = approvers_relation.where.not(user_id: author.id) approvers_relation = approvers_relation.where.not(user_id: authors.select(:id))
end end
results = code_owners.concat(approvers_relation.includes(:user).map(&:user)) results = code_owners.concat(approvers_relation.includes(:user).map(&:user))
...@@ -65,7 +65,12 @@ module VisibleApprovable ...@@ -65,7 +65,12 @@ module VisibleApprovable
def approvers_from_groups def approvers_from_groups
group_approvers = overall_approver_groups.flat_map(&:users) group_approvers = overall_approver_groups.flat_map(&:users)
group_approvers.delete(author) unless authors_can_approve?
unless authors_can_approve?
author_approver_ids = authors.where(id: group_approvers.map(&:id)).pluck(:id)
group_approvers.reject! { |user| author_approver_ids.include?(user.id) }
end
group_approvers group_approvers
end end
......
...@@ -5,7 +5,6 @@ ...@@ -5,7 +5,6 @@
- return unless issuable.is_a?(MergeRequest) - return unless issuable.is_a?(MergeRequest)
- return unless presenter.requires_approve? - return unless presenter.requires_approve?
- author = issuable.author || current_user
- can_update_approvers = can?(current_user, :update_approvers, issuable) - can_update_approvers = can?(current_user, :update_approvers, issuable)
.form-group.row .form-group.row
...@@ -13,7 +12,7 @@ ...@@ -13,7 +12,7 @@
Approvers Approvers
.col-sm-10 .col-sm-10
- if can_update_approvers - if can_update_approvers
- skip_users = [*presenter.all_approvers_including_groups, (author unless presenter.authors_can_approve?)].compact - skip_users = [*presenter.all_approvers_including_groups, *(issuable.authors unless presenter.authors_can_approve?)].compact
= users_select_tag("merge_request[approver_ids]", = users_select_tag("merge_request[approver_ids]",
multiple: true, multiple: true,
......
---
title: Prevent commit authors from self approvaling merge requests
merge_request: 9007
author:
type: changed
...@@ -2,6 +2,8 @@ require 'spec_helper' ...@@ -2,6 +2,8 @@ require 'spec_helper'
describe Approvable do describe Approvable do
let(:merge_request) { create(:merge_request) } let(:merge_request) { create(:merge_request) }
let(:project) { merge_request.project }
let(:author) { merge_request.author }
describe '#approvers_overwritten?' do describe '#approvers_overwritten?' do
subject { merge_request.approvers_overwritten? } subject { merge_request.approvers_overwritten? }
...@@ -23,4 +25,102 @@ describe Approvable do ...@@ -23,4 +25,102 @@ describe Approvable do
is_expected.to be true is_expected.to be true
end end
end end
describe '#can_approve?' do
subject { merge_request.can_approve?(user) }
it 'returns false if user is nil' do
expect(merge_request.can_approve?(nil)).to be false
end
it 'returns true when user is included in the approvers list' do
user = create(:approver, target: merge_request).user
expect(merge_request.can_approve?(user)).to be true
end
context 'when authors can approve' do
before do
project.update(merge_requests_author_approval: true)
end
context 'when the user is the author' do
it 'returns true when user is approver' do
create(:approver, target: merge_request, user: author)
expect(merge_request.can_approve?(author)).to be true
end
it 'returns false when user is not approver' do
expect(merge_request.can_approve?(author)).to be false
end
end
context 'when user is committer' do
let(:user) { create(:user, email: merge_request.commits.first.committer_email) }
before do
project.add_developer(user)
end
it 'returns true when user is approver' do
create(:approver, target: merge_request, user: user)
expect(merge_request.can_approve?(user)).to be true
end
it 'returns false when user is not approver' do
expect(merge_request.can_approve?(user)).to be false
end
end
end
context 'when authors cannot approve' do
before do
project.update(merge_requests_author_approval: false)
end
it 'returns false when user is the author' do
create(:approver, target: merge_request, user: author)
expect(merge_request.can_approve?(author)).to be false
end
it 'returns false when user is a committer' do
user = create(:user, email: merge_request.commits.first.committer_email)
project.add_developer(user)
create(:approver, target: merge_request, user: user)
expect(merge_request.can_approve?(user)).to be false
end
end
it 'returns false when user is unable to update the merge request' do
user = create(:user)
project.add_guest(user)
expect(merge_request.can_approve?(user)).to be false
end
context 'when approvals are required' do
before do
project.update(approvals_before_merge: 1)
end
it 'returns true when approvals are still accepted and user still has not approved' do
user = create(:user)
project.add_developer(user)
expect(merge_request.can_approve?(user)).to be true
end
it 'returns false when there is still one approver missing' do
user = create(:user)
project.add_developer(user)
create(:approver, target: merge_request)
expect(merge_request.can_approve?(user)).to be false
end
end
end
end end
...@@ -20,10 +20,10 @@ describe VisibleApprovable do ...@@ -20,10 +20,10 @@ describe VisibleApprovable do
subject { resource.approvers_left } subject { resource.approvers_left }
it 'only queries once' do it 'avoids N+1 queries' do
expect(User).to receive(:where).and_call_original.once control = ActiveRecord::QueryRecorder.new { subject }
3.times { subject } expect { subject }.not_to exceed_query_limit(control)
end end
it 'returns all approvers left' do it 'returns all approvers left' do
...@@ -69,6 +69,25 @@ describe VisibleApprovable do ...@@ -69,6 +69,25 @@ describe VisibleApprovable do
end end
end end
context 'when committer is approver' do
let(:user) { create(:user, email: resource.commits.first.committer_email) }
let!(:committer_approver) { create(:approver, target: project, user: user) }
before do
project.add_developer(user)
end
it 'excludes committer if committers cannot approve' do
is_expected.not_to include(committer_approver.user)
end
it 'includes committer if committers are able to approve' do
project.update(merge_requests_author_approval: true)
is_expected.to include(committer_approver.user)
end
end
context 'when approvers are overwritten' do context 'when approvers are overwritten' do
let!(:approver) { create(:approver, target: resource) } let!(:approver) { create(:approver, target: resource) }
......
...@@ -71,7 +71,7 @@ describe Projects::MergeRequests::CreationsController do ...@@ -71,7 +71,7 @@ describe Projects::MergeRequests::CreationsController do
expect(response).to be_success expect(response).to be_success
total = assigns(:total_commit_count) total = assigns(:total_commit_count)
expect(assigns(:commits)).to be_an Array expect(assigns(:commits)).to be_an CommitCollection
expect(total).to be > 0 expect(total).to be > 0
expect(assigns(:hidden_commit_count)).to eq(0) expect(assigns(:hidden_commit_count)).to eq(0)
expect(response).to have_gitlab_http_status(200) expect(response).to have_gitlab_http_status(200)
......
...@@ -2,7 +2,7 @@ require 'spec_helper' ...@@ -2,7 +2,7 @@ require 'spec_helper'
describe CommitCollection do describe CommitCollection do
let(:project) { create(:project, :repository) } let(:project) { create(:project, :repository) }
let(:commit) { project.commit } let(:commit) { project.commit("c1c67abbaf91f624347bb3ae96eabe3a1b742478") }
describe '#each' do describe '#each' do
it 'yields every commit' do it 'yields every commit' do
...@@ -12,6 +12,29 @@ describe CommitCollection do ...@@ -12,6 +12,29 @@ describe CommitCollection do
end end
end end
describe '.committers' do
it 'returns a relation of users when users are found' do
user = create(:user, email: commit.committer_email.upcase)
collection = described_class.new(project, [commit])
expect(collection.committers).to contain_exactly(user)
end
it 'returns empty array when committers cannot be found' do
collection = described_class.new(project, [commit])
expect(collection.committers).to be_empty
end
it 'excludes authors of merge commits' do
commit = project.commit("60ecb67744cb56576c30214ff52294f8ce2def98")
create(:user, email: commit.committer_email.upcase)
collection = described_class.new(project, [commit])
expect(collection.committers).to be_empty
end
end
describe '#with_pipeline_status' do describe '#with_pipeline_status' do
it 'sets the pipeline status for every commit so no additional queries are necessary' do it 'sets the pipeline status for every commit so no additional queries are necessary' do
create( create(
......
...@@ -1122,6 +1122,34 @@ describe MergeRequest do ...@@ -1122,6 +1122,34 @@ describe MergeRequest do
end end
end end
describe '#committers' do
it 'returns all the committers of every commit in the merge request' do
users = subject.commits.map(&:committer_email).uniq.map do |email|
create(:user, email: email)
end
expect(subject.committers).to match_array(users)
end
it 'returns an empty array if no committer is associated with a user' do
expect(subject.committers).to be_empty
end
end
describe '#authors' do
it 'returns a list with all the committers in the merge request and author' do
users = subject.commits.map(&:committer_email).uniq.map do |email|
create(:user, email: email)
end
expect(subject.authors).to match_array([subject.author, *users])
end
it 'returns only the author if no committer is associated with a user' do
expect(subject.authors).to contain_exactly(subject.author)
end
end
describe '#hook_attrs' do describe '#hook_attrs' do
it 'delegates to Gitlab::HookData::MergeRequestBuilder#build' do it 'delegates to Gitlab::HookData::MergeRequestBuilder#build' do
builder = double builder = double
......
...@@ -603,7 +603,7 @@ describe MergeRequests::RefreshService do ...@@ -603,7 +603,7 @@ describe MergeRequests::RefreshService do
committed_date: Time.now committed_date: Time.now
) )
allow_any_instance_of(MergeRequest).to receive(:commits).and_return([commit]) allow_any_instance_of(MergeRequest).to receive(:commits).and_return(CommitCollection.new(@project, [commit], 'feature'))
end end
context 'when the merge request is sourced from the same project' do context 'when the merge request is sourced from the same project' do
......
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