Skip to content
This repository has been archived by the owner on Apr 26, 2024. It is now read-only.

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

Merged
merged 4 commits into from
Aug 24, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/8142.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add support for shadow-banning users (ignoring any message send requests).
6 changes: 6 additions & 0 deletions synapse/handlers/directory.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
CodeMessageException,
Codes,
NotFoundError,
ShadowBanError,
StoreError,
SynapseError,
)
Expand Down Expand Up @@ -199,6 +200,8 @@ async def delete_association(

try:
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)
Comment on lines +203 to +204
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Previously this did not get caught and kept raising.

except AuthError as e:
logger.info("Failed to update alias events: %s", e)

Expand Down Expand Up @@ -292,6 +295,9 @@ async def _update_canonical_alias(
"""
Send an updated canonical alias event if the removed alias was set as
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(
room_id, EventTypes.CanonicalAlias, ""
Expand Down
10 changes: 10 additions & 0 deletions synapse/handlers/message.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import logging
import random
from typing import TYPE_CHECKING, Dict, List, Optional, Tuple

from canonicaljson import encode_canonical_json
Expand All @@ -34,6 +35,7 @@
Codes,
ConsentNotGivenError,
NotFoundError,
ShadowBanError,
SynapseError,
)
from synapse.api.room_versions import KNOWN_ROOM_VERSIONS, RoomVersions
Expand Down Expand Up @@ -716,12 +718,20 @@ async def create_and_send_nonmember_event(
event_dict: dict,
ratelimit: bool = True,
txn_id: Optional[str] = None,
ignore_shadow_ban: bool = False,
) -> Tuple[EventBase, int]:
"""
Creates an event, then sends it.

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
# don't fork the DAG too much. If we don't limit then we can end up in
Expand Down
19 changes: 18 additions & 1 deletion synapse/handlers/room.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,9 @@ async def upgrade_room(

Returns:
the new room id

Raises:
ShadowBanError if the requester is shadow-banned.
"""
await self.ratelimit(requester)

Expand Down Expand Up @@ -171,6 +174,15 @@ async def upgrade_room(
async def _upgrade_room(
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.
Comment on lines +183 to +184
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if this should catch here and just create a new room for the shadow-banned person instead of continuing to raise ShadowBanError?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Possibly, or just no-op. I think it'll be a pretty rare case given you need to be a mod to upgrade a room.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alright. This does get caught at the REST layer and a fake room ID is returned.

"""
user_id = requester.user.to_string()

# start by allocating a new room id
Expand Down Expand Up @@ -257,6 +269,9 @@ async def _update_upgraded_room_pls(
old_room_id: the id of the room to be replaced
new_room_id: the id of the replacement 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, ""))

Expand Down Expand Up @@ -829,11 +844,13 @@ def create(etype: str, content: JsonDict, **kwargs) -> JsonDict:
async def send(etype: str, content: JsonDict, **kwargs) -> int:
event = create(etype, content, **kwargs)
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,
) = 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

Expand Down
74 changes: 44 additions & 30 deletions synapse/rest/client/v1/room.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,8 +201,8 @@ async def on_PUT(self, request, room_id, event_type, state_key, txn_id=None):
if state_key is not None:
event_dict["state_key"] = state_key

if event_type == EventTypes.Member:
try:
try:
if event_type == EventTypes.Member:
membership = content.get("membership", None)
event_id, _ = await self.room_member_handler.update_membership(
requester,
Expand All @@ -211,16 +211,16 @@ async def on_PUT(self, request, room_id, event_type, state_key, txn_id=None):
action=membership,
content=content,
)
except ShadowBanError:
event_id = "$" + random_string(43)
else:
(
event,
_,
) = await self.event_creation_handler.create_and_send_nonmember_event(
requester, event_dict, txn_id=txn_id
)
event_id = event.event_id
else:
(
event,
_,
) = await self.event_creation_handler.create_and_send_nonmember_event(
requester, event_dict, txn_id=txn_id
)
event_id = event.event_id
except ShadowBanError:
event_id = "$" + random_string(43)

set_tag("event_id", event_id)
ret = {"event_id": event_id}
Expand Down Expand Up @@ -253,12 +253,19 @@ async def on_POST(self, request, room_id, event_type, txn_id=None):
if b"ts" in request.args and requester.app_service:
event_dict["origin_server_ts"] = parse_integer(request, "ts", 0)

