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

Commit 4e51621

Browse files
authored
Add a spamchecker method to allow or deny 3pid invites (#10894)
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. Note that a module callback already exists for 3pid invites (https://matrix-org.github.io/synapse/develop/modules/third_party_rules_callbacks.html#check_threepid_can_be_invited) but it doesn't check whether the sender of the invite is allowed to send it.
1 parent f4b1a9a commit 4e51621

File tree

5 files changed

+153
-0
lines changed

5 files changed

+153
-0
lines changed

changelog.d/10894.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add a `user_may_send_3pid_invite` spam checker callback for modules to allow or deny 3PID invites.

docs/modules/spam_checker_callbacks.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,41 @@ Called when processing an invitation. The module must return a `bool` indicating
4444
the inviter can invite the invitee to the given room. Both inviter and invitee are
4545
represented by their Matrix user ID (e.g. `@alice:example.com`).
4646

47+
### `user_may_send_3pid_invite`
48+
49+
```python
50+
async def user_may_send_3pid_invite(
51+
inviter: str,
52+
medium: str,
53+
address: str,
54+
room_id: str,
55+
) -> bool
56+
```
57+
58+
Called when processing an invitation using a third-party identifier (also called a 3PID,
59+
e.g. an email address or a phone number). The module must return a `bool` indicating
60+
whether the inviter can invite the invitee to the given room.
61+
62+
The inviter is represented by their Matrix user ID (e.g. `@alice:example.com`), and the
63+
invitee is represented by its medium (e.g. "email") and its address
64+
(e.g. `alice@example.com`). See [the Matrix specification](https://matrix.org/docs/spec/appendices#pid-types)
65+
for more information regarding third-party identifiers.
66+
67+
For example, a call to this callback to send an invitation to the email address
68+
`alice@example.com` would look like this:
69+
70+
```python
71+
await user_may_send_3pid_invite(
72+
"@bob:example.com", # The inviter's user ID
73+
"email", # The medium of the 3PID to invite
74+
"[email protected]", # The address of the 3PID to invite
75+
"!some_room:example.com", # The ID of the room to send the invite into
76+
)
77+
```
78+
79+
**Note**: If the third-party identifier is already associated with a matrix user ID,
80+
[`user_may_invite`](#user_may_invite) will be used instead.
81+
4782
### `user_may_create_room`
4883

4984
```python

synapse/events/spamcheck.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
]
4747
USER_MAY_JOIN_ROOM_CALLBACK = Callable[[str, str, bool], Awaitable[bool]]
4848
USER_MAY_INVITE_CALLBACK = Callable[[str, str, str], Awaitable[bool]]
49+
USER_MAY_SEND_3PID_INVITE_CALLBACK = Callable[[str, str, str, str], Awaitable[bool]]
4950
USER_MAY_CREATE_ROOM_CALLBACK = Callable[[str], Awaitable[bool]]
5051
USER_MAY_CREATE_ROOM_WITH_INVITES_CALLBACK = Callable[
5152
[str, List[str], List[Dict[str, str]]], Awaitable[bool]
@@ -168,6 +169,9 @@ def __init__(self):
168169
self._check_event_for_spam_callbacks: List[CHECK_EVENT_FOR_SPAM_CALLBACK] = []
169170
self._user_may_join_room_callbacks: List[USER_MAY_JOIN_ROOM_CALLBACK] = []
170171
self._user_may_invite_callbacks: List[USER_MAY_INVITE_CALLBACK] = []
172+
self._user_may_send_3pid_invite_callbacks: List[
173+
USER_MAY_SEND_3PID_INVITE_CALLBACK
174+
] = []
171175
self._user_may_create_room_callbacks: List[USER_MAY_CREATE_ROOM_CALLBACK] = []
172176
self._user_may_create_room_with_invites_callbacks: List[
173177
USER_MAY_CREATE_ROOM_WITH_INVITES_CALLBACK
@@ -191,6 +195,7 @@ def register_callbacks(
191195
check_event_for_spam: Optional[CHECK_EVENT_FOR_SPAM_CALLBACK] = None,
192196
user_may_join_room: Optional[USER_MAY_JOIN_ROOM_CALLBACK] = None,
193197
user_may_invite: Optional[USER_MAY_INVITE_CALLBACK] = None,
198+
user_may_send_3pid_invite: Optional[USER_MAY_SEND_3PID_INVITE_CALLBACK] = None,
194199
user_may_create_room: Optional[USER_MAY_CREATE_ROOM_CALLBACK] = None,
195200
user_may_create_room_with_invites: Optional[
196201
USER_MAY_CREATE_ROOM_WITH_INVITES_CALLBACK
@@ -215,6 +220,11 @@ def register_callbacks(
215220
if user_may_invite is not None:
216221
self._user_may_invite_callbacks.append(user_may_invite)
217222

223+
if user_may_send_3pid_invite is not None:
224+
self._user_may_send_3pid_invite_callbacks.append(
225+
user_may_send_3pid_invite,
226+
)
227+
218228
if user_may_create_room is not None:
219229
self._user_may_create_room_callbacks.append(user_may_create_room)
220230

@@ -304,6 +314,31 @@ async def user_may_invite(
304314

305315
return True
306316

317+
async def user_may_send_3pid_invite(
318+
self, inviter_userid: str, medium: str, address: str, room_id: str
319+
) -> bool:
320+
"""Checks if a given user may invite a given threepid into the room
321+
322+
If this method returns false, the threepid invite will be rejected.
323+
324+
Note that if the threepid is already associated with a Matrix user ID, Synapse
325+
will call user_may_invite with said user ID instead.
326+
327+
Args:
328+
inviter_userid: The user ID of the sender of the invitation
329+
medium: The 3PID's medium (e.g. "email")
330+
address: The 3PID's address (e.g. "[email protected]")
331+
room_id: The room ID
332+
333+
Returns:
334+
True if the user may send the invite, otherwise False
335+
"""
336+
for callback in self._user_may_send_3pid_invite_callbacks:
337+
if await callback(inviter_userid, medium, address, room_id) is False:
338+
return False
339+
340+
return True
341+
307342
async def user_may_create_room(self, userid: str) -> bool:
308343
"""Checks if a given user may create a room
309344

synapse/handlers/room_member.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1299,10 +1299,22 @@ async def do_3pid_invite(
12991299
if invitee:
13001300
# Note that update_membership with an action of "invite" can raise
13011301
# a ShadowBanError, but this was done above already.
1302+
# We don't check the invite against the spamchecker(s) here (through
1303+
# user_may_invite) because we'll do it further down the line anyway (in
1304+
# update_membership_locked).
13021305
_, stream_id = await self.update_membership(
13031306
requester, UserID.from_string(invitee), room_id, "invite", txn_id=txn_id
13041307
)
13051308
else:
1309+
# Check if the spamchecker(s) allow this invite to go through.
1310+
if not await self.spam_checker.user_may_send_3pid_invite(
1311+
inviter_userid=requester.user.to_string(),
1312+
medium=medium,
1313+
address=address,
1314+
room_id=room_id,
1315+
):
1316+
raise SynapseError(403, "Cannot send threepid invite")
1317+
13061318
stream_id = await self._make_and_store_3pid_invite(
13071319
requester,
13081320
id_server,

tests/rest/client/test_rooms.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2531,3 +2531,73 @@ def test_bad_alias(self):
25312531
"""An alias which does not point to the room raises a SynapseError."""
25322532
self._set_canonical_alias({"alias": "@unknown:test"}, expected_code=400)
25332533
self._set_canonical_alias({"alt_aliases": ["@unknown:test"]}, expected_code=400)
2534+
2535+
2536+
class ThreepidInviteTestCase(unittest.HomeserverTestCase):
2537+
2538+
servlets = [
2539+
admin.register_servlets,
2540+
login.register_servlets,
2541+
room.register_servlets,
2542+
]
2543+
2544+
def prepare(self, reactor, clock, homeserver):
2545+
self.user_id = self.register_user("thomas", "hackme")
2546+
self.tok = self.login("thomas", "hackme")
2547+
2548+
self.room_id = self.helper.create_room_as(self.user_id, tok=self.tok)
2549+
2550+
def test_threepid_invite_spamcheck(self):
2551+
# Mock a few functions to prevent the test from failing due to failing to talk to
2552+
# a remote IS. We keep the mock for _mock_make_and_store_3pid_invite around so we
2553+
# can check its call_count later on during the test.
2554+
make_invite_mock = Mock(return_value=make_awaitable(0))
2555+
self.hs.get_room_member_handler()._make_and_store_3pid_invite = make_invite_mock
2556+
self.hs.get_identity_handler().lookup_3pid = Mock(
2557+
return_value=make_awaitable(None),
2558+
)
2559+
2560+
# Add a mock to the spamchecker callbacks for user_may_send_3pid_invite. Make it
2561+
# allow everything for now.
2562+
mock = Mock(return_value=make_awaitable(True))
2563+
self.hs.get_spam_checker()._user_may_send_3pid_invite_callbacks.append(mock)
2564+
2565+
# Send a 3PID invite into the room and check that it succeeded.
2566+
email_to_invite = "[email protected]"
2567+
channel = self.make_request(
2568+
method="POST",
2569+
path="/rooms/" + self.room_id + "/invite",
2570+
content={
2571+
"id_server": "example.com",
2572+
"id_access_token": "sometoken",
2573+
"medium": "email",
2574+
"address": email_to_invite,
2575+
},
2576+
access_token=self.tok,
2577+
)
2578+
self.assertEquals(channel.code, 200)
2579+
2580+
# Check that the callback was called with the right params.
2581+
mock.assert_called_with(self.user_id, "email", email_to_invite, self.room_id)
2582+
2583+
# Check that the call to send the invite was made.
2584+
make_invite_mock.assert_called_once()
2585+
2586+
# Now change the return value of the callback to deny any invite and test that
2587+
# we can't send the invite.
2588+
mock.return_value = make_awaitable(False)
2589+
channel = self.make_request(
2590+
method="POST",
2591+
path="/rooms/" + self.room_id + "/invite",
2592+
content={
2593+
"id_server": "example.com",
2594+
"id_access_token": "sometoken",
2595+
"medium": "email",
2596+
"address": email_to_invite,
2597+
},
2598+
access_token=self.tok,
2599+
)
2600+
self.assertEquals(channel.code, 403)
2601+
2602+
# Also check that it stopped before calling _make_and_store_3pid_invite.
2603+
make_invite_mock.assert_called_once()

0 commit comments

Comments
 (0)