Commit ef1811f4 authored by Toon Claes's avatar Toon Claes

Subgroups page should show groups authorized through inheritance

When a user is authorized to a group, they are also authorized to see all the
ancestor groups and descendant groups.

When a user is authorized to a project, they are authorized to see all the
ancestor groups too.

Closes #32135

See merge request !11764
parent 42aaae99
...@@ -16,7 +16,11 @@ class GroupsFinder < UnionFinder ...@@ -16,7 +16,11 @@ class GroupsFinder < UnionFinder
def all_groups def all_groups
groups = [] groups = []
groups << current_user.authorized_groups if current_user if current_user
groups_for_ancestors = find_union([current_user.authorized_groups, authorized_project_groups], Group)
groups_for_descendants = current_user.authorized_groups
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
......
...@@ -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 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:
# #
...@@ -49,7 +54,7 @@ module Gitlab ...@@ -49,7 +54,7 @@ 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.
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 +77,7 @@ module Gitlab ...@@ -72,7 +77,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 +91,7 @@ module Gitlab ...@@ -86,7 +91,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,7 +38,7 @@ describe GroupsFinder do ...@@ -38,7 +38,7 @@ 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) }
...@@ -51,15 +51,48 @@ describe GroupsFinder do ...@@ -51,15 +51,48 @@ describe GroupsFinder do
end end
context 'with a user' do context 'with a user' do
subject { described_class.new(user, parent: parent_group).execute }
it 'returns public and internal subgroups' do it 'returns public and internal subgroups' do
expect(described_class.new(user, parent: parent_group).execute).to contain_exactly(public_subgroup, internal_subgroup) is_expected.to contain_exactly(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 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(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(public_subgroup, internal_subgroup, private_subgroup)
end
end
context 'authorized to private project' do
it 'returns the subgroup of the project' do
subproject = create(:empty_project, :private, namespace: private_subgroup)
subproject.add_guest(user)
is_expected.to include(private_subgroup)
end
it 'returns all the parent groups if project is several levels deep' do
private_subsubgroup = create(:group, :private, parent: private_subgroup)
subsubproject = create(:empty_project, :private, namespace: private_subsubgroup)
subsubproject.add_guest(user)
expect(described_class.new(user).execute).to include(private_subsubgroup, private_subgroup, parent_group)
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