Skip to content
This repository was archived by the owner on Apr 26, 2024. It is now read-only.

Commit 5649669

Browse files
authored
Merge pull request #8535 from matrix-org/rav/third_party_events_updates
Support modifying event content from ThirdPartyRules modules
2 parents 6b5a115 + 091e948 commit 5649669

File tree

6 files changed

+118
-81
lines changed

6 files changed

+118
-81
lines changed

changelog.d/8535.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Support modifying event content in `ThirdPartyRules` modules.

synapse/events/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,12 @@ def auth_event_ids(self):
312312
"""
313313
return [e for e, _ in self.auth_events]
314314

315+
def freeze(self):
316+
"""'Freeze' the event dict, so it cannot be modified by accident"""
317+
318+
# this will be a no-op if the event dict is already frozen.
319+
self._dict = freeze(self._dict)
320+
315321

316322
class FrozenEvent(EventBase):
317323
format_version = EventFormatVersions.V1 # All events of this type are V1

synapse/events/third_party_rules.py

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1313
# See the License for the specific language governing permissions and
1414
# limitations under the License.
15-
from typing import Callable
15+
16+
from typing import Callable, Union
1617

1718
from synapse.events import EventBase
1819
from synapse.events.snapshot import EventContext
@@ -44,15 +45,20 @@ def __init__(self, hs):
4445

4546
async def check_event_allowed(
4647
self, event: EventBase, context: EventContext
47-
) -> bool:
48+
) -> Union[bool, dict]:
4849
"""Check if a provided event should be allowed in the given context.
4950
51+
The module can return:
52+
* True: the event is allowed.
53+
* False: the event is not allowed, and should be rejected with M_FORBIDDEN.
54+
* a dict: replacement event data.
55+
5056
Args:
5157
event: The event to be checked.
5258
context: The context of the event.
5359
5460
Returns:
55-
True if the event should be allowed, False if not.
61+
The result from the ThirdPartyRules module, as above
5662
"""
5763
if self.third_party_rules is None:
5864
return True
@@ -63,9 +69,10 @@ async def check_event_allowed(
6369
events = await self.store.get_events(prev_state_ids.values())
6470
state_events = {(ev.type, ev.state_key): ev for ev in events.values()}
6571

66-
# The module can modify the event slightly if it wants, but caution should be
67-
# exercised, and it's likely to go very wrong if applied to events received over
68-
# federation.
72+
# Ensure that the event is frozen, to make sure that the module is not tempted
73+
# to try to modify it. Any attempt to modify it at this point will invalidate
74+
# the hashes and signatures.
75+
event.freeze()
6976

7077
return await self.third_party_rules.check_event_allowed(event, state_events)
7178

synapse/handlers/federation.py

Lines changed: 3 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1507,18 +1507,9 @@ async def on_make_join_request(
15071507
event, context = await self.event_creation_handler.create_new_client_event(
15081508
builder=builder
15091509
)
1510-
except AuthError as e:
1510+
except SynapseError as e:
15111511
logger.warning("Failed to create join to %s because %s", room_id, e)
1512-
raise e
1513-
1514-
event_allowed = await self.third_party_event_rules.check_event_allowed(
1515-
event, context
1516-
)
1517-
if not event_allowed:
1518-
logger.info("Creation of join %s forbidden by third-party rules", event)
1519-
raise SynapseError(
1520-
403, "This event is not allowed in this context", Codes.FORBIDDEN
1521-
)
1512+
raise
15221513

15231514
# The remote hasn't signed it yet, obviously. We'll do the full checks
15241515
# when we get the event back in `on_send_join_request`
@@ -1567,15 +1558,6 @@ async def on_send_join_request(self, origin, pdu):
15671558

15681559
context = await self._handle_new_event(origin, event)
15691560

1570-
event_allowed = await self.third_party_event_rules.check_event_allowed(
1571-
event, context
1572-
)
1573-
if not event_allowed:
1574-
logger.info("Sending of join %s forbidden by third-party rules", event)
1575-
raise SynapseError(
1576-
403, "This event is not allowed in this context", Codes.FORBIDDEN
1577-
)
1578-
15791561
logger.debug(
15801562
"on_send_join_request: After _handle_new_event: %s, sigs: %s",
15811563
event.event_id,
@@ -1748,15 +1730,6 @@ async def on_make_leave_request(
17481730
builder=builder
17491731
)
17501732

1751-
event_allowed = await self.third_party_event_rules.check_event_allowed(
1752-
event, context
1753-
)
1754-
if not event_allowed:
1755-
logger.warning("Creation of leave %s forbidden by third-party rules", event)
1756-
raise SynapseError(
1757-
403, "This event is not allowed in this context", Codes.FORBIDDEN
1758-
)
1759-
17601733
try:
17611734
# The remote hasn't signed it yet, obviously. We'll do the full checks
17621735
# when we get the event back in `on_send_leave_request`
@@ -1789,16 +1762,7 @@ async def on_send_leave_request(self, origin, pdu):
17891762

17901763
event.internal_metadata.outlier = False
17911764

1792-
context = await self._handle_new_event(origin, event)
1793-
1794-
event_allowed = await self.third_party_event_rules.check_event_allowed(
1795-
event, context
1796-
)
1797-
if not event_allowed:
1798-
logger.info("Sending of leave %s forbidden by third-party rules", event)
1799-
raise SynapseError(
1800-
403, "This event is not allowed in this context", Codes.FORBIDDEN
1801-
)
1765+
await self._handle_new_event(origin, event)
18021766

18031767
logger.debug(
18041768
"on_send_leave_request: After _handle_new_event: %s, sigs: %s",
@@ -2694,18 +2658,6 @@ async def exchange_third_party_invite(
26942658
builder=builder
26952659
)
26962660

2697-
event_allowed = await self.third_party_event_rules.check_event_allowed(
2698-
event, context
2699-
)
2700-
if not event_allowed:
2701-
logger.info(
2702-
"Creation of threepid invite %s forbidden by third-party rules",
2703-
event,
2704-
)
2705-
raise SynapseError(
2706-
403, "This event is not allowed in this context", Codes.FORBIDDEN
2707-
)
2708-
27092661
event, context = await self.add_display_name_to_third_party_invite(
27102662
room_version, event_dict, event, context
27112663
)
@@ -2756,18 +2708,6 @@ async def on_exchange_third_party_invite_request(
27562708
event, context = await self.event_creation_handler.create_new_client_event(
27572709
builder=builder
27582710
)
2759-
2760-
event_allowed = await self.third_party_event_rules.check_event_allowed(
2761-
event, context
2762-
)
2763-
if not event_allowed:
2764-
logger.warning(
2765-
"Exchange of threepid invite %s forbidden by third-party rules", event
2766-
)
2767-
raise SynapseError(
2768-
403, "This event is not allowed in this context", Codes.FORBIDDEN
2769-
)
2770-
27712711
event, context = await self.add_display_name_to_third_party_invite(
27722712
room_version, event_dict, event, context
27732713
)

synapse/handlers/message.py

Lines changed: 71 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -811,6 +811,23 @@ async def create_new_client_event(
811811
if requester:
812812
context.app_service = requester.app_service
813813

814+
third_party_result = await self.third_party_event_rules.check_event_allowed(
815+
event, context
816+
)
817+
if not third_party_result:
818+
logger.info(
819+
"Event %s forbidden by third-party rules", event,
820+
)
821+
raise SynapseError(
822+
403, "This event is not allowed in this context", Codes.FORBIDDEN
823+
)
824+
elif isinstance(third_party_result, dict):
825+
# the third-party rules want to replace the event. We'll need to build a new
826+
# event.
827+
event, context = await self._rebuild_event_after_third_party_rules(
828+
third_party_result, event
829+
)
830+
814831
self.validator.validate_new(event, self.config)
815832

816833
# If this event is an annotation then we check that that the sender
@@ -897,14 +914,6 @@ async def handle_new_client_event(
897914
else:
898915
room_version = await self.store.get_room_version_id(event.room_id)
899916

900-
event_allowed = await self.third_party_event_rules.check_event_allowed(
901-
event, context
902-
)
903-
if not event_allowed:
904-
raise SynapseError(
905-
403, "This event is not allowed in this context", Codes.FORBIDDEN
906-
)
907-
908917
if event.internal_metadata.is_out_of_band_membership():
909918
# the only sort of out-of-band-membership events we expect to see here
910919
# are invite rejections we have generated ourselves.
@@ -1307,3 +1316,57 @@ def _expire_rooms_to_exclude_from_dummy_event_insertion(self):
13071316
room_id,
13081317
)
13091318
del self._rooms_to_exclude_from_dummy_event_insertion[room_id]
1319+
1320+
async def _rebuild_event_after_third_party_rules(
1321+
self, third_party_result: dict, original_event: EventBase
1322+
) -> Tuple[EventBase, EventContext]:
1323+
# the third_party_event_rules want to replace the event.
1324+
# we do some basic checks, and then return the replacement event and context.
1325+
1326+
# Construct a new EventBuilder and validate it, which helps with the
1327+
# rest of these checks.
1328+
try:
1329+
builder = self.event_builder_factory.for_room_version(
1330+
original_event.room_version, third_party_result
1331+
)
1332+
self.validator.validate_builder(builder)
1333+
except SynapseError as e:
1334+
raise Exception(
1335+
"Third party rules module created an invalid event: " + e.msg,
1336+
)
1337+
1338+
immutable_fields = [
1339+
# changing the room is going to break things: we've already checked that the
1340+
# room exists, and are holding a concurrency limiter token for that room.
1341+
# Also, we might need to use a different room version.
1342+
"room_id",
1343+
# changing the type or state key might work, but we'd need to check that the
1344+
# calling functions aren't making assumptions about them.
1345+
"type",
1346+
"state_key",
1347+
]
1348+
1349+
for k in immutable_fields:
1350+
if getattr(builder, k, None) != original_event.get(k):
1351+
raise Exception(
1352+
"Third party rules module created an invalid event: "
1353+
"cannot change field " + k
1354+
)
1355+
1356+
# check that the new sender belongs to this HS
1357+
if not self.hs.is_mine_id(builder.sender):
1358+
raise Exception(
1359+
"Third party rules module created an invalid event: "
1360+
"invalid sender " + builder.sender
1361+
)
1362+
1363+
# copy over the original internal metadata
1364+
for k, v in original_event.internal_metadata.get_dict().items():
1365+
setattr(builder.internal_metadata, k, v)
1366+
1367+
event = await builder.build(prev_event_ids=original_event.prev_event_ids())
1368+
1369+
# we rebuild the event context, to be on the safe side. If nothing else,
1370+
# delta_ids might need an update.
1371+
context = await self.state.compute_event_context(event)
1372+
return event, context

tests/rest/client/test_third_party_rules.py

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -114,16 +114,36 @@ async def check(ev, state):
114114
self.render(request)
115115
self.assertEquals(channel.result["code"], b"403", channel.result)
116116

117-
def test_modify_event(self):
118-
"""Tests that the module can successfully tweak an event before it is persisted.
119-
"""
120-
# first patch the event checker so that it will modify the event
117+
def test_cannot_modify_event(self):
118+
"""cannot accidentally modify an event before it is persisted"""
119+
120+
# first patch the event checker so that it will try to modify the event
121121
async def check(ev: EventBase, state):
122122
ev.content = {"x": "y"}
123123
return True
124124

125125
current_rules_module().check_event_allowed = check
126126

127+
# now send the event
128+
request, channel = self.make_request(
129+
"PUT",
130+
"/_matrix/client/r0/rooms/%s/send/modifyme/1" % self.room_id,
131+
{"x": "x"},
132+
access_token=self.tok,
133+
)
134+
self.render(request)
135+
self.assertEqual(channel.result["code"], b"500", channel.result)
136+
137+
def test_modify_event(self):
138+
"""The module can return a modified version of the event"""
139+
# first patch the event checker so that it will modify the event
140+
async def check(ev: EventBase, state):
141+
d = ev.get_dict()
142+
d["content"] = {"x": "y"}
143+
return d
144+
145+
current_rules_module().check_event_allowed = check
146+
127147
# now send the event
128148
request, channel = self.make_request(
129149
"PUT",

0 commit comments

Comments
 (0)