Merge pull request #4910 from matrix-org/erikj/third_party_invite_create_spam

Add third party invite support to spam checker
pull/4997/head dinsic_2019-03-21
Erik Johnston 2019-03-21 16:07:27 +00:00 committed by GitHub
commit 53dd358c83
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 231 additions and 50 deletions

View File

@ -654,9 +654,27 @@ uploads_path: "DATADIR/uploads"
#
#disable_msisdn_registration: true
# Derive the user's matrix ID from a type of 3PID used when registering.
# This overrides any matrix ID the user proposes when calling /register
# The 3PID type should be present in registrations_require_3pid to avoid
# users failing to register if they don't specify the right kind of 3pid.
#
#register_mxid_from_3pid: email
# Mandate that users are only allowed to associate certain formats of
# 3PIDs with accounts on this server.
#
# Use an Identity Server to establish which 3PIDs are allowed to register?
# Overrides allowed_local_3pids below.
#
#check_is_for_allowed_local_3pids: matrix.org
#
# If you are using an IS you can also check whether that IS registers
# pending invites for the given 3PID (and then allow it to sign up on
# the platform):
#
#allow_invited_3pids: False
#
#allowed_local_3pids:
# - medium: email
# pattern: '.*@matrix\.org'
@ -665,6 +683,11 @@ uploads_path: "DATADIR/uploads"
# - medium: msisdn
# pattern: '\+44'
# If true, stop users from trying to change the 3PIDs associated with
# their accounts.
#
#disable_3pid_changes: False
# If set, allows registration of standard or admin accounts by anyone who
# has the shared secret, even if registration is otherwise disabled.
#
@ -702,6 +725,30 @@ uploads_path: "DATADIR/uploads"
# - matrix.org
# - vector.im
# If enabled, user IDs, display names and avatar URLs will be replicated
# to this server whenever they change.
# This is an experimental API currently implemented by sydent to support
# cross-homeserver user directories.
#
#replicate_user_profiles_to: example.com
# If specified, attempt to replay registrations, profile changes & 3pid
# bindings on the given target homeserver via the AS API. The HS is authed
# via a given AS token.
#
#shadow_server:
# hs_url: https://shadow.example.com
# hs: shadow.example.com
# as_token: 12u394refgbdhivsia
# If enabled, don't let users set their own display names/avatars
# other than for the very first time (unless they are a server admin).
# Useful when provisioning users based on the contents of a 3rd party
# directory and to avoid ambiguities.
#
#disable_set_displayname: False
#disable_set_avatar_url: False
# Users who register on this homeserver will automatically be joined
# to these rooms
#
@ -975,6 +1022,11 @@ password_config:
#user_directory:
# enabled: true
# search_all_users: false
#
# # If this is set, user search will be delegated to this ID server instead
# # of synapse performing the search itself.
# # This is an experimental API.
# defer_to_id_server: https://id.example.com
# User Consent configuration

View File

@ -52,9 +52,8 @@ class UserDirectoryConfig(Config):
# on your database to tell it to rebuild the user_directory search indexes.
#
#user_directory:
# enabled: true
#
# search_all_users: false
# enabled: true
# search_all_users: false
#
# # If this is set, user search will be delegated to this ID server instead
# # of synapse performing the search itself.

View File

