Skip to content

Commit 2bb1dd4

Browse files
awesome-manuelphil-flex
authored andcommitted
Return total number of users and profile attributes in admin users endpoint (matrix-org#6881)
Signed-off-by: Manuel Stahl <manuel.stahl@awesome-technologies.de>
1 parent 62a221d commit 2bb1dd4

6 files changed

Lines changed: 100 additions & 36 deletions

File tree

changelog.d/6881.misc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Return total number of users and profile attributes in admin users endpoint. Contributed by Awesome Technologies Innovationslabor GmbH.

docs/admin_api/user_admin_api.rst

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,17 +72,22 @@ It returns a JSON body like the following:
7272
"is_guest": 0,
7373
"admin": 0,
7474
"user_type": null,
75-
"deactivated": 0
75+
"deactivated": 0,
76+
"displayname": <User One>,
77+
"avatar_url": null
7678
}, {
7779
"name": "<user_id2>",
7880
"password_hash": "<password_hash2>",
7981
"is_guest": 0,
8082
"admin": 1,
8183
"user_type": null,
82-
"deactivated": 0
84+
"deactivated": 0,
85+
"displayname": <User Two>,
86+
"avatar_url": "<avatar_url>"
8387
}
8488
],
85-
"next_token": "100"
89+
"next_token": "100",
90+
"total": 200
8691
}
8792
8893

synapse/rest/admin/users.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -94,10 +94,10 @@ async def on_GET(self, request):
9494
guests = parse_boolean(request, "guests", default=True)
9595
deactivated = parse_boolean(request, "deactivated", default=False)
9696

97-
users = await self.store.get_users_paginate(
97+
users, total = await self.store.get_users_paginate(
9898
start, limit, user_id, guests, deactivated
9999
)
100-
ret = {"users": users}
100+
ret = {"users": users, "total": total}
101101
if len(users) >= limit:
102102
ret["next_token"] = str(start + len(users))
103103

@@ -199,7 +199,7 @@ async def on_PUT(self, request, user_id):
199199
user_id, threepid["medium"], threepid["address"], current_time
200200
)
201201

202-
if "avatar_url" in body:
202+
if "avatar_url" in body and type(body["avatar_url"]) == str:
203203
await self.profile_handler.set_avatar_url(
204204
target_user, requester, body["avatar_url"], True
205205
)
@@ -276,7 +276,7 @@ async def on_PUT(self, request, user_id):
276276
user_id, threepid["medium"], threepid["address"], current_time
277277
)
278278

279-
if "avatar_url" in body:
279+
if "avatar_url" in body and type(body["avatar_url"]) == str:
280280
await self.profile_handler.set_avatar_url(
281281
user_id, requester, body["avatar_url"], True
282282
)

synapse/storage/data_stores/main/__init__.py

Lines changed: 39 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -503,7 +503,8 @@ def get_users_paginate(
503503
self, start, limit, name=None, guests=True, deactivated=False
504504
):
505505
"""Function to retrieve a paginated list of users from
506-
users list. This will return a json list of users.
506+
users list. This will return a json list of users and the
507+
total number of users matching the filter criteria.
507508
508509
Args:
509510
start (int): start number to begin the query from
@@ -512,35 +513,44 @@ def get_users_paginate(
512513
guests (bool): whether to in include guest users
513514
deactivated (bool): whether to include deactivated users
514515
Returns:
515-
defer.Deferred: resolves to list[dict[str, Any]]
516+
defer.Deferred: resolves to list[dict[str, Any]], int
516517
"""
517-
name_filter = {}
518-
if name:
519-
name_filter["name"] = "%" + name + "%"
520-
521-
attr_filter = {}
522-
if not guests:
523-
attr_filter["is_guest"] = 0
524-
if not deactivated:
525-
attr_filter["deactivated"] = 0
526-
527-
return self.db.simple_select_list_paginate(
528-
desc="get_users_paginate",
529-
table="users",
530-
orderby="name",
531-
start=start,
532-
limit=limit,
533-
filters=name_filter,
534-
keyvalues=attr_filter,
535-
retcols=[
536-
"name",
537-
"password_hash",
538-
"is_guest",
539-
"admin",
540-
"user_type",
541-
"deactivated",
542-
],
543-
)
518+
519+
def get_users_paginate_txn(txn):
520+
filters = []
521+
args = []
522+
523+
if name:
524+
filters.append("name LIKE ?")
525+
args.append("%" + name + "%")
526+
527+
if not guests:
528+
filters.append("is_guest = 0")
529+
530+
if not deactivated:
531+
filters.append("deactivated = 0")
532+
533+
where_clause = "WHERE " + " AND ".join(filters) if len(filters) > 0 else ""
534+
535+
sql = "SELECT COUNT(*) as total_users FROM users %s" % (where_clause)
536+
txn.execute(sql, args)
537+
count = txn.fetchone()[0]
538+
539+
args = [self.hs.config.server_name] + args + [limit, start]
540+
sql = """
541+
SELECT name, user_type, is_guest, admin, deactivated, displayname, avatar_url
542+
FROM users as u
543+
LEFT JOIN profiles AS p ON u.name = '@' || p.user_id || ':' || ?
544+
{}
545+
ORDER BY u.name LIMIT ? OFFSET ?
546+
""".format(
547+
where_clause
548+
)
549+
txn.execute(sql, args)
550+
users = self.db.cursor_to_dict(txn)
551+
return users, count
552+
553+
return self.db.runInteraction("get_users_paginate_txn", get_users_paginate_txn)
544554

545555
def search_users(self, term):
546556
"""Function to search users list for one or more users with

tests/rest/admin/test_user.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,7 @@ def test_all_users(self):
360360

361361
self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"])
362362
self.assertEqual(3, len(channel.json_body["users"]))
363+
self.assertEqual(3, channel.json_body["total"])
363364

364365

365366
class UserRestTestCase(unittest.HomeserverTestCase):
@@ -434,6 +435,7 @@ def test_create_server_admin(self):
434435
"admin": True,
435436
"displayname": "Bob's name",
436437
"threepids": [{"medium": "email", "address": "bob@bob.bob"}],
438+
"avatar_url": None,
437439
}
438440
)
439441

tests/storage/test_main.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# -*- coding: utf-8 -*-
2+
# Copyright 2020 Awesome Technologies Innovationslabor GmbH
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
16+
17+
from twisted.internet import defer
18+
19+
from synapse.types import UserID
20+
21+
from tests import unittest
22+
from tests.utils import setup_test_homeserver
23+
24+
25+
class DataStoreTestCase(unittest.TestCase):
26+
@defer.inlineCallbacks
27+
def setUp(self):
28+
hs = yield setup_test_homeserver(self.addCleanup)
29+
30+
self.store = hs.get_datastore()
31+
32+
self.user = UserID.from_string("@abcde:test")
33+
self.displayname = "Frank"
34+
35+
@defer.inlineCallbacks
36+
def test_get_users_paginate(self):
37+
yield self.store.register_user(self.user.to_string(), "pass")
38+
yield self.store.create_profile(self.user.localpart)
39+
yield self.store.set_profile_displayname(self.user.localpart, self.displayname)
40+
41+
users, total = yield self.store.get_users_paginate(
42+
0, 10, name="bc", guests=False
43+
)
44+
45+
self.assertEquals(1, total)
46+
self.assertEquals(self.displayname, users.pop()["displayname"])

0 commit comments

Comments
 (0)