diff --git a/app/finders/users_finder.rb b/app/finders/users_finder.rb
index ebb686c2aa7107ce7e3167629cc2b5c562582e19..f87e0c67604a4e73d55f1e75dc585e9778e1c884 100644
--- a/app/finders/users_finder.rb
+++ b/app/finders/users_finder.rb
@@ -15,6 +15,8 @@
 #     blocked: boolean
 #     external: boolean
 #     without_projects: boolean
+#     sort: string
+#     id: integer
 #
 class UsersFinder
   include CreatedAtFilter
@@ -30,6 +32,7 @@ def initialize(current_user, params = {})
   def execute
     users = User.all.order_id_desc
     users = by_username(users)
+    users = by_id(users)
     users = by_search(users)
     users = by_blocked(users)
     users = by_active(users)
@@ -40,7 +43,7 @@ def execute
     users = by_without_projects(users)
     users = by_custom_attributes(users)
 
-    users
+    order(users)
   end
 
   private
@@ -51,6 +54,12 @@ def by_username(users)
     users.by_username(params[:username])
   end
 
+  def by_id(users)
+    return users unless params[:id]
+
+    users.id_in(params[:id])
+  end
+
   def by_search(users)
     return users unless params[:search].present?
 
@@ -102,6 +111,14 @@ def by_without_projects(users)
 
     users.without_projects
   end
+
+  # rubocop: disable CodeReuse/ActiveRecord
+  def order(users)
+    return users unless params[:sort]
+
+    users.order_by(params[:sort])
+  end
+  # rubocop: enable CodeReuse/ActiveRecord
 end
 
 UsersFinder.prepend_if_ee('EE::UsersFinder')
diff --git a/app/graphql/resolvers/users_resolver.rb b/app/graphql/resolvers/users_resolver.rb
new file mode 100644
index 0000000000000000000000000000000000000000..110a283b42edea5ebea9595a5a015057d5997b5b
--- /dev/null
+++ b/app/graphql/resolvers/users_resolver.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+module Resolvers
+  class UsersResolver < BaseResolver
+    include Gitlab::Graphql::Authorize::AuthorizeResource
+
+    description 'Find Users'
+
+    argument :ids, [GraphQL::ID_TYPE],
+             required: false,
+             description: 'List of user Global IDs'
+
+    argument :usernames, [GraphQL::STRING_TYPE], required: false,
+              description: 'List of usernames'
+
+    argument :sort, Types::SortEnum,
+             description: 'Sort users by this criteria',
+             required: false,
+             default_value: 'created_desc'
+
+    def resolve(ids: nil, usernames: nil, sort: nil)
+      authorize!
+
+      ::UsersFinder.new(context[:current_user], finder_params(ids, usernames, sort)).execute
+    end
+
+    def ready?(**args)
+      args = { ids: nil, usernames: nil }.merge!(args)
+
+      return super if args.values.compact.blank?
+
+      if args.values.all?
+        raise Gitlab::Graphql::Errors::ArgumentError, 'Provide either a list of usernames or ids'
+      end
+
+      super
+    end
+
+    def authorize!
+      Ability.allowed?(context[:current_user], :read_users_list) || raise_resource_not_available_error!
+    end
+
+    private
+
+    def finder_params(ids, usernames, sort)
+      params = {}
+      params[:sort] = sort if sort
+      params[:username] = usernames if usernames
+      params[:id] = parse_gids(ids) if ids
+      params
+    end
+
+    def parse_gids(gids)
+      gids.map { |gid| GitlabSchema.parse_gid(gid, expected_type: ::User).model_id }
+    end
+  end
+end
diff --git a/app/graphql/types/query_type.rb b/app/graphql/types/query_type.rb
index 5184d17e94e64f171128e41935b4db415b525507..362e4004b73d702fca9a823f9cdbbadafa3b9bd7 100644
--- a/app/graphql/types/query_type.rb
+++ b/app/graphql/types/query_type.rb
@@ -52,6 +52,11 @@ class QueryType < ::Types::BaseObject
           description: 'Find a user',
           resolver: Resolvers::UserResolver
 
