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

Add delete room admin endpoint #7613

Merged
merged 15 commits into from
Jul 14, 2020
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/7613.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add delete room admin endpoint (`DELETE /_synapse/admin/v1/rooms/<room_id>`).
2 changes: 2 additions & 0 deletions docs/admin_api/purge_room.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ This API will remove all trace of a room from your database.

All local users must have left the room before it can be removed.

See also: [Delete Room API](rooms.md#delete-room-api)

The API is:

```
Expand Down
85 changes: 85 additions & 0 deletions docs/admin_api/rooms.md
Original file line number Diff line number Diff line change
Expand Up @@ -318,3 +318,88 @@ Response:
"state_events": 93534
}
```

# Delete Room API

The Delete Room admin API allows server admins to remove rooms from server
and block these rooms.
It is a combination and improvement of "[Shutdown room](shutdown_room.md)"
and "[Purge room](purge_room.md)" API.

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
leaves the room without any information.
dklimpel marked this conversation as resolved.
Show resolved Hide resolved

The new room will be created with the user specified by the `new_room_user_id` parameter
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 preventing new joins to the old room.
dklimpel marked this conversation as resolved.
Show resolved Hide resolved

This API will remove all trace of the old room from your database after removing
all local users.
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
the new room. Users on other servers will be unaffected.

## Parameters

The following query parameters are available:

* `room_id` - The ID of the room.

The following JSON body parameters are available:

* `new_room_user_id` - Optional. A string representing the user ID of the user that will admin
the new room that all users in the old room will be moved to. If not
set the users will not be moved to a new room and only leave the old room
without any information. Defaults to `None`.
dklimpel marked this conversation as resolved.
Show resolved Hide resolved
* `room_name` - Optional. A string representing the name of the room that new users will be
invited to. Defaults to `Content Violation Notification`
* `message` - Optional. A string containing the first message that will be sent as
`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. A boolean if `room_id` will be set on blocking list. The room will be
blocked for this server and preventing new joins. Defaults to `True`.
dklimpel marked this conversation as resolved.
Show resolved Hide resolved
dklimpel marked this conversation as resolved.
Show resolved Hide resolved

The following fields are possible in the JSON response body:
dklimpel marked this conversation as resolved.
Show resolved Hide resolved

* `kicked_users` - An array of users (`user_id`) that were 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
the old room to the new.
* `new_room_id` - A string representing the room ID of the new room.

## Usage

A standard request:

```json
DELETE /_synapse/admin/v1/rooms/<room_id>
richvdh marked this conversation as resolved.
Show resolved Hide resolved

richvdh marked this conversation as resolved.
Show resolved Hide resolved
{
"new_room_user_id": "@someuser:example.com",
"room_name": "Content Violation Notification",
"message": "Bad Room has been shutdown due to content violations on this server. Please review our Terms of Service.",
"block": true
}
```

Response:

```json
{
"kicked_users": [
"@foobar:example.com"
],
"failed_to_kick_users": [],
"local_aliases": [
"#badroom:example.com",
"#evilsaloon:example.com"
],
"new_room_id": "!newroomid:example.com"
}
```
2 changes: 2 additions & 0 deletions docs/admin_api/shutdown_room.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ disallow any further invites or joins.
The local server will only have the power to move local user and room aliases to
the new room. Users on other servers will be unaffected.

See also: [Delete Room API](rooms.md#delete-room-api)

## API

You will need to authenticate with an access token for an admin user.
Expand Down
2 changes: 2 additions & 0 deletions synapse/rest/admin/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
from synapse.rest.admin.media import ListMediaInRoom, register_servlets_for_media_repo
from synapse.rest.admin.purge_room_servlet import PurgeRoomServlet
from synapse.rest.admin.rooms import (
DeleteRoomRestServlet,
JoinRoomAliasServlet,
ListRoomRestServlet,
RoomRestServlet,
Expand Down Expand Up @@ -195,6 +196,7 @@ def register_servlets(hs, http_server):
register_servlets_for_client_rest_resource(hs, http_server)
ListRoomRestServlet(hs).register(http_server)
RoomRestServlet(hs).register(http_server)
DeleteRoomRestServlet(hs).register(http_server)
JoinRoomAliasServlet(hs).register(http_server)
PurgeRoomServlet(hs).register(http_server)
SendServerNoticeServlet(hs).register(http_server)
Expand Down
190 changes: 190 additions & 0 deletions synapse/rest/admin/rooms.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
import logging
from typing import List, Optional

from six.moves import http_client

from synapse.api.constants import EventTypes, JoinRules, Membership
from synapse.api.errors import Codes, NotFoundError, SynapseError
from synapse.http.servlet import (
Expand Down Expand Up @@ -179,6 +181,194 @@ async def on_POST(self, request, room_id):
)


class DeleteRoomRestServlet(RestServlet):
"""Delete a room from server. It is a combination and improvement of
shut down and purge room.
Shuts down a room by removing all local users from the room.
Blocking all future invites and joins to the room is optional.
If desired any local aliases will be repointed to a new room
created by `new_room_user_id` and kicked users will be auto
joined to the new room.
It will remove all trace of a room from the database.
"""

PATTERNS = admin_patterns("/rooms/(?P<room_id>[^/]+)$")

DEFAULT_MESSAGE = (
"Sharing illegal content on this server is not permitted and rooms in"
" violation will be blocked."
)
DEFAULT_ROOM_NAME = "Content Violation Notification"

def __init__(self, hs):
self.hs = hs
self.store = hs.get_datastore()
self.state = hs.get_state_handler()
self._room_creation_handler = hs.get_room_creation_handler()
self.event_creation_handler = hs.get_event_creation_handler()
self.room_member_handler = hs.get_room_member_handler()
self.auth = hs.get_auth()
self._replication = hs.get_replication_data_handler()
self.pagination_handler = hs.get_pagination_handler()
self.admin_handler = hs.get_handlers().admin_handler

async def on_DELETE(self, request, room_id):
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, allow_empty_body=True)
richvdh marked this conversation as resolved.
Show resolved Hide resolved
requester_user_id = requester.user.to_string()

# Check RoomID
if not RoomID.is_valid(room_id):
raise SynapseError(400, "%s was not legal room ID" % (room_id))

if not await self.store.get_room_with_stats(room_id):
raise NotFoundError("Room not found")

# Check parameter `block`
dklimpel marked this conversation as resolved.
Show resolved Hide resolved
block = content.get("block", True)
if not isinstance(block, bool):
raise SynapseError(
http_client.BAD_REQUEST,
"Param 'block' must be a boolean, if given",
Codes.BAD_JSON,
)

# 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.
if block:
await self.store.block_room(room_id, requester_user_id)

new_room_user_id = content.get("new_room_user_id")
if new_room_user_id:
if not self.hs.is_mine_id(new_room_user_id):
raise SynapseError(
400, "This endpoint can only be used with local users"
)

if not await self.admin_handler.get_user(
UserID.from_string(new_room_user_id)
):
raise NotFoundError("User not found")

room_creator_requester = create_requester(new_room_user_id)

message = content.get("message", self.DEFAULT_MESSAGE)
room_name = content.get("room_name", self.DEFAULT_ROOM_NAME)

info, stream_id = await self._room_creation_handler.create_room(
room_creator_requester,
config={
"preset": "public_chat",
"name": room_name,
"power_level_content_override": {"users_default": -10},
},
ratelimit=False,
)
new_room_id = info["room_id"]

logger.info(
"Shutting down room %r, joining to new room: %r", room_id, new_room_id
)

# We now wait for the create room to come back in via replication so
# that we can assume that all the joins/invites have propogated before
# we try and auto join below.
#
# TODO: Currently the events stream is written to from master
await self._replication.wait_for_stream_position(
self.hs.config.worker.writers.events, "events", stream_id
)

users = await self.state.get_current_users_in_room(room_id)
kicked_users = []
failed_to_kick_users = []
for user_id in users:
if not self.hs.is_mine_id(user_id):
continue

logger.info("Kicking %r from %r...", user_id, room_id)

# Kick users from room
try:
target_requester = create_requester(user_id)
_, stream_id = await self.room_member_handler.update_membership(
requester=target_requester,
target=target_requester.user,
room_id=room_id,
action=Membership.LEAVE,
content={},
ratelimit=False,
require_consent=False,
)

# Wait for leave to come in over replication before trying to forget.
await self._replication.wait_for_stream_position(
self.hs.config.worker.writers.events, "events", stream_id
)

await self.room_member_handler.forget(target_requester.user, room_id)
kicked_users.append(user_id)
except Exception:
logger.exception("Failed to leave old room for %r", user_id)
failed_to_kick_users.append(user_id)

# Join users to new room
try:
if new_room_user_id:
await self.room_member_handler.update_membership(
requester=target_requester,
target=target_requester.user,
room_id=new_room_id,
action=Membership.JOIN,
content={},
ratelimit=False,
require_consent=False,
)
except Exception:
logger.exception("Failed to join new room for %r", user_id)

# Send message in new room and move aliases
if new_room_user_id:
await self.event_creation_handler.create_and_send_nonmember_event(
room_creator_requester,
{
"type": "m.room.message",
"content": {"body": message, "msgtype": "m.text"},
"room_id": new_room_id,
"sender": new_room_user_id,
},
ratelimit=False,
)

aliases_for_room = await maybe_awaitable(
self.store.get_aliases_for_room(room_id)
)

await self.store.update_aliases_for_room(
room_id, new_room_id, requester_user_id
)

# Purge room
await self.pagination_handler.purge_room(room_id)

# Set empty values if no new room was created
if not new_room_user_id:
aliases_for_room = []
new_room_id = ""

return (
200,
{
"kicked_users": kicked_users,
"failed_to_kick_users": failed_to_kick_users,
"local_aliases": aliases_for_room,
"new_room_id": new_room_id,
},
)


class ListRoomRestServlet(RestServlet):
"""
List all rooms that are known to the homeserver. Results are returned
Expand Down
11 changes: 7 additions & 4 deletions synapse/storage/data_stores/main/room.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,10 +118,13 @@ def get_room_with_stats_txn(txn, room_id):
WHERE room_id = ?
"""
txn.execute(sql, [room_id])
res = self.db.cursor_to_dict(txn)[0]
res["federatable"] = bool(res["federatable"])
res["public"] = bool(res["public"])
return res
try:
res = self.db.cursor_to_dict(txn)[0]
res["federatable"] = bool(res["federatable"])
res["public"] = bool(res["public"])
return res
except IndexError:
richvdh marked this conversation as resolved.
Show resolved Hide resolved
return
dklimpel marked this conversation as resolved.
Show resolved Hide resolved

return self.db.runInteraction(
"get_room_with_stats", get_room_with_stats_txn, room_id
Expand Down
Loading