Commit dfe9aca1 authored by James Lopez's avatar James Lopez

Merge branch 'fix/group_members_max_access_level' into 'master'

Fix effective access level of group members

See merge request gitlab-org/gitlab!56677
parents ec0fc9cf 46ef836b
...@@ -21,28 +21,13 @@ class GroupMembersFinder < UnionFinder ...@@ -21,28 +21,13 @@ class GroupMembersFinder < UnionFinder
end end
def execute(include_relations: DEFAULT_RELATIONS) def execute(include_relations: DEFAULT_RELATIONS)
group_members = group_members_list return filter_members(group_members_list) if include_relations == [:direct]
relations = []
return filter_members(group_members) if include_relations == [:direct] groups = groups_by_relations(include_relations)
return GroupMember.none unless groups
relations << group_members if include_relations.include?(:direct) members = all_group_members(groups).distinct_on_user_with_max_access_level
if include_relations.include?(:inherited) && group.parent
parents_members = relation_group_members(group.ancestors)
relations << parents_members
end
if include_relations.include?(:descendants)
descendant_members = relation_group_members(group.descendants)
relations << descendant_members
end
return GroupMember.none if relations.empty?
members = find_union(relations, GroupMember)
filter_members(members) filter_members(members)
end end
...@@ -50,6 +35,25 @@ class GroupMembersFinder < UnionFinder ...@@ -50,6 +35,25 @@ class GroupMembersFinder < UnionFinder
attr_reader :user, :group attr_reader :user, :group
def groups_by_relations(include_relations)
case include_relations.sort
when [:inherited]
group.ancestors
when [:descendants]
group.descendants
when [:direct, :inherited]
group.self_and_ancestors
when [:descendants, :direct]
group.self_and_descendants
when [:descendants, :inherited]
find_union([group.ancestors, group.descendants], Group)
when [:descendants, :direct, :inherited]
group.self_and_hierarchy
else
nil
end
end
def filter_members(members) def filter_members(members)
members = members.search(params[:search]) if params[:search].present? members = members.search(params[:search]) if params[:search].present?
members = members.sort_by_attribute(params[:sort]) if params[:sort].present? members = members.sort_by_attribute(params[:sort]) if params[:sort].present?
...@@ -69,17 +73,13 @@ class GroupMembersFinder < UnionFinder ...@@ -69,17 +73,13 @@ class GroupMembersFinder < UnionFinder
group.members group.members
end end
def relation_group_members(relation) def all_group_members(groups)
all_group_members(relation).non_minimal_access members_of_groups(groups).non_minimal_access
end end
# rubocop: disable CodeReuse/ActiveRecord def members_of_groups(groups)
def all_group_members(relation) GroupMember.non_request.of_groups(groups)
GroupMember.non_request
.where(source_id: relation.select(:id))
.where.not(user_id: group.users.select(:id))
end end
# rubocop: enable CodeReuse/ActiveRecord
end end
GroupMembersFinder.prepend_if_ee('EE::GroupMembersFinder') GroupMembersFinder.prepend_if_ee('EE::GroupMembersFinder')
...@@ -137,6 +137,12 @@ class Member < ApplicationRecord ...@@ -137,6 +137,12 @@ class Member < ApplicationRecord
scope :with_source_id, ->(source_id) { where(source_id: source_id) } scope :with_source_id, ->(source_id) { where(source_id: source_id) }
scope :including_source, -> { includes(:source) } scope :including_source, -> { includes(:source) }
scope :distinct_on_user_with_max_access_level, -> do
distinct_members = select('DISTINCT ON (user_id, invite_email) *')
.order('user_id, invite_email, access_level DESC, expires_at DESC, created_at ASC')
Member.from(distinct_members, :members)
end
scope :order_name_asc, -> { left_join_users.reorder(Gitlab::Database.nulls_last_order('users.name', 'ASC')) } scope :order_name_asc, -> { left_join_users.reorder(Gitlab::Database.nulls_last_order('users.name', 'ASC')) }
scope :order_name_desc, -> { left_join_users.reorder(Gitlab::Database.nulls_last_order('users.name', 'DESC')) } scope :order_name_desc, -> { left_join_users.reorder(Gitlab::Database.nulls_last_order('users.name', 'DESC')) }
scope :order_recent_sign_in, -> { left_join_users.reorder(Gitlab::Database.nulls_last_order('users.last_sign_in_at', 'DESC')) } scope :order_recent_sign_in, -> { left_join_users.reorder(Gitlab::Database.nulls_last_order('users.last_sign_in_at', 'DESC')) }
......
---
title: Fix derivation of effective permissions (access level) of group members
merge_request: 56677
author: Jonas Wälter @wwwjon
type: fixed
...@@ -90,8 +90,8 @@ Example response: ...@@ -90,8 +90,8 @@ Example response:
Gets a list of group or project members viewable by the authenticated user, including inherited members and permissions through ancestor groups. Gets a list of group or project members viewable by the authenticated user, including inherited members and permissions through ancestor groups.
WARNING: If a user is a member of this group or project and also of one or more ancestor groups, only its membership with the highest `access_level` is returned.
Due to [an issue](https://gitlab.com/gitlab-org/gitlab/-/issues/249523), the users effective `access_level` may actually be higher than returned value when listing group members. This represents the effective permission of the user.
This function takes pagination parameters `page` and `per_page` to restrict the list of users. This function takes pagination parameters `page` and `per_page` to restrict the list of users.
......
...@@ -21,9 +21,9 @@ module EE::GroupMembersFinder ...@@ -21,9 +21,9 @@ module EE::GroupMembersFinder
super super
end end
override :relation_group_members override :all_group_members
def relation_group_members(relation) def all_group_members(groups)
return all_group_members(relation) if group.minimal_access_role_allowed? return members_of_groups(groups) if group.minimal_access_role_allowed?
super super
end end
......
...@@ -4,80 +4,83 @@ require 'spec_helper' ...@@ -4,80 +4,83 @@ require 'spec_helper'
RSpec.describe GroupMembersFinder, '#execute' do RSpec.describe GroupMembersFinder, '#execute' do
let(:group) { create(:group) } let(:group) { create(:group) }
let(:nested_group) { create(:group, parent: group) } let(:sub_group) { create(:group, parent: group) }
let(:deeper_nested_group) { create(:group, parent: nested_group) } let(:sub_sub_group) { create(:group, parent: sub_group) }
let(:user1) { create(:user) } let(:user1) { create(:user) }
let(:user2) { create(:user) } let(:user2) { create(:user) }
let(:user3) { create(:user) } let(:user3) { create(:user) }
let(:user4) { create(:user) } let(:user4) { create(:user) }
let(:user5) { create(:user, :two_factor_via_otp) } let(:user5) { create(:user, :two_factor_via_otp) }
it 'returns members for top-level group' do let(:groups) do
member1 = group.add_maintainer(user1) {
member2 = group.add_maintainer(user2) group: group,
member3 = group.add_maintainer(user3) sub_group: sub_group,
create(:group_member, :minimal_access, user: create(:user), source: group) sub_sub_group: sub_sub_group
}
result = described_class.new(group).execute
expect(result.to_a).to match_array([member3, member2, member1])
end end
it 'returns members & inherited members for nested group by default' do context 'relations' do
group.add_developer(user2) let!(:members) do
nested_group.request_access(user4) {
member1 = group.add_maintainer(user1) user1_sub_sub_group: create(:group_member, :maintainer, group: sub_sub_group, user: user1),
member3 = nested_group.add_maintainer(user2) user1_sub_group: create(:group_member, :developer, group: sub_group, user: user1),
member4 = nested_group.add_maintainer(user3) user1_group: create(:group_member, :reporter, group: group, user: user1),
user2_sub_sub_group: create(:group_member, :reporter, group: sub_sub_group, user: user2),
result = described_class.new(nested_group).execute user2_sub_group: create(:group_member, :developer, group: sub_group, user: user2),
user2_group: create(:group_member, :maintainer, group: group, user: user2),
expect(result.to_a).to match_array([member1, member3, member4]) user3_sub_sub_group: create(:group_member, :developer, group: sub_sub_group, user: user3, expires_at: 1.day.from_now),
user3_sub_group: create(:group_member, :developer, group: sub_group, user: user3, expires_at: 2.days.from_now),
user3_group: create(:group_member, :reporter, group: group, user: user3),
user4_sub_sub_group: create(:group_member, :reporter, group: sub_sub_group, user: user4),
user4_sub_group: create(:group_member, :developer, group: sub_group, user: user4, expires_at: 1.day.from_now),
user4_group: create(:group_member, :developer, group: group, user: user4, expires_at: 2.days.from_now)
}
end end
it 'does not return inherited members for nested group if requested' do using RSpec::Parameterized::TableSyntax
group.add_maintainer(user1)
group.add_developer(user2) where(:subject_relations, :subject_group, :expected_members) do
member2 = nested_group.add_maintainer(user2) nil | :group | [:user1_group, :user2_group, :user3_group, :user4_group]
member3 = nested_group.add_maintainer(user3) [:direct] | :group | [:user1_group, :user2_group, :user3_group, :user4_group]
[:inherited] | :group | []
result = described_class.new(nested_group).execute(include_relations: [:direct]) [:descendants] | :group | [:user1_sub_sub_group, :user2_sub_group, :user3_sub_group, :user4_sub_group]
[:direct, :inherited] | :group | [:user1_group, :user2_group, :user3_group, :user4_group]
expect(result.to_a).to match_array([member2, member3]) [:direct, :descendants] | :group | [:user1_sub_sub_group, :user2_group, :user3_sub_group, :user4_group]
[:descendants, :inherited] | :group | [:user1_sub_sub_group, :user2_sub_group, :user3_sub_group, :user4_sub_group]
[:direct, :descendants, :inherited] | :group | [:user1_sub_sub_group, :user2_group, :user3_sub_group, :user4_group]
nil | :sub_group | [:user1_sub_group, :user2_group, :user3_sub_group, :user4_group]
[:direct] | :sub_group | [:user1_sub_group, :user2_sub_group, :user3_sub_group, :user4_sub_group]
[:inherited] | :sub_group | [:user1_group, :user2_group, :user3_group, :user4_group]
[:descendants] | :sub_group | [:user1_sub_sub_group, :user2_sub_sub_group, :user3_sub_sub_group, :user4_sub_sub_group]
[:direct, :inherited] | :sub_group | [:user1_sub_group, :user2_group, :user3_sub_group, :user4_group]
[:direct, :descendants] | :sub_group | [:user1_sub_sub_group, :user2_sub_group, :user3_sub_group, :user4_sub_group]
[:descendants, :inherited] | :sub_group | [:user1_sub_sub_group, :user2_group, :user3_sub_sub_group, :user4_group]
[:direct, :descendants, :inherited] | :sub_group | [:user1_sub_sub_group, :user2_group, :user3_sub_group, :user4_group]
nil | :sub_sub_group | [:user1_sub_sub_group, :user2_group, :user3_sub_group, :user4_group]
[:direct] | :sub_sub_group | [:user1_sub_sub_group, :user2_sub_sub_group, :user3_sub_sub_group, :user4_sub_sub_group]
[:inherited] | :sub_sub_group | [:user1_sub_group, :user2_group, :user3_sub_group, :user4_group]
[:descendants] | :sub_sub_group | []
[:direct, :inherited] | :sub_sub_group | [:user1_sub_sub_group, :user2_group, :user3_sub_group, :user4_group]
[:direct, :descendants] | :sub_sub_group | [:user1_sub_sub_group, :user2_sub_sub_group, :user3_sub_sub_group, :user4_sub_sub_group]
[:descendants, :inherited] | :sub_sub_group | [:user1_sub_group, :user2_group, :user3_sub_group, :user4_group]
[:direct, :descendants, :inherited] | :sub_sub_group | [:user1_sub_sub_group, :user2_group, :user3_sub_group, :user4_group]
end end
it 'returns only inherited members for nested group if requested' do with_them do
group.add_developer(user2) it 'returns correct members' do
nested_group.request_access(user4) result = if subject_relations
member1 = group.add_maintainer(user1) described_class.new(groups[subject_group]).execute(include_relations: subject_relations)
nested_group.add_maintainer(user2) else
nested_group.add_maintainer(user3) described_class.new(groups[subject_group]).execute
result = described_class.new(nested_group).execute(include_relations: [:inherited])
expect(result.to_a).to match_array([member1])
end end
it 'does not return nil if `inherited only` relation is requested on root group' do expect(result.to_a).to match_array(expected_members.map { |name| members[name] })
group.add_developer(user2) end
result = described_class.new(group).execute(include_relations: [:inherited])
expect(result).not_to be_nil
end end
it 'returns members for descendant groups if requested' do
member1 = group.add_maintainer(user2)
member2 = group.add_maintainer(user1)
nested_group.add_maintainer(user2)
member3 = nested_group.add_maintainer(user3)
member4 = nested_group.add_maintainer(user4)
result = described_class.new(group).execute(include_relations: [:direct, :descendants])
expect(result.to_a).to match_array([member1, member2, member3, member4])
end end
context 'search' do
it 'returns searched members if requested' do it 'returns searched members if requested' do
group.add_maintainer(user2) group.add_maintainer(user2)
group.add_developer(user3) group.add_developer(user3)
...@@ -98,17 +101,19 @@ RSpec.describe GroupMembersFinder, '#execute' do ...@@ -98,17 +101,19 @@ RSpec.describe GroupMembersFinder, '#execute' do
expect(result.to_a).to match_array([]) expect(result.to_a).to match_array([])
end end
it 'returns searched member only from nested_group if search only in inherited relation' do it 'returns searched member only from sub_group if search only in inherited relation' do
group.add_maintainer(user2) group.add_maintainer(user2)
group.add_developer(user3) group.add_developer(user3)
nested_group.add_maintainer(create(:user, name: user1.name)) sub_group.add_maintainer(create(:user, name: user1.name))
member = group.add_maintainer(user1) member = group.add_maintainer(user1)
result = described_class.new(nested_group, params: { search: member.user.name }).execute(include_relations: [:inherited]) result = described_class.new(sub_group, params: { search: member.user.name }).execute(include_relations: [:inherited])
expect(result.to_a).to contain_exactly(member) expect(result.to_a).to contain_exactly(member)
end end
end
context 'filter by two-factor' do
it 'returns members with two-factor auth if requested by owner' do it 'returns members with two-factor auth if requested by owner' do
group.add_owner(user2) group.add_owner(user2)
group.add_maintainer(user1) group.add_maintainer(user1)
...@@ -133,10 +138,10 @@ RSpec.describe GroupMembersFinder, '#execute' do ...@@ -133,10 +138,10 @@ RSpec.describe GroupMembersFinder, '#execute' do
it 'returns direct members with two-factor auth if requested by owner' do it 'returns direct members with two-factor auth if requested by owner' do
group.add_owner(user1) group.add_owner(user1)
group.add_maintainer(user2) group.add_maintainer(user2)
nested_group.add_maintainer(user3) sub_group.add_maintainer(user3)
member_with_2fa = nested_group.add_maintainer(user5) member_with_2fa = sub_group.add_maintainer(user5)
result = described_class.new(nested_group, user1, params: { two_factor: 'enabled' }).execute(include_relations: [:direct]) result = described_class.new(sub_group, user1, params: { two_factor: 'enabled' }).execute(include_relations: [:direct])
expect(result.to_a).to match_array([member_with_2fa]) expect(result.to_a).to match_array([member_with_2fa])
end end
...@@ -144,10 +149,10 @@ RSpec.describe GroupMembersFinder, '#execute' do ...@@ -144,10 +149,10 @@ RSpec.describe GroupMembersFinder, '#execute' do
it 'returns inherited members with two-factor auth if requested by owner' do it 'returns inherited members with two-factor auth if requested by owner' do
group.add_owner(user1) group.add_owner(user1)
member_with_2fa = group.add_maintainer(user5) member_with_2fa = group.add_maintainer(user5)
nested_group.add_maintainer(user2) sub_group.add_maintainer(user2)
nested_group.add_maintainer(user3) sub_group.add_maintainer(user3)
result = described_class.new(nested_group, user1, params: { two_factor: 'enabled' }).execute(include_relations: [:inherited]) result = described_class.new(sub_group, user1, params: { two_factor: 'enabled' }).execute(include_relations: [:inherited])
expect(result.to_a).to match_array([member_with_2fa]) expect(result.to_a).to match_array([member_with_2fa])
end end
...@@ -155,10 +160,10 @@ RSpec.describe GroupMembersFinder, '#execute' do ...@@ -155,10 +160,10 @@ RSpec.describe GroupMembersFinder, '#execute' do
it 'returns direct members without two-factor auth if requested by owner' do it 'returns direct members without two-factor auth if requested by owner' do
group.add_owner(user1) group.add_owner(user1)
group.add_maintainer(user2) group.add_maintainer(user2)
member3 = nested_group.add_maintainer(user3) member3 = sub_group.add_maintainer(user3)
nested_group.add_maintainer(user5) sub_group.add_maintainer(user5)
result = described_class.new(nested_group, user1, params: { two_factor: 'disabled' }).execute(include_relations: [:direct]) result = described_class.new(sub_group, user1, params: { two_factor: 'disabled' }).execute(include_relations: [:direct])
expect(result.to_a).to match_array([member3]) expect(result.to_a).to match_array([member3])
end end
...@@ -166,11 +171,12 @@ RSpec.describe GroupMembersFinder, '#execute' do ...@@ -166,11 +171,12 @@ RSpec.describe GroupMembersFinder, '#execute' do
it 'returns inherited members without two-factor auth if requested by owner' do it 'returns inherited members without two-factor auth if requested by owner' do
member1 = group.add_owner(user1) member1 = group.add_owner(user1)
group.add_maintainer(user5) group.add_maintainer(user5)
nested_group.add_maintainer(user2) sub_group.add_maintainer(user2)
nested_group.add_maintainer(user3) sub_group.add_maintainer(user3)
result = described_class.new(nested_group, user1, params: { two_factor: 'disabled' }).execute(include_relations: [:inherited]) result = described_class.new(sub_group, user1, params: { two_factor: 'disabled' }).execute(include_relations: [:inherited])
expect(result.to_a).to match_array([member1]) expect(result.to_a).to match_array([member1])
end end
end
end end
...@@ -413,6 +413,24 @@ RSpec.describe Member do ...@@ -413,6 +413,24 @@ RSpec.describe Member do
it { is_expected.not_to include @blocked_developer } it { is_expected.not_to include @blocked_developer }
it { is_expected.not_to include @member_with_minimal_access } it { is_expected.not_to include @member_with_minimal_access }
end end
describe '.distinct_on_user_with_max_access_level' do
let_it_be(:other_group) { create(:group) }
let_it_be(:member_with_lower_access_level) { create(:group_member, :developer, group: other_group, user: @owner_user) }
subject { described_class.default_scoped.distinct_on_user_with_max_access_level.to_a }
it { is_expected.not_to include member_with_lower_access_level }
it { is_expected.to include @owner }
it { is_expected.to include @maintainer }
it { is_expected.to include @invited_member }
it { is_expected.to include @accepted_invite_member }
it { is_expected.to include @requested_member }
it { is_expected.to include @accepted_request_member }
it { is_expected.to include @blocked_maintainer }
it { is_expected.to include @blocked_developer }
it { is_expected.to include @member_with_minimal_access }
end
end end
describe "Delegate methods" do describe "Delegate methods" do
......
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