Stop shadow-banned users from sending invites. (#8095)

This commit is contained in:
Patrick Cloke 2020-08-20 15:07:42 -04:00 committed by GitHub
parent 318f4e738e
commit e259d63f73
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 226 additions and 31 deletions

1
changelog.d/8095.feature Normal file
View File

@ -0,0 +1 @@
Add support for shadow-banning users (ignoring any message send requests).

View File

@ -604,3 +604,11 @@ class HttpResponseException(CodeMessageException):
errmsg = j.pop("error", self.msg) errmsg = j.pop("error", self.msg)
return ProxiedRequestError(self.code, errmsg, errcode, j) return ProxiedRequestError(self.code, errmsg, errcode, j)
class ShadowBanError(Exception):
"""
Raised when a shadow-banned user attempts to perform an action.
This should be caught and a proper "fake" success response sent to the user.
"""

View File

@ -20,6 +20,7 @@
import itertools import itertools
import logging import logging
import math import math
import random
import string import string
from collections import OrderedDict from collections import OrderedDict
from typing import TYPE_CHECKING, Any, Awaitable, Dict, List, Optional, Tuple from typing import TYPE_CHECKING, Any, Awaitable, Dict, List, Optional, Tuple
@ -626,6 +627,7 @@ class RoomCreationHandler(BaseHandler):
if mapping: if mapping:
raise SynapseError(400, "Room alias already taken", Codes.ROOM_IN_USE) raise SynapseError(400, "Room alias already taken", Codes.ROOM_IN_USE)
invite_3pid_list = config.get("invite_3pid", [])
invite_list = config.get("invite", []) invite_list = config.get("invite", [])
for i in invite_list: for i in invite_list:
try: try:
@ -634,6 +636,14 @@ class RoomCreationHandler(BaseHandler):
except Exception: except Exception:
raise SynapseError(400, "Invalid user_id: %s" % (i,)) raise SynapseError(400, "Invalid user_id: %s" % (i,))
if (invite_list or invite_3pid_list) and requester.shadow_banned:
# We randomly sleep a bit just to annoy the requester.
await self.clock.sleep(random.randint(1, 10))
# Allow the request to go through, but remove any associated invites.
invite_3pid_list = []
invite_list = []
await self.event_creation_handler.assert_accepted_privacy_policy(requester) await self.event_creation_handler.assert_accepted_privacy_policy(requester)
power_level_content_override = config.get("power_level_content_override") power_level_content_override = config.get("power_level_content_override")
@ -648,8 +658,6 @@ class RoomCreationHandler(BaseHandler):
% (user_id,), % (user_id,),
) )
invite_3pid_list = config.get("invite_3pid", [])
visibility = config.get("visibility", None) visibility = config.get("visibility", None)
is_public = visibility == "public" is_public = visibility == "public"
@ -744,6 +752,8 @@ class RoomCreationHandler(BaseHandler):
if is_direct: if is_direct:
content["is_direct"] = is_direct content["is_direct"] = is_direct
# Note that update_membership with an action of "invite" can raise a
# ShadowBanError, but this was handled above by emptying invite_list.
_, last_stream_id = await self.room_member_handler.update_membership( _, last_stream_id = await self.room_member_handler.update_membership(
requester, requester,
UserID.from_string(invitee), UserID.from_string(invitee),
@ -758,6 +768,8 @@ class RoomCreationHandler(BaseHandler):
id_access_token = invite_3pid.get("id_access_token") # optional id_access_token = invite_3pid.get("id_access_token") # optional
address = invite_3pid["address"] address = invite_3pid["address"]
medium = invite_3pid["medium"] medium = invite_3pid["medium"]
# Note that do_3pid_invite can raise a ShadowBanError, but this was
# handled above by emptying invite_3pid_list.
last_stream_id = await self.hs.get_room_member_handler().do_3pid_invite( last_stream_id = await self.hs.get_room_member_handler().do_3pid_invite(
room_id, room_id,
requester.user, requester.user,

View File

@ -15,6 +15,7 @@
import abc import abc
import logging import logging
import random
from http import HTTPStatus from http import HTTPStatus
from typing import TYPE_CHECKING, Dict, Iterable, List, Optional, Tuple, Union from typing import TYPE_CHECKING, Dict, Iterable, List, Optional, Tuple, Union
@ -22,7 +23,13 @@ from unpaddedbase64 import encode_base64
from synapse import types from synapse import types
from synapse.api.constants import MAX_DEPTH, EventTypes, Membership from synapse.api.constants import MAX_DEPTH, EventTypes, Membership
from synapse.api.errors import AuthError, Codes, LimitExceededError, SynapseError from synapse.api.errors import (
AuthError,
Codes,
LimitExceededError,
ShadowBanError,
SynapseError,
)
from synapse.api.ratelimiting import Ratelimiter from synapse.api.ratelimiting import Ratelimiter
from synapse.api.room_versions import EventFormatVersions from synapse.api.room_versions import EventFormatVersions
from synapse.crypto.event_signing import compute_event_reference_hash from synapse.crypto.event_signing import compute_event_reference_hash
@ -285,6 +292,31 @@ class RoomMemberHandler(object):
content: Optional[dict] = None, content: Optional[dict] = None,
require_consent: bool = True, require_consent: bool = True,
) -> Tuple[str, int]: ) -> Tuple[str, int]:
"""Update a user's membership in a room.
Params:
requester: The user who is performing the update.
target: The user whose membership is being updated.
room_id: The room ID whose membership is being updated.
action: The membership change, see synapse.api.constants.Membership.
txn_id: The transaction ID, if given.
remote_room_hosts: Remote servers to send the update to.
third_party_signed: Information from a 3PID invite.
ratelimit: Whether to rate limit the request.
content: The content of the created event.
require_consent: Whether consent is required.
Returns:
A tuple of the new event ID and stream ID.
Raises:
ShadowBanError if a shadow-banned requester attempts to send an invite.
"""
if action == Membership.INVITE and requester.shadow_banned:
# We randomly sleep a bit just to annoy the requester.
await self.clock.sleep(random.randint(1, 10))
raise ShadowBanError()
key = (room_id,) key = (room_id,)
with (await self.member_linearizer.queue(key)): with (await self.member_linearizer.queue(key)):
@ -773,6 +805,25 @@ class RoomMemberHandler(object):
txn_id: Optional[str], txn_id: Optional[str],
id_access_token: Optional[str] = None, id_access_token: Optional[str] = None,
) -> int: ) -> int:
"""Invite a 3PID to a room.
Args:
room_id: The room to invite the 3PID to.
inviter: The user sending the invite.
medium: The 3PID's medium.
address: The 3PID's address.
id_server: The identity server to use.
requester: The user making the request.
txn_id: The transaction ID this is part of, or None if this is not
part of a transaction.
id_access_token: The optional identity server access token.
Returns:
The new stream ID.
Raises:
ShadowBanError if the requester has been shadow-banned.
"""
if self.config.block_non_admin_invites: if self.config.block_non_admin_invites:
is_requester_admin = await self.auth.is_server_admin(requester.user) is_requester_admin = await self.auth.is_server_admin(requester.user)
if not is_requester_admin: if not is_requester_admin:
@ -780,6 +831,11 @@ class RoomMemberHandler(object):
403, "Invites have been disabled on this server", Codes.FORBIDDEN 403, "Invites have been disabled on this server", Codes.FORBIDDEN
) )
if requester.shadow_banned:
# We randomly sleep a bit just to annoy the requester.
await self.clock.sleep(random.randint(1, 10))
raise ShadowBanError()
# We need to rate limit *before* we send out any 3PID invites, so we # We need to rate limit *before* we send out any 3PID invites, so we
# can't just rely on the standard ratelimiting of events. # can't just rely on the standard ratelimiting of events.
await self.base_handler.ratelimit(requester) await self.base_handler.ratelimit(requester)
@ -804,6 +860,8 @@ class RoomMemberHandler(object):
) )
if invitee: if invitee:
# Note that update_membership with an action of "invite" can raise
# a ShadowBanError, but this was done above already.
_, stream_id = await self.update_membership( _, stream_id = await self.update_membership(
requester, UserID.from_string(invitee), room_id, "invite", txn_id=txn_id requester, UserID.from_string(invitee), room_id, "invite", txn_id=txn_id
) )
@ -1042,7 +1100,7 @@ class RoomMemberMasterHandler(RoomMemberHandler):
return event_id, stream_id return event_id, stream_id
# The room is too large. Leave. # The room is too large. Leave.
requester = types.create_requester(user, None, False, None) requester = types.create_requester(user, None, False, False, None)
await self.update_membership( await self.update_membership(
requester=requester, target=user, room_id=room_id, action="leave" requester=requester, target=user, room_id=room_id, action="leave"
) )

