Add delete room admin endpoint (#7613)
The Delete Room admin API allows server admins to remove rooms from server
and block these rooms.
`DELETE /_synapse/admin/v1/rooms/<room_id>`
It is a combination and improvement of "[Shutdown room](https://github.com/matrix-org/synapse/blob/develop/docs/admin_api/shutdown_room.md)" and "[Purge room](https://github.com/matrix-org/synapse/blob/develop/docs/admin_api/purge_room.md)" API.
Fixes: #6425
It also fixes a bug in [synapse/storage/data_stores/main/room.py](synapse/storage/data_stores/main/room.py) in ` get_room_with_stats`.
It should return `None` if the room is unknown. But it returns an `IndexError`.
901b1fa561/synapse/storage/data_stores/main/room.py (L99-L105)
Related to:
- #5575
- https://github.com/Awesome-Technologies/synapse-admin/issues/17
Signed-off-by: Dirk Klimpel dirk@klimpel.org
pull/7848/head
parent
77d2c05410
commit
491f0dab1b
|
@ -0,0 +1 @@
|
|||
Add delete room admin endpoint (`POST /_synapse/admin/v1/rooms/<room_id>/delete`). Contributed by @dklimpel.
|
|
@ -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:
|
||||
|
||||
```
|
||||
|
|
|
@ -318,3 +318,97 @@ 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
|
||||
leave the room without any information.
|
||||
|
||||
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 prevents new joins to the old room.
|
||||
|
||||
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.
|
||||
|
||||
The API is:
|
||||
|
||||
```json
|
||||
POST /_synapse/admin/v1/rooms/<room_id>/delete
|
||||
```
|
||||
|
||||
with a body of:
|
||||
```json
|
||||
{
|
||||
"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
|
||||
}
|
||||
```
|
||||
|
||||
To use it, you will need to authenticate by providing an ``access_token`` for a
|
||||
server admin: see [README.rst](README.rst).
|
||||
|
||||
A response body like the following is returned:
|
||||
|
||||
```json
|
||||
{
|
||||
"kicked_users": [
|
||||
"@foobar:example.com"
|
||||
],
|
||||
"failed_to_kick_users": [],
|
||||
"local_aliases": [
|
||||
"#badroom:example.com",
|
||||
"#evilsaloon:example.com"
|
||||
],
|
||||
"new_room_id": "!newroomid:example.com"
|
||||
}
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
The following parameters should be set in the URL:
|
||||
|
||||
* `room_id` - The ID of the room.
|
||||
|
||||
The following JSON body parameters are available:
|
||||
|
||||
* `new_room_user_id` - Optional. If set, a new room will be created with this user ID
|
||||
as the creator and admin, and all users in the old room will be moved into that
|
||||
room. If not set, no new room will be created and the users will just be removed
|
||||
from the old room. The user ID must be on the local server, but does not necessarily
|
||||
have to belong to a registered user.
|
||||
* `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. If set to `true`, this room will be added to a blocking list, preventing future attempts to
|
||||
join the room. Defaults to `false`.
|
||||
|
||||
The JSON body must not be empty. The body must be at least `{}`.
|
||||
|
||||
## Response
|
||||
|
||||
The following fields are returned in the JSON response body:
|
||||
|
||||
* `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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -22,11 +22,12 @@ import logging
|
|||
import math
|
||||
import string
|
||||
from collections import OrderedDict
|
||||
from typing import Tuple
|
||||
from typing import Optional, Tuple
|
||||
|
||||
from synapse.api.constants import (
|
||||
EventTypes,
|
||||
JoinRules,
|
||||
Membership,
|
||||
RoomCreationPreset,
|
||||
RoomEncryptionAlgorithms,
|
||||
)
|
||||
|
@ -43,9 +44,10 @@ from synapse.types import (
|
|||
StateMap,
|
||||
StreamToken,
|
||||
UserID,
|
||||
create_requester,
|
||||
)
|
||||
from synapse.util import stringutils
|
||||
from synapse.util.async_helpers import Linearizer
|
||||
from synapse.util.async_helpers import Linearizer, maybe_awaitable
|
||||
from synapse.util.caches.response_cache import ResponseCache
|
||||
from synapse.visibility import filter_events_for_client
|
||||
|
||||
|
@ -1089,3 +1091,205 @@ class RoomEventSource(object):
|
|||
|
||||
def get_current_key_for_room(self, room_id):
|
||||
return self.store.get_room_events_max_id(room_id)
|
||||
|
||||
|
||||
class RoomShutdownHandler(object):
|
||||
|
||||
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.room_member_handler = hs.get_room_member_handler()
|
||||
self._room_creation_handler = hs.get_room_creation_handler()
|
||||
self._replication = hs.get_replication_data_handler()
|
||||
self.event_creation_handler = hs.get_event_creation_handler()
|
||||
self.state = hs.get_state_handler()
|
||||
self.store = hs.get_datastore()
|
||||
|
||||
async def shutdown_room(
|
||||
self,
|
||||
room_id: str,
|
||||
requester_user_id: str,
|
||||
new_room_user_id: Optional[str] = None,
|
||||
new_room_name: Optional[str] = None,
|
||||
message: Optional[str] = None,
|
||||
block: bool = False,
|
||||
) -> dict:
|
||||
"""
|
||||
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
|
||||
leave the room without any information.
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
Args:
|
||||
room_id: The ID of the room to shut down.
|
||||
requester_user_id:
|
||||
User who requested the action and put the room on the
|
||||
blocking list.
|
||||
new_room_user_id:
|
||||
If set, a new room will be created with this user ID
|
||||
as the creator and admin, and all users in the old room will be
|
||||
moved into that room. If not set, no new room will be created
|
||||
and the users will just be removed from the old room.
|
||||
new_room_name:
|
||||
A string representing the name of the room that new users will
|
||||
be invited to. Defaults to `Content Violation Notification`
|
||||
message:
|
||||
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:
|
||||
If set to `true`, this room will be added to a blocking list,
|
||||
preventing future attempts to join the room. Defaults to `false`.
|
||||
|
||||
Returns: a dict containing the following keys:
|
||||
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.
|
||||
"""
|
||||
|
||||
if not new_room_name:
|
||||
new_room_name = self.DEFAULT_ROOM_NAME
|
||||
if not message:
|
||||
message = self.DEFAULT_MESSAGE
|
||||
|
||||
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.
|
||||
if block:
|
||||
await self.store.block_room(room_id, requester_user_id)
|
||||
|
||||
if new_room_user_id is not None:
|
||||
if not self.hs.is_mine_id(new_room_user_id):
|
||||
raise SynapseError(
|
||||
400, "User must be our own: %s" % (new_room_user_id,)
|
||||
)
|
||||
|
||||
room_creator_requester = create_requester(new_room_user_id)
|
||||
|
||||
info, stream_id = await self._room_creation_handler.create_room(
|
||||
room_creator_requester,
|
||||
config={
|
||||
"preset": RoomCreationPreset.PUBLIC_CHAT,
|
||||
"name": new_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
|
||||
)
|
||||
else:
|
||||
new_room_id = None
|
||||
logger.info("Shutting down room %r", room_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)
|
||||
|
||||
try:
|
||||
# Kick users from room
|
||||
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)
|
||||
|
||||
# Join users to new room
|
||||
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,
|
||||
)
|
||||
|
||||
kicked_users.append(user_id)
|
||||
except Exception:
|
||||
logger.exception(
|
||||
"Failed to leave old room and join new room for %r", user_id
|
||||
)
|
||||
failed_to_kick_users.append(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
|
||||
)
|
||||
else:
|
||||
aliases_for_room = []
|
||||
|
||||
return {
|
||||
"kicked_users": kicked_users,
|
||||
"failed_to_kick_users": failed_to_kick_users,
|
||||
"local_aliases": aliases_for_room,
|
||||
"new_room_id": new_room_id,
|
||||
}
|
||||
|
|
|
@ -35,6 +35,7 @@ from synapse.rest.admin.groups import DeleteGroupAdminRestServlet
|
|||
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,
|
||||
|
@ -200,6 +201,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)
|
||||
|
|
|
@ -13,9 +13,10 @@
|
|||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
import logging
|
||||
from http import HTTPStatus
|
||||
from typing import List, Optional
|
||||
|
||||
from synapse.api.constants import EventTypes, JoinRules, Membership, RoomCreationPreset
|
||||
from synapse.api.constants import EventTypes, JoinRules
|
||||
from synapse.api.errors import Codes, NotFoundError, SynapseError
|
||||
from synapse.http.servlet import (
|
||||
RestServlet,
|
||||
|
@ -32,7 +33,6 @@ from synapse.rest.admin._base import (
|
|||
)
|
||||
from synapse.storage.data_stores.main.room import RoomSortOrder
|
||||
from synapse.types import RoomAlias, RoomID, UserID, create_requester
|
||||
from synapse.util.async_helpers import maybe_awaitable
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -46,20 +46,10 @@ class ShutdownRoomRestServlet(RestServlet):
|
|||
|
||||
PATTERNS = historical_admin_path_patterns("/shutdown_room/(?P<room_id>[^/]+)")
|
||||
|
||||
DEFAULT_MESSAGE = (
|
||||
"Sharing illegal content on this server is not permitted and rooms in"
|
||||
" violation will be blocked."
|
||||
)
|
||||
|
||||
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.room_shutdown_handler = hs.get_room_shutdown_handler()
|
||||
|
||||
async def on_POST(self, request, room_id):
|
||||
requester = await self.auth.get_user_by_req(request)
|
||||
|
@ -67,116 +57,65 @@ class ShutdownRoomRestServlet(RestServlet):
|
|||
|
||||
content = parse_json_object_from_request(request)
|
||||
assert_params_in_dict(content, ["new_room_user_id"])
|
||||
new_room_user_id = content["new_room_user_id"]
|
||||
|
||||
room_creator_requester = create_requester(new_room_user_id)
|
||||
|
||||
message = content.get("message", self.DEFAULT_MESSAGE)
|
||||
room_name = content.get("room_name", "Content Violation Notification")
|
||||
|
||||
info, stream_id = await self._room_creation_handler.create_room(
|
||||
room_creator_requester,
|
||||
config={
|
||||
"preset": RoomCreationPreset.PUBLIC_CHAT,
|
||||
"name": room_name,
|
||||
"power_level_content_override": {"users_default": -10},
|
||||
},
|
||||
ratelimit=False,
|
||||
)
|
||||
new_room_id = info["room_id"]
|
||||
|
||||
requester_user_id = requester.user.to_string()
|
||||
|
||||
logger.info(
|
||||
"Shutting down room %r, joining to new room: %r", room_id, new_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.
|
||||
await self.store.block_room(room_id, requester_user_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)
|
||||
|
||||
try:
|
||||
target_requester = create_requester(user_id)
|
||||
_, stream_id = await self.room_member_handler.update_membership(
|
||||
requester=target_requester,
|
||||
target=target_requester.user,
|
||||
ret = await self.room_shutdown_handler.shutdown_room(
|
||||
room_id=room_id,
|
||||
action=Membership.LEAVE,
|
||||
content={},
|
||||
ratelimit=False,
|
||||
require_consent=False,
|
||||
new_room_user_id=content["new_room_user_id"],
|
||||
new_room_name=content.get("room_name"),
|
||||
message=content.get("message"),
|
||||
requester_user_id=requester.user.to_string(),
|
||||
block=True,
|
||||
)
|
||||
|
||||
# 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
|
||||
return (200, ret)
|
||||
|
||||
|
||||
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>[^/]+)/delete$")
|
||||
|
||||
def __init__(self, hs):
|
||||
self.hs = hs
|
||||
self.auth = hs.get_auth()
|
||||
self.room_shutdown_handler = hs.get_room_shutdown_handler()
|
||||
self.pagination_handler = hs.get_pagination_handler()
|
||||
|
||||
async def on_POST(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)
|
||||
|
||||
block = content.get("block", False)
|
||||
if not isinstance(block, bool):
|
||||
raise SynapseError(
|
||||
HTTPStatus.BAD_REQUEST,
|
||||
"Param 'block' must be a boolean, if given",
|
||||
Codes.BAD_JSON,
|
||||
)
|
||||
|
||||
await self.room_member_handler.forget(target_requester.user, room_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,
|
||||
ret = await self.room_shutdown_handler.shutdown_room(
|
||||
room_id=room_id,
|
||||
new_room_user_id=content.get("new_room_user_id"),
|
||||
new_room_name=content.get("room_name"),
|
||||
message=content.get("message"),
|
||||
requester_user_id=requester.user.to_string(),
|
||||
block=block,
|
||||
)
|
||||
|
||||
kicked_users.append(user_id)
|
||||
except Exception:
|
||||
logger.exception(
|
||||
"Failed to leave old room and join new room for %r", user_id
|
||||
)
|
||||
failed_to_kick_users.append(user_id)
|
||||
# Purge room
|
||||
await self.pagination_handler.purge_room(room_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
|
||||
)
|
||||
|
||||
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,
|
||||
},
|
||||
)
|
||||
return (200, ret)
|
||||
|
||||
|
||||
class ListRoomRestServlet(RestServlet):
|
||||
|
|
|
@ -73,7 +73,11 @@ from synapse.handlers.profile import BaseProfileHandler, MasterProfileHandler
|
|||
from synapse.handlers.read_marker import ReadMarkerHandler
|
||||
from synapse.handlers.receipts import ReceiptsHandler
|
||||
from synapse.handlers.register import RegistrationHandler
|
||||
from synapse.handlers.room import RoomContextHandler, RoomCreationHandler
|
||||
from synapse.handlers.room import (
|
||||
RoomContextHandler,
|
||||
RoomCreationHandler,
|
||||
RoomShutdownHandler,
|
||||
)
|
||||
from synapse.handlers.room_list import RoomListHandler
|
||||
from synapse.handlers.room_member import RoomMemberMasterHandler
|
||||
from synapse.handlers.room_member_worker import RoomMemberWorkerHandler
|
||||
|
@ -144,6 +148,7 @@ class HomeServer(object):
|
|||
"handlers",
|
||||
"auth",
|
||||
"room_creation_handler",
|
||||
"room_shutdown_handler",
|
||||
"state_handler",
|
||||
"state_resolution_handler",
|
||||
"presence_handler",
|
||||
|
@ -357,6 +362,9 @@ class HomeServer(object):
|
|||
def build_room_creation_handler(self):
|
||||
return RoomCreationHandler(self)
|
||||
|
||||
def build_room_shutdown_handler(self):
|
||||
return RoomShutdownHandler(self)
|
||||
|
||||
def build_sendmail(self):
|
||||
return sendmail
|
||||
|
||||
|
|
|
@ -71,6 +71,8 @@ class HomeServer(object):
|
|||
pass
|
||||
def get_room_member_handler(self) -> synapse.handlers.room_member.RoomMemberHandler:
|
||||
pass
|
||||
def get_room_shutdown_handler(self) -> synapse.handlers.room.RoomShutdownHandler:
|
||||
pass
|
||||
def get_event_creation_handler(
|
||||
self,
|
||||
) -> synapse.handlers.message.EventCreationHandler:
|
||||
|
|
|
@ -118,7 +118,12 @@ class RoomWorkerStore(SQLBaseStore):
|
|||
WHERE room_id = ?
|
||||
"""
|
||||
txn.execute(sql, [room_id])
|
||||
# Catch error if sql returns empty result to return "None" instead of an error
|
||||
try:
|
||||
res = self.db.cursor_to_dict(txn)[0]
|
||||
except IndexError:
|
||||
return None
|
||||
|
||||
res["federatable"] = bool(res["federatable"])
|
||||
res["public"] = bool(res["public"])
|
||||
return res
|
||||
|
|
|
@ -151,6 +151,401 @@ class ShutdownRoomTestCase(unittest.HomeserverTestCase):
|
|||
)
|
||||
|
||||
|
||||
class DeleteRoomTestCase(unittest.HomeserverTestCase):
|
||||
servlets = [
|
||||
synapse.rest.admin.register_servlets,
|
||||
login.register_servlets,
|
||||
events.register_servlets,
|
||||
room.register_servlets,
|
||||
room.register_deprecated_servlets,
|
||||
]
|
||||
|
||||
def prepare(self, reactor, clock, hs):
|
||||
self.event_creation_handler = hs.get_event_creation_handler()
|
||||
hs.config.user_consent_version = "1"
|
||||
|
||||
consent_uri_builder = Mock()
|
||||
consent_uri_builder.build_user_consent_uri.return_value = "http://example.com"
|
||||
self.event_creation_handler._consent_uri_builder = consent_uri_builder
|
||||
|
||||
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")
|
||||
|
||||
# Mark the admin user as having consented
|
||||
self.get_success(self.store.user_set_consent_version(self.admin_user, "1"))
|
||||
|
||||
self.room_id = self.helper.create_room_as(
|
||||
self.other_user, tok=self.other_user_tok
|
||||
)
|
||||
self.url = "/_synapse/admin/v1/rooms/%s/delete" % self.room_id
|
||||
|
||||
def test_requester_is_no_admin(self):
|
||||
"""
|
||||
If the user is not a server admin, an error 403 is returned.
|
||||
"""
|
||||
|
||||
request, channel = self.make_request(
|
||||
"POST", self.url, json.dumps({}), access_token=self.other_user_tok,
|
||||
)
|
||||
self.render(request)
|
||||
|
||||
self.assertEqual(403, int(channel.result["code"]), msg=channel.result["body"])
|
||||
self.assertEqual(Codes.FORBIDDEN, channel.json_body["errcode"])
|
||||
|
||||
def test_room_does_not_exist(self):
|
||||
"""
|
||||
Check that unknown rooms/server return error 404.
|
||||
"""
|
||||
url = "/_synapse/admin/v1/rooms/!unknown:test/delete"
|
||||
|
||||
request, channel = self.make_request(
|
||||
"POST", url, json.dumps({}), access_token=self.admin_user_tok,
|
||||
)
|
||||
self.render(request)
|
||||
|
||||
self.assertEqual(404, int(channel.result["code"]), msg=channel.result["body"])
|
||||
self.assertEqual(Codes.NOT_FOUND, channel.json_body["errcode"])
|
||||
|
||||
def test_room_is_not_valid(self):
|
||||
"""
|
||||
Check that invalid room names, return an error 400.
|
||||
"""
|
||||
url = "/_synapse/admin/v1/rooms/invalidroom/delete"
|
||||
|
||||
request, channel = self.make_request(
|
||||
"POST", url, json.dumps({}), access_token=self.admin_user_tok,
|
||||
)
|
||||
self.render(request)
|
||||
|
||||
self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"])
|
||||
self.assertEqual(
|
||||
"invalidroom is not a legal room ID", channel.json_body["error"],
|
||||
)
|
||||
|
||||
def test_new_room_user_does_not_exist(self):
|
||||
"""
|
||||
Tests that the user ID must be from local server but it does not have to exist.
|
||||
"""
|
||||
body = json.dumps({"new_room_user_id": "@unknown:test"})
|
||||
|
||||
request, channel = self.make_request(
|
||||
"POST",
|
||||
self.url,
|
||||
content=body.encode(encoding="utf_8"),
|
||||
access_token=self.admin_user_tok,
|
||||
)
|
||||
self.render(request)
|
||||
|
||||
self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"])
|
||||
self.assertIn("new_room_id", channel.json_body)
|
||||
self.assertIn("kicked_users", channel.json_body)
|
||||
self.assertIn("failed_to_kick_users", channel.json_body)
|
||||
self.assertIn("local_aliases", channel.json_body)
|
||||
|
||||
def test_new_room_user_is_not_local(self):
|
||||
"""
|
||||
Check that only local users can create new room to move members.
|
||||
"""
|
||||
body = json.dumps({"new_room_user_id": "@not:exist.bla"})
|
||||
|
||||
request, channel = self.make_request(
|
||||
"POST",
|
||||
self.url,
|
||||
content=body.encode(encoding="utf_8"),
|
||||
access_token=self.admin_user_tok,
|
||||
)
|
||||
self.render(request)
|
||||
|
||||
self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"])
|
||||
self.assertEqual(
|
||||
"User must be our own: @not:exist.bla", channel.json_body["error"],
|
||||
)
|
||||
|
||||
def test_block_is_not_bool(self):
|
||||
"""
|
||||
If parameter `block` is not boolean, return an error
|
||||
"""
|
||||
body = json.dumps({"block": "NotBool"})
|
||||
|
||||
request, channel = self.make_request(
|
||||
"POST",
|
||||
self.url,
|
||||
content=body.encode(encoding="utf_8"),
|
||||
access_token=self.admin_user_tok,
|
||||
)
|
||||
self.render(request)
|
||||
|
||||
self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"])
|
||||
self.assertEqual(Codes.BAD_JSON, channel.json_body["errcode"])
|
||||
|
||||
def test_purge_room_and_block(self):
|
||||
"""Test to purge a room and block it.
|
||||
Members will not be moved to a new room and will not receive a message.
|
||||
"""
|
||||
# Test that room is not purged
|
||||
with self.assertRaises(AssertionError):
|
||||
self._is_purged(self.room_id)
|
||||
|
||||
# Test that room is not blocked
|
||||
self._is_blocked(self.room_id, expect=False)
|
||||
|
||||
# Assert one user in room
|
||||
self._is_member(room_id=self.room_id, user_id=self.other_user)
|
||||
|
||||
body = json.dumps({"block": True})
|
||||
|
||||
request, channel = self.make_request(
|
||||
"POST",
|
||||
self.url.encode("ascii"),
|
||||
content=body.encode(encoding="utf_8"),
|
||||
access_token=self.admin_user_tok,
|
||||
)
|
||||
self.render(request)
|
||||
|
||||
self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"])
|
||||
self.assertEqual(None, channel.json_body["new_room_id"])
|
||||
self.assertEqual(self.other_user, channel.json_body["kicked_users"][0])
|
||||
self.assertIn("failed_to_kick_users", channel.json_body)
|
||||
self.assertIn("local_aliases", channel.json_body)
|
||||
|
||||
self._is_purged(self.room_id)
|
||||
self._is_blocked(self.room_id, expect=True)
|
||||
self._has_no_members(self.room_id)
|
||||
|
||||
def test_purge_room_and_not_block(self):
|
||||
"""Test to purge a room and do not block it.
|
||||
Members will not be moved to a new room and will not receive a message.
|
||||
"""
|
||||
# Test that room is not purged
|
||||
with self.assertRaises(AssertionError):
|
||||
self._is_purged(self.room_id)
|
||||
|
||||
# Test that room is not blocked
|
||||
self._is_blocked(self.room_id, expect=False)
|
||||
|
||||
# Assert one user in room
|
||||
self._is_member(room_id=self.room_id, user_id=self.other_user)
|
||||
|
||||
body = json.dumps({"block": False})
|
||||
|
||||
request, channel = self.make_request(
|
||||
"POST",
|
||||
self.url.encode("ascii"),
|
||||
content=body.encode(encoding="utf_8"),
|
||||
access_token=self.admin_user_tok,
|
||||
)
|
||||
self.render(request)
|
||||
|
||||
self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"])
|
||||
self.assertEqual(None, channel.json_body["new_room_id"])
|
||||
self.assertEqual(self.other_user, channel.json_body["kicked_users"][0])
|
||||
self.assertIn("failed_to_kick_users", channel.json_body)
|
||||
self.assertIn("local_aliases", channel.json_body)
|
||||
|
||||
self._is_purged(self.room_id)
|
||||
self._is_blocked(self.room_id, expect=False)
|
||||
self._has_no_members(self.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
|
||||
force part the user from the old room.
|
||||
Members will be moved to a new room and will receive a message.
|
||||
"""
|
||||
self.event_creation_handler._block_events_without_consent_error = None
|
||||
|
||||
# Assert one user in room
|
||||
users_in_room = self.get_success(self.store.get_users_in_room(self.room_id))
|
||||
self.assertEqual([self.other_user], users_in_room)
|
||||
|
||||
# Enable require consent to send events
|
||||
self.event_creation_handler._block_events_without_consent_error = "Error"
|
||||
|
||||
# Assert that the user is getting consent error
|
||||
self.helper.send(
|
||||
self.room_id, body="foo", tok=self.other_user_tok, expect_code=403
|
||||
)
|
||||
|
||||
# Test that room is not purged
|
||||
with self.assertRaises(AssertionError):
|
||||
self._is_purged(self.room_id)
|
||||
|
||||
# Assert one user in room
|
||||
self._is_member(room_id=self.room_id, user_id=self.other_user)
|
||||
|
||||
# Test that the admin can still send shutdown
|
||||
url = "/_synapse/admin/v1/rooms/%s/delete" % self.room_id
|
||||
request, channel = self.make_request(
|
||||
"POST",
|
||||
url.encode("ascii"),
|
||||
json.dumps({"new_room_user_id": self.admin_user}),
|
||||
access_token=self.admin_user_tok,
|
||||
)
|
||||
self.render(request)
|
||||
|
||||
self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"])
|
||||
self.assertEqual(self.other_user, channel.json_body["kicked_users"][0])
|
||||
self.assertIn("new_room_id", channel.json_body)
|
||||
self.assertIn("failed_to_kick_users", channel.json_body)
|
||||
self.assertIn("local_aliases", channel.json_body)
|
||||
|
||||
# Test that member has moved to new room
|
||||
self._is_member(
|
||||
room_id=channel.json_body["new_room_id"], user_id=self.other_user
|
||||
)
|
||||
|
||||
self._is_purged(self.room_id)
|
||||
self._has_no_members(self.room_id)
|
||||
|
||||
def test_shutdown_room_block_peek(self):
|
||||
"""Test that a world_readable room can no longer be peeked into after
|
||||
it has been shut down.
|
||||
Members will be moved to a new room and will receive a message.
|
||||
"""
|
||||
self.event_creation_handler._block_events_without_consent_error = None
|
||||
|
||||
# Enable world readable
|
||||
url = "rooms/%s/state/m.room.history_visibility" % (self.room_id,)
|
||||
request, channel = self.make_request(
|
||||
"PUT",
|
||||
url.encode("ascii"),
|
||||
json.dumps({"history_visibility": "world_readable"}),
|
||||
access_token=self.other_user_tok,
|
||||
)
|
||||
self.render(request)
|
||||
self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"])
|
||||
|
||||
# Test that room is not purged
|
||||
with self.assertRaises(AssertionError):
|
||||
self._is_purged(self.room_id)
|
||||
|
||||
# Assert one user in room
|
||||
self._is_member(room_id=self.room_id, user_id=self.other_user)
|
||||
|
||||
# Test that the admin can still send shutdown
|
||||
url = "/_synapse/admin/v1/rooms/%s/delete" % self.room_id
|
||||
request, channel = self.make_request(
|
||||
"POST",
|
||||
url.encode("ascii"),
|
||||
json.dumps({"new_room_user_id": self.admin_user}),
|
||||
access_token=self.admin_user_tok,
|
||||
)
|
||||
self.render(request)
|
||||
|
||||
self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"])
|
||||
self.assertEqual(self.other_user, channel.json_body["kicked_users"][0])
|
||||
self.assertIn("new_room_id", channel.json_body)
|
||||
self.assertIn("failed_to_kick_users", channel.json_body)
|
||||
self.assertIn("local_aliases", channel.json_body)
|
||||
|
||||
# Test that member has moved to new room
|
||||
self._is_member(
|
||||
room_id=channel.json_body["new_room_id"], user_id=self.other_user
|
||||
)
|
||||
|
||||
self._is_purged(self.room_id)
|
||||
self._has_no_members(self.room_id)
|
||||
|
||||
# Assert we can no longer peek into the room
|
||||
self._assert_peek(self.room_id, expect_code=403)
|
||||
|
||||
def _is_blocked(self, room_id, expect=True):
|
||||
"""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 _has_no_members(self, room_id):
|
||||
"""Assert there is now no longer anyone in the room
|
||||
"""
|
||||
users_in_room = self.get_success(self.store.get_users_in_room(room_id))
|
||||
self.assertEqual([], users_in_room)
|
||||
|
||||
def _is_member(self, room_id, user_id):
|
||||
"""Test that user is member of the room
|
||||
"""
|
||||
users_in_room = self.get_success(self.store.get_users_in_room(room_id))
|
||||
self.assertIn(user_id, users_in_room)
|
||||
|
||||
def _is_purged(self, room_id):
|
||||
"""Test that the following tables have been purged of all rows related to the room.
|
||||
"""
|
||||
for table in (
|
||||
"current_state_events",
|
||||
"event_backward_extremities",
|
||||
"event_forward_extremities",
|
||||
"event_json",
|
||||
"event_push_actions",
|
||||
"event_search",
|
||||
"events",
|
||||
"group_rooms",
|
||||
"public_room_list_stream",
|
||||
"receipts_graph",
|
||||
"receipts_linearized",
|
||||
"room_aliases",
|
||||
"room_depth",
|
||||
"room_memberships",
|
||||
"room_stats_state",
|
||||
"room_stats_current",
|
||||
"room_stats_historical",
|
||||
"room_stats_earliest_token",
|
||||
"rooms",
|
||||
"stream_ordering_to_exterm",
|
||||
"users_in_public_rooms",
|
||||
"users_who_share_private_rooms",
|
||||
"appservice_room_list",
|
||||
"e2e_room_keys",
|
||||
"event_push_summary",
|
||||
"pusher_throttle",
|
||||
"group_summary_rooms",
|
||||
"local_invites",
|
||||
"room_account_data",
|
||||
"room_tags",
|
||||
# "state_groups", # Current impl leaves orphaned state groups around.
|
||||
"state_groups_state",
|
||||
):
|
||||
count = self.get_success(
|
||||
self.store.db.simple_select_one_onecol(
|
||||
table=table,
|
||||
keyvalues={"room_id": room_id},
|
||||
retcol="COUNT(*)",
|
||||
desc="test_purge_room",
|
||||
)
|
||||
)
|
||||
|
||||
self.assertEqual(count, 0, msg="Rows not purged in {}".format(table))
|
||||
|
||||
def _assert_peek(self, room_id, expect_code):
|
||||
"""Assert that the admin user can (or cannot) peek into the room.
|
||||
"""
|
||||
|
||||
url = "rooms/%s/initialSync" % (room_id,)
|
||||
request, channel = self.make_request(
|
||||
"GET", url.encode("ascii"), access_token=self.admin_user_tok
|
||||
)
|
||||
self.render(request)
|
||||
self.assertEqual(
|
||||
expect_code, int(channel.result["code"]), msg=channel.result["body"]
|
||||
)
|
||||
|
||||
url = "events?timeout=0&room_id=" + room_id
|
||||
request, channel = self.make_request(
|
||||
"GET", url.encode("ascii"), access_token=self.admin_user_tok
|
||||
)
|
||||
self.render(request)
|
||||
self.assertEqual(
|
||||
expect_code, int(channel.result["code"]), msg=channel.result["body"]
|
||||
)
|
||||
|
||||
|
||||
class PurgeRoomTestCase(unittest.HomeserverTestCase):
|
||||
"""Test /purge_room admin API.
|
||||
"""
|
||||
|
|
|
@ -55,6 +55,10 @@ class RoomStoreTestCase(unittest.TestCase):
|
|||
(yield self.store.get_room(self.room.to_string())),
|
||||
)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_get_room_unknown_room(self):
|
||||
self.assertIsNone((yield self.store.get_room("!uknown:test")),)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_get_room_with_stats(self):
|
||||
self.assertDictContainsSubset(
|
||||
|
@ -66,6 +70,10 @@ class RoomStoreTestCase(unittest.TestCase):
|
|||
(yield self.store.get_room_with_stats(self.room.to_string())),
|
||||
)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_get_room_with_stats_unknown_room(self):
|
||||
self.assertIsNone((yield self.store.get_room_with_stats("!uknown:test")),)
|
||||
|
||||
|
||||
class RoomEventsStoreTestCase(unittest.TestCase):
|
||||
@defer.inlineCallbacks
|
||||
|
|
Loading…
Reference in New Issue