mirror of
https://git.anonymousland.org/anonymousland/synapse.git
synced 2024-09-09 13:11:52 +00:00
Stop shadow-banned users from sending invites. (#8095)
This commit is contained in:
parent
318f4e738e
commit
e259d63f73
1
changelog.d/8095.feature
Normal file
1
changelog.d/8095.feature
Normal file
@ -0,0 +1 @@
|
|||||||
|
Add support for shadow-banning users (ignoring any message send requests).
|
@ -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.
|
||||||
|
"""
|
||||||
|
@ -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,
|
||||||
|
@ -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"
|
||||||
)
|
)
|
||||||
|
@ -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,
|
||||||
|
@ -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 = {}
|
||||||
|
|
||||||
|
@ -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"])
|
||||||
|
Loading…
Reference in New Issue
Block a user