Resolve "Can add an existing group member into a group project with new permissions but permissions are not overridden"

This commit is contained in:
James Lopez 2018-12-06 13:15:29 +00:00 committed by Douglas Barbosa Alexandre
parent 39c769aee8
commit 64c11f104c
18 changed files with 230 additions and 11 deletions

View file

@ -7,6 +7,7 @@ class Member < ActiveRecord::Base
include Expirable include Expirable
include Gitlab::Access include Gitlab::Access
include Presentable include Presentable
include Gitlab::Utils::StrongMemoize
attr_accessor :raw_invite_token attr_accessor :raw_invite_token
@ -22,6 +23,7 @@ class Member < ActiveRecord::Base
message: "already exists in source", message: "already exists in source",
allow_nil: true } allow_nil: true }
validates :access_level, inclusion: { in: Gitlab::Access.all_values }, presence: true validates :access_level, inclusion: { in: Gitlab::Access.all_values }, presence: true
validate :higher_access_level_than_group, unless: :importing?
validates :invite_email, validates :invite_email,
presence: { presence: {
if: :invite? if: :invite?
@ -364,6 +366,15 @@ class Member < ActiveRecord::Base
end end
# rubocop: enable CodeReuse/ServiceClass # rubocop: enable CodeReuse/ServiceClass
# Find the user's group member with a highest access level
def highest_group_member
strong_memoize(:highest_group_member) do
next unless user_id && source&.ancestors&.any?
GroupMember.where(source: source.ancestors, user_id: user_id).order(:access_level).last
end
end
private private
def send_invite def send_invite
@ -430,4 +441,12 @@ class Member < ActiveRecord::Base
def notifiable_options def notifiable_options
{} {}
end end
def higher_access_level_than_group
if highest_group_member && highest_group_member.access_level >= access_level
error_parameters = { access: highest_group_member.human_access, group_name: highest_group_member.group.name }
errors.add(:access_level, s_("should be higher than %{access} inherited membership from group %{group_name}") % error_parameters)
end
end
end end

View file

@ -570,6 +570,8 @@ class Project < ActiveRecord::Base
.base_and_ancestors(upto: top, hierarchy_order: hierarchy_order) .base_and_ancestors(upto: top, hierarchy_order: hierarchy_order)
end end
alias_method :ancestors, :ancestors_upto
def lfs_enabled? def lfs_enabled?
return namespace.lfs_enabled? if self[:lfs_enabled].nil? return namespace.lfs_enabled? if self[:lfs_enabled].nil?

View file

@ -7,6 +7,14 @@ class MemberPresenter < Gitlab::View::Presenter::Delegated
member.class.access_level_roles member.class.access_level_roles
end end
def valid_level_roles
return access_level_roles unless member.highest_group_member
access_level_roles.reject do |_name, level|
member.highest_group_member.access_level > level
end
end
def can_resend_invite? def can_resend_invite?
invite? && invite? &&
can?(current_user, admin_member_permission, source) can?(current_user, admin_member_permission, source)

View file

@ -75,7 +75,7 @@
= dropdown_title(_("Change permissions")) = dropdown_title(_("Change permissions"))
.dropdown-content .dropdown-content
%ul %ul
- member.access_level_roles.each do |role, role_id| - member.valid_level_roles.each do |role, role_id|
%li %li
= link_to role, "javascript:void(0)", = link_to role, "javascript:void(0)",
class: ("is-active" if member.access_level == role_id), class: ("is-active" if member.access_level == role_id),

View file

@ -0,0 +1,5 @@
---
title: Restrict member access level to be higher than that of any parent group
merge_request: 23226
author:
type: fixed

View file

@ -7932,6 +7932,9 @@ msgid_plural "replies"
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
msgid "should be higher than %{access} inherited membership from group %{group_name}"
msgstr ""
msgid "source" msgid "source"
msgstr "" msgstr ""

View file

@ -19,7 +19,7 @@ describe GroupMembersFinder, '#execute' do
end end
it 'returns members for nested group', :nested_groups do it 'returns members for nested group', :nested_groups do
group.add_maintainer(user2) group.add_developer(user2)
nested_group.request_access(user4) nested_group.request_access(user4)
member1 = group.add_maintainer(user1) member1 = group.add_maintainer(user1)
member3 = nested_group.add_maintainer(user2) member3 = nested_group.add_maintainer(user2)

View file

@ -76,7 +76,7 @@ describe Group do
before do before do
group.add_developer(user) group.add_developer(user)
sub_group.add_developer(user) sub_group.add_maintainer(user)
end end
it 'also gets notification settings from parent groups' do it 'also gets notification settings from parent groups' do
@ -498,7 +498,7 @@ describe Group do
it 'returns member users on every nest level without duplication' do it 'returns member users on every nest level without duplication' do
group.add_developer(user_a) group.add_developer(user_a)
nested_group.add_developer(user_b) nested_group.add_developer(user_b)
deep_nested_group.add_developer(user_a) deep_nested_group.add_maintainer(user_a)
expect(group.users_with_descendants).to contain_exactly(user_a, user_b) expect(group.users_with_descendants).to contain_exactly(user_a, user_b)
expect(nested_group.users_with_descendants).to contain_exactly(user_a, user_b) expect(nested_group.users_with_descendants).to contain_exactly(user_a, user_b)

View file

@ -53,6 +53,29 @@ describe Member do
expect(member).to be_valid expect(member).to be_valid
end end
end end
context "when a child member inherits its access level" do
let(:user) { create(:user) }
let(:member) { create(:group_member, :developer, user: user) }
let(:child_group) { create(:group, parent: member.group) }
let(:child_member) { build(:group_member, group: child_group, user: user) }
it "requires a higher level" do
child_member.access_level = GroupMember::REPORTER
child_member.validate
expect(child_member).not_to be_valid
end
it "is valid with a higher level" do
child_member.access_level = GroupMember::MAINTAINER
child_member.validate
expect(child_member).to be_valid
end
end
end end
describe 'Scopes & finders' do describe 'Scopes & finders' do

View file

@ -50,4 +50,26 @@ describe GroupMember do
group_member.destroy group_member.destroy
end end
end end
context 'access levels', :nested_groups do
context 'with parent group' do
it_behaves_like 'inherited access level as a member of entity' do
let(:entity) { create(:group, parent: parent_entity) }
end
end
context 'with parent group and a sub subgroup' do
it_behaves_like 'inherited access level as a member of entity' do
let(:subgroup) { create(:group, parent: parent_entity) }
let(:entity) { create(:group, parent: subgroup) }
end
context 'when only the subgroup has the member' do
it_behaves_like 'inherited access level as a member of entity' do
let(:parent_entity) { create(:group, parent: create(:group)) }
let(:entity) { create(:group, parent: parent_entity) }
end
end
end
end
end end

View file

@ -124,4 +124,19 @@ describe ProjectMember do
end end
it_behaves_like 'members notifications', :project it_behaves_like 'members notifications', :project
context 'access levels' do
context 'with parent group' do
it_behaves_like 'inherited access level as a member of entity' do
let(:entity) { create(:project, group: parent_entity) }
end
end
context 'with parent group and a subgroup', :nested_groups do
it_behaves_like 'inherited access level as a member of entity' do
let(:subgroup) { create(:group, parent: parent_entity) }
let(:entity) { create(:project, group: subgroup) }
end
end
end
end end

View file

@ -538,7 +538,7 @@ describe Namespace do
it 'returns member users on every nest level without duplication' do it 'returns member users on every nest level without duplication' do
group.add_developer(user_a) group.add_developer(user_a)
nested_group.add_developer(user_b) nested_group.add_developer(user_b)
deep_nested_group.add_developer(user_a) deep_nested_group.add_maintainer(user_a)
expect(group.users_with_descendants).to contain_exactly(user_a, user_b) expect(group.users_with_descendants).to contain_exactly(user_a, user_b)
expect(nested_group.users_with_descendants).to contain_exactly(user_a, user_b) expect(nested_group.users_with_descendants).to contain_exactly(user_a, user_b)

View file

@ -2325,11 +2325,11 @@ describe User do
context 'user is member of all groups' do context 'user is member of all groups' do
before do before do
group.add_owner(user) group.add_reporter(user)
nested_group_1.add_owner(user) nested_group_1.add_developer(user)
nested_group_1_1.add_owner(user) nested_group_1_1.add_maintainer(user)
nested_group_2.add_owner(user) nested_group_2.add_developer(user)
nested_group_2_1.add_owner(user) nested_group_2_1.add_maintainer(user)
end end
it 'returns all groups' do it 'returns all groups' do

View file

@ -135,4 +135,12 @@ describe GroupMemberPresenter do
end end
end end
end end
it_behaves_like '#valid_level_roles', :group do
let(:expected_roles) { { 'Developer' => 30, 'Maintainer' => 40, 'Owner' => 50, 'Reporter' => 20 } }
before do
entity.parent = group
end
end
end end

View file

@ -135,4 +135,10 @@ describe ProjectMemberPresenter do
end end
end end
end end
it_behaves_like '#valid_level_roles', :project do
before do
entity.group = group
end
end
end end

View file

@ -224,6 +224,37 @@ describe API::Members do
end end
end end
context 'access levels' do
it 'does not create the member if group level is higher', :nested_groups do
parent = create(:group)
group.update(parent: parent)
project.update(group: group)
parent.add_developer(stranger)
post api("/#{source_type.pluralize}/#{source.id}/members", maintainer),
user_id: stranger.id, access_level: Member::REPORTER
expect(response).to have_gitlab_http_status(400)
expect(json_response['message']['access_level']).to eq(["should be higher than Developer inherited membership from group #{parent.name}"])
end
it 'creates the member if group level is lower', :nested_groups do
parent = create(:group)
group.update(parent: parent)
project.update(group: group)
parent.add_developer(stranger)
post api("/#{source_type.pluralize}/#{source.id}/members", maintainer),
user_id: stranger.id, access_level: Member::MAINTAINER
expect(response).to have_gitlab_http_status(201)
expect(json_response['id']).to eq(stranger.id)
expect(json_response['access_level']).to eq(Member::MAINTAINER)
end
end
it "returns 409 if member already exists" do it "returns 409 if member already exists" do
post api("/#{source_type.pluralize}/#{source.id}/members", maintainer), post api("/#{source_type.pluralize}/#{source.id}/members", maintainer),
user_id: maintainer.id, access_level: Member::MAINTAINER user_id: maintainer.id, access_level: Member::MAINTAINER

View file

@ -1906,7 +1906,7 @@ describe API::Projects do
let(:group) { create(:group) } let(:group) { create(:group) }
let(:group2) do let(:group2) do
group = create(:group, name: 'group2_name') group = create(:group, name: 'group2_name')
group.add_owner(user2) group.add_maintainer(user2)
group group
end end

View file

@ -0,0 +1,77 @@
# frozen_string_literal: true
shared_examples_for 'inherited access level as a member of entity' do
let(:parent_entity) { create(:group) }
let(:user) { create(:user) }
let(:member) { entity.is_a?(Group) ? entity.group_member(user) : entity.project_member(user) }
context 'with root parent_entity developer member' do
before do
parent_entity.add_developer(user)
end
it 'is allowed to be a maintainer of the entity' do
entity.add_maintainer(user)
expect(member.access_level).to eq(Gitlab::Access::MAINTAINER)
end
it 'is not allowed to be a reporter of the entity' do
entity.add_reporter(user)
expect(member).to be_nil
end
it 'is allowed to change to be a developer of the entity' do
entity.add_maintainer(user)
expect { member.update(access_level: Gitlab::Access::DEVELOPER) }
.to change { member.access_level }.to(Gitlab::Access::DEVELOPER)
end
it 'is not allowed to change to be a guest of the entity' do
entity.add_maintainer(user)
expect { member.update(access_level: Gitlab::Access::GUEST) }
.not_to change { member.reload.access_level }
end
it "shows an error if the member can't be updated" do
entity.add_maintainer(user)
member.update(access_level: Gitlab::Access::REPORTER)
expect(member.errors.full_messages).to eq(["Access level should be higher than Developer inherited membership from group #{parent_entity.name}"])
end
it 'allows changing the level from a non existing member' do
non_member_user = create(:user)
entity.add_maintainer(non_member_user)
non_member = entity.is_a?(Group) ? entity.group_member(non_member_user) : entity.project_member(non_member_user)
expect { non_member.update(access_level: Gitlab::Access::GUEST) }
.to change { non_member.reload.access_level }
end
end
end
shared_examples_for '#valid_level_roles' do |entity_name|
let(:member_user) { create(:user) }
let(:group) { create(:group) }
let(:entity) { create(entity_name) }
let(:entity_member) { create("#{entity_name}_member", :developer, source: entity, user: member_user) }
let(:presenter) { described_class.new(entity_member, current_user: member_user) }
let(:expected_roles) { { 'Developer' => 30, 'Maintainer' => 40, 'Reporter' => 20 } }
it 'returns all roles when no parent member is present' do
expect(presenter.valid_level_roles).to eq(entity_member.class.access_level_roles)
end
it 'returns higher roles when a parent member is present' do
group.add_reporter(member_user)
expect(presenter.valid_level_roles).to eq(expected_roles)
end
end