Skip to content
This repository was archived by the owner on Apr 26, 2024. It is now read-only.

Commit b6f4d12

Browse files
David Robertsondklimpelanoadragon453
authored
Allow admins to proactively block rooms (#11228)
Co-authored-by: Dirk Klimpel <[email protected]> Co-authored-by: Andrew Morgan <[email protected]>
1 parent a19d01c commit b6f4d12

File tree

6 files changed

+103
-21
lines changed

6 files changed

+103
-21
lines changed

Diff for: changelog.d/11228.feature

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Allow the admin [Delete Room API](https://matrix-org.github.io/synapse/latest/admin_api/rooms.html#delete-room-api) to block a room without the need to join it.

Diff for: docs/admin_api/rooms.md

+11-5
Original file line numberDiff line numberDiff line change
@@ -396,13 +396,17 @@ The new room will be created with the user specified by the `new_room_user_id` p
396396
as room administrator and will contain a message explaining what happened. Users invited
397397
to the new room will have power level `-10` by default, and thus be unable to speak.
398398

399-
If `block` is `True` it prevents new joins to the old room.
399+
If `block` is `true`, users will be prevented from joining the old room.
400+
This option can also be used to pre-emptively block a room, even if it's unknown
401+
to this homeserver. In this case, the room will be blocked, and no further action
402+
will be taken. If `block` is `false`, attempting to delete an unknown room is
403+
invalid and will be rejected as a bad request.
400404

401405
This API will remove all trace of the old room from your database after removing
402406
all local users. If `purge` is `true` (the default), all traces of the old room will
403407
be removed from your database after removing all local users. If you do not want
404408
this to happen, set `purge` to `false`.
405-
Depending on the amount of history being purged a call to the API may take
409+
Depending on the amount of history being purged, a call to the API may take
406410
several minutes or longer.
407411

408412
The local server will only have the power to move local user and room aliases to
@@ -464,8 +468,9 @@ The following JSON body parameters are available:
464468
`new_room_user_id` in the new room. Ideally this will clearly convey why the
465469
original room was shut down. Defaults to `Sharing illegal content on this server
466470
is not permitted and rooms in violation will be blocked.`
467-
* `block` - Optional. If set to `true`, this room will be added to a blocking list, preventing
468-
future attempts to join the room. Defaults to `false`.
471+
* `block` - Optional. If set to `true`, this room will be added to a blocking list,
472+
preventing future attempts to join the room. Rooms can be blocked
473+
even if they're not yet known to the homeserver. Defaults to `false`.
469474
* `purge` - Optional. If set to `true`, it will remove all traces of the room from your database.
470475
Defaults to `true`.
471476
* `force_purge` - Optional, and ignored unless `purge` is `true`. If set to `true`, it
@@ -483,7 +488,8 @@ The following fields are returned in the JSON response body:
483488
* `failed_to_kick_users` - An array of users (`user_id`) that that were not kicked.
484489
* `local_aliases` - An array of strings representing the local aliases that were migrated from
485490
the old room to the new.
486-
* `new_room_id` - A string representing the room ID of the new room.
491+
* `new_room_id` - A string representing the room ID of the new room, or `null` if
492+
no such room was created.
487493

488494

489495
## Undoing room deletions

Diff for: synapse/handlers/room.py

+40-11
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,7 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15-
"""Contains functions for performing events on rooms."""
16-
15+
"""Contains functions for performing actions on rooms."""
1716
import itertools
1817
import logging
1918
import math
@@ -31,6 +30,8 @@
3130
Tuple,
3231
)
3332

33+
from typing_extensions import TypedDict
34+
3435
from synapse.api.constants import (
3536
EventContentFields,
3637
EventTypes,
@@ -1277,6 +1278,13 @@ def get_current_key_for_room(self, room_id: str) -> Awaitable[str]:
12771278
return self.store.get_room_events_max_id(room_id)
12781279

12791280

1281+
class ShutdownRoomResponse(TypedDict):
1282+
kicked_users: List[str]
1283+
failed_to_kick_users: List[str]
1284+
local_aliases: List[str]
1285+
new_room_id: Optional[str]
1286+
1287+
12801288
class RoomShutdownHandler:
12811289

12821290
DEFAULT_MESSAGE = (
@@ -1302,7 +1310,7 @@ async def shutdown_room(
13021310
new_room_name: Optional[str] = None,
13031311
message: Optional[str] = None,
13041312
block: bool = False,
1305-
) -> dict:
1313+
) -> ShutdownRoomResponse:
13061314
"""
13071315
Shuts down a room. Moves all local users and room aliases automatically
13081316
to a new room if `new_room_user_id` is set. Otherwise local users only
@@ -1336,8 +1344,13 @@ async def shutdown_room(
13361344
Defaults to `Sharing illegal content on this server is not
13371345
permitted and rooms in violation will be blocked.`
13381346
block:
1339-
If set to `true`, this room will be added to a blocking list,
1340-
preventing future attempts to join the room. Defaults to `false`.
1347+
If set to `True`, users will be prevented from joining the old
1348+
room. This option can also be used to pre-emptively block a room,
1349+
even if it's unknown to this homeserver. In this case, the room
1350+
will be blocked, and no further action will be taken. If `False`,
1351+
attempting to delete an unknown room is invalid.
1352+
1353+
Defaults to `False`.
13411354
13421355
Returns: a dict containing the following keys:
13431356
kicked_users: An array of users (`user_id`) that were kicked.
@@ -1346,7 +1359,9 @@ async def shutdown_room(
13461359
local_aliases:
13471360
An array of strings representing the local aliases that were
13481361
migrated from the old room to the new.
1349-
new_room_id: A string representing the room ID of the new room.
1362+
new_room_id:
1363+
A string representing the room ID of the new room, or None if
1364+
no such room was created.
13501365
"""
13511366

13521367
if not new_room_name:
@@ -1357,14 +1372,28 @@ async def shutdown_room(
13571372
if not RoomID.is_valid(room_id):
13581373
raise SynapseError(400, "%s is not a legal room ID" % (room_id,))
13591374

1360-
if not await self.store.get_room(room_id):
1361-
raise NotFoundError("Unknown room id %s" % (room_id,))
1362-
1363-
# This will work even if the room is already blocked, but that is
1364-
# desirable in case the first attempt at blocking the room failed below.
1375+
# Action the block first (even if the room doesn't exist yet)
13651376
if block:
1377+
# This will work even if the room is already blocked, but that is
1378+
# desirable in case the first attempt at blocking the room failed below.
13661379
await self.store.block_room(room_id, requester_user_id)
13671380

1381+
if not await self.store.get_room(room_id):
1382+
if block:
1383+
# We allow you to block an unknown room.
1384+
return {
1385+
"kicked_users": [],
1386+
"failed_to_kick_users": [],
1387+
"local_aliases": [],
1388+
"new_room_id": None,
1389+
}
1390+
else:
1391+
# But if you don't want to preventatively block another room,
1392+
# this function can't do anything useful.
1393+
raise NotFoundError(
1394+
"Cannot shut down room: unknown room id %s" % (room_id,)
1395+
)
1396+
13681397
if new_room_user_id is not None:
13691398
if not self.hs.is_mine_id(new_room_user_id):
13701399
raise SynapseError(

Diff for: synapse/rest/admin/rooms.py

+17-4
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
# limitations under the License.
1414
import logging
1515
from http import HTTPStatus
16-
from typing import TYPE_CHECKING, List, Optional, Tuple
16+
from typing import TYPE_CHECKING, List, Optional, Tuple, cast
1717
from urllib import parse as urlparse
1818

1919
from synapse.api.constants import EventTypes, JoinRules, Membership
@@ -239,9 +239,22 @@ async def _delete_room(
239239

240240
# Purge room
241241
if purge:
242-
await pagination_handler.purge_room(room_id, force=force_purge)
243-
244-
return 200, ret
242+
try:
243+
await pagination_handler.purge_room(room_id, force=force_purge)
244+
except NotFoundError:
245+
if block:
246+
# We can block unknown rooms with this endpoint, in which case
247+
# a failed purge is expected.
248+
pass
249+
else:
250+
# But otherwise, we expect this purge to have succeeded.
251+
raise
252+
253+
# Cast safety: cast away the knowledge that this is a TypedDict.
254+
# See https://github.com/python/mypy/issues/4976#issuecomment-579883622
255+
# for some discussion on why this is necessary. Either way,
256+
# `ret` is an opaque dictionary blob as far as the rest of the app cares.
257+
return 200, cast(JsonDict, ret)
245258

246259

247260
class RoomMembersRestServlet(RestServlet):

Diff for: synapse/storage/databases/main/room.py

+6-1
Original file line numberDiff line numberDiff line change
@@ -1751,7 +1751,12 @@ def _get_event_reports_paginate_txn(txn):
17511751
)
17521752

17531753
async def block_room(self, room_id: str, user_id: str) -> None:
1754-
"""Marks the room as blocked. Can be called multiple times.
1754+
"""Marks the room as blocked.
1755+
1756+
Can be called multiple times (though we'll only track the last user to
1757+
block this room).
1758+
1759+
Can be called on a room unknown to this homeserver.
17551760
17561761
Args:
17571762
room_id: Room to block

Diff for: tests/rest/admin/test_room.py

+28
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,12 @@
1414

1515
import json
1616
import urllib.parse
17+
from http import HTTPStatus
1718
from typing import List, Optional
1819
from unittest.mock import Mock
1920

21+
from parameterized import parameterized
22+
2023
import synapse.rest.admin
2124
from synapse.api.constants import EventTypes, Membership
2225
from synapse.api.errors import Codes
@@ -281,6 +284,31 @@ def test_block_room_and_not_purge(self):
281284
self._is_blocked(self.room_id, expect=True)
282285
self._has_no_members(self.room_id)
283286

287+
@parameterized.expand([(True,), (False,)])
288+
def test_block_unknown_room(self, purge: bool) -> None:
289+
"""
290+
We can block an unknown room. In this case, the `purge` argument
291+
should be ignored.
292+
"""
293+
room_id = "!unknown:test"
294+
295+
# The room isn't already in the blocked rooms table
296+
self._is_blocked(room_id, expect=False)
297+
298+
# Request the room be blocked.
299+
channel = self.make_request(
300+
"DELETE",
301+
f"/_synapse/admin/v1/rooms/{room_id}",
302+
{"block": True, "purge": purge},
303+
access_token=self.admin_user_tok,
304+
)
305+
306+
# The room is now blocked.
307+
self.assertEqual(
308+
HTTPStatus.OK, int(channel.result["code"]), msg=channel.result["body"]
309+
)
310+
self._is_blocked(room_id)
311+
284312
def test_shutdown_room_consent(self):
285313
"""Test that we can shutdown rooms with local users who have not
286314
yet accepted the privacy policy. This used to fail when we tried to

0 commit comments

Comments
 (0)