mirror of
https://github.com/element-hq/synapse.git
synced 2024-12-19 09:31:35 +03:00
Merge pull request #4875 from matrix-org/erikj/spam_checker
Extend spam checking rules
This commit is contained in:
commit
8eb9f37a01
6 changed files with 326 additions and 43 deletions
|
@ -46,13 +46,18 @@ class SpamChecker(object):
|
|||
|
||||
return self.spam_checker.check_event_for_spam(event)
|
||||
|
||||
def user_may_invite(self, inviter_userid, invitee_userid, room_id):
|
||||
def user_may_invite(self, inviter_userid, invitee_userid, room_id, new_room):
|
||||
"""Checks if a given user may send an invite
|
||||
|
||||
If this method returns false, the invite will be rejected.
|
||||
|
||||
Args:
|
||||
userid (string): The sender's user ID
|
||||
inviter_userid (str)
|
||||
invitee_userid (str)
|
||||
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
|
||||
included in the call to `user_may_create_room`.
|
||||
|
||||
Returns:
|
||||
bool: True if the user may send an invite, otherwise False
|
||||
|
@ -60,15 +65,21 @@ class SpamChecker(object):
|
|||
if self.spam_checker is None:
|
||||
return True
|
||||
|
||||
return self.spam_checker.user_may_invite(inviter_userid, invitee_userid, room_id)
|
||||
return self.spam_checker.user_may_invite(
|
||||
inviter_userid, invitee_userid, room_id, new_room,
|
||||
)
|
||||
|
||||
def user_may_create_room(self, userid):
|
||||
def user_may_create_room(self, userid, invite_list, cloning):
|
||||
"""Checks if a given user may create a room
|
||||
|
||||
If this method returns false, the creation request will be rejected.
|
||||
|
||||
Args:
|
||||
userid (string): The sender's user ID
|
||||
invite_list (list[str]): List of user IDs that would be invited to
|
||||
the new room.
|
||||
cloning (bool): Whether the user is cloning an existing room, e.g.
|
||||
upgrading a room.
|
||||
|
||||
Returns:
|
||||
bool: True if the user may create a room, otherwise False
|
||||
|
@ -76,7 +87,7 @@ class SpamChecker(object):
|
|||
if self.spam_checker is None:
|
||||
return True
|
||||
|
||||
return self.spam_checker.user_may_create_room(userid)
|
||||
return self.spam_checker.user_may_create_room(userid, invite_list, cloning)
|
||||
|
||||
def user_may_create_room_alias(self, userid, room_alias):
|
||||
"""Checks if a given user may create a room alias
|
||||
|
@ -111,3 +122,21 @@ class SpamChecker(object):
|
|||
return True
|
||||
|
||||
return self.spam_checker.user_may_publish_room(userid, room_id)
|
||||
|
||||
def user_may_join_room(self, userid, room_id, is_invited):
|
||||
"""Checks if a given users is allowed to join a room.
|
||||
|
||||
Is not called when the user creates a room.
|
||||
|
||||
Args:
|
||||
userid (str)
|
||||
room_id (str)
|
||||
is_invited (bool): Whether the user is invited into the room
|
||||
|
||||
Returns:
|
||||
bool: Whether the user may join the room
|
||||
"""
|
||||
if self.spam_checker is None:
|
||||
return True
|
||||
|
||||
return self.spam_checker.user_may_join_room(userid, room_id, is_invited)
|
||||
|
|
|
@ -1299,7 +1299,7 @@ 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,
|
||||
event.sender, event.state_key, event.room_id, new_room=False,
|
||||
):
|
||||
raise SynapseError(
|
||||
403, "This user is not permitted to send invites to this server/user"
|
||||
|
|
|
@ -81,6 +81,8 @@ class RoomCreationHandler(BaseHandler):
|
|||
# linearizer to stop two upgrades happening at once
|
||||
self._upgrade_linearizer = Linearizer("room_upgrade_linearizer")
|
||||
|
||||
self._server_notices_mxid = hs.config.server_notices_mxid
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def upgrade_room(self, requester, old_room_id, new_version):
|
||||
"""Replace a room with a new room with a different version
|
||||
|
@ -254,7 +256,21 @@ class RoomCreationHandler(BaseHandler):
|
|||
"""
|
||||
user_id = requester.user.to_string()
|
||||
|
||||
if not self.spam_checker.user_may_create_room(user_id):
|
||||
if (self._server_notices_mxid is not None and
|
||||
requester.user.to_string() == self._server_notices_mxid):
|
||||
# allow the server notices mxid to create rooms
|
||||
is_requester_admin = True
|
||||
|
||||
else:
|
||||
is_requester_admin = yield self.auth.is_server_admin(
|
||||
requester.user,
|
||||
)
|
||||
|
||||
if not is_requester_admin and not self.spam_checker.user_may_create_room(
|
||||
user_id,
|
||||
invite_list=[],
|
||||
cloning=True,
|
||||
):
|
||||
raise SynapseError(403, "You are not permitted to create rooms")
|
||||
|
||||
creation_content = {
|
||||
|
@ -475,7 +491,22 @@ class RoomCreationHandler(BaseHandler):
|
|||
|
||||
yield self.auth.check_auth_blocking(user_id)
|
||||
|
||||
if not self.spam_checker.user_may_create_room(user_id):
|
||||
invite_list = config.get("invite", [])
|
||||
|
||||
if (self._server_notices_mxid is not None and
|
||||
requester.user.to_string() == self._server_notices_mxid):
|
||||
# allow the server notices mxid to create rooms
|
||||
is_requester_admin = True
|
||||
else:
|
||||
is_requester_admin = yield self.auth.is_server_admin(
|
||||
requester.user,
|
||||
)
|
||||
|
||||
if not is_requester_admin and not self.spam_checker.user_may_create_room(
|
||||
user_id,
|
||||
invite_list=invite_list,
|
||||
cloning=False,
|
||||
):
|
||||
raise SynapseError(403, "You are not permitted to create rooms")
|
||||
|
||||
if ratelimit:
|
||||
|
@ -518,7 +549,6 @@ class RoomCreationHandler(BaseHandler):
|
|||
else:
|
||||
room_alias = None
|
||||
|
||||
invite_list = config.get("invite", [])
|
||||
for i in invite_list:
|
||||
try:
|
||||
UserID.from_string(i)
|
||||
|
@ -615,6 +645,7 @@ class RoomCreationHandler(BaseHandler):
|
|||
"invite",
|
||||
ratelimit=False,
|
||||
content=content,
|
||||
new_room=True,
|
||||
)
|
||||
|
||||
for invite_3pid in invite_3pid_list:
|
||||
|
@ -699,6 +730,7 @@ class RoomCreationHandler(BaseHandler):
|
|||
"join",
|
||||
ratelimit=False,
|
||||
content=creator_join_profile,
|
||||
new_room=True,
|
||||
)
|
||||
|
||||
# We treat the power levels override specially as this needs to be one
|
||||
|
|
|
@ -301,7 +301,30 @@ class RoomMemberHandler(object):
|
|||
third_party_signed=None,
|
||||
ratelimit=True,
|
||||
content=None,
|
||||
new_room=False,
|
||||
):
|
||||
"""Update a users membership in a room
|
||||
|
||||
Args:
|
||||
requester (Requester)
|
||||
target (UserID)
|
||||
room_id (str)
|
||||
action (str): The "action" the requester is performing against the
|
||||
target. One of join/leave/kick/ban/invite/unban.
|
||||
txn_id (str|None): The transaction ID associated with the request,
|
||||
or None not provided.
|
||||
remote_room_hosts (list[str]|None): List of remote servers to try
|
||||
and join via if server isn't already in the room.
|
||||
third_party_signed (dict|None): The signed object for third party
|
||||
invites.
|
||||
ratelimit (bool): Whether to apply ratelimiting to this request.
|
||||
content (dict|None): Fields to include in the new events content.
|
||||
new_room (bool): Whether these membership changes are happening
|
||||
as part of a room creation (e.g. initial joins and invites)
|
||||
|
||||
Returns:
|
||||
Deferred[FrozenEvent]
|
||||
"""
|
||||
key = (room_id,)
|
||||
|
||||
with (yield self.member_linearizer.queue(key)):
|
||||
|
@ -315,6 +338,7 @@ class RoomMemberHandler(object):
|
|||
third_party_signed=third_party_signed,
|
||||
ratelimit=ratelimit,
|
||||
content=content,
|
||||
new_room=new_room,
|
||||
)
|
||||
|
||||
defer.returnValue(result)
|
||||
|
@ -331,6 +355,7 @@ class RoomMemberHandler(object):
|
|||
third_party_signed=None,
|
||||
ratelimit=True,
|
||||
content=None,
|
||||
new_room=False,
|
||||
):
|
||||
content_specified = bool(content)
|
||||
if content is None:
|
||||
|
@ -392,6 +417,7 @@ class RoomMemberHandler(object):
|
|||
|
||||
if not self.spam_checker.user_may_invite(
|
||||
requester.user.to_string(), target.to_string(), room_id,
|
||||
new_room=new_room,
|
||||
):
|
||||
logger.info("Blocking invite due to spam checker")
|
||||
block_invite = True
|
||||
|
@ -461,8 +487,29 @@ class RoomMemberHandler(object):
|
|||
# so don't really fit into the general auth process.
|
||||
raise AuthError(403, "Guest access not allowed")
|
||||
|
||||
if (self._server_notices_mxid is not None and
|
||||
requester.user.to_string() == self._server_notices_mxid):
|
||||
# allow the server notices mxid to join rooms
|
||||
is_requester_admin = True
|
||||
|
||||
else:
|
||||
is_requester_admin = yield self.auth.is_server_admin(
|
||||
requester.user,
|
||||
)
|
||||
|
||||
inviter = yield self._get_inviter(target.to_string(), room_id)
|
||||
if not is_requester_admin:
|
||||
# We assume that if the spam checker allowed the user to create
|
||||
# a room then they're allowed to join it.
|
||||
if not new_room and not self.spam_checker.user_may_join_room(
|
||||
target.to_string(), room_id,
|
||||
is_invited=inviter is not None,
|
||||
):
|
||||
raise SynapseError(
|
||||
403, "Not allowed to join this room",
|
||||
)
|
||||
|
||||
if not is_host_in_room:
|
||||
inviter = yield self._get_inviter(target.to_string(), room_id)
|
||||
if inviter and not self.hs.is_mine(inviter):
|
||||
remote_room_hosts.append(inviter.domain)
|
||||
|
||||
|
|
|
@ -34,7 +34,17 @@ class DomainRuleChecker(object):
|
|||
"inviter_domain": [ "invitee_domain_permitted", "other_domain_permitted" ]
|
||||
"other_inviter_domain": [ "invitee_domain_permitted" ]
|
||||
default: False
|
||||
}
|
||||
|
||||
# Only let local users join rooms if they were explicitly invited.
|
||||
can_only_join_rooms_with_invite: false
|
||||
|
||||
# Only let local users create rooms if they are inviting only one
|
||||
# other user, and that user matches the rules above.
|
||||
can_only_create_one_to_one_rooms: false
|
||||
|
||||
# Only let local users invite during room creation, regardless of the
|
||||
# domain mapping rules above.
|
||||
can_only_invite_during_room_creation: false
|
||||
|
||||
Don't forget to consider if you can invite users from your own domain.
|
||||
"""
|
||||
|
@ -43,14 +53,28 @@ class DomainRuleChecker(object):
|
|||
self.domain_mapping = config["domain_mapping"] or {}
|
||||
self.default = config["default"]
|
||||
|
||||
self.can_only_join_rooms_with_invite = config.get(
|
||||
"can_only_join_rooms_with_invite", False,
|
||||
)
|
||||
self.can_only_create_one_to_one_rooms = config.get(
|
||||
"can_only_create_one_to_one_rooms", False,
|
||||
)
|
||||
self.can_only_invite_during_room_creation = config.get(
|
||||
"can_only_invite_during_room_creation", False,
|
||||
)
|
||||
|
||||
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):
|
||||
def user_may_invite(self, inviter_userid, invitee_userid, 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
|
||||
|
||||
inviter_domain = self._get_domain_from_id(inviter_userid)
|
||||
invitee_domain = self._get_domain_from_id(invitee_userid)
|
||||
|
||||
|
@ -59,9 +83,16 @@ class DomainRuleChecker(object):
|
|||
|
||||
return invitee_domain in self.domain_mapping[inviter_domain]
|
||||
|
||||
def user_may_create_room(self, userid):
|
||||
def user_may_create_room(self, userid, 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:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def user_may_create_room_alias(self, userid, room_alias):
|
||||
|
@ -74,6 +105,14 @@ class DomainRuleChecker(object):
|
|||
"""
|
||||
return True
|
||||
|
||||
def user_may_join_room(self, userid, room_id, is_invited):
|
||||
"""Implements synapse.events.SpamChecker.user_may_join_room
|
||||
"""
|
||||
if self.can_only_join_rooms_with_invite and not is_invited:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def parse_config(config):
|
||||
"""Implements synapse.events.SpamChecker.parse_config
|
||||
|
|
|
@ -14,29 +14,35 @@
|
|||
# limitations under the License.
|
||||
|
||||
|
||||
import json
|
||||
|
||||
from synapse.config._base import ConfigError
|
||||
from synapse.rest.client.v1 import admin, login, room
|
||||
from synapse.rulecheck.domain_rule_checker import DomainRuleChecker
|
||||
|
||||
from tests import unittest
|
||||
from tests.server import make_request, render
|
||||
|
||||
|
||||
class DomainRuleCheckerTestCase(unittest.TestCase):
|
||||
|
||||
def test_allowed(self):
|
||||
config = {
|
||||
"default": False,
|
||||
"domain_mapping": {
|
||||
"source_one": ["target_one", "target_two"],
|
||||
"source_two": ["target_two"]
|
||||
}
|
||||
"source_two": ["target_two"],
|
||||
},
|
||||
}
|
||||
check = DomainRuleChecker(config)
|
||||
self.assertTrue(check.user_may_invite("test:source_one",
|
||||
"test:target_one", "room"))
|
||||
self.assertTrue(check.user_may_invite("test:source_one",
|
||||
"test:target_two", "room"))
|
||||
self.assertTrue(check.user_may_invite("test:source_two",
|
||||
"test:target_two", "room"))
|
||||
self.assertTrue(
|
||||
check.user_may_invite("test:source_one", "test:target_one", "room", False)
|
||||
)
|
||||
self.assertTrue(
|
||||
check.user_may_invite("test:source_one", "test:target_two", "room", False)
|
||||
)
|
||||
self.assertTrue(
|
||||
check.user_may_invite("test:source_two", "test:target_two", "room", False)
|
||||
)
|
||||
|
||||
def test_disallowed(self):
|
||||
config = {
|
||||
|
@ -44,50 +50,56 @@ class DomainRuleCheckerTestCase(unittest.TestCase):
|
|||
"domain_mapping": {
|
||||
"source_one": ["target_one", "target_two"],
|
||||
"source_two": ["target_two"],
|
||||
"source_four": []
|
||||
}
|
||||
"source_four": [],
|
||||
},
|
||||
}
|
||||
check = DomainRuleChecker(config)
|
||||
self.assertFalse(check.user_may_invite("test:source_one",
|
||||
"test:target_three", "room"))
|
||||
self.assertFalse(check.user_may_invite("test:source_two",
|
||||
"test:target_three", "room"))
|
||||
self.assertFalse(check.user_may_invite("test:source_two",
|
||||
"test:target_one", "room"))
|
||||
self.assertFalse(check.user_may_invite("test:source_four",
|
||||
"test:target_one", "room"))
|
||||
self.assertFalse(
|
||||
check.user_may_invite("test:source_one", "test:target_three", "room", False)
|
||||
)
|
||||
self.assertFalse(
|
||||
check.user_may_invite("test:source_two", "test:target_three", "room", False)
|
||||
)
|
||||
self.assertFalse(
|
||||
check.user_may_invite("test:source_two", "test:target_one", "room", False)
|
||||
)
|
||||
self.assertFalse(
|
||||
check.user_may_invite("test:source_four", "test:target_one", "room", False)
|
||||
)
|
||||
|
||||
def test_default_allow(self):
|
||||
config = {
|
||||
"default": True,
|
||||
"domain_mapping": {
|
||||
"source_one": ["target_one", "target_two"],
|
||||
"source_two": ["target_two"]
|
||||
}
|
||||
"source_two": ["target_two"],
|
||||
},
|
||||
}
|
||||
check = DomainRuleChecker(config)
|
||||
self.assertTrue(check.user_may_invite("test:source_three",
|
||||
"test:target_one", "room"))
|
||||
self.assertTrue(
|
||||
check.user_may_invite("test:source_three", "test:target_one", "room", False)
|
||||
)
|
||||
|
||||
def test_default_deny(self):
|
||||
config = {
|
||||
"default": False,
|
||||
"domain_mapping": {
|
||||
"source_one": ["target_one", "target_two"],
|
||||
"source_two": ["target_two"]
|
||||
}
|
||||
"source_two": ["target_two"],
|
||||
},
|
||||
}
|
||||
check = DomainRuleChecker(config)
|
||||
self.assertFalse(check.user_may_invite("test:source_three",
|
||||
"test:target_one", "room"))
|
||||
self.assertFalse(
|
||||
check.user_may_invite("test:source_three", "test:target_one", "room", False)
|
||||
)
|
||||
|
||||
def test_config_parse(self):
|
||||
config = {
|
||||
"default": False,
|
||||
"domain_mapping": {
|
||||
"source_one": ["target_one", "target_two"],
|
||||
"source_two": ["target_two"]
|
||||
}
|
||||
"source_two": ["target_two"],
|
||||
},
|
||||
}
|
||||
self.assertEquals(config, DomainRuleChecker.parse_config(config))
|
||||
|
||||
|
@ -95,7 +107,131 @@ class DomainRuleCheckerTestCase(unittest.TestCase):
|
|||
config = {
|
||||
"domain_mapping": {
|
||||
"source_one": ["target_one", "target_two"],
|
||||
"source_two": ["target_two"]
|
||||
"source_two": ["target_two"],
|
||||
}
|
||||
}
|
||||
self.assertRaises(ConfigError, DomainRuleChecker.parse_config, config)
|
||||
|
||||
|
||||
class DomainRuleCheckerRoomTestCase(unittest.HomeserverTestCase):
|
||||
servlets = [
|
||||
admin.register_servlets,
|
||||
room.register_servlets,
|
||||
login.register_servlets,
|
||||
]
|
||||
|
||||
hijack_auth = False
|
||||
|
||||
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,
|
||||
})
|
||||
|
||||
hs = self.setup_test_homeserver(config=config)
|
||||
return hs
|
||||
|
||||
def prepare(self, reactor, clock, hs):
|
||||
self.admin_user_id = self.register_user("admin_user", "pass", admin=True)
|
||||
self.admin_access_token = self.login("admin_user", "pass")
|
||||
|
||||
self.normal_user_id = self.register_user("normal_user", "pass", admin=False)
|
||||
self.normal_access_token = self.login("normal_user", "pass")
|
||||
|
||||
self.other_user_id = self.register_user("other_user", "pass", admin=False)
|
||||
|
||||
def test_admin_can_create_room(self):
|
||||
channel = self._create_room(self.admin_access_token)
|
||||
assert channel.result["code"] == b"200", channel.result
|
||||
|
||||
def test_normal_user_cannot_create_empty_room(self):
|
||||
channel = self._create_room(self.normal_access_token)
|
||||
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],
|
||||
})
|
||||
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],
|
||||
})
|
||||
assert channel.result["code"] == b"200", channel.result
|
||||
|
||||
def test_cannot_join_public_room(self):
|
||||
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.join(
|
||||
room_id, self.normal_user_id,
|
||||
tok=self.normal_access_token,
|
||||
expect_code=403,
|
||||
)
|
||||
|
||||
def test_can_join_invited_room(self):
|
||||
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,
|
||||
)
|
||||
|
||||
def test_cannot_invite(self):
|
||||
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,
|
||||
)
|
||||
|
||||
self.helper.invite(
|
||||
room_id,
|
||||
src=self.normal_user_id,
|
||||
targ=self.other_user_id,
|
||||
tok=self.normal_access_token,
|
||||
expect_code=403,
|
||||
)
|
||||
|
||||
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,
|
||||
content=json.dumps(content).encode("utf8"),
|
||||
)
|
||||
render(request, self.resource, self.hs.get_reactor())
|
||||
|
||||
return channel
|
||||
|
|
Loading…
Reference in a new issue