+    field :users, Types::UserType.connection_type,
+          null: true,
+          description: 'Find users',
+          resolver: Resolvers::UsersResolver
+
     field :echo, GraphQL::STRING_TYPE, null: false,
           description: 'Text to echo back',
           resolver: Resolvers::EchoResolver
diff --git a/changelogs/unreleased/215658-root-users-query.yml b/changelogs/unreleased/215658-root-users-query.yml
new file mode 100644
index 0000000000000000000000000000000000000000..24c388ba826f9edbaf617f5e0fc3fae6c9567ef1
--- /dev/null
+++ b/changelogs/unreleased/215658-root-users-query.yml
@@ -0,0 +1,5 @@
+---
+title: Add root users query to GraphQL API
+merge_request: 33195
+author:
+type: added
diff --git a/doc/api/graphql/index.md b/doc/api/graphql/index.md
index c9e1769a1b3c3ac1005739c89965b86ec6e94a2c..d653c4e0f47fd70096373b02030448d9e1e05518 100644
--- a/doc/api/graphql/index.md
+++ b/doc/api/graphql/index.md
@@ -60,6 +60,7 @@ The GraphQL API includes the following queries at the root level:
 1. `user` : Information about a particular user.
 1. `namespace` : Within a namespace it is also possible to fetch `projects`.
 1. `currentUser`: Information about the currently logged in user.
+1. `users`: Information about a collection of users.
 1. `metaData`: Metadata about GitLab and the GraphQL API.
 1. `snippets`: Snippets visible to the currently logged in user.
 
diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql
index e70f28d4b02dd71be764ba0b6d472796123dedfb..a342aa315465718639f27d785f5759bbe0d79f03 100644
--- a/doc/api/graphql/reference/gitlab_schema.graphql
+++ b/doc/api/graphql/reference/gitlab_schema.graphql
@@ -9559,6 +9559,46 @@ type Query {
     username: String
   ): User
 
+  """
+  Find users
+  """
+  users(
+    """
+    Returns the elements in the list that come after the specified cursor.
+    """
+    after: String
+
+    """
+    Returns the elements in the list that come before the specified cursor.
+    """
+    before: String
+
+    """
+    Returns the first _n_ elements from the list.
+    """
+    first: Int
+
+    """
+    List of user Global IDs
+    """
+    ids: [ID!]
+
+    """
+    Returns the last _n_ elements from the list.
+    """
+    last: Int
+
+    """
+    Sort users by this criteria
+    """
+    sort: Sort = created_desc
+
+    """
+    List of usernames
+    """
+    usernames: [String!]
+  ): UserConnection
+
   """
   Vulnerabilities reported on projects on the current user's instance security dashboard
   """
diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json
index ed2631363a067d2bf2cff9a7a86c67fbb0b4836d..8a6c232f3fc7ef8ca88bed959933d3fe18246af8 100644
--- a/doc/api/graphql/reference/gitlab_schema.json
+++ b/doc/api/graphql/reference/gitlab_schema.json
@@ -28017,6 +28017,105 @@
               "isDeprecated": false,
               "deprecationReason": null
             },