@ -46,14 +46,19 @@ class SpamChecker(object):
return self.spam_checker.check_event_for_spam(event)
def user_may_invite(self, inviter_userid, invitee_userid, room_id, new_room):
def user_may_invite(self, inviter_userid, invitee_userid, third_party_invite,
room_id, new_room):
"""Checks if a given user may send an invite
If this method returns false, the invite will be rejected.
Args:
inviter_userid (str)
invitee_userid (str)
invitee_userid (str|None): The user ID of the invitee. Is None
if this is a third party invite and the 3PID is not bound to a
user ID.
third_party_invite (dict|None): If a third party invite then is a
dict containing the medium and address of the invitee.
room_id (str)
new_room (bool): Wether the user is being invited to the room as
part of a room creation, if so the invitee would have been
@ -66,10 +71,11 @@ class SpamChecker(object):
return True
return self.spam_checker.user_may_invite(
inviter_userid, invitee_userid, room_id, new_room,
inviter_userid, invitee_userid, third_party_invite, room_id, new_room,
)
def user_may_create_room(self, userid, invite_list, cloning):
def user_may_create_room(self, userid, invite_list, third_party_invite_list,
cloning):
"""Checks if a given user may create a room
If this method returns false, the creation request will be rejected.
@ -78,6 +84,8 @@ class SpamChecker(object):
userid (string): The sender's user ID
invite_list (list[str]): List of user IDs that would be invited to
the new room.
third_party_invite_list (list[dict]): List of third party invites
for the new room.
cloning (bool): Whether the user is cloning an existing room, e.g.
upgrading a room.
@ -87,7 +95,9 @@ class SpamChecker(object):
if self.spam_checker is None:
return True
return self.spam_checker.user_may_create_room(userid, invite_list, cloning)
return self.spam_checker.user_may_create_room(
userid, invite_list, third_party_invite_list, cloning,
)
def user_may_create_room_alias(self, userid, room_alias):
"""Checks if a given user may create a room alias

View File

@ -1346,7 +1346,8 @@ class FederationHandler(BaseHandler):
raise SynapseError(403, "This server does not accept room invites")
if not self.spam_checker.user_may_invite(
event.sender, event.state_key, event.room_id, new_room=False,
event.sender, event.state_key, None,
room_id=event.room_id, new_room=False,
):
raise SynapseError(
403, "This user is not permitted to send invites to this server/user"

View File

@ -269,6 +269,7 @@ class RoomCreationHandler(BaseHandler):
if not is_requester_admin and not self.spam_checker.user_may_create_room(
user_id,
invite_list=[],
third_party_invite_list=[],
cloning=True,
):
raise SynapseError(403, "You are not permitted to create rooms")
@ -492,6 +493,7 @@ class RoomCreationHandler(BaseHandler):
yield self.auth.check_auth_blocking(user_id)
invite_list = config.get("invite", [])
invite_3pid_list = config.get("invite_3pid", [])
if (self._server_notices_mxid is not None and
requester.user.to_string() == self._server_notices_mxid):
@ -505,6 +507,7 @@ class RoomCreationHandler(BaseHandler):
if not is_requester_admin and not self.spam_checker.user_may_create_room(
user_id,
invite_list=invite_list,
third_party_invite_list=invite_3pid_list,
cloning=False,
):
raise SynapseError(403, "You are not permitted to create rooms")
@ -559,8 +562,6 @@ class RoomCreationHandler(BaseHandler):
requester,
)
invite_3pid_list = config.get("invite_3pid", [])
visibility = config.get("visibility", None)
is_public = visibility == "public"
@ -660,6 +661,7 @@ class RoomCreationHandler(BaseHandler):
id_server,
requester,
txn_id=None,
new_room=True,
)
result = {"room_id": room_id}

View File

@ -425,7 +425,9 @@ class RoomMemberHandler(object):
block_invite = True
if not self.spam_checker.user_may_invite(
requester.user.to_string(), target.to_string(), room_id,
requester.user.to_string(), target.to_string(),
third_party_invite=None,
room_id=room_id,
new_room=new_room,
):
logger.info("Blocking invite due to spam checker")
@ -728,7 +730,8 @@ class RoomMemberHandler(object):
address,
id_server,
requester,
txn_id
txn_id,
new_room=False,
):
if self.config.block_non_admin_invites:
is_requester_admin = yield self.auth.is_server_admin(
@ -744,6 +747,20 @@ class RoomMemberHandler(object):
id_server, medium, address
)
if not self.spam_checker.user_may_invite(
requester.user.to_string(), invitee,
third_party_invite={
"medium": medium,
"address": address,
},
room_id=room_id,
new_room=new_room,
):
logger.info("Blocking invite due to spam checker")
raise SynapseError(
403, "Invites have been disabled on this server",
)
if invitee:
yield self.update_membership(
requester,

View File

@ -666,7 +666,8 @@ class RoomMembershipRestServlet(ClientV1RestServlet):
content["address"],
content["id_server"],
requester,
txn_id
txn_id,
new_room=False,
)
defer.returnValue((200, {}))
return

View File

@ -46,6 +46,9 @@ class DomainRuleChecker(object):
# domain mapping rules above.
can_only_invite_during_room_creation: false
# Allow third party invites
can_invite_by_third_party_id: true
Don't forget to consider if you can invite users from your own domain.
"""
@ -62,19 +65,30 @@ class DomainRuleChecker(object):
self.can_only_invite_during_room_creation = config.get(
"can_only_invite_during_room_creation", False,
)
self.can_invite_by_third_party_id = config.get(
"can_invite_by_third_party_id", True,
)
def check_event_for_spam(self, event):
"""Implements synapse.events.SpamChecker.check_event_for_spam
"""
return False
def user_may_invite(self, inviter_userid, invitee_userid, room_id,
new_room):
def user_may_invite(self, inviter_userid, invitee_userid, third_party_invite,
room_id, new_room):
"""Implements synapse.events.SpamChecker.user_may_invite
"""
if self.can_only_invite_during_room_creation and not new_room:
return False
if not self.can_invite_by_third_party_id and third_party_invite:
return False
# This is a third party invite (without a bound mxid), so unless we have
# banned all third party invites (above) we allow it.
if not invitee_userid:
return True
inviter_domain = self._get_domain_from_id(inviter_userid)
invitee_domain = self._get_domain_from_id(invitee_userid)
@ -83,14 +97,20 @@ class DomainRuleChecker(object):
return invitee_domain in self.domain_mapping[inviter_domain]
def user_may_create_room(self, userid, invite_list, cloning):
def user_may_create_room(self, userid, invite_list, third_party_invite_list,
cloning):
"""Implements synapse.events.SpamChecker.user_may_create_room
"""
if cloning:
return True
if self.can_only_create_one_to_one_rooms and len(invite_list) != 1:
if not self.can_invite_by_third_party_id and third_party_invite_list:
return False
number_of_invites = len(invite_list) + len(third_party_invite_list)
if self.can_only_create_one_to_one_rooms and number_of_invites != 1:
return False
return True

