Compare commits

...

15 Commits

Author SHA1 Message Date
David Robertson
9fec1cc212 Fix typo 2021-10-11 22:12:14 +01:00
David Robertson
04078ff514 Please do dump logs 2021-10-11 21:20:52 +01:00
David Robertson
8550c0a4d3 Desperate attempt to make test pass on CI. It's fine locally! 2021-10-11 20:43:23 +01:00
David Robertson
7648898d8a Ensure dir rebuild includes appservice senders 2021-10-11 20:17:46 +01:00
David Robertson
4c5afd032c Lint 2021-10-11 18:33:07 +01:00
David Robertson
03db1e551d Update tests 2021-10-11 18:28:08 +01:00
David Robertson
1b84f7dfb0 Update changelog 2021-10-11 18:20:53 +01:00
David Robertson
3c053fdd51 On reflection, INCLUDE the appservice senders
That's the current behaviour (I didn't realise they typically don't
match their own user regex).
2021-10-11 18:20:08 +01:00
David Robertson
ff6212a5a5 Add test covering appservice sender joining a room
I don't think this actually improves coverage as such, but I'll sleep
better at night having this case checked!
2021-10-11 17:29:07 +01:00
Eric Eastwood
8d2025c2c8 Move to more concise assertion method for the usage 2021-10-08 17:52:40 -05:00
Eric Eastwood
65232341d8 Add better comment explanations 2021-10-08 17:28:53 -05:00
Eric Eastwood
a92cad8309 Adjust changelog to explain what/why 2021-10-08 17:22:16 -05:00
Eric Eastwood
1dea46bcb6 Merge branch 'develop' into madlittlemods/11025-fix-user-directory-throwing-exception-when-interacting-with-appservice-sender 2021-10-07 19:22:06 -05:00
Eric Eastwood
ede2a70211 Add changelog 2021-10-07 18:14:25 -05:00
Eric Eastwood
2c7c5c223c Fix exception thrown when attempting to notify appservice sender
Fix https://github.com/matrix-org/synapse/issues/11025

