Commit 019f8c6d authored by Jacopo's avatar Jacopo

Adds API endpoint `/api/v4/project/:id/members/all` and `/api/v4/project/:id/members/all`

The enpoint returns all members including inherited members through
ancestor groups.
parent 4bbfe531
---
title: Adds API endpoint /api/v4/(project/group)/:id/members/all to list also inherited
members
merge_request: 19748
author: Jacopo Beschi @jacopo-beschi
type: added
......@@ -54,6 +54,56 @@ Example response:
]
```
## List all members of a group or project including inherited members
Gets a list of group or project members viewable by the authenticated user, including inherited members through ancestor groups.
```
GET /groups/:id/members/all
GET /projects/:id/members/all
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project or group](README.md#namespaced-path-encoding) owned by the authenticated user |
| `query` | string | no | A query string to search for members |
```bash
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/:id/members/all
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/:id/members/all
```
Example response:
```json
[
{
"id": 1,
"username": "raymond_smith",
"name": "Raymond Smith",
"state": "active",
"created_at": "2012-10-22T14:13:35Z",
"access_level": 30
},
{
"id": 2,
"username": "john_doe",
"name": "John Doe",
"state": "active",
"created_at": "2012-10-22T14:13:35Z",
"access_level": 30
},
{
"id": 3,
"username": "foo_bar",
"name": "Foo bar",
"state": "active",
"created_at": "2012-11-22T14:13:35Z",
"access_level": 30
}
]
```
## Get a member of a group or project
Gets a member of a group or project.
......
......@@ -10,6 +10,10 @@ module API
def authorize_admin_source!(source_type, source)
authorize! :"admin_#{source_type}", source
end
def members_finder(source_type, source, current_user)
source_type == 'project' ? MembersFinder.new(source, current_user) : GroupMembersFinder.new(source)
end
end
end
end
......@@ -28,6 +28,23 @@ module API
present members, with: Entities::Member
end
desc 'Gets a list of group or project members viewable by the authenticated user, including those who gained membership through ancestor group.' do
success Entities::Member
end
params do
optional :query, type: String, desc: 'A query string to search for members'
use :pagination
end
get ":id/members/all" do
source = find_source(source_type, params[:id])
members = members_finder(source_type, source, current_user).execute.non_invite
members = members.joins(:user).merge(User.search(params[:query])) if params[:query].present?
members = paginate(members)
present members, with: Entities::Member
end
desc 'Gets a member of a group or project.' do
success Entities::Member
end
......
......@@ -22,10 +22,16 @@ describe API::Members do
end
end
shared_examples 'GET /:sources/:id/members' do |source_type|
shared_examples 'GET /:sources/:id/members/(all)' do |source_type, all|
let(:members_url) do
base_url = "/#{source_type.pluralize}/#{source.id}/members"
all ? base_url + '/all' : base_url
end
context "with :sources == #{source_type.pluralize}" do
it_behaves_like 'a 404 response when source is private' do
let(:route) { get api("/#{source_type.pluralize}/#{source.id}/members", stranger) }
let(:route) { get api(members_url, stranger) }
end
%i[maintainer developer access_requester stranger].each do |type|
......@@ -33,7 +39,7 @@ describe API::Members do
it 'returns 200' do
user = public_send(type)
get api("/#{source_type.pluralize}/#{source.id}/members", user)
get api(members_url, user)
expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
......@@ -46,23 +52,23 @@ describe API::Members do
it 'avoids N+1 queries' do
# Establish baseline
get api("/#{source_type.pluralize}/#{source.id}/members", maintainer)
get api(members_url, maintainer)
control = ActiveRecord::QueryRecorder.new do
get api("/#{source_type.pluralize}/#{source.id}/members", maintainer)
get api(members_url, maintainer)
end
project.add_developer(create(:user))
expect do
get api("/#{source_type.pluralize}/#{source.id}/members", maintainer)
get api(members_url, master)
end.not_to exceed_query_limit(control)
end
it 'does not return invitees' do
create(:"#{source_type}_member", invite_token: '123', invite_email: 'test@abc.com', source: source, user: nil)
get api("/#{source_type.pluralize}/#{source.id}/members", developer)
get api(members_url, developer)
expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
......@@ -72,7 +78,7 @@ describe API::Members do
end
it 'finds members with query string' do
get api("/#{source_type.pluralize}/#{source.id}/members", developer), query: maintainer.username
get api(members_url, developer), query: maintainer.username
expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
......@@ -82,7 +88,7 @@ describe API::Members do
end
it 'finds all members with no query specified' do
get api("/#{source_type.pluralize}/#{source.id}/members", developer), query: ''
get api(members_url, developer), query: ''
expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
......@@ -93,6 +99,43 @@ describe API::Members do
end
end
describe 'GET /:sources/:id/members/all', :nested_groups do
let(:nested_user) { create(:user) }
let(:project_user) { create(:user) }
let(:project) do
create(:project, :public, group: nested_group) do |project|
project.add_developer(project_user)
end
end
let(:nested_group) do
create(:group, :public, :access_requestable, parent: group) do |nested_group|
nested_group.add_developer(nested_user)
end
end
it 'finds all project members including inherited members' do
get api("/projects/#{project.id}/members/all", developer)
expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.count).to eq(4)
expect(json_response.map { |u| u['id'] }).to match_array [master.id, developer.id, nested_user.id, project_user.id]
end
it 'finds all group members including inherited members' do
get api("/groups/#{nested_group.id}/members/all", developer)
expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.count).to eq(3)
expect(json_response.map { |u| u['id'] }).to match_array [master.id, developer.id, nested_user.id]
end
end
shared_examples 'GET /:sources/:id/members/:user_id' do |source_type|
context "with :sources == #{source_type.pluralize}" do
it_behaves_like 'a 404 response when source is private' do
......@@ -323,12 +366,16 @@ describe API::Members do
end
end
it_behaves_like 'GET /:sources/:id/members', 'project' do
let(:source) { project }
[false, true].each do |all|
it_behaves_like 'GET /:sources/:id/members/(all)', 'project', all do
let(:source) { project }
end
end
it_behaves_like 'GET /:sources/:id/members', 'group' do
let(:source) { group }
[false, true].each do |all|
it_behaves_like 'GET /:sources/:id/members/(all)', 'group', all do
let(:source) { group }
end
end
it_behaves_like 'GET /:sources/:id/members/:user_id', 'project' 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