View File

@ -316,6 +316,9 @@ class JoinRoomAliasServlet(RestServlet):
join_rules_event = room_state.get((EventTypes.JoinRules, "")) join_rules_event = room_state.get((EventTypes.JoinRules, ""))
if join_rules_event: if join_rules_event:
if not (join_rules_event.content.get("join_rule") == JoinRules.PUBLIC): if not (join_rules_event.content.get("join_rule") == JoinRules.PUBLIC):
# update_membership with an action of "invite" can raise a
# ShadowBanError. This is not handled since it is assumed that
# an admin isn't going to call this API with a shadow-banned user.
await self.room_member_handler.update_membership( await self.room_member_handler.update_membership(
requester=requester, requester=requester,
target=fake_requester.user, target=fake_requester.user,

View File

@ -27,6 +27,7 @@ from synapse.api.errors import (
Codes, Codes,
HttpResponseException, HttpResponseException,
InvalidClientCredentialsError, InvalidClientCredentialsError,
ShadowBanError,
SynapseError, SynapseError,
) )
from synapse.api.filtering import Filter from synapse.api.filtering import Filter
@ -45,6 +46,7 @@ from synapse.storage.state import StateFilter
from synapse.streams.config import PaginationConfig from synapse.streams.config import PaginationConfig
from synapse.types import RoomAlias, RoomID, StreamToken, ThirdPartyInstanceID, UserID from synapse.types import RoomAlias, RoomID, StreamToken, ThirdPartyInstanceID, UserID
from synapse.util import json_decoder from synapse.util import json_decoder
from synapse.util.stringutils import random_string
MYPY = False MYPY = False
if MYPY: if MYPY:
@ -200,14 +202,17 @@ class RoomStateEventRestServlet(TransactionRestServlet):
event_dict["state_key"] = state_key event_dict["state_key"] = state_key
if event_type == EventTypes.Member: if event_type == EventTypes.Member:
membership = content.get("membership", None) try:
event_id, _ = await self.room_member_handler.update_membership( membership = content.get("membership", None)
requester, event_id, _ = await self.room_member_handler.update_membership(
target=UserID.from_string(state_key), requester,
room_id=room_id, target=UserID.from_string(state_key),
action=membership, room_id=room_id,
content=content, action=membership,
) content=content,
)
except ShadowBanError:
event_id = "$" + random_string(43)
else: else:
( (
event, event,
@ -719,16 +724,20 @@ class RoomMembershipRestServlet(TransactionRestServlet):
content = {} content = {}
if membership_action == "invite" and self._has_3pid_invite_keys(content): if membership_action == "invite" and self._has_3pid_invite_keys(content):
await self.room_member_handler.do_3pid_invite( try:
room_id, await self.room_member_handler.do_3pid_invite(
requester.user, room_id,
content["medium"], requester.user,
content["address"], content["medium"],
content["id_server"], content["address"],
requester, content["id_server"],
txn_id, requester,
content.get("id_access_token"), txn_id,
) content.get("id_access_token"),
)
except ShadowBanError:
# Pretend the request succeeded.
pass
return 200, {} return 200, {}
target = requester.user target = requester.user
@ -740,15 +749,19 @@ class RoomMembershipRestServlet(TransactionRestServlet):
if "reason" in content: if "reason" in content:
event_content = {"reason": content["reason"]} event_content = {"reason": content["reason"]}
await self.room_member_handler.update_membership( try:
requester=requester, await self.room_member_handler.update_membership(
target=target, requester=requester,
room_id=room_id, target=target,
action=membership_action, room_id=room_id,
txn_id=txn_id, action=membership_action,
third_party_signed=content.get("third_party_signed", None), txn_id=txn_id,
content=event_content, third_party_signed=content.get("third_party_signed", None),
) content=event_content,
)
except ShadowBanError:
# Pretend the request succeeded.
pass
return_value = {} return_value = {}

View File

@ -1974,3 +1974,103 @@ class RoomCanonicalAliasTestCase(unittest.HomeserverTestCase):
"""An alias which does not point to the room raises a SynapseError.""" """An alias which does not point to the room raises a SynapseError."""
self._set_canonical_alias({"alias": "@unknown:test"}, expected_code=400) self._set_canonical_alias({"alias": "@unknown:test"}, expected_code=400)
self._set_canonical_alias({"alt_aliases": ["@unknown:test"]}, expected_code=400) self._set_canonical_alias({"alt_aliases": ["@unknown:test"]}, expected_code=400)
class ShadowBannedTestCase(unittest.HomeserverTestCase):
servlets = [
synapse.rest.admin.register_servlets_for_client_rest_resource,
directory.register_servlets,
login.register_servlets,
room.register_servlets,
]
def prepare(self, reactor, clock, homeserver):
self.banned_user_id = self.register_user("banned", "test")
self.banned_access_token = self.login("banned", "test")
self.store = self.hs.get_datastore()
self.get_success(
self.store.db_pool.simple_update(
table="users",
keyvalues={"name": self.banned_user_id},
updatevalues={"shadow_banned": True},
desc="shadow_ban",
)
)
self.other_user_id = self.register_user("otheruser", "pass")
self.other_access_token = self.login("otheruser", "pass")
def test_invite(self):
"""Invites from shadow-banned users don't actually get sent."""
# The create works fine.
room_id = self.helper.create_room_as(
self.banned_user_id, tok=self.banned_access_token
)
# Inviting the user completes successfully.
self.helper.invite(
room=room_id,
src=self.banned_user_id,
tok=self.banned_access_token,
targ=self.other_user_id,
)
# But the user wasn't actually invited.
invited_rooms = self.get_success(
self.store.get_invited_rooms_for_local_user(self.other_user_id)
)
self.assertEqual(invited_rooms, [])
def test_invite_3pid(self):
"""Ensure that a 3PID invite does not attempt to contact the identity server."""
identity_handler = self.hs.get_handlers().identity_handler
identity_handler.lookup_3pid = Mock(
side_effect=AssertionError("This should not get called")
)
# The create works fine.
room_id = self.helper.create_room_as(
self.banned_user_id, tok=self.banned_access_token
)
# Inviting the user completes successfully.
request, channel = self.make_request(
"POST",
"/rooms/%s/invite" % (room_id,),
{"id_server": "test", "medium": "email", "address": "test@test.test"},
access_token=self.banned_access_token,
)
self.render(request)
self.assertEquals(200, channel.code, channel.result)
# This should have raised an error earlier, but double check this wasn't called.
identity_handler.lookup_3pid.assert_not_called()
def test_create_room(self):
"""Invitations during a room creation should be discarded, but the room still gets created."""
# The room creation is successful.
request, channel = self.make_request(
"POST",
"/_matrix/client/r0/createRoom",
{"visibility": "public", "invite": [self.other_user_id]},
access_token=self.banned_access_token,
)
self.render(request)
self.assertEquals(200, channel.code, channel.result)
room_id = channel.json_body["room_id"]
# But the user wasn't actually invited.
invited_rooms = self.get_success(
self.store.get_invited_rooms_for_local_user(self.other_user_id)
)
self.assertEqual(invited_rooms, [])
# Since a real room was created, the other user should be able to join it.
self.helper.join(room_id, self.other_user_id, tok=self.other_access_token)
# Both users should be in the room.
users = self.get_success(self.store.get_users_in_room(room_id))
self.assertCountEqual(users, ["@banned:test", "@otheruser:test"])