Commit 61a5c7c2 authored by Andy Soiron's avatar Andy Soiron Committed by Heinrich Lee Yu

New API endpoint user/memberships

This commits adds a new endpoint to the REST API.
It can be used only by admins to retrieve all projects
and groups where the given user is a member.
It also returns the access level.
parent 9a2dafa4
......@@ -82,6 +82,7 @@ class Member < ApplicationRecord
scope :with_user, -> (user) { where(user: user) }
scope :with_source_id, ->(source_id) { where(source_id: source_id) }
scope :including_source, -> { includes(:source) }
scope :order_name_asc, -> { left_join_users.reorder(Gitlab::Database.nulls_last_order('users.name', 'ASC')) }
scope :order_name_desc, -> { left_join_users.reorder(Gitlab::Database.nulls_last_order('users.name', 'DESC')) }
......
---
title: Add users memberships endpoints for admins
merge_request: 22518
author:
type: added
......@@ -2,7 +2,7 @@
>**Note:** This feature was introduced in GitLab 8.11
**Valid access levels**
## Valid access levels
The access levels are defined in the `Gitlab::Access` module. Currently, these levels are recognized:
......
......@@ -1405,3 +1405,53 @@ Example response:
```
Please note that `last_activity_at` is deprecated, please use `last_activity_on`.
## User memberships (admin only)
> [Introduced](https://gitlab.com/gitlab-org/gitlab/merge_requests/22518) in GitLab 12.8.
Lists all projects and groups a user is a member of. This endpoint is available for admins only.
It returns the `source_id`, `source_name`, `source_type` and `access_level` of a membership.
Source can be of type `Namespace` (representing a group) or `Project`. The response represents only direct memberships. Inherited memberships, for example in subgroups, will not be included.
Access levels will be represented by an integer value. Read more about the meaning of access level values [here](access_requests.md#valid-access-levels).
```
GET /users/:id/memberships
```
Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer | yes | The ID of a specified user |
| `type` | string | no | Filter memberships by type. Can be either `Project` or `Namespace` |
Returns:
- `200 OK` on success.
- `404 User Not Found` if user cannot be found.
- `403 Forbidden` when not requested by an admin.
- `400 Bad Request` when requested type is not supported.
```bash
curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/users/<user_id>/memberships
```
Example response:
```json
[
{
"source_id": 1,
"source_name": "Project one",
"source_type": "Project",
"access_level": "20"
},
{
"source_id": 3,
"source_name": "Group three",
"source_type": "Namespace",
"access_level": "20"
},
]
```
......@@ -2,6 +2,15 @@
module API
module Entities
class Membership < Grape::Entity
expose :source_id
expose :source_name do |member|
member.source.name
end
expose :source_type
expose :access_level
end
class BlameRangeCommit < Grape::Entity
expose :id
expose :parent_ids
......
......@@ -533,6 +533,32 @@ module API
end
# rubocop: enable CodeReuse/ActiveRecord
desc 'Get memberships' do
success Entities::Membership
end
params do
requires :user_id, type: Integer, desc: 'The ID of the user'
optional :type, type: String, values: %w[Project Namespace]
use :pagination
end
get ":user_id/memberships" do
authenticated_as_admin!
user = find_user_by_id(params)
members = case params[:type]
when 'Project'
user.project_members
when 'Namespace'
user.group_members
else
user.members
end
members = members.including_source
present paginate(members), with: Entities::Membership
end
params do
requires :user_id, type: Integer, desc: 'The ID of the user'
end
......
{
"type": "object",
"properties" : {
"source_id": { "type": "integer" },
"source_name": { "type": "string" },
"source_type": { "type": "string" },
"access_level": { "type": "integer" }
},
"additionalProperties": false
}
{
"type": "array",
"items": { "$ref": "membership.json" }
}
......@@ -2100,6 +2100,83 @@ describe API::Users do
end
end
describe "GET /users/:id/memberships" do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project) }
let_it_be(:group) { create(:group) }
let(:requesting_user) { create(:user) }
before_all do
project.add_guest(user)
group.add_guest(user)
end
it "responses with 403" do
get api("/users/#{user.id}/memberships", requesting_user)
expect(response).to have_gitlab_http_status(:forbidden)
end
context 'requested by admin user' do
let(:requesting_user) { create(:user, :admin) }
it "responses successfully" do
get api("/users/#{user.id}/memberships", requesting_user)
aggregate_failures 'expect successful response including groups and projects' do
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('public_api/v4/memberships')
expect(response).to include_pagination_headers
expect(json_response).to contain_exactly(
a_hash_including('source_type' => 'Project'),
a_hash_including('source_type' => 'Namespace')
)
end
end
it 'does not submit N+1 DB queries' do
# Avoid setup queries
get api("/users/#{user.id}/memberships", requesting_user)
control = ActiveRecord::QueryRecorder.new do
get api("/users/#{user.id}/memberships", requesting_user)
end
create_list(:project, 5).map { |project| project.add_guest(user) }
expect do
get api("/users/#{user.id}/memberships", requesting_user)
end.not_to exceed_query_limit(control)
end
context 'with type filter' do
it "only returns project memberships" do
get api("/users/#{user.id}/memberships?type=Project", requesting_user)
aggregate_failures do
expect(json_response).to contain_exactly(a_hash_including('source_type' => 'Project'))
expect(json_response).not_to include(a_hash_including('source_type' => 'Namespace'))
end
end
it "only returns group memberships" do
get api("/users/#{user.id}/memberships?type=Namespace", requesting_user)
aggregate_failures do
expect(json_response).to contain_exactly(a_hash_including('source_type' => 'Namespace'))
expect(json_response).not_to include(a_hash_including('source_type' => 'Project'))
end
end
it "recognizes unsupported types" do
get api("/users/#{user.id}/memberships?type=foo", requesting_user)
expect(response).to have_gitlab_http_status(:bad_request)
end
end
end
end
context "user activities", :clean_gitlab_redis_shared_state do
let!(:old_active_user) { create(:user, last_activity_on: Time.utc(2000, 1, 1)) }
let!(:newly_active_user) { create(:user, last_activity_on: 2.days.ago.midday) }
......
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