1
0

test_user_directory.py 42 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055
  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. # u1 can't see u2 until they share a private room, or u1 is in a public room.
  550. s = self.get_success(self.handler.search_users(u1, "user2", 10))
  551. self.assertEqual(len(s["results"]), 0)
  552. # Get u1 and u2 into a private room.
  553. room = self.helper.create_room_as(u1, is_public=False, tok=u1_token)
  554. self.helper.invite(room, src=u1, targ=u2, tok=u1_token)
  555. self.helper.join(room, user=u2, tok=u2_token)
  556. # Check we have populated the database correctly.
  557. users, public_users, shares_private = self.get_success(
  558. self.user_dir_helper.get_tables()
  559. )
  560. self.assertEqual(users, {u1, u2, u3})
  561. self.assertEqual(shares_private, {(u1, u2, room), (u2, u1, room)})
  562. self.assertEqual(public_users, set())
  563. # We get one search result when searching for user2 by user1.
  564. s = self.get_success(self.handler.search_users(u1, "user2", 10))
  565. self.assertEqual(len(s["results"]), 1)
  566. # We get NO search results when searching for user2 by user3.
  567. s = self.get_success(self.handler.search_users(u3, "user2", 10))
  568. self.assertEqual(len(s["results"]), 0)
  569. # We get NO search results when searching for user3 by user1.
  570. s = self.get_success(self.handler.search_users(u1, "user3", 10))
  571. self.assertEqual(len(s["results"]), 0)
  572. # User 2 then leaves.
  573. self.helper.leave(room, user=u2, tok=u2_token)
  574. # Check this is reflected in the DB.
  575. users, public_users, shares_private = self.get_success(
  576. self.user_dir_helper.get_tables()
  577. )
  578. self.assertEqual(users, {u1, u2, u3})
  579. self.assertEqual(shares_private, set())
  580. self.assertEqual(public_users, set())
  581. # User1 now gets no search results for any of the other users.
  582. s = self.get_success(self.handler.search_users(u1, "user2", 10))
  583. self.assertEqual(len(s["results"]), 0)
  584. s = self.get_success(self.handler.search_users(u1, "user3", 10))
  585. self.assertEqual(len(s["results"]), 0)
  586. def test_joining_private_room_with_excluded_user(self) -> None:
  587. """
  588. When a user excluded from the user directory, E say, joins a private
  589. room, E will not appear in the `users_who_share_private_rooms` table.
  590. When a normal user, U say, joins a private room containing E, then
  591. U will appear in the `users_who_share_private_rooms` table, but E will
  592. not.
  593. """
  594. # Setup a support and two normal users.
  595. alice = self.register_user("alice", "pass")
  596. alice_token = self.login(alice, "pass")
  597. bob = self.register_user("bob", "pass")
  598. bob_token = self.login(bob, "pass")
  599. support = "@support1:test"
  600. self.get_success(
  601. self.store.register_user(
  602. user_id=support, password_hash=None, user_type=UserTypes.SUPPORT
  603. )
  604. )
  605. # Alice makes a room. Inject the support user into the room.
  606. room = self.helper.create_room_as(alice, is_public=False, tok=alice_token)
  607. self.get_success(inject_member_event(self.hs, room, support, "join"))
  608. # Check the DB state. The support user should not be in the directory.
  609. users, in_public, in_private = self.get_success(
  610. self.user_dir_helper.get_tables()
  611. )
  612. self.assertEqual(users, {alice, bob})
  613. self.assertEqual(in_public, set())
  614. self.assertEqual(in_private, set())
  615. # Then invite Bob, who accepts.
  616. self.helper.invite(room, alice, bob, tok=alice_token)
  617. self.helper.join(room, bob, tok=bob_token)
  618. # Check the DB state. The support user should not be in the directory.
  619. users, in_public, in_private = self.get_success(
  620. self.user_dir_helper.get_tables()
  621. )
  622. self.assertEqual(users, {alice, bob})
  623. self.assertEqual(in_public, set())
  624. self.assertEqual(in_private, {(alice, bob, room), (bob, alice, room)})
  625. def test_spam_checker(self) -> None:
  626. """
  627. A user which fails the spam checks will not appear in search results.
  628. """
  629. u1 = self.register_user("user1", "pass")
  630. u1_token = self.login(u1, "pass")
  631. u2 = self.register_user("user2", "pass")
  632. u2_token = self.login(u2, "pass")
  633. # We do not add users to the directory until they join a room.
  634. s = self.get_success(self.handler.search_users(u1, "user2", 10))
  635. self.assertEqual(len(s["results"]), 0)
  636. room = self.helper.create_room_as(u1, is_public=False, tok=u1_token)
  637. self.helper.invite(room, src=u1, targ=u2, tok=u1_token)
  638. self.helper.join(room, user=u2, tok=u2_token)
  639. # Check we have populated the database correctly.
  640. shares_private = self.get_success(
  641. self.user_dir_helper.get_users_who_share_private_rooms()
  642. )
  643. public_users = self.get_success(
  644. self.user_dir_helper.get_users_in_public_rooms()
  645. )
  646. self.assertEqual(shares_private, {(u1, u2, room), (u2, u1, room)})
  647. self.assertEqual(public_users, set())
  648. # We get one search result when searching for user2 by user1.
  649. s = self.get_success(self.handler.search_users(u1, "user2", 10))
  650. self.assertEqual(len(s["results"]), 1)
  651. async def allow_all(user_profile: ProfileInfo) -> bool:
  652. # Allow all users.
  653. return False
  654. # Configure a spam checker that does not filter any users.
  655. spam_checker = self.hs.get_spam_checker()
  656. spam_checker._check_username_for_spam_callbacks = [allow_all]
  657. # The results do not change:
  658. # We get one search result when searching for user2 by user1.
  659. s = self.get_success(self.handler.search_users(u1, "user2", 10))
  660. self.assertEqual(len(s["results"]), 1)
  661. # Configure a spam checker that filters all users.
  662. async def block_all(user_profile: ProfileInfo) -> bool:
  663. # All users are spammy.
  664. return True
  665. spam_checker._check_username_for_spam_callbacks = [block_all]
  666. # User1 now gets no search results for any of the other users.
  667. s = self.get_success(self.handler.search_users(u1, "user2", 10))
  668. self.assertEqual(len(s["results"]), 0)
  669. def test_legacy_spam_checker(self) -> None:
  670. """
  671. A spam checker without the expected method should be ignored.
  672. """
  673. u1 = self.register_user("user1", "pass")
  674. u1_token = self.login(u1, "pass")
  675. u2 = self.register_user("user2", "pass")
  676. u2_token = self.login(u2, "pass")
  677. # We do not add users to the directory until they join a room.
  678. s = self.get_success(self.handler.search_users(u1, "user2", 10))
  679. self.assertEqual(len(s["results"]), 0)
  680. room = self.helper.create_room_as(u1, is_public=False, tok=u1_token)
  681. self.helper.invite(room, src=u1, targ=u2, tok=u1_token)
  682. self.helper.join(room, user=u2, tok=u2_token)
  683. # Check we have populated the database correctly.
  684. shares_private = self.get_success(
  685. self.user_dir_helper.get_users_who_share_private_rooms()
  686. )
  687. public_users = self.get_success(
  688. self.user_dir_helper.get_users_in_public_rooms()
  689. )
  690. self.assertEqual(shares_private, {(u1, u2, room), (u2, u1, room)})
  691. self.assertEqual(public_users, set())
  692. # Configure a spam checker.
  693. spam_checker = self.hs.get_spam_checker()
  694. # The spam checker doesn't need any methods, so create a bare object.
  695. spam_checker.spam_checker = object()
  696. # We get one search result when searching for user2 by user1.
  697. s = self.get_success(self.handler.search_users(u1, "user2", 10))
  698. self.assertEqual(len(s["results"]), 1)
  699. def test_initial_share_all_users(self) -> None:
  700. """
  701. Search all users = True means that a user does not have to share a
  702. private room with the searching user or be in a public room to be search
  703. visible.
  704. """
  705. self.handler.search_all_users = True
  706. self.hs.config.userdirectory.user_directory_search_all_users = True
  707. u1 = self.register_user("user1", "pass")
  708. self.register_user("user2", "pass")
  709. u3 = self.register_user("user3", "pass")
  710. shares_private = self.get_success(
  711. self.user_dir_helper.get_users_who_share_private_rooms()
  712. )
  713. public_users = self.get_success(
  714. self.user_dir_helper.get_users_in_public_rooms()
  715. )
  716. # No users share rooms
  717. self.assertEqual(public_users, set())
  718. self.assertEqual(shares_private, set())
  719. # Despite not sharing a room, search_all_users means we get a search
  720. # result.
  721. s = self.get_success(self.handler.search_users(u1, u3, 10))
  722. self.assertEqual(len(s["results"]), 1)
  723. # We can find the other two users
  724. s = self.get_success(self.handler.search_users(u1, "user", 10))
  725. self.assertEqual(len(s["results"]), 2)
  726. # Registering a user and then searching for them works.
  727. u4 = self.register_user("user4", "pass")
  728. s = self.get_success(self.handler.search_users(u1, u4, 10))
  729. self.assertEqual(len(s["results"]), 1)
  730. @override_config(
  731. {
  732. "user_directory": {
  733. "enabled": True,
  734. "search_all_users": True,
  735. "prefer_local_users": True,
  736. }
  737. }
  738. )
  739. def test_prefer_local_users(self) -> None:
  740. """Tests that local users are shown higher in search results when
  741. user_directory.prefer_local_users is True.
  742. """
  743. # Create a room and few users to test the directory with
  744. searching_user = self.register_user("searcher", "password")
  745. searching_user_tok = self.login("searcher", "password")
  746. room_id = self.helper.create_room_as(
  747. searching_user,
  748. room_version=RoomVersions.V1.identifier,
  749. tok=searching_user_tok,
  750. )
  751. # Create a few local users and join them to the room
  752. local_user_1 = self.register_user("user_xxxxx", "password")
  753. local_user_2 = self.register_user("user_bbbbb", "password")
  754. local_user_3 = self.register_user("user_zzzzz", "password")
  755. self._add_user_to_room(room_id, RoomVersions.V1, local_user_1)
  756. self._add_user_to_room(room_id, RoomVersions.V1, local_user_2)
  757. self._add_user_to_room(room_id, RoomVersions.V1, local_user_3)
  758. # Create a few "remote" users and join them to the room
  759. remote_user_1 = "@user_aaaaa:remote_server"
  760. remote_user_2 = "@user_yyyyy:remote_server"
  761. remote_user_3 = "@user_ccccc:remote_server"
  762. self._add_user_to_room(room_id, RoomVersions.V1, remote_user_1)
  763. self._add_user_to_room(room_id, RoomVersions.V1, remote_user_2)
  764. self._add_user_to_room(room_id, RoomVersions.V1, remote_user_3)
  765. local_users = [local_user_1, local_user_2, local_user_3]
  766. remote_users = [remote_user_1, remote_user_2, remote_user_3]
  767. # The local searching user searches for the term "user", which other users have
  768. # in their user id
  769. results = self.get_success(
  770. self.handler.search_users(searching_user, "user", 20)
  771. )["results"]
  772. received_user_id_ordering = [result["user_id"] for result in results]
  773. # Typically we'd expect Synapse to return users in lexicographical order,
  774. # assuming they have similar User IDs/display names, and profile information.
  775. # Check that the order of returned results using our module is as we expect,
  776. # i.e our local users show up first, despite all users having lexographically mixed
  777. # user IDs.
  778. [self.assertIn(user, local_users) for user in received_user_id_ordering[:3]]
  779. [self.assertIn(user, remote_users) for user in received_user_id_ordering[3:]]
  780. def _add_user_to_room(
  781. self,
  782. room_id: str,
  783. room_version: RoomVersion,
  784. user_id: str,
  785. ) -> None:
  786. # Add a user to the room.
  787. builder = self.event_builder_factory.for_room_version(
  788. room_version,
  789. {
  790. "type": "m.room.member",
  791. "sender": user_id,
  792. "state_key": user_id,
  793. "room_id": room_id,
  794. "content": {"membership": "join"},
  795. },
  796. )
  797. event, context = self.get_success(
  798. self.event_creation_handler.create_new_client_event(builder)
  799. )
  800. self.get_success(
  801. self.hs.get_storage().persistence.persist_event(event, context)
  802. )
  803. def test_local_user_leaving_room_remains_in_user_directory(self) -> None:
  804. """We've chosen to simplify the user directory's implementation by
  805. always including local users. Ensure this invariant is maintained when
  806. a local user
  807. - leaves a room, and
  808. - leaves the last room they're in which is visible to this server.
  809. This is user-visible if the "search_all_users" config option is on: the
  810. local user who left a room would no longer be searchable if this test fails!
  811. """
  812. alice = self.register_user("alice", "pass")
  813. alice_token = self.login(alice, "pass")
  814. bob = self.register_user("bob", "pass")
  815. bob_token = self.login(bob, "pass")
  816. # Alice makes two public rooms, which Bob joins.
  817. room1 = self.helper.create_room_as(alice, is_public=True, tok=alice_token)
  818. room2 = self.helper.create_room_as(alice, is_public=True, tok=alice_token)
  819. self.helper.join(room1, bob, tok=bob_token)
  820. self.helper.join(room2, bob, tok=bob_token)
  821. # The user directory tables are updated.
  822. users, in_public, in_private = self.get_success(
  823. self.user_dir_helper.get_tables()
  824. )
  825. self.assertEqual(users, {alice, bob})
  826. self.assertEqual(
  827. in_public, {(alice, room1), (alice, room2), (bob, room1), (bob, room2)}
  828. )
  829. self.assertEqual(in_private, set())
  830. # Alice leaves one room. She should still be in the directory.
  831. self.helper.leave(room1, alice, tok=alice_token)
  832. users, in_public, in_private = self.get_success(
  833. self.user_dir_helper.get_tables()
  834. )
  835. self.assertEqual(users, {alice, bob})
  836. self.assertEqual(in_public, {(alice, room2), (bob, room1), (bob, room2)})
  837. self.assertEqual(in_private, set())
  838. # Alice leaves the other. She should still be in the directory.
  839. self.helper.leave(room2, alice, tok=alice_token)
  840. self.wait_for_background_updates()
  841. users, in_public, in_private = self.get_success(
  842. self.user_dir_helper.get_tables()
  843. )
  844. self.assertEqual(users, {alice, bob})
  845. self.assertEqual(in_public, {(bob, room1), (bob, room2)})
  846. self.assertEqual(in_private, set())
  847. class TestUserDirSearchDisabled(unittest.HomeserverTestCase):
  848. servlets = [
  849. user_directory.register_servlets,
  850. room.register_servlets,
  851. login.register_servlets,
  852. synapse.rest.admin.register_servlets_for_client_rest_resource,
  853. ]
  854. def make_homeserver(self, reactor: MemoryReactor, clock: Clock) -> HomeServer:
  855. config = self.default_config()
  856. config["update_user_directory"] = True
  857. hs = self.setup_test_homeserver(config=config)
  858. self.config = hs.config
  859. return hs
  860. def test_disabling_room_list(self) -> None:
  861. self.config.userdirectory.user_directory_search_enabled = True
  862. # Create two users and put them in the same room.
  863. u1 = self.register_user("user1", "pass")
  864. u1_token = self.login(u1, "pass")
  865. u2 = self.register_user("user2", "pass")
  866. u2_token = self.login(u2, "pass")
  867. room = self.helper.create_room_as(u1, tok=u1_token)
  868. self.helper.join(room, user=u2, tok=u2_token)
  869. # Each should see the other when searching the user directory.
  870. channel = self.make_request(
  871. "POST",
  872. b"user_directory/search",
  873. b'{"search_term":"user2"}',
  874. access_token=u1_token,
  875. )
  876. self.assertEquals(200, channel.code, channel.result)
  877. self.assertTrue(len(channel.json_body["results"]) > 0)
  878. # Disable user directory and check search returns nothing
  879. self.config.userdirectory.user_directory_search_enabled = False
  880. channel = self.make_request(
  881. "POST",
  882. b"user_directory/search",
  883. b'{"search_term":"user2"}',
  884. access_token=u1_token,
  885. )
  886. self.assertEquals(200, channel.code, channel.result)
  887. self.assertTrue(len(channel.json_body["results"]) == 0)