From eb12a4a5da035ab1fe0917156f37f9006c690536 Mon Sep 17 00:00:00 2001 From: Hordur Freyr Yngvason Date: Tue, 24 Sep 2019 15:06:41 +0200 Subject: [PATCH 1/3] 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. --- ...w-api-lookup-of-inherited-member-by-id.yml | 5 +++ doc/api/members.md | 43 +++++++++++++++++-- lib/api/members.rb | 21 +++++++++ spec/requests/api/members_spec.rb | 29 +++++++++---- 4 files changed, 85 insertions(+), 13 deletions(-) create mode 100644 changelogs/unreleased/allow-api-lookup-of-inherited-member-by-id.yml diff --git a/changelogs/unreleased/allow-api-lookup-of-inherited-member-by-id.yml b/changelogs/unreleased/allow-api-lookup-of-inherited-member-by-id.yml new file mode 100644 index 00000000000000..f266d197c6c8ab --- /dev/null +++ b/changelogs/unreleased/allow-api-lookup-of-inherited-member-by-id.yml @@ -0,0 +1,5 @@ +--- +title: Add individual inherited member lookup API +merge_request: 17744 +author: +type: added diff --git a/doc/api/members.md b/doc/api/members.md index da62dc53659093..4d89e774eeba02 100644 --- a/doc/api/members.md +++ b/doc/api/members.md @@ -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: " 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: " 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,40 @@ Example response: } ``` +## Get a member of a group or project, including inherited members + +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: " https://gitlab.example.com/api/v4/groups/:id/members/all/:user_id +curl --header "PRIVATE-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. diff --git a/lib/api/members.rb b/lib/api/members.rb index 461ffe71a621b9..1d4616fed52fa2 100644 --- a/lib/api/members.rb +++ b/lib/api/members.rb @@ -18,6 +18,7 @@ class Members < Grape::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 @@ class Members < Grape::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 @@ class Members < Grape::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 @@ class Members < Grape::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 @@ class Members < Grape::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 diff --git a/spec/requests/api/members_spec.rb b/spec/requests/api/members_spec.rb index 26f6e7055287f5..9f0a8359fe8002 100644 --- a/spec/requests/api/members_spec.rb +++ b/spec/requests/api/members_spec.rb @@ -87,6 +87,15 @@ 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 @@ 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 @@ 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 @@ 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 -- GitLab From 2834ec469ef67839c3e41961e01ea9a70ee6d322 Mon Sep 17 00:00:00 2001 From: Hordur Freyr Yngvason Date: Wed, 16 Oct 2019 13:21:09 +0000 Subject: [PATCH 2/3] Apply suggestion to doc/api/members.md --- doc/api/members.md | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/api/members.md b/doc/api/members.md index 4d89e774eeba02..66a3c7d961c361 100644 --- a/doc/api/members.md +++ b/doc/api/members.md @@ -155,6 +155,7 @@ 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. ``` -- GitLab From d7fef38c5a6f0f8d11adfad1b34d9807962b157f Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Wed, 16 Oct 2019 16:28:37 +0000 Subject: [PATCH 3/3] Apply suggestion to doc/api/members.md --- doc/api/members.md | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/api/members.md b/doc/api/members.md index 66a3c7d961c361..50dcf86c972738 100644 --- a/doc/api/members.md +++ b/doc/api/members.md @@ -156,6 +156,7 @@ 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. ``` -- GitLab