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

Commit

Permalink
Implement MSC2815: allow room moderators to view redacted event conte…
Browse files Browse the repository at this point in the history
…nt (#12427)

Implements matrix-org/matrix-spec-proposals#2815

Signed-off-by: Tulir Asokan <tulir@maunium.net>
  • Loading branch information
tulir authored Apr 20, 2022
1 parent eed38c5 commit 4bc8cb4
Show file tree
Hide file tree
Showing 7 changed files with 100 additions and 3 deletions.
1 change: 1 addition & 0 deletions changelog.d/12427.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Implement [MSC2815](https://github.com/matrix-org/matrix-spec-proposals/pull/2815) to allow room moderators to view redacted event content. Contributed by @tulir.
18 changes: 18 additions & 0 deletions synapse/api/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ class Codes:
UNABLE_AUTHORISE_JOIN = "M_UNABLE_TO_AUTHORISE_JOIN"
UNABLE_TO_GRANT_JOIN = "M_UNABLE_TO_GRANT_JOIN"

UNREDACTED_CONTENT_DELETED = "FI.MAU.MSC2815_UNREDACTED_CONTENT_DELETED"


class CodeMessageException(RuntimeError):
"""An exception with integer code and message string attributes.
Expand Down Expand Up @@ -483,6 +485,22 @@ def __init__(self, inner_exception: BaseException, can_retry: bool):
self.can_retry = can_retry


class UnredactedContentDeletedError(SynapseError):
def __init__(self, content_keep_ms: Optional[int] = None):
super().__init__(
404,
"The content for that event has already been erased from the database",
errcode=Codes.UNREDACTED_CONTENT_DELETED,
)
self.content_keep_ms = content_keep_ms

def error_dict(self) -> "JsonDict":
extra = {}
if self.content_keep_ms is not None:
extra = {"fi.mau.msc2815.content_keep_ms": self.content_keep_ms}
return cs_error(self.msg, self.errcode, **extra)


def cs_error(msg: str, code: str = Codes.UNKNOWN, **kwargs: Any) -> "JsonDict":
"""Utility method for constructing an error response for client-server
interactions.
Expand Down
3 changes: 3 additions & 0 deletions synapse/config/experimental.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,6 @@ def read_config(self, config: JsonDict, **kwargs: Any) -> None:

# MSC2654: Unread counts
self.msc2654_enabled: bool = experimental.get("msc2654_enabled", False)

# MSC2815 (allow room moderators to view redacted event content)
self.msc2815_enabled: bool = experimental.get("msc2815_enabled", False)
15 changes: 13 additions & 2 deletions synapse/handlers/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from synapse.events import EventBase
from synapse.events.utils import SerializeEventConfig
from synapse.handlers.presence import format_user_presence_state
from synapse.storage.databases.main.events_worker import EventRedactBehaviour
from synapse.streams.config import PaginationConfig
from synapse.types import JsonDict, UserID
from synapse.visibility import filter_events_for_client
Expand Down Expand Up @@ -141,7 +142,11 @@ def __init__(self, hs: "HomeServer"):
self.storage = hs.get_storage()

async def get_event(
self, user: UserID, room_id: Optional[str], event_id: str
self,
user: UserID,
room_id: Optional[str],
event_id: str,
show_redacted: bool = False,
) -> Optional[EventBase]:
"""Retrieve a single specified event.
Expand All @@ -150,14 +155,20 @@ async def get_event(
room_id: The expected room id. We'll return None if the
event's room does not match.
event_id: The event ID to obtain.
show_redacted: Should the full content of redacted events be returned?
Returns:
An event, or None if there is no event matching this ID.
Raises:
SynapseError if there was a problem retrieving this event, or
AuthError if the user does not have the rights to inspect this
event.
"""
event = await self.store.get_event(event_id, check_room_id=room_id)
redact_behaviour = (
EventRedactBehaviour.AS_IS if show_redacted else EventRedactBehaviour.REDACT
)
event = await self.store.get_event(
event_id, check_room_id=room_id, redact_behaviour=redact_behaviour
)

if not event:
return None
Expand Down
46 changes: 45 additions & 1 deletion synapse/rest/client/room.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

from twisted.web.server import Request

from synapse import event_auth
from synapse.api.constants import EventTypes, Membership
from synapse.api.errors import (
AuthError,
Expand All @@ -29,6 +30,7 @@
MissingClientTokenError,
ShadowBanError,
SynapseError,
UnredactedContentDeletedError,
)
from synapse.api.filtering import Filter
from synapse.events.utils import format_event_for_client_v2
Expand Down Expand Up @@ -643,18 +645,55 @@ def __init__(self, hs: "HomeServer"):
super().__init__()
self.clock = hs.get_clock()
self._store = hs.get_datastores().main
self._state = hs.get_state_handler()
self.event_handler = hs.get_event_handler()
self._event_serializer = hs.get_event_client_serializer()
self._relations_handler = hs.get_relations_handler()
self.auth = hs.get_auth()
self.content_keep_ms = hs.config.server.redaction_retention_period
self.msc2815_enabled = hs.config.experimental.msc2815_enabled

async def on_GET(
self, request: SynapseRequest, room_id: str, event_id: str
) -> Tuple[int, JsonDict]:
requester = await self.auth.get_user_by_req(request, allow_guest=True)

include_unredacted_content = self.msc2815_enabled and (
parse_string(
request,
"fi.mau.msc2815.include_unredacted_content",
allowed_values=("true", "false"),
)
== "true"
)
if include_unredacted_content and not await self.auth.is_server_admin(
requester.user
):
power_level_event = await self._state.get_current_state(
room_id, EventTypes.PowerLevels, ""
)

auth_events = {}
if power_level_event:
auth_events[(EventTypes.PowerLevels, "")] = power_level_event

redact_level = event_auth.get_named_level(auth_events, "redact", 50)
user_level = event_auth.get_user_power_level(
requester.user.to_string(), auth_events
)
if user_level < redact_level:
raise SynapseError(
403,
"You don't have permission to view redacted events in this room.",
errcode=Codes.FORBIDDEN,
)

try:
event = await self.event_handler.get_event(
requester.user, room_id, event_id
requester.user,
room_id,
event_id,
show_redacted=include_unredacted_content,
)
except AuthError:
# This endpoint is supposed to return a 404 when the requester does
Expand All @@ -663,6 +702,11 @@ async def on_GET(
raise SynapseError(404, "Event not found.", errcode=Codes.NOT_FOUND)

if event:
if include_unredacted_content and await self._store.have_censored_event(
event_id
):
raise UnredactedContentDeletedError(self.content_keep_ms)

# Ensure there are bundled aggregations available.
aggregations = await self._relations_handler.get_bundled_aggregations(
[event], requester.user.to_string()
Expand Down
2 changes: 2 additions & 0 deletions synapse/rest/client/versions.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,8 @@ def on_GET(self, request: Request) -> Tuple[int, JsonDict]:
"org.matrix.msc3030": self.config.experimental.msc3030_enabled,
# Adds support for thread relations, per MSC3440.
"org.matrix.msc3440.stable": True, # TODO: remove when "v1.3" is added above
# Allows moderators to fetch redacted event content as described in MSC2815
"fi.mau.msc2815": self.config.experimental.msc2815_enabled,
},
},
)
Expand Down
18 changes: 18 additions & 0 deletions synapse/storage/databases/main/events_worker.py
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,24 @@ async def get_received_ts(self, event_id: str) -> Optional[int]:
desc="get_received_ts",
)

async def have_censored_event(self, event_id: str) -> bool:
"""Check if an event has been censored, i.e. if the content of the event has been erased
from the database due to a redaction.
Args:
event_id: The event ID that was redacted.
Returns:
True if the event has been censored, False otherwise.
"""
censored_redactions_list = await self.db_pool.simple_select_onecol(
table="redactions",
keyvalues={"redacts": event_id},
retcol="have_censored",
desc="get_have_censored",
)
return any(censored_redactions_list)

# Inform mypy that if allow_none is False (the default) then get_event
# always returns an EventBase.
@overload
Expand Down

0 comments on commit 4bc8cb4

Please sign in to comment.