Before in [`user_directory_handler._handle_deltas`, we just checked for `is_support_user(user_id)`](https://github.com/matrix-org/synapse/pull/10960/files#diff-e02a9a371e03b8615b53c6b6552f76fc7d3ef58931ca64d28b3512caf305449fL232) which works just fine.
Now with https://github.com/matrix-org/synapse/pull/10960, we [call `should_include_local_user_in_dir`](https://github.com/matrix-org/synapse/pull/10960/files#diff-e02a9a371e03b8615b53c6b6552f76fc7d3ef58931ca64d28b3512caf305449fR229) which does a [bunch of additional checks](e79ee48313/synapse/storage/databases/main/user_directory.py (L382-L398)) which aren't all compatible with the main appservice sender (main bridge user/sender). More specifically, we can't check the `users` database table for whether the user is deactivated.

In the `should_include_local_user_in_dir` checks, we should return early if we encounter a main appservice sender before the incompatible checks.
2021-10-07 18:04:00 -05:00
8 changed files with 180 additions and 20 deletions

View File

@@ -122,6 +122,7 @@ jobs:
# Note: Dumps to workflow logs instead of using actions/upload-artifact
# This keeps logs colocated with failing jobs
# It also ignores find's exit code; this is a best effort affair
if: ${{ always() }}
run: >-
find _trial_temp -name '*.log'
-exec echo "::group::{}" \;
@@ -146,6 +147,7 @@ jobs:
# Note: Dumps to workflow logs instead of using actions/upload-artifact
# This keeps logs colocated with failing jobs
# It also ignores find's exit code; this is a best effort affair
if: ${{ always() }}
run: >-
find _trial_temp -name '*.log'
-exec echo "::group::{}" \;
@@ -176,6 +178,7 @@ jobs:
# Note: Dumps to workflow logs instead of using actions/upload-artifact
# This keeps logs colocated with failing jobs
# It also ignores find's exit code; this is a best effort affair
if: ${{ always() }}
run: >-
find _trial_temp -name '*.log'
-exec echo "::group::{}" \;

View File

@@ -36,6 +36,7 @@ jobs:
# Note: Dumps to workflow logs instead of using actions/upload-artifact
# This keeps logs colocated with failing jobs
# It also ignores find's exit code; this is a best effort affair
if: ${{ always() }}
run: >-
find _trial_temp -name '*.log'
-exec echo "::group::{}" \;

1
changelog.d/11026.bugfix Normal file
View File

@@ -0,0 +1 @@
Fix a long-standing bug where rebuilding the user directory wouldn't exclude support and disabled users.

View File

@@ -5,11 +5,6 @@ The user directory is currently maintained based on the 'visible' users
on this particular server - i.e. ones which your account shares a room with, or
who are present in a publicly viewable room present on the server.
The directory info is stored in various tables, which can (typically after
DB corruption) get stale or out of sync. If this happens, for now the
solution to fix it is to execute the SQL [here](https://github.com/matrix-org/synapse/blob/master/synapse/storage/schema/main/delta/53/user_dir_populate.sql)
and then restart synapse. This should then start a background task to
flush the current tables and regenerate the directory.
Data model
----------
@@ -21,7 +16,8 @@ see who.
From all of these tables we exclude three types of local user:
- support users
- appservice users
- appservice users (e.g. people using IRC)
- but not the "appservice sender" (e.g. the bot which bridges Matrix to IRC).
- deactivated users
* `user_directory`. This contains the user_id, display name and avatar we'll
@@ -47,3 +43,36 @@ From all of these tables we exclude three types of local user:
* `users_who_share_private_rooms`. Rows are triples `(L, M, room id)` where `L`
is a local user and `M` is a local or remote user. `L` and `M` should be
different, but this isn't enforced by a constraint.
Rebuilding the directory
------------------------
The directory info is stored in various tables, which can (typically after
DB corruption) get stale or out of sync. If this happens, for now the
solution to fix it is to execute the following SQL and then restart Synapse.
```sql
-- Set up staging tables
INSERT INTO background_updates (update_name, progress_json) VALUES
('populate_user_directory_createtables', '{}');
-- Run through each room and update the room sharing tables.
-- Also add directory entries for remote users.
INSERT INTO background_updates (update_name, progress_json, depends_on) VALUES
('populate_user_directory_process_rooms', '{}', 'populate_user_directory_createtables');
-- Insert directory entries for all local users.
INSERT INTO background_updates (update_name, progress_json, depends_on) VALUES
('populate_user_directory_process_users', '{}', 'populate_user_directory_process_rooms');
-- Insert directory entries for all appservice senders.
INSERT INTO background_updates (update_name, progress_json, depends_on) VALUES
('populate_user_directory_process_appservice_senders', '{}', 'populate_user_directory_process_users');
-- Clean up staging tables
INSERT INTO background_updates (update_name, progress_json, depends_on) VALUES
('populate_user_directory_cleanup', '{}', 'populate_user_directory_process_appservice_senders');
```
This should then start a background task to
flush the current tables and regenerate the directory.

View File

@@ -28,6 +28,7 @@ from typing import (
if TYPE_CHECKING:
from synapse.server import HomeServer
from synapse.appservice import ApplicationService
from synapse.api.constants import EventTypes, HistoryVisibility, JoinRules
from synapse.storage.database import DatabasePool, LoggingTransaction
@@ -70,6 +71,10 @@ class UserDirectoryBackgroundUpdateStore(StateDeltasStore):
"populate_user_directory_process_users",
self._populate_user_directory_process_users,
)
self.db_pool.updates.register_background_update_handler(
"populate_user_directory_process_appservice_senders",
self._populate_user_directory_process_appservice_senders,
)
self.db_pool.updates.register_background_update_handler(
"populate_user_directory_cleanup", self._populate_user_directory_cleanup
)
@@ -379,11 +384,40 @@ class UserDirectoryBackgroundUpdateStore(StateDeltasStore):
return len(users_to_work_on)
async def _populate_user_directory_process_appservice_senders(
self, progress: JsonDict, batch_size: int
) -> int:
"""Add appservice senders to the `user_directory` table.
Appservices users don't live in the users table, so they won't be picked up
by `_populate_user_directory_process_users`. Process them explicitly here.
"""
service: "ApplicationService"
for service in self.services_cache:
await self.update_profile_in_user_dir(service.sender, None, None)
await self.db_pool.updates._end_background_update(
"populate_user_directory_process_appservice_senders"
)
return 1
async def should_include_local_user_in_dir(self, user: str) -> bool:
"""Certain classes of local user are omitted from the user directory.
Is this user one of them?
"""
# App service users aren't usually contactable, so exclude them.
# We include the appservice sender in the directory. These typically
# aren't covered by `get_if_app_services_interested_in_user`.
# We need to handle it here or else we'll get a "row not found" error
# in the deactivated user check, because the sender doesn't have an
# entry in the `users` table.
if self.get_app_service_by_user_id(user) is not None:
return True
# We're opting to exclude appservice users (anyone matching the user
# namespace regex in the appservice registration) even though technically
# they could be DM-able. In the future, this could potentially
# be configurable per-appservice whether the appservice users can be
# contacted.
if self.get_if_app_services_interested_in_user(user):
# TODO we might want to make this configurable for each app service
return False

View File

@@ -13,6 +13,8 @@
* limitations under the License.
*/
-- For the most up-to-date version of these instructions, see docs/user_directory.md
-- Set up staging tables
INSERT INTO background_updates (update_name, progress_json) VALUES
('populate_user_directory_createtables', '{}');

View File

@@ -63,7 +63,9 @@ class UserDirectoryTestCase(unittest.HomeserverTestCase):
hostname="test",
id="1234",
namespaces={"users": [{"regex": r"@as_user.*", "exclusive": True}]},
sender="@as:test",
# Note: this user does not match the regex above, so that tests
# can distinguish the sender from the AS user.
sender="@as_main:test",
)
mock_load_appservices = Mock(return_value=[self.appservice])
@@ -179,6 +181,26 @@ class UserDirectoryTestCase(unittest.HomeserverTestCase):
)
self._check_only_one_user_in_directory(user, public)
def test_includes_appservice_sender(self) -> None:
user = self.register_user("user", "pass")
token = self.login(user, "pass")
room = self.helper.create_room_as(
self.appservice.sender, is_public=True, tok=self.appservice.token
)
self.helper.join(room, user, tok=token)
# Will this make the test pass in CI? It's fine locally.
self.wait_for_background_updates()
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.appservice.sender})
self.assertEqual(set(in_public), {(user, room), (self.appservice.sender, room)})
self.assertEqual(in_private, [])
def _create_rooms_and_inject_memberships(
self, creator: str, token: str, joiner: str
) -> Tuple[str, str]:
@@ -230,7 +252,7 @@ class UserDirectoryTestCase(unittest.HomeserverTestCase):
)
)
profile = self.get_success(self.store.get_user_in_directory(support_user_id))
self.assertTrue(profile is None)
self.assertIsNone(profile)
display_name = "display_name"
profile_info = ProfileInfo(avatar_url="avatar_url", display_name=display_name)
@@ -264,7 +286,7 @@ class UserDirectoryTestCase(unittest.HomeserverTestCase):
# profile is not in directory
profile = self.get_success(self.store.get_user_in_directory(r_user_id))
self.assertTrue(profile is None)
self.assertIsNone(profile)
# update profile after deactivation
self.get_success(
@@ -273,7 +295,7 @@ class UserDirectoryTestCase(unittest.HomeserverTestCase):
# profile is furthermore not in directory
profile = self.get_success(self.store.get_user_in_directory(r_user_id))
self.assertTrue(profile is None)
self.assertIsNone(profile)
def test_handle_local_profile_change_with_appservice_user(self) -> None:
# create user
@@ -283,7 +305,7 @@ class UserDirectoryTestCase(unittest.HomeserverTestCase):
# profile is not in directory
profile = self.get_success(self.store.get_user_in_directory(as_user_id))
self.assertTrue(profile is None)
self.assertIsNone(profile)
# update profile
profile_info = ProfileInfo(avatar_url="avatar_url", display_name="4L1c3")
@@ -293,7 +315,30 @@ class UserDirectoryTestCase(unittest.HomeserverTestCase):
# profile is still not in directory
profile = self.get_success(self.store.get_user_in_directory(as_user_id))
self.assertTrue(profile is None)
self.assertIsNone(profile)
def test_handle_local_profile_change_with_appservice_sender(self) -> None:
# profile is not in directory
profile = self.get_success(
self.store.get_user_in_directory(self.appservice.sender)
)
self.assertIsNone(profile)
# update profile
profile_info = ProfileInfo(avatar_url="avatar_url", display_name="beep boop")
self.get_success(
self.handler.handle_local_profile_change(
self.appservice.sender, profile_info
)
)
# profile is now set
profile = self.get_success(
self.store.get_user_in_directory(self.appservice.sender)
)
self.assertEqual(
profile, {"display_name": "beep boop", "avatar_url": "avatar_url"}
)
def test_handle_user_deactivated_support_user(self) -> None:
s_user_id = "@support:test"

View File

@@ -134,7 +134,9 @@ class UserDirectoryInitialPopulationTestcase(HomeserverTestCase):
hostname="test",
id="1234",
namespaces={"users": [{"regex": r"@as_user.*", "exclusive": True}]},
sender="@as:test",
# Note: this user does not match the regex above, so that tests
# can distinguish the sender from the AS user.
sender="@as_main:test",
)
mock_load_appservices = Mock(return_value=[self.appservice])
@@ -205,12 +207,22 @@ class UserDirectoryInitialPopulationTestcase(HomeserverTestCase):
self.store.db_pool.simple_insert(
"background_updates",
{
"update_name": "populate_user_directory_cleanup",
"update_name": "populate_user_directory_process_appservice_senders",
"progress_json": "{}",
"depends_on": "populate_user_directory_process_users",
},
)
)
self.get_success(
self.store.db_pool.simple_insert(
"background_updates",
{
"update_name": "populate_user_directory_cleanup",
"progress_json": "{}",
"depends_on": "populate_user_directory_process_appservice_senders",
},
)
)
self.wait_for_background_updates()
@@ -254,7 +266,7 @@ class UserDirectoryInitialPopulationTestcase(HomeserverTestCase):
# All three should have entries in the directory
users = self.get_success(self.user_dir_helper.get_users_in_user_directory())
self.assertEqual(users, {u1, u2, u3})
self.assertEqual(users, {u1, u2, u3, self.appservice.sender})
# The next three tests (test_population_excludes_*) all set up
# - A normal user included in the user dir
@@ -290,7 +302,7 @@ class UserDirectoryInitialPopulationTestcase(HomeserverTestCase):
) -> None:
# After rebuilding the directory, we should only see the normal user.
users = self.get_success(self.user_dir_helper.get_users_in_user_directory())
self.assertEqual(users, {normal_user})
self.assertEqual(users, {normal_user, self.appservice.sender})
in_public_rooms = self.get_success(
self.user_dir_helper.get_users_in_public_rooms()
)
@@ -364,6 +376,38 @@ class UserDirectoryInitialPopulationTestcase(HomeserverTestCase):
# Check the AS user is not in the directory.
self._check_room_sharing_tables(user, public, private)
def test_population_includes_appservice_sender(self) -> None:
user = self.register_user("user", "pass")
token = self.login(user, "pass")
# Join the AS sender to rooms owned by the normal user.
public, private = self._create_rooms_and_inject_memberships(
user, token, self.appservice.sender
)
# Rebuild the directory.
self._purge_and_rebuild_user_dir()
# Check the AS sender is in the directory.
users = self.get_success(self.user_dir_helper.get_users_in_user_directory())
self.assertEqual(users, {user, self.appservice.sender})
in_public_rooms = self.get_success(
self.user_dir_helper.get_users_in_public_rooms()
)
self.assertEqual(
set(in_public_rooms), {(user, public), (self.appservice.sender, public)}
)
in_private_rooms = self.get_success(
self.user_dir_helper.get_users_who_share_private_rooms()
)
self.assertEqual(
self.user_dir_helper._compress_shared(in_private_rooms),
{
(user, self.appservice.sender, private),
(self.appservice.sender, user, private),
},
)
def test_population_conceals_private_nickname(self) -> None:
# Make a private room, and set a nickname within
user = self.register_user("aaaa", "pass")
@@ -392,15 +436,16 @@ class UserDirectoryInitialPopulationTestcase(HomeserverTestCase):
self._purge_and_rebuild_user_dir()
# Local users are ignored by the scan over rooms
users = self.get_success(self.user_dir_helper.get_profiles_in_user_directory())
self.assertEqual(users, {})
users = self.get_success(self.user_dir_helper.get_users_in_user_directory())
self.assertEqual(users, {self.appservice.sender})
# Do a full rebuild including the scan over the `users` table. The local
# user should appear with their profile name.
self._purge_and_rebuild_user_dir()
users = self.get_success(self.user_dir_helper.get_profiles_in_user_directory())
self.assertEqual(
users, {user: ProfileInfo(display_name="aaaa", avatar_url=None)}
users[user],
ProfileInfo(display_name="aaaa", avatar_url=None),
)