event, _ = await self.event_creation_handler.create_and_send_nonmember_event(
requester, event_dict, txn_id=txn_id
)
try:
(
event,
_,
) = await self.event_creation_handler.create_and_send_nonmember_event(
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)
return 200, {"event_id": event.event_id}
set_tag("event_id", event_id)
return 200, {"event_id": event_id}

def on_GET(self, request, room_id, event_type, txn_id):
return 200, "Not implemented"
Expand Down Expand Up @@ -799,20 +806,27 @@ async def on_POST(self, request, room_id, event_id, txn_id=None):
requester = await self.auth.get_user_by_req(request)
content = parse_json_object_from_request(request)

event, _ = await self.event_creation_handler.create_and_send_nonmember_event(
requester,
{
"type": EventTypes.Redaction,
"content": content,
"room_id": room_id,
"sender": requester.user.to_string(),
"redacts": event_id,
},
txn_id=txn_id,
)
try:
(
event,
_,
) = await self.event_creation_handler.create_and_send_nonmember_event(
requester,
{
"type": EventTypes.Redaction,
"content": content,
"room_id": room_id,
"sender": requester.user.to_string(),
"redacts": event_id,
},
txn_id=txn_id,
)
event_id = event.event_id
except ShadowBanError:
event_id = "$" + random_string(43)

set_tag("event_id", event.event_id)
return 200, {"event_id": event.event_id}
set_tag("event_id", event_id)
return 200, {"event_id": event_id}

def on_PUT(self, request, room_id, event_id, txn_id):
set_tag("txn_id", txn_id)
Expand Down
18 changes: 13 additions & 5 deletions synapse/rest/client/v2_alpha/relations.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
import logging

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 (
RestServlet,
parse_integer,
Expand All @@ -35,6 +35,7 @@
PaginationChunk,
RelationPaginationToken,
)
from synapse.util.stringutils import random_string

from ._base import client_patterns

Expand Down Expand Up @@ -111,11 +112,18 @@ async def on_PUT_or_POST(
"sender": requester.user.to_string(),
}

event, _ = await self.event_creation_handler.create_and_send_nonmember_event(
requester, event_dict=event_dict, txn_id=txn_id
)
try:
(
event,
_,
) = await self.event_creation_handler.create_and_send_nonmember_event(
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):
Expand Down
14 changes: 9 additions & 5 deletions synapse/rest/client/v2_alpha/room_upgrade_rest_servlet.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,14 @@

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.http.servlet import (
RestServlet,
assert_params_in_dict,
parse_json_object_from_request,
)
from synapse.util import stringutils

from ._base import client_patterns

Expand Down Expand Up @@ -62,7 +63,6 @@ async def on_POST(self, request, room_id):

content = parse_json_object_from_request(request)
assert_params_in_dict(content, ("new_version",))
new_version = content["new_version"]

new_version = KNOWN_ROOM_VERSIONS.get(content["new_version"])
if new_version is None:
Expand All @@ -72,9 +72,13 @@ async def on_POST(self, request, room_id):
Codes.UNSUPPORTED_ROOM_VERSION,
)

new_room_id = await self._room_creation_handler.upgrade_room(
requester, room_id, new_version
)
try:
new_room_id = await self._room_creation_handler.upgrade_room(
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}

Expand Down
55 changes: 54 additions & 1 deletion tests/rest/client/v1/test_rooms.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
from synapse.api.constants import EventContentFields, EventTypes, Membership
from synapse.handlers.pagination import PurgeStatus
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.util.stringutils import random_string

Expand Down Expand Up @@ -1984,6 +1984,7 @@ class ShadowBannedTestCase(unittest.HomeserverTestCase):
directory.register_servlets,
login.register_servlets,
room.register_servlets,
room_upgrade_rest_servlet.register_servlets,
]

def prepare(self, reactor, clock, homeserver):
Expand Down Expand Up @@ -2076,3 +2077,55 @@ def test_create_room(self):
# 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"])

def test_message(self):
clokep marked this conversation as resolved.
Show resolved Hide resolved
"""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, {})