Skip to content
This repository has been archived by the owner on Apr 26, 2024. It is now read-only.

Commit

Permalink
Replace /admin/v1/users_paginate endpoint with /admin/v2/users (#5925)
Browse files Browse the repository at this point in the history
  • Loading branch information
awesome-manuel authored and richvdh committed Dec 5, 2019
1 parent d085a8a commit 649b6bc
Show file tree
Hide file tree
Showing 9 changed files with 161 additions and 109 deletions.
1 change: 1 addition & 0 deletions changelog.d/5925.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add admin/v2/users endpoint with pagination. Contributed by Awesome Technologies Innovationslabor GmbH.
1 change: 1 addition & 0 deletions changelog.d/5925.removal
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Remove admin/v1/users_paginate endpoint. Contributed by Awesome Technologies Innovationslabor GmbH.
45 changes: 45 additions & 0 deletions docs/admin_api/user_admin_api.rst
Original file line number Diff line number Diff line change
@@ -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
=============

Expand Down
21 changes: 12 additions & 9 deletions synapse/handlers/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ def get_whois(self, user):

@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:
Expand All @@ -67,19 +67,22 @@ def get_users(self):
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

Expand Down
4 changes: 2 additions & 2 deletions synapse/rest/admin/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,12 @@
from synapse.rest.admin.users import (
AccountValidityRenewServlet,
DeactivateAccountRestServlet,
GetUsersPaginatedRestServlet,
ResetPasswordRestServlet,
SearchUsersRestServlet,
UserAdminServlet,
UserRegisterServlet,
UsersRestServlet,
UsersRestServletV2,
WhoisRestServlet,
)
from synapse.util.versionstring import get_version_string
Expand Down Expand Up @@ -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):
Expand All @@ -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)
Expand Down
83 changes: 29 additions & 54 deletions synapse/rest/admin/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
from synapse.http.servlet import (
RestServlet,
assert_params_in_dict,
parse_boolean,
parse_integer,
parse_json_object_from_request,
parse_string,
Expand Down Expand Up @@ -59,71 +60,45 @@ async def on_GET(self, request, user_id):
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


Expand Down
50 changes: 28 additions & 22 deletions synapse/storage/_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -1350,11 +1350,12 @@ def _attempt_to_invalidate_cache(self, cache_name, key):
def simple_select_list_paginate(
self,
table,
keyvalues,
orderby,
start,
limit,
retcols,
filters=None,
keyvalues=None,
order_direction="ASC",
desc="simple_select_list_paginate",
):
Expand All @@ -1365,6 +1366,9 @@ def simple_select_list_paginate(
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.
Expand All @@ -1380,11 +1384,12 @@ def simple_select_list_paginate(
desc,
self.simple_select_list_paginate_txn,
table,
keyvalues,
orderby,
start,
limit,
retcols,
filters=filters,
keyvalues=keyvalues,
order_direction=order_direction,
)

Expand All @@ -1393,39 +1398,52 @@ def simple_select_list_paginate_txn(
cls,
txn,
table,
keyvalues,
orderby,
start,
limit,
retcols,
filters=None,
keyvalues=None,
order_direction="ASC",
):
"""
Executes a SELECT query on the named table with start and limit,
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]]
"""
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),
Expand All @@ -1434,22 +1452,10 @@ def simple_select_list_paginate_txn(
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.
Expand Down
Loading

0 comments on commit 649b6bc

Please sign in to comment.