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