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

Commit 18862f2

Browse files
Remove the 'password_hash' from the Users Admin API endpoint response dictionary (#11576)
1 parent 904bb04 commit 18862f2

File tree

5 files changed

+86
-43
lines changed

5 files changed

+86
-43
lines changed

changelog.d/11576.feature

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Remove the `"password_hash"` field from the response dictionaries of the [Users Admin API](https://matrix-org.github.io/synapse/latest/admin_api/user_admin_api.html).

docs/admin_api/user_admin_api.md

+5-4
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,10 @@ server admin: [Admin API](../usage/administration/admin_api)
1515

1616
It returns a JSON body like the following:
1717

18-
```json
18+
```jsonc
1919
{
20-
"displayname": "User",
20+
"name": "@user:example.com",
21+
"displayname": "User", // can be null if not set
2122
"threepids": [
2223
{
2324
"medium": "email",
@@ -32,11 +33,11 @@ It returns a JSON body like the following:
3233
"validated_at": 1586458409743
3334
}
3435
],
35-
"avatar_url": "<avatar_url>",
36+
"avatar_url": "<avatar_url>", // can be null if not set
37+
"is_guest": 0,
3638
"admin": 0,
3739
"deactivated": 0,
3840
"shadow_banned": 0,
39-
"password_hash": "$2b$12$p9B4GkqYdRTPGD",
4041
"creation_ts": 1560432506,
4142
"appservice_id": null,
4243
"consent_server_notice_sent": null,

synapse/handlers/admin.py

+41-15
Original file line numberDiff line numberDiff line change
@@ -55,21 +55,47 @@ async def get_whois(self, user: UserID) -> JsonDict:
5555

5656
async def get_user(self, user: UserID) -> Optional[JsonDict]:
5757
"""Function to get user details"""
58-
ret = await self.store.get_user_by_id(user.to_string())
59-
if ret:
60-
profile = await self.store.get_profileinfo(user.localpart)
61-
threepids = await self.store.user_get_threepids(user.to_string())
62-
external_ids = [
63-
({"auth_provider": auth_provider, "external_id": external_id})
64-
for auth_provider, external_id in await self.store.get_external_ids_by_user(
65-
user.to_string()
66-
)
67-
]
68-
ret["displayname"] = profile.display_name
69-
ret["avatar_url"] = profile.avatar_url
70-
ret["threepids"] = threepids
71-
ret["external_ids"] = external_ids
72-
return ret
58+
user_info_dict = await self.store.get_user_by_id(user.to_string())
59+
if user_info_dict is None:
60+
return None
61+
62+
# Restrict returned information to a known set of fields. This prevents additional
63+
# fields added to get_user_by_id from modifying Synapse's external API surface.
64+
user_info_to_return = {
65+
"name",
66+
"admin",
67+
"deactivated",
68+
"shadow_banned",
69+
"creation_ts",
70+
"appservice_id",
71+
"consent_server_notice_sent",
72+
"consent_version",
73+
"user_type",
74+
"is_guest",
75+
}
76+
77+
# Restrict returned keys to a known set.
78+
user_info_dict = {
79+
key: value
80+
for key, value in user_info_dict.items()
81+
if key in user_info_to_return
82+
}
83+
84+
# Add additional user metadata
85+
profile = await self.store.get_profileinfo(user.localpart)
86+
threepids = await self.store.user_get_threepids(user.to_string())
87+
external_ids = [
88+
({"auth_provider": auth_provider, "external_id": external_id})
89+
for auth_provider, external_id in await self.store.get_external_ids_by_user(
90+
user.to_string()
91+
)
92+
]
93+
user_info_dict["displayname"] = profile.display_name
94+
user_info_dict["avatar_url"] = profile.avatar_url
95+
user_info_dict["threepids"] = threepids
96+
user_info_dict["external_ids"] = external_ids
97+
98+
return user_info_dict
7399

74100
async def export_user_data(self, user_id: str, writer: "ExfiltrationWriter") -> Any:
75101
"""Write all data we have on the user to the given writer.

synapse/rest/admin/users.py

+6-7
Original file line numberDiff line numberDiff line change
@@ -173,12 +173,11 @@ async def on_GET(
173173
if not self.hs.is_mine(target_user):
174174
raise SynapseError(HTTPStatus.BAD_REQUEST, "Can only look up local users")
175175

176-
ret = await self.admin_handler.get_user(target_user)
177-
178-
if not ret:
176+
user_info_dict = await self.admin_handler.get_user(target_user)
177+
if not user_info_dict:
179178
raise NotFoundError("User not found")
180179

181-
return HTTPStatus.OK, ret
180+
return HTTPStatus.OK, user_info_dict
182181

183182
async def on_PUT(
184183
self, request: SynapseRequest, user_id: str
@@ -399,10 +398,10 @@ async def on_PUT(
399398
target_user, requester, body["avatar_url"], True
400399
)
401400

402-
user = await self.admin_handler.get_user(target_user)
403-
assert user is not None
401+
user_info_dict = await self.admin_handler.get_user(target_user)
402+
assert user_info_dict is not None
404403

405-
return 201, user
404+
return HTTPStatus.CREATED, user_info_dict
406405

407406

408407
class UserRegisterServlet(RestServlet):

tests/rest/admin/test_user.py

+33-17
Original file line numberDiff line numberDiff line change
@@ -1181,14 +1181,15 @@ def prepare(self, reactor, clock, hs):
11811181
self.other_user, device_id=None, valid_until_ms=None
11821182
)
11831183
)
1184+
11841185
self.url_prefix = "/_synapse/admin/v2/users/%s"
11851186
self.url_other_user = self.url_prefix % self.other_user
11861187

11871188
def test_requester_is_no_admin(self):
11881189
"""
11891190
If the user is not a server admin, an error is returned.
11901191
"""
1191-
url = "/_synapse/admin/v2/users/@bob:test"
1192+
url = self.url_prefix % "@bob:test"
11921193

11931194
channel = self.make_request(
11941195
"GET",
@@ -1216,7 +1217,7 @@ def test_user_does_not_exist(self):
12161217

12171218
channel = self.make_request(
12181219
"GET",
1219-
"/_synapse/admin/v2/users/@unknown_person:test",
1220+
self.url_prefix % "@unknown_person:test",
12201221
access_token=self.admin_user_tok,
12211222
)
12221223

@@ -1337,7 +1338,7 @@ def test_create_server_admin(self):
13371338
"""
13381339
Check that a new admin user is created successfully.
13391340
"""
1340-
url = "/_synapse/admin/v2/users/@bob:test"
1341+
url = self.url_prefix % "@bob:test"
13411342

13421343
# Create user (server admin)
13431344
body = {
@@ -1386,7 +1387,7 @@ def test_create_user(self):
13861387
"""
13871388
Check that a new regular user is created successfully.
13881389
"""
1389-
url = "/_synapse/admin/v2/users/@bob:test"
1390+
url = self.url_prefix % "@bob:test"
13901391

13911392
# Create user
13921393
body = {
@@ -1478,7 +1479,7 @@ def test_create_user_mau_limit_reached_active_admin(self):
14781479
)
14791480

14801481
# Register new user with admin API
1481-
url = "/_synapse/admin/v2/users/@bob:test"
1482+
url = self.url_prefix % "@bob:test"
14821483

14831484
# Create user
14841485
channel = self.make_request(
@@ -1515,7 +1516,7 @@ def test_create_user_mau_limit_reached_passive_admin(self):
15151516
)
15161517

15171518
# Register new user with admin API
1518-
url = "/_synapse/admin/v2/users/@bob:test"
1519+
url = self.url_prefix % "@bob:test"
15191520

15201521
# Create user
15211522
channel = self.make_request(
@@ -1545,7 +1546,7 @@ def test_create_user_email_notif_for_new_users(self):
15451546
Check that a new regular user is created successfully and
15461547
got an email pusher.
15471548
"""
1548-
url = "/_synapse/admin/v2/users/@bob:test"
1549+
url = self.url_prefix % "@bob:test"
15491550

15501551
# Create user
15511552
body = {
@@ -1588,7 +1589,7 @@ def test_create_user_email_no_notif_for_new_users(self):
15881589
Check that a new regular user is created successfully and
15891590
got not an email pusher.
15901591
"""
1591-
url = "/_synapse/admin/v2/users/@bob:test"
1592+
url = self.url_prefix % "@bob:test"
15921593

15931594
# Create user
15941595
body = {
@@ -2085,10 +2086,13 @@ def test_deactivate_user(self):
20852086
self.assertEqual(HTTPStatus.OK, channel.code, msg=channel.json_body)
20862087
self.assertEqual("@user:test", channel.json_body["name"])
20872088
self.assertTrue(channel.json_body["deactivated"])
2088-
self.assertIsNone(channel.json_body["password_hash"])
20892089
self.assertEqual(0, len(channel.json_body["threepids"]))
20902090
self.assertEqual("mxc://servername/mediaid", channel.json_body["avatar_url"])
20912091
self.assertEqual("User", channel.json_body["displayname"])
2092+
2093+
# This key was removed intentionally. Ensure it is not accidentally re-included.
2094+
self.assertNotIn("password_hash", channel.json_body)
2095+
20922096
# the user is deactivated, the threepid will be deleted
20932097

20942098
# Get user
@@ -2101,11 +2105,13 @@ def test_deactivate_user(self):
21012105
self.assertEqual(HTTPStatus.OK, channel.code, msg=channel.json_body)
21022106
self.assertEqual("@user:test", channel.json_body["name"])
21032107
self.assertTrue(channel.json_body["deactivated"])
2104-
self.assertIsNone(channel.json_body["password_hash"])
21052108
self.assertEqual(0, len(channel.json_body["threepids"]))
21062109
self.assertEqual("mxc://servername/mediaid", channel.json_body["avatar_url"])
21072110
self.assertEqual("User", channel.json_body["displayname"])
21082111

2112+
# This key was removed intentionally. Ensure it is not accidentally re-included.
2113+
self.assertNotIn("password_hash", channel.json_body)
2114+
21092115
@override_config({"user_directory": {"enabled": True, "search_all_users": True}})
21102116
def test_change_name_deactivate_user_user_directory(self):
21112117
"""
@@ -2177,9 +2183,11 @@ def test_reactivate_user(self):
21772183
self.assertEqual(HTTPStatus.OK, channel.code, msg=channel.json_body)
21782184
self.assertEqual("@user:test", channel.json_body["name"])
21792185
self.assertFalse(channel.json_body["deactivated"])
2180-
self.assertIsNotNone(channel.json_body["password_hash"])
21812186
self._is_erased("@user:test", False)
21822187

2188+
# This key was removed intentionally. Ensure it is not accidentally re-included.
2189+
self.assertNotIn("password_hash", channel.json_body)
2190+
21832191
@override_config({"password_config": {"localdb_enabled": False}})
21842192
def test_reactivate_user_localdb_disabled(self):
21852193
"""
@@ -2209,9 +2217,11 @@ def test_reactivate_user_localdb_disabled(self):
22092217
self.assertEqual(HTTPStatus.OK, channel.code, msg=channel.json_body)
22102218
self.assertEqual("@user:test", channel.json_body["name"])
22112219
self.assertFalse(channel.json_body["deactivated"])
2212-
self.assertIsNone(channel.json_body["password_hash"])
22132220
self._is_erased("@user:test", False)
22142221

2222+
# This key was removed intentionally. Ensure it is not accidentally re-included.
2223+
self.assertNotIn("password_hash", channel.json_body)
2224+
22152225
@override_config({"password_config": {"enabled": False}})
22162226
def test_reactivate_user_password_disabled(self):
22172227
"""
@@ -2241,9 +2251,11 @@ def test_reactivate_user_password_disabled(self):
22412251
self.assertEqual(HTTPStatus.OK, channel.code, msg=channel.json_body)
22422252
self.assertEqual("@user:test", channel.json_body["name"])
22432253
self.assertFalse(channel.json_body["deactivated"])
2244-
self.assertIsNone(channel.json_body["password_hash"])
22452254
self._is_erased("@user:test", False)
22462255

2256+
# This key was removed intentionally. Ensure it is not accidentally re-included.
2257+
self.assertNotIn("password_hash", channel.json_body)
2258+
22472259
def test_set_user_as_admin(self):
22482260
"""
22492261
Test setting the admin flag on a user.
@@ -2328,7 +2340,7 @@ def test_accidental_deactivation_prevention(self):
23282340
Ensure an account can't accidentally be deactivated by using a str value
23292341
for the deactivated body parameter
23302342
"""
2331-
url = "/_synapse/admin/v2/users/@bob:test"
2343+
url = self.url_prefix % "@bob:test"
23322344

23332345
# Create user
23342346
channel = self.make_request(
@@ -2392,18 +2404,20 @@ def _deactivate_user(self, user_id: str) -> None:
23922404
# Deactivate the user.
23932405
channel = self.make_request(
23942406
"PUT",
2395-
"/_synapse/admin/v2/users/%s" % urllib.parse.quote(user_id),
2407+
self.url_prefix % urllib.parse.quote(user_id),
23962408
access_token=self.admin_user_tok,
23972409
content={"deactivated": True},
23982410
)
23992411
self.assertEqual(HTTPStatus.OK, channel.code, msg=channel.json_body)
24002412
self.assertTrue(channel.json_body["deactivated"])
2401-
self.assertIsNone(channel.json_body["password_hash"])
24022413
self._is_erased(user_id, False)
24032414
d = self.store.mark_user_erased(user_id)
24042415
self.assertIsNone(self.get_success(d))
24052416
self._is_erased(user_id, True)
24062417

2418+
# This key was removed intentionally. Ensure it is not accidentally re-included.
2419+
self.assertNotIn("password_hash", channel.json_body)
2420+
24072421
def _check_fields(self, content: JsonDict):
24082422
"""Checks that the expected user attributes are present in content
24092423
@@ -2416,13 +2430,15 @@ def _check_fields(self, content: JsonDict):
24162430
self.assertIn("admin", content)
24172431
self.assertIn("deactivated", content)
24182432
self.assertIn("shadow_banned", content)
2419-
self.assertIn("password_hash", content)
24202433
self.assertIn("creation_ts", content)
24212434
self.assertIn("appservice_id", content)
24222435
self.assertIn("consent_server_notice_sent", content)
24232436
self.assertIn("consent_version", content)
24242437
self.assertIn("external_ids", content)
24252438

2439+
# This key was removed intentionally. Ensure it is not accidentally re-included.
2440+
self.assertNotIn("password_hash", content)
2441+
24262442

24272443
class UserMembershipRestTestCase(unittest.HomeserverTestCase):
24282444

0 commit comments

Comments
 (0)