Skip to content
23 changes: 23 additions & 0 deletions docs/admin_api/user_admin_api.md
Original file line number Diff line number Diff line change
Expand Up @@ -1361,3 +1361,26 @@ Returns a `404` HTTP status code if no user was found, with a response body like
```

_Added in Synapse 1.72.0._


## Redact all the events of a user

The API is
```
POST /_synapse/admin/v1/user/$user_id/redact

{
Comment thread
H-Shay marked this conversation as resolved.
"rooms": [!roomid1, !roomid2]
Comment thread
H-Shay marked this conversation as resolved.
Outdated
}
```
If an empty dict is provided as the key for `rooms`, all events in all the rooms the user is member of will be redacted,
Comment thread
H-Shay marked this conversation as resolved.
Outdated
otherwise all the events in the rooms provided in the request will be redacted.

An empty JSON dict is returned.

**Parameters**

The following parameters should be set in the URL:

- `user_id` - The fully qualified MXID of the user: for example, `@user:server.com`.

43 changes: 41 additions & 2 deletions synapse/handlers/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,16 @@

import attr

from synapse.api.constants import Direction, Membership
from synapse.api.constants import Direction, EventTypes, Membership
from synapse.events import EventBase
from synapse.types import JsonMapping, RoomStreamToken, StateMap, UserID, UserInfo
from synapse.types import (
JsonMapping,
Requester,
RoomStreamToken,
StateMap,
UserID,
UserInfo,
)
from synapse.visibility import filter_events_for_client

if TYPE_CHECKING:
Expand All @@ -43,6 +50,7 @@ def __init__(self, hs: "HomeServer"):
self._storage_controllers = hs.get_storage_controllers()
self._state_storage_controller = self._storage_controllers.state
self._msc3866_enabled = hs.config.experimental.msc3866.enabled
self.event_creation_handler = hs.get_event_creation_handler()

async def get_whois(self, user: UserID) -> JsonMapping:
connections = []
Expand Down Expand Up @@ -305,6 +313,37 @@ async def export_user_data(self, user_id: str, writer: "ExfiltrationWriter") ->

return writer.finished()

async def redact_events(
self, user_id: str, rooms: list, requester: Requester
) -> None:
"""
For a given set of rooms, redact all the events in those rooms sent by the user

Args:
user_id: user ID of the user whose events should be redacted
rooms: list of rooms to redact their events in
requester: the user requesting the redactions
"""
for room in rooms:
room_version = await self._store.get_room_version(room)
events = await self._store.get_events_sent_by_user(user_id, room)

for event in events:
event_dict = {
"type": EventTypes.Redaction,
"content": {},
"room_id": room,
"sender": requester.user.to_string(),
}
if room_version.updated_redaction_rules:
event_dict["content"]["redacts"] = event[0]
else:
event_dict["redacts"] = event[0]

await self.event_creation_handler.create_and_send_nonmember_event(
requester, event_dict
)


class ExfiltrationWriter(metaclass=abc.ABCMeta):
"""Interface used to specify how to write exported data."""
Expand Down
2 changes: 2 additions & 0 deletions synapse/rest/admin/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@
DeactivateAccountRestServlet,
PushersRestServlet,
RateLimitRestServlet,
RedactUser,
ResetPasswordRestServlet,
SearchUsersRestServlet,
ShadowBanRestServlet,
Expand Down Expand Up @@ -319,6 +320,7 @@ def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None:
UserReplaceMasterCrossSigningKeyRestServlet(hs).register(http_server)
UserByExternalId(hs).register(http_server)
UserByThreePid(hs).register(http_server)
RedactUser(hs).register(http_server)

DeviceRestServlet(hs).register(http_server)
DevicesRestServlet(hs).register(http_server)
Expand Down
30 changes: 30 additions & 0 deletions synapse/rest/admin/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -1410,3 +1410,33 @@ async def on_GET(
raise NotFoundError("User not found")

return HTTPStatus.OK, {"user_id": user_id}


class RedactUser(RestServlet):
"""
Redact all the events of a given user in the given rooms or if empty dict is provided
then all events in all rooms user is member of
"""

PATTERNS = admin_patterns("/user/(?P<user_id>[^/]*)/redact")

def __init__(self, hs: "HomeServer"):
self._auth = hs.get_auth()
self._store = hs.get_datastores().main
self.admin_handler = hs.get_admin_handler()

async def on_POST(
self, request: SynapseRequest, user_id: str
) -> Tuple[int, JsonDict]:
requester = await self._auth.get_user_by_req(request)
await assert_user_is_admin(self._auth, requester)
Comment thread
anoadragon453 marked this conversation as resolved.

body = parse_json_object_from_request(request, allow_empty_body=True)
rooms = body["rooms"]
Comment thread
H-Shay marked this conversation as resolved.
Outdated

if rooms == {}:
Comment thread
H-Shay marked this conversation as resolved.
Outdated
rooms = await self._store.get_rooms_for_user(user_id)

await self.admin_handler.redact_events(user_id, rooms, requester)

return HTTPStatus.OK, {}
21 changes: 21 additions & 0 deletions synapse/storage/databases/main/events_worker.py
Original file line number Diff line number Diff line change
Expand Up @@ -2439,3 +2439,24 @@ def mark_event_rejected_txn(
)

self.invalidate_get_event_cache_after_txn(txn, event_id)

async def get_events_sent_by_user(self, user_id: str, room_id: str) -> List[tuple]:
"""
Get a list of event ids of events sent by user in room
"""

def _get_events_by_user_txn(
Comment thread
H-Shay marked this conversation as resolved.
Outdated
txn: LoggingTransaction, user_id: str, room_id: str
) -> List[tuple]:
return self.db_pool.simple_select_many_txn(
txn,
"events",
Comment thread
anoadragon453 marked this conversation as resolved.
Outdated
"sender",
Comment thread
H-Shay marked this conversation as resolved.
Outdated
[user_id],
{"room_id": room_id},
retcols=["event_id"],
)

return await self.db_pool.runInteraction(
"get_events_by_user", _get_events_by_user_txn, user_id, room_id
)
Comment thread
anoadragon453 marked this conversation as resolved.
Outdated