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())
|