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