Stop shadow-banned users from sending non-member events. (#8142)

This commit is contained in:
Patrick Cloke 2020-08-24 13:58:56 -04:00 committed by GitHub
parent 420484a334
commit cbd8d83da7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 155 additions and 42 deletions

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

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

View File

@ -23,6 +23,7 @@ from synapse.api.errors import (
CodeMessageException, CodeMessageException,
Codes, Codes,
NotFoundError, NotFoundError,
ShadowBanError,
StoreError, StoreError,
SynapseError, SynapseError,
) )
@ -199,6 +200,8 @@ class DirectoryHandler(BaseHandler):
try: try:
await self._update_canonical_alias(requester, user_id, room_id, room_alias) await self._update_canonical_alias(requester, user_id, room_id, room_alias)
except ShadowBanError as e:
logger.info("Failed to update alias events due to shadow-ban: %s", e)
except AuthError as e: except AuthError as e:
logger.info("Failed to update alias events: %s", e) logger.info("Failed to update alias events: %s", e)
@ -292,6 +295,9 @@ class DirectoryHandler(BaseHandler):
""" """
Send an updated canonical alias event if the removed alias was set as Send an updated canonical alias event if the removed alias was set as
the canonical alias or listed in the alt_aliases field. the canonical alias or listed in the alt_aliases field.
Raises:
ShadowBanError if the requester has been shadow-banned.
""" """
alias_event = await self.state.get_current_state( alias_event = await self.state.get_current_state(
room_id, EventTypes.CanonicalAlias, "" room_id, EventTypes.CanonicalAlias, ""

View File

@ -15,6 +15,7 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import logging import logging
import random
from typing import TYPE_CHECKING, Dict, List, Optional, Tuple from typing import TYPE_CHECKING, Dict, List, Optional, Tuple
from canonicaljson import encode_canonical_json from canonicaljson import encode_canonical_json
@ -34,6 +35,7 @@ from synapse.api.errors import (
Codes, Codes,
ConsentNotGivenError, ConsentNotGivenError,
NotFoundError, NotFoundError,
ShadowBanError,
SynapseError, SynapseError,
) )
from synapse.api.room_versions import KNOWN_ROOM_VERSIONS, RoomVersions from synapse.api.room_versions import KNOWN_ROOM_VERSIONS, RoomVersions
@ -716,12 +718,20 @@ class EventCreationHandler(object):
event_dict: dict, event_dict: dict,
ratelimit: bool = True, ratelimit: bool = True,
txn_id: Optional[str] = None, txn_id: Optional[str] = None,
ignore_shadow_ban: bool = False,
) -> Tuple[EventBase, int]: ) -> Tuple[EventBase, int]:
""" """
Creates an event, then sends it. Creates an event, then sends it.
See self.create_event and self.send_nonmember_event. See self.create_event and self.send_nonmember_event.
Raises:
ShadowBanError if the requester has been shadow-banned.
""" """
if not ignore_shadow_ban and requester.shadow_banned:
# We randomly sleep a bit just to annoy the requester.
await self.clock.sleep(random.randint(1, 10))
raise ShadowBanError()
# We limit the number of concurrent event sends in a room so that we # We limit the number of concurrent event sends in a room so that we
# don't fork the DAG too much. If we don't limit then we can end up in # don't fork the DAG too much. If we don't limit then we can end up in

View File

@ -136,6 +136,9 @@ class RoomCreationHandler(BaseHandler):
Returns: Returns:
the new room id the new room id
Raises:
ShadowBanError if the requester is shadow-banned.
""" """
await self.ratelimit(requester) await self.ratelimit(requester)
@ -171,6 +174,15 @@ class RoomCreationHandler(BaseHandler):
async def _upgrade_room( async def _upgrade_room(
self, requester: Requester, old_room_id: str, new_version: RoomVersion self, requester: Requester, old_room_id: str, new_version: RoomVersion
): ):
"""
Args:
requester: the user requesting the upgrade
old_room_id: the id of the room to be replaced
new_versions: the version to upgrade the room to
Raises:
ShadowBanError if the requester is shadow-banned.
"""
user_id = requester.user.to_string() user_id = requester.user.to_string()
# start by allocating a new room id # start by allocating a new room id
@ -257,6 +269,9 @@ class RoomCreationHandler(BaseHandler):
old_room_id: the id of the room to be replaced old_room_id: the id of the room to be replaced
new_room_id: the id of the replacement room new_room_id: the id of the replacement room
old_room_state: the state map for the old room old_room_state: the state map for the old room
Raises:
ShadowBanError if the requester is shadow-banned.
""" """
old_room_pl_event_id = old_room_state.get((EventTypes.PowerLevels, "")) old_room_pl_event_id = old_room_state.get((EventTypes.PowerLevels, ""))
@ -829,11 +844,13 @@ class RoomCreationHandler(BaseHandler):
async def send(etype: str, content: JsonDict, **kwargs) -> int: async def send(etype: str, content: JsonDict, **kwargs) -> int:
event = create(etype, content, **kwargs) event = create(etype, content, **kwargs)
logger.debug("Sending %s in new room", etype) logger.debug("Sending %s in new room", etype)
# Allow these events to be sent even if the user is shadow-banned to
# allow the room creation to complete.
( (
_, _,
last_stream_id, last_stream_id,
) = await self.event_creation_handler.create_and_send_nonmember_event( ) = await self.event_creation_handler.create_and_send_nonmember_event(
creator, event, ratelimit=False creator, event, ratelimit=False, ignore_shadow_ban=True,
) )
return last_stream_id return last_stream_id

