Add an option to issue redactions as admin user on admin redaction endpoint (#18671)

Currently the [admin redaction
endpoint](https://element-hq.github.io/synapse/latest/admin_api/user_admin_api.html#redact-all-the-events-of-a-user)
defaults to puppeting the user being redacted. This PR adds an optional
param `use_admin`, which when provided issues the redactions as the
admin user instead.
This commit is contained in:
Shay
2025-07-21 09:40:45 -07:00
committed by GitHub
parent 8a4e2e826d
commit 11a11414c5
5 changed files with 80 additions and 9 deletions

View File

@@ -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).

View File

@@ -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
@@ -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
```
@@ -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
@@ -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._

View File

@@ -358,6 +358,7 @@ class AdminHandler:
user_id: str,
rooms: list,
requester: JsonMapping,
use_admin: bool,
reason: Optional[str],
limit: Optional[int],
) -> str:
@@ -368,6 +369,7 @@ class AdminHandler:
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
@@ -395,6 +397,7 @@ class AdminHandler:
"rooms": rooms,
"requester": requester,
"user_id": user_id,
"use_admin": use_admin,
"reason": reason,
"limit": limit,
},
@@ -426,9 +429,17 @@ class AdminHandler:
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(),
)

View File

@@ -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")
@@ -1428,6 +1428,7 @@ class RedactUser(RestServlet):
rooms: List[StrictStr]
reason: Optional[StrictStr]
limit: Optional[StrictInt]
use_admin: Optional[StrictBool]
async def on_POST(
self, request: SynapseRequest, user_id: str
@@ -1455,8 +1456,12 @@ class RedactUser(RestServlet):
)
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}

View File

@@ -5667,6 +5667,54 @@ class UserRedactionTestCase(unittest.HomeserverTestCase):
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 = [