Commit 7516148b authored by Ethan Urie's avatar Ethan Urie Committed by Bob Van Landuyt

Fix linting errors

Also, fix specs for UserType.
parent 60ad6272
...@@ -34,6 +34,7 @@ class UsersFinder ...@@ -34,6 +34,7 @@ class UsersFinder
users = User.all.order_id_desc users = User.all.order_id_desc
users = by_username(users) users = by_username(users)
users = by_id(users) users = by_id(users)
users = by_admins(users)
users = by_search(users) users = by_search(users)
users = by_blocked(users) users = by_blocked(users)
users = by_active(users) users = by_active(users)
...@@ -62,6 +63,12 @@ class UsersFinder ...@@ -62,6 +63,12 @@ class UsersFinder
users.id_in(params[:id]) users.id_in(params[:id])
end end
def by_admins(users)
return users unless params[:admins] && current_user&.can_read_all_resources?
users.admins
end
def by_search(users) def by_search(users)
return users unless params[:search].present? return users unless params[:search].present?
......
...@@ -23,10 +23,15 @@ module Resolvers ...@@ -23,10 +23,15 @@ module Resolvers
required: false, required: false,
description: "Query to search users by name, username, or primary email." 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! 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 end
def ready?(**args) def ready?(**args)
...@@ -34,7 +39,7 @@ module Resolvers ...@@ -34,7 +39,7 @@ module Resolvers
return super if args.values.compact.blank? 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' raise Gitlab::Graphql::Errors::ArgumentError, 'Provide either a list of usernames or ids'
end end
...@@ -47,12 +52,13 @@ module Resolvers ...@@ -47,12 +52,13 @@ module Resolvers
private private
def finder_params(ids, usernames, sort, search) def finder_params(ids, usernames, sort, search, admins)
params = {} params = {}
params[:sort] = sort if sort params[:sort] = sort if sort
params[:username] = usernames if usernames params[:username] = usernames if usernames
params[:id] = parse_gids(ids) if ids params[:id] = parse_gids(ids) if ids
params[:search] = search if search params[:search] = search if search
params[:admins] = admins if admins
params params
end end
......
---
title: Add ability to get admins via REST and GraphQL API
merge_request: 46244
author:
type: added
...@@ -20366,6 +20366,11 @@ type Query { ...@@ -20366,6 +20366,11 @@ type Query {
Find users Find users
""" """
users( users(
"""
Return only admin users.
"""
admins: Boolean = false
""" """
Returns the elements in the list that come after the specified cursor. Returns the elements in the list that come after the specified cursor.
""" """
......
...@@ -59244,6 +59244,16 @@ ...@@ -59244,6 +59244,16 @@
}, },
"defaultValue": null "defaultValue": null
}, },
{
"name": "admins",
"description": "Return only admin users.",
"type": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
},
"defaultValue": "false"
},
{ {
"name": "after", "name": "after",
"description": "Returns the elements in the list that come after the specified cursor.", "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 ...@@ -89,6 +89,7 @@ GET /users
| `sort` | string | no | Return users sorted in `asc` or `desc` order. Default is `desc` | | `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 | | `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` | | `without_projects` | boolean | no | Filter users without projects. Default is `false` |
| `admins` | boolean | no | Return only admin users. Default is `false` |
```json ```json
[ [
......
...@@ -13,13 +13,13 @@ RSpec.describe UsersFinder do ...@@ -13,13 +13,13 @@ RSpec.describe UsersFinder do
it 'returns ldap users by default' do it 'returns ldap users by default' do
users = described_class.new(normal_user).execute 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 end
it 'returns only non-ldap users with skip_ldap: true' do it 'returns only non-ldap users with skip_ldap: true' do
users = described_class.new(normal_user, skip_ldap: true).execute 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 end
end end
......
...@@ -87,6 +87,7 @@ module API ...@@ -87,6 +87,7 @@ module API
optional :created_before, type: DateTime, desc: 'Return users created before the specified time' 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 :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 :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 all_or_none_of :extern_uid, :provider
use :sort_params use :sort_params
......
...@@ -12,7 +12,7 @@ RSpec.describe UsersFinder do ...@@ -12,7 +12,7 @@ RSpec.describe UsersFinder do
it 'returns all users' do it 'returns all users' do
users = described_class.new(user).execute 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 end
it 'filters by username' do it 'filters by username' do
...@@ -48,13 +48,13 @@ RSpec.describe UsersFinder do ...@@ -48,13 +48,13 @@ RSpec.describe UsersFinder do
it 'filters by active users' do it 'filters by active users' do
users = described_class.new(user, active: true).execute 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 end
it 'returns no external users' do it 'returns no external users' do
users = described_class.new(user, external: true).execute 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 end
it 'filters by created_at' do it 'filters by created_at' do
...@@ -71,7 +71,7 @@ RSpec.describe UsersFinder do ...@@ -71,7 +71,7 @@ RSpec.describe UsersFinder do
it 'filters by non internal users' do it 'filters by non internal users' do
users = described_class.new(user, non_internal: true).execute 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 end
it 'does not filter by custom attributes' do it 'does not filter by custom attributes' do
...@@ -80,13 +80,18 @@ RSpec.describe UsersFinder do ...@@ -80,13 +80,18 @@ RSpec.describe UsersFinder do
custom_attributes: { foo: 'bar' } custom_attributes: { foo: 'bar' }
).execute ).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 end
it 'orders returned results' do it 'orders returned results' do
users = described_class.new(user, sort: 'id_asc').execute 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
end end
...@@ -102,7 +107,13 @@ RSpec.describe UsersFinder do ...@@ -102,7 +107,13 @@ RSpec.describe UsersFinder do
it 'returns all users' do it 'returns all users' do
users = described_class.new(admin).execute 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 end
it 'filters by custom attributes' do it 'filters by custom attributes' do
......
...@@ -27,7 +27,7 @@ RSpec.describe Resolvers::UsersResolver do ...@@ -27,7 +27,7 @@ RSpec.describe Resolvers::UsersResolver do
context 'when both ids and usernames are passed ' do context 'when both ids and usernames are passed ' do
it 'raises an error' 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) .to raise_error(Gitlab::Graphql::Errors::ArgumentError)
end end
end end
...@@ -35,7 +35,7 @@ RSpec.describe Resolvers::UsersResolver do ...@@ -35,7 +35,7 @@ RSpec.describe Resolvers::UsersResolver do
context 'when a set of IDs is passed' do context 'when a set of IDs is passed' do
it 'returns those users' do it 'returns those users' do
expect( 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) ).to contain_exactly(user1, user2)
end end
end end
...@@ -43,21 +43,31 @@ RSpec.describe Resolvers::UsersResolver do ...@@ -43,21 +43,31 @@ RSpec.describe Resolvers::UsersResolver do
context 'when a set of usernames is passed' do context 'when a set of usernames is passed' do
it 'returns those users' do it 'returns those users' do
expect( expect(
resolve_users(usernames: [user1.username, user2.username]) resolve_users( args: { usernames: [user1.username, user2.username] } )
).to contain_exactly(user1, user2) ).to contain_exactly(user1, user2)
end end
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 context 'when a search term is passed' do
it 'returns all users who match', :aggregate_failures do it 'returns all users who match', :aggregate_failures do
expect(resolve_users(search: "some")).to contain_exactly(user1, user2) expect(resolve_users( args: { search: "some" } )).to contain_exactly(user1, user2)
expect(resolve_users(search: "123784")).to contain_exactly(user2) expect(resolve_users( args: { search: "123784" } )).to contain_exactly(user2)
expect(resolve_users(search: "someperson")).to contain_exactly(user1) expect(resolve_users( args: { search: "someperson" } )).to contain_exactly(user1)
end end
end end
end end
def resolve_users(args = {}) def resolve_users(args: {}, ctx: {})
resolve(described_class, args: args) resolve(described_class, args: args, ctx: ctx)
end end
end end
...@@ -54,6 +54,52 @@ RSpec.describe 'Users' do ...@@ -54,6 +54,52 @@ RSpec.describe 'Users' do
) )
end end
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 end
describe 'sorting and pagination' do describe 'sorting and pagination' do
......
...@@ -368,6 +368,16 @@ RSpec.describe API::Users do ...@@ -368,6 +368,16 @@ RSpec.describe API::Users do
expect(json_response.map { |u| u['id'] }).not_to include(internal_user.id) expect(json_response.map { |u| u['id'] }).not_to include(internal_user.id)
end end
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 end
context "when admin" do context "when admin" do
...@@ -487,6 +497,16 @@ RSpec.describe API::Users do ...@@ -487,6 +497,16 @@ RSpec.describe API::Users do
expect(response).to have_gitlab_http_status(:bad_request) expect(response).to have_gitlab_http_status(:bad_request)
end end
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 end
describe "GET /users/:id" do describe "GET /users/:id" do
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
RSpec.shared_context 'UsersFinder#execute filter by project context' do RSpec.shared_context 'UsersFinder#execute filter by project context' do
let_it_be(:normal_user) { create(:user, username: 'johndoe') } 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(:blocked_user) { create(:user, :blocked, username: 'notsorandom') }
let_it_be(:external_user) { create(:user, :external) } let_it_be(:external_user) { create(:user, :external) }
let_it_be(:omniauth_user) { create(:omniauth_user, provider: 'twitter', extern_uid: '123456') } 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