Skip to content

Commit 93674ac

Browse files
jason-famedlynico-famedly
authored andcommitted
chore: add on_upgrade_room() to third_party_event_rules ModuleApi
Called in the process of upgrading a room, provides access to the `Requester` object for data about the requesting user, and access to the `RoomVersion` object for data about the requested new version for the room. Returns nothing, denials should raise a `SynapseError` with appropriate codes and messages that will be returned to the client. Multiple modules may implement this callback, first one that raises an Exception denies further processing of the upgrade to the room
1 parent c7c2b5b commit 93674ac

File tree

4 files changed

+124
-3
lines changed

4 files changed

+124
-3
lines changed

synapse/handlers/room.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,8 @@ async def upgrade_room(
220220
if old_room is None:
221221
raise NotFoundError("Unknown room id %s" % (old_room_id,))
222222

223+
await self._third_party_event_rules.on_upgrade_room(requester, new_version)
224+
223225
new_room_id = self._generate_room_id()
224226

225227
# Try several times, it could fail with PartialStateConflictError

synapse/module_api/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@
117117
ON_PROFILE_UPDATE_CALLBACK,
118118
ON_REMOVE_USER_THIRD_PARTY_IDENTIFIER_CALLBACK,
119119
ON_THREEPID_BIND_CALLBACK,
120+
ON_UPGRADE_ROOM_CALLBACK,
120121
ON_USER_DEACTIVATION_STATUS_CHANGED_CALLBACK,
121122
)
122123
from synapse.push.httppusher import HttpPusher
@@ -385,6 +386,7 @@ def register_third_party_rules_callbacks(
385386
on_remove_user_third_party_identifier: Optional[
386387
ON_REMOVE_USER_THIRD_PARTY_IDENTIFIER_CALLBACK
387388
] = None,
389+
on_upgrade_room: Optional[ON_UPGRADE_ROOM_CALLBACK] = None,
388390
) -> None:
389391
"""Registers callbacks for third party event rules capabilities.
390392
@@ -403,6 +405,7 @@ def register_third_party_rules_callbacks(
403405
on_threepid_bind=on_threepid_bind,
404406
on_add_user_third_party_identifier=on_add_user_third_party_identifier,
405407
on_remove_user_third_party_identifier=on_remove_user_third_party_identifier,
408+
on_upgrade_room=on_upgrade_room,
406409
)
407410

408411
def register_presence_router_callbacks(

synapse/module_api/callbacks/third_party_event_rules_callbacks.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
from twisted.internet.defer import CancelledError
2525

2626
from synapse.api.errors import ModuleFailedException, SynapseError
27+
from synapse.api.room_versions import RoomVersion
2728
from synapse.events import EventBase
2829
from synapse.events.snapshot import UnpersistedEventContextBase
2930
from synapse.storage.roommember import ProfileInfo
@@ -54,6 +55,7 @@
5455
ON_THREEPID_BIND_CALLBACK = Callable[[str, str, str], Awaitable]
5556
ON_ADD_USER_THIRD_PARTY_IDENTIFIER_CALLBACK = Callable[[str, str, str], Awaitable]
5657
ON_REMOVE_USER_THIRD_PARTY_IDENTIFIER_CALLBACK = Callable[[str, str, str], Awaitable]
58+
ON_UPGRADE_ROOM_CALLBACK = Callable[[Requester, RoomVersion], Awaitable]
5759

5860

5961
def load_legacy_third_party_event_rules(hs: "HomeServer") -> None:
@@ -185,6 +187,7 @@ def __init__(self, hs: "HomeServer"):
185187
self._on_remove_user_third_party_identifier_callbacks: List[
186188
ON_REMOVE_USER_THIRD_PARTY_IDENTIFIER_CALLBACK
187189
] = []
190+
self._on_upgrade_room_callbacks: List[ON_UPGRADE_ROOM_CALLBACK] = []
188191

189192
def register_third_party_rules_callbacks(
190193
self,
@@ -210,6 +213,7 @@ def register_third_party_rules_callbacks(
210213
on_remove_user_third_party_identifier: Optional[
211214
ON_REMOVE_USER_THIRD_PARTY_IDENTIFIER_CALLBACK
212215
] = None,
216+
on_upgrade_room: Optional[ON_UPGRADE_ROOM_CALLBACK] = None,
213217
) -> None:
214218
"""Register callbacks from modules for each hook."""
215219
if check_event_allowed is not None:
@@ -256,6 +260,8 @@ def register_third_party_rules_callbacks(
256260
self._on_remove_user_third_party_identifier_callbacks.append(
257261
on_remove_user_third_party_identifier
258262
)
263+
if on_upgrade_room is not None:
264+
self._on_upgrade_room_callbacks.append(on_upgrade_room)
259265

260266
async def check_event_allowed(
261267
self,
@@ -597,3 +603,29 @@ async def on_remove_user_third_party_identifier(
597603
logger.exception(
598604
"Failed to run module API callback %s: %s", callback, e
599605
)
606+
607+
async def on_upgrade_room(
608+
self, requester: Requester, room_version: RoomVersion
609+
) -> None:
610+
"""Intercept requests to upgrade a room to maybe deny it (via an exception).
611+
612+
Args:
613+
requester
614+
room_version: The RoomVersion requested for the upgrade.
615+
"""
616+
for callback in self._on_upgrade_room_callbacks:
617+
try:
618+
await callback(requester, room_version)
619+
except Exception as e:
620+
logger.exception(
621+
"Failed to run module API callback %s: %s", callback, e
622+
)
623+
# Don't silence the errors raised by this callback since we expect it to
624+
# raise an exception to deny the upgrade of the room; instead make sure
625+
# it's a SynapseError we can send to clients.
626+
if not isinstance(e, SynapseError):
627+
e = SynapseError(
628+
403, "Room upgrade forbidden with these parameters"
629+
)
630+
631+
raise e

tests/rest/client/test_third_party_rules.py

Lines changed: 87 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,27 +19,29 @@
1919
#
2020
#
2121
import threading
22-
from typing import TYPE_CHECKING, Any, Dict, Optional, Tuple, Union
22+
from http import HTTPStatus
23+
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union
2324
from unittest.mock import AsyncMock, Mock
2425

2526
from twisted.test.proto_helpers import MemoryReactor
2627

2728
from synapse.api.constants import EventTypes, LoginType, Membership
28-
from synapse.api.errors import SynapseError
29+
from synapse.api.errors import Codes, SynapseError
2930
from synapse.api.room_versions import RoomVersion
3031
from synapse.config.homeserver import HomeServerConfig
3132
from synapse.events import EventBase
3233
from synapse.module_api.callbacks.third_party_event_rules_callbacks import (
3334
load_legacy_third_party_event_rules,
3435
)
3536
from synapse.rest import admin
36-
from synapse.rest.client import account, login, profile, room
37+
from synapse.rest.client import account, login, profile, room, room_upgrade_rest_servlet
3738
from synapse.server import HomeServer
3839
from synapse.types import JsonDict, Requester, StateMap
3940
from synapse.util import Clock
4041
from synapse.util.frozenutils import unfreeze
4142

4243
from tests import unittest
44+
from tests.server import make_request
4345

4446
if TYPE_CHECKING:
4547
from synapse.module_api import ModuleApi
@@ -93,11 +95,33 @@ async def check_event_allowed(
9395
return d
9496

9597

98+
class OnUpgradeRoomModule:
99+
def __init__(self, config: Dict, module_api: "ModuleApi") -> None:
100+
self.allowed_room_versions: List[str] = []
101+
if allowed_room_ver_from_config := config.get("allowed_room_versions"):
102+
self.allowed_room_versions = allowed_room_ver_from_config
103+
104+
module_api.register_third_party_rules_callbacks(
105+
on_upgrade_room=self.on_upgrade_room
106+
)
107+
108+
async def on_upgrade_room(
109+
self, requester: Requester, room_version: RoomVersion
110+
) -> None:
111+
if room_version.identifier not in self.allowed_room_versions:
112+
raise SynapseError(
113+
400,
114+
"You can not upgrade room to that version",
115+
Codes.UNSUPPORTED_ROOM_VERSION,
116+
)
117+
118+
96119
class ThirdPartyRulesTestCase(unittest.FederatingHomeserverTestCase):
97120
servlets = [
98121
admin.register_servlets,
99122
login.register_servlets,
100123
room.register_servlets,
124+
room_upgrade_rest_servlet.register_servlets,
101125
profile.register_servlets,
102126
account.register_servlets,
103127
]
@@ -426,6 +450,66 @@ def test_legacy_on_create_room(self) -> None:
426450
"""
427451
self.helper.create_room_as(self.user_id, tok=self.tok, expect_code=403)
428452

