Commit dcf38617 authored by Stan Hu's avatar Stan Hu

Allow overriding of project approvers in merge request

Previously specifying blank `user_ids` and `group_ids` during the
creation of a merge request approver rule would cause the merge request
to inherit from the project rule. However, that prevents users from
setting the merge request approvers to "any approver". To fix this, we
only set the "any approver" type if `user_ids` and `group_ids` is not
specified and no project approvers are present.

In addition, this commit also fixes the approval rule type when project
rule has users/groups.

Suppose a project has an approval rule that includes specific users or
groups as approvers. If a user attempts to create a merge request
approval rule based on this project rule but omits `user_ids` and
`group_ids`, previously this rule would be erroneously assigned "any
approver". However, the project approvers should be inherited.

To fix this, we:

1. First copy the project approval rules.

2. If the newly-modified params indicate specific users or groups, don't
assign the merge request rule the "any approver" type.

Found while investigating failures in upgrading Grape v1.3.3 in
https://gitlab.com/gitlab-org/gitlab/-/merge_requests/33450.
parent 5e090e9c
---
title: Fix approval rule type when project rule has users/groups
merge_request: 34026
author:
type: fixed
...@@ -7,6 +7,7 @@ module ApprovalRules ...@@ -7,6 +7,7 @@ module ApprovalRules
# @param target [Project, MergeRequest] # @param target [Project, MergeRequest]
def initialize(target, user, params) def initialize(target, user, params)
@rule = target.approval_rules.build @rule = target.approval_rules.build
@params = params
# report_approver rule_type is currently auto-set according to rulename # report_approver rule_type is currently auto-set according to rulename
# Techdebt to be addressed with: https://gitlab.com/gitlab-org/gitlab/issues/12759 # Techdebt to be addressed with: https://gitlab.com/gitlab-org/gitlab/issues/12759
...@@ -14,8 +15,10 @@ module ApprovalRules ...@@ -14,8 +15,10 @@ module ApprovalRules
params.reverse_merge!(rule_type: :report_approver) params.reverse_merge!(rule_type: :report_approver)
end end
handle_any_approver_rule_creation(target, @rule.project, params) # If merge request approvers are specified, they take precedence over project
# approvers.
copy_approval_project_rule_properties(params) if target.is_a?(MergeRequest) copy_approval_project_rule_properties(params) if target.is_a?(MergeRequest)
handle_any_approver_rule_creation(target, @rule.project, params)
super(@rule.project, user, params) super(@rule.project, user, params)
end end
...@@ -29,17 +32,16 @@ module ApprovalRules ...@@ -29,17 +32,16 @@ module ApprovalRules
return if approval_project_rule.blank? return if approval_project_rule.blank?
# Remove the following from params so when set they'll be ignored
params.delete(:user_ids)
params.delete(:group_ids)
params[:name] = approval_project_rule.name params[:name] = approval_project_rule.name
params[:users] = approval_project_rule.users
params[:groups] = approval_project_rule.groups unless approvers_set?
params[:users] = approval_project_rule.users
params[:groups] = approval_project_rule.groups
end
end end
def handle_any_approver_rule_creation(target, project, params) def handle_any_approver_rule_creation(target, project, params)
if params[:user_ids].blank? && params[:group_ids].blank? unless approvers_present?
params.reverse_merge!(rule_type: :any_approver, name: ApprovalRuleLike::ALL_MEMBERS) params.reverse_merge!(rule_type: :any_approver, name: ApprovalRuleLike::ALL_MEMBERS)
return return
...@@ -49,5 +51,13 @@ module ApprovalRules ...@@ -49,5 +51,13 @@ module ApprovalRules
target.approval_rules.any_approver.delete_all target.approval_rules.any_approver.delete_all
end end
def approvers_set?
@params.key?(:user_ids) || @params.key?(:group_ids)
end
def approvers_present?
%i(user_ids group_ids users groups).any? { |key| @params[key].present? }
end
end end
end end
...@@ -146,6 +146,12 @@ RSpec.describe API::MergeRequestApprovalRules do ...@@ -146,6 +146,12 @@ RSpec.describe API::MergeRequestApprovalRules do
let(:approver) { create(:user) } let(:approver) { create(:user) }
let(:group) { create(:group) } let(:group) { create(:group) }
let(:approval_project_rule_id) { nil } let(:approval_project_rule_id) { nil }
let(:approver_params) do
{
user_ids: user_ids,
group_ids: group_ids
}
end
let(:user_ids) { [] } let(:user_ids) { [] }
let(:group_ids) { [] } let(:group_ids) { [] }
...@@ -153,10 +159,8 @@ RSpec.describe API::MergeRequestApprovalRules do ...@@ -153,10 +159,8 @@ RSpec.describe API::MergeRequestApprovalRules do
{ {
name: 'Test', name: 'Test',
approvals_required: 1, approvals_required: 1,
approval_project_rule_id: approval_project_rule_id, approval_project_rule_id: approval_project_rule_id
user_ids: user_ids, }.merge(approver_params)
group_ids: group_ids
}
end end
let(:action) { post api(url, current_user), params: params } let(:action) { post api(url, current_user), params: params }
...@@ -222,15 +226,32 @@ RSpec.describe API::MergeRequestApprovalRules do ...@@ -222,15 +226,32 @@ RSpec.describe API::MergeRequestApprovalRules do
let(:approval_project_rule_id) { approval_project_rule.id } let(:approval_project_rule_id) { approval_project_rule.id }
it 'copies the attributes from the project rule execpt approvals_required' do context 'with blank approver params' do
rule = json_response it 'copies the attributes from the project rule except approvers' do
rule = json_response
expect(rule['name']).to eq(approval_project_rule.name) expect(rule['name']).to eq(approval_project_rule.name)
expect(rule['approvals_required']).to eq(params[:approvals_required]) expect(rule['approvals_required']).to eq(params[:approvals_required])
expect(rule['source_rule']['approvals_required']).to eq(approval_project_rule.approvals_required) expect(rule['source_rule']['approvals_required']).to eq(approval_project_rule.approvals_required)
expect(rule['eligible_approvers']).to match([hash_including('id' => approver.id)]) expect(rule['eligible_approvers']).to eq([])
expect(rule['users']).to match([hash_including('id' => approver.id)]) expect(rule['users']).to eq([])
expect(rule['groups']).to match([hash_including('id' => group.id)]) expect(rule['groups']).to eq([])
end
end
context 'with omitted approver params' do
let(:approver_params) { {} }
it 'copies the attributes from the project rule except approvals_required' do
rule = json_response
expect(rule['name']).to eq(approval_project_rule.name)
expect(rule['approvals_required']).to eq(params[:approvals_required])
expect(rule['source_rule']['approvals_required']).to eq(approval_project_rule.approvals_required)
expect(rule['eligible_approvers']).to match([hash_including('id' => approver.id)])
expect(rule['users']).to match([hash_including('id' => approver.id)])
expect(rule['groups']).to match([hash_including('id' => group.id)])
end
end end
end end
end end
......
...@@ -195,14 +195,19 @@ RSpec.describe ApprovalRules::CreateService do ...@@ -195,14 +195,19 @@ RSpec.describe ApprovalRules::CreateService do
it_behaves_like "creatable" it_behaves_like "creatable"
context 'when project rule id is present' do context 'when project rule id is present' do
let_it_be(:project_user) { create(:user) }
let_it_be(:public_group) { create(:group, :public) }
let(:project_user_approvers) { [project_user] }
let(:group_user_approvers) { [public_group] }
let(:merge_request_approvers) { {} }
let(:project_rule) do let(:project_rule) do
create( create(
:approval_project_rule, :approval_project_rule,
project: project, project: project,
name: 'bar', name: 'bar',
approvals_required: 1, approvals_required: 1,
users: [create(:user)], users: project_user_approvers,
groups: [create(:group)] groups: group_user_approvers
) )
end end
...@@ -210,26 +215,70 @@ RSpec.describe ApprovalRules::CreateService do ...@@ -210,26 +215,70 @@ RSpec.describe ApprovalRules::CreateService do
described_class.new(target, user, { described_class.new(target, user, {
name: 'foo', name: 'foo',
approvals_required: 0, approvals_required: 0,
approval_project_rule_id: project_rule.id, approval_project_rule_id: project_rule.id
user_ids: [], }.merge(merge_request_approvers)).execute
group_ids: []
}).execute
end end
let(:rule) { result[:rule] } let(:rule) { result[:rule] }
it 'associates with project rule' do before do
project.add_developer(project_user)
end
it 'associates with project rule and copies its properites' do
expect(result[:status]).to eq(:success) expect(result[:status]).to eq(:success)
expect(rule.approvals_required).to eq(0) expect(rule.approvals_required).to eq(0)
expect(rule.approval_project_rule).to eq(project_rule) expect(rule.approval_project_rule).to eq(project_rule)
end
it 'copies properties from the project rule' do
expect(rule.name).to eq(project_rule.name) expect(rule.name).to eq(project_rule.name)
expect(rule.rule_type).to eq('regular')
expect(rule.users).to match(project_rule.users) expect(rule.users).to match(project_rule.users)
expect(rule.groups).to match(project_rule.groups) expect(rule.groups).to match(project_rule.groups)
end end
context 'when project rule includes no specific approvers' do
let(:project_user_approvers) { User.none }
let(:group_user_approvers) { Group.none }
it 'associates with project rule and copies its properties' do
expect(result[:status]).to eq(:success)
expect(rule.approvals_required).to eq(0)
expect(rule.approval_project_rule).to eq(project_rule)
expect(rule.name).to eq(project_rule.name)
expect(rule.rule_type).to eq('any_approver')
expect(rule.users).to match([])
expect(rule.groups).to match([])
end
end
context 'when merge request includes empty approvers' do
let(:merge_request_approvers) do
{
user_ids: [],
group_ids: []
}
end
it 'sets any approver' do
expect(result[:status]).to eq(:success)
expect(rule.name).to eq(project_rule.name)
expect(rule.rule_type).to eq('any_approver')
expect(rule.users).to eq([])
expect(rule.groups).to eq([])
end
end
context 'when merge request overrides approvers' do
let(:merge_request_approvers) { { user_ids: [user.id] } }
it 'sets single user as the approver' do
expect(result[:status]).to eq(:success)
expect(rule.name).to eq(project_rule.name)
expect(rule.rule_type).to eq('regular')
expect(rule.users).to eq([user])
expect(rule.groups).to eq([])
end
end
context 'when project rule is under the same project as MR' do context 'when project rule is under the same project as MR' do
let(:another_project) { create(:project) } let(:another_project) { create(: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