Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add an Admin API endpoint to redact all a user's events #17506

Merged
merged 15 commits into from
Sep 18, 2024
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

{
H-Shay marked this conversation as resolved.
Show resolved Hide resolved
"rooms": [!roomid1, !roomid2]
H-Shay marked this conversation as resolved.
Show resolved Hide resolved
}
```
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,
H-Shay marked this conversation as resolved.
Show resolved Hide resolved
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)
anoadragon453 marked this conversation as resolved.
Show resolved Hide resolved

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

if rooms == {}:
H-Shay marked this conversation as resolved.
Show resolved Hide resolved
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(
H-Shay marked this conversation as resolved.
Show resolved Hide resolved
txn: LoggingTransaction, user_id: str, room_id: str
) -> List[tuple]:
return self.db_pool.simple_select_many_txn(
txn,
"events",
anoadragon453 marked this conversation as resolved.
Show resolved Hide resolved
"sender",
H-Shay marked this conversation as resolved.
Show resolved Hide resolved
[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
)
anoadragon453 marked this conversation as resolved.
Show resolved Hide resolved