+            {
+              "name": "users",
+              "description": "Find users",
+              "args": [
+                {
+                  "name": "ids",
+                  "description": "List of user Global IDs",
+                  "type": {
+                    "kind": "LIST",
+                    "name": null,
+                    "ofType": {
+                      "kind": "NON_NULL",
+                      "name": null,
+                      "ofType": {
+                        "kind": "SCALAR",
+                        "name": "ID",
+                        "ofType": null
+                      }
+                    }
+                  },
+                  "defaultValue": null
+                },
+                {
+                  "name": "usernames",
+                  "description": "List of usernames",
+                  "type": {
+                    "kind": "LIST",
+                    "name": null,
+                    "ofType": {
+                      "kind": "NON_NULL",
+                      "name": null,
+                      "ofType": {
+                        "kind": "SCALAR",
+                        "name": "String",
+                        "ofType": null
+                      }
+                    }
+                  },
+                  "defaultValue": null
+                },
+                {
+                  "name": "sort",
+                  "description": "Sort users by this criteria",
+                  "type": {
+                    "kind": "ENUM",
+                    "name": "Sort",
+                    "ofType": null
+                  },
+                  "defaultValue": "created_desc"
+                },
+                {
+                  "name": "after",
+                  "description": "Returns the elements in the list that come after the specified cursor.",
+                  "type": {
+                    "kind": "SCALAR",
+                    "name": "String",
+                    "ofType": null
+                  },
+                  "defaultValue": null
+                },
+                {
+                  "name": "before",
+                  "description": "Returns the elements in the list that come before the specified cursor.",
+                  "type": {
+                    "kind": "SCALAR",
+                    "name": "String",
+                    "ofType": null
+                  },
+                  "defaultValue": null
+                },
+                {
+                  "name": "first",
+                  "description": "Returns the first _n_ elements from the list.",
+                  "type": {
+                    "kind": "SCALAR",
+                    "name": "Int",
+                    "ofType": null
+                  },
+                  "defaultValue": null
+                },
+                {
+                  "name": "last",
+                  "description": "Returns the last _n_ elements from the list.",
+                  "type": {
+                    "kind": "SCALAR",
+                    "name": "Int",
+                    "ofType": null
+                  },
+                  "defaultValue": null
+                }
+              ],
+              "type": {
+                "kind": "OBJECT",
+                "name": "UserConnection",
+                "ofType": null
+              },
+              "isDeprecated": false,
+              "deprecationReason": null
+            },
             {
               "name": "vulnerabilities",
               "description": "Vulnerabilities reported on projects on the current user's instance security dashboard",
diff --git a/spec/finders/users_finder_spec.rb b/spec/finders/users_finder_spec.rb
index 7f1fc1cc1c5cba32ffaf13a3ba46fd999bdcd2f1..67c97511c3e2bf9a0e33cc9f2f930a78b57d090b 100644
--- a/spec/finders/users_finder_spec.rb
+++ b/spec/finders/users_finder_spec.rb
@@ -21,6 +21,12 @@
         expect(users).to contain_exactly(normal_user)
       end
 
+      it 'filters by id' do
+        users = described_class.new(user, id: normal_user.id).execute
+
+        expect(users).to contain_exactly(normal_user)
+      end
+
       it 'filters by username (case insensitive)' do
         users = described_class.new(user, username: 'joHNdoE').execute
 
@@ -70,6 +76,12 @@
 
         expect(users).to contain_exactly(user, normal_user, blocked_user, omniauth_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, user])
+      end
     end
 
     context 'with an admin user' do
diff --git a/spec/graphql/resolvers/users_resolver_spec.rb b/spec/graphql/resolvers/users_resolver_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..e752500d52f6c5c27b31e1fbfc44fac42ba9e97b
--- /dev/null
+++ b/spec/graphql/resolvers/users_resolver_spec.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Resolvers::UsersResolver do
+  include GraphqlHelpers
+
+  let_it_be(:user1) { create(:user) }
+  let_it_be(:user2) { create(:user) }
+
+  describe '#resolve' do
+    it 'raises an error when read_users_list is not authorized' do
+      expect(Ability).to receive(:allowed?).with(nil, :read_users_list).and_return(false)
+
+      expect { resolve_users }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
+    end
+
+    context 'when no arguments are passed' do
+      it 'returns all users' do
+        expect(resolve_users).to contain_exactly(user1, user2)
+      end
+    end
+
+    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]) }
+        .to raise_error(Gitlab::Graphql::Errors::ArgumentError)
+      end
+    end
+
+    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])
+        ).to contain_exactly(user1, user2)
+      end
+    end
+
+    context 'when a set of usernames is passed' do
+      it 'returns those users' do
+        expect(
+          resolve_users(usernames: [user1.username, user2.username])
+        ).to contain_exactly(user1, user2)
+      end
+    end
+  end
+
+  def resolve_users(args = {})
+    resolve(described_class, args: args)
+  end
+end
diff --git a/spec/graphql/types/query_type_spec.rb b/spec/graphql/types/query_type_spec.rb
index 1bb04f1bb68271f370de21500fed84d66c58dad5..1194391c26a0a0d70746902496ee2ed2634f5b19 100644
--- a/spec/graphql/types/query_type_spec.rb
+++ b/spec/graphql/types/query_type_spec.rb
@@ -18,6 +18,7 @@
       snippets
       design_management
       user
