diff --git a/changelog.d/18252.misc b/changelog.d/18252.misc new file mode 100644 index 00000000000..6c76aca29c5 --- /dev/null +++ b/changelog.d/18252.misc @@ -0,0 +1 @@ +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 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 125ed8c4913..09baf8ce219 100644 --- a/synapse/rest/admin/devices.py +++ b/synapse/rest/admin/devices.py @@ -145,6 +145,17 @@ async def on_GET( devices = await self.device_worker_handler.get_devices_by_user( target_user.to_string() ) + + # mark the dehydrated device by adding a "dehydrated" flag + dehydrated_device_info = await self.device_worker_handler.get_dehydrated_device( + target_user.to_string() + ) + if dehydrated_device_info: + dehydrated_device_id = dehydrated_device_info[0] + for device in devices: + 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 a88c77bd19c..531162a6e98 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,22 @@ 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.assertTrue(d.get("dehydrated")) + found_dehydrated = True + else: + # Either the field is not present, or set to False + self.assertFalse(d.get("dehydrated")) + + self.assertTrue(found_dehydrated) class DeleteDevicesRestTestCase(unittest.HomeserverTestCase):