diff --git a/changelog.d/5925.feature b/changelog.d/5925.feature
new file mode 100644
index 0000000000..8025cc8231
--- /dev/null
+++ b/changelog.d/5925.feature
@@ -0,0 +1 @@
+Add admin/v2/users endpoint with pagination. Contributed by Awesome Technologies Innovationslabor GmbH.
diff --git a/changelog.d/5925.removal b/changelog.d/5925.removal
new file mode 100644
index 0000000000..cbba2855cb
--- /dev/null
+++ b/changelog.d/5925.removal
@@ -0,0 +1 @@
+Remove admin/v1/users_paginate endpoint. Contributed by Awesome Technologies Innovationslabor GmbH.
diff --git a/docs/admin_api/user_admin_api.rst b/docs/admin_api/user_admin_api.rst
index d0871f9438..b451dc5014 100644
--- a/docs/admin_api/user_admin_api.rst
+++ b/docs/admin_api/user_admin_api.rst
@@ -1,3 +1,48 @@
+List Accounts
+=============
+
+This API returns all local user accounts.
+
+The api is::
+
+ GET /_synapse/admin/v2/users?from=0&limit=10&guests=false
+
+including an ``access_token`` of a server admin.
+The parameters ``from`` and ``limit`` are required only for pagination.
+By default, a ``limit`` of 100 is used.
+The parameter ``user_id`` can be used to select only users with user ids that
+contain this value.
+The parameter ``guests=false`` can be used to exclude guest users,
+default is to include guest users.
+The parameter ``deactivated=true`` can be used to include deactivated users,
+default is to exclude deactivated users.
+If the endpoint does not return a ``next_token`` then there are no more users left.
+It returns a JSON body like the following:
+
+.. code:: json
+
+ {
+ "users": [
+ {
+ "name": "<user_id1>",
+ "password_hash": "<password_hash1>",
+ "is_guest": 0,
+ "admin": 0,
+ "user_type": null,
+ "deactivated": 0
+ }, {
+ "name": "<user_id2>",
+ "password_hash": "<password_hash2>",
+ "is_guest": 0,
+ "admin": 1,
+ "user_type": null,
+ "deactivated": 0
+ }
+ ],
+ "next_token": "100"
+ }
+
+
Query Account
=============
diff --git a/synapse/handlers/admin.py b/synapse/handlers/admin.py
index 6407d56f8e..14449b9a1e 100644
--- a/synapse/handlers/admin.py
+++ b/synapse/handlers/admin.py
@@ -56,7 +56,7 @@ class AdminHandler(BaseHandler):
@defer.inlineCallbacks
def get_users(self):
- """Function to reterive a list of users in users table.
+ """Function to retrieve a list of users in users table.
Args:
Returns:
@@ -67,19 +67,22 @@ class AdminHandler(BaseHandler):
return ret
@defer.inlineCallbacks
- def get_users_paginate(self, order, start, limit):
- """Function to reterive a paginated list of users from
- users list. This will return a json object, which contains
- list of users and the total number of users in users table.
+ def get_users_paginate(self, start, limit, name, guests, deactivated):
+ """Function to retrieve a paginated list of users from
+ users list. This will return a json list of users.
Args:
- order (str): column name to order the select by this column
start (int): start number to begin the query from
- limit (int): number of rows to reterive
+ limit (int): number of rows to retrieve
+ name (string): filter for user names
+ guests (bool): whether to in include guest users
+ deactivated (bool): whether to include deactivated users
Returns:
- defer.Deferred: resolves to json object {list[dict[str, Any]], count}
+ defer.Deferred: resolves to json list[dict[str, Any]]
"""
- ret = yield self.store.get_users_paginate(order, start, limit)
+ ret = yield self.store.get_users_paginate(
+ start, limit, name, guests, deactivated
+ )
return ret
diff --git a/synapse/rest/admin/__init__.py b/synapse/rest/admin/__init__.py
index 68a59a3424..c122c449f4 100644
--- a/synapse/rest/admin/__init__.py
+++ b/synapse/rest/admin/__init__.py
@@ -34,12 +34,12 @@ from synapse.rest.admin.server_notice_servlet import SendServerNoticeServlet
from synapse.rest.admin.users import (
AccountValidityRenewServlet,
DeactivateAccountRestServlet,
- GetUsersPaginatedRestServlet,
ResetPasswordRestServlet,
SearchUsersRestServlet,
UserAdminServlet,
UserRegisterServlet,
UsersRestServlet,
+ UsersRestServletV2,
WhoisRestServlet,
)
from synapse.util.versionstring import get_version_string
@@ -191,6 +191,7 @@ def register_servlets(hs, http_server):
SendServerNoticeServlet(hs).register(http_server)
VersionServlet(hs).register(http_server)
UserAdminServlet(hs).register(http_server)
+ UsersRestServletV2(hs).register(http_server)
def register_servlets_for_client_rest_resource(hs, http_server):
@@ -201,7 +202,6 @@ def register_servlets_for_client_rest_resource(hs, http_server):
PurgeHistoryRestServlet(hs).register(http_server)
UsersRestServlet(hs).register(http_server)
ResetPasswordRestServlet(hs).register(http_server)
- GetUsersPaginatedRestServlet(hs).register(http_server)
SearchUsersRestServlet(hs).register(http_server)
ShutdownRoomRestServlet(hs).register(http_server)
UserRegisterServlet(hs).register(http_server)
diff --git a/synapse/rest/admin/users.py b/synapse/rest/admin/users.py
index 58a83f93af..1937879dbe 100644
--- a/synapse/rest/admin/users.py
+++ b/synapse/rest/admin/users.py
@@ -25,6 +25,7 @@ from synapse.api.errors import Codes, SynapseError
from synapse.http.servlet import (
RestServlet,
assert_params_in_dict,
+ parse_boolean,
parse_integer,
parse_json_object_from_request,
parse_string,
@@ -59,71 +60,45 @@ class UsersRestServlet(RestServlet):
return 200, ret
-class GetUsersPaginatedRestServlet(RestServlet):
- """Get request to get specific number of users from Synapse.
+class UsersRestServletV2(RestServlet):
+ PATTERNS = (re.compile("^/_synapse/admin/v2/users$"),)
+
+ """Get request to list all local users.
This needs user to have administrator access in Synapse.
- Example:
- http://localhost:8008/_synapse/admin/v1/users_paginate/
- @admin:user?access_token=admin_access_token&start=0&limit=10
- Returns:
- 200 OK with json object {list[dict[str, Any]], count} or empty object.
- """
- PATTERNS = historical_admin_path_patterns(
- "/users_paginate/(?P<target_user_id>[^/]*)"
- )
+ GET /_synapse/admin/v2/users?from=0&limit=10&guests=false
+
+ returns:
+ 200 OK with list of users if success otherwise an error.
+
+ The parameters `from` and `limit` are required only for pagination.
+ By default, a `limit` of 100 is used.
+ The parameter `user_id` can be used to filter by user id.
+ The parameter `guests` can be used to exclude guest users.
+ The parameter `deactivated` can be used to include deactivated users.
+ """
def __init__(self, hs):
- self.store = hs.get_datastore()
self.hs = hs
self.auth = hs.get_auth()
- self.handlers = hs.get_handlers()
+ self.admin_handler = hs.get_handlers().admin_handler
- async def on_GET(self, request, target_user_id):
- """Get request to get specific number of users from Synapse.
- This needs user to have administrator access in Synapse.
- """
+ async def on_GET(self, request):
await assert_requester_is_admin(self.auth, request)
- target_user = UserID.from_string(target_user_id)
-
- if not self.hs.is_mine(target_user):
- raise SynapseError(400, "Can only users a local user")
-
- order = "name" # order by name in user table
- start = parse_integer(request, "start", required=True)
- limit = parse_integer(request, "limit", required=True)
-
- logger.info("limit: %s, start: %s", limit, start)
-
- ret = await self.handlers.admin_handler.get_users_paginate(order, start, limit)
- return 200, ret
+ start = parse_integer(request, "from", default=0)
+ limit = parse_integer(request, "limit", default=100)
+ user_id = parse_string(request, "user_id", default=None)
+ guests = parse_boolean(request, "guests", default=True)
+ deactivated = parse_boolean(request, "deactivated", default=False)
- async def on_POST(self, request, target_user_id):
- """Post request to get specific number of users from Synapse..
- This needs user to have administrator access in Synapse.
- Example:
- http://localhost:8008/_synapse/admin/v1/users_paginate/
- @admin:user?access_token=admin_access_token
- JsonBodyToSend:
- {
- "start": "0",
- "limit": "10
- }
- Returns:
- 200 OK with json object {list[dict[str, Any]], count} or empty object.
- """
- await assert_requester_is_admin(self.auth, request)
- UserID.from_string(target_user_id)
-
- order = "name" # order by name in user table
- params = parse_json_object_from_request(request)
- assert_params_in_dict(params, ["limit", "start"])
- limit = params["limit"]
- start = params["start"]
- logger.info("limit: %s, start: %s", limit, start)
+ users = await self.admin_handler.get_users_paginate(
+ start, limit, user_id, guests, deactivated
+ )
+ ret = {"users": users}
+ if len(users) >= limit:
+ ret["next_token"] = str(start + len(users))
- ret = await self.handlers.admin_handler.get_users_paginate(order, start, limit)
return 200, ret
diff --git a/synapse/storage/_base.py b/synapse/storage/_base.py
index 9205e550bb..0d7c7dff27 100644
--- a/synapse/storage/_base.py
+++ b/synapse/storage/_base.py
@@ -1350,11 +1350,12 @@ class SQLBaseStore(object):
def simple_select_list_paginate(
self,
table,
- keyvalues,
orderby,
start,
limit,
retcols,
+ filters=None,
+ keyvalues=None,
order_direction="ASC",
desc="simple_select_list_paginate",
):
@@ -1365,6 +1366,9 @@ class SQLBaseStore(object):
Args:
table (str): the table name
+ filters (dict[str, T] | None):
+ column names and values to filter the rows with, or None to not
+ apply a WHERE ? LIKE ? clause.
keyvalues (dict[str, T] | None):
column names and values to select the rows with, or None to not
apply a WHERE clause.
@@ -1380,11 +1384,12 @@ class SQLBaseStore(object):
desc,
self.simple_select_list_paginate_txn,
table,
- keyvalues,
orderby,
start,
limit,
retcols,
+ filters=filters,
+ keyvalues=keyvalues,
order_direction=order_direction,
)
@@ -1393,11 +1398,12 @@ class SQLBaseStore(object):
cls,
txn,
table,
- keyvalues,
orderby,
start,
limit,
retcols,
+ filters=None,
+ keyvalues=None,
order_direction="ASC",
):
"""
@@ -1405,16 +1411,23 @@ class SQLBaseStore(object):
of row numbers, which may return zero or number of rows from start to limit,
returning the result as a list of dicts.
+ Use `filters` to search attributes using SQL wildcards and/or `keyvalues` to
+ select attributes with exact matches. All constraints are joined together
+ using 'AND'.
+
Args:
txn : Transaction object
table (str): the table name
- keyvalues (dict[str, T] | None):
- column names and values to select the rows with, or None to not
- apply a WHERE clause.
orderby (str): Column to order the results by.
start (int): Index to begin the query at.
limit (int): Number of results to return.
retcols (iterable[str]): the names of the columns to return
+ filters (dict[str, T] | None):
+ column names and values to filter the rows with, or None to not
+ apply a WHERE ? LIKE ? clause.
+ keyvalues (dict[str, T] | None):
+ column names and values to select the rows with, or None to not
+ apply a WHERE clause.
order_direction (str): Whether the results should be ordered "ASC" or "DESC".
Returns:
defer.Deferred: resolves to list[dict[str, Any]]
@@ -1422,10 +1435,15 @@ class SQLBaseStore(object):
if order_direction not in ["ASC", "DESC"]:
raise ValueError("order_direction must be one of 'ASC' or 'DESC'.")
+ where_clause = "WHERE " if filters or keyvalues else ""
+ arg_list = []
+ if filters:
+ where_clause += " AND ".join("%s LIKE ?" % (k,) for k in filters)
+ arg_list += list(filters.values())
+ where_clause += " AND " if filters and keyvalues else ""
if keyvalues:
- where_clause = "WHERE " + " AND ".join("%s = ?" % (k,) for k in keyvalues)
- else:
- where_clause = ""
+ where_clause += " AND ".join("%s = ?" % (k,) for k in keyvalues)
+ arg_list += list(keyvalues.values())
sql = "SELECT %s FROM %s %s ORDER BY %s %s LIMIT ? OFFSET ?" % (
", ".join(retcols),
@@ -1434,22 +1452,10 @@ class SQLBaseStore(object):
orderby,
order_direction,
)
- txn.execute(sql, list(keyvalues.values()) + [limit, start])
+ txn.execute(sql, arg_list + [limit, start])
return cls.cursor_to_dict(txn)
- def get_user_count_txn(self, txn):
- """Get a total number of registered users in the users list.
-
- Args:
- txn : Transaction object
- Returns:
- int : number of users
- """
- sql_count = "SELECT COUNT(*) FROM users WHERE is_guest = 0;"
- txn.execute(sql_count)
- return txn.fetchone()[0]
-
def simple_search_list(self, table, term, col, retcols, desc="simple_search_list"):
"""Executes a SELECT query on the named table, which may return zero or
more rows, returning the result as a list of dicts.
diff --git a/synapse/storage/data_stores/main/__init__.py b/synapse/storage/data_stores/main/__init__.py
index 2a5b33dda1..3720ff3088 100644
--- a/synapse/storage/data_stores/main/__init__.py
+++ b/synapse/storage/data_stores/main/__init__.py
@@ -19,8 +19,6 @@ import calendar
import logging
import time
-from twisted.internet import defer
-
from synapse.api.constants import PresenceState
from synapse.storage.engines import PostgresEngine
from synapse.storage.util.id_generators import (
@@ -476,7 +474,7 @@ class DataStore(
)
def get_users(self):
- """Function to reterive a list of users in users table.
+ """Function to retrieve a list of users in users table.
Args:
Returns:
@@ -485,36 +483,59 @@ class DataStore(
return self.simple_select_list(
table="users",
keyvalues={},
- retcols=["name", "password_hash", "is_guest", "admin", "user_type"],
+ retcols=[
+ "name",
+ "password_hash",
+ "is_guest",
+ "admin",
+ "user_type",
+ "deactivated",
+ ],
desc="get_users",
)
- @defer.inlineCallbacks
- def get_users_paginate(self, order, start, limit):
- """Function to reterive a paginated list of users from
- users list. This will return a json object, which contains
- list of users and the total number of users in users table.
+ def get_users_paginate(
+ self, start, limit, name=None, guests=True, deactivated=False
+ ):
+ """Function to retrieve a paginated list of users from
+ users list. This will return a json list of users.
Args:
- order (str): column name to order the select by this column
start (int): start number to begin the query from
- limit (int): number of rows to reterive
+ limit (int): number of rows to retrieve
+ name (string): filter for user names
+ guests (bool): whether to in include guest users
+ deactivated (bool): whether to include deactivated users
Returns:
- defer.Deferred: resolves to json object {list[dict[str, Any]], count}
+ defer.Deferred: resolves to list[dict[str, Any]]
"""
- users = yield self.runInteraction(
- "get_users_paginate",
- self.simple_select_list_paginate_txn,
+ name_filter = {}
+ if name:
+ name_filter["name"] = "%" + name + "%"
+
+ attr_filter = {}
+ if not guests:
+ attr_filter["is_guest"] = False
+ if not deactivated:
+ attr_filter["deactivated"] = False
+
+ return self.simple_select_list_paginate(
+ desc="get_users_paginate",
table="users",
- keyvalues={"is_guest": False},
- orderby=order,
+ orderby="name",
start=start,
limit=limit,
- retcols=["name", "password_hash", "is_guest", "admin", "user_type"],
+ filters=name_filter,
+ keyvalues=attr_filter,
+ retcols=[
+ "name",
+ "password_hash",
+ "is_guest",
+ "admin",
+ "user_type",
+ "deactivated",
+ ],
)
- count = yield self.runInteraction("get_users_paginate", self.get_user_count_txn)
- retval = {"users": users, "total": count}
- return retval
def search_users(self, term):
"""Function to search users list for one or more users with
diff --git a/synapse/storage/data_stores/main/stats.py b/synapse/storage/data_stores/main/stats.py
index 3aeba859fd..b306478824 100644
--- a/synapse/storage/data_stores/main/stats.py
+++ b/synapse/storage/data_stores/main/stats.py
@@ -260,11 +260,11 @@ class StatsStore(StateDeltasStore):
slice_list = self.simple_select_list_paginate_txn(
txn,
table + "_historical",
- {id_col: stats_id},
"end_ts",
start,
size,
retcols=selected_columns + ["bucket_size", "end_ts"],
+ keyvalues={id_col: stats_id},
order_direction="DESC",
)
|