Add support for MSC4293 - Redact on Kick/Ban (#18540)

This commit is contained in:
Shay 2025-07-23 08:00:01 -07:00 committed by GitHub
parent a82b8a966a
commit 8fb9c105c9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 1194 additions and 14 deletions

View File

@ -0,0 +1 @@
Add support for [MSC4293](https://github.com/matrix-org/matrix-spec-proposals/pull/4293) - Redact on Kick/Ban.

View File

@ -582,6 +582,9 @@ class ExperimentalConfig(Config):
# MSC4155: Invite filtering
self.msc4155_enabled: bool = experimental.get("msc4155_enabled", False)
# MSC4293: Redact on Kick/Ban
self.msc4293_enabled: bool = experimental.get("msc4293_enabled", False)
# MSC4306: Thread Subscriptions
# (and MSC4308: sliding sync extension for thread subscriptions)
self.msc4306_enabled: bool = experimental.get("msc4306_enabled", False)

View File

@ -1100,6 +1100,7 @@ class RoomMembershipRestServlet(TransactionRestServlet):
super().__init__(hs)
self.room_member_handler = hs.get_room_member_handler()
self.auth = hs.get_auth()
self.config = hs.config
def register(self, http_server: HttpServer) -> None:
# /rooms/$roomid/[join|invite|leave|ban|unban|kick]
@ -1123,12 +1124,12 @@ class RoomMembershipRestServlet(TransactionRestServlet):
}:
raise AuthError(403, "Guest access not allowed")
content = parse_json_object_from_request(request, allow_empty_body=True)
request_body = parse_json_object_from_request(request, allow_empty_body=True)
if membership_action == "invite" and all(
key in content for key in ("medium", "address")
key in request_body for key in ("medium", "address")
):
if not all(key in content for key in ("id_server", "id_access_token")):
if not all(key in request_body for key in ("id_server", "id_access_token")):
raise SynapseError(
HTTPStatus.BAD_REQUEST,
"`id_server` and `id_access_token` are required when doing 3pid invite",
@ -1139,12 +1140,12 @@ class RoomMembershipRestServlet(TransactionRestServlet):
await self.room_member_handler.do_3pid_invite(
room_id,
requester.user,
content["medium"],
content["address"],
content["id_server"],
request_body["medium"],
request_body["address"],
request_body["id_server"],
requester,
txn_id,
content["id_access_token"],
request_body["id_access_token"],
)
except ShadowBanError:
# Pretend the request succeeded.
@ -1153,12 +1154,19 @@ class RoomMembershipRestServlet(TransactionRestServlet):
target = requester.user
if membership_action in ["invite", "ban", "unban", "kick"]:
assert_params_in_dict(content, ["user_id"])
target = UserID.from_string(content["user_id"])
assert_params_in_dict(request_body, ["user_id"])
target = UserID.from_string(request_body["user_id"])
event_content = None
if "reason" in content:
event_content = {"reason": content["reason"]}
if "reason" in request_body:
event_content = {"reason": request_body["reason"]}
if self.config.experimental.msc4293_enabled:
if "org.matrix.msc4293.redact_events" in request_body:
if event_content is None:
event_content = {}
event_content["org.matrix.msc4293.redact_events"] = request_body[
"org.matrix.msc4293.redact_events"
]
try:
await self.room_member_handler.update_membership(
@ -1167,7 +1175,7 @@ class RoomMembershipRestServlet(TransactionRestServlet):
room_id=room_id,
action=membership_action,
txn_id=txn_id,
third_party_signed=content.get("third_party_signed", None),
third_party_signed=request_body.get("third_party_signed", None),
content=event_content,
)
except ShadowBanError:

View File

@ -377,11 +377,130 @@ class PersistEventsStore:
event_counter.labels(event.type, origin_type, origin_entity).inc()
if (
not self.hs.config.experimental.msc4293_enabled
or event.type != EventTypes.Member
or event.state_key is None
):
continue
# check if this is an unban/join that will undo a ban/kick redaction for
# a user in the room
if event.membership in [Membership.LEAVE, Membership.JOIN]:
if (
event.membership == Membership.LEAVE
and event.sender == event.state_key
):
# self-leave, ignore
continue
# if there is an existing ban/leave causing redactions for
# this user/room combination update the entry with the stream
# ordering when the redactions should stop - in the case of a backfilled
# event where the stream ordering is negative, use the current max stream
# ordering
stream_ordering = event.internal_metadata.stream_ordering
assert stream_ordering is not None
if stream_ordering < 0:
stream_ordering = self._stream_id_gen.get_current_token()
await self.db_pool.simple_update(
"room_ban_redactions",
{"room_id": event.room_id, "user_id": event.state_key},
{"redact_end_ordering": stream_ordering},
desc="room_ban_redactions update redact_end_ordering",
)
# check for msc4293 redact_events flag and apply if found
if event.membership not in [Membership.LEAVE, Membership.BAN]:
continue
redact = event.content.get("org.matrix.msc4293.redact_events", False)
if not redact or not isinstance(redact, bool):
continue
# self-bans currently are not authorized so we don't check for that
# case
if (
event.membership == Membership.BAN
and event.sender == event.state_key
):
continue
# check that sender can redact
redact_allowed = await self._can_sender_redact(event)
# Signal that this user's past events in this room
# should be redacted by adding an entry to
# `room_ban_redactions`.
if redact_allowed:
await self.db_pool.simple_upsert(
"room_ban_redactions",
{"room_id": event.room_id, "user_id": event.state_key},
{
"redacting_event_id": event.event_id,
"redact_end_ordering": None,
},
{
"room_id": event.room_id,
"user_id": event.state_key,
"redacting_event_id": event.event_id,
"redact_end_ordering": None,
},
)
# normally the cache entry for a redacted event would be invalidated
# by an arriving redaction event, but since we are not creating redaction
# events we invalidate manually
self.store._invalidate_local_get_event_cache_room_id(event.room_id)
self.store._invalidate_async_get_event_cache_room_id(event.room_id)
if new_forward_extremities:
self.store.get_latest_event_ids_in_room.prefill(
(room_id,), frozenset(new_forward_extremities)
)
async def _can_sender_redact(self, event: EventBase) -> bool:
state_filter = StateFilter.from_types(
[(EventTypes.PowerLevels, ""), (EventTypes.Create, "")]
)
state = await self.store.get_partial_filtered_current_state_ids(
event.room_id, state_filter
)
pl_id = state[(EventTypes.PowerLevels, "")]
pl_event = await self.store.get_event(pl_id, allow_none=True)
if pl_event is None:
# per the spec, if a power level event isn't in the room, grant the creator
# level 100 and all other users 0
create_id = state[(EventTypes.Create, "")]
create_event = await self.store.get_event(create_id, allow_none=True)
if create_event is None:
# not sure how this would happen but if it does then just deny the redaction
logger.warning("No create event found for room %s", event.room_id)
return False
if create_event.sender == event.sender:
return True
assert pl_event is not None
sender_level = pl_event.content.get("users", {}).get(event.sender)
if sender_level is None:
sender_level = pl_event.content.get("users_default", 0)
redact_level = pl_event.content.get("redact")
if redact_level is None:
redact_level = pl_event.content.get("events_default", 0)
room_redaction_level = pl_event.content.get("events", {}).get(
"m.room.redaction"
)
if room_redaction_level is not None:
if sender_level < room_redaction_level:
return False
if sender_level >= redact_level:
return True
return False
async def _calculate_sliding_sync_table_changes(
self,
room_id: str,

View File

@ -17,7 +17,7 @@
# [This file includes modifications made by New Vector Limited]
#
#
import json
import logging
import threading
import weakref
@ -976,6 +976,13 @@ class EventsWorkerStore(SQLBaseStore):
self._event_ref.clear()
self._current_event_fetches.clear()
def _invalidate_async_get_event_cache_room_id(self, room_id: str) -> None:
"""
Clears the async get_event cache for a room. Currently a no-op until
an async get_event cache is implemented - see https://github.com/matrix-org/synapse/pull/13242
for preliminary work.
"""
async def _get_events_from_cache(
self, events: Iterable[str], update_metrics: bool = True
) -> Dict[str, EventCacheEntry]:
@ -1575,6 +1582,44 @@ class EventsWorkerStore(SQLBaseStore):
if d:
d.redactions.append(redacter)
# check for MSC4932 redactions
to_check = []
events: List[_EventRow] = []
for e in evs:
event = event_dict.get(e)
if not event:
continue
events.append(event)
event_json = json.loads(event.json)
room_id = event_json.get("room_id")
user_id = event_json.get("sender")
to_check.append((room_id, user_id))
# likely that some of these events may be for the same room/user combo, in
# which case we don't need to do redundant queries
to_check_set = set(to_check)
for room_and_user in to_check_set:
room_redactions_sql = "SELECT redacting_event_id, redact_end_ordering FROM room_ban_redactions WHERE room_id = ? and user_id = ?"
txn.execute(room_redactions_sql, room_and_user)
res = txn.fetchone()
# we have a redaction for a room, user_id combo - apply it to matching events
if not res:
continue
for e_row in events:
e_json = json.loads(e_row.json)
room_id = e_json.get("room_id")
user_id = e_json.get("sender")
if room_and_user != (room_id, user_id):
continue
redacting_event_id, redact_end_ordering = res
if redact_end_ordering:
# Avoid redacting any events arriving *after* the membership event which
# ends an active redaction - note that this will always redact
# backfilled events, as they have a negative stream ordering
if e_row.stream_ordering >= redact_end_ordering:
continue
e_row.redactions.append(redacting_event_id)
return event_dict
def _maybe_redact_event_row(

View File

@ -0,0 +1,21 @@
--
-- This file is licensed under the Affero General Public License (AGPL) version 3.
--
-- Copyright (C) 2025 New Vector, Ltd
--
-- This program is free software: you can redistribute it and/or modify
-- it under the terms of the GNU Affero General Public License as
-- published by the Free Software Foundation, either version 3 of the
-- License, or (at your option) any later version.
--
-- See the GNU Affero General Public License for more details:
-- <https://www.gnu.org/licenses/agpl-3.0.html>.
CREATE TABLE room_ban_redactions(
room_id text NOT NULL,
user_id text NOT NULL,
redacting_event_id text NOT NULL,
redact_end_ordering bigint DEFAULT NULL, -- stream ordering after which redactions are not applied
CONSTRAINT room_ban_redaction_uniqueness UNIQUE (room_id, user_id)
);

View File

@ -43,8 +43,9 @@ from synapse.api.constants import (
RoomTypes,
)
from synapse.api.errors import Codes, HttpResponseException
from synapse.api.room_versions import RoomVersions
from synapse.appservice import ApplicationService
from synapse.events import EventBase
from synapse.events import EventBase, make_event_from_dict
from synapse.events.snapshot import EventContext
from synapse.rest import admin
from synapse.rest.client import (
@ -4499,3 +4500,985 @@ class RoomParticipantTestCase(unittest.HomeserverTestCase):
self.store.get_room_participation(self.user2, self.room1)
)
self.assertFalse(participant)
class MSC4293RedactOnBanKickTestCase(unittest.FederatingHomeserverTestCase):
servlets = [
synapse.rest.admin.register_servlets_for_client_rest_resource,
room.register_servlets,
login.register_servlets,
admin.register_servlets,
]
def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
super().prepare(reactor, clock, hs)
self.creator = self.register_user("creator", "test")
self.creator_tok = self.login("creator", "test")
self.bad_user_id = self.register_user("bad", "test")
self.bad_tok = self.login("bad", "test")
self.room_id = self.helper.create_room_as(self.creator, tok=self.creator_tok)
self.store = hs.get_datastores().main
self._storage_controllers = hs.get_storage_controllers()
self.federation_event_handler = self.hs.get_federation_event_handler()
self.hs.config.experimental.msc4293_enabled = True
def _check_redactions(
self,
original_events: List[EventBase],
pulled_events: List[JsonDict],
expect_redaction: bool,
reason: Optional[str] = None,
) -> None:
"""
Checks a set of original events against a second set of the same events, pulled
from the /messages api. If expect_redaction is true, we expect that the second
set of events will be redacted, and the test will fail if that is not the case.
Otherwise, verifies that the events have not been redacted and fails if not.
Args:
original_events: A list of the original events sent
pulled_events: A list of the same events as the orignal events, fetched
over the /messages api
expect_redaction: Whether or not the pulled_events should be redacted
reason: If the events are expected to be redacted, the expected reason
for the redaction
"""
if expect_redaction:
redacted_count = 0
for pulled_event in pulled_events:
for old_event in original_events:
if pulled_event["event_id"] != old_event.event_id:
continue
# we have a matching event, check that it is redacted
event_content = pulled_event["content"]
if event_content:
self.fail(f"Expected event {pulled_event} to be redacted")
redacting_event = pulled_event.get("redacted_because")
if not redacting_event:
self.fail(
f"Expected event {pulled_event} to have a redacting event."
)
# check that the redacting event records the expected reason, and the
# redact_events flag
content = redacting_event["content"]
self.assertEqual(content["reason"], reason)
self.assertEqual(content["org.matrix.msc4293.redact_events"], True)
redacted_count += 1
# all provided events should be redacted
self.assertEqual(len(original_events), redacted_count)
else:
unredacted_events = 0
for pulled_event in pulled_events:
for old_event in original_events:
if pulled_event["event_id"] != old_event.event_id:
continue
# we have a matching event, make sure it is not redacted
redacted_because = pulled_event.get("redacted_because")
if redacted_because:
self.fail("Event should not have been redacted")
self.assertEqual(old_event.content, pulled_event["content"])
unredacted_events += 1
# all provided events should not have been redacted
self.assertEqual(unredacted_events, len(original_events))
def test_banning_local_member_with_flag_redacts_their_events(self) -> None:
self.helper.join(self.room_id, self.bad_user_id, tok=self.bad_tok)
# bad user sends some messages
originals = []
for i in range(5):
event = {"body": f"bothersome noise {i}", "msgtype": "m.text"}
res = self.helper.send_event(
self.room_id, "m.room.message", event, tok=self.bad_tok, expect_code=200
)
originals.append(res["event_id"])
# grab original events for comparison
original_events = [self.get_success(self.store.get_event(x)) for x in originals]
# creator bans user with redaction flag set
content = {
"reason": "flooding",
"org.matrix.msc4293.redact_events": True,
}
self.helper.change_membership(
self.room_id,
self.creator,
self.bad_user_id,
"ban",
content,
self.creator_tok,
)
filter = json.dumps({"types": [EventTypes.Message]})
channel = self.make_request(
"GET",
f"rooms/{self.room_id}/messages?filter={filter}&limit=50",
access_token=self.creator_tok,
)
self.assertEqual(channel.code, 200)
self._check_redactions(
original_events,
channel.json_body["chunk"],
expect_redaction=True,
reason="flooding",
)
def test_banning_remote_member_with_flag_redacts_their_events(self) -> None:
bad_user = "@remote_bad_user:" + self.OTHER_SERVER_NAME
channel = self.make_signed_federation_request(
"GET",
f"/_matrix/federation/v1/make_join/{self.room_id}/{bad_user}?ver=10",
)
self.assertEqual(channel.code, HTTPStatus.OK, channel.json_body)
join_result = channel.json_body
join_event_dict = join_result["event"]
self.add_hashes_and_signatures_from_other_server(
join_event_dict,
RoomVersions.V10,
)
channel = self.make_signed_federation_request(
"PUT",
f"/_matrix/federation/v2/send_join/{self.room_id}/x",
content=join_event_dict,
)
self.assertEqual(channel.code, HTTPStatus.OK, channel.json_body)
# the room should show that the bad user is a member
r = self.get_success(
self._storage_controllers.state.get_current_state(self.room_id)
)
self.assertEqual(r[("m.room.member", bad_user)].membership, "join")
auth_ids = [
r[("m.room.create", "")].event_id,
r[("m.room.power_levels", "")].event_id,
r[("m.room.member", "@remote_bad_user:other.example.com")].event_id,
]
original_messages = []
for i in range(5):
remote_message = make_event_from_dict(
self.add_hashes_and_signatures_from_other_server(
{
"room_id": self.room_id,
"sender": bad_user,
"depth": 1000,
"origin_server_ts": 1,
"type": "m.room.message",
"content": {"body": f"remote bummer{i}"},
"auth_events": auth_ids,
"prev_events": auth_ids,
}
),
room_version=RoomVersions.V10,
)
self.get_success(
self.federation_event_handler.on_receive_pdu(
self.OTHER_SERVER_NAME, remote_message
)
)
original_messages.append(remote_message)
# creator bans bad user with redaction flag set
content = {
"reason": "bummer messages",
"org.matrix.msc4293.redact_events": True,
}
res = self.helper.change_membership(
self.room_id, self.creator, bad_user, "ban", content, self.creator_tok
)
ban_event_id = res["event_id"]
filter = json.dumps({"types": [EventTypes.Message]})
channel = self.make_request(
"GET",
f"rooms/{self.room_id}/messages?filter={filter}&limit=50",
access_token=self.creator_tok,
)
self.assertEqual(channel.code, 200)
self._check_redactions(
original_messages,
channel.json_body["chunk"],
expect_redaction=True,
reason="bummer messages",
)
# any future messages that are soft-failed are also redacted - send messages referencing
# dag before ban, they should be soft-failed but also redacted
new_original_messages = []
for i in range(5):
remote_message = make_event_from_dict(
self.add_hashes_and_signatures_from_other_server(
{
"room_id": self.room_id,
"sender": bad_user,
"depth": 1000,
"origin_server_ts": 1,
"type": "m.room.message",
"content": {"body": f"soft-fail remote bummer{i}"},
"auth_events": auth_ids,
"prev_events": auth_ids,
}
),
room_version=RoomVersions.V10,
)
self.get_success(
self.federation_event_handler.on_receive_pdu(
self.OTHER_SERVER_NAME, remote_message
)
)
new_original_messages.append(remote_message)
# pull them from the db to check because they should be soft-failed and thus not available over
# cs-api
for message in new_original_messages:
original = self.get_success(self.store.get_event(message.event_id))
if not original:
self.fail("Expected to find remote message in DB")
redacted_because = original.unsigned.get("redacted_because")
if not redacted_because:
self.fail("Did not find redacted_because field")
self.assertEqual(redacted_because.event_id, ban_event_id)
def test_unbanning_remote_user_stops_redaction_action(self) -> None:
bad_user = "@remote_bad_user:" + self.OTHER_SERVER_NAME
channel = self.make_signed_federation_request(
"GET",
f"/_matrix/federation/v1/make_join/{self.room_id}/{bad_user}?ver=10",
)
self.assertEqual(channel.code, HTTPStatus.OK, channel.json_body)
join_result = channel.json_body
join_event_dict = join_result["event"]
self.add_hashes_and_signatures_from_other_server(
join_event_dict,
RoomVersions.V10,
)
channel = self.make_signed_federation_request(
"PUT",
f"/_matrix/federation/v2/send_join/{self.room_id}/x",
content=join_event_dict,
)
self.assertEqual(channel.code, HTTPStatus.OK, channel.json_body)
# the room should show that the bad user is a member
r = self.get_success(
self._storage_controllers.state.get_current_state(self.room_id)
)
self.assertEqual(r[("m.room.member", bad_user)].membership, "join")
auth_ids = [
r[("m.room.create", "")].event_id,
r[("m.room.power_levels", "")].event_id,
r[("m.room.member", "@remote_bad_user:other.example.com")].event_id,
]
original_messages = []
for i in range(5):
remote_message = make_event_from_dict(
self.add_hashes_and_signatures_from_other_server(
{
"room_id": self.room_id,
"sender": bad_user,
"depth": 1000,
"origin_server_ts": 1,
"type": "m.room.message",
"content": {"body": f"annoying messages {i}"},
"auth_events": auth_ids,
"prev_events": auth_ids,
}
),
room_version=RoomVersions.V10,
)
self.get_success(
self.federation_event_handler.on_receive_pdu(
self.OTHER_SERVER_NAME, remote_message
)
)
original_messages.append(remote_message)
# creator bans bad user with redaction flag set
content = {
"reason": "this dude sucks",
"org.matrix.msc4293.redact_events": True,
}
self.helper.change_membership(
self.room_id, self.creator, bad_user, "ban", content, self.creator_tok
)
filter = json.dumps({"types": [EventTypes.Message]})
channel = self.make_request(
"GET",
f"rooms/{self.room_id}/messages?filter={filter}&limit=50",
access_token=self.creator_tok,
)
self.assertEqual(channel.code, 200)
self._check_redactions(
original_messages,
channel.json_body["chunk"],
True,
reason="this dude sucks",
)
# unban user
self.helper.change_membership(
self.room_id, self.creator, bad_user, "unban", {}, self.creator_tok
)
# user should be able to join again
channel = self.make_signed_federation_request(
"GET",
f"/_matrix/federation/v1/make_join/{self.room_id}/{bad_user}?ver=10",
)
self.assertEqual(channel.code, HTTPStatus.OK, channel.json_body)
join_result = channel.json_body
join_event_dict = join_result["event"]
self.add_hashes_and_signatures_from_other_server(
join_event_dict,
RoomVersions.V10,
)
channel = self.make_signed_federation_request(
"PUT",
f"/_matrix/federation/v2/send_join/{self.room_id}/x",
content=join_event_dict,
)
self.assertEqual(channel.code, HTTPStatus.OK, channel.json_body)
# the room should show that the bad user is a member again
new_state = self.get_success(
self._storage_controllers.state.get_current_state(self.room_id)
)
self.assertEqual(new_state[("m.room.member", bad_user)].membership, "join")
new_state = self.get_success(
self._storage_controllers.state.get_current_state(self.room_id)
)
auth_ids = [
new_state[("m.room.create", "")].event_id,
new_state[("m.room.power_levels", "")].event_id,
new_state[("m.room.member", "@remote_bad_user:other.example.com")].event_id,
]
# messages after unban and join proceed unredacted
new_original_messages = []
for i in range(5):
remote_message = make_event_from_dict(
self.add_hashes_and_signatures_from_other_server(
{
"room_id": self.room_id,
"sender": bad_user,
"depth": 1000,
"origin_server_ts": 1,
"type": "m.room.message",
"content": {"body": f"no longer a bummer {i}"},
"auth_events": auth_ids,
"prev_events": auth_ids,
}
),
room_version=RoomVersions.V10,
)
self.get_success(
self.federation_event_handler.on_receive_pdu(
self.OTHER_SERVER_NAME, remote_message
)
)
new_original_messages.append(remote_message)
filter = json.dumps({"types": [EventTypes.Message]})
channel = self.make_request(
"GET",
f"rooms/{self.room_id}/messages?filter={filter}&limit=50",
access_token=self.creator_tok,
)
self.assertEqual(channel.code, 200)
self._check_redactions(new_original_messages, channel.json_body["chunk"], False)
def test_redaction_flag_ignored_for_user_if_banner_lacks_redaction_power(
self,
) -> None:
# change power levels so creator can ban but not redact
self.helper.send_state(
self.room_id,
"m.room.power_levels",
{"events_default": 0, "redact": 100, "users": {self.creator: 75}},
tok=self.creator_tok,
)
self.helper.join(self.room_id, self.bad_user_id, tok=self.bad_tok)
# bad user sends some messages
original_ids = []
for i in range(15):
event = {"body": f"being a menace {i}", "msgtype": "m.text"}
res = self.helper.send_event(
self.room_id, "m.room.message", event, tok=self.bad_tok, expect_code=200
)
original_ids.append(res["event_id"])
# grab original events before ban
originals = [self.get_success(self.store.get_event(x)) for x in original_ids]
# creator bans bad user with redaction flag
content = {
"reason": "flooding",
"org.matrix.msc4293.redact_events": True,
}
self.helper.change_membership(
self.room_id,
self.creator,
self.bad_user_id,
"ban",
content,
self.creator_tok,
)
filter = json.dumps({"types": [EventTypes.Message]})
channel = self.make_request(
"GET",
f"rooms/{self.room_id}/messages?filter={filter}&limit=50",
access_token=self.creator_tok,
)
self.assertEqual(channel.code, 200)
# messages are not redacted
self._check_redactions(originals, channel.json_body["chunk"], False)
def test_kicking_local_member_with_flag_redacts_their_events(self) -> None:
self.helper.join(self.room_id, self.bad_user_id, tok=self.bad_tok)
# bad user sends some messages
originals = []
for i in range(5):
event = {"body": f"bothersome noise {i}", "msgtype": "m.text"}
res = self.helper.send_event(
self.room_id, "m.room.message", event, tok=self.bad_tok, expect_code=200
)
originals.append(res["event_id"])
# grab original events for comparison
original_events = [self.get_success(self.store.get_event(x)) for x in originals]
# creator kicks user with redaction flag set
content = {
"reason": "flooding",
"org.matrix.msc4293.redact_events": True,
}
self.helper.change_membership(
self.room_id,
self.creator,
self.bad_user_id,
"kick",
content,
self.creator_tok,
)
filter = json.dumps({"types": [EventTypes.Message]})
channel = self.make_request(
"GET",
f"rooms/{self.room_id}/messages?filter={filter}&limit=50",
access_token=self.creator_tok,
)
self.assertEqual(channel.code, 200)
self._check_redactions(
original_events,
channel.json_body["chunk"],
expect_redaction=True,
reason="flooding",
)
def test_kicking_remote_member_with_flag_redacts_their_events(self) -> None:
bad_user = "@remote_bad_user:" + self.OTHER_SERVER_NAME
channel = self.make_signed_federation_request(
"GET",
f"/_matrix/federation/v1/make_join/{self.room_id}/{bad_user}?ver=10",
)
self.assertEqual(channel.code, HTTPStatus.OK, channel.json_body)
join_result = channel.json_body
join_event_dict = join_result["event"]
self.add_hashes_and_signatures_from_other_server(
join_event_dict,
RoomVersions.V10,
)
channel = self.make_signed_federation_request(
"PUT",
f"/_matrix/federation/v2/send_join/{self.room_id}/x",
content=join_event_dict,
)
self.assertEqual(channel.code, HTTPStatus.OK, channel.json_body)
# the room should show that the bad user is a member
r = self.get_success(
self._storage_controllers.state.get_current_state(self.room_id)
)
self.assertEqual(r[("m.room.member", bad_user)].membership, "join")
auth_ids = [
r[("m.room.create", "")].event_id,
r[("m.room.power_levels", "")].event_id,
r[("m.room.member", "@remote_bad_user:other.example.com")].event_id,
]
original_messages = []
for i in range(5):
remote_message = make_event_from_dict(
self.add_hashes_and_signatures_from_other_server(
{
"room_id": self.room_id,
"sender": bad_user,
"depth": 1000,
"origin_server_ts": 1,
"type": "m.room.message",
"content": {"body": f"remote bummer{i}"},
"auth_events": auth_ids,
"prev_events": auth_ids,
}
),
room_version=RoomVersions.V10,
)
self.get_success(
self.federation_event_handler.on_receive_pdu(
self.OTHER_SERVER_NAME, remote_message
)
)
original_messages.append(remote_message)
# creator kicks bad user with redaction flag set
content = {
"reason": "bummer messages",
"org.matrix.msc4293.redact_events": True,
}
res = self.helper.change_membership(
self.room_id, self.creator, bad_user, "kick", content, self.creator_tok
)
ban_event_id = res["event_id"]
filter = json.dumps({"types": [EventTypes.Message]})
channel = self.make_request(
"GET",
f"rooms/{self.room_id}/messages?filter={filter}&limit=50",
access_token=self.creator_tok,
)
self.assertEqual(channel.code, 200)
self._check_redactions(
original_messages,
channel.json_body["chunk"],
expect_redaction=True,
reason="bummer messages",
)
# any future messages that are soft-failed are also redacted - send messages referencing
# dag before ban, they should be soft-failed but also redacted
new_original_messages = []
for i in range(5):
remote_message = make_event_from_dict(
self.add_hashes_and_signatures_from_other_server(
{
"room_id": self.room_id,
"sender": bad_user,
"depth": 1000,
"origin_server_ts": 1,
"type": "m.room.message",
"content": {"body": f"soft-fail remote bummer{i}"},
"auth_events": auth_ids,
"prev_events": auth_ids,
}
),
room_version=RoomVersions.V10,
)
self.get_success(
self.federation_event_handler.on_receive_pdu(
self.OTHER_SERVER_NAME, remote_message
)
)
new_original_messages.append(remote_message)
# pull them from the db to check because they should be soft-failed and thus not available over
# cs-api
for message in new_original_messages:
original = self.get_success(self.store.get_event(message.event_id))
if not original:
self.fail("Expected to find remote message in DB")
self.assertEqual(original.unsigned["redacted_by"], ban_event_id)
def test_rejoining_kicked_remote_user_stops_redaction_action(self) -> None:
bad_user = "@remote_bad_user:" + self.OTHER_SERVER_NAME
channel = self.make_signed_federation_request(
"GET",
f"/_matrix/federation/v1/make_join/{self.room_id}/{bad_user}?ver=10",
)
self.assertEqual(channel.code, HTTPStatus.OK, channel.json_body)
join_result = channel.json_body
join_event_dict = join_result["event"]
self.add_hashes_and_signatures_from_other_server(
join_event_dict,
RoomVersions.V10,
)
channel = self.make_signed_federation_request(
"PUT",
f"/_matrix/federation/v2/send_join/{self.room_id}/x",
content=join_event_dict,
)
self.assertEqual(channel.code, HTTPStatus.OK, channel.json_body)
# the room should show that the bad user is a member
r = self.get_success(
self._storage_controllers.state.get_current_state(self.room_id)
)
self.assertEqual(r[("m.room.member", bad_user)].membership, "join")
auth_ids = [
r[("m.room.create", "")].event_id,
r[("m.room.power_levels", "")].event_id,
r[("m.room.member", "@remote_bad_user:other.example.com")].event_id,
]
original_messages = []
for i in range(5):
remote_message = make_event_from_dict(
self.add_hashes_and_signatures_from_other_server(
{
"room_id": self.room_id,
"sender": bad_user,
"depth": 1000,
"origin_server_ts": 1,
"type": "m.room.message",
"content": {"body": f"annoying messages {i}"},
"auth_events": auth_ids,
"prev_events": auth_ids,
}
),
room_version=RoomVersions.V10,
)
self.get_success(
self.federation_event_handler.on_receive_pdu(
self.OTHER_SERVER_NAME, remote_message
)
)
original_messages.append(remote_message)
# creator kicks bad user with redaction flag set
content = {
"reason": "this dude sucks",
"org.matrix.msc4293.redact_events": True,
}
self.helper.change_membership(
self.room_id, self.creator, bad_user, "kick", content, self.creator_tok
)
filter = json.dumps({"types": [EventTypes.Message]})
channel = self.make_request(
"GET",
f"rooms/{self.room_id}/messages?filter={filter}&limit=50",
access_token=self.creator_tok,
)
self.assertEqual(channel.code, 200)
self._check_redactions(
original_messages,
channel.json_body["chunk"],
True,
reason="this dude sucks",
)
# user re-joins after kick
channel = self.make_signed_federation_request(
"GET",
f"/_matrix/federation/v1/make_join/{self.room_id}/{bad_user}?ver=10",
)
self.assertEqual(channel.code, HTTPStatus.OK, channel.json_body)
join_result = channel.json_body
join_event_dict = join_result["event"]
self.add_hashes_and_signatures_from_other_server(
join_event_dict,
RoomVersions.V10,
)
channel = self.make_signed_federation_request(
"PUT",
f"/_matrix/federation/v2/send_join/{self.room_id}/x",
content=join_event_dict,
)
self.assertEqual(channel.code, HTTPStatus.OK, channel.json_body)
# the room should show that the bad user is a member again
new_state = self.get_success(
self._storage_controllers.state.get_current_state(self.room_id)
)
self.assertEqual(new_state[("m.room.member", bad_user)].membership, "join")
new_state = self.get_success(
self._storage_controllers.state.get_current_state(self.room_id)
)
auth_ids = [
new_state[("m.room.create", "")].event_id,
new_state[("m.room.power_levels", "")].event_id,
new_state[("m.room.member", "@remote_bad_user:other.example.com")].event_id,
]
# messages after kick and re-join proceed unredacted
new_original_messages = []
for i in range(5):
remote_message = make_event_from_dict(
self.add_hashes_and_signatures_from_other_server(
{
"room_id": self.room_id,
"sender": bad_user,
"depth": 1000,
"origin_server_ts": 1,
"type": "m.room.message",
"content": {"body": f"no longer a bummer {i}"},
"auth_events": auth_ids,
"prev_events": auth_ids,
}
),
room_version=RoomVersions.V10,
)
self.get_success(
self.federation_event_handler.on_receive_pdu(
self.OTHER_SERVER_NAME, remote_message
)
)
new_original_messages.append(remote_message)
filter = json.dumps({"types": [EventTypes.Message]})
channel = self.make_request(
"GET",
f"rooms/{self.room_id}/messages?filter={filter}&limit=50",
access_token=self.creator_tok,
)
self.assertEqual(channel.code, 200)
self._check_redactions(new_original_messages, channel.json_body["chunk"], False)
def test_redaction_flag_ignored_for_user_if_kicker_lacks_redaction_power(
self,
) -> None:
# change power levels so creator can kick but not redact
self.helper.send_state(
self.room_id,
"m.room.power_levels",
{"events_default": 0, "redact": 100, "users": {self.creator: 75}},
tok=self.creator_tok,
)
self.helper.join(self.room_id, self.bad_user_id, tok=self.bad_tok)
# bad user sends some messages
original_ids = []
for i in range(15):
event = {"body": f"being a menace {i}", "msgtype": "m.text"}
res = self.helper.send_event(
self.room_id, "m.room.message", event, tok=self.bad_tok, expect_code=200
)
original_ids.append(res["event_id"])
# grab original events before ban
originals = [self.get_success(self.store.get_event(x)) for x in original_ids]
# creator kicks bad user with redaction flag
content = {
"reason": "flooding",
"org.matrix.msc4293.redact_events": True,
}
self.helper.change_membership(
self.room_id,
self.creator,
self.bad_user_id,
"kick",
content,
self.creator_tok,
)
filter = json.dumps({"types": [EventTypes.Message]})
channel = self.make_request(
"GET",
f"rooms/{self.room_id}/messages?filter={filter}&limit=50",
access_token=self.creator_tok,
)
self.assertEqual(channel.code, 200)
# messages are not redacted
self._check_redactions(originals, channel.json_body["chunk"], False)
def test_MSC4293_flag_ignored_in_other_membership_events(self) -> None:
self.helper.join(self.room_id, self.bad_user_id, tok=self.bad_tok)
# bad user sends some messages
original_ids = []
for i in range(15):
event = {"body": f"being a menace {i}", "msgtype": "m.text"}
res = self.helper.send_event(
self.room_id, "m.room.message", event, tok=self.bad_tok, expect_code=200
)
original_ids.append(res["event_id"])
# grab original events before ban
originals = [self.get_success(self.store.get_event(x)) for x in original_ids]
# bad user leaves on their own with flag
content = {
"org.matrix.msc4293.redact_events": True,
}
self.helper.change_membership(
self.room_id,
self.bad_user_id,
self.bad_user_id,
"leave",
content,
self.bad_tok,
)
# their messages are not redacted
filter = json.dumps({"types": [EventTypes.Message]})
channel = self.make_request(
"GET",
f"rooms/{self.room_id}/messages?filter={filter}&limit=50",
access_token=self.creator_tok,
)
self.assertEqual(channel.code, 200)
self._check_redactions(originals, channel.json_body["chunk"], False)
# bad user is invited with flag in invite event
content = {
"org.matrix.msc4293.redact_events": True,
}
self.helper.change_membership(
self.room_id,
self.creator,
self.bad_user_id,
"invite",
content,
self.creator_tok,
)
# their messages are still not redacted
filter = json.dumps({"types": [EventTypes.Message]})
channel = self.make_request(
"GET",
f"rooms/{self.room_id}/messages?filter={filter}&limit=50",
access_token=self.creator_tok,
)
self.assertEqual(channel.code, 200)
self._check_redactions(originals, channel.json_body["chunk"], False)
# bad user joins with flag in invite event
content = {
"org.matrix.msc4293.redact_events": True,
}
self.helper.change_membership(
self.room_id,
self.bad_user_id,
self.bad_user_id,
"join",
content,
self.bad_tok,
)
# and still their messages are not redacted
filter = json.dumps({"types": [EventTypes.Message]})
channel = self.make_request(
"GET",
f"rooms/{self.room_id}/messages?filter={filter}&limit=50",
access_token=self.creator_tok,
)
self.assertEqual(channel.code, 200)
self._check_redactions(originals, channel.json_body["chunk"], False)
def test_MSC4293_redaction_applied_via_kick_api(self) -> None:
"""
Test that MSC4239 field passed through and applied when using /kick
"""
self.helper.join(self.room_id, self.bad_user_id, tok=self.bad_tok)
# bad user sends some messages
original_ids = []
for i in range(15):
event = {"body": f"being a menace {i}", "msgtype": "m.text"}
res = self.helper.send_event(
self.room_id, "m.room.message", event, tok=self.bad_tok, expect_code=200
)
original_ids.append(res["event_id"])
# grab original events before kick
originals = [self.get_success(self.store.get_event(x)) for x in original_ids]
channel = self.make_request(
"POST",
f"/_matrix/client/v3/rooms/{self.room_id}/kick",
access_token=self.creator_tok,
content={
"reason": "being annoying",
"org.matrix.msc4293.redact_events": True,
"user_id": self.bad_user_id,
},
shorthand=False,
)
self.assertEqual(channel.code, 200)
filter = json.dumps({"types": [EventTypes.Message]})
channel = self.make_request(
"GET",
f"rooms/{self.room_id}/messages?filter={filter}&limit=50",
access_token=self.creator_tok,
)
self.assertEqual(channel.code, 200)
self._check_redactions(
originals,
channel.json_body["chunk"],
expect_redaction=True,
reason="being annoying",
)
def test_MSC4293_redaction_applied_via_ban_api(self) -> None:
"""
Test that MSC4239 field passed through and applied when using /ban
"""
self.helper.join(self.room_id, self.bad_user_id, tok=self.bad_tok)
# bad user sends some messages
original_ids = []
for i in range(15):
event = {"body": f"being a menace {i}", "msgtype": "m.text"}
res = self.helper.send_event(
self.room_id, "m.room.message", event, tok=self.bad_tok, expect_code=200
)
original_ids.append(res["event_id"])
# grab original events before ban
originals = [self.get_success(self.store.get_event(x)) for x in original_ids]
channel = self.make_request(
"POST",
f"/_matrix/client/v3/rooms/{self.room_id}/ban",
access_token=self.creator_tok,
content={
"reason": "being disruptive",
"org.matrix.msc4293.redact_events": True,
"user_id": self.bad_user_id,
},
shorthand=False,
)
self.assertEqual(channel.code, 200)
filter = json.dumps({"types": [EventTypes.Message]})
channel = self.make_request(
"GET",
f"rooms/{self.room_id}/messages?filter={filter}&limit=50",
access_token=self.creator_tok,
)
self.assertEqual(channel.code, 200)
self._check_redactions(
originals,
channel.json_body["chunk"],
expect_redaction=True,
reason="being disruptive",
)