Check appservice user interest against the local users instead of all users (`get_users_in_room` mis-use) (#13958)

This commit is contained in:
Eric Eastwood 2022-10-27 13:29:23 -05:00 committed by GitHub
parent 67583281e3
commit aa70556699
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 214 additions and 14 deletions

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

@ -0,0 +1 @@
Check appservice user interest against the local users instead of all users in the room to align with [MSC3905](https://github.com/matrix-org/matrix-spec-proposals/pull/3905).

View File

@ -97,6 +97,25 @@ As announced with the release of [Synapse 1.69.0](#deprecation-of-the-generate_s
Modules relying on it can instead use the `create_login_token` method. Modules relying on it can instead use the `create_login_token` method.
## Changes to the events received by application services (interest)
To align with spec (changed in
[MSC3905](https://github.com/matrix-org/matrix-spec-proposals/pull/3905)), Synapse now
only considers local users to be interesting. In other words, the `users` namespace
regex is only be applied against local users of the homeserver.
Please note, this probably doesn't affect the expected behavior of your application
service, since an interesting local user in a room still means all messages in the room
(from local or remote users) will still be considered interesting. And matching a room
with the `rooms` or `aliases` namespace regex will still consider all events sent in the
room to be interesting to the application service.
If one of your application service's `users` regex was intending to match a remote user,
this will no longer match as you expect. The behavioral mismatch between matching all
local users and some remote users is why the spec was changed/clarified and this
caveat is no longer supported.
# Upgrading to v1.69.0 # Upgrading to v1.69.0
## Changes to the receipts replication streams ## Changes to the receipts replication streams

View File

@ -172,12 +172,24 @@ class ApplicationService:
Returns: Returns:
True if this service would like to know about this room. True if this service would like to know about this room.
""" """
member_list = await store.get_users_in_room( # We can use `get_local_users_in_room(...)` here because an application service
# can only be interested in local users of the server it's on (ignore any remote
# users that might match the user namespace regex).
#
# In the future, we can consider re-using
# `store.get_app_service_users_in_room` which is very similar to this
# function but has a slightly worse performance than this because we
# have an early escape-hatch if we find a single user that the
# appservice is interested in. The juice would be worth the squeeze if
# `store.get_app_service_users_in_room` was used in more places besides
# an experimental MSC. But for now we can avoid doing more work and
# barely using it later.
local_user_ids = await store.get_local_users_in_room(
room_id, on_invalidate=cache_context.invalidate room_id, on_invalidate=cache_context.invalidate
) )
# check joined member events # check joined member events
for user_id in member_list: for user_id in local_user_ids:
if self.is_interested_in_user(user_id): if self.is_interested_in_user(user_id):
return True return True
return False return False

View File

@ -157,10 +157,23 @@ class ApplicationServiceWorkerStore(RoomMemberWorkerStore):
app_service: "ApplicationService", app_service: "ApplicationService",
cache_context: _CacheContext, cache_context: _CacheContext,
) -> List[str]: ) -> List[str]:
users_in_room = await self.get_users_in_room( """
Get all users in a room that the appservice controls.
Args:
room_id: The room to check in.
app_service: The application service to check interest/control against
Returns:
List of user IDs that the appservice controls.
"""
# We can use `get_local_users_in_room(...)` here because an application service
# can only be interested in local users of the server it's on (ignore any remote
# users that might match the user namespace regex).
local_users_in_room = await self.get_local_users_in_room(
room_id, on_invalidate=cache_context.invalidate room_id, on_invalidate=cache_context.invalidate
) )
return list(filter(app_service.is_interested_in_user, users_in_room)) return list(filter(app_service.is_interested_in_user, local_users_in_room))
class ApplicationServiceStore(ApplicationServiceWorkerStore): class ApplicationServiceStore(ApplicationServiceWorkerStore):

View File

@ -152,6 +152,9 @@ class RoomMemberWorkerStore(EventsWorkerStore):
the forward extremities of those rooms will exclude most members. We may also the forward extremities of those rooms will exclude most members. We may also
calculate room state incorrectly for such rooms and believe that a member is or calculate room state incorrectly for such rooms and believe that a member is or
is not in the room when the opposite is true. is not in the room when the opposite is true.
Note: If you only care about users in the room local to the homeserver, use
`get_local_users_in_room(...)` instead which will be more performant.
""" """
return await self.db_pool.simple_select_onecol( return await self.db_pool.simple_select_onecol(
table="current_state_events", table="current_state_events",

View File

@ -43,7 +43,7 @@ class ApplicationServiceTestCase(unittest.TestCase):
self.store = Mock() self.store = Mock()
self.store.get_aliases_for_room = simple_async_mock([]) self.store.get_aliases_for_room = simple_async_mock([])
self.store.get_users_in_room = simple_async_mock([]) self.store.get_local_users_in_room = simple_async_mock([])
@defer.inlineCallbacks @defer.inlineCallbacks
def test_regex_user_id_prefix_match(self): def test_regex_user_id_prefix_match(self):
@ -129,7 +129,7 @@ class ApplicationServiceTestCase(unittest.TestCase):
self.store.get_aliases_for_room = simple_async_mock( self.store.get_aliases_for_room = simple_async_mock(
["#irc_foobar:matrix.org", "#athing:matrix.org"] ["#irc_foobar:matrix.org", "#athing:matrix.org"]
) )
self.store.get_users_in_room = simple_async_mock([]) self.store.get_local_users_in_room = simple_async_mock([])
self.assertTrue( self.assertTrue(
( (
yield defer.ensureDeferred( yield defer.ensureDeferred(
@ -184,7 +184,7 @@ class ApplicationServiceTestCase(unittest.TestCase):
self.store.get_aliases_for_room = simple_async_mock( self.store.get_aliases_for_room = simple_async_mock(
["#xmpp_foobar:matrix.org", "#athing:matrix.org"] ["#xmpp_foobar:matrix.org", "#athing:matrix.org"]
) )
self.store.get_users_in_room = simple_async_mock([]) self.store.get_local_users_in_room = simple_async_mock([])
self.assertFalse( self.assertFalse(
( (
yield defer.ensureDeferred( yield defer.ensureDeferred(
@ -203,7 +203,7 @@ class ApplicationServiceTestCase(unittest.TestCase):
self.service.namespaces[ApplicationService.NS_USERS].append(_regex("@irc_.*")) self.service.namespaces[ApplicationService.NS_USERS].append(_regex("@irc_.*"))
self.event.sender = "@irc_foobar:matrix.org" self.event.sender = "@irc_foobar:matrix.org"
self.store.get_aliases_for_room = simple_async_mock(["#irc_barfoo:matrix.org"]) self.store.get_aliases_for_room = simple_async_mock(["#irc_barfoo:matrix.org"])
self.store.get_users_in_room = simple_async_mock([]) self.store.get_local_users_in_room = simple_async_mock([])
self.assertTrue( self.assertTrue(
( (
yield defer.ensureDeferred( yield defer.ensureDeferred(
@ -236,7 +236,7 @@ class ApplicationServiceTestCase(unittest.TestCase):
def test_member_list_match(self): def test_member_list_match(self):
self.service.namespaces[ApplicationService.NS_USERS].append(_regex("@irc_.*")) self.service.namespaces[ApplicationService.NS_USERS].append(_regex("@irc_.*"))
# Note that @irc_fo:here is the AS user. # Note that @irc_fo:here is the AS user.
self.store.get_users_in_room = simple_async_mock( self.store.get_local_users_in_room = simple_async_mock(
["@alice:here", "@irc_fo:here", "@bob:here"] ["@alice:here", "@irc_fo:here", "@bob:here"]
) )
self.store.get_aliases_for_room = simple_async_mock([]) self.store.get_aliases_for_room = simple_async_mock([])

View File

@ -22,7 +22,7 @@ from twisted.test.proto_helpers import MemoryReactor
import synapse.rest.admin import synapse.rest.admin
import synapse.storage import synapse.storage
from synapse.api.constants import EduTypes from synapse.api.constants import EduTypes, EventTypes
from synapse.appservice import ( from synapse.appservice import (
ApplicationService, ApplicationService,
TransactionOneTimeKeyCounts, TransactionOneTimeKeyCounts,
@ -36,7 +36,7 @@ from synapse.util import Clock
from synapse.util.stringutils import random_string from synapse.util.stringutils import random_string
from tests import unittest from tests import unittest
from tests.test_utils import make_awaitable, simple_async_mock from tests.test_utils import event_injection, make_awaitable, simple_async_mock
from tests.unittest import override_config from tests.unittest import override_config
from tests.utils import MockClock from tests.utils import MockClock
@ -390,15 +390,16 @@ class ApplicationServicesHandlerSendEventsTestCase(unittest.HomeserverTestCase):
receipts.register_servlets, receipts.register_servlets,
] ]
def prepare(self, reactor, clock, hs): def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer):
self.hs = hs
# Mock the ApplicationServiceScheduler's _TransactionController's send method so that # Mock the ApplicationServiceScheduler's _TransactionController's send method so that
# we can track any outgoing ephemeral events # we can track any outgoing ephemeral events
self.send_mock = simple_async_mock() self.send_mock = simple_async_mock()
hs.get_application_service_handler().scheduler.txn_ctrl.send = self.send_mock hs.get_application_service_handler().scheduler.txn_ctrl.send = self.send_mock # type: ignore[assignment]
# Mock out application services, and allow defining our own in tests # Mock out application services, and allow defining our own in tests
self._services: List[ApplicationService] = [] self._services: List[ApplicationService] = []
self.hs.get_datastores().main.get_app_services = Mock( self.hs.get_datastores().main.get_app_services = Mock( # type: ignore[assignment]
return_value=self._services return_value=self._services
) )
@ -416,6 +417,157 @@ class ApplicationServicesHandlerSendEventsTestCase(unittest.HomeserverTestCase):
"exclusive_as_user", "password", self.exclusive_as_user_device_id "exclusive_as_user", "password", self.exclusive_as_user_device_id
) )
def _notify_interested_services(self):
# This is normally set in `notify_interested_services` but we need to call the
# internal async version so the reactor gets pushed to completion.
self.hs.get_application_service_handler().current_max += 1
self.get_success(
self.hs.get_application_service_handler()._notify_interested_services(
RoomStreamToken(
None, self.hs.get_application_service_handler().current_max
)
)
)
@parameterized.expand(
[
("@local_as_user:test", True),
# Defining remote users in an application service user namespace regex is a
# footgun since the appservice might assume that it'll receive all events
# sent by that remote user, but it will only receive events in rooms that
# are shared with a local user. So we just remove this footgun possibility
# entirely and we won't notify the application service based on remote
# users.
("@remote_as_user:remote", False),
]
)
def test_match_interesting_room_members(
self, interesting_user: str, should_notify: bool
):
"""
Test to make sure that a interesting user (local or remote) in the room is
notified as expected when someone else in the room sends a message.
"""
# Register an application service that's interested in the `interesting_user`
interested_appservice = self._register_application_service(
namespaces={
ApplicationService.NS_USERS: [
{
"regex": interesting_user,
"exclusive": False,
},
],
},
)
# Create a room
alice = self.register_user("alice", "pass")
alice_access_token = self.login("alice", "pass")
room_id = self.helper.create_room_as(room_creator=alice, tok=alice_access_token)
# Join the interesting user to the room
self.get_success(
event_injection.inject_member_event(
self.hs, room_id, interesting_user, "join"
)
)
# Kick the appservice into checking this membership event to get the event out
# of the way
self._notify_interested_services()
# We don't care about the interesting user join event (this test is making sure
# the next thing works)
self.send_mock.reset_mock()
# Send a message from an uninteresting user
self.helper.send_event(
room_id,
type=EventTypes.Message,
content={
"msgtype": "m.text",
"body": "message from uninteresting user",
},
tok=alice_access_token,
)
# Kick the appservice into checking this new event
self._notify_interested_services()
if should_notify:
self.send_mock.assert_called_once()
(
service,
events,
_ephemeral,
_to_device_messages,
_otks,
_fbks,
_device_list_summary,
) = self.send_mock.call_args[0]
# Even though the message came from an uninteresting user, it should still
# notify us because the interesting user is joined to the room where the
# message was sent.
self.assertEqual(service, interested_appservice)
self.assertEqual(events[0]["type"], "m.room.message")
self.assertEqual(events[0]["sender"], alice)
else:
self.send_mock.assert_not_called()
def test_application_services_receive_events_sent_by_interesting_local_user(self):
"""
Test to make sure that a messages sent from a local user can be interesting and
picked up by the appservice.
"""
# Register an application service that's interested in all local users
interested_appservice = self._register_application_service(
namespaces={
ApplicationService.NS_USERS: [
{
"regex": ".*",
"exclusive": False,
},
],
},
)
# Create a room
alice = self.register_user("alice", "pass")
alice_access_token = self.login("alice", "pass")
room_id = self.helper.create_room_as(room_creator=alice, tok=alice_access_token)
# We don't care about interesting events before this (this test is making sure
# the next thing works)
self.send_mock.reset_mock()
# Send a message from the interesting local user
self.helper.send_event(
room_id,
type=EventTypes.Message,
content={
"msgtype": "m.text",
"body": "message from interesting local user",
},
tok=alice_access_token,
)
# Kick the appservice into checking this new event
self._notify_interested_services()
self.send_mock.assert_called_once()
(
service,
events,
_ephemeral,
_to_device_messages,
_otks,
_fbks,
_device_list_summary,
) = self.send_mock.call_args[0]
# Events sent from an interesting local user should also be picked up as
# interesting to the appservice.
self.assertEqual(service, interested_appservice)
self.assertEqual(events[0]["type"], "m.room.message")
self.assertEqual(events[0]["sender"], alice)
def test_sending_read_receipt_batches_to_application_services(self): def test_sending_read_receipt_batches_to_application_services(self):
"""Tests that a large batch of read receipts are sent correctly to """Tests that a large batch of read receipts are sent correctly to
interested application services. interested application services.