Commit 26cf7513 authored by Aishwarya Subramanian's avatar Aishwarya Subramanian Committed by Bob Van Landuyt

Display user group count in Admin dashboard

Displays the total number of authorized groups
for individual users in the admin dashboard.
parent e70f863d
# frozen_string_literal: true
class UserGroupsCounter
def initialize(user_ids)
@user_ids = user_ids
end
def execute
Namespace.unscoped do
Namespace.from_union([
groups,
project_groups
]).group(:user_id).count # rubocop: disable CodeReuse/ActiveRecord
end
end
private
attr_reader :user_ids
def groups
Group.for_authorized_group_members(user_ids)
.select('namespaces.*, members.user_id as user_id')
end
def project_groups
Group.for_authorized_project_members(user_ids)
.select('namespaces.*, project_authorizations.user_id as user_id')
end
end
# frozen_string_literal: true
module Resolvers
module Users
class GroupCountResolver < BaseResolver
alias_method :user, :object
def resolve(**args)
return unless can_read_group_count?
BatchLoader::GraphQL.for(user.id).batch do |user_ids, loader|
results = UserGroupsCounter.new(user_ids).execute
results.each do |user_id, count|
loader.call(user_id, count)
end
end
end
def can_read_group_count?
current_user&.can?(:read_group_count, user)
end
end
end
end
...@@ -32,6 +32,10 @@ module Types ...@@ -32,6 +32,10 @@ module Types
field :group_memberships, Types::GroupMemberType.connection_type, null: true, field :group_memberships, Types::GroupMemberType.connection_type, null: true,
description: 'Group memberships of the user', description: 'Group memberships of the user',
method: :group_members method: :group_members
field :group_count, GraphQL::INT_TYPE, null: true,
resolver: Resolvers::Users::GroupCountResolver,
description: 'Group count for the user',
feature_flag: :user_group_counts
field :status, Types::UserStatusType, null: true, field :status, Types::UserStatusType, null: true,
description: 'User status' description: 'User status'
field :project_memberships, Types::ProjectMemberType.connection_type, null: true, field :project_memberships, Types::ProjectMemberType.connection_type, null: true,
......
...@@ -98,6 +98,17 @@ class Group < Namespace ...@@ -98,6 +98,17 @@ class Group < Namespace
scope :by_id, ->(groups) { where(id: groups) } scope :by_id, ->(groups) { where(id: groups) }
scope :for_authorized_group_members, -> (user_ids) do
joins(:group_members)
.where("members.user_id IN (?)", user_ids)
.where("access_level >= ?", Gitlab::Access::GUEST)
end
scope :for_authorized_project_members, -> (user_ids) do
joins(projects: :project_authorizations)
.where("project_authorizations.user_id IN (?)", user_ids)
end
class << self class << self
def sort_by_attribute(method) def sort_by_attribute(method)
if method == 'storage_size_desc' if method == 'storage_size_desc'
......
...@@ -21,6 +21,7 @@ class UserPolicy < BasePolicy ...@@ -21,6 +21,7 @@ class UserPolicy < BasePolicy
enable :update_user enable :update_user
enable :update_user_status enable :update_user_status
enable :read_user_personal_access_tokens enable :read_user_personal_access_tokens
enable :read_group_count
end end
rule { default }.enable :read_user_profile rule { default }.enable :read_user_profile
......
---
name: user_group_counts
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/44069/
rollout_issue_url:
type: development
group: group::compliance
default_enabled: false
...@@ -21541,6 +21541,11 @@ type User { ...@@ -21541,6 +21541,11 @@ type User {
""" """
email: String email: String
"""
Group count for the user. Available only when feature flag `user_group_counts` is enabled
"""
groupCount: Int
""" """
Group memberships of the user Group memberships of the user
""" """
......
...@@ -62400,6 +62400,20 @@ ...@@ -62400,6 +62400,20 @@
"isDeprecated": false, "isDeprecated": false,
"deprecationReason": null "deprecationReason": null
}, },
{
"name": "groupCount",
"description": "Group count for the user. Available only when feature flag `user_group_counts` is enabled",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{ {
"name": "groupMemberships", "name": "groupMemberships",
"description": "Group memberships of the user", "description": "Group memberships of the user",
...@@ -3041,6 +3041,7 @@ Autogenerated return type of UpdateSnippet. ...@@ -3041,6 +3041,7 @@ Autogenerated return type of UpdateSnippet.
| ----- | ---- | ----------- | | ----- | ---- | ----------- |
| `avatarUrl` | String | URL of the user's avatar | | `avatarUrl` | String | URL of the user's avatar |
| `email` | String | User email | | `email` | String | User email |
| `groupCount` | Int | Group count for the user. Available only when feature flag `user_group_counts` is enabled |
| `id` | ID! | ID of the user | | `id` | ID! | ID of the user |
| `name` | String! | Human-readable name of the user | | `name` | String! | Human-readable name of the user |
| `state` | UserState! | State of the user | | `state` | UserState! | State of the user |
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe UserGroupsCounter do
subject { described_class.new(user_ids).execute }
describe '#execute' do
let_it_be(:user) { create(:user) }
let_it_be(:group1) { create(:group) }
let_it_be(:group_member1) { create(:group_member, source: group1, user_id: user.id, access_level: Gitlab::Access::OWNER) }
let_it_be(:user_ids) { [user.id] }
it 'returns authorized group count for the user' do
expect(subject[user.id]).to eq(1)
end
context 'when request to join group is pending' do
let_it_be(:pending_group) { create(:group) }
let_it_be(:pending_group_member) { create(:group_member, requested_at: Time.current.utc, source: pending_group, user_id: user.id) }
it 'does not include pending group in the count' do
expect(subject[user.id]).to eq(1)
end
end
context 'when user is part of sub group' do
let_it_be(:sub_group) { create(:group, parent: create(:group)) }
let_it_be(:sub_group_member1) { create(:group_member, source: sub_group, user_id: user.id, access_level: Gitlab::Access::DEVELOPER) }
it 'includes sub group in the count' do
expect(subject[user.id]).to eq(2)
end
end
context 'when user is part of namespaced project' do
let_it_be(:project) { create(:project, group: create(:group)) }
let_it_be(:project_member) { create(:project_member, source: project, user_id: user.id, access_level: Gitlab::Access::REPORTER) }
it 'includes the project group' do
expect(subject[user.id]).to eq(2)
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Resolvers::Users::GroupCountResolver do
include GraphqlHelpers
describe '#resolve' do
let_it_be(:user1) { create(:user) }
let_it_be(:user2) { create(:user) }
let_it_be(:group1) { create(:group) }
let_it_be(:group2) { create(:group) }
let_it_be(:project) { create(:project, group: create(:group)) }
let_it_be(:group_member1) { create(:group_member, source: group1, user_id: user1.id, access_level: Gitlab::Access::OWNER) }
let_it_be(:project_member1) { create(:project_member, source: project, user_id: user1.id, access_level: Gitlab::Access::DEVELOPER) }
let_it_be(:group_member2) { create(:group_member, source: group2, user_id: user2.id, access_level: Gitlab::Access::DEVELOPER) }
it 'resolves group count for users' do
current_user = user1
result = batch_sync do
[user1, user2].map { |user| resolve_group_count(user, current_user) }
end
expect(result).to eq([2, nil])
end
context 'permissions' do
context 'when current_user is an admin', :enable_admin_mode do
let_it_be(:admin) { create(:admin) }
it do
result = batch_sync do
[user1, user2].map { |user| resolve_group_count(user, admin) }
end
expect(result).to eq([2, 1])
end
end
context 'when current_user does not have access to the requested resource' do
it do
result = batch_sync { resolve_group_count(user1, user2) }
expect(result).to be nil
end
end
context 'when current_user does not exist' do
it do
result = batch_sync { resolve_group_count(user1, nil) }
expect(result).to be nil
end
end
end
end
def resolve_group_count(user, current_user)
resolve(described_class, obj: user, ctx: { current_user: current_user })
end
end
...@@ -24,6 +24,7 @@ RSpec.describe GitlabSchema.types['User'] do ...@@ -24,6 +24,7 @@ RSpec.describe GitlabSchema.types['User'] do
authoredMergeRequests authoredMergeRequests
assignedMergeRequests assignedMergeRequests
groupMemberships groupMemberships
groupCount
projectMemberships projectMemberships
starredProjects starredProjects
] ]
......
...@@ -308,8 +308,10 @@ RSpec.describe Group do ...@@ -308,8 +308,10 @@ RSpec.describe Group do
end end
describe 'scopes' do describe 'scopes' do
let!(:private_group) { create(:group, :private) } let_it_be(:private_group) { create(:group, :private) }
let!(:internal_group) { create(:group, :internal) } let_it_be(:internal_group) { create(:group, :internal) }
let_it_be(:user1) { create(:user) }
let_it_be(:user2) { create(:user) }
describe 'public_only' do describe 'public_only' do
subject { described_class.public_only.to_a } subject { described_class.public_only.to_a }
...@@ -328,6 +330,27 @@ RSpec.describe Group do ...@@ -328,6 +330,27 @@ RSpec.describe Group do
it { is_expected.to match_array([private_group, internal_group]) } it { is_expected.to match_array([private_group, internal_group]) }
end end
describe 'for_authorized_group_members' do
let_it_be(:group_member1) { create(:group_member, source: private_group, user_id: user1.id, access_level: Gitlab::Access::OWNER) }
it do
result = described_class.for_authorized_group_members([user1.id, user2.id])
expect(result).to match_array([private_group])
end
end
describe 'for_authorized_project_members' do
let_it_be(:project) { create(:project, group: internal_group) }
let_it_be(:project_member1) { create(:project_member, source: project, user_id: user1.id, access_level: Gitlab::Access::DEVELOPER) }
it do
result = described_class.for_authorized_project_members([user1.id, user2.id])
expect(result).to match_array([internal_group])
end
end
end end
describe '#to_reference' do describe '#to_reference' do
......
...@@ -102,4 +102,22 @@ RSpec.describe UserPolicy do ...@@ -102,4 +102,22 @@ RSpec.describe UserPolicy do
end end
end end
end end
describe "reading a user's group count" do
context "when current_user is an admin", :enable_admin_mode do
let(:current_user) { create(:user, :admin) }
it { is_expected.to be_allowed(:read_group_count) }
end
context "for self users" do
let(:user) { current_user }
it { is_expected.to be_allowed(:read_group_count) }
end
context "when accessing a different user's group count" do
it { is_expected.not_to be_allowed(:read_group_count) }
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