test_device.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581
  1. # Copyright 2016 OpenMarket Ltd
  2. # Copyright 2018 New Vector Ltd
  3. # Copyright 2020 The Matrix.org Foundation C.I.C.
  4. #
  5. # Licensed under the Apache License, Version 2.0 (the "License");
  6. # you may not use this file except in compliance with the License.
  7. # You may obtain a copy of the License at
  8. #
  9. # http://www.apache.org/licenses/LICENSE-2.0
  10. #
  11. # Unless required by applicable law or agreed to in writing, software
  12. # distributed under the License is distributed on an "AS IS" BASIS,
  13. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. # See the License for the specific language governing permissions and
  15. # limitations under the License.
  16. from typing import Optional
  17. from unittest import mock
  18. from twisted.internet.defer import ensureDeferred
  19. from twisted.test.proto_helpers import MemoryReactor
  20. from synapse.api.constants import RoomEncryptionAlgorithms
  21. from synapse.api.errors import NotFoundError, SynapseError
  22. from synapse.appservice import ApplicationService
  23. from synapse.handlers.device import MAX_DEVICE_DISPLAY_NAME_LEN, DeviceHandler
  24. from synapse.rest import admin
  25. from synapse.rest.client import devices, login, register
  26. from synapse.server import HomeServer
  27. from synapse.storage.databases.main.appservice import _make_exclusive_regex
  28. from synapse.types import JsonDict, create_requester
  29. from synapse.util import Clock
  30. from tests import unittest
  31. from tests.test_utils import make_awaitable
  32. from tests.unittest import override_config
  33. user1 = "@boris:aaa"
  34. user2 = "@theresa:bbb"
  35. class DeviceTestCase(unittest.HomeserverTestCase):
  36. def make_homeserver(self, reactor: MemoryReactor, clock: Clock) -> HomeServer:
  37. self.appservice_api = mock.Mock()
  38. hs = self.setup_test_homeserver(
  39. "server",
  40. application_service_api=self.appservice_api,
  41. )
  42. handler = hs.get_device_handler()
  43. assert isinstance(handler, DeviceHandler)
  44. self.handler = handler
  45. self.store = hs.get_datastores().main
  46. return hs
  47. def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
  48. # These tests assume that it starts 1000 seconds in.
  49. self.reactor.advance(1000)
  50. def test_device_is_created_with_invalid_name(self) -> None:
  51. self.get_failure(
  52. self.handler.check_device_registered(
  53. user_id="@boris:foo",
  54. device_id="foo",
  55. initial_device_display_name="a" * (MAX_DEVICE_DISPLAY_NAME_LEN + 1),
  56. ),
  57. SynapseError,
  58. )
  59. def test_device_is_created_if_doesnt_exist(self) -> None:
  60. res = self.get_success(
  61. self.handler.check_device_registered(
  62. user_id="@boris:foo",
  63. device_id="fco",
  64. initial_device_display_name="display name",
  65. )
  66. )
  67. self.assertEqual(res, "fco")
  68. dev = self.get_success(self.handler.store.get_device("@boris:foo", "fco"))
  69. assert dev is not None
  70. self.assertEqual(dev["display_name"], "display name")
  71. def test_device_is_preserved_if_exists(self) -> None:
  72. res1 = self.get_success(
  73. self.handler.check_device_registered(
  74. user_id="@boris:foo",
  75. device_id="fco",
  76. initial_device_display_name="display name",
  77. )
  78. )
  79. self.assertEqual(res1, "fco")
  80. res2 = self.get_success(
  81. self.handler.check_device_registered(
  82. user_id="@boris:foo",
  83. device_id="fco",
  84. initial_device_display_name="new display name",
  85. )
  86. )
  87. self.assertEqual(res2, "fco")
  88. dev = self.get_success(self.handler.store.get_device("@boris:foo", "fco"))
  89. assert dev is not None
  90. self.assertEqual(dev["display_name"], "display name")
  91. def test_device_id_is_made_up_if_unspecified(self) -> None:
  92. device_id = self.get_success(
  93. self.handler.check_device_registered(
  94. user_id="@theresa:foo",
  95. device_id=None,
  96. initial_device_display_name="display",
  97. )
  98. )
  99. dev = self.get_success(self.handler.store.get_device("@theresa:foo", device_id))
  100. assert dev is not None
  101. self.assertEqual(dev["display_name"], "display")
  102. def test_get_devices_by_user(self) -> None:
  103. self._record_users()
  104. res = self.get_success(self.handler.get_devices_by_user(user1))
  105. self.assertEqual(3, len(res))
  106. device_map = {d["device_id"]: d for d in res}
  107. self.assertDictContainsSubset(
  108. {
  109. "user_id": user1,
  110. "device_id": "xyz",
  111. "display_name": "display 0",
  112. "last_seen_ip": None,
  113. "last_seen_ts": None,
  114. },
  115. device_map["xyz"],
  116. )
  117. self.assertDictContainsSubset(
  118. {
  119. "user_id": user1,
  120. "device_id": "fco",
  121. "display_name": "display 1",
  122. "last_seen_ip": "ip1",
  123. "last_seen_ts": 1000000,
  124. },
  125. device_map["fco"],
  126. )
  127. self.assertDictContainsSubset(
  128. {
  129. "user_id": user1,
  130. "device_id": "abc",
  131. "display_name": "display 2",
  132. "last_seen_ip": "ip3",
  133. "last_seen_ts": 3000000,
  134. },
  135. device_map["abc"],
  136. )
  137. def test_get_device(self) -> None:
  138. self._record_users()
  139. res = self.get_success(self.handler.get_device(user1, "abc"))
  140. self.assertDictContainsSubset(
  141. {
  142. "user_id": user1,
  143. "device_id": "abc",
  144. "display_name": "display 2",
  145. "last_seen_ip": "ip3",
  146. "last_seen_ts": 3000000,
  147. },
  148. res,
  149. )
  150. def test_delete_device(self) -> None:
  151. self._record_users()
  152. # delete the device
  153. self.get_success(self.handler.delete_devices(user1, ["abc"]))
  154. # check the device was deleted
  155. self.get_failure(self.handler.get_device(user1, "abc"), NotFoundError)
  156. # we'd like to check the access token was invalidated, but that's a
  157. # bit of a PITA.
  158. def test_delete_device_and_device_inbox(self) -> None:
  159. self._record_users()
  160. # add an device_inbox
  161. self.get_success(
  162. self.store.db_pool.simple_insert(
  163. "device_inbox",
  164. {
  165. "user_id": user1,
  166. "device_id": "abc",
  167. "stream_id": 1,
  168. "message_json": "{}",
  169. },
  170. )
  171. )
  172. # delete the device
  173. self.get_success(self.handler.delete_devices(user1, ["abc"]))
  174. # check that the device_inbox was deleted
  175. res = self.get_success(
  176. self.store.db_pool.simple_select_one(
  177. table="device_inbox",
  178. keyvalues={"user_id": user1, "device_id": "abc"},
  179. retcols=("user_id", "device_id"),
  180. allow_none=True,
  181. desc="get_device_id_from_device_inbox",
  182. )
  183. )
  184. self.assertIsNone(res)
  185. def test_update_device(self) -> None:
  186. self._record_users()
  187. update = {"display_name": "new display"}
  188. self.get_success(self.handler.update_device(user1, "abc", update))
  189. res = self.get_success(self.handler.get_device(user1, "abc"))
  190. self.assertEqual(res["display_name"], "new display")
  191. def test_update_device_too_long_display_name(self) -> None:
  192. """Update a device with a display name that is invalid (too long)."""
  193. self._record_users()
  194. # Request to update a device display name with a new value that is longer than allowed.
  195. update = {"display_name": "a" * (MAX_DEVICE_DISPLAY_NAME_LEN + 1)}
  196. self.get_failure(
  197. self.handler.update_device(user1, "abc", update),
  198. SynapseError,
  199. )
  200. # Ensure the display name was not updated.
  201. res = self.get_success(self.handler.get_device(user1, "abc"))
  202. self.assertEqual(res["display_name"], "display 2")
  203. def test_update_unknown_device(self) -> None:
  204. update = {"display_name": "new_display"}
  205. self.get_failure(
  206. self.handler.update_device("user_id", "unknown_device_id", update),
  207. NotFoundError,
  208. )
  209. def _record_users(self) -> None:
  210. # check this works for both devices which have a recorded client_ip,
  211. # and those which don't.
  212. self._record_user(user1, "xyz", "display 0")
  213. self._record_user(user1, "fco", "display 1", "token1", "ip1")
  214. self._record_user(user1, "abc", "display 2", "token2", "ip2")
  215. self._record_user(user1, "abc", "display 2", "token3", "ip3")
  216. self._record_user(user2, "def", "dispkay", "token4", "ip4")
  217. self.reactor.advance(10000)
  218. def _record_user(
  219. self,
  220. user_id: str,
  221. device_id: str,
  222. display_name: str,
  223. access_token: Optional[str] = None,
  224. ip: Optional[str] = None,
  225. ) -> None:
  226. device_id = self.get_success(
  227. self.handler.check_device_registered(
  228. user_id=user_id,
  229. device_id=device_id,
  230. initial_device_display_name=display_name,
  231. )
  232. )
  233. if access_token is not None and ip is not None:
  234. self.get_success(
  235. self.store.insert_client_ip(
  236. user_id, access_token, ip, "user_agent", device_id
  237. )
  238. )
  239. self.reactor.advance(1000)
  240. @override_config({"experimental_features": {"msc3984_appservice_key_query": True}})
  241. def test_on_federation_query_user_devices_appservice(self) -> None:
  242. """Test that querying of appservices for keys overrides responses from the database."""
  243. local_user = "@boris:" + self.hs.hostname
  244. device_1 = "abc"
  245. device_2 = "def"
  246. device_3 = "ghi"
  247. # There are 3 devices:
  248. #
  249. # 1. One which is uploaded to the homeserver.
  250. # 2. One which is uploaded to the homeserver, but a newer copy is returned
  251. # by the appservice.
  252. # 3. One which is only returned by the appservice.
  253. device_key_1: JsonDict = {
  254. "user_id": local_user,
  255. "device_id": device_1,
  256. "algorithms": [
  257. "m.olm.curve25519-aes-sha2",
  258. RoomEncryptionAlgorithms.MEGOLM_V1_AES_SHA2,
  259. ],
  260. "keys": {
  261. "ed25519:abc": "base64+ed25519+key",
  262. "curve25519:abc": "base64+curve25519+key",
  263. },
  264. "signatures": {local_user: {"ed25519:abc": "base64+signature"}},
  265. }
  266. device_key_2a: JsonDict = {
  267. "user_id": local_user,
  268. "device_id": device_2,
  269. "algorithms": [
  270. "m.olm.curve25519-aes-sha2",
  271. RoomEncryptionAlgorithms.MEGOLM_V1_AES_SHA2,
  272. ],
  273. "keys": {
  274. "ed25519:def": "base64+ed25519+key",
  275. "curve25519:def": "base64+curve25519+key",
  276. },
  277. "signatures": {local_user: {"ed25519:def": "base64+signature"}},
  278. }
  279. device_key_2b: JsonDict = {
  280. "user_id": local_user,
  281. "device_id": device_2,
  282. "algorithms": [
  283. "m.olm.curve25519-aes-sha2",
  284. RoomEncryptionAlgorithms.MEGOLM_V1_AES_SHA2,
  285. ],
  286. # The device ID is the same (above), but the keys are different.
  287. "keys": {
  288. "ed25519:xyz": "base64+ed25519+key",
  289. "curve25519:xyz": "base64+curve25519+key",
  290. },
  291. "signatures": {local_user: {"ed25519:xyz": "base64+signature"}},
  292. }
  293. device_key_3: JsonDict = {
  294. "user_id": local_user,
  295. "device_id": device_3,
  296. "algorithms": [
  297. "m.olm.curve25519-aes-sha2",
  298. RoomEncryptionAlgorithms.MEGOLM_V1_AES_SHA2,
  299. ],
  300. "keys": {
  301. "ed25519:jkl": "base64+ed25519+key",
  302. "curve25519:jkl": "base64+curve25519+key",
  303. },
  304. "signatures": {local_user: {"ed25519:jkl": "base64+signature"}},
  305. }
  306. # Upload keys for devices 1 & 2a.
  307. e2e_keys_handler = self.hs.get_e2e_keys_handler()
  308. self.get_success(
  309. e2e_keys_handler.upload_keys_for_user(
  310. local_user, device_1, {"device_keys": device_key_1}
  311. )
  312. )
  313. self.get_success(
  314. e2e_keys_handler.upload_keys_for_user(
  315. local_user, device_2, {"device_keys": device_key_2a}
  316. )
  317. )
  318. # Inject an appservice interested in this user.
  319. appservice = ApplicationService(
  320. token="i_am_an_app_service",
  321. id="1234",
  322. namespaces={"users": [{"regex": r"@boris:.+", "exclusive": True}]},
  323. # Note: this user does not have to match the regex above
  324. sender="@as_main:test",
  325. )
  326. self.hs.get_datastores().main.services_cache = [appservice]
  327. self.hs.get_datastores().main.exclusive_user_regex = _make_exclusive_regex(
  328. [appservice]
  329. )
  330. # Setup a response.
  331. self.appservice_api.query_keys.return_value = make_awaitable(
  332. {
  333. "device_keys": {
  334. local_user: {device_2: device_key_2b, device_3: device_key_3}
  335. }
  336. }
  337. )
  338. # Request all devices.
  339. res = self.get_success(
  340. self.handler.on_federation_query_user_devices(local_user)
  341. )
  342. self.assertIn("devices", res)
  343. res_devices = res["devices"]
  344. for device in res_devices:
  345. device["keys"].pop("unsigned", None)
  346. self.assertEqual(
  347. res_devices,
  348. [
  349. {"device_id": device_1, "keys": device_key_1},
  350. {"device_id": device_2, "keys": device_key_2b},
  351. {"device_id": device_3, "keys": device_key_3},
  352. ],
  353. )
  354. class DehydrationTestCase(unittest.HomeserverTestCase):
  355. servlets = [
  356. admin.register_servlets_for_client_rest_resource,
  357. login.register_servlets,
  358. register.register_servlets,
  359. devices.register_servlets,
  360. ]
  361. def make_homeserver(self, reactor: MemoryReactor, clock: Clock) -> HomeServer:
  362. hs = self.setup_test_homeserver("server")
  363. handler = hs.get_device_handler()
  364. assert isinstance(handler, DeviceHandler)
  365. self.handler = handler
  366. self.message_handler = hs.get_device_message_handler()
  367. self.registration = hs.get_registration_handler()
  368. self.auth = hs.get_auth()
  369. self.store = hs.get_datastores().main
  370. return hs
  371. def test_dehydrate_and_rehydrate_device(self) -> None:
  372. user_id = "@boris:dehydration"
  373. self.get_success(self.store.register_user(user_id, "foobar"))
  374. # First check if we can store and fetch a dehydrated device
  375. stored_dehydrated_device_id = self.get_success(
  376. self.handler.store_dehydrated_device(
  377. user_id=user_id,
  378. device_id=None,
  379. device_data={"device_data": {"foo": "bar"}},
  380. initial_device_display_name="dehydrated device",
  381. )
  382. )
  383. result = self.get_success(self.handler.get_dehydrated_device(user_id=user_id))
  384. assert result is not None
  385. retrieved_device_id, device_data = result
  386. self.assertEqual(retrieved_device_id, stored_dehydrated_device_id)
  387. self.assertEqual(device_data, {"device_data": {"foo": "bar"}})
  388. # Create a new login for the user and dehydrated the device
  389. device_id, access_token, _expiration_time, _refresh_token = self.get_success(
  390. self.registration.register_device(
  391. user_id=user_id,
  392. device_id=None,
  393. initial_display_name="new device",
  394. )
  395. )
  396. # Trying to claim a nonexistent device should throw an error
  397. self.get_failure(
  398. self.handler.rehydrate_device(
  399. user_id=user_id,
  400. access_token=access_token,
  401. device_id="not the right device ID",
  402. ),
  403. NotFoundError,
  404. )
  405. # dehydrating the right devices should succeed and change our device ID
  406. # to the dehydrated device's ID
  407. res = self.get_success(
  408. self.handler.rehydrate_device(
  409. user_id=user_id,
  410. access_token=access_token,
  411. device_id=retrieved_device_id,
  412. )
  413. )
  414. self.assertEqual(res, {"success": True})
  415. # make sure that our device ID has changed
  416. user_info = self.get_success(self.auth.get_user_by_access_token(access_token))
  417. self.assertEqual(user_info.device_id, retrieved_device_id)
  418. # make sure the device has the display name that was set from the login
  419. res = self.get_success(self.handler.get_device(user_id, retrieved_device_id))
  420. self.assertEqual(res["display_name"], "new device")
  421. # make sure that the device ID that we were initially assigned no longer exists
  422. self.get_failure(
  423. self.handler.get_device(user_id, device_id),
  424. NotFoundError,
  425. )
  426. # make sure that there's no device available for dehydrating now
  427. ret = self.get_success(self.handler.get_dehydrated_device(user_id=user_id))
  428. self.assertIsNone(ret)
  429. @unittest.override_config(
  430. {"experimental_features": {"msc2697_enabled": False, "msc3814_enabled": True}}
  431. )
  432. def test_dehydrate_v2_and_fetch_events(self) -> None:
  433. user_id = "@boris:server"
  434. self.get_success(self.store.register_user(user_id, "foobar"))
  435. # First check if we can store and fetch a dehydrated device
  436. stored_dehydrated_device_id = self.get_success(
  437. self.handler.store_dehydrated_device(
  438. user_id=user_id,
  439. device_id=None,
  440. device_data={"device_data": {"foo": "bar"}},
  441. initial_device_display_name="dehydrated device",
  442. )
  443. )
  444. device_info = self.get_success(
  445. self.handler.get_dehydrated_device(user_id=user_id)
  446. )
  447. assert device_info is not None
  448. retrieved_device_id, device_data = device_info
  449. self.assertEqual(retrieved_device_id, stored_dehydrated_device_id)
  450. self.assertEqual(device_data, {"device_data": {"foo": "bar"}})
  451. # Create a new login for the user
  452. device_id, access_token, _expiration_time, _refresh_token = self.get_success(
  453. self.registration.register_device(
  454. user_id=user_id,
  455. device_id=None,
  456. initial_display_name="new device",
  457. )
  458. )
  459. requester = create_requester(user_id, device_id=device_id)
  460. # Fetching messages for a non-existing device should return an error
  461. self.get_failure(
  462. self.message_handler.get_events_for_dehydrated_device(
  463. requester=requester,
  464. device_id="not the right device ID",
  465. since_token=None,
  466. limit=10,
  467. ),
  468. SynapseError,
  469. )
  470. # Send a message to the dehydrated device
  471. ensureDeferred(
  472. self.message_handler.send_device_message(
  473. requester=requester,
  474. message_type="test.message",
  475. messages={user_id: {stored_dehydrated_device_id: {"body": "foo"}}},
  476. )
  477. )
  478. self.pump()
  479. # Fetch the message of the dehydrated device
  480. res = self.get_success(
  481. self.message_handler.get_events_for_dehydrated_device(
  482. requester=requester,
  483. device_id=stored_dehydrated_device_id,
  484. since_token=None,
  485. limit=10,
  486. )
  487. )
  488. self.assertTrue(len(res["next_batch"]) > 1)
  489. self.assertEqual(len(res["events"]), 1)
  490. self.assertEqual(res["events"][0]["content"]["body"], "foo")
  491. # Fetch the message of the dehydrated device again, which should return
  492. # the same message as it has not been deleted
  493. res = self.get_success(
  494. self.message_handler.get_events_for_dehydrated_device(
  495. requester=requester,
  496. device_id=stored_dehydrated_device_id,
  497. since_token=None,
  498. limit=10,
  499. )
  500. )
  501. self.assertTrue(len(res["next_batch"]) > 1)
  502. self.assertEqual(len(res["events"]), 1)
  503. self.assertEqual(res["events"][0]["content"]["body"], "foo")