453+
@unittest.override_config(
454+
{
455+
"third_party_event_rules": {
456+
"module": __name__ + ".OnUpgradeRoomModule",
457+
"config": {
458+
"allowed_room_versions": ["9", "10"],
459+
},
460+
}
461+
}
462+
)
463+
def test_on_upgrade_room(self) -> None:
464+
"""Tests that the on_upgrade_room callbacks works correctly."""
465+
466+
def upgrade_room_to_version(
467+
_room_id: str,
468+
room_version: str,
469+
tok: Optional[str] = None,
470+
expect_code: int = HTTPStatus.OK,
471+
) -> Optional[str]:
472+
"""
473+
Upgrade a room.
474+
475+
Args:
476+
_room_id
477+
room_version: The room version to upgrade the room to.
478+
tok: The access token to use in the request.
479+
expect_code: The expected HTTP response code.
480+
Returns:
481+
The ID of the newly created room, or None if the request failed.
482+
"""
483+
path = f"/_matrix/client/r0/rooms/{_room_id}/upgrade"
484+
content = {"new_version": room_version}
485+
486+
channel = make_request(
487+
self.reactor,
488+
self.site,
489+
"POST",
490+
path,
491+
content,
492+
access_token=tok,
493+
)
494+
495+
assert channel.code == expect_code, channel.result
496+
497+
if expect_code == HTTPStatus.OK:
498+
return channel.json_body["replacement_room"]
499+
else:
500+
return None
501+
502+
# Room in configured list
503+
room_id_1 = self.helper.create_room_as(
504+
self.user_id, room_version="9", tok=self.tok
505+
)
506+
upgrade_room_to_version(room_id_1, "10", tok=self.tok, expect_code=200)
507+
# Room not in configured list
508+
room_id_2 = self.helper.create_room_as(
509+
self.user_id, room_version="10", tok=self.tok
510+
)
511+
upgrade_room_to_version(room_id_2, "11", tok=self.tok, expect_code=400)
512+
429513
def test_sent_event_end_up_in_room_state(self) -> None:
430514
"""Tests that a state event sent by a module while processing another state event
431515
doesn't get dropped from the state of the room. This is to guard against a bug

0 commit comments

Comments
 (0)