Add dedicated admin API for blocking a room (#11324)

This commit is contained in:
Dirk Klimpel 2021-11-18 18:43:49 +01:00 committed by GitHub
parent 5f81c0ce9c
commit 81b18fe5c0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 404 additions and 0 deletions

View File

@ -0,0 +1 @@
Add dedicated admin API for blocking a room.

View File

@ -3,6 +3,7 @@
- [Room Details API](#room-details-api) - [Room Details API](#room-details-api)
- [Room Members API](#room-members-api) - [Room Members API](#room-members-api)
- [Room State API](#room-state-api) - [Room State API](#room-state-api)
- [Block Room API](#block-room-api)
- [Delete Room API](#delete-room-api) - [Delete Room API](#delete-room-api)
* [Version 1 (old version)](#version-1-old-version) * [Version 1 (old version)](#version-1-old-version)
* [Version 2 (new version)](#version-2-new-version) * [Version 2 (new version)](#version-2-new-version)
@ -386,6 +387,83 @@ A response body like the following is returned:
} }
``` ```
# Block Room API
The Block Room admin API allows server admins to block and unblock rooms,
and query to see if a given room is blocked.
This API can be used to pre-emptively block a room, even if it's unknown to this
homeserver. Users will be prevented from joining a blocked room.
## Block or unblock a room
The API is:
```
PUT /_synapse/admin/v1/rooms/<room_id>/block
```
with a body of:
```json
{
"block": true
}
```
A response body like the following is returned:
```json
{
"block": true
}
```
**Parameters**
The following parameters should be set in the URL:
- `room_id` - The ID of the room.
The following JSON body parameters are available:
- `block` - If `true` the room will be blocked and if `false` the room will be unblocked.
**Response**
The following fields are possible in the JSON response body:
- `block` - A boolean. `true` if the room is blocked, otherwise `false`
## Get block status
The API is:
```
GET /_synapse/admin/v1/rooms/<room_id>/block
```
A response body like the following is returned:
```json
{
"block": true,
"user_id": "<user_id>"
}
```
**Parameters**
The following parameters should be set in the URL:
- `room_id` - The ID of the room.
**Response**
The following fields are possible in the JSON response body:
- `block` - A boolean. `true` if the room is blocked, otherwise `false`
- `user_id` - An optional string. If the room is blocked (`block` is `true`) shows
the user who has add the room to blocking list. Otherwise it is not displayed.
# Delete Room API # Delete Room API
The Delete Room admin API allows server admins to remove rooms from the server The Delete Room admin API allows server admins to remove rooms from the server

View File

@ -46,6 +46,7 @@ from synapse.rest.admin.registration_tokens import (
RegistrationTokenRestServlet, RegistrationTokenRestServlet,
) )
from synapse.rest.admin.rooms import ( from synapse.rest.admin.rooms import (
BlockRoomRestServlet,
DeleteRoomStatusByDeleteIdRestServlet, DeleteRoomStatusByDeleteIdRestServlet,
DeleteRoomStatusByRoomIdRestServlet, DeleteRoomStatusByRoomIdRestServlet,
ForwardExtremitiesRestServlet, ForwardExtremitiesRestServlet,
@ -223,6 +224,7 @@ def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None:
Register all the admin servlets. Register all the admin servlets.
""" """
register_servlets_for_client_rest_resource(hs, http_server) register_servlets_for_client_rest_resource(hs, http_server)
BlockRoomRestServlet(hs).register(http_server)
ListRoomRestServlet(hs).register(http_server) ListRoomRestServlet(hs).register(http_server)
RoomStateRestServlet(hs).register(http_server) RoomStateRestServlet(hs).register(http_server)
RoomRestServlet(hs).register(http_server) RoomRestServlet(hs).register(http_server)

View File

@ -782,3 +782,66 @@ class RoomEventContextServlet(RestServlet):
) )
return 200, results return 200, results
class BlockRoomRestServlet(RestServlet):
"""
Manage blocking of rooms.
On PUT: Add or remove a room from blocking list.
On GET: Get blocking status of room and user who has blocked this room.
"""
PATTERNS = admin_patterns("/rooms/(?P<room_id>[^/]+)/block$")
def __init__(self, hs: "HomeServer"):
self._auth = hs.get_auth()
self._store = hs.get_datastore()
async def on_GET(
self, request: SynapseRequest, room_id: str
) -> Tuple[int, JsonDict]:
await assert_requester_is_admin(self._auth, request)
if not RoomID.is_valid(room_id):
raise SynapseError(
HTTPStatus.BAD_REQUEST, "%s is not a legal room ID" % (room_id,)
)
blocked_by = await self._store.room_is_blocked_by(room_id)
# Test `not None` if `user_id` is an empty string
# if someone add manually an entry in database
if blocked_by is not None:
response = {"block": True, "user_id": blocked_by}
else:
response = {"block": False}
return HTTPStatus.OK, response
async def on_PUT(
self, request: SynapseRequest, room_id: str
) -> Tuple[int, JsonDict]:
requester = await self._auth.get_user_by_req(request)
await assert_user_is_admin(self._auth, requester.user)
content = parse_json_object_from_request(request)
if not RoomID.is_valid(room_id):
raise SynapseError(
HTTPStatus.BAD_REQUEST, "%s is not a legal room ID" % (room_id,)
)
assert_params_in_dict(content, ["block"])
block = content.get("block")
if not isinstance(block, bool):
raise SynapseError(
HTTPStatus.BAD_REQUEST,
"Param 'block' must be a boolean.",
Codes.BAD_JSON,
)
if block:
await self._store.block_room(room_id, requester.user.to_string())
else:
await self._store.unblock_room(room_id)
return HTTPStatus.OK, {"block": block}

View File

@ -397,6 +397,20 @@ class RoomWorkerStore(SQLBaseStore):
desc="is_room_blocked", desc="is_room_blocked",
) )
async def room_is_blocked_by(self, room_id: str) -> Optional[str]:
"""
Function to retrieve user who has blocked the room.
user_id is non-nullable
It returns None if the room is not blocked.
"""
return await self.db_pool.simple_select_one_onecol(
table="blocked_rooms",
keyvalues={"room_id": room_id},
retcol="user_id",
allow_none=True,
desc="room_is_blocked_by",
)
async def get_rooms_paginate( async def get_rooms_paginate(
self, self,
start: int, start: int,
@ -1775,3 +1789,21 @@ class RoomStore(RoomBackgroundUpdateStore, RoomWorkerStore, SearchStore):
self.is_room_blocked, self.is_room_blocked,
(room_id,), (room_id,),
) )
async def unblock_room(self, room_id: str) -> None:
"""Remove the room from blocking list.
Args:
room_id: Room to unblock
"""
await self.db_pool.simple_delete(
table="blocked_rooms",
keyvalues={"room_id": room_id},
desc="unblock_room",
)
await self.db_pool.runInteraction(
"block_room_invalidation",
self._invalidate_cache_and_stream,
self.is_room_blocked,
(room_id,),
)

View File

@ -2226,6 +2226,234 @@ class MakeRoomAdminTestCase(unittest.HomeserverTestCase):
) )
class BlockRoomTestCase(unittest.HomeserverTestCase):
servlets = [
synapse.rest.admin.register_servlets,
room.register_servlets,
login.register_servlets,
]
def prepare(self, reactor, clock, hs):
self._store = hs.get_datastore()
self.admin_user = self.register_user("admin", "pass", admin=True)
self.admin_user_tok = self.login("admin", "pass")
self.other_user = self.register_user("user", "pass")
self.other_user_tok = self.login("user", "pass")
self.room_id = self.helper.create_room_as(
self.other_user, tok=self.other_user_tok
)
self.url = "/_synapse/admin/v1/rooms/%s/block"
@parameterized.expand([("PUT",), ("GET",)])
def test_requester_is_no_admin(self, method: str):
"""If the user is not a server admin, an error 403 is returned."""
channel = self.make_request(
method,
self.url % self.room_id,
content={},
access_token=self.other_user_tok,
)
self.assertEqual(HTTPStatus.FORBIDDEN, channel.code, msg=channel.json_body)
self.assertEqual(Codes.FORBIDDEN, channel.json_body["errcode"])
@parameterized.expand([("PUT",), ("GET",)])
def test_room_is_not_valid(self, method: str):
"""Check that invalid room names, return an error 400."""
channel = self.make_request(
method,
self.url % "invalidroom",
content={},
access_token=self.admin_user_tok,
)
self.assertEqual(HTTPStatus.BAD_REQUEST, channel.code, msg=channel.json_body)
self.assertEqual(
"invalidroom is not a legal room ID",
channel.json_body["error"],
)
def test_block_is_not_valid(self):
"""If parameter `block` is not valid, return an error."""
# `block` is not valid
channel = self.make_request(
"PUT",
self.url % self.room_id,
content={"block": "NotBool"},
access_token=self.admin_user_tok,
)
self.assertEqual(HTTPStatus.BAD_REQUEST, channel.code, msg=channel.json_body)
self.assertEqual(Codes.BAD_JSON, channel.json_body["errcode"])
# `block` is not set
channel = self.make_request(
"PUT",
self.url % self.room_id,
content={},
access_token=self.admin_user_tok,
)
self.assertEqual(HTTPStatus.BAD_REQUEST, channel.code, msg=channel.json_body)
self.assertEqual(Codes.MISSING_PARAM, channel.json_body["errcode"])
# no content is send
channel = self.make_request(
"PUT",
self.url % self.room_id,
access_token=self.admin_user_tok,
)
self.assertEqual(HTTPStatus.BAD_REQUEST, channel.code, msg=channel.json_body)
self.assertEqual(Codes.NOT_JSON, channel.json_body["errcode"])
def test_block_room(self):
"""Test that block a room is successful."""
def _request_and_test_block_room(room_id: str) -> None:
self._is_blocked(room_id, expect=False)
channel = self.make_request(
"PUT",
self.url % room_id,
content={"block": True},
access_token=self.admin_user_tok,
)
self.assertEqual(HTTPStatus.OK, channel.code, msg=channel.json_body)
self.assertTrue(channel.json_body["block"])
self._is_blocked(room_id, expect=True)
# known internal room
_request_and_test_block_room(self.room_id)
# unknown internal room
_request_and_test_block_room("!unknown:test")
# unknown remote room
_request_and_test_block_room("!unknown:remote")
def test_block_room_twice(self):
"""Test that block a room that is already blocked is successful."""
self._is_blocked(self.room_id, expect=False)
for _ in range(2):
channel = self.make_request(
"PUT",
self.url % self.room_id,
content={"block": True},
access_token=self.admin_user_tok,
)
self.assertEqual(HTTPStatus.OK, channel.code, msg=channel.json_body)
self.assertTrue(channel.json_body["block"])
self._is_blocked(self.room_id, expect=True)
def test_unblock_room(self):
"""Test that unblock a room is successful."""
def _request_and_test_unblock_room(room_id: str) -> None:
self._block_room(room_id)
channel = self.make_request(
"PUT",
self.url % room_id,
content={"block": False},
access_token=self.admin_user_tok,
)
self.assertEqual(HTTPStatus.OK, channel.code, msg=channel.json_body)
self.assertFalse(channel.json_body["block"])
self._is_blocked(room_id, expect=False)
# known internal room
_request_and_test_unblock_room(self.room_id)
# unknown internal room
_request_and_test_unblock_room("!unknown:test")
# unknown remote room
_request_and_test_unblock_room("!unknown:remote")
def test_unblock_room_twice(self):
"""Test that unblock a room that is not blocked is successful."""
self._block_room(self.room_id)
for _ in range(2):
channel = self.make_request(
"PUT",
self.url % self.room_id,
content={"block": False},
access_token=self.admin_user_tok,
)
self.assertEqual(HTTPStatus.OK, channel.code, msg=channel.json_body)
self.assertFalse(channel.json_body["block"])
self._is_blocked(self.room_id, expect=False)
def test_get_blocked_room(self):
"""Test get status of a blocked room"""
def _request_blocked_room(room_id: str) -> None:
self._block_room(room_id)
channel = self.make_request(
"GET",
self.url % room_id,
access_token=self.admin_user_tok,
)
self.assertEqual(HTTPStatus.OK, channel.code, msg=channel.json_body)
self.assertTrue(channel.json_body["block"])
self.assertEqual(self.other_user, channel.json_body["user_id"])
# known internal room
_request_blocked_room(self.room_id)
# unknown internal room
_request_blocked_room("!unknown:test")
# unknown remote room
_request_blocked_room("!unknown:remote")
def test_get_unblocked_room(self):
"""Test get status of a unblocked room"""
def _request_unblocked_room(room_id: str) -> None:
self._is_blocked(room_id, expect=False)
channel = self.make_request(
"GET",
self.url % room_id,
access_token=self.admin_user_tok,
)
self.assertEqual(HTTPStatus.OK, channel.code, msg=channel.json_body)
self.assertFalse(channel.json_body["block"])
self.assertNotIn("user_id", channel.json_body)
# known internal room
_request_unblocked_room(self.room_id)
# unknown internal room
_request_unblocked_room("!unknown:test")
# unknown remote room
_request_unblocked_room("!unknown:remote")
def _is_blocked(self, room_id: str, expect: bool = True) -> None:
"""Assert that the room is blocked or not"""
d = self._store.is_room_blocked(room_id)
if expect:
self.assertTrue(self.get_success(d))
else:
self.assertIsNone(self.get_success(d))
def _block_room(self, room_id: str) -> None:
"""Block a room in database"""
self.get_success(self._store.block_room(room_id, self.other_user))
self._is_blocked(room_id, expect=True)
PURGE_TABLES = [ PURGE_TABLES = [
"current_state_events", "current_state_events",
"event_backward_extremities", "event_backward_extremities",