+      users
     ]
 
     expect(described_class).to have_graphql_fields(*expected_fields).at_least
diff --git a/spec/requests/api/graphql/users_spec.rb b/spec/requests/api/graphql/users_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..1e6d73cbd7d2d4835d0becda26d0c98b8b3eebf6
--- /dev/null
+++ b/spec/requests/api/graphql/users_spec.rb
@@ -0,0 +1,90 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'Users' do
+  include GraphqlHelpers
+
+  let_it_be(:current_user) { create(:user, created_at: 1.day.ago) }
+  let_it_be(:user1) { create(:user, created_at: 2.days.ago) }
+  let_it_be(:user2) { create(:user, created_at: 3.days.ago) }
+  let_it_be(:user3) { create(:user, created_at: 4.days.ago) }
+
+  describe '.users' do
+    shared_examples 'a working users query' do
+      it_behaves_like 'a working graphql query' do
+        before do
+          post_graphql(query, current_user: current_user)
+        end
+      end
+
+      it 'includes a list of users' do
+        post_graphql(query)
+
+        expect(graphql_data.dig('users', 'nodes')).not_to be_empty
+      end
+    end
+
+    context 'with no arguments' do
+      let_it_be(:query) { graphql_query_for(:users, { usernames: [user1.username] }, 'nodes { id }') }
+
+      it_behaves_like 'a working users query'
+    end
+
+    context 'with a list of usernames' do
+      let(:query) { graphql_query_for(:users, { usernames: [user1.username] }, 'nodes { id }') }
+
+      it_behaves_like 'a working users query'
+    end
+
+    context 'with a list of IDs' do
+      let(:query) { graphql_query_for(:users, { ids: [user1.to_global_id.to_s] }, 'nodes { id }') }
+
+      it_behaves_like 'a working users query'
+    end
+
+    context 'when usernames and ids parameter are used' do
+      let_it_be(:query) { graphql_query_for(:users, { ids: user1.to_global_id.to_s, usernames: user1.username }, 'nodes { id }') }
+
+      it 'displays an error' do
+        post_graphql(query)
+
+        expect(graphql_errors).to include(
+          a_hash_including('message' => a_string_matching(%r{Provide either a list of usernames or ids}))
+        )
+      end
+    end
+  end
+
+  describe 'sorting and pagination' do
+    let_it_be(:data_path) { [:users] }
+
+    def pagination_query(params, page_info)
+      graphql_query_for("users", params, "#{page_info} edges { node { id } }")
+    end
+
+    def pagination_results_data(data)
+      data.map { |user| user.dig('node', 'id') }
+    end
+
+    context 'when sorting by created_at' do
+      let_it_be(:ascending_users) { [user3, user2, user1, current_user].map(&:to_global_id).map(&:to_s) }
+
+      context 'when ascending' do
+        it_behaves_like 'sorted paginated query' do
+          let(:sort_param)       { 'created_asc' }
+          let(:first_param)      { 1 }
+          let(:expected_results) { ascending_users }
+        end
+      end
+
+      context 'when descending' do
+        it_behaves_like 'sorted paginated query' do
+          let(:sort_param)       { 'created_desc' }
+          let(:first_param)      { 1 }
+          let(:expected_results) { ascending_users.reverse }
+        end
+      end
+    end
+  end
+end