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

Commit

Permalink
Allow admins to proactively block rooms (#11228)
Browse files Browse the repository at this point in the history
Co-authored-by: Dirk Klimpel <5740567+dklimpel@users.noreply.github.com>
Co-authored-by: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com>
  • Loading branch information
3 people authored Nov 9, 2021
1 parent a19d01c commit b6f4d12
Show file tree
Hide file tree
Showing 6 changed files with 103 additions and 21 deletions.
1 change: 1 addition & 0 deletions changelog.d/11228.feature
Original file line number Diff line number Diff line change
@@ -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.
16 changes: 11 additions & 5 deletions docs/admin_api/rooms.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
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
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
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.

The local server will only have the power to move local user and room aliases to
Expand Down Expand Up @@ -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
original room was shut down. Defaults to `Sharing illegal content on this server
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
future attempts to join the room. Defaults to `false`.
* `block` - Optional. If set to `true`, this room will be added to a blocking list,
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.
Defaults to `true`.
* `force_purge` - Optional, and ignored unless `purge` is `true`. If set to `true`, it
Expand All @@ -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.
* `local_aliases` - An array of strings representing the local aliases that were 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 `null` if
no such room was created.


## Undoing room deletions
Expand Down
51 changes: 40 additions & 11 deletions synapse/handlers/room.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.

"""Contains functions for performing events on rooms."""

"""Contains functions for performing actions on rooms."""
import itertools
import logging
import math
Expand All @@ -31,6 +30,8 @@
Tuple,
)

from typing_extensions import TypedDict

from synapse.api.constants import (
EventContentFields,
EventTypes,
Expand Down Expand Up @@ -1277,6 +1278,13 @@ def get_current_key_for_room(self, room_id: str) -> Awaitable[str]:
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:

DEFAULT_MESSAGE = (
Expand All @@ -1302,7 +1310,7 @@ async def shutdown_room(
new_room_name: Optional[str] = None,
message: Optional[str] = None,
block: bool = False,
) -> dict:
) -> ShutdownRoomResponse:
"""
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
Expand Down Expand Up @@ -1336,8 +1344,13 @@ async def shutdown_room(
Defaults to `Sharing illegal content on this server is not
permitted and rooms in violation will be blocked.`
block:
If set to `true`, this room will be added to a blocking list,
preventing future attempts to join the room. Defaults to `false`.
If set to `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 `False`,
attempting to delete an unknown room is invalid.
Defaults to `False`.
Returns: a dict containing the following keys:
kicked_users: An array of users (`user_id`) that were kicked.
Expand All @@ -1346,7 +1359,9 @@ async def shutdown_room(
local_aliases:
An array of strings representing the local aliases that were
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:
Expand All @@ -1357,14 +1372,28 @@ async def shutdown_room(
if not RoomID.is_valid(room_id):
raise SynapseError(400, "%s is not a legal room ID" % (room_id,))

if not await self.store.get_room(room_id):
raise NotFoundError("Unknown room id %s" % (room_id,))

# 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.
# Action the block first (even if the room doesn't exist yet)
if block:
# 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.
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 not self.hs.is_mine_id(new_room_user_id):
raise SynapseError(
Expand Down
21 changes: 17 additions & 4 deletions synapse/rest/admin/rooms.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
# limitations under the License.
import logging
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 synapse.api.constants import EventTypes, JoinRules, Membership
Expand Down Expand Up @@ -239,9 +239,22 @@ async def _delete_room(

# Purge room
if purge:
await pagination_handler.purge_room(room_id, force=force_purge)

return 200, ret
try:
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

# 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):
Expand Down
7 changes: 6 additions & 1 deletion synapse/storage/databases/main/room.py
Original file line number Diff line number Diff line change
Expand Up @@ -1751,7 +1751,12 @@ def _get_event_reports_paginate_txn(txn):
)

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:
room_id: Room to block
Expand Down
28 changes: 28 additions & 0 deletions tests/rest/admin/test_room.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,12 @@

import json
import urllib.parse
from http import HTTPStatus
from typing import List, Optional
from unittest.mock import Mock

from parameterized import parameterized

import synapse.rest.admin
from synapse.api.constants import EventTypes, Membership
from synapse.api.errors import Codes
Expand Down Expand Up @@ -281,6 +284,31 @@ def test_block_room_and_not_purge(self):
self._is_blocked(self.room_id, expect=True)
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):
"""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
Expand Down

0 comments on commit b6f4d12

Please sign in to comment.