Commit daa4c18e authored by Doug Stull's avatar Doug Stull

Switch group member max access to use bulk load

- already created paradigm and exactly what we want.
- prevents duplication...has a much larger impact.

Changelog: performance
parent 83bc32ed
...@@ -25,7 +25,6 @@ class Groups::GroupMembersController < Groups::ApplicationController ...@@ -25,7 +25,6 @@ class Groups::GroupMembersController < Groups::ApplicationController
helper_method :can_manage_members? helper_method :can_manage_members?
def index def index
preload_max_access
@sort = params[:sort].presence || sort_value_name @sort = params[:sort].presence || sort_value_name
@members = GroupMembersFinder @members = GroupMembersFinder
...@@ -54,14 +53,6 @@ class Groups::GroupMembersController < Groups::ApplicationController ...@@ -54,14 +53,6 @@ class Groups::GroupMembersController < Groups::ApplicationController
private private
def preload_max_access
return unless current_user
# this allows the can? against admin type queries in this action to
# only perform the query once, even if it is cached
current_user.max_access_for_group[@group.id] = @group.max_member_access(current_user)
end
def can_manage_members? def can_manage_members?
strong_memoize(:can_manage_members) do strong_memoize(:can_manage_members) do
can?(current_user, :admin_group_member, @group) can?(current_user, :admin_group_member, @group)
......
...@@ -17,6 +17,7 @@ class Group < Namespace ...@@ -17,6 +17,7 @@ class Group < Namespace
include GroupAPICompatibility include GroupAPICompatibility
include EachBatch include EachBatch
include HasTimelogsReport include HasTimelogsReport
include BulkMemberAccessLoad
ACCESS_REQUEST_APPROVERS_TO_BE_NOTIFIED_LIMIT = 10 ACCESS_REQUEST_APPROVERS_TO_BE_NOTIFIED_LIMIT = 10
...@@ -569,24 +570,8 @@ class Group < Namespace ...@@ -569,24 +570,8 @@ class Group < Namespace
def max_member_access_for_user(user, only_concrete_membership: false) def max_member_access_for_user(user, only_concrete_membership: false)
return GroupMember::NO_ACCESS unless user return GroupMember::NO_ACCESS unless user
return GroupMember::OWNER if user.can_admin_all_resources? && !only_concrete_membership return GroupMember::OWNER if user.can_admin_all_resources? && !only_concrete_membership
# Use the preloaded value that exists instead of performing the db query again(cached or not).
# Groups::GroupMembersController#preload_max_access makes use of this by
# calling Group#max_member_access. This helps when we have a process
# that may query this multiple times from the outside through a policy query
# like the GroupPolicy#lookup_access_level! does as a condition for any role
return user.max_access_for_group[id] if user.max_access_for_group[id]
max_member_access(user) max_member_access([user.id])[user.id]
end
def max_member_access(user)
max_member_access = members_with_parents
.where(user_id: user)
.reorder(access_level: :desc)
.first
&.access_level
max_member_access || GroupMember::NO_ACCESS
end end
def mattermost_team_params def mattermost_team_params
...@@ -730,6 +715,12 @@ class Group < Namespace ...@@ -730,6 +715,12 @@ class Group < Namespace
private private
def max_member_access(user_ids)
max_member_access_for_resource_ids(User, user_ids) do |user_ids|
members_with_parents.where(user_id: user_ids).group(:user_id).maximum(:access_level)
end
end
def update_two_factor_requirement def update_two_factor_requirement
return unless saved_change_to_require_two_factor_authentication? || saved_change_to_two_factor_grace_period? return unless saved_change_to_require_two_factor_authentication? || saved_change_to_two_factor_grace_period?
......
...@@ -99,12 +99,6 @@ class User < ApplicationRecord ...@@ -99,12 +99,6 @@ class User < ApplicationRecord
# Virtual attribute for impersonator # Virtual attribute for impersonator
attr_accessor :impersonator attr_accessor :impersonator
attr_writer :max_access_for_group
def max_access_for_group
@max_access_for_group ||= {}
end
# #
# Relations # Relations
# #
......
...@@ -20,15 +20,15 @@ RSpec.describe Groups::GroupMembersController do ...@@ -20,15 +20,15 @@ RSpec.describe Groups::GroupMembersController do
let!(:invited) { create(:group_member, :invited, :developer, group: group) } let!(:invited) { create(:group_member, :invited, :developer, group: group) }
let!(:requested) { create(:group_member, :access_request, group: group) } let!(:requested) { create(:group_member, :access_request, group: group) }
it 'records queries', :use_sql_query_cache do it 'records queries', :request_store, :use_sql_query_cache do
get :index, params: { group_id: group } get :index, params: { group_id: group }
control = ActiveRecord::QueryRecorder.new(skip_cached: false) { get :index, params: { group_id: group } } control = ActiveRecord::QueryRecorder.new(skip_cached: false) { get :index, params: { group_id: group } }
create_list(:group_member, 5, group: group, created_by: user) create_list(:group_member, 5, group: group, created_by: user)
create_list(:group_member, 5, :invited, group: group, created_by: user) create_list(:group_member, 5, :invited, group: group, created_by: user)
create_list(:group_member, 5, :access_request, group: group) create_list(:group_member, 5, :access_request, group: group)
# locally 47 vs 52 GDK vs 57 CI # locally 39 vs 43 GDK vs 48 CI
unresolved_n_plus_ones = 5 # still have a few queries created by can_update/can_remove that should be reduced unresolved_n_plus_ones = 4 # still have a few queries created by can_update/can_remove that could be reduced
multiple_members_threshold = 5 # GDK vs CI difference multiple_members_threshold = 5 # GDK vs CI difference
expect do expect do
......
...@@ -32,14 +32,6 @@ RSpec.describe Groups::GroupMembersController do ...@@ -32,14 +32,6 @@ RSpec.describe Groups::GroupMembersController do
sign_in(user) sign_in(user)
end end
it 'assigns max_access_for_group' do
allow(controller).to receive(:current_user).and_return(user)
get :index, params: { group_id: group }
expect(user.max_access_for_group[group.id]).to eq(Gitlab::Access::OWNER)
end
it 'assigns invited members' do it 'assigns invited members' do
get :index, params: { group_id: group } get :index, params: { group_id: group }
......
...@@ -1093,167 +1093,151 @@ RSpec.describe Group do ...@@ -1093,167 +1093,151 @@ RSpec.describe Group do
it { expect(subject.parent).to be_kind_of(described_class) } it { expect(subject.parent).to be_kind_of(described_class) }
end end
context "with member access" do describe '#max_member_access_for_user' do
let_it_be(:group_user) { create(:user) } let_it_be(:group_user) { create(:user) }
describe '#max_member_access_for_user' do context 'with user in the group' do
context 'with user in the group' do before do
before do group.add_owner(group_user)
group.add_owner(group_user)
end
it 'returns correct access level' do
expect(group.max_member_access_for_user(group_user)).to eq(Gitlab::Access::OWNER)
end
end end
context 'when user is nil' do it 'returns correct access level' do
it 'returns NO_ACCESS' do expect(group.max_member_access_for_user(group_user)).to eq(Gitlab::Access::OWNER)
expect(group.max_member_access_for_user(nil)).to eq(Gitlab::Access::NO_ACCESS)
end
end end
end
context 'evaluating admin access level' do context 'when user is nil' do
let_it_be(:admin) { create(:admin) } it 'returns NO_ACCESS' do
expect(group.max_member_access_for_user(nil)).to eq(Gitlab::Access::NO_ACCESS)
context 'when admin mode is enabled', :enable_admin_mode do end
it 'returns OWNER by default' do end
expect(group.max_member_access_for_user(admin)).to eq(Gitlab::Access::OWNER)
end
end
context 'when admin mode is disabled' do context 'evaluating admin access level' do
it 'returns NO_ACCESS' do let_it_be(:admin) { create(:admin) }
expect(group.max_member_access_for_user(admin)).to eq(Gitlab::Access::NO_ACCESS)
end
end
it 'returns NO_ACCESS when only concrete membership should be considered' do context 'when admin mode is enabled', :enable_admin_mode do
expect(group.max_member_access_for_user(admin, only_concrete_membership: true)) it 'returns OWNER by default' do
.to eq(Gitlab::Access::NO_ACCESS) expect(group.max_member_access_for_user(admin)).to eq(Gitlab::Access::OWNER)
end end
end end
context 'when max_access_for_group is set' do context 'when admin mode is disabled' do
let(:max_member_access) { 111 } it 'returns NO_ACCESS' do
expect(group.max_member_access_for_user(admin)).to eq(Gitlab::Access::NO_ACCESS)
before do
group_user.max_access_for_group[group.id] = max_member_access
end end
end
it 'uses the cached value' do it 'returns NO_ACCESS when only concrete membership should be considered' do
expect(group.max_member_access_for_user(group_user)).to eq(max_member_access) expect(group.max_member_access_for_user(admin, only_concrete_membership: true))
end .to eq(Gitlab::Access::NO_ACCESS)
end end
end end
describe '#max_member_access' do context 'group shared with another group' do
context 'group shared with another group' do let_it_be(:parent_group_user) { create(:user) }
let_it_be(:parent_group_user) { create(:user) } let_it_be(:child_group_user) { create(:user) }
let_it_be(:child_group_user) { create(:user) }
let_it_be(:group_parent) { create(:group, :private) } let_it_be(:group_parent) { create(:group, :private) }
let_it_be(:group) { create(:group, :private, parent: group_parent) } let_it_be(:group) { create(:group, :private, parent: group_parent) }
let_it_be(:group_child) { create(:group, :private, parent: group) } let_it_be(:group_child) { create(:group, :private, parent: group) }
let_it_be(:shared_group_parent) { create(:group, :private) } let_it_be(:shared_group_parent) { create(:group, :private) }
let_it_be(:shared_group) { create(:group, :private, parent: shared_group_parent) } let_it_be(:shared_group) { create(:group, :private, parent: shared_group_parent) }
let_it_be(:shared_group_child) { create(:group, :private, parent: shared_group) } let_it_be(:shared_group_child) { create(:group, :private, parent: shared_group) }
before do before do
group_parent.add_owner(parent_group_user) group_parent.add_owner(parent_group_user)
group.add_owner(group_user) group.add_owner(group_user)
group_child.add_owner(child_group_user) group_child.add_owner(child_group_user)
create(:group_group_link, { shared_with_group: group, create(:group_group_link, { shared_with_group: group,
shared_group: shared_group, shared_group: shared_group,
group_access: GroupMember::DEVELOPER }) group_access: GroupMember::DEVELOPER })
end end
context 'with user in the group' do context 'with user in the group' do
it 'returns correct access level' do it 'returns correct access level' do
expect(shared_group_parent.max_member_access(group_user)).to eq(Gitlab::Access::NO_ACCESS) expect(shared_group_parent.max_member_access_for_user(group_user)).to eq(Gitlab::Access::NO_ACCESS)
expect(shared_group.max_member_access(group_user)).to eq(Gitlab::Access::DEVELOPER) expect(shared_group.max_member_access_for_user(group_user)).to eq(Gitlab::Access::DEVELOPER)
expect(shared_group_child.max_member_access(group_user)).to eq(Gitlab::Access::DEVELOPER) expect(shared_group_child.max_member_access_for_user(group_user)).to eq(Gitlab::Access::DEVELOPER)
end end
context 'with lower group access level than max access level for share' do context 'with lower group access level than max access level for share' do
let(:user) { create(:user) } let(:user) { create(:user) }
it 'returns correct access level' do it 'returns correct access level' do
group.add_reporter(user) group.add_reporter(user)
expect(shared_group_parent.max_member_access(user)).to eq(Gitlab::Access::NO_ACCESS) expect(shared_group_parent.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
expect(shared_group.max_member_access(user)).to eq(Gitlab::Access::REPORTER) expect(shared_group.max_member_access_for_user(user)).to eq(Gitlab::Access::REPORTER)
expect(shared_group_child.max_member_access(user)).to eq(Gitlab::Access::REPORTER) expect(shared_group_child.max_member_access_for_user(user)).to eq(Gitlab::Access::REPORTER)
end
end end
end end
end
context 'with user in the parent group' do context 'with user in the parent group' do
it 'returns correct access level' do it 'returns correct access level' do
expect(shared_group_parent.max_member_access(parent_group_user)).to eq(Gitlab::Access::NO_ACCESS) expect(shared_group_parent.max_member_access_for_user(parent_group_user)).to eq(Gitlab::Access::NO_ACCESS)
expect(shared_group.max_member_access(parent_group_user)).to eq(Gitlab::Access::NO_ACCESS) expect(shared_group.max_member_access_for_user(parent_group_user)).to eq(Gitlab::Access::NO_ACCESS)
expect(shared_group_child.max_member_access(parent_group_user)).to eq(Gitlab::Access::NO_ACCESS) expect(shared_group_child.max_member_access_for_user(parent_group_user)).to eq(Gitlab::Access::NO_ACCESS)
end
end end
end
context 'with user in the child group' do context 'with user in the child group' do
it 'returns correct access level' do it 'returns correct access level' do
expect(shared_group_parent.max_member_access(child_group_user)).to eq(Gitlab::Access::NO_ACCESS) expect(shared_group_parent.max_member_access_for_user(child_group_user)).to eq(Gitlab::Access::NO_ACCESS)
expect(shared_group.max_member_access(child_group_user)).to eq(Gitlab::Access::NO_ACCESS) expect(shared_group.max_member_access_for_user(child_group_user)).to eq(Gitlab::Access::NO_ACCESS)
expect(shared_group_child.max_member_access(child_group_user)).to eq(Gitlab::Access::NO_ACCESS) expect(shared_group_child.max_member_access_for_user(child_group_user)).to eq(Gitlab::Access::NO_ACCESS)
end
end end
end
context 'unrelated project owner' do context 'unrelated project owner' do
let(:common_id) { [Project.maximum(:id).to_i, Namespace.maximum(:id).to_i].max + 999 } let(:common_id) { [Project.maximum(:id).to_i, Namespace.maximum(:id).to_i].max + 999 }
let!(:group) { create(:group, id: common_id) } let!(:group) { create(:group, id: common_id) }
let!(:unrelated_project) { create(:project, id: common_id) } let!(:unrelated_project) { create(:project, id: common_id) }
let(:user) { unrelated_project.owner } let(:user) { unrelated_project.owner }
it 'returns correct access level' do it 'returns correct access level' do
expect(shared_group_parent.max_member_access(user)).to eq(Gitlab::Access::NO_ACCESS) expect(shared_group_parent.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
expect(shared_group.max_member_access(user)).to eq(Gitlab::Access::NO_ACCESS) expect(shared_group.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
expect(shared_group_child.max_member_access(user)).to eq(Gitlab::Access::NO_ACCESS) expect(shared_group_child.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
end
end end
end
context 'user without accepted access request' do context 'user without accepted access request' do
let!(:user) { create(:user) } let!(:user) { create(:user) }
before do before do
create(:group_member, :developer, :access_request, user: user, group: group) create(:group_member, :developer, :access_request, user: user, group: group)
end end
it 'returns correct access level' do it 'returns correct access level' do
expect(shared_group_parent.max_member_access(user)).to eq(Gitlab::Access::NO_ACCESS) expect(shared_group_parent.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
expect(shared_group.max_member_access(user)).to eq(Gitlab::Access::NO_ACCESS) expect(shared_group.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
expect(shared_group_child.max_member_access(user)).to eq(Gitlab::Access::NO_ACCESS) expect(shared_group_child.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
end
end end
end end
end
context 'multiple groups shared with group' do context 'multiple groups shared with group' do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:group) { create(:group, :private) } let(:group) { create(:group, :private) }
let(:shared_group_parent) { create(:group, :private) } let(:shared_group_parent) { create(:group, :private) }
let(:shared_group) { create(:group, :private, parent: shared_group_parent) } let(:shared_group) { create(:group, :private, parent: shared_group_parent) }
before do before do
group.add_owner(user) group.add_owner(user)
create(:group_group_link, { shared_with_group: group, create(:group_group_link, { shared_with_group: group,
shared_group: shared_group, shared_group: shared_group,
group_access: GroupMember::DEVELOPER }) group_access: GroupMember::DEVELOPER })
create(:group_group_link, { shared_with_group: group, create(:group_group_link, { shared_with_group: group,
shared_group: shared_group_parent, shared_group: shared_group_parent,
group_access: GroupMember::MAINTAINER }) group_access: GroupMember::MAINTAINER })
end end
it 'returns correct access level' do it 'returns correct access level' do
expect(shared_group.max_member_access(user)).to eq(Gitlab::Access::MAINTAINER) expect(shared_group.max_member_access_for_user(user)).to eq(Gitlab::Access::MAINTAINER)
end
end 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