Commit e1ef201b authored by Bob Van Landuyt's avatar Bob Van Landuyt

Merge branch '249589-group-count' into 'master'

API for user groups count

See merge request gitlab-org/gitlab!44069
parents cb9a7736 26cf7513
# 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
field :group_memberships, Types::GroupMemberType.connection_type, null: true,
description: 'Group memberships of the user',
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,
description: 'User status'
field :project_memberships, Types::ProjectMemberType.connection_type, null: true,
......
......@@ -98,6 +98,17 @@ class Group < Namespace
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
def sort_by_attribute(method)
if method == 'storage_size_desc'
......
......@@ -21,6 +21,7 @@ class UserPolicy < BasePolicy
enable :update_user
enable :update_user_status
enable :read_user_personal_access_tokens
enable :read_group_count
end
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 {
"""
email: String
"""
Group count for the user. Available only when feature flag `user_group_counts` is enabled
"""
groupCount: Int
"""
Group memberships of the user
"""
......
......@@ -62400,6 +62400,20 @@
"isDeprecated": false,
"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",
"description": "Group memberships of the user",
......@@ -3041,6 +3041,7 @@ Autogenerated return type of UpdateSnippet.
| ----- | ---- | ----------- |
| `avatarUrl` | String | URL of the user's avatar |
| `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 |
| `name` | String! | Human-readable name 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
authoredMergeRequests
assignedMergeRequests
groupMemberships
groupCount
projectMemberships
starredProjects
]
......
......@@ -308,8 +308,10 @@ RSpec.describe Group do
end
describe 'scopes' do
let!(:private_group) { create(:group, :private) }
let!(:internal_group) { create(:group, :internal) }
let_it_be(:private_group) { create(:group, :private) }
let_it_be(:internal_group) { create(:group, :internal) }
let_it_be(:user1) { create(:user) }
let_it_be(:user2) { create(:user) }
describe 'public_only' do
subject { described_class.public_only.to_a }
......@@ -328,6 +330,27 @@ RSpec.describe Group do
it { is_expected.to match_array([private_group, internal_group]) }
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
describe '#to_reference' do
......
......@@ -102,4 +102,22 @@ RSpec.describe UserPolicy do
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
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