View File

@ -201,8 +201,8 @@ class RoomStateEventRestServlet(TransactionRestServlet):
if state_key is not None: if state_key is not None:
event_dict["state_key"] = state_key event_dict["state_key"] = state_key
if event_type == EventTypes.Member:
try: try:
if event_type == EventTypes.Member:
membership = content.get("membership", None) membership = content.get("membership", None)
event_id, _ = await self.room_member_handler.update_membership( event_id, _ = await self.room_member_handler.update_membership(
requester, requester,
@ -211,8 +211,6 @@ class RoomStateEventRestServlet(TransactionRestServlet):
action=membership, action=membership,
content=content, content=content,
) )
except ShadowBanError:
event_id = "$" + random_string(43)
else: else:
( (
event, event,
@ -221,6 +219,8 @@ class RoomStateEventRestServlet(TransactionRestServlet):
requester, event_dict, txn_id=txn_id requester, event_dict, txn_id=txn_id
) )
event_id = event.event_id event_id = event.event_id
except ShadowBanError:
event_id = "$" + random_string(43)
set_tag("event_id", event_id) set_tag("event_id", event_id)
ret = {"event_id": event_id} ret = {"event_id": event_id}
@ -253,12 +253,19 @@ class RoomSendEventRestServlet(TransactionRestServlet):
if b"ts" in request.args and requester.app_service: if b"ts" in request.args and requester.app_service:
event_dict["origin_server_ts"] = parse_integer(request, "ts", 0) event_dict["origin_server_ts"] = parse_integer(request, "ts", 0)
event, _ = await self.event_creation_handler.create_and_send_nonmember_event( try:
(
event,
_,
) = await self.event_creation_handler.create_and_send_nonmember_event(
requester, event_dict, txn_id=txn_id requester, event_dict, txn_id=txn_id
) )
event_id = event.event_id
except ShadowBanError:
event_id = "$" + random_string(43)
set_tag("event_id", event.event_id) set_tag("event_id", event_id)
return 200, {"event_id": event.event_id} return 200, {"event_id": event_id}
def on_GET(self, request, room_id, event_type, txn_id): def on_GET(self, request, room_id, event_type, txn_id):
return 200, "Not implemented" return 200, "Not implemented"
@ -799,7 +806,11 @@ class RoomRedactEventRestServlet(TransactionRestServlet):
requester = await self.auth.get_user_by_req(request) requester = await self.auth.get_user_by_req(request)
content = parse_json_object_from_request(request) content = parse_json_object_from_request(request)
event, _ = await self.event_creation_handler.create_and_send_nonmember_event( try:
(
event,
_,
) = await self.event_creation_handler.create_and_send_nonmember_event(
requester, requester,
{ {
"type": EventTypes.Redaction, "type": EventTypes.Redaction,
@ -810,9 +821,12 @@ class RoomRedactEventRestServlet(TransactionRestServlet):
}, },
txn_id=txn_id, txn_id=txn_id,
) )
event_id = event.event_id
except ShadowBanError:
event_id = "$" + random_string(43)
set_tag("event_id", event.event_id) set_tag("event_id", event_id)
return 200, {"event_id": event.event_id} return 200, {"event_id": event_id}
def on_PUT(self, request, room_id, event_id, txn_id): def on_PUT(self, request, room_id, event_id, txn_id):
set_tag("txn_id", txn_id) set_tag("txn_id", txn_id)

View File

@ -22,7 +22,7 @@ any time to reflect changes in the MSC.
import logging import logging
from synapse.api.constants import EventTypes, RelationTypes from synapse.api.constants import EventTypes, RelationTypes
from synapse.api.errors import SynapseError from synapse.api.errors import ShadowBanError, SynapseError
from synapse.http.servlet import ( from synapse.http.servlet import (
RestServlet, RestServlet,
parse_integer, parse_integer,
@ -35,6 +35,7 @@ from synapse.storage.relations import (
PaginationChunk, PaginationChunk,
RelationPaginationToken, RelationPaginationToken,
) )
from synapse.util.stringutils import random_string
from ._base import client_patterns from ._base import client_patterns
@ -111,11 +112,18 @@ class RelationSendServlet(RestServlet):
"sender": requester.user.to_string(), "sender": requester.user.to_string(),
} }
event, _ = await self.event_creation_handler.create_and_send_nonmember_event( try:
(
event,
_,
) = await self.event_creation_handler.create_and_send_nonmember_event(
requester, event_dict=event_dict, txn_id=txn_id requester, event_dict=event_dict, txn_id=txn_id
) )
event_id = event.event_id
except ShadowBanError:
event_id = "$" + random_string(43)
return 200, {"event_id": event.event_id} return 200, {"event_id": event_id}
class RelationPaginationServlet(RestServlet): class RelationPaginationServlet(RestServlet):

