Commit c67d0826 authored by Bob Van Landuyt's avatar Bob Van Landuyt

Merge branch '24163-add-admin-filter-to-user-api' into 'master'

Add ability to filter by admins in the /users API endpoint

See merge request gitlab-org/gitlab!46244
parents 640c5e42 7516148b
......@@ -34,6 +34,7 @@ class UsersFinder
users = User.all.order_id_desc
users = by_username(users)
users = by_id(users)
users = by_admins(users)
users = by_search(users)
users = by_blocked(users)
users = by_active(users)
......@@ -62,6 +63,12 @@ class UsersFinder
users.id_in(params[:id])
end
def by_admins(users)
return users unless params[:admins] && current_user&.can_read_all_resources?
users.admins
end
def by_search(users)
return users unless params[:search].present?
......
......@@ -23,10 +23,15 @@ module Resolvers
required: false,
description: "Query to search users by name, username, or primary email."
def resolve(ids: nil, usernames: nil, sort: nil, search: nil)
argument :admins, GraphQL::BOOLEAN_TYPE,
required: false,
default_value: false,
description: 'Return only admin users.'
def resolve(ids: nil, usernames: nil, sort: nil, search: nil, admins: nil)
authorize!
::UsersFinder.new(context[:current_user], finder_params(ids, usernames, sort, search)).execute
::UsersFinder.new(context[:current_user], finder_params(ids, usernames, sort, search, admins)).execute
end
def ready?(**args)
......@@ -34,7 +39,7 @@ module Resolvers
return super if args.values.compact.blank?
if args.values.all?
if args[:usernames]&.present? && args[:ids]&.present?
raise Gitlab::Graphql::Errors::ArgumentError, 'Provide either a list of usernames or ids'
end
......@@ -47,12 +52,13 @@ module Resolvers
private
def finder_params(ids, usernames, sort, search)
def finder_params(ids, usernames, sort, search, admins)
params = {}
params[:sort] = sort if sort
params[:username] = usernames if usernames
params[:id] = parse_gids(ids) if ids
params[:search] = search if search
params[:admins] = admins if admins
params
end
......
---
title: Add ability to get admins via REST and GraphQL API
merge_request: 46244
author:
type: added
......@@ -20366,6 +20366,11 @@ type Query {
Find users
"""
users(
"""
Return only admin users.
"""
admins: Boolean = false
"""
Returns the elements in the list that come after the specified cursor.
"""
......
......@@ -59244,6 +59244,16 @@
},
"defaultValue": null
},
{
"name": "admins",
"description": "Return only admin users.",
"type": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
},
"defaultValue": "false"
},
{
"name": "after",
"description": "Returns the elements in the list that come after the specified cursor.",
---
stage: none
group: unassigned
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
# Query users with GraphQL
This page describes how you can use the GraphiQL explorer to query users.
You can run the same query directly via a HTTP endpoint, using `cURL`. For more information, see our
guidance on getting started from the [command line](getting_started.md#command-line).
The [example users query](#set-up-the-graphiql-explorer) looks for a subset of users in
o
a GitLab instance either by username or
[Global ID](../../development/api_graphql_styleguide.md#global-ids).
The query includes:
- [`pageInfo`](#pageinfo)
- [`nodes`](#nodes)
## pageInfo
This contains the data needed to implement pagination. GitLab uses cursor-based
[pagination](getting_started.md#pagination). For more information, see
[Pagination](https://graphql.org/learn/pagination/) in the GraphQL documentation.
## nodes
In a GraphQL query, `nodes` is used to represent a collection of [`nodes` on a graph](https://en.wikipedia.org/wiki/Vertex_(graph_theory)).
In this case, the collection of nodes is a collection of `User` objects. For each one,
we output:
- Their user's `id`.
- The `membership` fragment, which represents a Project or Group membership belonging
to that user. Outputting a fragment is denoted with the `...memberships` notation.
The GitLab GraphQL API is extensive and a large amount of data for a wide variety of entities can be output.
See the official [reference documentation](reference/index.md) for the most up-to-date information.
## Set up the GraphiQL explorer
This procedure presents a substantive example that you can copy and paste into GraphiQL
explorer. GraphiQL explorer is available for:
- GitLab.com users at [https://gitlab.com/-/graphql-explorer](https://gitlab.com/-/graphql-explorer).
- Self-managed users at `https://gitlab.example.com/-/graphql-explorer`.
1. Copy the following code excerpt:
```graphql
{
users(usernames: ["user1", "user3", "user4"]) {
pageInfo {
endCursor
startCursor
hasNextPage
}
nodes {
id
username,
publicEmail
location
webUrl
userPermissions {
createSnippet
}
}
}
}
```
1. Open the [GraphiQL explorer tool](https://gitlab.com/-/graphql-explorer).
1. Paste the `query` listed above into the left window of your GraphiQL explorer tool.
1. Click Play to get the result shown here:
![GraphiQL explorer search for boards](img/users_query_example_v13_8.png)
NOTE:
[The GraphQL API returns a GlobalID, rather than a standard ID.](getting_started.md#queries-and-mutations) It also expects a GlobalID as an input rather than
a single integer.
This GraphQL query returns the specified information for the three users with the listed username. Since the GraphiQL explorer uses the session token to authorize access to resources,
the output is limited to the projects and groups accessible to the currently signed-in user.
If you've signed in as an instance administrator, you would have access to all records, regardless of ownership.
If you are signed in as an administrator, you can show just the matching administrators on the instance by adding the `admins: true` parameter to the query changing the second line to:
```graphql
users(usernames: ["user1", "user3", "user4"], admins: true) {
...
}
```
Or you can just get all of the administrators:
```graphql
users(admins: true) {
...
}
```
For more information on:
- GraphQL specific entities, such as Fragments and Interfaces, see the official
[GraphQL documentation](https://graphql.org/learn/).
- Individual attributes, see the [GraphQL API Resources](reference/index.md).
......@@ -89,6 +89,7 @@ GET /users
| `sort` | string | no | Return users sorted in `asc` or `desc` order. Default is `desc` |
| `two_factor` | string | no | Filter users by Two-factor authentication. Filter values are `enabled` or `disabled`. By default it returns all users |
| `without_projects` | boolean | no | Filter users without projects. Default is `false` |
| `admins` | boolean | no | Return only admin users. Default is `false` |
```json
[
......
......@@ -13,13 +13,13 @@ RSpec.describe UsersFinder do
it 'returns ldap users by default' do
users = described_class.new(normal_user).execute
expect(users).to contain_exactly(normal_user, blocked_user, omniauth_user, ldap_user, internal_user)
expect(users).to contain_exactly(normal_user, blocked_user, omniauth_user, ldap_user, internal_user, admin_user)
end
it 'returns only non-ldap users with skip_ldap: true' do
users = described_class.new(normal_user, skip_ldap: true).execute
expect(users).to contain_exactly(normal_user, blocked_user, omniauth_user, internal_user)
expect(users).to contain_exactly(normal_user, blocked_user, omniauth_user, internal_user, admin_user)
end
end
end
......
......@@ -87,6 +87,7 @@ module API
optional :created_before, type: DateTime, desc: 'Return users created before the specified time'
optional :without_projects, type: Boolean, default: false, desc: 'Filters only users without projects'
optional :exclude_internal, as: :non_internal, type: Boolean, default: false, desc: 'Filters only non internal users'
optional :admins, type: Boolean, default: false, desc: 'Filters only admin users'
all_or_none_of :extern_uid, :provider
use :sort_params
......
......@@ -12,7 +12,7 @@ RSpec.describe UsersFinder do
it 'returns all users' do
users = described_class.new(user).execute
expect(users).to contain_exactly(user, normal_user, blocked_user, omniauth_user, internal_user)
expect(users).to contain_exactly(user, normal_user, blocked_user, omniauth_user, internal_user, admin_user)
end
it 'filters by username' do
......@@ -48,13 +48,13 @@ RSpec.describe UsersFinder do
it 'filters by active users' do
users = described_class.new(user, active: true).execute
expect(users).to contain_exactly(user, normal_user, omniauth_user)
expect(users).to contain_exactly(user, normal_user, omniauth_user, admin_user)
end
it 'returns no external users' do
users = described_class.new(user, external: true).execute
expect(users).to contain_exactly(user, normal_user, blocked_user, omniauth_user, internal_user)
expect(users).to contain_exactly(user, normal_user, blocked_user, omniauth_user, internal_user, admin_user)
end
it 'filters by created_at' do
......@@ -71,7 +71,7 @@ RSpec.describe UsersFinder do
it 'filters by non internal users' do
users = described_class.new(user, non_internal: true).execute
expect(users).to contain_exactly(user, normal_user, blocked_user, omniauth_user)
expect(users).to contain_exactly(user, normal_user, blocked_user, omniauth_user, admin_user)
end
it 'does not filter by custom attributes' do
......@@ -80,13 +80,18 @@ RSpec.describe UsersFinder do
custom_attributes: { foo: 'bar' }
).execute
expect(users).to contain_exactly(user, normal_user, blocked_user, omniauth_user, internal_user)
expect(users).to contain_exactly(user, normal_user, blocked_user, omniauth_user, internal_user, admin_user)
end
it 'orders returned results' do
users = described_class.new(user, sort: 'id_asc').execute
expect(users).to eq([normal_user, blocked_user, omniauth_user, internal_user, user])
expect(users).to eq([normal_user, admin_user, blocked_user, omniauth_user, internal_user, user])
end
it 'does not filter by admins' do
users = described_class.new(user, admins: true).execute
expect(users).to contain_exactly(user, normal_user, admin_user, blocked_user, omniauth_user, internal_user)
end
end
......@@ -102,7 +107,13 @@ RSpec.describe UsersFinder do
it 'returns all users' do
users = described_class.new(admin).execute
expect(users).to contain_exactly(admin, normal_user, blocked_user, external_user, omniauth_user, internal_user)
expect(users).to contain_exactly(admin, normal_user, blocked_user, external_user, omniauth_user, internal_user, admin_user)
end
it 'returns only admins' do
users = described_class.new(admin, admins: true).execute
expect(users).to contain_exactly(admin, admin_user)
end
it 'filters by custom attributes' do
......
......@@ -27,7 +27,7 @@ RSpec.describe Resolvers::UsersResolver do
context 'when both ids and usernames are passed ' do
it 'raises an error' do
expect { resolve_users(ids: [user1.to_global_id.to_s], usernames: [user1.username]) }
expect { resolve_users( args: { ids: [user1.to_global_id.to_s], usernames: [user1.username] } ) }
.to raise_error(Gitlab::Graphql::Errors::ArgumentError)
end
end
......@@ -35,7 +35,7 @@ RSpec.describe Resolvers::UsersResolver do
context 'when a set of IDs is passed' do
it 'returns those users' do
expect(
resolve_users(ids: [user1.to_global_id.to_s, user2.to_global_id.to_s])
resolve_users( args: { ids: [user1.to_global_id.to_s, user2.to_global_id.to_s] } )
).to contain_exactly(user1, user2)
end
end
......@@ -43,21 +43,31 @@ RSpec.describe Resolvers::UsersResolver do
context 'when a set of usernames is passed' do
it 'returns those users' do
expect(
resolve_users(usernames: [user1.username, user2.username])
resolve_users( args: { usernames: [user1.username, user2.username] } )
).to contain_exactly(user1, user2)
end
end
context 'when admins is true', :enable_admin_mode do
let(:admin_user) { create(:user, :admin) }
it 'returns only admins' do
expect(
resolve_users( args: { admins: true }, ctx: { current_user: admin_user } )
).to contain_exactly(admin_user)
end
end
context 'when a search term is passed' do
it 'returns all users who match', :aggregate_failures do
expect(resolve_users(search: "some")).to contain_exactly(user1, user2)
expect(resolve_users(search: "123784")).to contain_exactly(user2)
expect(resolve_users(search: "someperson")).to contain_exactly(user1)
expect(resolve_users( args: { search: "some" } )).to contain_exactly(user1, user2)
expect(resolve_users( args: { search: "123784" } )).to contain_exactly(user2)
expect(resolve_users( args: { search: "someperson" } )).to contain_exactly(user1)
end
end
end
def resolve_users(args = {})
resolve(described_class, args: args)
def resolve_users(args: {}, ctx: {})
resolve(described_class, args: args, ctx: ctx)
end
end
......@@ -54,6 +54,52 @@ RSpec.describe 'Users' do
)
end
end
context 'when admins is true' do
let_it_be(:admin) { create(:user, :admin) }
let_it_be(:another_admin) { create(:user, :admin) }
let(:query) { graphql_query_for(:users, { admins: true }, 'nodes { id }') }
context 'current user is not an admin' do
let(:post_query) { post_graphql(query, current_user: current_user) }
it_behaves_like 'a working users query'
it 'includes all non-admin users', :aggregate_failures do
post_graphql(query)
expect(graphql_data.dig('users', 'nodes')).to include(
{ "id" => user1.to_global_id.to_s },
{ "id" => user2.to_global_id.to_s },
{ "id" => user3.to_global_id.to_s },
{ "id" => current_user.to_global_id.to_s },
{ "id" => admin.to_global_id.to_s },
{ "id" => another_admin.to_global_id.to_s }
)
end
end
context 'when current user is an admin' do
it_behaves_like 'a working users query'
it 'includes only admins', :aggregate_failures do
post_graphql(query, current_user: admin)
expect(graphql_data.dig('users', 'nodes')).to include(
{ "id" => another_admin.to_global_id.to_s },
{ "id" => admin.to_global_id.to_s }
)
expect(graphql_data.dig('users', 'nodes')).not_to include(
{ "id" => user1.to_global_id.to_s },
{ "id" => user2.to_global_id.to_s },
{ "id" => user3.to_global_id.to_s },
{ "id" => current_user.to_global_id.to_s }
)
end
end
end
end
describe 'sorting and pagination' do
......
......@@ -368,6 +368,16 @@ RSpec.describe API::Users do
expect(json_response.map { |u| u['id'] }).not_to include(internal_user.id)
end
end
context 'admins param' do
it 'returns all users' do
get api("/users?admins=true", user)
expect(response).to match_response_schema('public_api/v4/user/basics')
expect(json_response.size).to eq(2)
expect(json_response.map { |u| u['id'] }).to include(user.id, admin.id)
end
end
end
context "when admin" do
......@@ -487,6 +497,16 @@ RSpec.describe API::Users do
expect(response).to have_gitlab_http_status(:bad_request)
end
end
context 'admins param' do
it 'returns only admins' do
get api("/users?admins=true", admin)
expect(response).to match_response_schema('public_api/v4/user/basics')
expect(json_response.size).to eq(1)
expect(json_response.first['id']).to eq(admin.id)
end
end
end
describe "GET /users/:id" do
......
......@@ -2,6 +2,7 @@
RSpec.shared_context 'UsersFinder#execute filter by project context' do
let_it_be(:normal_user) { create(:user, username: 'johndoe') }
let_it_be(:admin_user) { create(:user, :admin, username: 'iamadmin') }
let_it_be(:blocked_user) { create(:user, :blocked, username: 'notsorandom') }
let_it_be(:external_user) { create(:user, :external) }
let_it_be(:omniauth_user) { create(:omniauth_user, provider: 'twitter', extern_uid: '123456') }
......
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