diff --git a/changelog.d/16908.misc b/changelog.d/16908.misc
new file mode 100644
index 0000000000..d13c59aa35
--- /dev/null
+++ b/changelog.d/16908.misc
@@ -0,0 +1 @@
+Improve event validation (#16908).
\ No newline at end of file
diff --git a/synapse/api/constants.py b/synapse/api/constants.py
index d25aff98ff..98884b4967 100644
--- a/synapse/api/constants.py
+++ b/synapse/api/constants.py
@@ -129,6 +129,8 @@ class EventTypes:
Reaction: Final = "m.reaction"
+ CallInvite: Final = "m.call.invite"
+
class ToDeviceEventTypes:
RoomKeyRequest: Final = "m.room_key_request"
diff --git a/synapse/handlers/message.py b/synapse/handlers/message.py
index 0ce6eeee15..ccaa5508ff 100644
--- a/synapse/handlers/message.py
+++ b/synapse/handlers/message.py
@@ -34,6 +34,7 @@ from synapse.api.constants import (
EventTypes,
GuestAccess,
HistoryVisibility,
+ JoinRules,
Membership,
RelationTypes,
UserTypes,
@@ -1325,6 +1326,18 @@ class EventCreationHandler:
self.validator.validate_new(event, self.config)
await self._validate_event_relation(event)
+
+ if event.type == EventTypes.CallInvite:
+ room_id = event.room_id
+ room_info = await self.store.get_room_with_stats(room_id)
+ assert room_info is not None
+
+ if room_info.join_rules == JoinRules.PUBLIC:
+ raise SynapseError(
+ 403,
+ "Call invites are not allowed in public rooms.",
+ Codes.FORBIDDEN,
+ )
logger.debug("Created event %s", event.event_id)
return event, context
diff --git a/synapse/handlers/sync.py b/synapse/handlers/sync.py
index 0aedb37f16..3aa2e2b7ba 100644
--- a/synapse/handlers/sync.py
+++ b/synapse/handlers/sync.py
@@ -41,6 +41,7 @@ from synapse.api.constants import (
AccountDataTypes,
EventContentFields,
EventTypes,
+ JoinRules,
Membership,
)
from synapse.api.filtering import FilterCollection
@@ -675,13 +676,22 @@ class SyncHandler:
)
)
- loaded_recents = await filter_events_for_client(
+ filtered_recents = await filter_events_for_client(
self._storage_controllers,
sync_config.user.to_string(),
loaded_recents,
always_include_ids=current_state_ids,
)
+ loaded_recents = []
+ for event in filtered_recents:
+ if event.type == EventTypes.CallInvite:
+ room_info = await self.store.get_room_with_stats(event.room_id)
+ assert room_info is not None
+ if room_info.join_rules == JoinRules.PUBLIC:
+ continue
+ loaded_recents.append(event)
+
log_kv({"loaded_recents_after_client_filtering": len(loaded_recents)})
loaded_recents.extend(recents)
diff --git a/tests/handlers/test_message.py b/tests/handlers/test_message.py
index 0ee5eee385..76ab83d1f7 100644
--- a/tests/handlers/test_message.py
+++ b/tests/handlers/test_message.py
@@ -24,6 +24,7 @@ from typing import Tuple
from twisted.test.proto_helpers import MemoryReactor
from synapse.api.constants import EventTypes
+from synapse.api.errors import SynapseError
from synapse.events import EventBase
from synapse.events.snapshot import EventContext, UnpersistedEventContextBase
from synapse.rest import admin
@@ -51,11 +52,15 @@ class EventCreationTestCase(unittest.HomeserverTestCase):
persistence = self.hs.get_storage_controllers().persistence
assert persistence is not None
self._persist_event_storage_controller = persistence
+ self.store = self.hs.get_datastores().main
self.user_id = self.register_user("tester", "foobar")
device_id = "dev-1"
access_token = self.login("tester", "foobar", device_id=device_id)
self.room_id = self.helper.create_room_as(self.user_id, tok=access_token)
+ self.private_room_id = self.helper.create_room_as(
+ self.user_id, tok=access_token, extra_content={"preset": "private_chat"}
+ )
self.requester = create_requester(self.user_id, device_id=device_id)
@@ -285,6 +290,41 @@ class EventCreationTestCase(unittest.HomeserverTestCase):
AssertionError,
)
+ def test_call_invite_event_creation_fails_in_public_room(self) -> None:
+ # get prev_events for room
+ prev_events = self.get_success(
+ self.store.get_prev_events_for_room(self.room_id)
+ )
+
+ # the invite in a public room should fail
+ self.get_failure(
+ self.handler.create_event(
+ self.requester,
+ {
+ "type": EventTypes.CallInvite,
+ "room_id": self.room_id,
+ "sender": self.requester.user.to_string(),
+ },
+ prev_event_ids=prev_events,
+ auth_event_ids=prev_events,
+ ),
+ SynapseError,
+ )
+
+ # but a call invite in a private room should succeed
+ self.get_success(
+ self.handler.create_event(
+ self.requester,
+ {
+ "type": EventTypes.CallInvite,
+ "room_id": self.private_room_id,
+ "sender": self.requester.user.to_string(),
+ },
+ prev_event_ids=prev_events,
+ auth_event_ids=prev_events,
+ )
+ )
+
class ServerAclValidationTestCase(unittest.HomeserverTestCase):
servlets = [
diff --git a/tests/handlers/test_sync.py b/tests/handlers/test_sync.py
index 37904926e3..1b36324b8f 100644
--- a/tests/handlers/test_sync.py
+++ b/tests/handlers/test_sync.py
@@ -17,7 +17,7 @@
# [This file includes modifications made by New Vector Limited]
#
#
-from typing import Optional
+from typing import Collection, List, Optional
from unittest.mock import AsyncMock, Mock, patch
from twisted.test.proto_helpers import MemoryReactor
@@ -25,7 +25,10 @@ from twisted.test.proto_helpers import MemoryReactor
from synapse.api.constants import EventTypes, JoinRules
from synapse.api.errors import Codes, ResourceLimitError
from synapse.api.filtering import Filtering
-from synapse.api.room_versions import RoomVersions
+from synapse.api.room_versions import RoomVersion, RoomVersions
+from synapse.events import EventBase
+from synapse.events.snapshot import EventContext
+from synapse.federation.federation_base import event_from_pdu_json
from synapse.handlers.sync import SyncConfig, SyncResult
from synapse.rest import admin
from synapse.rest.client import knock, login, room
@@ -285,6 +288,114 @@ class SyncTestCase(tests.unittest.HomeserverTestCase):
)
self.assertEqual(eve_initial_sync_after_join.joined, [])
+ def test_call_invite_in_public_room_not_returned(self) -> None:
+ user = self.register_user("alice", "password")
+ tok = self.login(user, "password")
+ room_id = self.helper.create_room_as(user, is_public=True, tok=tok)
+ self.handler = self.hs.get_federation_handler()
+ federation_event_handler = self.hs.get_federation_event_handler()
+
+ async def _check_event_auth(
+ origin: Optional[str], event: EventBase, context: EventContext
+ ) -> None:
+ pass
+
+ federation_event_handler._check_event_auth = _check_event_auth # type: ignore[method-assign]
+ self.client = self.hs.get_federation_client()
+
+ async def _check_sigs_and_hash_for_pulled_events_and_fetch(
+ dest: str, pdus: Collection[EventBase], room_version: RoomVersion
+ ) -> List[EventBase]:
+ return list(pdus)
+
+ self.client._check_sigs_and_hash_for_pulled_events_and_fetch = _check_sigs_and_hash_for_pulled_events_and_fetch # type: ignore[assignment]
+
+ prev_events = self.get_success(self.store.get_prev_events_for_room(room_id))
+
+ # create a call invite event
+ call_event = event_from_pdu_json(
+ {
+ "type": EventTypes.CallInvite,
+ "content": {},
+ "room_id": room_id,
+ "sender": user,
+ "depth": 32,
+ "prev_events": prev_events,
+ "auth_events": prev_events,
+ "origin_server_ts": self.clock.time_msec(),
+ },
+ RoomVersions.V10,
+ )
+
+ self.assertEqual(
+ self.get_success(
+ federation_event_handler.on_receive_pdu("test.serv", call_event)
+ ),
+ None,
+ )
+
+ # check that it is in DB
+ recent_event = self.get_success(self.store.get_prev_events_for_room(room_id))
+ self.assertIn(call_event.event_id, recent_event)
+
+ # but that it does not come down /sync in public room
+ sync_result: SyncResult = self.get_success(
+ self.sync_handler.wait_for_sync_for_user(
+ create_requester(user), generate_sync_config(user)
+ )
+ )
+ event_ids = []
+ for event in sync_result.joined[0].timeline.events:
+ event_ids.append(event.event_id)
+ self.assertNotIn(call_event.event_id, event_ids)
+
+ # it will come down in a private room, though
+ user2 = self.register_user("bob", "password")
+ tok2 = self.login(user2, "password")
+ private_room_id = self.helper.create_room_as(
+ user2, is_public=False, tok=tok2, extra_content={"preset": "private_chat"}
+ )
+
+ priv_prev_events = self.get_success(
+ self.store.get_prev_events_for_room(private_room_id)
+ )
+ private_call_event = event_from_pdu_json(
+ {
+ "type": EventTypes.CallInvite,
+ "content": {},
+ "room_id": private_room_id,
+ "sender": user,
+ "depth": 32,
+ "prev_events": priv_prev_events,
+ "auth_events": priv_prev_events,
+ "origin_server_ts": self.clock.time_msec(),
+ },
+ RoomVersions.V10,
+ )
+
+ self.assertEqual(
+ self.get_success(
+ federation_event_handler.on_receive_pdu("test.serv", private_call_event)
+ ),
+ None,
+ )
+
+ recent_events = self.get_success(
+ self.store.get_prev_events_for_room(private_room_id)
+ )
+ self.assertIn(private_call_event.event_id, recent_events)
+
+ private_sync_result: SyncResult = self.get_success(
+ self.sync_handler.wait_for_sync_for_user(
+ create_requester(user2), generate_sync_config(user2)
+ )
+ )
+ priv_event_ids = []
+ for event in private_sync_result.joined[0].timeline.events:
+ priv_event_ids.append(event.event_id)
+
+ self.assertIn(private_call_event.event_id, priv_event_ids)
+
_request_key = 0
|