Commit bf9300f7 authored by Etienne Baqué's avatar Etienne Baqué

Merge branch '329835-restrict-group-project-selections' into 'master'

Exclude self and ancestor in Invite a group to project/group modal

See merge request gitlab-org/gitlab!79036
parents c9333445 fd5a6666
...@@ -38,6 +38,10 @@ export default { ...@@ -38,6 +38,10 @@ export default {
required: false, required: false,
default: null, default: null,
}, },
invalidGroups: {
type: Array,
required: true,
},
}, },
data() { data() {
return { return {
...@@ -75,18 +79,26 @@ export default { ...@@ -75,18 +79,26 @@ export default {
this.isFetching = true; this.isFetching = true;
return this.fetchGroups() return this.fetchGroups()
.then((response) => { .then((response) => {
this.groups = response.map((group) => ({ this.groups = this.processGroups(response);
id: group.id,
name: group.full_name,
path: group.path,
avatarUrl: group.avatar_url,
}));
this.isFetching = false; this.isFetching = false;
}) })
.catch(() => { .catch(() => {
this.isFetching = false; this.isFetching = false;
}); });
}, SEARCH_DELAY), }, SEARCH_DELAY),
processGroups(response) {
const rawGroups = response.map((group) => ({
id: group.id,
name: group.full_name,
path: group.path,
avatarUrl: group.avatar_url,
}));
return this.filterOutInvalidGroups(rawGroups);
},
filterOutInvalidGroups(groups) {
return groups.filter((group) => this.invalidGroups.indexOf(group.id) === -1);
},
selectGroup(group) { selectGroup(group) {
this.selectedGroup = group; this.selectedGroup = group;
......
...@@ -107,6 +107,10 @@ export default { ...@@ -107,6 +107,10 @@ export default {
type: Array, type: Array,
required: true, required: true,
}, },
invalidGroups: {
type: Array,
required: true,
},
}, },
data() { data() {
return { return {
...@@ -431,6 +435,7 @@ export default { ...@@ -431,6 +435,7 @@ export default {
:access-levels="accessLevels" :access-levels="accessLevels"
:groups-filter="groupSelectFilter" :groups-filter="groupSelectFilter"
:parent-group-id="groupSelectParentId" :parent-group-id="groupSelectParentId"
:invalid-groups="invalidGroups"
@input="handleMembersTokenSelectClear" @input="handleMembersTokenSelectClear"
/> />
</gl-form-group> </gl-form-group>
......
...@@ -40,6 +40,7 @@ export default function initInviteMembersModal() { ...@@ -40,6 +40,7 @@ export default function initInviteMembersModal() {
defaultAccessLevel: parseInt(el.dataset.defaultAccessLevel, 10), defaultAccessLevel: parseInt(el.dataset.defaultAccessLevel, 10),
groupSelectFilter: el.dataset.groupsFilter, groupSelectFilter: el.dataset.groupsFilter,
groupSelectParentId: parseInt(el.dataset.parentId, 10), groupSelectParentId: parseInt(el.dataset.parentId, 10),
invalidGroups: JSON.parse(el.dataset.invalidGroups || '[]'),
tasksToBeDoneOptions: JSON.parse(el.dataset.tasksToBeDoneOptions || '[]'), tasksToBeDoneOptions: JSON.parse(el.dataset.tasksToBeDoneOptions || '[]'),
projects: JSON.parse(el.dataset.projects || '[]'), projects: JSON.parse(el.dataset.projects || '[]'),
usersFilter: el.dataset.usersFilter, usersFilter: el.dataset.usersFilter,
......
...@@ -13,8 +13,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController ...@@ -13,8 +13,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController
def index def index
@sort = params[:sort].presence || sort_value_name @sort = params[:sort].presence || sort_value_name
@skip_groups = @project.invited_group_ids @skip_groups = @project.related_group_ids
@skip_groups += @project.group.self_and_ancestors_ids if @project.group
@group_links = @project.project_group_links @group_links = @project.project_group_links
@group_links = @group_links.search(params[:search_groups]) if params[:search_groups].present? @group_links = @group_links.search(params[:search_groups]) if params[:search_groups].present?
......
...@@ -21,6 +21,11 @@ module InviteMembersHelper ...@@ -21,6 +21,11 @@ module InviteMembersHelper
end end
def group_select_data(group) def group_select_data(group)
# This should only be used for groups to load the invite group modal.
# For instance the invite groups modal should not call this from a project scope
# this is only to be called in scope of a group context as noted in this thread
# https://gitlab.com/gitlab-org/gitlab/-/merge_requests/79036#note_821465513
# the group sharing in projects disabling is explained there as well
if group.root_ancestor.namespace_settings.prevent_sharing_groups_outside_hierarchy if group.root_ancestor.namespace_settings.prevent_sharing_groups_outside_hierarchy
{ groups_filter: 'descendant_groups', parent_id: group.root_ancestor.id } { groups_filter: 'descendant_groups', parent_id: group.root_ancestor.id }
else else
...@@ -32,7 +37,8 @@ module InviteMembersHelper ...@@ -32,7 +37,8 @@ module InviteMembersHelper
dataset = { dataset = {
id: source.id, id: source.id,
name: source.name, name: source.name,
default_access_level: Gitlab::Access::GUEST default_access_level: Gitlab::Access::GUEST,
invalid_groups: source.related_group_ids
} }
if show_invite_members_for_task?(source) if show_invite_members_for_task?(source)
......
...@@ -2618,6 +2618,14 @@ class Project < ApplicationRecord ...@@ -2618,6 +2618,14 @@ class Project < ApplicationRecord
[project&.id, root_group&.id] [project&.id, root_group&.id]
end end
def related_group_ids
ids = invited_group_ids
ids += group.self_and_ancestors_ids if group
ids
end
def package_already_taken?(package_name, package_version, package_type:) def package_already_taken?(package_name, package_version, package_type:)
Packages::Package.with_name(package_name) Packages::Package.with_name(package_name)
.with_version(package_version) .with_version(package_version)
......
...@@ -156,6 +156,26 @@ RSpec.describe 'Groups > Members > Manage groups', :js do ...@@ -156,6 +156,26 @@ RSpec.describe 'Groups > Members > Manage groups', :js do
group_outside_hierarchy.add_owner(user) group_outside_hierarchy.add_owner(user)
end end
context 'when the invite members group modal is enabled' do
it 'does not show self or ancestors', :aggregate_failures do
group_sibbling = create(:group, parent: group)
group_sibbling.add_owner(user)
visit group_group_members_path(group_within_hierarchy)
click_on 'Invite a group'
click_on 'Select a group'
wait_for_requests
page.within('[data-testid="group-select-dropdown"]') do
expect(page).to have_selector("[entity-id='#{group_outside_hierarchy.id}']")
expect(page).to have_selector("[entity-id='#{group_sibbling.id}']")
expect(page).not_to have_selector("[entity-id='#{group.id}']")
expect(page).not_to have_selector("[entity-id='#{group_within_hierarchy.id}']")
end
end
end
context 'when sharing with groups outside the hierarchy is enabled' do context 'when sharing with groups outside the hierarchy is enabled' do
context 'when the invite members group modal is disabled' do context 'when the invite members group modal is disabled' do
before do before do
......
...@@ -8,7 +8,7 @@ RSpec.describe 'Project > Members > Invite group', :js do ...@@ -8,7 +8,7 @@ RSpec.describe 'Project > Members > Invite group', :js do
include Spec::Support::Helpers::Features::MembersHelpers include Spec::Support::Helpers::Features::MembersHelpers
include Spec::Support::Helpers::Features::InviteMembersModalHelper include Spec::Support::Helpers::Features::InviteMembersModalHelper
let(:maintainer) { create(:user) } let_it_be(:maintainer) { create(:user) }
using RSpec::Parameterized::TableSyntax using RSpec::Parameterized::TableSyntax
...@@ -190,17 +190,26 @@ RSpec.describe 'Project > Members > Invite group', :js do ...@@ -190,17 +190,26 @@ RSpec.describe 'Project > Members > Invite group', :js do
end end
describe 'the groups dropdown' do describe 'the groups dropdown' do
context 'with multiple groups to choose from' do let_it_be(:parent_group) { create(:group, :public) }
let(:project) { create(:project) } let_it_be(:project_group) { create(:group, :public, parent: parent_group) }
let_it_be(:public_sub_subgroup) { create(:group, :public, parent: project_group) }
let_it_be(:public_sibbling_group) { create(:group, :public, parent: parent_group) }
let_it_be(:private_sibbling_group) { create(:group, :private, parent: parent_group) }
let_it_be(:private_membership_group) { create(:group, :private) }
let_it_be(:public_membership_group) { create(:group, :public) }
let_it_be(:project) { create(:project, group: project_group) }
it 'includes multiple groups' do before do
project.add_maintainer(maintainer) private_membership_group.add_guest(maintainer)
sign_in(maintainer) public_membership_group.add_maintainer(maintainer)
group1 = create(:group) sign_in(maintainer)
group1.add_owner(maintainer) end
group2 = create(:group)
group2.add_owner(maintainer) context 'for a project in a nested group' do
it 'does not show the groups inherited from projects' do
project.add_maintainer(maintainer)
public_sibbling_group.add_maintainer(maintainer)
visit project_project_members_path(project) visit project_project_members_path(project)
...@@ -208,57 +217,36 @@ RSpec.describe 'Project > Members > Invite group', :js do ...@@ -208,57 +217,36 @@ RSpec.describe 'Project > Members > Invite group', :js do
click_on 'Select a group' click_on 'Select a group'
wait_for_requests wait_for_requests
expect(page).to have_button(group1.name) page.within('[data-testid="group-select-dropdown"]') do
expect(page).to have_button(group2.name) expect_to_have_group(public_membership_group)
end expect_to_have_group(public_sibbling_group)
end expect_to_have_group(private_membership_group)
context 'for a project in a nested group' do
let!(:parent_group) { create(:group, :public) }
let!(:public_subgroup) { create(:group, :public, parent: parent_group) }
let!(:public_sub_subgroup) { create(:group, :public, parent: public_subgroup) }
let!(:private_subgroup) { create(:group, :private, parent: parent_group) }
let!(:project) { create(:project, :public, namespace: public_subgroup) }
let!(:membership_group) { create(:group, :public) } expect_not_to_have_group(public_sub_subgroup)
expect_not_to_have_group(private_sibbling_group)
before do expect_not_to_have_group(parent_group)
project.add_maintainer(maintainer) expect_not_to_have_group(project_group)
membership_group.add_guest(maintainer) end
sign_in(maintainer)
end end
context 'when invite_members_group_modal feature enabled' do it 'does not show the ancestors or project group', :aggregate_failures do
it 'does not show the groups inherited from projects' do parent_group.add_maintainer(maintainer)
visit project_project_members_path(project)
click_on 'Invite a group'
click_on 'Select a group'
wait_for_requests
expect(page).to have_button(membership_group.name)
expect(page).not_to have_button(parent_group.name)
expect(page).not_to have_button(public_subgroup.name)
expect(page).not_to have_button(public_sub_subgroup.name)
expect(page).not_to have_button(private_subgroup.name)
end
# This behavior should be changed to exclude the ancestor and project visit project_project_members_path(project)
# group from the options once issue is fixed for the modal:
# https://gitlab.com/gitlab-org/gitlab/-/issues/329835
it 'does show ancestors and the project group' do
parent_group.add_maintainer(maintainer)
visit project_project_members_path(project) click_on 'Invite a group'
click_on 'Select a group'
wait_for_requests
click_on 'Invite a group' page.within('[data-testid="group-select-dropdown"]') do
click_on 'Select a group' expect_to_have_group(public_membership_group)
wait_for_requests expect_to_have_group(public_sibbling_group)
expect_to_have_group(private_membership_group)
expect_to_have_group(public_sub_subgroup)
expect_to_have_group(private_sibbling_group)
expect(page).to have_button(membership_group.name) expect_not_to_have_group(parent_group)
expect(page).to have_button(parent_group.name) expect_not_to_have_group(project_group)
expect(page).to have_button(public_subgroup.name)
end end
end end
...@@ -269,21 +257,26 @@ RSpec.describe 'Project > Members > Invite group', :js do ...@@ -269,21 +257,26 @@ RSpec.describe 'Project > Members > Invite group', :js do
stub_feature_flags(invite_members_group_modal: false) stub_feature_flags(invite_members_group_modal: false)
end end
it 'does not show the groups inherited from projects' do it 'does not show the groups inherited from projects', :aggregate_failures do
project.add_maintainer(maintainer)
public_sibbling_group.add_maintainer(maintainer)
visit project_project_members_path(project) visit project_project_members_path(project)
click_on 'Invite group' click_on 'Invite group'
click_on 'Search for a group' click_on 'Search for a group'
wait_for_requests wait_for_requests
expect(group_invite_dropdown).to have_text(membership_group.name) expect(group_invite_dropdown).to have_text(public_membership_group.full_path)
expect(group_invite_dropdown).not_to have_text(parent_group.name) expect(group_invite_dropdown).to have_text(public_sibbling_group.full_path)
expect(group_invite_dropdown).not_to have_text(public_subgroup.name) expect(group_invite_dropdown).to have_text(private_membership_group.full_path)
expect(group_invite_dropdown).not_to have_text(public_sub_subgroup.name) expect(group_invite_dropdown).not_to have_text(public_sub_subgroup.full_path)
expect(group_invite_dropdown).not_to have_text(private_subgroup.name) expect(group_invite_dropdown).not_to have_text(private_sibbling_group.full_path)
expect(group_invite_dropdown).not_to have_text(parent_group.full_path, exact: true)
expect(group_invite_dropdown).not_to have_text(project_group.full_path, exact: true)
end end
it 'does not show ancestors and the project group' do it 'does not show the ancestors or project group', :aggregate_failures do
parent_group.add_maintainer(maintainer) parent_group.add_maintainer(maintainer)
visit project_project_members_path(project) visit project_project_members_path(project)
...@@ -292,11 +285,23 @@ RSpec.describe 'Project > Members > Invite group', :js do ...@@ -292,11 +285,23 @@ RSpec.describe 'Project > Members > Invite group', :js do
click_on 'Search for a group' click_on 'Search for a group'
wait_for_requests wait_for_requests
expect(group_invite_dropdown).to have_text(membership_group.name) expect(group_invite_dropdown).to have_text(public_membership_group.full_path)
expect(group_invite_dropdown).not_to have_text(parent_group.name, exact: true) expect(group_invite_dropdown).to have_text(public_sub_subgroup.full_path)
expect(group_invite_dropdown).not_to have_text(public_subgroup.name, exact: true) expect(group_invite_dropdown).to have_text(public_sibbling_group.full_path)
expect(group_invite_dropdown).to have_text(private_sibbling_group.full_path)
expect(group_invite_dropdown).to have_text(private_membership_group.full_path)
expect(group_invite_dropdown).not_to have_text(parent_group.full_path, exact: true)
expect(group_invite_dropdown).not_to have_text(project_group.full_path, exact: true)
end end
end end
def expect_to_have_group(group)
expect(page).to have_selector("[entity-id='#{group.id}']")
end
def expect_not_to_have_group(group)
expect(page).not_to have_selector("[entity-id='#{group.id}']")
end
end end
end end
end end
...@@ -5,19 +5,20 @@ import * as groupsApi from '~/api/groups_api'; ...@@ -5,19 +5,20 @@ import * as groupsApi from '~/api/groups_api';
import GroupSelect from '~/invite_members/components/group_select.vue'; import GroupSelect from '~/invite_members/components/group_select.vue';
const accessLevels = { Guest: 10, Reporter: 20, Developer: 30, Maintainer: 40, Owner: 50 }; const accessLevels = { Guest: 10, Reporter: 20, Developer: 30, Maintainer: 40, Owner: 50 };
const group1 = { id: 1, full_name: 'Group One', avatar_url: 'test' };
const group2 = { id: 2, full_name: 'Group Two', avatar_url: 'test' };
const allGroups = [group1, group2];
const createComponent = () => { const createComponent = (props = {}) => {
return mount(GroupSelect, { return mount(GroupSelect, {
propsData: { propsData: {
invalidGroups: [],
accessLevels, accessLevels,
...props,
}, },
}); });
}; };
const group1 = { id: 1, full_name: 'Group One', avatar_url: 'test' };
const group2 = { id: 2, full_name: 'Group Two', avatar_url: 'test' };
const allGroups = [group1, group2];
describe('GroupSelect', () => { describe('GroupSelect', () => {
let wrapper; let wrapper;
...@@ -90,6 +91,20 @@ describe('GroupSelect', () => { ...@@ -90,6 +91,20 @@ describe('GroupSelect', () => {
size: '32', size: '32',
}); });
}); });
describe('when filtering out the group from results', () => {
beforeEach(() => {
wrapper = createComponent({ invalidGroups: [group1.id] });
});
it('does not find an invalid group', () => {
expect(findAvatarByLabel(group1.full_name)).toBe(undefined);
});
it('finds a group that is valid', () => {
expect(findAvatarByLabel(group2.full_name).exists()).toBe(true);
});
});
}); });
describe('when group is selected from the dropdown', () => { describe('when group is selected from the dropdown', () => {
......
...@@ -46,6 +46,7 @@ jest.mock('~/lib/utils/url_utility', () => ({ ...@@ -46,6 +46,7 @@ jest.mock('~/lib/utils/url_utility', () => ({
const id = '1'; const id = '1';
const name = 'test name'; const name = 'test name';
const isProject = false; const isProject = false;
const invalidGroups = [];
const inviteeType = 'members'; const inviteeType = 'members';
const accessLevels = { Guest: 10, Reporter: 20, Developer: 30, Maintainer: 40, Owner: 50 }; const accessLevels = { Guest: 10, Reporter: 20, Developer: 30, Maintainer: 40, Owner: 50 };
const defaultAccessLevel = 10; const defaultAccessLevel = 10;
...@@ -93,6 +94,7 @@ const createComponent = (data = {}, props = {}) => { ...@@ -93,6 +94,7 @@ const createComponent = (data = {}, props = {}) => {
tasksToBeDoneOptions, tasksToBeDoneOptions,
projects, projects,
helpLink, helpLink,
invalidGroups,
...props, ...props,
}, },
data() { data() {
......
...@@ -20,7 +20,8 @@ RSpec.describe InviteMembersHelper do ...@@ -20,7 +20,8 @@ RSpec.describe InviteMembersHelper do
attributes = { attributes = {
id: project.id, id: project.id,
name: project.name, name: project.name,
default_access_level: Gitlab::Access::GUEST default_access_level: Gitlab::Access::GUEST,
invalid_groups: project.related_group_ids
} }
expect(helper.common_invite_modal_dataset(project)).to include(attributes) expect(helper.common_invite_modal_dataset(project)).to include(attributes)
...@@ -155,4 +156,28 @@ RSpec.describe InviteMembersHelper do ...@@ -155,4 +156,28 @@ RSpec.describe InviteMembersHelper do
end end
end end
end end
describe '#group_select_data' do
let_it_be(:group) { create(:group) }
context 'when sharing with groups outside the hierarchy is disabled' do
before do
group.namespace_settings.update!(prevent_sharing_groups_outside_hierarchy: true)
end
it 'provides the correct attributes' do
expect(helper.group_select_data(group)).to eq({ groups_filter: 'descendant_groups', parent_id: group.id })
end
end
context 'when sharing with groups outside the hierarchy is enabled' do
before do
group.namespace_settings.update!(prevent_sharing_groups_outside_hierarchy: false)
end
it 'returns an empty hash' do
expect(helper.group_select_data(project.group)).to eq({})
end
end
end
end end
...@@ -7118,6 +7118,29 @@ RSpec.describe Project, factory_default: :keep do ...@@ -7118,6 +7118,29 @@ RSpec.describe Project, factory_default: :keep do
it { is_expected.to be true } it { is_expected.to be true }
end end
describe '#related_group_ids' do
let_it_be(:group) { create(:group) }
let_it_be(:sub_group) { create(:group, parent: group) }
context 'when associated with a namespace' do
let(:project) { create(:project, namespace: create(:namespace)) }
let!(:linked_group) { create(:project_group_link, project: project).group }
it 'only includes linked groups' do
expect(project.related_group_ids).to contain_exactly(linked_group.id)
end
end
context 'when associated with a group' do
let(:project) { create(:project, group: sub_group) }
let!(:linked_group) { create(:project_group_link, project: project).group }
it 'includes self, ancestors and linked groups' do
expect(project.related_group_ids).to contain_exactly(group.id, sub_group.id, linked_group.id)
end
end
end
describe '#package_already_taken?' do describe '#package_already_taken?' do
let_it_be(:namespace) { create(:namespace, path: 'test') } let_it_be(:namespace) { create(:namespace, path: 'test') }
let_it_be(:project) { create(:project, :public, namespace: namespace) } let_it_be(:project) { create(:project, :public, namespace: namespace) }
......
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