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

Commit b10257e

Browse files
authored
Add a spamchecker callback to allow or deny room creation based on invites (#10898)
This is in the context of creating new module callbacks that modules in https://github.com/matrix-org/synapse-dinsic can use, in an effort to reconcile the spam checker API in synapse-dinsic with the one in mainline. This adds a callback that's fairly similar to user_may_create_room except it also allows processing based on the invites sent at room creation.
1 parent ea01d4c commit b10257e

File tree

5 files changed

+199
-6
lines changed

5 files changed

+199
-6
lines changed

changelog.d/10898.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add a `user_may_create_room_with_invites` spam checker callback to allow modules to allow or deny a room creation request based on the invites and/or 3PID invites it includes.

docs/modules/spam_checker_callbacks.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,35 @@ async def user_may_create_room(user: str) -> bool
3838
Called when processing a room creation request. The module must return a `bool` indicating
3939
whether the given user (represented by their Matrix user ID) is allowed to create a room.
4040

41+
### `user_may_create_room_with_invites`
42+
43+
```python
44+
async def user_may_create_room_with_invites(
45+
user: str,
46+
invites: List[str],
47+
threepid_invites: List[Dict[str, str]],
48+
) -> bool
49+
```
50+
51+
Called when processing a room creation request (right after `user_may_create_room`).
52+
The module is given the Matrix user ID of the user trying to create a room, as well as a
53+
list of Matrix users to invite and a list of third-party identifiers (3PID, e.g. email
54+
addresses) to invite.
55+
56+
An invited Matrix user to invite is represented by their Matrix user IDs, and an invited
57+
3PIDs is represented by a dict that includes the 3PID medium (e.g. "email") through its
58+
`medium` key and its address (e.g. "[email protected]") through its `address` key.
59+
60+
See [the Matrix specification](https://matrix.org/docs/spec/appendices#pid-types) for more
61+
information regarding third-party identifiers.
62+
63+
If no invite and/or 3PID invite were specified in the room creation request, the
64+
corresponding list(s) will be empty.
65+
66+
**Note**: This callback is not called when a room is cloned (e.g. during a room upgrade)
67+
since no invites are sent when cloning a room. To cover this case, modules also need to
68+
implement `user_may_create_room`.
69+
4170
### `user_may_create_room_alias`
4271

4372
```python

synapse/events/spamcheck.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@
4646
]
4747
USER_MAY_INVITE_CALLBACK = Callable[[str, str, str], Awaitable[bool]]
4848
USER_MAY_CREATE_ROOM_CALLBACK = Callable[[str], Awaitable[bool]]
49+
USER_MAY_CREATE_ROOM_WITH_INVITES_CALLBACK = Callable[
50+
[str, List[str], List[Dict[str, str]]], Awaitable[bool]
51+
]
4952
USER_MAY_CREATE_ROOM_ALIAS_CALLBACK = Callable[[str, RoomAlias], Awaitable[bool]]
5053
USER_MAY_PUBLISH_ROOM_CALLBACK = Callable[[str, str], Awaitable[bool]]
5154
CHECK_USERNAME_FOR_SPAM_CALLBACK = Callable[[Dict[str, str]], Awaitable[bool]]
@@ -164,6 +167,9 @@ def __init__(self):
164167
self._check_event_for_spam_callbacks: List[CHECK_EVENT_FOR_SPAM_CALLBACK] = []
165168
self._user_may_invite_callbacks: List[USER_MAY_INVITE_CALLBACK] = []
166169
self._user_may_create_room_callbacks: List[USER_MAY_CREATE_ROOM_CALLBACK] = []
170+
self._user_may_create_room_with_invites_callbacks: List[
171+
USER_MAY_CREATE_ROOM_WITH_INVITES_CALLBACK
172+
] = []
167173
self._user_may_create_room_alias_callbacks: List[
168174
USER_MAY_CREATE_ROOM_ALIAS_CALLBACK
169175
] = []
@@ -183,6 +189,9 @@ def register_callbacks(
183189
check_event_for_spam: Optional[CHECK_EVENT_FOR_SPAM_CALLBACK] = None,
184190
user_may_invite: Optional[USER_MAY_INVITE_CALLBACK] = None,
185191
user_may_create_room: Optional[USER_MAY_CREATE_ROOM_CALLBACK] = None,
192+
user_may_create_room_with_invites: Optional[
193+
USER_MAY_CREATE_ROOM_WITH_INVITES_CALLBACK
194+
] = None,
186195
user_may_create_room_alias: Optional[
187196
USER_MAY_CREATE_ROOM_ALIAS_CALLBACK
188197
] = None,
@@ -203,6 +212,11 @@ def register_callbacks(
203212
if user_may_create_room is not None:
204213
self._user_may_create_room_callbacks.append(user_may_create_room)
205214

215+
if user_may_create_room_with_invites is not None:
216+
self._user_may_create_room_with_invites_callbacks.append(
217+
user_may_create_room_with_invites,
218+
)
219+
206220
if user_may_create_room_alias is not None:
207221
self._user_may_create_room_alias_callbacks.append(
208222
user_may_create_room_alias,
@@ -283,6 +297,34 @@ async def user_may_create_room(self, userid: str) -> bool:
283297

284298
return True
285299

300+
async def user_may_create_room_with_invites(
301+
self,
302+
userid: str,
303+
invites: List[str],
304+
threepid_invites: List[Dict[str, str]],
305+
) -> bool:
306+
"""Checks if a given user may create a room with invites
307+
308+
If this method returns false, the creation request will be rejected.
309+
310+
Args:
311+
userid: The ID of the user attempting to create a room
312+
invites: The IDs of the Matrix users to be invited if the room creation is
313+
allowed.
314+
threepid_invites: The threepids to be invited if the room creation is allowed,
315+
as a dict including a "medium" key indicating the threepid's medium (e.g.
316+
"email") and an "address" key indicating the threepid's address (e.g.
317+
318+
319+
Returns:
320+
True if the user may create the room, otherwise False
321+
"""
322+
for callback in self._user_may_create_room_with_invites_callbacks:
323+
if await callback(userid, invites, threepid_invites) is False:
324+
return False
325+
326+
return True
327+
286328
async def user_may_create_room_alias(
287329
self, userid: str, room_alias: RoomAlias
288330
) -> bool:

synapse/handlers/room.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -649,8 +649,16 @@ async def create_room(
649649
requester, config, is_requester_admin=is_requester_admin
650650
)
651651

652-
if not is_requester_admin and not await self.spam_checker.user_may_create_room(
653-
user_id
652+
invite_3pid_list = config.get("invite_3pid", [])
653+
invite_list = config.get("invite", [])
654+
655+
if not is_requester_admin and not (
656+
await self.spam_checker.user_may_create_room(user_id)
657+
and await self.spam_checker.user_may_create_room_with_invites(
658+
user_id,
659+
invite_list,
660+
invite_3pid_list,
661+
)
654662
):
655663
raise SynapseError(403, "You are not permitted to create rooms")
656664

@@ -684,8 +692,6 @@ async def create_room(
684692
if mapping:
685693
raise SynapseError(400, "Room alias already taken", Codes.ROOM_IN_USE)
686694

687-
invite_3pid_list = config.get("invite_3pid", [])
688-
invite_list = config.get("invite", [])
689695
for i in invite_list:
690696
try:
691697
uid = UserID.from_string(i)

tests/rest/client/test_rooms.py

Lines changed: 117 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
"""Tests REST events for /rooms paths."""
1919

2020
import json
21-
from typing import Iterable
21+
from typing import Dict, Iterable, List, Optional
2222
from unittest.mock import Mock, call
2323
from urllib import parse as urlparse
2424

@@ -30,7 +30,7 @@
3030
from synapse.handlers.pagination import PurgeStatus
3131
from synapse.rest import admin
3232
from synapse.rest.client import account, directory, login, profile, room, sync
33-
from synapse.types import JsonDict, RoomAlias, UserID, create_requester
33+
from synapse.types import JsonDict, Requester, RoomAlias, UserID, create_requester
3434
from synapse.util.stringutils import random_string
3535

3636
from tests import unittest
@@ -669,6 +669,121 @@ def test_post_room_invitees_ratelimit(self):
669669
channel = self.make_request("POST", "/createRoom", content)
670670
self.assertEqual(200, channel.code)
671671

672+
def test_spamchecker_invites(self):
673+
"""Tests the user_may_create_room_with_invites spam checker callback."""
674+
675+
# Mock do_3pid_invite, so we don't fail from failing to send a 3PID invite to an
676+
# IS.
677+
async def do_3pid_invite(
678+
room_id: str,
679+
inviter: UserID,
680+
medium: str,
681+
address: str,
682+
id_server: str,
683+
requester: Requester,
684+
txn_id: Optional[str],
685+
id_access_token: Optional[str] = None,
686+
) -> int:
687+
return 0
688+
689+
do_3pid_invite_mock = Mock(side_effect=do_3pid_invite)
690+
self.hs.get_room_member_handler().do_3pid_invite = do_3pid_invite_mock
691+
692+
# Add a mock callback for user_may_create_room_with_invites. Make it allow any
693+
# room creation request for now.
694+
return_value = True
695+
696+
async def user_may_create_room_with_invites(
697+
user: str,
698+
invites: List[str],
699+
threepid_invites: List[Dict[str, str]],
700+
) -> bool:
701+
return return_value
702+
703+
callback_mock = Mock(side_effect=user_may_create_room_with_invites)
704+
self.hs.get_spam_checker()._user_may_create_room_with_invites_callbacks.append(
705+
callback_mock,
706+
)
707+
708+
# The MXIDs we'll try to invite.
709+
invited_mxids = [
710+
"@alice1:red",
711+
"@alice2:red",
712+
"@alice3:red",
713+
"@alice4:red",
714+
]
715+
716+
# The 3PIDs we'll try to invite.
717+
invited_3pids = [
718+
{
719+
"id_server": "example.com",
720+
"id_access_token": "sometoken",
721+
"medium": "email",
722+
"address": "[email protected]",
723+
},
724+
{
725+
"id_server": "example.com",
726+
"id_access_token": "sometoken",
727+
"medium": "email",
728+
"address": "[email protected]",
729+
},
730+
{
731+
"id_server": "example.com",
732+
"id_access_token": "sometoken",
733+
"medium": "email",
734+
"address": "[email protected]",
735+
},
736+
]
737+
738+
# Create a room and invite the Matrix users, and check that it succeeded.
739+
channel = self.make_request(
740+
"POST",
741+
"/createRoom",
742+
json.dumps({"invite": invited_mxids}).encode("utf8"),
743+
)
744+
self.assertEqual(200, channel.code)
745+
746+
# Check that the callback was called with the right arguments.
747+
expected_call_args = ((self.user_id, invited_mxids, []),)
748+
self.assertEquals(
749+
callback_mock.call_args,
750+
expected_call_args,
751+
callback_mock.call_args,
752+
)
753+
754+
# Create a room and invite the 3PIDs, and check that it succeeded.
755+
channel = self.make_request(
756+
"POST",
757+
"/createRoom",
758+
json.dumps({"invite_3pid": invited_3pids}).encode("utf8"),
759+
)
760+
self.assertEqual(200, channel.code)
761+
762+
# Check that do_3pid_invite was called the right amount of time
763+
self.assertEquals(do_3pid_invite_mock.call_count, len(invited_3pids))
764+
765+
# Check that the callback was called with the right arguments.
766+
expected_call_args = ((self.user_id, [], invited_3pids),)
767+
self.assertEquals(
768+
callback_mock.call_args,
769+
expected_call_args,
770+
callback_mock.call_args,
771+
)
772+
773+
# Now deny any room creation.
774+
return_value = False
775+
776+
# Create a room and invite the 3PIDs, and check that it failed.
777+
channel = self.make_request(
778+
"POST",
779+
"/createRoom",
780+
json.dumps({"invite_3pid": invited_3pids}).encode("utf8"),
781+
)
782+
self.assertEqual(403, channel.code)
783+
784+
# Check that do_3pid_invite wasn't called this time.
785+
self.assertEquals(do_3pid_invite_mock.call_count, len(invited_3pids))
786+
672787

673788
class RoomTopicTestCase(RoomBase):
674789
"""Tests /rooms/$room_id/topic REST events."""

0 commit comments

Comments
 (0)