Replace /admin/v1/users_paginate endpoint with /admin/v2/users (#5925)

This commit is contained in:
Manuel Stahl 2019-12-05 19:12:23 +01:00 committed by Richard van der Hoff
parent d085a8a0a5
commit 649b6bc088
9 changed files with 162 additions and 110 deletions

1
changelog.d/5925.feature Normal file
View file

@ -0,0 +1 @@
Add admin/v2/users endpoint with pagination. Contributed by Awesome Technologies Innovationslabor GmbH.

1
changelog.d/5925.removal Normal file
View file

@ -0,0 +1 @@
Remove admin/v1/users_paginate endpoint. Contributed by Awesome Technologies Innovationslabor GmbH.

View file

@ -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 Query Account
============= =============

View file

@ -56,7 +56,7 @@ class AdminHandler(BaseHandler):
@defer.inlineCallbacks @defer.inlineCallbacks
def get_users(self): 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: Args:
Returns: Returns:
@ -67,19 +67,22 @@ class AdminHandler(BaseHandler):
return ret return ret
@defer.inlineCallbacks @defer.inlineCallbacks
def get_users_paginate(self, order, start, limit): def get_users_paginate(self, start, limit, name, guests, deactivated):
"""Function to reterive a paginated list of users from """Function to retrieve a paginated list of users from
users list. This will return a json object, which contains users list. This will return a json list of users.
list of users and the total number of users in users table.
Args: Args:
order (str): column name to order the select by this column
start (int): start number to begin the query from 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: 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 return ret

View file

@ -34,12 +34,12 @@ from synapse.rest.admin.server_notice_servlet import SendServerNoticeServlet
from synapse.rest.admin.users import ( from synapse.rest.admin.users import (
AccountValidityRenewServlet, AccountValidityRenewServlet,
DeactivateAccountRestServlet, DeactivateAccountRestServlet,
GetUsersPaginatedRestServlet,
ResetPasswordRestServlet, ResetPasswordRestServlet,
SearchUsersRestServlet, SearchUsersRestServlet,
UserAdminServlet, UserAdminServlet,
UserRegisterServlet, UserRegisterServlet,
UsersRestServlet, UsersRestServlet,
UsersRestServletV2,
WhoisRestServlet, WhoisRestServlet,
) )
from synapse.util.versionstring import get_version_string from synapse.util.versionstring import get_version_string
@ -191,6 +191,7 @@ def register_servlets(hs, http_server):
SendServerNoticeServlet(hs).register(http_server) SendServerNoticeServlet(hs).register(http_server)
VersionServlet(hs).register(http_server) VersionServlet(hs).register(http_server)
UserAdminServlet(hs).register(http_server) UserAdminServlet(hs).register(http_server)
UsersRestServletV2(hs).register(http_server)
def register_servlets_for_client_rest_resource(hs, 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) PurgeHistoryRestServlet(hs).register(http_server)
UsersRestServlet(hs).register(http_server) UsersRestServlet(hs).register(http_server)
ResetPasswordRestServlet(hs).register(http_server) ResetPasswordRestServlet(hs).register(http_server)
GetUsersPaginatedRestServlet(hs).register(http_server)
SearchUsersRestServlet(hs).register(http_server) SearchUsersRestServlet(hs).register(http_server)
ShutdownRoomRestServlet(hs).register(http_server) ShutdownRoomRestServlet(hs).register(http_server)
UserRegisterServlet(hs).register(http_server) UserRegisterServlet(hs).register(http_server)

View file

@ -25,6 +25,7 @@ from synapse.api.errors import Codes, SynapseError
from synapse.http.servlet import ( from synapse.http.servlet import (
RestServlet, RestServlet,
assert_params_in_dict, assert_params_in_dict,
parse_boolean,
parse_integer, parse_integer,
parse_json_object_from_request, parse_json_object_from_request,
parse_string, parse_string,
@ -59,71 +60,45 @@ class UsersRestServlet(RestServlet):
return 200, ret return 200, ret
class GetUsersPaginatedRestServlet(RestServlet): class UsersRestServletV2(RestServlet):
"""Get request to get specific number of users from Synapse. PATTERNS = (re.compile("^/_synapse/admin/v2/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( """Get request to list all local users.
"/users_paginate/(?P<target_user_id>[^/]*)" This needs user to have administrator access in Synapse.
)
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): def __init__(self, hs):
self.store = hs.get_datastore()
self.hs = hs self.hs = hs
self.auth = hs.get_auth() 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): async def on_GET(self, request):
"""Get request to get specific number of users from Synapse.
This needs user to have administrator access in Synapse.
"""
await assert_requester_is_admin(self.auth, request) await assert_requester_is_admin(self.auth, request)
target_user = UserID.from_string(target_user_id) 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)
if not self.hs.is_mine(target_user): users = await self.admin_handler.get_users_paginate(
raise SynapseError(400, "Can only users a local user") start, limit, user_id, guests, deactivated
)
ret = {"users": users}
if len(users) >= limit:
ret["next_token"] = str(start + len(users))
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
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)
ret = await self.handlers.admin_handler.get_users_paginate(order, start, limit)
return 200, ret return 200, ret

View file

