test_user_directory.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587
  1. # -*- coding: utf-8 -*-
  2. # Copyright 2018 New Vector
  3. #
  4. # Licensed under the Apache License, Version 2.0 (the "License");
  5. # you may not use this file except in compliance with the License.
  6. # You may obtain a copy of the License at
  7. #
  8. # http://www.apache.org/licenses/LICENSE-2.0
  9. #
  10. # Unless required by applicable law or agreed to in writing, software
  11. # distributed under the License is distributed on an "AS IS" BASIS,
  12. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. # See the License for the specific language governing permissions and
  14. # limitations under the License.
  15. from mock import Mock
  16. from twisted.internet import defer
  17. import synapse.rest.admin
  18. from synapse.api.constants import EventTypes, RoomEncryptionAlgorithms, UserTypes
  19. from synapse.rest.client.v1 import login, room
  20. from synapse.rest.client.v2_alpha import user_directory
  21. from synapse.storage.roommember import ProfileInfo
  22. from tests import unittest
  23. from tests.unittest import override_config
  24. class UserDirectoryTestCase(unittest.HomeserverTestCase):
  25. """
  26. Tests the UserDirectoryHandler.
  27. """
  28. servlets = [
  29. login.register_servlets,
  30. synapse.rest.admin.register_servlets_for_client_rest_resource,
  31. room.register_servlets,
  32. ]
  33. def make_homeserver(self, reactor, clock):
  34. config = self.default_config()
  35. config["update_user_directory"] = True
  36. return self.setup_test_homeserver(config=config)
  37. def prepare(self, reactor, clock, hs):
  38. self.store = hs.get_datastore()
  39. self.handler = hs.get_user_directory_handler()
  40. def test_handle_local_profile_change_with_support_user(self):
  41. support_user_id = "@support:test"
  42. self.get_success(
  43. self.store.register_user(
  44. user_id=support_user_id, password_hash=None, user_type=UserTypes.SUPPORT
  45. )
  46. )
  47. regular_user_id = "@regular:test"
  48. self.get_success(
  49. self.store.register_user(user_id=regular_user_id, password_hash=None)
  50. )
  51. self.get_success(
  52. self.handler.handle_local_profile_change(support_user_id, None)
  53. )
  54. profile = self.get_success(self.store.get_user_in_directory(support_user_id))
  55. self.assertTrue(profile is None)
  56. display_name = "display_name"
  57. profile_info = ProfileInfo(avatar_url="avatar_url", display_name=display_name)
  58. self.get_success(
  59. self.handler.handle_local_profile_change(regular_user_id, profile_info)
  60. )
  61. profile = self.get_success(self.store.get_user_in_directory(regular_user_id))
  62. self.assertTrue(profile["display_name"] == display_name)
  63. def test_handle_local_profile_change_with_deactivated_user(self):
  64. # create user
  65. r_user_id = "@regular:test"
  66. self.get_success(
  67. self.store.register_user(user_id=r_user_id, password_hash=None)
  68. )
  69. # update profile
  70. display_name = "Regular User"
  71. profile_info = ProfileInfo(avatar_url="avatar_url", display_name=display_name)
  72. self.get_success(
  73. self.handler.handle_local_profile_change(r_user_id, profile_info)
  74. )
  75. # profile is in directory
  76. profile = self.get_success(self.store.get_user_in_directory(r_user_id))
  77. self.assertTrue(profile["display_name"] == display_name)
  78. # deactivate user
  79. self.get_success(self.store.set_user_deactivated_status(r_user_id, True))
  80. self.get_success(self.handler.handle_user_deactivated(r_user_id))
  81. # profile is not in directory
  82. profile = self.get_success(self.store.get_user_in_directory(r_user_id))
  83. self.assertTrue(profile is None)
  84. # update profile after deactivation
  85. self.get_success(
  86. self.handler.handle_local_profile_change(r_user_id, profile_info)
  87. )
  88. # profile is furthermore not in directory
  89. profile = self.get_success(self.store.get_user_in_directory(r_user_id))
  90. self.assertTrue(profile is None)
  91. def test_handle_user_deactivated_support_user(self):
  92. s_user_id = "@support:test"
  93. self.get_success(
  94. self.store.register_user(
  95. user_id=s_user_id, password_hash=None, user_type=UserTypes.SUPPORT
  96. )
  97. )
  98. self.store.remove_from_user_dir = Mock(return_value=defer.succeed(None))
  99. self.get_success(self.handler.handle_user_deactivated(s_user_id))
  100. self.store.remove_from_user_dir.not_called()
  101. def test_handle_user_deactivated_regular_user(self):
  102. r_user_id = "@regular:test"
  103. self.get_success(
  104. self.store.register_user(user_id=r_user_id, password_hash=None)
  105. )
  106. self.store.remove_from_user_dir = Mock(return_value=defer.succeed(None))
  107. self.get_success(self.handler.handle_user_deactivated(r_user_id))
  108. self.store.remove_from_user_dir.called_once_with(r_user_id)
  109. def test_private_room(self):
  110. """
  111. A user can be searched for only by people that are either in a public
  112. room, or that share a private chat.
  113. """
  114. u1 = self.register_user("user1", "pass")
  115. u1_token = self.login(u1, "pass")
  116. u2 = self.register_user("user2", "pass")
  117. u2_token = self.login(u2, "pass")
  118. u3 = self.register_user("user3", "pass")
  119. # We do not add users to the directory until they join a room.
  120. s = self.get_success(self.handler.search_users(u1, "user2", 10))
  121. self.assertEqual(len(s["results"]), 0)
  122. room = self.helper.create_room_as(u1, is_public=False, tok=u1_token)
  123. self.helper.invite(room, src=u1, targ=u2, tok=u1_token)
  124. self.helper.join(room, user=u2, tok=u2_token)
  125. # Check we have populated the database correctly.
  126. shares_private = self.get_users_who_share_private_rooms()
  127. public_users = self.get_users_in_public_rooms()
  128. self.assertEqual(
  129. self._compress_shared(shares_private), {(u1, u2, room), (u2, u1, room)}
  130. )
  131. self.assertEqual(public_users, [])
  132. # We get one search result when searching for user2 by user1.
  133. s = self.get_success(self.handler.search_users(u1, "user2", 10))
  134. self.assertEqual(len(s["results"]), 1)
  135. # We get NO search results when searching for user2 by user3.
  136. s = self.get_success(self.handler.search_users(u3, "user2", 10))
  137. self.assertEqual(len(s["results"]), 0)
  138. # We get NO search results when searching for user3 by user1.
  139. s = self.get_success(self.handler.search_users(u1, "user3", 10))
  140. self.assertEqual(len(s["results"]), 0)
  141. # User 2 then leaves.
  142. self.helper.leave(room, user=u2, tok=u2_token)
  143. # Check we have removed the values.
  144. shares_private = self.get_users_who_share_private_rooms()
  145. public_users = self.get_users_in_public_rooms()
  146. self.assertEqual(self._compress_shared(shares_private), set())
  147. self.assertEqual(public_users, [])
  148. # User1 now gets no search results for any of the other users.
  149. s = self.get_success(self.handler.search_users(u1, "user2", 10))
  150. self.assertEqual(len(s["results"]), 0)
  151. s = self.get_success(self.handler.search_users(u1, "user3", 10))
  152. self.assertEqual(len(s["results"]), 0)
  153. @override_config({"encryption_enabled_by_default_for_room_type": "all"})
  154. def test_encrypted_by_default_config_option_all(self):
  155. """Tests that invite-only and non-invite-only rooms have encryption enabled by
  156. default when the config option encryption_enabled_by_default_for_room_type is "all".
  157. """
  158. # Create a user
  159. user = self.register_user("user", "pass")
  160. user_token = self.login(user, "pass")
  161. # Create an invite-only room as that user
  162. room_id = self.helper.create_room_as(user, is_public=False, tok=user_token)
  163. # Check that the room has an encryption state event
  164. event_content = self.helper.get_state(
  165. room_id=room_id, event_type=EventTypes.RoomEncryption, tok=user_token,
  166. )
  167. self.assertEqual(event_content, {"algorithm": RoomEncryptionAlgorithms.DEFAULT})
  168. # Create a non invite-only room as that user
  169. room_id = self.helper.create_room_as(user, is_public=True, tok=user_token)
  170. # Check that the room has an encryption state event
  171. event_content = self.helper.get_state(
  172. room_id=room_id, event_type=EventTypes.RoomEncryption, tok=user_token,
  173. )
  174. self.assertEqual(event_content, {"algorithm": RoomEncryptionAlgorithms.DEFAULT})
  175. @override_config({"encryption_enabled_by_default_for_room_type": "invite"})
  176. def test_encrypted_by_default_config_option_invite(self):
  177. """Tests that only new, invite-only rooms have encryption enabled by default when
  178. the config option encryption_enabled_by_default_for_room_type is "invite".
  179. """
  180. # Create a user
  181. user = self.register_user("user", "pass")
  182. user_token = self.login(user, "pass")
  183. # Create an invite-only room as that user
  184. room_id = self.helper.create_room_as(user, is_public=False, tok=user_token)
  185. # Check that the room has an encryption state event
  186. event_content = self.helper.get_state(
  187. room_id=room_id, event_type=EventTypes.RoomEncryption, tok=user_token,
  188. )
  189. self.assertEqual(event_content, {"algorithm": RoomEncryptionAlgorithms.DEFAULT})
  190. # Create a non invite-only room as that user
  191. room_id = self.helper.create_room_as(user, is_public=True, tok=user_token)
  192. # Check that the room does not have an encryption state event
  193. self.helper.get_state(
  194. room_id=room_id,
  195. event_type=EventTypes.RoomEncryption,
  196. tok=user_token,
  197. expect_code=404,
  198. )
  199. @override_config({"encryption_enabled_by_default_for_room_type": "off"})
  200. def test_encrypted_by_default_config_option_off(self):
  201. """Tests that neither new invite-only nor non-invite-only rooms have encryption
  202. enabled by default when the config option
  203. encryption_enabled_by_default_for_room_type is "off".
  204. """
  205. # Create a user
  206. user = self.register_user("user", "pass")
  207. user_token = self.login(user, "pass")
  208. # Create an invite-only room as that user
  209. room_id = self.helper.create_room_as(user, is_public=False, tok=user_token)
  210. # Check that the room does not have an encryption state event
  211. self.helper.get_state(
  212. room_id=room_id,
  213. event_type=EventTypes.RoomEncryption,
  214. tok=user_token,
  215. expect_code=404,
  216. )
  217. # Create a non invite-only room as that user
  218. room_id = self.helper.create_room_as(user, is_public=True, tok=user_token)
  219. # Check that the room does not have an encryption state event
  220. self.helper.get_state(
  221. room_id=room_id,
  222. event_type=EventTypes.RoomEncryption,
  223. tok=user_token,
  224. expect_code=404,
  225. )
  226. def test_spam_checker(self):
  227. """
  228. A user which fails the spam checks will not appear in search results.
  229. """
  230. u1 = self.register_user("user1", "pass")
  231. u1_token = self.login(u1, "pass")
  232. u2 = self.register_user("user2", "pass")
  233. u2_token = self.login(u2, "pass")
  234. # We do not add users to the directory until they join a room.
  235. s = self.get_success(self.handler.search_users(u1, "user2", 10))
  236. self.assertEqual(len(s["results"]), 0)
  237. room = self.helper.create_room_as(u1, is_public=False, tok=u1_token)
  238. self.helper.invite(room, src=u1, targ=u2, tok=u1_token)
  239. self.helper.join(room, user=u2, tok=u2_token)
  240. # Check we have populated the database correctly.
  241. shares_private = self.get_users_who_share_private_rooms()
  242. public_users = self.get_users_in_public_rooms()
  243. self.assertEqual(
  244. self._compress_shared(shares_private), {(u1, u2, room), (u2, u1, room)}
  245. )
  246. self.assertEqual(public_users, [])
  247. # We get one search result when searching for user2 by user1.
  248. s = self.get_success(self.handler.search_users(u1, "user2", 10))
  249. self.assertEqual(len(s["results"]), 1)
  250. # Configure a spam checker that does not filter any users.
  251. spam_checker = self.hs.get_spam_checker()
  252. class AllowAll:
  253. async def check_username_for_spam(self, user_profile):
  254. # Allow all users.
  255. return False
  256. spam_checker.spam_checkers = [AllowAll()]
  257. # The results do not change:
  258. # We get one search result when searching for user2 by user1.
  259. s = self.get_success(self.handler.search_users(u1, "user2", 10))
  260. self.assertEqual(len(s["results"]), 1)
  261. # Configure a spam checker that filters all users.
  262. class BlockAll:
  263. async def check_username_for_spam(self, user_profile):
  264. # All users are spammy.
  265. return True
  266. spam_checker.spam_checkers = [BlockAll()]
  267. # User1 now gets no search results for any of the other users.
  268. s = self.get_success(self.handler.search_users(u1, "user2", 10))
  269. self.assertEqual(len(s["results"]), 0)
  270. def test_legacy_spam_checker(self):
  271. """
  272. A spam checker without the expected method should be ignored.
  273. """
  274. u1 = self.register_user("user1", "pass")
  275. u1_token = self.login(u1, "pass")
  276. u2 = self.register_user("user2", "pass")
  277. u2_token = self.login(u2, "pass")
  278. # We do not add users to the directory until they join a room.
  279. s = self.get_success(self.handler.search_users(u1, "user2", 10))
  280. self.assertEqual(len(s["results"]), 0)
  281. room = self.helper.create_room_as(u1, is_public=False, tok=u1_token)
  282. self.helper.invite(room, src=u1, targ=u2, tok=u1_token)
  283. self.helper.join(room, user=u2, tok=u2_token)
  284. # Check we have populated the database correctly.
  285. shares_private = self.get_users_who_share_private_rooms()
  286. public_users = self.get_users_in_public_rooms()
  287. self.assertEqual(
  288. self._compress_shared(shares_private), {(u1, u2, room), (u2, u1, room)}
  289. )
  290. self.assertEqual(public_users, [])
  291. # Configure a spam checker.
  292. spam_checker = self.hs.get_spam_checker()
  293. # The spam checker doesn't need any methods, so create a bare object.
  294. spam_checker.spam_checker = object()
  295. # We get one search result when searching for user2 by user1.
  296. s = self.get_success(self.handler.search_users(u1, "user2", 10))
  297. self.assertEqual(len(s["results"]), 1)
  298. def _compress_shared(self, shared):
  299. """
  300. Compress a list of users who share rooms dicts to a list of tuples.
  301. """
  302. r = set()
  303. for i in shared:
  304. r.add((i["user_id"], i["other_user_id"], i["room_id"]))
  305. return r
  306. def get_users_in_public_rooms(self):
  307. r = self.get_success(
  308. self.store.db_pool.simple_select_list(
  309. "users_in_public_rooms", None, ("user_id", "room_id")
  310. )
  311. )
  312. retval = []
  313. for i in r:
  314. retval.append((i["user_id"], i["room_id"]))
  315. return retval
  316. def get_users_who_share_private_rooms(self):
  317. return self.get_success(
  318. self.store.db_pool.simple_select_list(
  319. "users_who_share_private_rooms",
  320. None,
  321. ["user_id", "other_user_id", "room_id"],
  322. )
  323. )
  324. def _add_background_updates(self):
  325. """
  326. Add the background updates we need to run.
  327. """
  328. # Ugh, have to reset this flag
  329. self.store.db_pool.updates._all_done = False
  330. self.get_success(
  331. self.store.db_pool.simple_insert(
  332. "background_updates",
  333. {
  334. "update_name": "populate_user_directory_createtables",
  335. "progress_json": "{}",
  336. },
  337. )
  338. )
  339. self.get_success(
  340. self.store.db_pool.simple_insert(
  341. "background_updates",
  342. {
  343. "update_name": "populate_user_directory_process_rooms",
  344. "progress_json": "{}",
  345. "depends_on": "populate_user_directory_createtables",
  346. },
  347. )
  348. )
  349. self.get_success(
  350. self.store.db_pool.simple_insert(
  351. "background_updates",
  352. {
  353. "update_name": "populate_user_directory_process_users",
  354. "progress_json": "{}",
  355. "depends_on": "populate_user_directory_process_rooms",
  356. },
  357. )
  358. )
  359. self.get_success(
  360. self.store.db_pool.simple_insert(
  361. "background_updates",
  362. {
  363. "update_name": "populate_user_directory_cleanup",
  364. "progress_json": "{}",
  365. "depends_on": "populate_user_directory_process_users",
  366. },
  367. )
  368. )
  369. def test_initial(self):
  370. """
  371. The user directory's initial handler correctly updates the search tables.
  372. """
  373. u1 = self.register_user("user1", "pass")
  374. u1_token = self.login(u1, "pass")
  375. u2 = self.register_user("user2", "pass")
  376. u2_token = self.login(u2, "pass")
  377. u3 = self.register_user("user3", "pass")
  378. u3_token = self.login(u3, "pass")
  379. room = self.helper.create_room_as(u1, is_public=True, tok=u1_token)
  380. self.helper.invite(room, src=u1, targ=u2, tok=u1_token)
  381. self.helper.join(room, user=u2, tok=u2_token)
  382. private_room = self.helper.create_room_as(u1, is_public=False, tok=u1_token)
  383. self.helper.invite(private_room, src=u1, targ=u3, tok=u1_token)
  384. self.helper.join(private_room, user=u3, tok=u3_token)
  385. self.get_success(self.store.update_user_directory_stream_pos(None))
  386. self.get_success(self.store.delete_all_from_user_dir())
  387. shares_private = self.get_users_who_share_private_rooms()
  388. public_users = self.get_users_in_public_rooms()
  389. # Nothing updated yet
  390. self.assertEqual(shares_private, [])
  391. self.assertEqual(public_users, [])
  392. # Do the initial population of the user directory via the background update
  393. self._add_background_updates()
  394. while not self.get_success(
  395. self.store.db_pool.updates.has_completed_background_updates()
  396. ):
  397. self.get_success(
  398. self.store.db_pool.updates.do_next_background_update(100), by=0.1
  399. )
  400. shares_private = self.get_users_who_share_private_rooms()
  401. public_users = self.get_users_in_public_rooms()
  402. # User 1 and User 2 are in the same public room
  403. self.assertEqual(set(public_users), {(u1, room), (u2, room)})
  404. # User 1 and User 3 share private rooms
  405. self.assertEqual(
  406. self._compress_shared(shares_private),
  407. {(u1, u3, private_room), (u3, u1, private_room)},
  408. )
  409. def test_initial_share_all_users(self):
  410. """
  411. Search all users = True means that a user does not have to share a
  412. private room with the searching user or be in a public room to be search
  413. visible.
  414. """
  415. self.handler.search_all_users = True
  416. self.hs.config.user_directory_search_all_users = True
  417. u1 = self.register_user("user1", "pass")
  418. self.register_user("user2", "pass")
  419. u3 = self.register_user("user3", "pass")
  420. # Wipe the user dir
  421. self.get_success(self.store.update_user_directory_stream_pos(None))
  422. self.get_success(self.store.delete_all_from_user_dir())
  423. # Do the initial population of the user directory via the background update
  424. self._add_background_updates()
  425. while not self.get_success(
  426. self.store.db_pool.updates.has_completed_background_updates()
  427. ):
  428. self.get_success(
  429. self.store.db_pool.updates.do_next_background_update(100), by=0.1
  430. )
  431. shares_private = self.get_users_who_share_private_rooms()
  432. public_users = self.get_users_in_public_rooms()
  433. # No users share rooms
  434. self.assertEqual(public_users, [])
  435. self.assertEqual(self._compress_shared(shares_private), set())
  436. # Despite not sharing a room, search_all_users means we get a search
  437. # result.
  438. s = self.get_success(self.handler.search_users(u1, u3, 10))
  439. self.assertEqual(len(s["results"]), 1)
  440. # We can find the other two users
  441. s = self.get_success(self.handler.search_users(u1, "user", 10))
  442. self.assertEqual(len(s["results"]), 2)
  443. # Registering a user and then searching for them works.
  444. u4 = self.register_user("user4", "pass")
  445. s = self.get_success(self.handler.search_users(u1, u4, 10))
  446. self.assertEqual(len(s["results"]), 1)
  447. class TestUserDirSearchDisabled(unittest.HomeserverTestCase):
  448. user_id = "@test:test"
  449. servlets = [
  450. user_directory.register_servlets,
  451. room.register_servlets,
  452. login.register_servlets,
  453. synapse.rest.admin.register_servlets_for_client_rest_resource,
  454. ]
  455. def make_homeserver(self, reactor, clock):
  456. config = self.default_config()
  457. config["update_user_directory"] = True
  458. hs = self.setup_test_homeserver(config=config)
  459. self.config = hs.config
  460. return hs
  461. def test_disabling_room_list(self):
  462. self.config.user_directory_search_enabled = True
  463. # First we create a room with another user so that user dir is non-empty
  464. # for our user
  465. self.helper.create_room_as(self.user_id)
  466. u2 = self.register_user("user2", "pass")
  467. room = self.helper.create_room_as(self.user_id)
  468. self.helper.join(room, user=u2)
  469. # Assert user directory is not empty
  470. channel = self.make_request(
  471. "POST", b"user_directory/search", b'{"search_term":"user2"}'
  472. )
  473. self.assertEquals(200, channel.code, channel.result)
  474. self.assertTrue(len(channel.json_body["results"]) > 0)
  475. # Disable user directory and check search returns nothing
  476. self.config.user_directory_search_enabled = False
  477. channel = self.make_request(
  478. "POST", b"user_directory/search", b'{"search_term":"user2"}'
  479. )
  480. self.assertEquals(200, channel.code, channel.result)
  481. self.assertTrue(len(channel.json_body["results"]) == 0)