Commit 63807723 authored by Mark Chao's avatar Mark Chao

Add API to return all billable users of group

Only applicable to root group, and to group owner
parent 5fad87ba
......@@ -223,6 +223,58 @@ Example response:
}
```
## List all billable members of a group
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/217384) in GitLab 13.5.
Gets a list of group members who counts as billable, including members in the sub group/project.
This function takes [pagination](README.md#pagination) parameters `page` and `per_page` to restrict the list of users.
```plaintext
GET /groups/:id/billable_members
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user |
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/groups/:id/billable_members"
```
Example response:
```json
[
{
"id": 1,
"username": "raymond_smith",
"name": "Raymond Smith",
"state": "active",
"avatar_url": "https://www.gravatar.com/avatar/c2525a7f58ae3776070e44c106c48e15?s=80&d=identicon",
"web_url": "http://192.168.1.8:3000/root",
},
{
"id": 2,
"username": "john_doe",
"name": "John Doe",
"state": "active",
"avatar_url": "https://www.gravatar.com/avatar/c2525a7f58ae3776070e44c106c48e15?s=80&d=identicon",
"web_url": "http://192.168.1.8:3000/root",
"email": "john@example.com"
},
{
"id": 3,
"username": "foo_bar",
"name": "Foo bar",
"state": "active",
"avatar_url": "https://www.gravatar.com/avatar/c2525a7f58ae3776070e44c106c48e15?s=80&d=identicon",
"web_url": "http://192.168.1.8:3000/root"
}
]
```
## Add a member to a group or project
Adds a member to a group or project.
......
---
name: api_billable_member_list
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/43093
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/254947
group: group::fulfillment
type: development
default_enabled: false
......@@ -67,6 +67,16 @@ module EE
action: :create
).for_member(member).security_event
end
def paginate_billable_from_user_ids(user_ids)
paginated = paginate(::Kaminari.paginate_array(user_ids.sort))
users_as_hash = ::User.id_in(paginated).index_by(&:id)
# map! ensures same paginatable array is manipulated
# instead of creating a new non-paginatable array
paginated.map! { |user_id| users_as_hash[user_id] }
end
end
end
end
......
......@@ -41,6 +41,25 @@ module EE
present_member(updated_member)
end
desc 'Gets a list of billable users of root group.' do
success Entities::Member
end
params do
use :pagination
end
get ":id/billable_members" do
group = find_group!(params[:id])
not_found! unless ::Feature.enabled?(:api_billable_member_list, group)
bad_request!(nil) if group.subgroup?
bad_request!(nil) unless ::Ability.allowed?(current_user, :admin_group_member, group)
users = paginate_billable_from_user_ids(group.billed_user_ids)
present users, with: ::API::Entities::UserBasic, current_user: current_user
end
end
end
end
......
......@@ -11,7 +11,7 @@ RSpec.describe EE::API::Helpers::MembersHelpers do
shared_examples 'creates security_event' do |source_type|
context "with :source_type == #{source_type.pluralize}" do
it 'creates security_event' do
security_event = subject.log_audit_event(member)
security_event = members_helpers.log_audit_event(member)
expect(security_event.entity_id).to eq(source.id)
expect(security_event.entity_type).to eq(source_type.capitalize)
......@@ -31,4 +31,38 @@ RSpec.describe EE::API::Helpers::MembersHelpers do
let(:member) { create(:project_member, project: source, user: create(:user)) }
end
end
describe '#paginate_billable_from_user_ids' do
subject(:members_helpers) { Class.new.include(described_class, API::Helpers::Pagination).new }
let_it_be(:users) { create_list(:user, 3) }
let(:user_ids) { users.map(&:id) }
let(:page) { 1 }
let(:per_page) { 2 }
before do
allow(members_helpers).to receive(:params).and_return({ page: page, per_page: per_page })
allow(members_helpers).to receive(:header) { }
allow(members_helpers).to receive(:request).and_return(double(:request, url: ''))
end
it 'returns paginated User array in asc order' do
results = members_helpers.paginate_billable_from_user_ids(user_ids.reverse)
expect(results).to all be_a(User)
expect(results.size).to eq(per_page)
expect(results.map { |result| result.id }).to eq(user_ids.first(2))
end
context 'when page is 2' do
let(:page) { 2 }
it 'returns User as paginated array' do
results = members_helpers.paginate_billable_from_user_ids(user_ids.reverse)
expect(results.size).to eq(1)
expect(results.map { |result| result.id }).to contain_exactly(user_ids.last)
end
end
end
end
......@@ -218,6 +218,113 @@ RSpec.describe API::Members do
end
end
describe "GET /groups/:id/billable_members" do
let_it_be(:owner) { create(:user) }
let_it_be(:maintainer) { create(:user) }
let_it_be(:group) do
create(:group) do |group|
group.add_owner(owner)
group.add_maintainer(maintainer)
end
end
let_it_be(:nested_user) { create(:user) }
let_it_be(:nested_group) do
create(:group, parent: group) do |nested_group|
nested_group.add_developer(nested_user)
end
end
let(:url) { "/groups/#{group.id}/billable_members" }
subject do
get api(url, owner)
json_response
end
context 'with sub group and projects' do
let!(:project_user) { create(:user) }
let!(:project) do
create(:project, :public, group: nested_group) do |project|
project.add_developer(project_user)
end
end
let!(:linked_group_user) { create(:user) }
let!(:linked_group) do
create(:group) do |linked_group|
linked_group.add_developer(linked_group_user)
end
end
let!(:project_group_link) { create(:project_group_link, project: project, group: linked_group) }
it 'returns paginated billable users' do
subject
expect_paginated_array_response(*[owner, maintainer, nested_user, project_user, linked_group_user].map(&:id))
end
end
context 'when feature is disabled' do
before do
stub_feature_flags(api_billable_member_list: false)
end
it 'returns error' do
subject
expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'with non owner' do
it 'returns error' do
get api(url, maintainer)
expect(response).to have_gitlab_http_status(:bad_request)
end
end
context 'when group can not be found' do
let(:url) { "/groups/foo/billable_members" }
it 'returns error' do
get api(url, owner)
expect(response).to have_gitlab_http_status(:not_found)
expect(json_response['message']).to eq('404 Group Not Found')
end
end
context 'with non-root group' do
let(:child_group) { create :group, parent: group }
let(:url) { "/groups/#{child_group.id}/billable_members" }
it 'returns error' do
subject
expect(response).to have_gitlab_http_status(:bad_request)
end
end
context 'email' do
before do
group.add_owner(owner)
end
include_context "group managed account with group members"
it_behaves_like 'members response with exposed emails' do
let(:emails) { gma_member.email }
end
it_behaves_like 'members response with hidden emails' do
let(:emails) { member.email }
end
end
end
context 'without LDAP' do
let(:group) { create(:group) }
let(:owner) { create(:user) }
......
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