summary refs log tree commit diff
diff options
context:
space:
mode:
authorErik Johnston <erikj@jki.re>2019-03-21 16:07:27 +0000
committerGitHub <noreply@github.com>2019-03-21 16:07:27 +0000
commit53dd358c8382198f8ecaf9c25d06b16d3131cdc6 (patch)
tree58fb1cfeb27d92d863dce4e90bf7b1959eb0a764
parentTurn off newsfile check (diff)
parentFix comments (diff)
downloadsynapse-53dd358c8382198f8ecaf9c25d06b16d3131cdc6.tar.xz
Merge pull request #4910 from matrix-org/erikj/third_party_invite_create_spam dinsic_2019-03-21
Add third party invite support to spam checker
-rw-r--r--docs/sample_config.yaml52
-rw-r--r--synapse/config/user_directory.py5
-rw-r--r--synapse/events/spamcheck.py20
-rw-r--r--synapse/handlers/federation.py3
-rw-r--r--synapse/handlers/room.py6
-rw-r--r--synapse/handlers/room_member.py21
-rw-r--r--synapse/rest/client/v1/room.py3
-rw-r--r--synapse/rulecheck/domain_rule_checker.py28
-rw-r--r--tests/rulecheck/test_domainrulecheck.py143
9 files changed, 231 insertions, 50 deletions
diff --git a/docs/sample_config.yaml b/docs/sample_config.yaml

index 4ada0fba0e..73d02a63fc 100644 --- a/docs/sample_config.yaml +++ b/docs/sample_config.yaml
@@ -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 diff --git a/synapse/config/user_directory.py b/synapse/config/user_directory.py
index 7abf40f37d..56eff15ef8 100644 --- a/synapse/config/user_directory.py +++ b/synapse/config/user_directory.py
@@ -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. diff --git a/synapse/events/spamcheck.py b/synapse/events/spamcheck.py
index e4fc988cfc..92aaa05e79 100644 --- a/synapse/events/spamcheck.py +++ b/synapse/events/spamcheck.py
@@ -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 diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py
index 1f799a04c1..c106fab0dc 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py
@@ -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" diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py
index 581cff9526..6f5666e624 100644 --- a/synapse/handlers/room.py +++ b/synapse/handlers/room.py
@@ -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} diff --git a/synapse/handlers/room_member.py b/synapse/handlers/room_member.py
index 645f615d74..ee7a390b1c 100644 --- a/synapse/handlers/room_member.py +++ b/synapse/handlers/room_member.py
@@ -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, diff --git a/synapse/rest/client/v1/room.py b/synapse/rest/client/v1/room.py
index 48da4d557f..17a1503cdb 100644 --- a/synapse/rest/client/v1/room.py +++ b/synapse/rest/client/v1/room.py
@@ -666,7 +666,8 @@ class RoomMembershipRestServlet(ClientV1RestServlet): content["address"], content["id_server"], requester, - txn_id + txn_id, + new_room=False, ) defer.returnValue((200, {})) return diff --git a/synapse/rulecheck/domain_rule_checker.py b/synapse/rulecheck/domain_rule_checker.py
index 410757041b..bcec465ef4 100644 --- a/synapse/rulecheck/domain_rule_checker.py +++ b/synapse/rulecheck/domain_rule_checker.py
@@ -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 diff --git a/tests/rulecheck/test_domainrulecheck.py b/tests/rulecheck/test_domainrulecheck.py
index de89f95e3c..803d680cec 100644 --- a/tests/rulecheck/test_domainrulecheck.py +++ b/tests/rulecheck/test_domainrulecheck.py
@@ -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): @@ -210,6 +249,33 @@ class DomainRuleCheckerRoomTestCase(unittest.HomeserverTestCase): ) 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, expect_code=200, @@ -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())