Skip to content
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/18671.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add an option to issue redactions as admin user on via the [admin redaction endpoint](https://element-hq.github.io/synapse/latest/admin_api/user_admin_api.html#redact-all-the-events-of-a-user).
16 changes: 11 additions & 5 deletions docs/admin_api/user_admin_api.md
Original file line number Diff line number Diff line change
Expand Up @@ -1227,7 +1227,7 @@ See also the

## Controlling whether a user is shadow-banned

Shadow-banning is a useful tool for moderating malicious or egregiously abusive users.
Shadow-banning is a useful tool for moderating malicious or egregiously abusive users.
A shadow-banned users receives successful responses to their client-server API requests,
but the events are not propagated into rooms. This can be an effective tool as it
(hopefully) takes longer for the user to realise they are being moderated before
Expand Down Expand Up @@ -1464,8 +1464,11 @@ _Added in Synapse 1.72.0._

## Redact all the events of a user

This endpoint allows an admin to redact the events of a given user. There are no restrictions on redactions for a
local user. By default, we puppet the user who sent the message to redact it themselves. Redactions for non-local users are issued using the admin user, and will fail in rooms where the admin user is not admin/does not have the specified power level to issue redactions.
This endpoint allows an admin to redact the events of a given user. There are no restrictions on
redactions for a local user. By default, we puppet the user who sent the message to redact it themselves.
Redactions for non-local users are issued using the admin user, and will fail in rooms where the
admin user is not admin/does not have the specified power level to issue redactions. An option
is provided to override the default and allow the admin to issue the redactions in all cases.

The API is
```
Expand All @@ -1475,7 +1478,7 @@ POST /_synapse/admin/v1/user/$user_id/redact
"rooms": ["!roomid1", "!roomid2"]
}
```
If an empty list is provided as the key for `rooms`, all events in all the rooms the user is member of will be redacted,
If an empty list is provided as the key for `rooms`, all events in all the rooms the user is member of will be redacted,
otherwise all the events in the rooms provided in the request will be redacted.

The API starts redaction process running, and returns immediately with a JSON body with
Expand All @@ -1501,7 +1504,10 @@ The following JSON body parameter must be provided:
The following JSON body parameters are optional:

- `reason` - Reason the redaction is being requested, ie "spam", "abuse", etc. This will be included in each redaction event, and be visible to users.
- `limit` - a limit on the number of the user's events to search for ones that can be redacted (events are redacted newest to oldest) in each room, defaults to 1000 if not provided
- `limit` - a limit on the number of the user's events to search for ones that can be redacted (events are redacted newest to oldest) in each room, defaults to 1000 if not provided.
- `use_admin` - If set to `true`, the admin user is used to issue the redactions, rather than puppeting the user. Useful
when the admin is also the moderator of the rooms that require redactions. Note that the redactions will fail in rooms
where the admin does not have the sufficient power level to issue the redactions.

_Added in Synapse 1.116.0._

Expand Down
15 changes: 13 additions & 2 deletions synapse/handlers/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,7 @@ async def start_redact_events(
user_id: str,
rooms: list,
requester: JsonMapping,
use_admin: bool,
reason: Optional[str],
limit: Optional[int],
) -> str:
Expand All @@ -368,6 +369,7 @@ async def start_redact_events(
user_id: the user ID of the user whose events should be redacted
rooms: the rooms in which to redact the user's events
requester: the user requesting the events
use_admin: whether to use the admin account to issue the redactions
reason: reason for requesting the redaction, ie spam, etc
limit: limit on the number of events in each room to redact

Expand Down Expand Up @@ -395,6 +397,7 @@ async def start_redact_events(
"rooms": rooms,
"requester": requester,
"user_id": user_id,
"use_admin": use_admin,
"reason": reason,
"limit": limit,
},
Expand Down Expand Up @@ -426,9 +429,17 @@ async def _redact_all_events(
user_id = task.params.get("user_id")
assert user_id is not None

# puppet the user if they're ours, otherwise use admin to redact
use_admin = task.params.get("use_admin", False)

# default to puppeting the user unless they are not local or it's been requested to
# use the admin user to issue the redactions
requester_id = (
admin.user.to_string()
if use_admin or not self.hs.is_mine_id(user_id)
else user_id
)
requester = create_requester(
user_id if self.hs.is_mine_id(user_id) else admin.user.to_string(),
requester_id,
authenticated_entity=admin.user.to_string(),
)

Expand Down
9 changes: 7 additions & 2 deletions synapse/rest/admin/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -1414,7 +1414,7 @@ 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. Kicks off a background process and
returns an id that can be used to check on the progress of the redaction progress
returns an id that can be used to check on the progress of the redaction progress.
"""

PATTERNS = admin_patterns("/user/(?P<user_id>[^/]*)/redact")
Expand All @@ -1428,6 +1428,7 @@ class PostBody(RequestBodyModel):
rooms: List[StrictStr]
reason: Optional[StrictStr]
limit: Optional[StrictInt]
use_admin: Optional[StrictBool]

async def on_POST(
self, request: SynapseRequest, user_id: str
Expand Down Expand Up @@ -1455,8 +1456,12 @@ async def on_POST(
)
rooms = current_rooms + banned_rooms

use_admin = body.use_admin
if not use_admin:
use_admin = False

redact_id = await self.admin_handler.start_redact_events(
user_id, rooms, requester.serialize(), body.reason, limit
user_id, rooms, requester.serialize(), use_admin, body.reason, limit
)

return HTTPStatus.OK, {"redact_id": redact_id}
Expand Down
48 changes: 48 additions & 0 deletions tests/rest/admin/test_user.py
Original file line number Diff line number Diff line change
Expand Up @@ -5667,6 +5667,54 @@ def test_redact_redacts_encrypted_messages(self) -> None:
matched.append(event_id)
self.assertEqual(len(matched), len(originals))

def test_use_admin_param_for_redactions(self) -> None:
"""
Test that if the `use_admin` param is set to true, the admin user is used to issue
the redactions and that they succeed in a room where the admin user has sufficient
power to issue redactions
"""

originals = []
join = self.helper.join(self.rm1, self.bad_user, tok=self.bad_user_tok)
originals.append(join["event_id"])
for i in range(15):
event = {"body": f"hello{i}", "msgtype": "m.text"}
res = self.helper.send_event(
self.rm1, "m.room.message", event, tok=self.bad_user_tok
)
originals.append(res["event_id"])

# redact messages
channel = self.make_request(
"POST",
f"/_synapse/admin/v1/user/{self.bad_user}/redact",
content={"rooms": [self.rm1], "use_admin": True},
access_token=self.admin_tok,
)
self.assertEqual(channel.code, 200)

# messages are redacted, and redactions are issued by the admin user
filter = json.dumps({"types": [EventTypes.Redaction]})
channel = self.make_request(
"GET",
f"rooms/{self.rm1}/messages?filter={filter}&limit=50",
access_token=self.admin_tok,
)
self.assertEqual(channel.code, 200)

matches = []
for event in channel.json_body["chunk"]:
for event_id in originals:
if event["type"] == "m.room.redaction" and event["redacts"] == event_id:
matches.append((event_id, event))
# we redacted 16 messages
self.assertEqual(len(matches), 16)

for redaction_tuple in matches:
redaction = redaction_tuple[1]
if redaction["sender"] != self.admin:
self.fail("Redaction was not issued by admin account")


class UserRedactionBackgroundTaskTestCase(BaseMultiWorkerStreamTestCase):
servlets = [
Expand Down
Loading