forked from MirrorHub/synapse
Allow admins to proactively block rooms (#11228)
Co-authored-by: Dirk Klimpel <5740567+dklimpel@users.noreply.github.com> Co-authored-by: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com>
This commit is contained in:
parent
a19d01c3d9
commit
b6f4d122ef
6 changed files with 102 additions and 20 deletions
1
changelog.d/11228.feature
Normal file
1
changelog.d/11228.feature
Normal file
|
@ -0,0 +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.
|
|
@ -396,13 +396,17 @@ The new room will be created with the user specified by the `new_room_user_id` p
|
||||||
as room administrator and will contain a message explaining what happened. Users invited
|
as room administrator and will contain a message explaining what happened. Users invited
|
||||||
to the new room will have power level `-10` by default, and thus be unable to speak.
|
to the new room will have power level `-10` by default, and thus be unable to speak.
|
||||||
|
|
||||||
If `block` is `True` it prevents new joins to the old room.
|
If `block` is `true`, users will be prevented from joining the old room.
|
||||||
|
This option can also be used to pre-emptively block a room, even if it's unknown
|
||||||
|
to this homeserver. In this case, the room will be blocked, and no further action
|
||||||
|
will be taken. If `block` is `false`, attempting to delete an unknown room is
|
||||||
|
invalid and will be rejected as a bad request.
|
||||||
|
|
||||||
This API will remove all trace of the old room from your database after removing
|
This API will remove all trace of the old room from your database after removing
|
||||||
all local users. If `purge` is `true` (the default), all traces of the old room will
|
all local users. If `purge` is `true` (the default), all traces of the old room will
|
||||||
be removed from your database after removing all local users. If you do not want
|
be removed from your database after removing all local users. If you do not want
|
||||||
this to happen, set `purge` to `false`.
|
this to happen, set `purge` to `false`.
|
||||||
Depending on the amount of history being purged a call to the API may take
|
Depending on the amount of history being purged, a call to the API may take
|
||||||
several minutes or longer.
|
several minutes or longer.
|
||||||
|
|
||||||
The local server will only have the power to move local user and room aliases to
|
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:
|
||||||
`new_room_user_id` in the new room. Ideally this will clearly convey why the
|
`new_room_user_id` in the new room. Ideally this will clearly convey why the
|
||||||
original room was shut down. Defaults to `Sharing illegal content on this server
|
original room was shut down. Defaults to `Sharing illegal content on this server
|
||||||
is not permitted and rooms in violation will be blocked.`
|
is not permitted and rooms in violation will be blocked.`
|
||||||
* `block` - Optional. If set to `true`, this room will be added to a blocking list, preventing
|
* `block` - Optional. If set to `true`, this room will be added to a blocking list,
|
||||||
future attempts to join the room. Defaults to `false`.
|
preventing future attempts to join the room. Rooms can be blocked
|
||||||
|
even if they're not yet known to the homeserver. Defaults to `false`.
|
||||||
* `purge` - Optional. If set to `true`, it will remove all traces of the room from your database.
|
* `purge` - Optional. If set to `true`, it will remove all traces of the room from your database.
|
||||||
Defaults to `true`.
|
Defaults to `true`.
|
||||||
* `force_purge` - Optional, and ignored unless `purge` is `true`. If set to `true`, it
|
* `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:
|
||||||
* `failed_to_kick_users` - An array of users (`user_id`) that that were not kicked.
|
* `failed_to_kick_users` - An array of users (`user_id`) that that were not kicked.
|
||||||
* `local_aliases` - An array of strings representing the local aliases that were migrated from
|
* `local_aliases` - An array of strings representing the local aliases that were migrated from
|
||||||
the old room to the new.
|
the old room to the new.
|
||||||
* `new_room_id` - A string representing the room ID of the new room.
|
* `new_room_id` - A string representing the room ID of the new room, or `null` if
|
||||||
|
no such room was created.
|
||||||
|
|
||||||
|
|
||||||
## Undoing room deletions
|
## Undoing room deletions
|
||||||
|
|
|
@ -12,8 +12,7 @@
|
||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
"""Contains functions for performing events on rooms."""
|
"""Contains functions for performing actions on rooms."""
|
||||||
|
|
||||||
import itertools
|
import itertools
|
||||||
import logging
|
import logging
|
||||||
import math
|
import math
|
||||||
|
@ -31,6 +30,8 @@ from typing import (
|
||||||
Tuple,
|
Tuple,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from typing_extensions import TypedDict
|
||||||
|
|
||||||
from synapse.api.constants import (
|
from synapse.api.constants import (
|
||||||
EventContentFields,
|
EventContentFields,
|
||||||
EventTypes,
|
EventTypes,
|
||||||
|
@ -1277,6 +1278,13 @@ class RoomEventSource(EventSource[RoomStreamToken, EventBase]):
|
||||||
return self.store.get_room_events_max_id(room_id)
|
return self.store.get_room_events_max_id(room_id)
|
||||||
|
|
||||||
|
|
||||||
|
class ShutdownRoomResponse(TypedDict):
|
||||||
|
kicked_users: List[str]
|
||||||
|
failed_to_kick_users: List[str]
|
||||||
|
local_aliases: List[str]
|
||||||
|
new_room_id: Optional[str]
|
||||||
|
|
||||||
|
|
||||||
class RoomShutdownHandler:
|
class RoomShutdownHandler:
|
||||||
|
|
||||||
DEFAULT_MESSAGE = (
|
DEFAULT_MESSAGE = (
|
||||||
|
@ -1302,7 +1310,7 @@ class RoomShutdownHandler:
|
||||||
new_room_name: Optional[str] = None,
|
new_room_name: Optional[str] = None,
|
||||||
message: Optional[str] = None,
|
message: Optional[str] = None,
|
||||||
block: bool = False,
|
block: bool = False,
|
||||||
) -> dict:
|
) -> ShutdownRoomResponse:
|
||||||
"""
|
"""
|
||||||
Shuts down a room. Moves all local users and room aliases automatically
|
Shuts down a room. Moves all local users and room aliases automatically
|
||||||
to a new room if `new_room_user_id` is set. Otherwise local users only
|
to a new room if `new_room_user_id` is set. Otherwise local users only
|
||||||
|
@ -1336,8 +1344,13 @@ class RoomShutdownHandler:
|
||||||
Defaults to `Sharing illegal content on this server is not
|
Defaults to `Sharing illegal content on this server is not
|
||||||
permitted and rooms in violation will be blocked.`
|
permitted and rooms in violation will be blocked.`
|
||||||
block:
|
block:
|
||||||
If set to `true`, this room will be added to a blocking list,
|
If set to `True`, users will be prevented from joining the old
|
||||||
preventing future attempts to join the room. Defaults to `false`.
|
room. This option can also be used to pre-emptively block a room,
|
||||||
|
even if it's unknown to this homeserver. In this case, the room
|
||||||
|
will be blocked, and no further action will be taken. If `False`,
|
||||||
|
attempting to delete an unknown room is invalid.
|
||||||
|
|
||||||
|
Defaults to `False`.
|
||||||
|
|
||||||
Returns: a dict containing the following keys:
|
Returns: a dict containing the following keys:
|
||||||
kicked_users: An array of users (`user_id`) that were kicked.
|
kicked_users: An array of users (`user_id`) that were kicked.
|
||||||
|
@ -1346,7 +1359,9 @@ class RoomShutdownHandler:
|
||||||
local_aliases:
|
local_aliases:
|
||||||
An array of strings representing the local aliases that were
|
An array of strings representing the local aliases that were
|
||||||
migrated from the old room to the new.
|
migrated from the old room to the new.
|
||||||
new_room_id: A string representing the room ID of the new room.
|
new_room_id:
|
||||||
|
A string representing the room ID of the new room, or None if
|
||||||
|
no such room was created.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if not new_room_name:
|
if not new_room_name:
|
||||||
|
@ -1357,14 +1372,28 @@ class RoomShutdownHandler:
|
||||||
if not RoomID.is_valid(room_id):
|
if not RoomID.is_valid(room_id):
|
||||||
raise SynapseError(400, "%s is not a legal room ID" % (room_id,))
|
raise SynapseError(400, "%s is not a legal room ID" % (room_id,))
|
||||||
|
|
||||||
if not await self.store.get_room(room_id):
|
# Action the block first (even if the room doesn't exist yet)
|
||||||
raise NotFoundError("Unknown room id %s" % (room_id,))
|
if block:
|
||||||
|
|
||||||
# This will work even if the room is already blocked, but that is
|
# This will work even if the room is already blocked, but that is
|
||||||
# desirable in case the first attempt at blocking the room failed below.
|
# desirable in case the first attempt at blocking the room failed below.
|
||||||
if block:
|
|
||||||
await self.store.block_room(room_id, requester_user_id)
|
await self.store.block_room(room_id, requester_user_id)
|
||||||
|
|
||||||
|
if not await self.store.get_room(room_id):
|
||||||
|
if block:
|
||||||
|
# We allow you to block an unknown room.
|
||||||
|
return {
|
||||||
|
"kicked_users": [],
|
||||||
|
"failed_to_kick_users": [],
|
||||||
|
"local_aliases": [],
|
||||||
|
"new_room_id": None,
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
# But if you don't want to preventatively block another room,
|
||||||
|
# this function can't do anything useful.
|
||||||
|
raise NotFoundError(
|
||||||
|
"Cannot shut down room: unknown room id %s" % (room_id,)
|
||||||
|
)
|
||||||
|
|
||||||
if new_room_user_id is not None:
|
if new_room_user_id is not None:
|
||||||
if not self.hs.is_mine_id(new_room_user_id):
|
if not self.hs.is_mine_id(new_room_user_id):
|
||||||
raise SynapseError(
|
raise SynapseError(
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
import logging
|
import logging
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
from typing import TYPE_CHECKING, List, Optional, Tuple
|
from typing import TYPE_CHECKING, List, Optional, Tuple, cast
|
||||||
from urllib import parse as urlparse
|
from urllib import parse as urlparse
|
||||||
|
|
||||||
from synapse.api.constants import EventTypes, JoinRules, Membership
|
from synapse.api.constants import EventTypes, JoinRules, Membership
|
||||||
|
@ -239,9 +239,22 @@ class RoomRestServlet(RestServlet):
|
||||||
|
|
||||||
# Purge room
|
# Purge room
|
||||||
if purge:
|
if purge:
|
||||||
|
try:
|
||||||
await pagination_handler.purge_room(room_id, force=force_purge)
|
await pagination_handler.purge_room(room_id, force=force_purge)
|
||||||
|
except NotFoundError:
|
||||||
|
if block:
|
||||||
|
# We can block unknown rooms with this endpoint, in which case
|
||||||
|
# a failed purge is expected.
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
# But otherwise, we expect this purge to have succeeded.
|
||||||
|
raise
|
||||||
|
|
||||||
return 200, ret
|
# Cast safety: cast away the knowledge that this is a TypedDict.
|
||||||
|
# See https://github.com/python/mypy/issues/4976#issuecomment-579883622
|
||||||
|
# for some discussion on why this is necessary. Either way,
|
||||||
|
# `ret` is an opaque dictionary blob as far as the rest of the app cares.
|
||||||
|
return 200, cast(JsonDict, ret)
|
||||||
|
|
||||||
|
|
||||||
class RoomMembersRestServlet(RestServlet):
|
class RoomMembersRestServlet(RestServlet):
|
||||||
|
|
|
@ -1751,7 +1751,12 @@ class RoomStore(RoomBackgroundUpdateStore, RoomWorkerStore, SearchStore):
|
||||||
)
|
)
|
||||||
|
|
||||||
async def block_room(self, room_id: str, user_id: str) -> None:
|
async def block_room(self, room_id: str, user_id: str) -> None:
|
||||||
"""Marks the room as blocked. Can be called multiple times.
|
"""Marks the room as blocked.
|
||||||
|
|
||||||
|
Can be called multiple times (though we'll only track the last user to
|
||||||
|
block this room).
|
||||||
|
|
||||||
|
Can be called on a room unknown to this homeserver.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
room_id: Room to block
|
room_id: Room to block
|
||||||
|
|
|
@ -14,9 +14,12 @@
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
|
from http import HTTPStatus
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
from unittest.mock import Mock
|
from unittest.mock import Mock
|
||||||
|
|
||||||
|
from parameterized import parameterized
|
||||||
|
|
||||||
import synapse.rest.admin
|
import synapse.rest.admin
|
||||||
from synapse.api.constants import EventTypes, Membership
|
from synapse.api.constants import EventTypes, Membership
|
||||||
from synapse.api.errors import Codes
|
from synapse.api.errors import Codes
|
||||||
|
@ -281,6 +284,31 @@ class DeleteRoomTestCase(unittest.HomeserverTestCase):
|
||||||
self._is_blocked(self.room_id, expect=True)
|
self._is_blocked(self.room_id, expect=True)
|
||||||
self._has_no_members(self.room_id)
|
self._has_no_members(self.room_id)
|
||||||
|
|
||||||
|
@parameterized.expand([(True,), (False,)])
|
||||||
|
def test_block_unknown_room(self, purge: bool) -> None:
|
||||||
|
"""
|
||||||
|
We can block an unknown room. In this case, the `purge` argument
|
||||||
|
should be ignored.
|
||||||
|
"""
|
||||||
|
room_id = "!unknown:test"
|
||||||
|
|
||||||
|
# The room isn't already in the blocked rooms table
|
||||||
|
self._is_blocked(room_id, expect=False)
|
||||||
|
|
||||||
|
# Request the room be blocked.
|
||||||
|
channel = self.make_request(
|
||||||
|
"DELETE",
|
||||||
|
f"/_synapse/admin/v1/rooms/{room_id}",
|
||||||
|
{"block": True, "purge": purge},
|
||||||
|
access_token=self.admin_user_tok,
|
||||||
|
)
|
||||||
|
|
||||||
|
# The room is now blocked.
|
||||||
|
self.assertEqual(
|
||||||
|
HTTPStatus.OK, int(channel.result["code"]), msg=channel.result["body"]
|
||||||
|
)
|
||||||
|
self._is_blocked(room_id)
|
||||||
|
|
||||||
def test_shutdown_room_consent(self):
|
def test_shutdown_room_consent(self):
|
||||||
"""Test that we can shutdown rooms with local users who have not
|
"""Test that we can shutdown rooms with local users who have not
|
||||||
yet accepted the privacy policy. This used to fail when we tried to
|
yet accepted the privacy policy. This used to fail when we tried to
|
||||||
|
|
Loading…
Reference in a new issue