Merge pull request #4910 from matrix-org/erikj/third_party_invite_create_spam
Add third party invite support to spam checkerpull/4997/head dinsic_2019-03-21
commit
53dd358c83
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -666,7 +666,8 @@ class RoomMembershipRestServlet(ClientV1RestServlet):
|
|||
content["address"],
|
||||
content["id_server"],
|
||||
requester,
|
||||
txn_id
|
||||
txn_id,
|
||||
new_room=False,
|
||||
)
|
||||
defer.returnValue((200, {}))
|
||||
return
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
Loading…
Reference in New Issue