From cc2022dfaf69f0f171e5f7c40cfc4248621d9996 Mon Sep 17 00:00:00 2001 From: "H. Shay" Date: Wed, 19 Jul 2023 11:02:08 -0700 Subject: [PATCH] apply provided patch --- synapse/handlers/device.py | 4 +- synapse/handlers/devicemessage.py | 8 +- synapse/rest/client/devices.py | 125 +++++++++++++++++++++++++++++- tests/handlers/test_device.py | 20 +---- tests/rest/client/test_devices.py | 79 +++++++++++-------- 5 files changed, 172 insertions(+), 64 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..583e3334a03d 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 @@ -397,12 +397,6 @@ async def get_events_for_dehydrated_device( device_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..3368f360c8aa 100644 --- a/synapse/rest/client/devices.py +++ b/synapse/rest/client/devices.py @@ -12,14 +12,14 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # 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 +28,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 @@ -228,7 +229,7 @@ class Config: class DehydratedDeviceServlet(RestServlet): - """Retrieve or store a dehydrated device. + """Retrieve, store or delete a dehydrated device. Implements either MSC2697 and MSC3814. @@ -262,6 +263,75 @@ class DehydratedDeviceServlet(RestServlet): "device_id": "dehydrated_device_id" } + 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", + } + """ def __init__(self, hs: "HomeServer", msc2697: bool = True): @@ -270,6 +340,7 @@ def __init__(self, hs: "HomeServer", msc2697: bool = True): 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 self.PATTERNS = client_patterns( @@ -279,6 +350,13 @@ def __init__(self, hs: "HomeServer", msc2697: bool = True): releases=(), ) + 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( @@ -291,19 +369,58 @@ async def on_GET(self, request: SynapseRequest) -> Tuple[int, JsonDict]: 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 + + 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: Optional[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_id = await self.device_handler.store_dehydrated_device( - requester.user.to_string(), + user_id, + submission.device_id, submission.device_data.dict(), submission.initial_device_display_name, ) + + 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: Do we need to do something with the result here? + await self.key_uploader(user_id=user_id, device_id=device_id, keys=device_info) + return 200, {"device_id": device_id} diff --git a/tests/handlers/test_device.py b/tests/handlers/test_device.py index 1be366702b51..962e6319e916 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", ) @@ -576,21 +578,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..9c62b8b88989 100644 --- a/tests/rest/client/test_devices.py +++ b/tests/rest/client/test_devices.py @@ -20,7 +20,7 @@ from synapse.rest import admin, devices, room, sync from synapse.rest.client import account, keys, login, register from synapse.server import HomeServer -from synapse.types import JsonDict, create_requester +from synapse.types import create_requester from synapse.util import Clock from tests import unittest @@ -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, @@ -248,48 +262,37 @@ def test_PUT(self) -> None: def test_dehydrate_msc3814(self) -> None: user = self.register_user("mikey", "pass") token = self.login(user, "pass", device_id="device1") - content: JsonDict = { + content: dict = { + "device_id": "device1", "device_data": { - "algorithm": "m.dehydration.v1.olm", + "algorithm": "org.matrix.msc2697.v1.dehydration.v1.olm", }, - "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) # test that we can now GET the dehydrated device info channel = self.make_request( @@ -303,7 +306,7 @@ def test_dehydrate_msc3814(self) -> None: self.assertEqual(returned_device_id, device_id) device_data = channel.json_body.get("device_data") expected_device_data = { - "algorithm": "m.dehydration.v1.olm", + "algorithm": "org.matrix.msc2697.v1.dehydration.v1.olm", } self.assertEqual(device_data, expected_device_data) @@ -345,8 +348,7 @@ def test_dehydrate_msc3814(self) -> None: self.assertEqual(channel.json_body["events"][0]["content"], expected_content) next_batch_token = channel.json_body.get("next_batch") - # fetch messages again and make sure that the message was deleted and we are returned an - # empty array + # fetch messages again and make sure we are returned an empty array content = {"next_batch": next_batch_token} channel = self.make_request( "POST", @@ -358,7 +360,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",