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
def all_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
......
......@@ -3,33 +3,38 @@ module Gitlab
#
# This class uses recursive CTEs and as a result will only work on PostgreSQL.
class GroupHierarchy
attr_reader :base, :model
# base - An instance of ActiveRecord::Relation for which to get parent or
# child groups.
def initialize(base)
@base = base
@model = base.model
attr_reader :ancestors_base, :descendants_base, :model
# ancestors_base - An instance of ActiveRecord::Relation for which to
# get parent groups.
# descendants_base - An instance of ActiveRecord::Relation for which to
# get child groups. If omitted, ancestors_base is used.
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
# Returns a relation that includes the base set of groups and all their
# ancestors (recursively).
# Returns a relation that includes the ancestors_base set of groups
# and all their ancestors (recursively).
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)
end
# Returns a relation that includes the base set of groups and all their
# descendants (recursively).
# Returns a relation that includes the descendants_base set of groups
# and all their descendants (recursively).
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)
end
# Returns a relation that includes the base groups, their ancestors, and the
# descendants of the base groups.
# Returns a relation that includes the base groups, their ancestors,
# and the descendants of the base groups.
#
# The resulting query will roughly look like the following:
#
......@@ -49,7 +54,7 @@ module Gitlab
# Using this approach allows us to further add criteria to the relation with
# Rails thinking it's selecting data the usual way.
def all_groups
return base unless Group.supports_nested_groups?
return ancestors_base unless Group.supports_nested_groups?
ancestors = base_and_ancestors_cte
descendants = base_and_descendants_cte
......@@ -72,7 +77,7 @@ module Gitlab
def base_and_ancestors_cte
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.
cte << model.
......@@ -86,7 +91,7 @@ module Gitlab
def base_and_descendants_cte
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.
cte << model.
......
......@@ -2,7 +2,7 @@ require 'rails_helper'
describe GroupsController do
let(:user) { create(:user) }
let(:group) { create(:group) }
let(:group) { create(:group, :public) }
let(:project) { create(:empty_project, namespace: group) }
let!(:group_member) { create(:group_member, group: group, user: user) }
......@@ -35,14 +35,15 @@ describe GroupsController do
sign_in(user)
end
it 'shows the public subgroups' do
it 'shows all subgroups' do
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
context 'being member' do
context 'being member of private subgroup' do
it 'shows public and private subgroups the user is member of' do
group_member.destroy!
private_subgroup.add_guest(user)
get :subgroups, id: group.to_param
......
......@@ -38,7 +38,7 @@ describe GroupsFinder do
end
end
context 'subgroups' do
context 'subgroups', :nested_groups do
let!(:parent_group) { create(:group, :public) }
let!(:public_subgroup) { create(:group, :public, parent: parent_group) }
let!(:internal_subgroup) { create(:group, :internal, parent: parent_group) }
......@@ -51,15 +51,48 @@ describe GroupsFinder do
end
context 'with a user' do
subject { described_class.new(user, parent: parent_group).execute }
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
context 'being member' do
it 'returns public subgroups, internal subgroups, and private subgroups user is member of' do
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
......
......@@ -17,6 +17,12 @@ describe Gitlab::GroupHierarchy, :postgresql do
it 'includes all of the ancestors' do
expect(relation).to include(parent, child1)
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
describe '#base_and_descendants' do
......@@ -31,6 +37,12 @@ describe Gitlab::GroupHierarchy, :postgresql do
it 'includes all the descendants' do
expect(relation).to include(child1, child2)
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
describe '#all_groups' do
......@@ -49,5 +61,17 @@ describe Gitlab::GroupHierarchy, :postgresql do
it 'includes the descendants' do
expect(relation).to include(child2)
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
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