Commit 9b835d10 authored by Douwe Maan's avatar Douwe Maan

Merge branch 'tc-fix-group-finder-subgrouping' into 'master'

Show private subgroups if member of parent group

Closes #32135

See merge request !11764
parents 11b3e54c aeaf5860
...@@ -5,8 +5,10 @@ class GroupsFinder < UnionFinder ...@@ -5,8 +5,10 @@ class GroupsFinder < UnionFinder
end end
def execute def execute
groups = find_union(all_groups, Group).with_route.order_id_desc items = all_groups.map do |item|
by_parent(groups) by_parent(item)
end
find_union(items, Group).with_route.order_id_desc
end end
private private
...@@ -16,12 +18,22 @@ class GroupsFinder < UnionFinder ...@@ -16,12 +18,22 @@ class GroupsFinder < UnionFinder
def all_groups def all_groups
groups = [] groups = []
groups << current_user.authorized_groups if current_user if current_user
groups << Gitlab::GroupHierarchy.new(groups_for_ancestors, groups_for_descendants).all_groups
end
groups << Group.unscoped.public_to_user(current_user) groups << Group.unscoped.public_to_user(current_user)
groups groups
end end
def groups_for_ancestors
current_user.authorized_groups
end
def groups_for_descendants
current_user.groups
end
def by_parent(groups) def by_parent(groups)
return groups unless params[:parent] return groups unless params[:parent]
......
...@@ -3,33 +3,38 @@ module Gitlab ...@@ -3,33 +3,38 @@ module Gitlab
# #
# This class uses recursive CTEs and as a result will only work on PostgreSQL. # This class uses recursive CTEs and as a result will only work on PostgreSQL.
class GroupHierarchy class GroupHierarchy
attr_reader :base, :model attr_reader :ancestors_base, :descendants_base, :model
# base - An instance of ActiveRecord::Relation for which to get parent or # ancestors_base - An instance of ActiveRecord::Relation for which to
# child groups. # get parent groups.
def initialize(base) # descendants_base - An instance of ActiveRecord::Relation for which to
@base = base # get child groups. If omitted, ancestors_base is used.
@model = base.model def initialize(ancestors_base, descendants_base = ancestors_base)
raise ArgumentError.new("Model of ancestors_base does not match model of descendants_base") if ancestors_base.model != descendants_base.model
@ancestors_base = ancestors_base
@descendants_base = descendants_base
@model = ancestors_base.model
end end
# Returns a relation that includes the base set of groups and all their # Returns a relation that includes the ancestors_base set of groups
# ancestors (recursively). # and all their ancestors (recursively).
def base_and_ancestors def base_and_ancestors
return model.none unless Group.supports_nested_groups? return ancestors_base unless Group.supports_nested_groups?
base_and_ancestors_cte.apply_to(model.all) base_and_ancestors_cte.apply_to(model.all)
end end
# Returns a relation that includes the base set of groups and all their # Returns a relation that includes the descendants_base set of groups
# descendants (recursively). # and all their descendants (recursively).
def base_and_descendants def base_and_descendants
return model.none unless Group.supports_nested_groups? return descendants_base unless Group.supports_nested_groups?
base_and_descendants_cte.apply_to(model.all) base_and_descendants_cte.apply_to(model.all)
end end
# Returns a relation that includes the base groups, their ancestors, and the # Returns a relation that includes the base groups, their ancestors,
# descendants of the base groups. # and the descendants of the base groups.
# #
# The resulting query will roughly look like the following: # The resulting query will roughly look like the following:
# #
...@@ -48,8 +53,10 @@ module Gitlab ...@@ -48,8 +53,10 @@ module Gitlab
# #
# Using this approach allows us to further add criteria to the relation with # Using this approach allows us to further add criteria to the relation with
# Rails thinking it's selecting data the usual way. # Rails thinking it's selecting data the usual way.
#
# If nested groups are not supported, ancestors_base is returned.
def all_groups def all_groups
return base unless Group.supports_nested_groups? return ancestors_base unless Group.supports_nested_groups?
ancestors = base_and_ancestors_cte ancestors = base_and_ancestors_cte
descendants = base_and_descendants_cte descendants = base_and_descendants_cte
...@@ -72,7 +79,7 @@ module Gitlab ...@@ -72,7 +79,7 @@ module Gitlab
def base_and_ancestors_cte def base_and_ancestors_cte
cte = SQL::RecursiveCTE.new(:base_and_ancestors) cte = SQL::RecursiveCTE.new(:base_and_ancestors)
cte << base.except(:order) cte << ancestors_base.except(:order)
# Recursively get all the ancestors of the base set. # Recursively get all the ancestors of the base set.
cte << model. cte << model.
...@@ -86,7 +93,7 @@ module Gitlab ...@@ -86,7 +93,7 @@ module Gitlab
def base_and_descendants_cte def base_and_descendants_cte
cte = SQL::RecursiveCTE.new(:base_and_descendants) cte = SQL::RecursiveCTE.new(:base_and_descendants)
cte << base.except(:order) cte << descendants_base.except(:order)
# Recursively get all the descendants of the base set. # Recursively get all the descendants of the base set.
cte << model. cte << model.
......
...@@ -2,7 +2,7 @@ require 'rails_helper' ...@@ -2,7 +2,7 @@ require 'rails_helper'
describe GroupsController do describe GroupsController do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:group) { create(:group) } let(:group) { create(:group, :public) }
let(:project) { create(:empty_project, namespace: group) } let(:project) { create(:empty_project, namespace: group) }
let!(:group_member) { create(:group_member, group: group, user: user) } let!(:group_member) { create(:group_member, group: group, user: user) }
...@@ -35,14 +35,15 @@ describe GroupsController do ...@@ -35,14 +35,15 @@ describe GroupsController do
sign_in(user) sign_in(user)
end end
it 'shows the public subgroups' do it 'shows all subgroups' do
get :subgroups, id: group.to_param get :subgroups, id: group.to_param
expect(assigns(:nested_groups)).to contain_exactly(public_subgroup) expect(assigns(:nested_groups)).to contain_exactly(public_subgroup, private_subgroup)
end end
context 'being member' do context 'being member of private subgroup' do
it 'shows public and private subgroups the user is member of' do it 'shows public and private subgroups the user is member of' do
group_member.destroy!
private_subgroup.add_guest(user) private_subgroup.add_guest(user)
get :subgroups, id: group.to_param get :subgroups, id: group.to_param
......
...@@ -38,28 +38,79 @@ describe GroupsFinder do ...@@ -38,28 +38,79 @@ describe GroupsFinder do
end end
end end
context 'subgroups' do context 'subgroups', :nested_groups do
let!(:parent_group) { create(:group, :public) } let!(:parent_group) { create(:group, :public) }
let!(:public_subgroup) { create(:group, :public, parent: parent_group) } let!(:public_subgroup) { create(:group, :public, parent: parent_group) }
let!(:internal_subgroup) { create(:group, :internal, parent: parent_group) } let!(:internal_subgroup) { create(:group, :internal, parent: parent_group) }
let!(:private_subgroup) { create(:group, :private, parent: parent_group) } let!(:private_subgroup) { create(:group, :private, parent: parent_group) }
context 'without a user' do context 'without a user' do
it 'only returns public subgroups' do it 'only returns parent and public subgroups' do
expect(described_class.new(nil, parent: parent_group).execute).to contain_exactly(public_subgroup) expect(described_class.new(nil).execute).to contain_exactly(parent_group, public_subgroup)
end end
end end
context 'with a user' do context 'with a user' do
it 'returns public and internal subgroups' do subject { described_class.new(user).execute }
expect(described_class.new(user, parent: parent_group).execute).to contain_exactly(public_subgroup, internal_subgroup)
it 'returns parent, public, and internal subgroups' do
is_expected.to contain_exactly(parent_group, public_subgroup, internal_subgroup)
end end
context 'being member' do context 'being member' do
it 'returns public subgroups, internal subgroups, and private subgroups user is member of' do it 'returns parent, public subgroups, internal subgroups, and private subgroups user is member of' do
private_subgroup.add_guest(user) private_subgroup.add_guest(user)
expect(described_class.new(user, parent: parent_group).execute).to contain_exactly(public_subgroup, internal_subgroup, private_subgroup) is_expected.to contain_exactly(parent_group, public_subgroup, internal_subgroup, private_subgroup)
end
end
context 'parent group private' do
before do
parent_group.update_attribute(:visibility_level, Gitlab::VisibilityLevel::PRIVATE)
end
context 'being member of parent group' do
it 'returns all subgroups' do
parent_group.add_guest(user)
is_expected.to contain_exactly(parent_group, public_subgroup, internal_subgroup, private_subgroup)
end
end
context 'authorized to private project' do
context 'project one level deep' do
let!(:subproject) { create(:empty_project, :private, namespace: private_subgroup) }
before do
subproject.add_guest(user)
end
it 'includes the subgroup of the project' do
is_expected.to include(private_subgroup)
end
it 'does not include private subgroups deeper down' do
subsubgroup = create(:group, :private, parent: private_subgroup)
is_expected.not_to include(subsubgroup)
end
end
context 'project two levels deep' do
let!(:private_subsubgroup) { create(:group, :private, parent: private_subgroup) }
let!(:subsubproject) { create(:empty_project, :private, namespace: private_subsubgroup) }
before do
subsubproject.add_guest(user)
end
it 'returns all the ancestor groups' do
is_expected.to include(private_subsubgroup, private_subgroup, parent_group)
end
it 'returns the groups for a given parent' do
expect(described_class.new(user, parent: parent_group).execute).to include(private_subgroup)
end
end
end end
end end
end end
......
...@@ -17,6 +17,12 @@ describe Gitlab::GroupHierarchy, :postgresql do ...@@ -17,6 +17,12 @@ describe Gitlab::GroupHierarchy, :postgresql do
it 'includes all of the ancestors' do it 'includes all of the ancestors' do
expect(relation).to include(parent, child1) expect(relation).to include(parent, child1)
end end
it 'uses ancestors_base #initialize argument' do
relation = described_class.new(Group.where(id: child2.id), Group.none).base_and_ancestors
expect(relation).to include(parent, child1, child2)
end
end end
describe '#base_and_descendants' do describe '#base_and_descendants' do
...@@ -31,6 +37,12 @@ describe Gitlab::GroupHierarchy, :postgresql do ...@@ -31,6 +37,12 @@ describe Gitlab::GroupHierarchy, :postgresql do
it 'includes all the descendants' do it 'includes all the descendants' do
expect(relation).to include(child1, child2) expect(relation).to include(child1, child2)
end end
it 'uses descendants_base #initialize argument' do
relation = described_class.new(Group.none, Group.where(id: parent.id)).base_and_descendants
expect(relation).to include(parent, child1, child2)
end
end end
describe '#all_groups' do describe '#all_groups' do
...@@ -49,5 +61,17 @@ describe Gitlab::GroupHierarchy, :postgresql do ...@@ -49,5 +61,17 @@ describe Gitlab::GroupHierarchy, :postgresql do
it 'includes the descendants' do it 'includes the descendants' do
expect(relation).to include(child2) expect(relation).to include(child2)
end end
it 'uses ancestors_base #initialize argument for ancestors' do
relation = described_class.new(Group.where(id: child1.id), Group.where(id: Group.maximum(:id).succ)).all_groups
expect(relation).to include(parent)
end
it 'uses descendants_base #initialize argument for descendants' do
relation = described_class.new(Group.where(id: Group.maximum(:id).succ), Group.where(id: child1.id)).all_groups
expect(relation).to include(child2)
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