@ -1350,11 +1350,12 @@ class SQLBaseStore(object):
def simple_select_list_paginate( def simple_select_list_paginate(
self, self,
table, table,
keyvalues,
orderby, orderby,
start, start,
limit, limit,
retcols, retcols,
filters=None,
keyvalues=None,
order_direction="ASC", order_direction="ASC",
desc="simple_select_list_paginate", desc="simple_select_list_paginate",
): ):
@ -1365,6 +1366,9 @@ class SQLBaseStore(object):
Args: Args:
table (str): the table name 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): keyvalues (dict[str, T] | None):
column names and values to select the rows with, or None to not column names and values to select the rows with, or None to not
apply a WHERE clause. apply a WHERE clause.
@ -1380,11 +1384,12 @@ class SQLBaseStore(object):
desc, desc,
self.simple_select_list_paginate_txn, self.simple_select_list_paginate_txn,
table, table,
keyvalues,
orderby, orderby,
start, start,
limit, limit,
retcols, retcols,
filters=filters,
keyvalues=keyvalues,
order_direction=order_direction, order_direction=order_direction,
) )
@ -1393,11 +1398,12 @@ class SQLBaseStore(object):
cls, cls,
txn, txn,
table, table,
keyvalues,
orderby, orderby,
start, start,
limit, limit,
retcols, retcols,
filters=None,
keyvalues=None,
order_direction="ASC", 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, of row numbers, which may return zero or number of rows from start to limit,
returning the result as a list of dicts. 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: Args:
txn : Transaction object txn : Transaction object
table (str): the table name 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. orderby (str): Column to order the results by.
start (int): Index to begin the query at. start (int): Index to begin the query at.
limit (int): Number of results to return. limit (int): Number of results to return.
retcols (iterable[str]): the names of the columns 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". order_direction (str): Whether the results should be ordered "ASC" or "DESC".
Returns: Returns:
defer.Deferred: resolves to list[dict[str, Any]] defer.Deferred: resolves to list[dict[str, Any]]
@ -1422,10 +1435,15 @@ class SQLBaseStore(object):
if order_direction not in ["ASC", "DESC"]: if order_direction not in ["ASC", "DESC"]:
raise ValueError("order_direction must be one of 'ASC' or '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: if keyvalues:
where_clause = "WHERE " + " AND ".join("%s = ?" % (k,) for k in keyvalues) where_clause += " AND ".join("%s = ?" % (k,) for k in keyvalues)
else: arg_list += list(keyvalues.values())
where_clause = ""
sql = "SELECT %s FROM %s %s ORDER BY %s %s LIMIT ? OFFSET ?" % ( sql = "SELECT %s FROM %s %s ORDER BY %s %s LIMIT ? OFFSET ?" % (
", ".join(retcols), ", ".join(retcols),
@ -1434,22 +1452,10 @@ class SQLBaseStore(object):
orderby, orderby,
order_direction, order_direction,
) )
txn.execute(sql, list(keyvalues.values()) + [limit, start]) txn.execute(sql, arg_list + [limit, start])
return cls.cursor_to_dict(txn) 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"): 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 """Executes a SELECT query on the named table, which may return zero or
more rows, returning the result as a list of dicts. more rows, returning the result as a list of dicts.

View file

@ -19,8 +19,6 @@ import calendar
import logging import logging
import time import time
from twisted.internet import defer
from synapse.api.constants import PresenceState from synapse.api.constants import PresenceState
from synapse.storage.engines import PostgresEngine from synapse.storage.engines import PostgresEngine
from synapse.storage.util.id_generators import ( from synapse.storage.util.id_generators import (
@ -476,7 +474,7 @@ class DataStore(
) )
def get_users(self): 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: Args:
Returns: Returns:
@ -485,36 +483,59 @@ class DataStore(
return self.simple_select_list( return self.simple_select_list(
table="users", table="users",
keyvalues={}, keyvalues={},
retcols=["name", "password_hash", "is_guest", "admin", "user_type"], retcols=[
"name",
"password_hash",
"is_guest",
"admin",
"user_type",
"deactivated",
],
desc="get_users", desc="get_users",
) )
@defer.inlineCallbacks def get_users_paginate(
def get_users_paginate(self, order, start, limit): self, start, limit, name=None, guests=True, deactivated=False
"""Function to reterive a paginated list of users from ):
users list. This will return a json object, which contains """Function to retrieve a paginated list of users from
list of users and the total number of users in users table. users list. This will return a json list of users.
Args: Args:
order (str): column name to order the select by this column
start (int): start number to begin the query from 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: Returns:
defer.Deferred: resolves to json object {list[dict[str, Any]], count} defer.Deferred: resolves to list[dict[str, Any]]
""" """
users = yield self.runInteraction( name_filter = {}
"get_users_paginate", if name:
self.simple_select_list_paginate_txn, 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", table="users",
keyvalues={"is_guest": False}, orderby="name",
orderby=order,
start=start, start=start,
limit=limit, 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): def search_users(self, term):
"""Function to search users list for one or more users with """Function to search users list for one or more users with

View file

@ -260,11 +260,11 @@ class StatsStore(StateDeltasStore):
slice_list = self.simple_select_list_paginate_txn( slice_list = self.simple_select_list_paginate_txn(
txn, txn,
table + "_historical", table + "_historical",
{id_col: stats_id},
"end_ts", "end_ts",
start, start,
size, size,
retcols=selected_columns + ["bucket_size", "end_ts"], retcols=selected_columns + ["bucket_size", "end_ts"],
keyvalues={id_col: stats_id},
order_direction="DESC", order_direction="DESC",
) )