View File

@ -35,13 +35,19 @@ class DomainRuleCheckerTestCase(unittest.TestCase):
}
check = DomainRuleChecker(config)
self.assertTrue(
check.user_may_invite("test:source_one", "test:target_one", "room", False)
check.user_may_invite(
"test:source_one", "test:target_one", None, "room", False
)
)
self.assertTrue(
check.user_may_invite("test:source_one", "test:target_two", "room", False)
check.user_may_invite(
"test:source_one", "test:target_two", None, "room", False
)
)
self.assertTrue(
check.user_may_invite("test:source_two", "test:target_two", "room", False)
check.user_may_invite(
"test:source_two", "test:target_two", None, "room", False
)
)
def test_disallowed(self):
@ -55,16 +61,24 @@ class DomainRuleCheckerTestCase(unittest.TestCase):
}
check = DomainRuleChecker(config)
self.assertFalse(
check.user_may_invite("test:source_one", "test:target_three", "room", False)
check.user_may_invite(
"test:source_one", "test:target_three", None, "room", False
)
)
self.assertFalse(
check.user_may_invite("test:source_two", "test:target_three", "room", False)
check.user_may_invite(
"test:source_two", "test:target_three", None, "room", False
)
)
self.assertFalse(
check.user_may_invite("test:source_two", "test:target_one", "room", False)
check.user_may_invite(
"test:source_two", "test:target_one", None, "room", False
)
)
self.assertFalse(
check.user_may_invite("test:source_four", "test:target_one", "room", False)
check.user_may_invite(
"test:source_four", "test:target_one", None, "room", False
)
)
def test_default_allow(self):
@ -77,7 +91,9 @@ class DomainRuleCheckerTestCase(unittest.TestCase):
}
check = DomainRuleChecker(config)
self.assertTrue(
check.user_may_invite("test:source_three", "test:target_one", "room", False)
check.user_may_invite(
"test:source_three", "test:target_one", None, "room", False
)
)
def test_default_deny(self):
@ -90,7 +106,9 @@ class DomainRuleCheckerTestCase(unittest.TestCase):
}
check = DomainRuleChecker(config)
self.assertFalse(
check.user_may_invite("test:source_three", "test:target_one", "room", False)
check.user_may_invite(
"test:source_three", "test:target_one", None, "room", False
)
)
def test_config_parse(self):
@ -125,13 +143,17 @@ class DomainRuleCheckerRoomTestCase(unittest.HomeserverTestCase):
def make_homeserver(self, reactor, clock):
config = self.default_config()
config.spam_checker = (DomainRuleChecker, {
"default": True,
"domain_mapping": {},
"can_only_join_rooms_with_invite": True,
"can_only_create_one_to_one_rooms": True,
"can_only_invite_during_room_creation": True,
})
config.spam_checker = (
DomainRuleChecker,
{
"default": True,
"domain_mapping": {},
"can_only_join_rooms_with_invite": True,
"can_only_create_one_to_one_rooms": True,
"can_only_invite_during_room_creation": True,
"can_invite_by_third_party_id": False,
},
)
hs = self.setup_test_homeserver(config=config)
return hs
@ -154,15 +176,36 @@ class DomainRuleCheckerRoomTestCase(unittest.HomeserverTestCase):
assert channel.result["code"] == b"403", channel.result
def test_normal_user_cannot_create_room_with_multiple_invites(self):
channel = self._create_room(self.normal_access_token, content={
"invite": [self.other_user_id, self.admin_user_id],
})
channel = self._create_room(
self.normal_access_token,
content={"invite": [self.other_user_id, self.admin_user_id]},
)
assert channel.result["code"] == b"403", channel.result
# Test that it correctly counts both normal and third party invites
channel = self._create_room(
self.normal_access_token,
content={
"invite": [self.other_user_id],
"invite_3pid": [{"medium": "email", "address": "foo@example.com"}],
},
)
assert channel.result["code"] == b"403", channel.result
# Test that it correctly rejects third party invites
channel = self._create_room(
self.normal_access_token,
content={
"invite": [],
"invite_3pid": [{"medium": "email", "address": "foo@example.com"}],
},
)
assert channel.result["code"] == b"403", channel.result
def test_normal_user_can_room_with_single_invites(self):
channel = self._create_room(self.normal_access_token, content={
"invite": [self.other_user_id],
})
channel = self._create_room(
self.normal_access_token, content={"invite": [self.other_user_id]}
)
assert channel.result["code"] == b"200", channel.result
def test_cannot_join_public_room(self):
@ -172,9 +215,7 @@ class DomainRuleCheckerRoomTestCase(unittest.HomeserverTestCase):
room_id = channel.json_body["room_id"]
self.helper.join(
room_id, self.normal_user_id,
tok=self.normal_access_token,
expect_code=403,
room_id, self.normal_user_id, tok=self.normal_access_token, expect_code=403
)
def test_can_join_invited_room(self):
@ -191,9 +232,7 @@ class DomainRuleCheckerRoomTestCase(unittest.HomeserverTestCase):
)
self.helper.join(
room_id, self.normal_user_id,
tok=self.normal_access_token,
expect_code=200,
room_id, self.normal_user_id, tok=self.normal_access_token, expect_code=200
)
def test_cannot_invite(self):
@ -209,6 +248,33 @@ class DomainRuleCheckerRoomTestCase(unittest.HomeserverTestCase):
tok=self.admin_access_token,
)
self.helper.join(
room_id, self.normal_user_id, tok=self.normal_access_token, expect_code=200
)
self.helper.invite(
room_id,
src=self.normal_user_id,
targ=self.other_user_id,
tok=self.normal_access_token,
expect_code=403,
)
def test_cannot_3pid_invite(self):
"""Test that unbound 3pid invites get rejected.
"""
channel = self._create_room(self.admin_access_token)
assert channel.result["code"] == b"200", channel.result
room_id = channel.json_body["room_id"]
self.helper.invite(
room_id,
src=self.admin_user_id,
targ=self.normal_user_id,
tok=self.admin_access_token,
)
self.helper.join(
room_id, self.normal_user_id,
tok=self.normal_access_token,
@ -223,13 +289,26 @@ class DomainRuleCheckerRoomTestCase(unittest.HomeserverTestCase):
expect_code=403,
)
def _create_room(self, token, content={}):
path = "/_matrix/client/r0/createRoom?access_token=%s" % (
token,
request, channel = self.make_request(
"POST",
"rooms/%s/invite" % (room_id),
{
"address": "foo@bar.com",
"medium": "email",
"id_server": "localhost"
},
access_token=self.normal_access_token,
)
self.render(request)
self.assertEqual(channel.code, 403, channel.result["body"])
def _create_room(self, token, content={}):
path = "/_matrix/client/r0/createRoom?access_token=%s" % (token,)
request, channel = make_request(
self.hs.get_reactor(), "POST", path,
self.hs.get_reactor(),
"POST",
path,
content=json.dumps(content).encode("utf8"),
)
render(request, self.resource, self.hs.get_reactor())