Skip to content

Commit a5986ac

Browse files
authored
Improvements to admin redact api (#17792)
- better validation on user input - fix an early task completion - when checking membership in rooms, check for rooms user has been banned from as well
1 parent 006251a commit a5986ac

File tree

5 files changed

+107
-41
lines changed

5 files changed

+107
-41
lines changed

changelog.d/17792.bugfix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Improve input validation and room membership checks in admin redaction API.

synapse/handlers/admin.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -443,8 +443,8 @@ async def _redact_all_events(
443443
["m.room.member", "m.room.message"],
444444
)
445445
if not event_ids:
446-
# there's nothing to redact
447-
return TaskStatus.COMPLETE, result, None
446+
# nothing to redact in this room
447+
continue
448448

449449
events = await self._store.get_events_as_list(event_ids)
450450
for event in events:

synapse/rest/admin/users.py

Lines changed: 22 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727

2828
import attr
2929

30-
from synapse._pydantic_compat import StrictBool
30+
from synapse._pydantic_compat import StrictBool, StrictInt, StrictStr
3131
from synapse.api.constants import Direction, UserTypes
3232
from synapse.api.errors import Codes, NotFoundError, SynapseError
3333
from synapse.http.servlet import (
@@ -1421,40 +1421,39 @@ def __init__(self, hs: "HomeServer"):
14211421
self._store = hs.get_datastores().main
14221422
self.admin_handler = hs.get_admin_handler()
14231423

1424+
class PostBody(RequestBodyModel):
1425+
rooms: List[StrictStr]
1426+
reason: Optional[StrictStr]
1427+
limit: Optional[StrictInt]
1428+
14241429
async def on_POST(
14251430
self, request: SynapseRequest, user_id: str
14261431
) -> Tuple[int, JsonDict]:
14271432
requester = await self._auth.get_user_by_req(request)
14281433
await assert_user_is_admin(self._auth, requester)
14291434

1430-
body = parse_json_object_from_request(request, allow_empty_body=True)
1431-
rooms = body.get("rooms")
1432-
if rooms is None:
1433-
raise SynapseError(
1434-
HTTPStatus.BAD_REQUEST, "Must provide a value for rooms."
1435-
)
1435+
# parse provided user id to check that it is valid
1436+
UserID.from_string(user_id)
14361437

1437-
reason = body.get("reason")
1438-
if reason:
1439-
if not isinstance(reason, str):
1440-
raise SynapseError(
1441-
HTTPStatus.BAD_REQUEST,
1442-
"If a reason is provided it must be a string.",
1443-
)
1438+
body = parse_and_validate_json_object_from_request(request, self.PostBody)
14441439

1445-
limit = body.get("limit")
1446-
if limit:
1447-
if not isinstance(limit, int) or limit <= 0:
1448-
raise SynapseError(
1449-
HTTPStatus.BAD_REQUEST,
1450-
"If limit is provided it must be a non-negative integer greater than 0.",
1451-
)
1440+
limit = body.limit
1441+
if limit and limit <= 0:
1442+
raise SynapseError(
1443+
HTTPStatus.BAD_REQUEST,
1444+
"If limit is provided it must be a non-negative integer greater than 0.",
1445+
)
14521446

1447+
rooms = body.rooms
14531448
if not rooms:
1454-
rooms = await self._store.get_rooms_for_user(user_id)
1449+
current_rooms = list(await self._store.get_rooms_for_user(user_id))
1450+
banned_rooms = list(
1451+
await self._store.get_rooms_user_currently_banned_from(user_id)
1452+
)
1453+
rooms = current_rooms + banned_rooms
14551454

14561455
redact_id = await self.admin_handler.start_redact_events(
1457-
user_id, list(rooms), requester.serialize(), reason, limit
1456+
user_id, rooms, requester.serialize(), body.reason, limit
14581457
)
14591458

14601459
return HTTPStatus.OK, {"redact_id": redact_id}

synapse/storage/databases/main/roommember.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -711,6 +711,27 @@ def get_users_server_still_shares_room_with_txn(
711711

712712
return {row[0] for row in txn}
713713

714+
async def get_rooms_user_currently_banned_from(
715+
self, user_id: str
716+
) -> FrozenSet[str]:
717+
"""Returns a set of room_ids the user is currently banned from.
718+
719+
If a remote user only returns rooms this server is currently
720+
participating in.
721+
"""
722+
room_ids = await self.db_pool.simple_select_onecol(
723+
table="current_state_events",
724+
keyvalues={
725+
"type": EventTypes.Member,
726+
"membership": Membership.BAN,
727+
"state_key": user_id,
728+
},
729+
retcol="room_id",
730+
desc="get_rooms_user_currently_banned_from",
731+
)
732+
733+
return frozenset(room_ids)
734+
714735
@cached(max_entries=500000, iterable=True)
715736
async def get_rooms_for_user(self, user_id: str) -> FrozenSet[str]:
716737
"""Returns a set of room_ids the user is currently joined to.

tests/rest/admin/test_user.py

Lines changed: 61 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5288,19 +5288,26 @@ async def check_event_for_spam(event: str) -> str:
52885288
self.assertEqual(len(matched), len(rm2_originals))
52895289

52905290
def test_admin_redact_works_if_user_kicked_or_banned(self) -> None:
5291-
originals = []
5291+
originals1 = []
5292+
originals2 = []
52925293
for rm in [self.rm1, self.rm2, self.rm3]:
52935294
join = self.helper.join(rm, self.bad_user, tok=self.bad_user_tok)
5294-
originals.append(join["event_id"])
5295+
if rm in [self.rm1, self.rm3]:
5296+
originals1.append(join["event_id"])
5297+
else:
5298+
originals2.append(join["event_id"])
52955299
for i in range(5):
52965300
event = {"body": f"hello{i}", "msgtype": "m.text"}
52975301
res = self.helper.send_event(
52985302
rm, "m.room.message", event, tok=self.bad_user_tok
52995303
)
5300-
originals.append(res["event_id"])
5304+
if rm in [self.rm1, self.rm3]:
5305+
originals1.append(res["event_id"])
5306+
else:
5307+
originals2.append(res["event_id"])
53015308

53025309
# kick user from rooms 1 and 3
5303-
for r in [self.rm1, self.rm2]:
5310+
for r in [self.rm1, self.rm3]:
53045311
channel = self.make_request(
53055312
"POST",
53065313
f"/_matrix/client/r0/rooms/{r}/kick",
@@ -5330,32 +5337,70 @@ def test_admin_redact_works_if_user_kicked_or_banned(self) -> None:
53305337
failed_redactions = channel2.json_body.get("failed_redactions")
53315338
self.assertEqual(failed_redactions, {})
53325339

5333-
# ban user
5334-
channel3 = self.make_request(
5340+
# double check
5341+
for rm in [self.rm1, self.rm3]:
5342+
filter = json.dumps({"types": [EventTypes.Redaction]})
5343+
channel3 = self.make_request(
5344+
"GET",
5345+
f"rooms/{rm}/messages?filter={filter}&limit=50",
5346+
access_token=self.admin_tok,
5347+
)
5348+
self.assertEqual(channel3.code, 200)
5349+
5350+
matches = []
5351+
for event in channel3.json_body["chunk"]:
5352+
for event_id in originals1:
5353+
if (
5354+
event["type"] == "m.room.redaction"
5355+
and event["redacts"] == event_id
5356+
):
5357+
matches.append((event_id, event))
5358+
# we redacted 6 messages
5359+
self.assertEqual(len(matches), 6)
5360+
5361+
# ban user from room 2
5362+
channel4 = self.make_request(
53355363
"POST",
53365364
f"/_matrix/client/r0/rooms/{self.rm2}/ban",
53375365
content={"reason": "being a bummer", "user_id": self.bad_user},
53385366
access_token=self.admin_tok,
53395367
)
5340-
self.assertEqual(channel3.code, HTTPStatus.OK, channel3.result)
5368+
self.assertEqual(channel4.code, HTTPStatus.OK, channel4.result)
53415369

5342-
# redact messages in room 2
5343-
channel4 = self.make_request(
5370+
# make a request to ban all user's messages
5371+
channel5 = self.make_request(
53445372
"POST",
53455373
f"/_synapse/admin/v1/user/{self.bad_user}/redact",
5346-
content={"rooms": [self.rm2]},
5374+
content={"rooms": []},
53475375
access_token=self.admin_tok,
53485376
)
5349-
self.assertEqual(channel4.code, 200)
5350-
id2 = channel1.json_body.get("redact_id")
5377+
self.assertEqual(channel5.code, 200)
5378+
id2 = channel5.json_body.get("redact_id")
53515379

53525380
# check that there were no failed redactions in room 2
5353-
channel5 = self.make_request(
5381+
channel6 = self.make_request(
53545382
"GET",
53555383
f"/_synapse/admin/v1/user/redact_status/{id2}",
53565384
access_token=self.admin_tok,
53575385
)
5358-
self.assertEqual(channel5.code, 200)
5359-
self.assertEqual(channel5.json_body.get("status"), "complete")
5360-
failed_redactions = channel5.json_body.get("failed_redactions")
5386+
self.assertEqual(channel6.code, 200)
5387+
self.assertEqual(channel6.json_body.get("status"), "complete")
5388+
failed_redactions = channel6.json_body.get("failed_redactions")
53615389
self.assertEqual(failed_redactions, {})
5390+
5391+
# double check messages in room 2 were redacted
5392+
filter = json.dumps({"types": [EventTypes.Redaction]})
5393+
channel7 = self.make_request(
5394+
"GET",
5395+
f"rooms/{self.rm2}/messages?filter={filter}&limit=50",
5396+
access_token=self.admin_tok,
5397+
)
5398+
self.assertEqual(channel7.code, 200)
5399+
5400+
matches = []
5401+
for event in channel7.json_body["chunk"]:
5402+
for event_id in originals2:
5403+
if event["type"] == "m.room.redaction" and event["redacts"] == event_id:
5404+
matches.append((event_id, event))
5405+
# we redacted 6 messages
5406+
self.assertEqual(len(matches), 6)

0 commit comments

Comments
 (0)