From b6b879abd2fce44635a0d67ff6de11b16f5f2fb1 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Fri, 14 Mar 2025 16:33:23 -0400 Subject: [PATCH 1/4] mark dehydrated devices in admin devices endpoint --- synapse/rest/admin/devices.py | 11 ++++++ tests/rest/admin/test_device.py | 63 +++++++++++++++++++++++++++++++-- 2 files changed, 71 insertions(+), 3 deletions(-) diff --git a/synapse/rest/admin/devices.py b/synapse/rest/admin/devices.py index 449b0669232..533e5d25fb0 100644 --- a/synapse/rest/admin/devices.py +++ b/synapse/rest/admin/devices.py @@ -142,6 +142,17 @@ async def on_GET( raise NotFoundError("Unknown user") devices = await self.device_handler.get_devices_by_user(target_user.to_string()) + + # mark the dehydrated device by adding a "dehydrated" flag + dehydrated_device_info = await self.device_handler.get_dehydrated_device( + target_user.to_string() + ) + if dehydrated_device_info: + dehydrated_device_id = dehydrated_device_info[0] + for device in devices: + if device["device_id"] == dehydrated_device_id: + device["dehydrated"] = True + return HTTPStatus.OK, {"devices": devices, "total": len(devices)} async def on_POST( diff --git a/tests/rest/admin/test_device.py b/tests/rest/admin/test_device.py index a88c77bd19c..57586a0e5af 100644 --- a/tests/rest/admin/test_device.py +++ b/tests/rest/admin/test_device.py @@ -27,7 +27,7 @@ import synapse.rest.admin from synapse.api.errors import Codes from synapse.handlers.device import DeviceHandler -from synapse.rest.client import login +from synapse.rest.client import devices, login from synapse.server import HomeServer from synapse.util import Clock @@ -299,6 +299,7 @@ def test_delete_device(self) -> None: class DevicesRestTestCase(unittest.HomeserverTestCase): servlets = [ synapse.rest.admin.register_servlets, + devices.register_servlets, login.register_servlets, ] @@ -390,15 +391,63 @@ def test_user_has_no_devices(self) -> None: self.assertEqual(0, channel.json_body["total"]) self.assertEqual(0, len(channel.json_body["devices"])) + @unittest.override_config( + {"experimental_features": {"msc2697_enabled": False, "msc3814_enabled": True}} + ) def test_get_devices(self) -> None: """ Tests that a normal lookup for devices is successfully """ # Create devices number_devices = 5 - for _ in range(number_devices): + # we create 2 fewer devices in the loop, because we will create another + # login after the loop, and we will create a dehydrated device + for _ in range(number_devices - 2): self.login("user", "pass") + other_user_token = self.login("user", "pass") + dehydrated_device_url = ( + "/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device" + ) + content = { + "device_data": { + "algorithm": "m.dehydration.v1.olm", + }, + "device_id": "dehydrated_device", + "initial_device_display_name": "foo bar", + "device_keys": { + "user_id": "@user:test", + "device_id": "dehydrated_device", + "valid_until_ts": "80", + "algorithms": [ + "m.olm.curve25519-aes-sha2", + ], + "keys": { + ":": "", + }, + "signatures": { + "@user:test": {":": ""} + }, + }, + "fallback_keys": { + "alg1:device1": "f4llb4ckk3y", + "signed_:": { + "fallback": "true", + "key": "f4llb4ckk3y", + "signatures": { + "@user:test": {":": ""} + }, + }, + }, + "one_time_keys": {"alg1:k1": "0net1m3k3y"}, + } + self.make_request( + "PUT", + dehydrated_device_url, + access_token=other_user_token, + content=content, + ) + # Get devices channel = self.make_request( "GET", @@ -410,13 +459,21 @@ def test_get_devices(self) -> None: self.assertEqual(number_devices, channel.json_body["total"]) self.assertEqual(number_devices, len(channel.json_body["devices"])) self.assertEqual(self.other_user, channel.json_body["devices"][0]["user_id"]) - # Check that all fields are available + # Check that all fields are available, and that the dehydrated device is marked as dehydrated + found_dehydrated = False for d in channel.json_body["devices"]: self.assertIn("user_id", d) self.assertIn("device_id", d) self.assertIn("display_name", d) self.assertIn("last_seen_ip", d) self.assertIn("last_seen_ts", d) + if d["device_id"] == "dehydrated_device": + self.assertEqual(True, d.get("dehydrated")) + found_dehydrated = True + else: + self.assertNotIn("dehydrated", d) + + self.assertEqual(True, found_dehydrated) class DeleteDevicesRestTestCase(unittest.HomeserverTestCase): From 9deca6d87c3bcec8049e9bf654cbb80085199ee6 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Tue, 18 Mar 2025 15:41:00 -0400 Subject: [PATCH 2/4] add changelog --- changelog.d/18252.misc | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/18252.misc diff --git a/changelog.d/18252.misc b/changelog.d/18252.misc new file mode 100644 index 00000000000..5e1962839dc --- /dev/null +++ b/changelog.d/18252.misc @@ -0,0 +1 @@ +Mark dehydrated devices in the admin get devices endpoint. \ No newline at end of file From e23297e07293aa6da2f70193c3567d7e87fdd57d Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Mon, 12 May 2025 17:17:24 -0400 Subject: [PATCH 3/4] Apply changes from review --- docs/admin_api/user_admin_api.md | 7 +++++-- synapse/rest/admin/devices.py | 4 ++-- tests/rest/admin/test_device.py | 7 ++++--- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/docs/admin_api/user_admin_api.md b/docs/admin_api/user_admin_api.md index 875876081f1..ee22b0db50b 100644 --- a/docs/admin_api/user_admin_api.md +++ b/docs/admin_api/user_admin_api.md @@ -954,7 +954,8 @@ A response body like the following is returned: "last_seen_ip": "1.2.3.4", "last_seen_user_agent": "Mozilla/5.0 (X11; Linux x86_64; rv:103.0) Gecko/20100101 Firefox/103.0", "last_seen_ts": 1474491775024, - "user_id": "" + "user_id": "", + "dehydrated": false }, { "device_id": "AUIECTSRND", @@ -962,7 +963,8 @@ A response body like the following is returned: "last_seen_ip": "1.2.3.5", "last_seen_user_agent": "Mozilla/5.0 (X11; Linux x86_64; rv:103.0) Gecko/20100101 Firefox/103.0", "last_seen_ts": 1474491775025, - "user_id": "" + "user_id": "", + "dehydrated": false } ], "total": 2 @@ -992,6 +994,7 @@ The following fields are returned in the JSON response body: - `last_seen_ts` - The timestamp (in milliseconds since the unix epoch) when this devices was last seen. (May be a few minutes out of date, for efficiency reasons). - `user_id` - Owner of device. + - `dehydrated` - Whether the device is a dehydrated device. - `total` - Total number of user's devices. diff --git a/synapse/rest/admin/devices.py b/synapse/rest/admin/devices.py index 533e5d25fb0..8fc408a3256 100644 --- a/synapse/rest/admin/devices.py +++ b/synapse/rest/admin/devices.py @@ -150,8 +150,8 @@ async def on_GET( if dehydrated_device_info: dehydrated_device_id = dehydrated_device_info[0] for device in devices: - if device["device_id"] == dehydrated_device_id: - device["dehydrated"] = True + is_dehydrated = device["device_id"] == dehydrated_device_id + device["dehydrated"] = is_dehydrated return HTTPStatus.OK, {"devices": devices, "total": len(devices)} diff --git a/tests/rest/admin/test_device.py b/tests/rest/admin/test_device.py index 57586a0e5af..531162a6e98 100644 --- a/tests/rest/admin/test_device.py +++ b/tests/rest/admin/test_device.py @@ -468,12 +468,13 @@ def test_get_devices(self) -> None: self.assertIn("last_seen_ip", d) self.assertIn("last_seen_ts", d) if d["device_id"] == "dehydrated_device": - self.assertEqual(True, d.get("dehydrated")) + self.assertTrue(d.get("dehydrated")) found_dehydrated = True else: - self.assertNotIn("dehydrated", d) + # Either the field is not present, or set to False + self.assertFalse(d.get("dehydrated")) - self.assertEqual(True, found_dehydrated) + self.assertTrue(found_dehydrated) class DeleteDevicesRestTestCase(unittest.HomeserverTestCase): From 8d6712bc52bd8162db1cf5bd4f43219bf2aae4ba Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Wed, 28 May 2025 12:20:13 +0100 Subject: [PATCH 4/4] Link to endpoint in newsfile --- changelog.d/18252.misc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.d/18252.misc b/changelog.d/18252.misc index 5e1962839dc..6c76aca29c5 100644 --- a/changelog.d/18252.misc +++ b/changelog.d/18252.misc @@ -1 +1 @@ -Mark dehydrated devices in the admin get devices endpoint. \ No newline at end of file +Mark dehydrated devices in the [List All User Devices Admin API](https://element-hq.github.io/synapse/latest/admin_api/user_admin_api.html#list-all-devices). \ No newline at end of file