test_user_directory.py 38 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966
  1. # Copyright 2018 New Vector
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License");
  4. # you may not use this file except in compliance with the License.
  5. # You may obtain a copy of the License at
  6. #
  7. # http://www.apache.org/licenses/LICENSE-2.0
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS,
  11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. # See the License for the specific language governing permissions and
  13. # limitations under the License.
  14. from typing import Tuple
  15. from unittest.mock import Mock, patch
  16. from urllib.parse import quote
  17. from twisted.internet import defer
  18. from twisted.test.proto_helpers import MemoryReactor
  19. import synapse.rest.admin
  20. from synapse.api.constants import UserTypes
  21. from synapse.api.room_versions import RoomVersion, RoomVersions
  22. from synapse.appservice import ApplicationService
  23. from synapse.rest.client import login, register, room, user_directory
  24. from synapse.server import HomeServer
  25. from synapse.storage.roommember import ProfileInfo
  26. from synapse.types import create_requester
  27. from synapse.util import Clock
  28. from tests import unittest
  29. from tests.storage.test_user_directory import GetUserDirectoryTables
  30. from tests.test_utils.event_injection import inject_member_event
  31. from tests.unittest import override_config
  32. class UserDirectoryTestCase(unittest.HomeserverTestCase):
  33. """Tests the UserDirectoryHandler.
  34. We're broadly testing two kinds of things here.
  35. 1. Check that we correctly update the user directory in response
  36. to events (e.g. join a room, leave a room, change name, make public)
  37. 2. Check that the search logic behaves as expected.
  38. The background process that rebuilds the user directory is tested in
  39. tests/storage/test_user_directory.py.
  40. """
  41. servlets = [
  42. login.register_servlets,
  43. synapse.rest.admin.register_servlets,
  44. register.register_servlets,
  45. room.register_servlets,
  46. ]
  47. def make_homeserver(self, reactor: MemoryReactor, clock: Clock) -> HomeServer:
  48. config = self.default_config()
  49. config["update_user_directory"] = True
  50. self.appservice = ApplicationService(
  51. token="i_am_an_app_service",
  52. hostname="test",
  53. id="1234",
  54. namespaces={"users": [{"regex": r"@as_user.*", "exclusive": True}]},
  55. # Note: this user does not match the regex above, so that tests
  56. # can distinguish the sender from the AS user.
  57. sender="@as_main:test",
  58. )
  59. mock_load_appservices = Mock(return_value=[self.appservice])
  60. with patch(
  61. "synapse.storage.databases.main.appservice.load_appservices",
  62. mock_load_appservices,
  63. ):
  64. hs = self.setup_test_homeserver(config=config)
  65. return hs
  66. def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
  67. self.store = hs.get_datastore()
  68. self.handler = hs.get_user_directory_handler()
  69. self.event_builder_factory = self.hs.get_event_builder_factory()
  70. self.event_creation_handler = self.hs.get_event_creation_handler()
  71. self.user_dir_helper = GetUserDirectoryTables(self.store)
  72. def test_normal_user_pair(self) -> None:
  73. """Sanity check that the room-sharing tables are updated correctly."""
  74. alice = self.register_user("alice", "pass")
  75. alice_token = self.login(alice, "pass")
  76. bob = self.register_user("bob", "pass")
  77. bob_token = self.login(bob, "pass")
  78. public = self.helper.create_room_as(
  79. alice,
  80. is_public=True,
  81. extra_content={"visibility": "public"},
  82. tok=alice_token,
  83. )
  84. private = self.helper.create_room_as(alice, is_public=False, tok=alice_token)
  85. self.helper.invite(private, alice, bob, tok=alice_token)
  86. self.helper.join(public, bob, tok=bob_token)
  87. self.helper.join(private, bob, tok=bob_token)
  88. # Alice also makes a second public room but no-one else joins
  89. public2 = self.helper.create_room_as(
  90. alice,
  91. is_public=True,
  92. extra_content={"visibility": "public"},
  93. tok=alice_token,
  94. )
  95. # The user directory should reflect the room memberships above.
  96. users, in_public, in_private = self.get_success(
  97. self.user_dir_helper.get_tables()
  98. )
  99. self.assertEqual(users, {alice, bob})
  100. self.assertEqual(in_public, {(alice, public), (bob, public), (alice, public2)})
  101. self.assertEqual(
  102. in_private,
  103. {(alice, bob, private), (bob, alice, private)},
  104. )
  105. # The next four tests (test_excludes_*) all setup
  106. # - A normal user included in the user dir
  107. # - A public and private room created by that user
  108. # - A user excluded from the room dir, belonging to both rooms
  109. # They match similar logic in storage/test_user_directory. But that tests
  110. # rebuilding the directory; this tests updating it incrementally.
  111. def test_excludes_support_user(self) -> None:
  112. alice = self.register_user("alice", "pass")
  113. alice_token = self.login(alice, "pass")
  114. support = "@support1:test"
  115. self.get_success(
  116. self.store.register_user(
  117. user_id=support, password_hash=None, user_type=UserTypes.SUPPORT
  118. )
  119. )
  120. public, private = self._create_rooms_and_inject_memberships(
  121. alice, alice_token, support
  122. )
  123. self._check_only_one_user_in_directory(alice, public)
  124. def test_excludes_deactivated_user(self) -> None:
  125. admin = self.register_user("admin", "pass", admin=True)
  126. admin_token = self.login(admin, "pass")
  127. user = self.register_user("naughty", "pass")
  128. # Deactivate the user.
  129. channel = self.make_request(
  130. "PUT",
  131. f"/_synapse/admin/v2/users/{user}",
  132. access_token=admin_token,
  133. content={"deactivated": True},
  134. )
  135. self.assertEqual(channel.code, 200)
  136. self.assertEqual(channel.json_body["deactivated"], True)
  137. # Join the deactivated user to rooms owned by the admin.
  138. # Is this something that could actually happen outside of a test?
  139. public, private = self._create_rooms_and_inject_memberships(
  140. admin, admin_token, user
  141. )
  142. self._check_only_one_user_in_directory(admin, public)
  143. def test_excludes_appservices_user(self) -> None:
  144. # Register an AS user.
  145. user = self.register_user("user", "pass")
  146. token = self.login(user, "pass")
  147. as_user = self.register_appservice_user("as_user_potato", self.appservice.token)
  148. # Join the AS user to rooms owned by the normal user.
  149. public, private = self._create_rooms_and_inject_memberships(
  150. user, token, as_user
  151. )
  152. self._check_only_one_user_in_directory(user, public)
  153. def test_excludes_appservice_sender(self) -> None:
  154. user = self.register_user("user", "pass")
  155. token = self.login(user, "pass")
  156. room = self.helper.create_room_as(user, is_public=True, tok=token)
  157. self.helper.join(room, self.appservice.sender, tok=self.appservice.token)
  158. self._check_only_one_user_in_directory(user, room)
  159. def test_user_not_in_users_table(self) -> None:
  160. """Unclear how it happens, but on matrix.org we've seen join events
  161. for users who aren't in the users table. Test that we don't fall over
  162. when processing such a user.
  163. """
  164. user1 = self.register_user("user1", "pass")
  165. token1 = self.login(user1, "pass")
  166. room = self.helper.create_room_as(user1, is_public=True, tok=token1)
  167. # Inject a join event for a user who doesn't exist
  168. self.get_success(inject_member_event(self.hs, room, "@not-a-user:test", "join"))
  169. # Another new user registers and joins the room
  170. user2 = self.register_user("user2", "pass")
  171. token2 = self.login(user2, "pass")
  172. self.helper.join(room, user2, tok=token2)
  173. # The dodgy event should not have stopped us from processing user2's join.
  174. in_public = self.get_success(self.user_dir_helper.get_users_in_public_rooms())
  175. self.assertEqual(set(in_public), {(user1, room), (user2, room)})
  176. def test_excludes_users_when_making_room_public(self) -> None:
  177. # Create a regular user and a support user.
  178. alice = self.register_user("alice", "pass")
  179. alice_token = self.login(alice, "pass")
  180. support = "@support1:test"
  181. self.get_success(
  182. self.store.register_user(
  183. user_id=support, password_hash=None, user_type=UserTypes.SUPPORT
  184. )
  185. )
  186. # Make a public and private room containing Alice and the support user
  187. public, initially_private = self._create_rooms_and_inject_memberships(
  188. alice, alice_token, support
  189. )
  190. self._check_only_one_user_in_directory(alice, public)
  191. # Alice makes the private room public.
  192. self.helper.send_state(
  193. initially_private,
  194. "m.room.join_rules",
  195. {"join_rule": "public"},
  196. tok=alice_token,
  197. )
  198. users, in_public, in_private = self.get_success(
  199. self.user_dir_helper.get_tables()
  200. )
  201. self.assertEqual(users, {alice})
  202. self.assertEqual(in_public, {(alice, public), (alice, initially_private)})
  203. self.assertEqual(in_private, set())
  204. def test_switching_from_private_to_public_to_private(self) -> None:
  205. """Check we update the room sharing tables when switching a room
  206. from private to public, then back again to private."""
  207. # Alice and Bob share a private room.
  208. alice = self.register_user("alice", "pass")
  209. alice_token = self.login(alice, "pass")
  210. bob = self.register_user("bob", "pass")
  211. bob_token = self.login(bob, "pass")
  212. room = self.helper.create_room_as(alice, is_public=False, tok=alice_token)
  213. self.helper.invite(room, alice, bob, tok=alice_token)
  214. self.helper.join(room, bob, tok=bob_token)
  215. # The user directory should reflect this.
  216. def check_user_dir_for_private_room() -> None:
  217. users, in_public, in_private = self.get_success(
  218. self.user_dir_helper.get_tables()
  219. )
  220. self.assertEqual(users, {alice, bob})
  221. self.assertEqual(in_public, set())
  222. self.assertEqual(in_private, {(alice, bob, room), (bob, alice, room)})
  223. check_user_dir_for_private_room()
  224. # Alice makes the room public.
  225. self.helper.send_state(
  226. room,
  227. "m.room.join_rules",
  228. {"join_rule": "public"},
  229. tok=alice_token,
  230. )
  231. # The user directory should be updated accordingly
  232. users, in_public, in_private = self.get_success(
  233. self.user_dir_helper.get_tables()
  234. )
  235. self.assertEqual(users, {alice, bob})
  236. self.assertEqual(in_public, {(alice, room), (bob, room)})
  237. self.assertEqual(in_private, set())
  238. # Alice makes the room private.
  239. self.helper.send_state(
  240. room,
  241. "m.room.join_rules",
  242. {"join_rule": "invite"},
  243. tok=alice_token,
  244. )
  245. # The user directory should be updated accordingly
  246. check_user_dir_for_private_room()
  247. def _create_rooms_and_inject_memberships(
  248. self, creator: str, token: str, joiner: str
  249. ) -> Tuple[str, str]:
  250. """Create a public and private room as a normal user.
  251. Then get the `joiner` into those rooms.
  252. """
  253. # TODO: Duplicates the same-named method in UserDirectoryInitialPopulationTest.
  254. public_room = self.helper.create_room_as(
  255. creator,
  256. is_public=True,
  257. # See https://github.com/matrix-org/synapse/issues/10951
  258. extra_content={"visibility": "public"},
  259. tok=token,
  260. )
  261. private_room = self.helper.create_room_as(creator, is_public=False, tok=token)
  262. # HACK: get the user into these rooms
  263. self.get_success(inject_member_event(self.hs, public_room, joiner, "join"))
  264. self.get_success(inject_member_event(self.hs, private_room, joiner, "join"))
  265. return public_room, private_room
  266. def _check_only_one_user_in_directory(self, user: str, public: str) -> None:
  267. """Check that the user directory DB tables show that:
  268. - only one user is in the user directory
  269. - they belong to exactly one public room
  270. - they don't share a private room with anyone.
  271. """
  272. users, in_public, in_private = self.get_success(
  273. self.user_dir_helper.get_tables()
  274. )
  275. self.assertEqual(users, {user})
  276. self.assertEqual(in_public, {(user, public)})
  277. self.assertEqual(in_private, set())
  278. def test_handle_local_profile_change_with_support_user(self) -> None:
  279. support_user_id = "@support:test"
  280. self.get_success(
  281. self.store.register_user(
  282. user_id=support_user_id, password_hash=None, user_type=UserTypes.SUPPORT
  283. )
  284. )
  285. regular_user_id = "@regular:test"
  286. self.get_success(
  287. self.store.register_user(user_id=regular_user_id, password_hash=None)
  288. )
  289. self.get_success(
  290. self.handler.handle_local_profile_change(
  291. support_user_id, ProfileInfo("I love support me", None)
  292. )
  293. )
  294. profile = self.get_success(self.store.get_user_in_directory(support_user_id))
  295. self.assertIsNone(profile)
  296. display_name = "display_name"
  297. profile_info = ProfileInfo(avatar_url="avatar_url", display_name=display_name)
  298. self.get_success(
  299. self.handler.handle_local_profile_change(regular_user_id, profile_info)
  300. )
  301. profile = self.get_success(self.store.get_user_in_directory(regular_user_id))
  302. self.assertTrue(profile["display_name"] == display_name)
  303. def test_handle_local_profile_change_with_deactivated_user(self) -> None:
  304. # create user
  305. r_user_id = "@regular:test"
  306. self.get_success(
  307. self.store.register_user(user_id=r_user_id, password_hash=None)
  308. )
  309. # update profile
  310. display_name = "Regular User"
  311. profile_info = ProfileInfo(avatar_url="avatar_url", display_name=display_name)
  312. self.get_success(
  313. self.handler.handle_local_profile_change(r_user_id, profile_info)
  314. )
  315. # profile is in directory
  316. profile = self.get_success(self.store.get_user_in_directory(r_user_id))
  317. self.assertTrue(profile["display_name"] == display_name)
  318. # deactivate user
  319. self.get_success(self.store.set_user_deactivated_status(r_user_id, True))
  320. self.get_success(self.handler.handle_local_user_deactivated(r_user_id))
  321. # profile is not in directory
  322. profile = self.get_success(self.store.get_user_in_directory(r_user_id))
  323. self.assertIsNone(profile)
  324. # update profile after deactivation
  325. self.get_success(
  326. self.handler.handle_local_profile_change(r_user_id, profile_info)
  327. )
  328. # profile is furthermore not in directory
  329. profile = self.get_success(self.store.get_user_in_directory(r_user_id))
  330. self.assertIsNone(profile)
  331. def test_handle_local_profile_change_with_appservice_user(self) -> None:
  332. # create user
  333. as_user_id = self.register_appservice_user(
  334. "as_user_alice", self.appservice.token
  335. )
  336. # profile is not in directory
  337. profile = self.get_success(self.store.get_user_in_directory(as_user_id))
  338. self.assertIsNone(profile)
  339. # update profile
  340. profile_info = ProfileInfo(avatar_url="avatar_url", display_name="4L1c3")
  341. self.get_success(
  342. self.handler.handle_local_profile_change(as_user_id, profile_info)
  343. )
  344. # profile is still not in directory
  345. profile = self.get_success(self.store.get_user_in_directory(as_user_id))
  346. self.assertIsNone(profile)
  347. def test_handle_local_profile_change_with_appservice_sender(self) -> None:
  348. # profile is not in directory
  349. profile = self.get_success(
  350. self.store.get_user_in_directory(self.appservice.sender)
  351. )
  352. self.assertIsNone(profile)
  353. # update profile
  354. profile_info = ProfileInfo(avatar_url="avatar_url", display_name="4L1c3")
  355. self.get_success(
  356. self.handler.handle_local_profile_change(
  357. self.appservice.sender, profile_info
  358. )
  359. )
  360. # profile is still not in directory
  361. profile = self.get_success(
  362. self.store.get_user_in_directory(self.appservice.sender)
  363. )
  364. self.assertIsNone(profile)
  365. def test_handle_user_deactivated_support_user(self) -> None:
  366. s_user_id = "@support:test"
  367. self.get_success(
  368. self.store.register_user(
  369. user_id=s_user_id, password_hash=None, user_type=UserTypes.SUPPORT
  370. )
  371. )
  372. mock_remove_from_user_dir = Mock(return_value=defer.succeed(None))
  373. with patch.object(
  374. self.store, "remove_from_user_dir", mock_remove_from_user_dir
  375. ):
  376. self.get_success(self.handler.handle_local_user_deactivated(s_user_id))
  377. # BUG: the correct spelling is assert_not_called, but that makes the test fail
  378. # and it's not clear that this is actually the behaviour we want.
  379. mock_remove_from_user_dir.not_called()
  380. def test_handle_user_deactivated_regular_user(self) -> None:
  381. r_user_id = "@regular:test"
  382. self.get_success(
  383. self.store.register_user(user_id=r_user_id, password_hash=None)
  384. )
  385. mock_remove_from_user_dir = Mock(return_value=defer.succeed(None))
  386. with patch.object(
  387. self.store, "remove_from_user_dir", mock_remove_from_user_dir
  388. ):
  389. self.get_success(self.handler.handle_local_user_deactivated(r_user_id))
  390. mock_remove_from_user_dir.assert_called_once_with(r_user_id)
  391. def test_reactivation_makes_regular_user_searchable(self) -> None:
  392. user = self.register_user("regular", "pass")
  393. user_token = self.login(user, "pass")
  394. admin_user = self.register_user("admin", "pass", admin=True)
  395. admin_token = self.login(admin_user, "pass")
  396. # Ensure the regular user is publicly visible and searchable.
  397. self.helper.create_room_as(user, is_public=True, tok=user_token)
  398. s = self.get_success(self.handler.search_users(admin_user, user, 10))
  399. self.assertEqual(len(s["results"]), 1)
  400. self.assertEqual(s["results"][0]["user_id"], user)
  401. # Deactivate the user and check they're not searchable.
  402. deactivate_handler = self.hs.get_deactivate_account_handler()
  403. self.get_success(
  404. deactivate_handler.deactivate_account(
  405. user, erase_data=False, requester=create_requester(admin_user)
  406. )
  407. )
  408. s = self.get_success(self.handler.search_users(admin_user, user, 10))
  409. self.assertEqual(s["results"], [])
  410. # Reactivate the user
  411. channel = self.make_request(
  412. "PUT",
  413. f"/_synapse/admin/v2/users/{quote(user)}",
  414. access_token=admin_token,
  415. content={"deactivated": False, "password": "pass"},
  416. )
  417. self.assertEqual(channel.code, 200)
  418. user_token = self.login(user, "pass")
  419. self.helper.create_room_as(user, is_public=True, tok=user_token)
  420. # Check they're searchable.
  421. s = self.get_success(self.handler.search_users(admin_user, user, 10))
  422. self.assertEqual(len(s["results"]), 1)
  423. self.assertEqual(s["results"][0]["user_id"], user)
  424. def test_process_join_after_server_leaves_room(self) -> None:
  425. alice = self.register_user("alice", "pass")
  426. alice_token = self.login(alice, "pass")
  427. bob = self.register_user("bob", "pass")
  428. bob_token = self.login(bob, "pass")
  429. # Alice makes two rooms. Bob joins one of them.
  430. room1 = self.helper.create_room_as(alice, tok=alice_token)
  431. room2 = self.helper.create_room_as(alice, tok=alice_token)
  432. self.helper.join(room1, bob, tok=bob_token)
  433. # The user sharing tables should have been updated.
  434. public1 = self.get_success(self.user_dir_helper.get_users_in_public_rooms())
  435. self.assertEqual(set(public1), {(alice, room1), (alice, room2), (bob, room1)})
  436. # Alice leaves room1. The user sharing tables should be updated.
  437. self.helper.leave(room1, alice, tok=alice_token)
  438. public2 = self.get_success(self.user_dir_helper.get_users_in_public_rooms())
  439. self.assertEqual(set(public2), {(alice, room2), (bob, room1)})
  440. # Pause the processing of new events.
  441. dir_handler = self.hs.get_user_directory_handler()
  442. dir_handler.update_user_directory = False
  443. # Bob leaves one room and joins the other.
  444. self.helper.leave(room1, bob, tok=bob_token)
  445. self.helper.join(room2, bob, tok=bob_token)
  446. # Process the leave and join in one go.
  447. dir_handler.update_user_directory = True
  448. dir_handler.notify_new_event()
  449. self.wait_for_background_updates()
  450. # The user sharing tables should have been updated.
  451. public3 = self.get_success(self.user_dir_helper.get_users_in_public_rooms())
  452. self.assertEqual(set(public3), {(alice, room2), (bob, room2)})
  453. def test_per_room_profile_doesnt_alter_directory_entry(self) -> None:
  454. alice = self.register_user("alice", "pass")
  455. alice_token = self.login(alice, "pass")
  456. bob = self.register_user("bob", "pass")
  457. # Alice should have a user directory entry created at registration.
  458. users = self.get_success(self.user_dir_helper.get_profiles_in_user_directory())
  459. self.assertEqual(
  460. users[alice], ProfileInfo(display_name="alice", avatar_url=None)
  461. )
  462. # Alice makes a room for herself.
  463. room = self.helper.create_room_as(alice, is_public=True, tok=alice_token)
  464. # Alice sets a nickname unique to that room.
  465. self.helper.send_state(
  466. room,
  467. "m.room.member",
  468. {
  469. "displayname": "Freddy Mercury",
  470. "membership": "join",
  471. },
  472. alice_token,
  473. state_key=alice,
  474. )
  475. # Alice's display name remains the same in the user directory.
  476. search_result = self.get_success(self.handler.search_users(bob, alice, 10))
  477. self.assertEqual(
  478. search_result["results"],
  479. [{"display_name": "alice", "avatar_url": None, "user_id": alice}],
  480. 0,
  481. )
  482. def test_making_room_public_doesnt_alter_directory_entry(self) -> None:
  483. """Per-room names shouldn't go to the directory when the room becomes public.
  484. This isn't about preventing a leak (the room is now public, so the nickname
  485. is too). It's about preserving the invariant that we only show a user's public
  486. profile in the user directory results.
  487. I made this a Synapse test case rather than a Complement one because
  488. I think this is (strictly speaking) an implementation choice. Synapse
  489. has chosen to only ever use the public profile when responding to a user
  490. directory search. There's no privacy leak here, because making the room
  491. public discloses the per-room name.
  492. The spec doesn't mandate anything about _how_ a user
  493. should appear in a /user_directory/search result. Hypothetical example:
  494. suppose Bob searches for Alice. When representing Alice in a search
  495. result, it's reasonable to use any of Alice's nicknames that Bob is
  496. aware of. Heck, maybe we even want to use lots of them in a combined
  497. displayname like `Alice (aka "ali", "ally", "41iC3")`.
  498. """
  499. # TODO the same should apply when Alice is a remote user.
  500. alice = self.register_user("alice", "pass")
  501. alice_token = self.login(alice, "pass")
  502. bob = self.register_user("bob", "pass")
  503. bob_token = self.login(bob, "pass")
  504. # Alice and Bob are in a private room.
  505. room = self.helper.create_room_as(alice, is_public=False, tok=alice_token)
  506. self.helper.invite(room, src=alice, targ=bob, tok=alice_token)
  507. self.helper.join(room, user=bob, tok=bob_token)
  508. # Alice has a nickname unique to that room.
  509. self.helper.send_state(
  510. room,
  511. "m.room.member",
  512. {
  513. "displayname": "Freddy Mercury",
  514. "membership": "join",
  515. },
  516. alice_token,
  517. state_key=alice,
  518. )
  519. # Check Alice isn't recorded as being in a public room.
  520. public = self.get_success(self.user_dir_helper.get_users_in_public_rooms())
  521. self.assertNotIn((alice, room), public)
  522. # One of them makes the room public.
  523. self.helper.send_state(
  524. room,
  525. "m.room.join_rules",
  526. {"join_rule": "public"},
  527. alice_token,
  528. )
  529. # Check that Alice is now recorded as being in a public room
  530. public = self.get_success(self.user_dir_helper.get_users_in_public_rooms())
  531. self.assertIn((alice, room), public)
  532. # Alice's display name remains the same in the user directory.
  533. search_result = self.get_success(self.handler.search_users(bob, alice, 10))
  534. self.assertEqual(
  535. search_result["results"],
  536. [{"display_name": "alice", "avatar_url": None, "user_id": alice}],
  537. 0,
  538. )
  539. def test_private_room(self) -> None:
  540. """
  541. A user can be searched for only by people that are either in a public
  542. room, or that share a private chat.
  543. """
  544. u1 = self.register_user("user1", "pass")
  545. u1_token = self.login(u1, "pass")
  546. u2 = self.register_user("user2", "pass")
  547. u2_token = self.login(u2, "pass")
  548. u3 = self.register_user("user3", "pass")
  549. # We do not add users to the directory until they join a room.
  550. s = self.get_success(self.handler.search_users(u1, "user2", 10))
  551. self.assertEqual(len(s["results"]), 0)
  552. room = self.helper.create_room_as(u1, is_public=False, tok=u1_token)
  553. self.helper.invite(room, src=u1, targ=u2, tok=u1_token)
  554. self.helper.join(room, user=u2, tok=u2_token)
  555. # Check we have populated the database correctly.
  556. shares_private = self.get_success(
  557. self.user_dir_helper.get_users_who_share_private_rooms()
  558. )
  559. public_users = self.get_success(
  560. self.user_dir_helper.get_users_in_public_rooms()
  561. )
  562. self.assertEqual(shares_private, {(u1, u2, room), (u2, u1, room)})
  563. self.assertEqual(public_users, set())
  564. # We get one search result when searching for user2 by user1.
  565. s = self.get_success(self.handler.search_users(u1, "user2", 10))
  566. self.assertEqual(len(s["results"]), 1)
  567. # We get NO search results when searching for user2 by user3.
  568. s = self.get_success(self.handler.search_users(u3, "user2", 10))
  569. self.assertEqual(len(s["results"]), 0)
  570. # We get NO search results when searching for user3 by user1.
  571. s = self.get_success(self.handler.search_users(u1, "user3", 10))
  572. self.assertEqual(len(s["results"]), 0)
  573. # User 2 then leaves.
  574. self.helper.leave(room, user=u2, tok=u2_token)
  575. # Check we have removed the values.
  576. shares_private = self.get_success(
  577. self.user_dir_helper.get_users_who_share_private_rooms()
  578. )
  579. public_users = self.get_success(
  580. self.user_dir_helper.get_users_in_public_rooms()
  581. )
  582. self.assertEqual(shares_private, set())
  583. self.assertEqual(public_users, set())
  584. # User1 now gets no search results for any of the other users.
  585. s = self.get_success(self.handler.search_users(u1, "user2", 10))
  586. self.assertEqual(len(s["results"]), 0)
  587. s = self.get_success(self.handler.search_users(u1, "user3", 10))
  588. self.assertEqual(len(s["results"]), 0)
  589. def test_spam_checker(self) -> None:
  590. """
  591. A user which fails the spam checks will not appear in search results.
  592. """
  593. u1 = self.register_user("user1", "pass")
  594. u1_token = self.login(u1, "pass")
  595. u2 = self.register_user("user2", "pass")
  596. u2_token = self.login(u2, "pass")
  597. # We do not add users to the directory until they join a room.
  598. s = self.get_success(self.handler.search_users(u1, "user2", 10))
  599. self.assertEqual(len(s["results"]), 0)
  600. room = self.helper.create_room_as(u1, is_public=False, tok=u1_token)
  601. self.helper.invite(room, src=u1, targ=u2, tok=u1_token)
  602. self.helper.join(room, user=u2, tok=u2_token)
  603. # Check we have populated the database correctly.
  604. shares_private = self.get_success(
  605. self.user_dir_helper.get_users_who_share_private_rooms()
  606. )
  607. public_users = self.get_success(
  608. self.user_dir_helper.get_users_in_public_rooms()
  609. )
  610. self.assertEqual(shares_private, {(u1, u2, room), (u2, u1, room)})
  611. self.assertEqual(public_users, set())
  612. # We get one search result when searching for user2 by user1.
  613. s = self.get_success(self.handler.search_users(u1, "user2", 10))
  614. self.assertEqual(len(s["results"]), 1)
  615. async def allow_all(user_profile: ProfileInfo) -> bool:
  616. # Allow all users.
  617. return False
  618. # Configure a spam checker that does not filter any users.
  619. spam_checker = self.hs.get_spam_checker()
  620. spam_checker._check_username_for_spam_callbacks = [allow_all]
  621. # The results do not change:
  622. # We get one search result when searching for user2 by user1.
  623. s = self.get_success(self.handler.search_users(u1, "user2", 10))
  624. self.assertEqual(len(s["results"]), 1)
  625. # Configure a spam checker that filters all users.
  626. async def block_all(user_profile: ProfileInfo) -> bool:
  627. # All users are spammy.
  628. return True
  629. spam_checker._check_username_for_spam_callbacks = [block_all]
  630. # User1 now gets no search results for any of the other users.
  631. s = self.get_success(self.handler.search_users(u1, "user2", 10))
  632. self.assertEqual(len(s["results"]), 0)
  633. def test_legacy_spam_checker(self) -> None:
  634. """
  635. A spam checker without the expected method should be ignored.
  636. """
  637. u1 = self.register_user("user1", "pass")
  638. u1_token = self.login(u1, "pass")
  639. u2 = self.register_user("user2", "pass")
  640. u2_token = self.login(u2, "pass")
  641. # We do not add users to the directory until they join a room.
  642. s = self.get_success(self.handler.search_users(u1, "user2", 10))
  643. self.assertEqual(len(s["results"]), 0)
  644. room = self.helper.create_room_as(u1, is_public=False, tok=u1_token)
  645. self.helper.invite(room, src=u1, targ=u2, tok=u1_token)
  646. self.helper.join(room, user=u2, tok=u2_token)
  647. # Check we have populated the database correctly.
  648. shares_private = self.get_success(
  649. self.user_dir_helper.get_users_who_share_private_rooms()
  650. )
  651. public_users = self.get_success(
  652. self.user_dir_helper.get_users_in_public_rooms()
  653. )
  654. self.assertEqual(shares_private, {(u1, u2, room), (u2, u1, room)})
  655. self.assertEqual(public_users, set())
  656. # Configure a spam checker.
  657. spam_checker = self.hs.get_spam_checker()
  658. # The spam checker doesn't need any methods, so create a bare object.
  659. spam_checker.spam_checker = object()
  660. # We get one search result when searching for user2 by user1.
  661. s = self.get_success(self.handler.search_users(u1, "user2", 10))
  662. self.assertEqual(len(s["results"]), 1)
  663. def test_initial_share_all_users(self) -> None:
  664. """
  665. Search all users = True means that a user does not have to share a
  666. private room with the searching user or be in a public room to be search
  667. visible.
  668. """
  669. self.handler.search_all_users = True
  670. self.hs.config.userdirectory.user_directory_search_all_users = True
  671. u1 = self.register_user("user1", "pass")
  672. self.register_user("user2", "pass")
  673. u3 = self.register_user("user3", "pass")
  674. shares_private = self.get_success(
  675. self.user_dir_helper.get_users_who_share_private_rooms()
  676. )
  677. public_users = self.get_success(
  678. self.user_dir_helper.get_users_in_public_rooms()
  679. )
  680. # No users share rooms
  681. self.assertEqual(public_users, set())
  682. self.assertEqual(shares_private, set())
  683. # Despite not sharing a room, search_all_users means we get a search
  684. # result.
  685. s = self.get_success(self.handler.search_users(u1, u3, 10))
  686. self.assertEqual(len(s["results"]), 1)
  687. # We can find the other two users
  688. s = self.get_success(self.handler.search_users(u1, "user", 10))
  689. self.assertEqual(len(s["results"]), 2)
  690. # Registering a user and then searching for them works.
  691. u4 = self.register_user("user4", "pass")
  692. s = self.get_success(self.handler.search_users(u1, u4, 10))
  693. self.assertEqual(len(s["results"]), 1)
  694. @override_config(
  695. {
  696. "user_directory": {
  697. "enabled": True,
  698. "search_all_users": True,
  699. "prefer_local_users": True,
  700. }
  701. }
  702. )
  703. def test_prefer_local_users(self) -> None:
  704. """Tests that local users are shown higher in search results when
  705. user_directory.prefer_local_users is True.
  706. """
  707. # Create a room and few users to test the directory with
  708. searching_user = self.register_user("searcher", "password")
  709. searching_user_tok = self.login("searcher", "password")
  710. room_id = self.helper.create_room_as(
  711. searching_user,
  712. room_version=RoomVersions.V1.identifier,
  713. tok=searching_user_tok,
  714. )
  715. # Create a few local users and join them to the room
  716. local_user_1 = self.register_user("user_xxxxx", "password")
  717. local_user_2 = self.register_user("user_bbbbb", "password")
  718. local_user_3 = self.register_user("user_zzzzz", "password")
  719. self._add_user_to_room(room_id, RoomVersions.V1, local_user_1)
  720. self._add_user_to_room(room_id, RoomVersions.V1, local_user_2)
  721. self._add_user_to_room(room_id, RoomVersions.V1, local_user_3)
  722. # Create a few "remote" users and join them to the room
  723. remote_user_1 = "@user_aaaaa:remote_server"
  724. remote_user_2 = "@user_yyyyy:remote_server"
  725. remote_user_3 = "@user_ccccc:remote_server"
  726. self._add_user_to_room(room_id, RoomVersions.V1, remote_user_1)
  727. self._add_user_to_room(room_id, RoomVersions.V1, remote_user_2)
  728. self._add_user_to_room(room_id, RoomVersions.V1, remote_user_3)
  729. local_users = [local_user_1, local_user_2, local_user_3]
  730. remote_users = [remote_user_1, remote_user_2, remote_user_3]
  731. # The local searching user searches for the term "user", which other users have
  732. # in their user id
  733. results = self.get_success(
  734. self.handler.search_users(searching_user, "user", 20)
  735. )["results"]
  736. received_user_id_ordering = [result["user_id"] for result in results]
  737. # Typically we'd expect Synapse to return users in lexicographical order,
  738. # assuming they have similar User IDs/display names, and profile information.
  739. # Check that the order of returned results using our module is as we expect,
  740. # i.e our local users show up first, despite all users having lexographically mixed
  741. # user IDs.
  742. [self.assertIn(user, local_users) for user in received_user_id_ordering[:3]]
  743. [self.assertIn(user, remote_users) for user in received_user_id_ordering[3:]]
  744. def _add_user_to_room(
  745. self,
  746. room_id: str,
  747. room_version: RoomVersion,
  748. user_id: str,
  749. ) -> None:
  750. # Add a user to the room.
  751. builder = self.event_builder_factory.for_room_version(
  752. room_version,
  753. {
  754. "type": "m.room.member",
  755. "sender": user_id,
  756. "state_key": user_id,
  757. "room_id": room_id,
  758. "content": {"membership": "join"},
  759. },
  760. )
  761. event, context = self.get_success(
  762. self.event_creation_handler.create_new_client_event(builder)
  763. )
  764. self.get_success(
  765. self.hs.get_storage().persistence.persist_event(event, context)
  766. )
  767. class TestUserDirSearchDisabled(unittest.HomeserverTestCase):
  768. servlets = [
  769. user_directory.register_servlets,
  770. room.register_servlets,
  771. login.register_servlets,
  772. synapse.rest.admin.register_servlets_for_client_rest_resource,
  773. ]
  774. def make_homeserver(self, reactor: MemoryReactor, clock: Clock) -> HomeServer:
  775. config = self.default_config()
  776. config["update_user_directory"] = True
  777. hs = self.setup_test_homeserver(config=config)
  778. self.config = hs.config
  779. return hs
  780. def test_disabling_room_list(self) -> None:
  781. self.config.userdirectory.user_directory_search_enabled = True
  782. # Create two users and put them in the same room.
  783. u1 = self.register_user("user1", "pass")
  784. u1_token = self.login(u1, "pass")
  785. u2 = self.register_user("user2", "pass")
  786. u2_token = self.login(u2, "pass")
  787. room = self.helper.create_room_as(u1, tok=u1_token)
  788. self.helper.join(room, user=u2, tok=u2_token)
  789. # Each should see the other when searching the user directory.
  790. channel = self.make_request(
  791. "POST",
  792. b"user_directory/search",
  793. b'{"search_term":"user2"}',
  794. access_token=u1_token,
  795. )
  796. self.assertEquals(200, channel.code, channel.result)
  797. self.assertTrue(len(channel.json_body["results"]) > 0)
  798. # Disable user directory and check search returns nothing
  799. self.config.userdirectory.user_directory_search_enabled = False
  800. channel = self.make_request(
  801. "POST",
  802. b"user_directory/search",
  803. b'{"search_term":"user2"}',
  804. access_token=u1_token,
  805. )
  806. self.assertEquals(200, channel.code, channel.result)
  807. self.assertTrue(len(channel.json_body["results"]) == 0)