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

Commit 1433b5d

Browse files
tadzikDMRobertson
andauthored
Show erasure status when listing users in the Admin API (#14205)
* Show erasure status when listing users in the Admin API * Use USING when joining erased_users * Add changelog entry * Revert "Use USING when joining erased_users" This reverts commit 30bd2bf. * Make the erased check work on postgres * Add a testcase for showing erased user status * Appease the style linter * Explicitly convert `erased` to bool to make SQLite consistent with Postgres This also adds us an easy way in to fix the other accidentally integered columns. * Move erasure status test to UsersListTestCase * Include user erased status when fetching user info via the admin API * Document the erase status in user_admin_api * Appease the linter and mypy * Signpost comments in tests Co-authored-by: Tadeusz Sośnierz <[email protected]> Co-authored-by: David Robertson <[email protected]>
1 parent fab495a commit 1433b5d

File tree

5 files changed

+51
-3
lines changed

5 files changed

+51
-3
lines changed

changelog.d/14205.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Show erasure status when listing users in the Admin API.

docs/admin_api/user_admin_api.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ It returns a JSON body like the following:
3737
"is_guest": 0,
3838
"admin": 0,
3939
"deactivated": 0,
40+
"erased": false,
4041
"shadow_banned": 0,
4142
"creation_ts": 1560432506,
4243
"appservice_id": null,
@@ -167,6 +168,7 @@ A response body like the following is returned:
167168
"admin": 0,
168169
"user_type": null,
169170
"deactivated": 0,
171+
"erased": false,
170172
"shadow_banned": 0,
171173
"displayname": "<User One>",
172174
"avatar_url": null,
@@ -177,6 +179,7 @@ A response body like the following is returned:
177179
"admin": 1,
178180
"user_type": null,
179181
"deactivated": 0,
182+
"erased": false,
180183
"shadow_banned": 0,
181184
"displayname": "<User Two>",
182185
"avatar_url": "<avatar_url>",
@@ -247,6 +250,7 @@ The following fields are returned in the JSON response body:
247250
- `user_type` - string - Type of the user. Normal users are type `None`.
248251
This allows user type specific behaviour. There are also types `support` and `bot`.
249252
- `deactivated` - bool - Status if that user has been marked as deactivated.
253+
- `erased` - bool - Status if that user has been marked as erased.
250254
- `shadow_banned` - bool - Status if that user has been marked as shadow banned.
251255
- `displayname` - string - The user's display name if they have set one.
252256
- `avatar_url` - string - The user's avatar URL if they have set one.

synapse/handlers/admin.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ async def get_user(self, user: UserID) -> Optional[JsonDict]:
100100
user_info_dict["avatar_url"] = profile.avatar_url
101101
user_info_dict["threepids"] = threepids
102102
user_info_dict["external_ids"] = external_ids
103+
user_info_dict["erased"] = await self.store.is_user_erased(user.to_string())
103104

104105
return user_info_dict
105106

synapse/storage/databases/main/__init__.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,7 @@ async def get_users_paginate(
201201
name: Optional[str] = None,
202202
guests: bool = True,
203203
deactivated: bool = False,
204-
order_by: str = UserSortOrder.USER_ID.value,
204+
order_by: str = UserSortOrder.NAME.value,
205205
direction: str = "f",
206206
approved: bool = True,
207207
) -> Tuple[List[JsonDict], int]:
@@ -261,6 +261,7 @@ def get_users_paginate_txn(
261261
sql_base = f"""
262262
FROM users as u
263263
LEFT JOIN profiles AS p ON u.name = '@' || p.user_id || ':' || ?
264+
LEFT JOIN erased_users AS eu ON u.name = eu.user_id
264265
{where_clause}
265266
"""
266267
sql = "SELECT COUNT(*) as total_users " + sql_base
@@ -269,14 +270,22 @@ def get_users_paginate_txn(
269270

270271
sql = f"""
271272
SELECT name, user_type, is_guest, admin, deactivated, shadow_banned,
272-
displayname, avatar_url, creation_ts * 1000 as creation_ts, approved
273+
displayname, avatar_url, creation_ts * 1000 as creation_ts, approved,
274+
eu.user_id is not null as erased
273275
{sql_base}
274276
ORDER BY {order_by_column} {order}, u.name ASC
275277
LIMIT ? OFFSET ?
276278
"""
277279
args += [limit, start]
278280
txn.execute(sql, args)
279281
users = self.db_pool.cursor_to_dict(txn)
282+
283+
# some of those boolean values are returned as integers when we're on SQLite
284+
columns_to_boolify = ["erased"]
285+
for user in users:
286+
for column in columns_to_boolify:
287+
user[column] = bool(user[column])
288+
280289
return users, count
281290

282291
return await self.db_pool.runInteraction(

tests/rest/admin/test_user.py

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
from synapse.rest.client import devices, login, logout, profile, register, room, sync
3232
from synapse.rest.media.v1.filepath import MediaFilePaths
3333
from synapse.server import HomeServer
34-
from synapse.types import JsonDict, UserID
34+
from synapse.types import JsonDict, UserID, create_requester
3535
from synapse.util import Clock
3636

3737
from tests import unittest
@@ -924,6 +924,36 @@ def test_filter_out_approved(self) -> None:
924924
self.assertEqual(1, len(non_admin_user_ids), non_admin_user_ids)
925925
self.assertEqual(not_approved_user, non_admin_user_ids[0])
926926

927+
def test_erasure_status(self) -> None:
928+
# Create a new user.
929+
user_id = self.register_user("eraseme", "eraseme")
930+
931+
# They should appear in the list users API, marked as not erased.
932+
channel = self.make_request(
933+
"GET",
934+
self.url + "?deactivated=true",
935+
access_token=self.admin_user_tok,
936+
)
937+
users = {user["name"]: user for user in channel.json_body["users"]}
938+
self.assertIs(users[user_id]["erased"], False)
939+
940+
# Deactivate that user, requesting erasure.
941+
deactivate_account_handler = self.hs.get_deactivate_account_handler()
942+
self.get_success(
943+
deactivate_account_handler.deactivate_account(
944+
user_id, erase_data=True, requester=create_requester(user_id)
945+
)
946+
)
947+
948+
# Repeat the list users query. They should now be marked as erased.
949+
channel = self.make_request(
950+
"GET",
951+
self.url + "?deactivated=true",
952+
access_token=self.admin_user_tok,
953+
)
954+
users = {user["name"]: user for user in channel.json_body["users"]}
955+
self.assertIs(users[user_id]["erased"], True)
956+
927957
def _order_test(
928958
self,
929959
expected_user_list: List[str],
@@ -1195,6 +1225,7 @@ def test_deactivate_user_erase_true(self) -> None:
11951225
self.assertEqual("[email protected]", channel.json_body["threepids"][0]["address"])
11961226
self.assertEqual("mxc://servername/mediaid", channel.json_body["avatar_url"])
11971227
self.assertEqual("User1", channel.json_body["displayname"])
1228+
self.assertFalse(channel.json_body["erased"])
11981229

11991230
# Deactivate and erase user
12001231
channel = self.make_request(
@@ -1219,6 +1250,7 @@ def test_deactivate_user_erase_true(self) -> None:
12191250
self.assertEqual(0, len(channel.json_body["threepids"]))
12201251
self.assertIsNone(channel.json_body["avatar_url"])
12211252
self.assertIsNone(channel.json_body["displayname"])
1253+
self.assertTrue(channel.json_body["erased"])
12221254

12231255
self._is_erased("@user:test", True)
12241256

@@ -2757,6 +2789,7 @@ def _check_fields(self, content: JsonDict) -> None:
27572789
self.assertIn("avatar_url", content)
27582790
self.assertIn("admin", content)
27592791
self.assertIn("deactivated", content)
2792+
self.assertIn("erased", content)
27602793
self.assertIn("shadow_banned", content)
27612794
self.assertIn("creation_ts", content)
27622795
self.assertIn("appservice_id", content)

0 commit comments

Comments
 (0)