This repository was archived by the owner on Apr 26, 2024. It is now read-only.
-
-
Notifications
You must be signed in to change notification settings - Fork 2.1k
Add a spamchecker method to allow or deny 3pid invites #10894
Merged
Merged
Changes from 4 commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
423e767
Add a spamchecker method to allow or deny 3pid invites
babolivier d548b4f
Changelog
babolivier 78c211d
Make changelog more explicit
babolivier 897ef5f
Lint
babolivier a7ad613
Merge branch 'develop' into babolivier/user_may_send_threepid_invite
babolivier 949bcf7
Incorporate comment
babolivier a82e14e
Lint
babolivier 8785f5b
Add simplifications from review
babolivier 9ee44db
Merge branch 'develop' of github.com:matrix-org/synapse into babolivi…
babolivier 1e40ce8
Use mock's assertions in test
babolivier File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
Add a `user_may_send_3pid_invite` spam checker callback for modules to allow or deny 3PID invites. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -29,6 +29,41 @@ Called when processing an invitation. The module must return a `bool` indicating | |
the inviter can invite the invitee to the given room. Both inviter and invitee are | ||
represented by their Matrix user ID (e.g. `@alice:example.com`). | ||
|
||
### `user_may_send_threepid_invite` | ||
|
||
```python | ||
async def user_may_send_3pid_invite( | ||
inviter: str, | ||
invitee: Dict[str, str], | ||
room_id: str, | ||
) -> bool | ||
``` | ||
|
||
Called when processing an invitation using a third-party identifier (also called a 3PID, | ||
e.g. an email address or a phone number). The module must return a `bool` indicating | ||
whether the inviter can invite the invitee to the given room. | ||
|
||
The inviter is represented by their Matrix user ID (e.g. `@alice:example.com`), and the | ||
invitee is represented by a dict describing the third-party identifier to send an | ||
invitation to, with a `medium` key indicating the identifier's medium (e.g. "email") and | ||
an `address` key indicating the identifier's address (e.g. `[email protected]`). See | ||
[the Matrix specification](https://matrix.org/docs/spec/appendices#pid-types) for more | ||
information regarding third-party identifiers. | ||
|
||
For example, a call to this callback to send an invitation to the email address | ||
`[email protected]` would look like this: | ||
|
||
```python | ||
await user_may_send_3pid_invite( | ||
"@bob:example.com", # The inviter's user ID | ||
{"medium": "email", "address": "[email protected]"}, # The 3PID to invite | ||
"!some_room:example.com", # The ID of the room to send the invite into | ||
) | ||
``` | ||
|
||
**Note**: If the third-party identifier is already associated with a matrix user ID, | ||
[`user_may_invite`](#user_may_invite) will be used instead. | ||
|
||
### `user_may_create_room` | ||
|
||
```python | ||
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -45,6 +45,9 @@ | |
Awaitable[Union[bool, str]], | ||
] | ||
USER_MAY_INVITE_CALLBACK = Callable[[str, str, str], Awaitable[bool]] | ||
USER_MAY_SEND_3PID_INVITE_CALLBACK = Callable[ | ||
[str, Dict[str, str], str], Awaitable[bool] | ||
] | ||
USER_MAY_CREATE_ROOM_CALLBACK = Callable[[str], Awaitable[bool]] | ||
USER_MAY_CREATE_ROOM_ALIAS_CALLBACK = Callable[[str, RoomAlias], Awaitable[bool]] | ||
USER_MAY_PUBLISH_ROOM_CALLBACK = Callable[[str, str], Awaitable[bool]] | ||
|
@@ -163,6 +166,9 @@ class SpamChecker: | |
def __init__(self): | ||
self._check_event_for_spam_callbacks: List[CHECK_EVENT_FOR_SPAM_CALLBACK] = [] | ||
self._user_may_invite_callbacks: List[USER_MAY_INVITE_CALLBACK] = [] | ||
self._user_may_send_3pid_invite_callbacks: List[ | ||
USER_MAY_SEND_3PID_INVITE_CALLBACK | ||
] = [] | ||
self._user_may_create_room_callbacks: List[USER_MAY_CREATE_ROOM_CALLBACK] = [] | ||
self._user_may_create_room_alias_callbacks: List[ | ||
USER_MAY_CREATE_ROOM_ALIAS_CALLBACK | ||
|
@@ -182,6 +188,7 @@ def register_callbacks( | |
self, | ||
check_event_for_spam: Optional[CHECK_EVENT_FOR_SPAM_CALLBACK] = None, | ||
user_may_invite: Optional[USER_MAY_INVITE_CALLBACK] = None, | ||
user_may_send_3pid_invite: Optional[USER_MAY_SEND_3PID_INVITE_CALLBACK] = None, | ||
user_may_create_room: Optional[USER_MAY_CREATE_ROOM_CALLBACK] = None, | ||
user_may_create_room_alias: Optional[ | ||
USER_MAY_CREATE_ROOM_ALIAS_CALLBACK | ||
|
@@ -200,6 +207,11 @@ def register_callbacks( | |
if user_may_invite is not None: | ||
self._user_may_invite_callbacks.append(user_may_invite) | ||
|
||
if user_may_send_3pid_invite is not None: | ||
self._user_may_send_3pid_invite_callbacks.append( | ||
user_may_send_3pid_invite, | ||
) | ||
|
||
if user_may_create_room is not None: | ||
self._user_may_create_room_callbacks.append(user_may_create_room) | ||
|
||
|
@@ -266,6 +278,32 @@ async def user_may_invite( | |
|
||
return True | ||
|
||
async def user_may_send_3pid_invite( | ||
self, inviter_userid: str, invitee_threepid: Dict[str, str], room_id: str | ||
) -> bool: | ||
"""Checks if a given user may invite a given threepid into the room | ||
|
||
If this method returns false, the threepid invite will be rejected. | ||
|
||
Note that if the threepid is already associated with a Matrix user ID, Synapse | ||
will call user_may_invite with said user ID instead. | ||
|
||
Args: | ||
inviter_userid: The user ID of the sender of the invitation | ||
invitee_threepid: The threepid targeted in the invitation, as a dict including | ||
a "medium" key indicating the threepid's medium (e.g. "email") and an | ||
"address" key indicating the threepid's address (e.g. "[email protected]") | ||
babolivier marked this conversation as resolved.
Show resolved
Hide resolved
|
||
room_id: The room ID | ||
|
||
Returns: | ||
True if the user may send the invite, otherwise False | ||
""" | ||
for callback in self._user_may_send_3pid_invite_callbacks: | ||
if await callback(inviter_userid, invitee_threepid, room_id) is False: | ||
return False | ||
|
||
return True | ||
|
||
async def user_may_create_room(self, userid: str) -> bool: | ||
"""Checks if a given user may create a room | ||
|
||
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -18,7 +18,7 @@ | |
"""Tests REST events for /rooms paths.""" | ||
|
||
import json | ||
from typing import Iterable | ||
from typing import Dict, Iterable, Optional | ||
from unittest.mock import Mock, call | ||
from urllib import parse as urlparse | ||
|
||
|
@@ -30,7 +30,7 @@ | |
from synapse.handlers.pagination import PurgeStatus | ||
from synapse.rest import admin | ||
from synapse.rest.client import account, directory, login, profile, room, sync | ||
from synapse.types import JsonDict, RoomAlias, UserID, create_requester | ||
from synapse.types import JsonDict, Requester, RoomAlias, UserID, create_requester | ||
from synapse.util.stringutils import random_string | ||
|
||
from tests import unittest | ||
|
@@ -2315,3 +2315,140 @@ def test_bad_alias(self): | |
"""An alias which does not point to the room raises a SynapseError.""" | ||
self._set_canonical_alias({"alias": "@unknown:test"}, expected_code=400) | ||
self._set_canonical_alias({"alt_aliases": ["@unknown:test"]}, expected_code=400) | ||
|
||
|
||
class ThreepidInviteTestCase(unittest.HomeserverTestCase): | ||
|
||
servlets = [ | ||
admin.register_servlets, | ||
login.register_servlets, | ||
room.register_servlets, | ||
] | ||
|
||
def prepare(self, reactor, clock, homeserver): | ||
self.user_id = self.register_user("thomas", "hackme") | ||
self.tok = self.login("thomas", "hackme") | ||
|
||
self.room_id = self.helper.create_room_as(self.user_id, tok=self.tok) | ||
|
||
def test_threepid_invite_spamcheck(self): | ||
# Mock a few functions to prevent the test from failing due to failing to talk to | ||
# a remote IS. We keep the mock for _mock_make_and_store_3pid_invite around so we | ||
# can check its call_count later on during the test. | ||
make_invite_mock = self._mock_make_and_store_3pid_invite() | ||
self._mock_lookup_3pid() | ||
|
||
# Add a mock to the spamchecker callbacks for user_may_send_3pid_invite. Make it | ||
# allow everything for now. | ||
return_value = True | ||
|
||
async def _user_may_send_3pid_invite( | ||
inviter: str, | ||
invitee: Dict[str, str], | ||
room_id: str, | ||
) -> bool: | ||
return return_value | ||
|
||
allow_mock = Mock(side_effect=_user_may_send_3pid_invite) | ||
|
||
self.hs.get_spam_checker()._user_may_send_3pid_invite_callbacks.append( | ||
allow_mock | ||
) | ||
|
||
# Send a 3PID invite into the room and check that it succeeded. | ||
email_to_invite = "[email protected]" | ||
channel = self.make_request( | ||
method="POST", | ||
path="/rooms/" + self.room_id + "/invite", | ||
content={ | ||
"id_server": "example.com", | ||
"id_access_token": "sometoken", | ||
"medium": "email", | ||
"address": email_to_invite, | ||
}, | ||
access_token=self.tok, | ||
) | ||
self.assertEquals(channel.code, 200) | ||
|
||
# Check that the callback was called with the right params. | ||
expected_call_args = ( | ||
( | ||
self.user_id, | ||
{"medium": "email", "address": email_to_invite}, | ||
self.room_id, | ||
), | ||
) | ||
|
||
self.assertEquals( | ||
allow_mock.call_args, expected_call_args, allow_mock.call_args | ||
) | ||
|
||
# Check that the call to send the invite was made. | ||
self.assertEquals(make_invite_mock.call_count, 1) | ||
|
||
# Now change the return value of the callback to deny any invite and test that | ||
# we can't send the invite. | ||
return_value = False | ||
babolivier marked this conversation as resolved.
Show resolved
Hide resolved
|
||
channel = self.make_request( | ||
method="POST", | ||
path="/rooms/" + self.room_id + "/invite", | ||
content={ | ||
"id_server": "example.com", | ||
"id_access_token": "sometoken", | ||
"medium": "email", | ||
"address": email_to_invite, | ||
}, | ||
access_token=self.tok, | ||
) | ||
self.assertEquals(channel.code, 403) | ||
|
||
# Also check that it stopped before calling _make_and_store_3pid_invite. | ||
self.assertEquals(make_invite_mock.call_count, 1) | ||
|
||
def _mock_make_and_store_3pid_invite(self) -> Mock: | ||
"""Mocks RoomMemberHandler._make_and_store_3pid_invite with a function that just | ||
returns the integer 0. | ||
|
||
Returns: | ||
The Mock object _make_and_store_3pid_invite was replaced with. | ||
""" | ||
|
||
async def _make_and_store_3pid_invite( | ||
requester: Requester, | ||
id_server: str, | ||
medium: str, | ||
address: str, | ||
room_id: str, | ||
user: UserID, | ||
txn_id: Optional[str], | ||
id_access_token: Optional[str] = None, | ||
) -> int: | ||
return 0 | ||
|
||
mock = Mock(side_effect=_make_and_store_3pid_invite) | ||
|
||
self.hs.get_room_member_handler()._make_and_store_3pid_invite = mock | ||
babolivier marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
return mock | ||
|
||
def _mock_lookup_3pid(self) -> Mock: | ||
"""Mocks IdentityHandler.lookup_3pid with a function that just returns None (ie | ||
no binding for the 3PID. | ||
|
||
Returns: | ||
The Mock object lookup_3pid was replaced with. | ||
""" | ||
|
||
async def _lookup_3pid( | ||
id_server: str, | ||
medium: str, | ||
address: str, | ||
id_access_token: Optional[str] = None, | ||
) -> Optional[str]: | ||
return None | ||
|
||
mock = Mock(side_effect=_lookup_3pid) | ||
|
||
self.hs.get_identity_handler().lookup_3pid = mock | ||
|
||
return mock |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.