View File

@ -15,13 +15,14 @@
import logging import logging
from synapse.api.errors import Codes, SynapseError from synapse.api.errors import Codes, ShadowBanError, SynapseError
from synapse.api.room_versions import KNOWN_ROOM_VERSIONS from synapse.api.room_versions import KNOWN_ROOM_VERSIONS
from synapse.http.servlet import ( from synapse.http.servlet import (
RestServlet, RestServlet,
assert_params_in_dict, assert_params_in_dict,
parse_json_object_from_request, parse_json_object_from_request,
) )
from synapse.util import stringutils
from ._base import client_patterns from ._base import client_patterns
@ -62,7 +63,6 @@ class RoomUpgradeRestServlet(RestServlet):
content = parse_json_object_from_request(request) content = parse_json_object_from_request(request)
assert_params_in_dict(content, ("new_version",)) assert_params_in_dict(content, ("new_version",))
new_version = content["new_version"]
new_version = KNOWN_ROOM_VERSIONS.get(content["new_version"]) new_version = KNOWN_ROOM_VERSIONS.get(content["new_version"])
if new_version is None: if new_version is None:
@ -72,9 +72,13 @@ class RoomUpgradeRestServlet(RestServlet):
Codes.UNSUPPORTED_ROOM_VERSION, Codes.UNSUPPORTED_ROOM_VERSION,
) )
try:
new_room_id = await self._room_creation_handler.upgrade_room( new_room_id = await self._room_creation_handler.upgrade_room(
requester, room_id, new_version requester, room_id, new_version
) )
except ShadowBanError:
# Generate a random room ID.
new_room_id = stringutils.random_string(18)
ret = {"replacement_room": new_room_id} ret = {"replacement_room": new_room_id}

View File

@ -27,7 +27,7 @@ import synapse.rest.admin
from synapse.api.constants import EventContentFields, EventTypes, Membership from synapse.api.constants import EventContentFields, EventTypes, Membership
from synapse.handlers.pagination import PurgeStatus from synapse.handlers.pagination import PurgeStatus
from synapse.rest.client.v1 import directory, login, profile, room from synapse.rest.client.v1 import directory, login, profile, room
from synapse.rest.client.v2_alpha import account from synapse.rest.client.v2_alpha import account, room_upgrade_rest_servlet
from synapse.types import JsonDict, RoomAlias from synapse.types import JsonDict, RoomAlias
from synapse.util.stringutils import random_string from synapse.util.stringutils import random_string
@ -1984,6 +1984,7 @@ class ShadowBannedTestCase(unittest.HomeserverTestCase):
directory.register_servlets, directory.register_servlets,
login.register_servlets, login.register_servlets,
room.register_servlets, room.register_servlets,
room_upgrade_rest_servlet.register_servlets,
] ]
def prepare(self, reactor, clock, homeserver): def prepare(self, reactor, clock, homeserver):
@ -2076,3 +2077,55 @@ class ShadowBannedTestCase(unittest.HomeserverTestCase):
# Both users should be in the room. # Both users should be in the room.
users = self.get_success(self.store.get_users_in_room(room_id)) users = self.get_success(self.store.get_users_in_room(room_id))
self.assertCountEqual(users, ["@banned:test", "@otheruser:test"]) self.assertCountEqual(users, ["@banned:test", "@otheruser:test"])
def test_message(self):
"""Messages from shadow-banned users don't actually get sent."""
room_id = self.helper.create_room_as(
self.other_user_id, tok=self.other_access_token
)
# The user should be in the room.
self.helper.join(room_id, self.banned_user_id, tok=self.banned_access_token)
# Sending a message should complete successfully.
result = self.helper.send_event(
room_id=room_id,
type=EventTypes.Message,
content={"msgtype": "m.text", "body": "with right label"},
tok=self.banned_access_token,
)
self.assertIn("event_id", result)
event_id = result["event_id"]
latest_events = self.get_success(
self.store.get_latest_event_ids_in_room(room_id)
)
self.assertNotIn(event_id, latest_events)
def test_upgrade(self):
"""A room upgrade should fail, but look like it succeeded."""
# The create works fine.
room_id = self.helper.create_room_as(
self.banned_user_id, tok=self.banned_access_token
)
request, channel = self.make_request(
"POST",
"/_matrix/client/r0/rooms/%s/upgrade" % (room_id,),
{"new_version": "6"},
access_token=self.banned_access_token,
)
self.render(request)
self.assertEquals(200, channel.code, channel.result)
# A new room_id should be returned.
self.assertIn("replacement_room", channel.json_body)
new_room_id = channel.json_body["replacement_room"]
# It doesn't really matter what API we use here, we just want to assert
# that the room doesn't exist.
summary = self.get_success(self.store.get_room_summary(new_room_id))
# The summary should be empty since the room doesn't exist.
self.assertEqual(summary, {})