Commit 5984ed07 authored by Hordur Freyr Yngvason's avatar Hordur Freyr Yngvason Committed by Achilleas Pipinellis

Add individual inherited member lookup API

Adds the endpoints

    /(groups|projects)/:id/members/all/:user_id

for finding individual members, including inherited membership,
complementing the existing endpoints

    /(groups|projects)/:id/members
    /(groups|projects)/:id/members/all
    /(groups|projects)/:id/members/:user_id

which list direct members, list all members (including inherited), and
find individual direct members, respectively.
parent b370adbe
---
title: Add individual inherited member lookup API
merge_request: 17744
author:
type: added
......@@ -26,6 +26,7 @@ GET /projects/:id/members
| --------- | ---- | -------- | ----------- |
| `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 |
| `user_ids` | array of integers | no | Filter the results on the given user IDs |
```bash
curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/:id/members
......@@ -62,9 +63,8 @@ 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.
When a user is a member of the project/group and of one or more ancestor groups the user is returned only once with the project access_level (if exists)
or the access_level for the user in the first group which he belongs to in the project groups ancestors chain.
**Note:** We plan to [change](https://gitlab.com/gitlab-org/gitlab-foss/issues/62284) this behavior to return highest access_level instead.
When a user is a member of the project/group and of one or more ancestor groups the user is returned only once with the project `access_level` (if exists)
or the `access_level` for the user in the first group which he belongs to in the project groups ancestors chain.
```
GET /groups/:id/members/all
......@@ -75,6 +75,7 @@ GET /projects/:id/members/all
| --------- | ---- | -------- | ----------- |
| `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 |
| `user_ids` | array of integers | no | Filter the results on the given user IDs |
```bash
curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/:id/members/all
......@@ -120,7 +121,7 @@ Example response:
## Get a member of a group or project
Gets a member of a group or project.
Gets a member of a group or project. Returns only direct members and not inherited members through ancestor groups.
```
GET /groups/:id/members/:user_id
......@@ -152,6 +153,42 @@ Example response:
}
```
## Get a member of a group or project, including inherited members
> [Introduced](https://gitlab.com/gitlab-org/gitlab/merge_requests/17744) in GitLab 12.4.
Gets a member of a group or project, including members inherited through ancestor groups. See the corresponding [endpoint to list all inherited members](#list-all-members-of-a-group-or-project-including-inherited-members) for details.
```
GET /groups/:id/members/all/:user_id
GET /projects/:id/members/all/:user_id
```
| 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 |
| `user_id` | integer | yes | The user ID of the member |
```bash
curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/:id/members/all/:user_id
curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/:id/members/all/:user_id
```
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",
"access_level": 30,
"expires_at": null
}
```
## Add a member to a group or project
Adds a member to a group or project.
......
......@@ -18,6 +18,7 @@ module API
end
params do
optional :query, type: String, desc: 'A query string to search for members'
optional :user_ids, type: Array[Integer], desc: 'Array of user ids to look up for membership'
use :pagination
end
# rubocop: disable CodeReuse/ActiveRecord
......@@ -26,6 +27,7 @@ module API
members = source.members.where.not(user_id: nil).includes(:user)
members = members.joins(:user).merge(User.search(params[:query])) if params[:query].present?
members = members.where(user_id: params[:user_ids]) if params[:user_ids].present?
members = paginate(members)
present members, with: Entities::Member
......@@ -37,6 +39,7 @@ module API
end
params do
optional :query, type: String, desc: 'A query string to search for members'
optional :user_ids, type: Array[Integer], desc: 'Array of user ids to look up for membership'
use :pagination
end
# rubocop: disable CodeReuse/ActiveRecord
......@@ -45,6 +48,7 @@ module API
members = find_all_members(source_type, source)
members = members.includes(:user).references(:user).merge(User.search(params[:query])) if params[:query].present?
members = members.where(user_id: params[:user_ids]) if params[:user_ids].present?
members = paginate(members)
present members, with: Entities::Member
......@@ -68,6 +72,23 @@ module API
end
# rubocop: enable CodeReuse/ActiveRecord
desc 'Gets a member of a group or project, including those who gained membership through ancestor group' do
success Entities::Member
end
params do
requires :user_id, type: Integer, desc: 'The user ID of the member'
end
# rubocop: disable CodeReuse/ActiveRecord
get ":id/members/all/:user_id" do
source = find_source(source_type, params[:id])
members = find_all_members(source_type, source)
member = members.find_by!(user_id: params[:user_id])
present member, with: Entities::Member
end
# rubocop: enable CodeReuse/ActiveRecord
desc 'Adds a member to a group or project.' do
success Entities::Member
end
......
......@@ -87,6 +87,15 @@ describe API::Members do
expect(json_response.first['username']).to eq(maintainer.username)
end
it 'finds members with the given user_ids' do
get api(members_url, developer), params: { user_ids: [maintainer.id, developer.id, stranger.id] }
expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.map { |u| u['id'] }).to contain_exactly(maintainer.id, developer.id)
end
it 'finds all members with no query specified' do
get api(members_url, developer), params: { query: '' }
......@@ -155,10 +164,10 @@ describe API::Members do
end
end
shared_examples 'GET /:source_type/:id/members/:user_id' do |source_type|
context "with :source_type == #{source_type.pluralize}" do
shared_examples 'GET /:source_type/:id/members/(all/):user_id' do |source_type, all|
context "with :source_type == #{source_type.pluralize} and all == #{all}" do
it_behaves_like 'a 404 response when source is private' do
let(:route) { get api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", stranger) }
let(:route) { get api("/#{source_type.pluralize}/#{source.id}/members/#{all ? 'all/' : ''}#{developer.id}", stranger) }
end
context 'when authenticated as a non-member' do
......@@ -166,7 +175,7 @@ describe API::Members do
context "as a #{type}" do
it 'returns 200' do
user = public_send(type)
get api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", user)
get api("/#{source_type.pluralize}/#{source.id}/members/#{all ? 'all/' : ''}#{developer.id}", user)
expect(response).to have_gitlab_http_status(200)
# User attributes
......@@ -434,12 +443,14 @@ describe API::Members do
end
end
it_behaves_like 'GET /:source_type/:id/members/:user_id', 'project' do
let(:source) { project }
end
[false, true].each do |all|
it_behaves_like 'GET /:source_type/:id/members/(all/):user_id', 'project', all do
let(:source) { all ? create(:project, :public, group: group) : project }
end
it_behaves_like 'GET /:source_type/:id/members/:user_id', 'group' do
let(:source) { group }
it_behaves_like 'GET /:source_type/:id/members/(all/):user_id', 'group', all do
let(:source) { all ? create(:group, parent: group) : group }
end
end
it_behaves_like 'POST /:source_type/:id/members', '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