From f7e09335b0e95df27e73059f187695074bc41122 Mon Sep 17 00:00:00 2001 From: "H. Shay" Date: Sun, 23 Jul 2023 16:23:57 -0700 Subject: [PATCH] apply patch --- synapse/handlers/device.py | 4 +- synapse/handlers/devicemessage.py | 18 +-- synapse/rest/client/devices.py | 178 +++++++++++++++++++++++++++++- tests/handlers/test_device.py | 25 +---- tests/rest/client/test_devices.py | 68 +++++++----- 5 files changed, 233 insertions(+), 60 deletions(-) diff --git a/synapse/handlers/device.py b/synapse/handlers/device.py index 5d12a39e26a4..225fd75bc267 100644 --- a/synapse/handlers/device.py +++ b/synapse/handlers/device.py @@ -653,6 +653,7 @@ async def notify_user_signature_update( async def store_dehydrated_device( self, user_id: str, + device_id: Optional[str], device_data: JsonDict, initial_device_display_name: Optional[str] = None, ) -> str: @@ -661,6 +662,7 @@ async def store_dehydrated_device( Args: user_id: the user that we are storing the device for + device_id: device id supplied by client device_data: the dehydrated device information initial_device_display_name: The display name to use for the device Returns: @@ -668,7 +670,7 @@ async def store_dehydrated_device( """ device_id = await self.check_device_registered( user_id, - None, + device_id, initial_device_display_name, ) old_device_id = await self.store.store_dehydrated_device( diff --git a/synapse/handlers/devicemessage.py b/synapse/handlers/devicemessage.py index 92d4b7b206c4..15e94a03cbe7 100644 --- a/synapse/handlers/devicemessage.py +++ b/synapse/handlers/devicemessage.py @@ -317,7 +317,7 @@ async def get_events_for_dehydrated_device( ) -> JsonDict: """Fetches up to `limit` events sent to `device_id` starting from `since_token` and returns the new since token. If there are no more messages, returns an empty - array and deletes the dehydrated device associated with the user/device_id. + array. Args: requester: the user requesting the messages @@ -373,7 +373,11 @@ async def get_events_for_dehydrated_device( user_id, device_id, since_stream_id ) logger.debug( - "Deleted %d to-device messages up to %d", deleted, since_stream_id + "Deleted %d to-device messages up to %d for user_id %s device_id %s", + deleted, + since_stream_id, + user_id, + device_id, ) to_token = self.event_sources.get_current_token().to_device_key @@ -389,20 +393,16 @@ async def get_events_for_dehydrated_device( set_tag(SynapseTags.TO_DEVICE_EDU_ID, message_id) logger.debug( - "Returning %d to-device messages between %d and %d (current token: %d) for dehydrated device %s", + "Returning %d to-device messages between %d and %d (current token: %d) for " + "dehydrated device %s, user_id %s", len(messages), since_stream_id, stream_id, to_token, device_id, + user_id, ) - if messages == []: - # we've fetched all the messages, delete the dehydrated device - await self.store.remove_dehydrated_device( - requester.user.to_string(), device_id - ) - return { "events": messages, "next_batch": f"d{stream_id}", diff --git a/synapse/rest/client/devices.py b/synapse/rest/client/devices.py index 44cf76b0af7e..690d2ec406fc 100644 --- a/synapse/rest/client/devices.py +++ b/synapse/rest/client/devices.py @@ -14,12 +14,13 @@ # limitations under the License. import logging +from http import HTTPStatus from typing import TYPE_CHECKING, List, Optional, Tuple from pydantic import Extra, StrictStr from synapse.api import errors -from synapse.api.errors import NotFoundError, UnrecognizedRequestError +from synapse.api.errors import NotFoundError, SynapseError, UnrecognizedRequestError from synapse.handlers.device import DeviceHandler from synapse.http.server import HttpServer from synapse.http.servlet import ( @@ -28,6 +29,7 @@ parse_integer, ) from synapse.http.site import SynapseRequest +from synapse.replication.http.devices import ReplicationUploadKeysForUserRestServlet from synapse.rest.client._base import client_patterns, interactive_auth_handler from synapse.rest.client.models import AuthenticationData from synapse.rest.models import RequestBodyModel @@ -230,7 +232,7 @@ class Config: class DehydratedDeviceServlet(RestServlet): """Retrieve or store a dehydrated device. - Implements either MSC2697 and MSC3814. + Implements either MSC2697 or MSC3814. GET /org.matrix.msc2697.v2/dehydrated_device @@ -301,6 +303,7 @@ async def on_PUT(self, request: SynapseRequest) -> Tuple[int, JsonDict]: device_id = await self.device_handler.store_dehydrated_device( requester.user.to_string(), + None, submission.device_data.dict(), submission.initial_device_display_name, ) @@ -390,6 +393,175 @@ async def on_POST( return 200, msgs +class DehydratedDeviceV2Servlet(RestServlet): + """Upload, retrieve, or delete a dehydrated device. + + GET /org.matrix.msc3814.v1/dehydrated_device + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "device_id": "dehydrated_device_id", + "device_data": { + "algorithm": "org.matrix.msc2697.v1.dehydration.v1.olm", + "account": "dehydrated_device" + } + } + + PUT /org.matrix.msc3814.v1/dehydrated_device + Content-Type: application/json + + { + "device_id": "dehydrated_device_id", + "device_data": { + "algorithm": "org.matrix.msc2697.v1.dehydration.v1.olm", + "account": "dehydrated_device" + }, + "device_keys": { + "user_id": "", + "device_id": "", + "valid_until_ts": , + "algorithms": [ + "m.olm.curve25519-aes-sha2", + ] + "keys": { + ":": "", + }, + "signatures:" { + "" { + ":": "" + } + } + }, + "fallback_keys": { + ":": "", + "signed_:": { + "fallback": true, + "key": "", + "signatures": { + "": { + ":": "" + } + } + } + } + "one_time_keys": { + ":": "" + }, + + } + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "device_id": "dehydrated_device_id" + } + + DELETE /org.matrix.msc3814.v1/dehydrated_device + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "device_id": "dehydrated_device_id", + } + """ + + PATTERNS = [ + *client_patterns("/org.matrix.msc3814.v1/dehydrated_device$", releases=()), + ] + + def __init__(self, hs: "HomeServer"): + super().__init__() + self.hs = hs + self.auth = hs.get_auth() + handler = hs.get_device_handler() + assert isinstance(handler, DeviceHandler) + self.e2e_keys_handler = hs.get_e2e_keys_handler() + self.device_handler = handler + + if hs.config.worker.worker_app is None: + # if main process + self.key_uploader = self.e2e_keys_handler.upload_keys_for_user + else: + # then a worker + self.key_uploader = ReplicationUploadKeysForUserRestServlet.make_client(hs) + + async def on_GET(self, request: SynapseRequest) -> Tuple[int, JsonDict]: + requester = await self.auth.get_user_by_req(request) + + dehydrated_device = await self.device_handler.get_dehydrated_device( + requester.user.to_string() + ) + + if dehydrated_device is not None: + (device_id, device_data) = dehydrated_device + result = {"device_id": device_id, "device_data": device_data} + return 200, result + else: + raise errors.NotFoundError("No dehydrated device available") + + async def on_DELETE(self, request: SynapseRequest) -> Tuple[int, JsonDict]: + requester = await self.auth.get_user_by_req(request) + + dehydrated_device = await self.device_handler.get_dehydrated_device( + requester.user.to_string() + ) + + if dehydrated_device is not None: + (device_id, device_data) = dehydrated_device + + result = await self.device_handler.rehydrate_device( + requester.user.to_string(), + self.auth.get_access_token_from_request(request), + device_id, + ) + + result = {"device_id": device_id} + + return 200, result + else: + raise errors.NotFoundError("No dehydrated device available") + + class PutBody(RequestBodyModel): + device_data: DehydratedDeviceDataModel + device_id: StrictStr + initial_device_display_name: Optional[StrictStr] + + class Config: + extra = Extra.allow + + async def on_PUT(self, request: SynapseRequest) -> Tuple[int, JsonDict]: + submission = parse_and_validate_json_object_from_request(request, self.PutBody) + requester = await self.auth.get_user_by_req(request) + user_id = requester.user.to_string() + + device_info = submission.dict() + if "device_keys" not in device_info.keys(): + raise SynapseError( + HTTPStatus.BAD_REQUEST, + "Device key(s) not found, these must be provided.", + ) + + # TODO: Those two operations, creating a device and storing the + # device's keys should be atomic. + device_id = await self.device_handler.store_dehydrated_device( + requester.user.to_string(), + submission.device_id, + submission.device_data.dict(), + submission.initial_device_display_name, + ) + + # TODO: Do we need to do something with the result here? + await self.key_uploader( + user_id=user_id, device_id=submission.device_id, keys=submission.dict() + ) + + return 200, {"device_id": device_id} + + def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None: if ( hs.config.worker.worker_app is None @@ -404,5 +576,5 @@ def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None: DehydratedDeviceServlet(hs, msc2697=True).register(http_server) ClaimDehydratedDeviceServlet(hs).register(http_server) if hs.config.experimental.msc3814_enabled: - DehydratedDeviceServlet(hs, msc2697=False).register(http_server) + DehydratedDeviceV2Servlet(hs).register(http_server) DehydratedDeviceEventsServlet(hs).register(http_server) diff --git a/tests/handlers/test_device.py b/tests/handlers/test_device.py index 1be366702b51..56c31217462f 100644 --- a/tests/handlers/test_device.py +++ b/tests/handlers/test_device.py @@ -430,6 +430,7 @@ def test_dehydrate_and_rehydrate_device(self) -> None: stored_dehydrated_device_id = self.get_success( self.handler.store_dehydrated_device( user_id=user_id, + device_id=None, device_data={"device_data": {"foo": "bar"}}, initial_device_display_name="dehydrated device", ) @@ -506,6 +507,7 @@ def test_dehydrate_v2_and_fetch_events(self) -> None: stored_dehydrated_device_id = self.get_success( self.handler.store_dehydrated_device( user_id=user_id, + device_id=None, device_data={"device_data": {"foo": "bar"}}, initial_device_display_name="dehydrated device", ) @@ -530,7 +532,7 @@ def test_dehydrate_v2_and_fetch_events(self) -> None: requester = create_requester(user_id, device_id=device_id) - # Fetching messages for a non existing device should return an error + # Fetching messages for a non-existing device should return an error self.get_failure( self.message_handler.get_events_for_dehydrated_device( requester=requester, @@ -565,7 +567,8 @@ def test_dehydrate_v2_and_fetch_events(self) -> None: self.assertEqual(len(res["events"]), 1) self.assertEqual(res["events"][0]["content"]["body"], "foo") - # Fetch the message of the dehydrated device again, which should return nothing and delete the old messages + # Fetch the message of the dehydrated device again, which should return nothing + # and delete the old messages res = self.get_success( self.message_handler.get_events_for_dehydrated_device( requester=requester, @@ -576,21 +579,3 @@ def test_dehydrate_v2_and_fetch_events(self) -> None: ) self.assertTrue(len(res["next_batch"]) > 1) self.assertEqual(len(res["events"]), 0) - - # Fetching messages again should fail, since the messages and dehydrated device - # were deleted - self.get_failure( - self.message_handler.get_events_for_dehydrated_device( - requester=requester, - device_id=stored_dehydrated_device_id, - since_token=None, - limit=10, - ), - SynapseError, - ) - - # make sure that the dehydrated device ID is deleted after fetching messages - res2 = self.get_success( - self.handler.get_dehydrated_device(requester.user.to_string()), - ) - self.assertEqual(res2, None) diff --git a/tests/rest/client/test_devices.py b/tests/rest/client/test_devices.py index 25e425b0fe6e..b7d420cfec02 100644 --- a/tests/rest/client/test_devices.py +++ b/tests/rest/client/test_devices.py @@ -233,7 +233,21 @@ def test_PUT(self) -> None: "device_data": { "algorithm": "org.matrix.msc2697.v1.dehydration.v1.olm", "account": "dehydrated_device", - } + }, + "device_keys": { + "user_id": "@alice:test", + "device_id": "device1", + "valid_until_ts": "80", + "algorithms": [ + "m.olm.curve25519-aes-sha2", + ], + "keys": { + ":": "", + }, + "signatures": { + "": {":": ""} + }, + }, }, access_token=token, shorthand=False, @@ -252,44 +266,35 @@ def test_dehydrate_msc3814(self) -> None: "device_data": { "algorithm": "m.dehydration.v1.olm", }, + "device_id": "device1", "initial_device_display_name": "foo bar", - } - channel = self.make_request( - "PUT", - "_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device", - content=content, - access_token=token, - shorthand=False, - ) - self.assertEqual(channel.code, 200) - device_id = channel.json_body.get("device_id") - assert device_id is not None - self.assertIsInstance(device_id, str) - - # test that you can upload keys for this device - content = { "device_keys": { - "algorithms": ["m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"], - "device_id": f"{device_id}", + "user_id": "@mikey:test", + "device_id": "device1", + "valid_until_ts": "80", + "algorithms": [ + "m.olm.curve25519-aes-sha2", + ], "keys": { - "curve25519:JLAFKJWSCS": "3C5BFWi2Y8MaVvjM8M22DBmh24PmgR0nPvJOIArzgyI", - "ed25519:JLAFKJWSCS": "lEuiRJBit0IG6nUf5pUzWTUEsRVVe/HJkoKuEww9ULI", + ":": "", }, "signatures": { - "@alice:example.com": { - "ed25519:JLAFKJWSCS": "dSO80A01XiigH3uBiDVx/EjzaoycHcjq9lfQX0uWsqxl2giMIiSPR8a4d291W1ihKJL/a+myXS367WT6NAIcBA" - } + "": {":": ""} }, - "user_id": f"{user}", }, } channel = self.make_request( - "POST", - f"/_matrix/client/r0/keys/upload/{device_id}", + "PUT", + "_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device", content=content, access_token=token, + shorthand=False, ) self.assertEqual(channel.code, 200) + device_id = channel.json_body.get("device_id") + assert device_id is not None + self.assertIsInstance(device_id, str) + self.assertEqual("device1", device_id) # test that we can now GET the dehydrated device info channel = self.make_request( @@ -358,7 +363,16 @@ def test_dehydrate_msc3814(self) -> None: self.assertEqual(channel.code, 200) self.assertEqual(channel.json_body["events"], []) - # make sure that the dehydrated device id is deleted after we received the messages + # make sure we can delete the dehydrated device + channel = self.make_request( + "DELETE", + "_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device", + access_token=token, + shorthand=False, + ) + self.assertEqual(channel.code, 200) + + # ...and after deleting it is no longer available channel = self.make_request( "GET", "_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device",