Consistently exclude from user_directory (#10960)

* Introduce `should_include_local_users_in_dir`

We exclude three kinds of local users from the user_directory tables. At
present we don't consistently exclude all three in the same places. This
commit introduces a new function to gather those exclusion conditions
together. Because we have to handle local and remote users in different
ways, I've made that function only consider the case of remote users.
It's the caller's responsibility to make the local versus remote
distinction clear and correct.

A test fixup is required. The test now hits a path which makes db
queries against the users table. The expected rows were missing, because
we were using a dummy user that hadn't actually been registered.

We also add new test cases to covert the exclusion logic.

----

By my reading this makes these changes:

* When an app service user registers or changes their profile, they will
  _not_ be added to the user directory. (Previously only support and
  deactivated users were excluded). This is consistent with the logic that
  rebuilds the user directory. See also [the discussion
  here](https://github.com/matrix-org/synapse/pull/10914#discussion_r716859548).
* When rebuilding the directory, exclude support and disabled users from
  room sharing tables. Previously only appservice users were excluded.
* Exclude all three categories of local users when rebuilding the
  directory. Previously `_populate_user_directory_process_users` didn't do
  any exclusion.

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
This commit is contained in:
David Robertson 2021-10-04 12:45:51 +01:00 committed by GitHub
parent a0f48ee89d
commit f7b034a24b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 409 additions and 57 deletions

View file

@ -11,6 +11,7 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from typing import Tuple
from unittest.mock import Mock, patch
from urllib.parse import quote
@ -20,7 +21,8 @@ from twisted.test.proto_helpers import MemoryReactor
import synapse.rest.admin
from synapse.api.constants import UserTypes
from synapse.api.room_versions import RoomVersion, RoomVersions
from synapse.rest.client import login, room, user_directory
from synapse.appservice import ApplicationService
from synapse.rest.client import login, register, room, user_directory
from synapse.server import HomeServer
from synapse.storage.roommember import ProfileInfo
from synapse.types import create_requester
@ -28,6 +30,7 @@ from synapse.util import Clock
from tests import unittest
from tests.storage.test_user_directory import GetUserDirectoryTables
from tests.test_utils.event_injection import inject_member_event
from tests.unittest import override_config
@ -47,13 +50,29 @@ class UserDirectoryTestCase(unittest.HomeserverTestCase):
servlets = [
login.register_servlets,
synapse.rest.admin.register_servlets,
register.register_servlets,
room.register_servlets,
]
def make_homeserver(self, reactor: MemoryReactor, clock: Clock) -> HomeServer:
config = self.default_config()
config["update_user_directory"] = True
return self.setup_test_homeserver(config=config)
self.appservice = ApplicationService(
token="i_am_an_app_service",
hostname="test",
id="1234",
namespaces={"users": [{"regex": r"@as_user.*", "exclusive": True}]},
sender="@as:test",
)
mock_load_appservices = Mock(return_value=[self.appservice])
with patch(
"synapse.storage.databases.main.appservice.load_appservices",
mock_load_appservices,
):
hs = self.setup_test_homeserver(config=config)
return hs
def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
self.store = hs.get_datastore()
@ -62,6 +81,137 @@ class UserDirectoryTestCase(unittest.HomeserverTestCase):
self.event_creation_handler = self.hs.get_event_creation_handler()
self.user_dir_helper = GetUserDirectoryTables(self.store)
def test_normal_user_pair(self) -> None:
"""Sanity check that the room-sharing tables are updated correctly."""
alice = self.register_user("alice", "pass")
alice_token = self.login(alice, "pass")
bob = self.register_user("bob", "pass")
bob_token = self.login(bob, "pass")
public = self.helper.create_room_as(
alice,
is_public=True,
extra_content={"visibility": "public"},
tok=alice_token,
)
private = self.helper.create_room_as(alice, is_public=False, tok=alice_token)
self.helper.invite(private, alice, bob, tok=alice_token)
self.helper.join(public, bob, tok=bob_token)
self.helper.join(private, bob, tok=bob_token)
# Alice also makes a second public room but no-one else joins
public2 = self.helper.create_room_as(
alice,
is_public=True,
extra_content={"visibility": "public"},
tok=alice_token,
)
users = self.get_success(self.user_dir_helper.get_users_in_user_directory())
in_public = self.get_success(self.user_dir_helper.get_users_in_public_rooms())
in_private = self.get_success(
self.user_dir_helper.get_users_who_share_private_rooms()
)
self.assertEqual(users, {alice, bob})
self.assertEqual(
set(in_public), {(alice, public), (bob, public), (alice, public2)}
)
self.assertEqual(
self.user_dir_helper._compress_shared(in_private),
{(alice, bob, private), (bob, alice, private)},
)
# The next three tests (test_population_excludes_*) all setup
# - A normal user included in the user dir
# - A public and private room created by that user
# - A user excluded from the room dir, belonging to both rooms
# They match similar logic in storage/test_user_directory. But that tests
# rebuilding the directory; this tests updating it incrementally.
def test_excludes_support_user(self) -> None:
alice = self.register_user("alice", "pass")
alice_token = self.login(alice, "pass")
support = "@support1:test"
self.get_success(
self.store.register_user(
user_id=support, password_hash=None, user_type=UserTypes.SUPPORT
)
)
public, private = self._create_rooms_and_inject_memberships(
alice, alice_token, support
)
self._check_only_one_user_in_directory(alice, public)
def test_excludes_deactivated_user(self) -> None:
admin = self.register_user("admin", "pass", admin=True)
admin_token = self.login(admin, "pass")
user = self.register_user("naughty", "pass")
# Deactivate the user.
channel = self.make_request(
"PUT",
f"/_synapse/admin/v2/users/{user}",
access_token=admin_token,
content={"deactivated": True},
)
self.assertEqual(channel.code, 200)
self.assertEqual(channel.json_body["deactivated"], True)
# Join the deactivated user to rooms owned by the admin.
# Is this something that could actually happen outside of a test?
public, private = self._create_rooms_and_inject_memberships(
admin, admin_token, user
)
self._check_only_one_user_in_directory(admin, public)
def test_excludes_appservices_user(self) -> None:
# Register an AS user.
user = self.register_user("user", "pass")
token = self.login(user, "pass")
as_user = self.register_appservice_user("as_user_potato", self.appservice.token)
# Join the AS user to rooms owned by the normal user.
public, private = self._create_rooms_and_inject_memberships(
user, token, as_user
)
self._check_only_one_user_in_directory(user, public)
def _create_rooms_and_inject_memberships(
self, creator: str, token: str, joiner: str
) -> Tuple[str, str]:
"""Create a public and private room as a normal user.
Then get the `joiner` into those rooms.
"""
# TODO: Duplicates the same-named method in UserDirectoryInitialPopulationTest.
public_room = self.helper.create_room_as(
creator,
is_public=True,
# See https://github.com/matrix-org/synapse/issues/10951
extra_content={"visibility": "public"},
tok=token,
)
private_room = self.helper.create_room_as(creator, is_public=False, tok=token)
# HACK: get the user into these rooms
self.get_success(inject_member_event(self.hs, public_room, joiner, "join"))
self.get_success(inject_member_event(self.hs, private_room, joiner, "join"))
return public_room, private_room
def _check_only_one_user_in_directory(self, user: str, public: str) -> None:
users = self.get_success(self.user_dir_helper.get_users_in_user_directory())
in_public = self.get_success(self.user_dir_helper.get_users_in_public_rooms())
in_private = self.get_success(
self.user_dir_helper.get_users_who_share_private_rooms()
)
self.assertEqual(users, {user})
self.assertEqual(set(in_public), {(user, public)})
self.assertEqual(in_private, [])
def test_handle_local_profile_change_with_support_user(self) -> None:
support_user_id = "@support:test"
self.get_success(
@ -125,6 +275,26 @@ class UserDirectoryTestCase(unittest.HomeserverTestCase):
profile = self.get_success(self.store.get_user_in_directory(r_user_id))
self.assertTrue(profile is None)
def test_handle_local_profile_change_with_appservice_user(self) -> None:
# create user
as_user_id = self.register_appservice_user(
"as_user_alice", self.appservice.token
)
# profile is not in directory
profile = self.get_success(self.store.get_user_in_directory(as_user_id))
self.assertTrue(profile is None)
# update profile
profile_info = ProfileInfo(avatar_url="avatar_url", display_name="4L1c3")
self.get_success(
self.handler.handle_local_profile_change(as_user_id, profile_info)
)
# profile is still not in directory
profile = self.get_success(self.store.get_user_in_directory(as_user_id))
self.assertTrue(profile is None)
def test_handle_user_deactivated_support_user(self) -> None:
s_user_id = "@support:test"
self.get_success(
@ -483,8 +653,6 @@ class UserDirectoryTestCase(unittest.HomeserverTestCase):
class TestUserDirSearchDisabled(unittest.HomeserverTestCase):
user_id = "@test:test"
servlets = [
user_directory.register_servlets,
room.register_servlets,
@ -504,16 +672,21 @@ class TestUserDirSearchDisabled(unittest.HomeserverTestCase):
def test_disabling_room_list(self) -> None:
self.config.userdirectory.user_directory_search_enabled = True
# First we create a room with another user so that user dir is non-empty
# for our user
self.helper.create_room_as(self.user_id)
# Create two users and put them in the same room.
u1 = self.register_user("user1", "pass")
u1_token = self.login(u1, "pass")
u2 = self.register_user("user2", "pass")
room = self.helper.create_room_as(self.user_id)
self.helper.join(room, user=u2)
u2_token = self.login(u2, "pass")
# Assert user directory is not empty
room = self.helper.create_room_as(u1, tok=u1_token)
self.helper.join(room, user=u2, tok=u2_token)
# Each should see the other when searching the user directory.
channel = self.make_request(
"POST", b"user_directory/search", b'{"search_term":"user2"}'
"POST",
b"user_directory/search",
b'{"search_term":"user2"}',
access_token=u1_token,
)
self.assertEquals(200, channel.code, channel.result)
self.assertTrue(len(channel.json_body["results"]) > 0)
@ -521,7 +694,10 @@ class TestUserDirSearchDisabled(unittest.HomeserverTestCase):
# Disable user directory and check search returns nothing
self.config.userdirectory.user_directory_search_enabled = False
channel = self.make_request(
"POST", b"user_directory/search", b'{"search_term":"user2"}'
"POST",
b"user_directory/search",
b'{"search_term":"user2"}',
access_token=u1_token,
)
self.assertEquals(200, channel.code, channel.result)
self.assertTrue(len(channel.json_body["results"]) == 0)