test_user_directory.py 21 KB

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