Add an admin API to get the space hierarchy (#19021)
It is often useful when investigating a space to get information about that space and it's children. This PR adds an Admin API to return information about a space and it's children, regardless of room membership. Will not fetch information over federation about remote rooms that the server is not participating in.
This commit is contained in:
parent
9d81bb703c
commit
f1695ac20e
2
changelog.d/19021.feature
Normal file
2
changelog.d/19021.feature
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
Add an [Admin API](https://element-hq.github.io/synapse/latest/usage/administration/admin_api/index.html)
|
||||||
|
to allow an admin to fetch the space/room hierarchy for a given space.
|
||||||
@ -1115,3 +1115,76 @@ Example response:
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
# Admin Space Hierarchy Endpoint
|
||||||
|
|
||||||
|
This API allows an admin to fetch the space/room hierarchy for a given space,
|
||||||
|
returning details about that room and any children the room may have, paginating
|
||||||
|
over the space tree in a depth-first manner to locate child rooms. This is
|
||||||
|
functionally similar to the [CS Hierarchy](https://spec.matrix.org/v1.16/client-server-api/#get_matrixclientv1roomsroomidhierarchy) endpoint but does not check for
|
||||||
|
room membership when returning room summaries.
|
||||||
|
|
||||||
|
The endpoint does not query other servers over federation about remote rooms
|
||||||
|
that the server has not joined. This is a deliberate trade-off: while this
|
||||||
|
means it will leave some holes in the hierarchy that we could otherwise
|
||||||
|
sometimes fill in, it significantly improves the endpoint's response time and
|
||||||
|
the admin endpoint is designed for managing rooms local to the homeserver
|
||||||
|
anyway.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
The following query parameters are available:
|
||||||
|
|
||||||
|
* `from` - An optional pagination token, provided when there are more rooms to
|
||||||
|
return than the limit.
|
||||||
|
* `limit` - Maximum amount of rooms to return. Must be a non-negative integer,
|
||||||
|
defaults to `50`.
|
||||||
|
* `max_depth` - The maximum depth in the tree to explore, must be a non-negative
|
||||||
|
integer. 0 would correspond to just the root room, 1 would include just the
|
||||||
|
root room's children, etc. If not provided will recurse into the space tree without limit.
|
||||||
|
|
||||||
|
Request:
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /_synapse/admin/v1/rooms/<room_id>/hierarchy
|
||||||
|
```
|
||||||
|
|
||||||
|
Response:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"rooms":
|
||||||
|
[
|
||||||
|
{ "children_state": [
|
||||||
|
{
|
||||||
|
"content": {
|
||||||
|
"via": ["local_test_server"]
|
||||||
|
},
|
||||||
|
"origin_server_ts": 1500,
|
||||||
|
"sender": "@user:test",
|
||||||
|
"state_key": "!QrMkkqBSwYRIFNFCso:test",
|
||||||
|
"type": "m.space.child"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"name": "space room",
|
||||||
|
"guest_can_join": false,
|
||||||
|
"join_rule": "public",
|
||||||
|
"num_joined_members": 1,
|
||||||
|
"room_id": "!sPOpNyMHbZAoAOsOFL:test",
|
||||||
|
"room_type": "m.space",
|
||||||
|
"world_readable": false
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"children_state": [],
|
||||||
|
"guest_can_join": true,
|
||||||
|
"join_rule": "invite",
|
||||||
|
"name": "nefarious",
|
||||||
|
"num_joined_members": 1,
|
||||||
|
"room_id": "!QrMkkqBSwYRIFNFCso:test",
|
||||||
|
"topic": "being bad",
|
||||||
|
"world_readable": false}
|
||||||
|
],
|
||||||
|
"next_batch": "KUYmRbeSpAoaAIgOKGgyaCEn"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|||||||
@ -116,6 +116,8 @@ class RoomSummaryHandler:
|
|||||||
str,
|
str,
|
||||||
str,
|
str,
|
||||||
bool,
|
bool,
|
||||||
|
bool,
|
||||||
|
bool,
|
||||||
Optional[int],
|
Optional[int],
|
||||||
Optional[int],
|
Optional[int],
|
||||||
Optional[str],
|
Optional[str],
|
||||||
@ -133,6 +135,8 @@ class RoomSummaryHandler:
|
|||||||
requester: Requester,
|
requester: Requester,
|
||||||
requested_room_id: str,
|
requested_room_id: str,
|
||||||
suggested_only: bool = False,
|
suggested_only: bool = False,
|
||||||
|
omit_remote_room_hierarchy: bool = False,
|
||||||
|
admin_skip_room_visibility_check: bool = False,
|
||||||
max_depth: Optional[int] = None,
|
max_depth: Optional[int] = None,
|
||||||
limit: Optional[int] = None,
|
limit: Optional[int] = None,
|
||||||
from_token: Optional[str] = None,
|
from_token: Optional[str] = None,
|
||||||
@ -146,6 +150,11 @@ class RoomSummaryHandler:
|
|||||||
requested_room_id: The room ID to start the hierarchy at (the "root" room).
|
requested_room_id: The room ID to start the hierarchy at (the "root" room).
|
||||||
suggested_only: Whether we should only return children with the "suggested"
|
suggested_only: Whether we should only return children with the "suggested"
|
||||||
flag set.
|
flag set.
|
||||||
|
omit_remote_room_hierarchy: Whether to skip reaching out over
|
||||||
|
federation to get information on rooms which the server
|
||||||
|
is not currently joined to
|
||||||
|
admin_skip_room_visibility_check: Whether to skip checking if the room can
|
||||||
|
be accessed by the requester, used for the admin endpoints.
|
||||||
max_depth: The maximum depth in the tree to explore, must be a
|
max_depth: The maximum depth in the tree to explore, must be a
|
||||||
non-negative integer.
|
non-negative integer.
|
||||||
|
|
||||||
@ -173,6 +182,8 @@ class RoomSummaryHandler:
|
|||||||
requester.user.to_string(),
|
requester.user.to_string(),
|
||||||
requested_room_id,
|
requested_room_id,
|
||||||
suggested_only,
|
suggested_only,
|
||||||
|
omit_remote_room_hierarchy,
|
||||||
|
admin_skip_room_visibility_check,
|
||||||
max_depth,
|
max_depth,
|
||||||
limit,
|
limit,
|
||||||
from_token,
|
from_token,
|
||||||
@ -182,6 +193,8 @@ class RoomSummaryHandler:
|
|||||||
requester.user.to_string(),
|
requester.user.to_string(),
|
||||||
requested_room_id,
|
requested_room_id,
|
||||||
suggested_only,
|
suggested_only,
|
||||||
|
omit_remote_room_hierarchy,
|
||||||
|
admin_skip_room_visibility_check,
|
||||||
max_depth,
|
max_depth,
|
||||||
limit,
|
limit,
|
||||||
from_token,
|
from_token,
|
||||||
@ -193,6 +206,8 @@ class RoomSummaryHandler:
|
|||||||
requester: str,
|
requester: str,
|
||||||
requested_room_id: str,
|
requested_room_id: str,
|
||||||
suggested_only: bool = False,
|
suggested_only: bool = False,
|
||||||
|
omit_remote_room_hierarchy: bool = False,
|
||||||
|
admin_skip_room_visibility_check: bool = False,
|
||||||
max_depth: Optional[int] = None,
|
max_depth: Optional[int] = None,
|
||||||
limit: Optional[int] = None,
|
limit: Optional[int] = None,
|
||||||
from_token: Optional[str] = None,
|
from_token: Optional[str] = None,
|
||||||
@ -204,17 +219,18 @@ class RoomSummaryHandler:
|
|||||||
local_room = await self._store.is_host_joined(
|
local_room = await self._store.is_host_joined(
|
||||||
requested_room_id, self._server_name
|
requested_room_id, self._server_name
|
||||||
)
|
)
|
||||||
if local_room and not await self._is_local_room_accessible(
|
if not admin_skip_room_visibility_check:
|
||||||
requested_room_id, requester
|
if local_room and not await self._is_local_room_accessible(
|
||||||
):
|
requested_room_id, requester
|
||||||
raise UnstableSpecAuthError(
|
):
|
||||||
403,
|
raise UnstableSpecAuthError(
|
||||||
"User %s not in room %s, and room previews are disabled"
|
403,
|
||||||
% (requester, requested_room_id),
|
"User %s not in room %s, and room previews are disabled"
|
||||||
errcode=Codes.NOT_JOINED,
|
% (requester, requested_room_id),
|
||||||
)
|
errcode=Codes.NOT_JOINED,
|
||||||
|
)
|
||||||
|
|
||||||
if not local_room:
|
if not local_room and not omit_remote_room_hierarchy:
|
||||||
room_hierarchy = await self._summarize_remote_room_hierarchy(
|
room_hierarchy = await self._summarize_remote_room_hierarchy(
|
||||||
_RoomQueueEntry(requested_room_id, remote_room_hosts or ()),
|
_RoomQueueEntry(requested_room_id, remote_room_hosts or ()),
|
||||||
False,
|
False,
|
||||||
@ -223,12 +239,13 @@ class RoomSummaryHandler:
|
|||||||
if not root_room_entry or not await self._is_remote_room_accessible(
|
if not root_room_entry or not await self._is_remote_room_accessible(
|
||||||
requester, requested_room_id, root_room_entry.room
|
requester, requested_room_id, root_room_entry.room
|
||||||
):
|
):
|
||||||
raise UnstableSpecAuthError(
|
if not admin_skip_room_visibility_check:
|
||||||
403,
|
raise UnstableSpecAuthError(
|
||||||
"User %s not in room %s, and room previews are disabled"
|
403,
|
||||||
% (requester, requested_room_id),
|
"User %s not in room %s, and room previews are disabled"
|
||||||
errcode=Codes.NOT_JOINED,
|
% (requester, requested_room_id),
|
||||||
)
|
errcode=Codes.NOT_JOINED,
|
||||||
|
)
|
||||||
|
|
||||||
# If this is continuing a previous session, pull the persisted data.
|
# If this is continuing a previous session, pull the persisted data.
|
||||||
if from_token:
|
if from_token:
|
||||||
@ -240,13 +257,18 @@ class RoomSummaryHandler:
|
|||||||
except StoreError:
|
except StoreError:
|
||||||
raise SynapseError(400, "Unknown pagination token", Codes.INVALID_PARAM)
|
raise SynapseError(400, "Unknown pagination token", Codes.INVALID_PARAM)
|
||||||
|
|
||||||
# If the requester, room ID, suggested-only, or max depth were modified
|
# If the requester, room ID, suggested-only, max depth,
|
||||||
# the session is invalid.
|
# omit_remote_room_hierarchy, or admin_skip_room_visibility_check
|
||||||
|
# were modified the session is invalid.
|
||||||
if (
|
if (
|
||||||
requester != pagination_session["requester"]
|
requester != pagination_session["requester"]
|
||||||
or requested_room_id != pagination_session["room_id"]
|
or requested_room_id != pagination_session["room_id"]
|
||||||
or suggested_only != pagination_session["suggested_only"]
|
or suggested_only != pagination_session["suggested_only"]
|
||||||
or max_depth != pagination_session["max_depth"]
|
or max_depth != pagination_session["max_depth"]
|
||||||
|
or omit_remote_room_hierarchy
|
||||||
|
!= pagination_session["omit_remote_room_hierarchy"]
|
||||||
|
or admin_skip_room_visibility_check
|
||||||
|
!= pagination_session["admin_skip_room_visibility_check"]
|
||||||
):
|
):
|
||||||
raise SynapseError(400, "Unknown pagination token", Codes.INVALID_PARAM)
|
raise SynapseError(400, "Unknown pagination token", Codes.INVALID_PARAM)
|
||||||
|
|
||||||
@ -301,6 +323,7 @@ class RoomSummaryHandler:
|
|||||||
None,
|
None,
|
||||||
room_id,
|
room_id,
|
||||||
suggested_only,
|
suggested_only,
|
||||||
|
admin_skip_room_visibility_check=admin_skip_room_visibility_check,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Otherwise, attempt to use information for federation.
|
# Otherwise, attempt to use information for federation.
|
||||||
@ -321,7 +344,7 @@ class RoomSummaryHandler:
|
|||||||
|
|
||||||
# If the above isn't true, attempt to fetch the room
|
# If the above isn't true, attempt to fetch the room
|
||||||
# information over federation.
|
# information over federation.
|
||||||
else:
|
elif not omit_remote_room_hierarchy:
|
||||||
(
|
(
|
||||||
room_entry,
|
room_entry,
|
||||||
children_room_entries,
|
children_room_entries,
|
||||||
@ -378,6 +401,8 @@ class RoomSummaryHandler:
|
|||||||
"room_id": requested_room_id,
|
"room_id": requested_room_id,
|
||||||
"suggested_only": suggested_only,
|
"suggested_only": suggested_only,
|
||||||
"max_depth": max_depth,
|
"max_depth": max_depth,
|
||||||
|
"omit_remote_room_hierarchy": omit_remote_room_hierarchy,
|
||||||
|
"admin_skip_room_visibility_check": admin_skip_room_visibility_check,
|
||||||
# The stored state.
|
# The stored state.
|
||||||
"room_queue": [
|
"room_queue": [
|
||||||
attr.astuple(room_entry) for room_entry in room_queue
|
attr.astuple(room_entry) for room_entry in room_queue
|
||||||
@ -460,6 +485,7 @@ class RoomSummaryHandler:
|
|||||||
room_id: str,
|
room_id: str,
|
||||||
suggested_only: bool,
|
suggested_only: bool,
|
||||||
include_children: bool = True,
|
include_children: bool = True,
|
||||||
|
admin_skip_room_visibility_check: bool = False,
|
||||||
) -> Optional["_RoomEntry"]:
|
) -> Optional["_RoomEntry"]:
|
||||||
"""
|
"""
|
||||||
Generate a room entry and a list of event entries for a given room.
|
Generate a room entry and a list of event entries for a given room.
|
||||||
@ -476,11 +502,16 @@ class RoomSummaryHandler:
|
|||||||
Otherwise, all children are returned.
|
Otherwise, all children are returned.
|
||||||
include_children:
|
include_children:
|
||||||
Whether to include the events of any children.
|
Whether to include the events of any children.
|
||||||
|
admin_skip_room_visibility_check: Whether to skip checking if the room
|
||||||
|
can be accessed by the requester, used for the admin endpoints.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
A room entry if the room should be returned. None, otherwise.
|
A room entry if the room should be returned. None, otherwise.
|
||||||
"""
|
"""
|
||||||
if not await self._is_local_room_accessible(room_id, requester, origin):
|
if (
|
||||||
|
not admin_skip_room_visibility_check
|
||||||
|
and not await self._is_local_room_accessible(room_id, requester, origin)
|
||||||
|
):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
room_entry = await self._build_room_entry(room_id, for_federation=bool(origin))
|
room_entry = await self._build_room_entry(room_id, for_federation=bool(origin))
|
||||||
|
|||||||
@ -74,6 +74,7 @@ from synapse.rest.admin.registration_tokens import (
|
|||||||
RegistrationTokenRestServlet,
|
RegistrationTokenRestServlet,
|
||||||
)
|
)
|
||||||
from synapse.rest.admin.rooms import (
|
from synapse.rest.admin.rooms import (
|
||||||
|
AdminRoomHierarchy,
|
||||||
BlockRoomRestServlet,
|
BlockRoomRestServlet,
|
||||||
DeleteRoomStatusByDeleteIdRestServlet,
|
DeleteRoomStatusByDeleteIdRestServlet,
|
||||||
DeleteRoomStatusByRoomIdRestServlet,
|
DeleteRoomStatusByRoomIdRestServlet,
|
||||||
@ -342,6 +343,7 @@ def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None:
|
|||||||
ExperimentalFeaturesRestServlet(hs).register(http_server)
|
ExperimentalFeaturesRestServlet(hs).register(http_server)
|
||||||
SuspendAccountRestServlet(hs).register(http_server)
|
SuspendAccountRestServlet(hs).register(http_server)
|
||||||
ScheduledTasksRestServlet(hs).register(http_server)
|
ScheduledTasksRestServlet(hs).register(http_server)
|
||||||
|
AdminRoomHierarchy(hs).register(http_server)
|
||||||
EventRestServlet(hs).register(http_server)
|
EventRestServlet(hs).register(http_server)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -63,6 +63,50 @@ if TYPE_CHECKING:
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class AdminRoomHierarchy(RestServlet):
|
||||||
|
"""
|
||||||
|
Given a room, returns room details on that room and any space children of
|
||||||
|
the provided room. Does not reach out over federation to fetch information about
|
||||||
|
any remote rooms which the server is not currently participating in
|
||||||
|
"""
|
||||||
|
|
||||||
|
PATTERNS = admin_patterns("/rooms/(?P<room_id>[^/]*)/hierarchy$")
|
||||||
|
|
||||||
|
def __init__(self, hs: "HomeServer"):
|
||||||
|
self._auth = hs.get_auth()
|
||||||
|
self._room_summary_handler = hs.get_room_summary_handler()
|
||||||
|
self._store = hs.get_datastores().main
|
||||||
|
self._storage_controllers = hs.get_storage_controllers()
|
||||||
|
|
||||||
|
async def on_GET(
|
||||||
|
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)
|
||||||
|
|
||||||
|
max_depth = parse_integer(request, "max_depth")
|
||||||
|
limit = parse_integer(request, "limit")
|
||||||
|
|
||||||
|
room_entry_summary = await self._room_summary_handler.get_room_hierarchy(
|
||||||
|
requester,
|
||||||
|
room_id,
|
||||||
|
# We omit details about remote rooms because we only care
|
||||||
|
# about managing rooms local to the homeserver. This
|
||||||
|
# also immensely helps with the response time of the
|
||||||
|
# endpoint since we don't need to reach out over federation.
|
||||||
|
# There is a trade-off as this will leave holes where
|
||||||
|
# information about public/peekable remote rooms the
|
||||||
|
# server is not participating in will be omitted.
|
||||||
|
omit_remote_room_hierarchy=True,
|
||||||
|
admin_skip_room_visibility_check=True,
|
||||||
|
max_depth=max_depth,
|
||||||
|
limit=limit,
|
||||||
|
from_token=parse_string(request, "from"),
|
||||||
|
)
|
||||||
|
|
||||||
|
return HTTPStatus.OK, room_entry_summary
|
||||||
|
|
||||||
|
|
||||||
class RoomRestV2Servlet(RestServlet):
|
class RoomRestV2Servlet(RestServlet):
|
||||||
"""Delete a room from server asynchronously with a background task.
|
"""Delete a room from server asynchronously with a background task.
|
||||||
|
|
||||||
|
|||||||
@ -31,7 +31,7 @@ from twisted.internet.task import deferLater
|
|||||||
from twisted.internet.testing import MemoryReactor
|
from twisted.internet.testing import MemoryReactor
|
||||||
|
|
||||||
import synapse.rest.admin
|
import synapse.rest.admin
|
||||||
from synapse.api.constants import EventTypes, Membership, RoomTypes
|
from synapse.api.constants import EventContentFields, EventTypes, Membership, RoomTypes
|
||||||
from synapse.api.errors import Codes
|
from synapse.api.errors import Codes
|
||||||
from synapse.api.room_versions import RoomVersions
|
from synapse.api.room_versions import RoomVersions
|
||||||
from synapse.handlers.pagination import (
|
from synapse.handlers.pagination import (
|
||||||
@ -56,6 +56,308 @@ from tests import unittest
|
|||||||
ONE_HOUR_IN_S = 3600
|
ONE_HOUR_IN_S = 3600
|
||||||
|
|
||||||
|
|
||||||
|
class AdminHierarchyTestCase(unittest.HomeserverTestCase):
|
||||||
|
servlets = [
|
||||||
|
synapse.rest.admin.register_servlets,
|
||||||
|
login.register_servlets,
|
||||||
|
room.register_servlets,
|
||||||
|
]
|
||||||
|
|
||||||
|
def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
|
||||||
|
# create some users
|
||||||
|
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.third_user = self.register_user("third_user", "pass")
|
||||||
|
self.third_user_tok = self.login("third_user", "pass")
|
||||||
|
|
||||||
|
# mock out the function which pulls room information in over federation.
|
||||||
|
self._room_summary_handler = hs.get_room_summary_handler()
|
||||||
|
self._room_summary_handler._summarize_remote_room_hierarchy = Mock() # type: ignore[method-assign]
|
||||||
|
|
||||||
|
# create some rooms with different options
|
||||||
|
self.room_id1 = self.helper.create_room_as(
|
||||||
|
self.other_user,
|
||||||
|
is_public=False,
|
||||||
|
tok=self.other_user_tok,
|
||||||
|
extra_content={"name": "nefarious", "topic": "being bad"},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.room_id2 = self.helper.create_room_as(
|
||||||
|
self.third_user,
|
||||||
|
tok=self.third_user_tok,
|
||||||
|
extra_content={"name": "also nefarious"},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.room_id3 = self.helper.create_room_as(
|
||||||
|
self.admin_user,
|
||||||
|
is_public=False,
|
||||||
|
tok=self.admin_user_tok,
|
||||||
|
extra_content={
|
||||||
|
"name": "not nefarious",
|
||||||
|
"topic": "happy things",
|
||||||
|
"creation_content": {
|
||||||
|
"additional_creators": [self.other_user, self.third_user]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
room_version="12",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.not_in_space_room_id = self.helper.create_room_as(
|
||||||
|
self.other_user,
|
||||||
|
tok=self.other_user_tok,
|
||||||
|
extra_content={"name": "not related to other rooms"},
|
||||||
|
)
|
||||||
|
|
||||||
|
# create a space room
|
||||||
|
self.space_room_id = self.helper.create_room_as(
|
||||||
|
self.other_user,
|
||||||
|
is_public=True,
|
||||||
|
extra_content={
|
||||||
|
"visibility": "public",
|
||||||
|
"creation_content": {EventContentFields.ROOM_TYPE: RoomTypes.SPACE},
|
||||||
|
"name": "space_room",
|
||||||
|
},
|
||||||
|
tok=self.other_user_tok,
|
||||||
|
)
|
||||||
|
|
||||||
|
# and an unjoined remote room
|
||||||
|
self.remote_room_id = "!remote_room"
|
||||||
|
|
||||||
|
self.room_id_to_human_name_map = {
|
||||||
|
self.room_id1: "room1",
|
||||||
|
self.room_id2: "room2",
|
||||||
|
self.room_id3: "room3",
|
||||||
|
self.not_in_space_room_id: "room4",
|
||||||
|
self.space_room_id: "space_room",
|
||||||
|
self.remote_room_id: "remote_room",
|
||||||
|
}
|
||||||
|
|
||||||
|
# add three of the rooms to space
|
||||||
|
for state_key in [self.room_id1, self.room_id2, self.room_id3]:
|
||||||
|
self.helper.send_state(
|
||||||
|
self.space_room_id,
|
||||||
|
EventTypes.SpaceChild,
|
||||||
|
body={"via": ["local_test_server"]},
|
||||||
|
tok=self.other_user_tok,
|
||||||
|
state_key=state_key,
|
||||||
|
)
|
||||||
|
|
||||||
|
# and add remote room to space - ideally we'd add an actual remote
|
||||||
|
# space with rooms in it but the test framework doesn't currently
|
||||||
|
# support that. Instead we add a room which the server would have to
|
||||||
|
# reach out over federation to get details about and assert that the
|
||||||
|
# federation call was not made
|
||||||
|
self.helper.send_state(
|
||||||
|
self.space_room_id,
|
||||||
|
EventTypes.SpaceChild,
|
||||||
|
body={"via": ["remote_test_server"]},
|
||||||
|
tok=self.other_user_tok,
|
||||||
|
state_key=self.remote_room_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_no_auth(self) -> None:
|
||||||
|
"""
|
||||||
|
If the requester does not provide authentication, a 401 is returned
|
||||||
|
"""
|
||||||
|
|
||||||
|
channel = self.make_request(
|
||||||
|
"GET",
|
||||||
|
f"/_synapse/admin/v1/rooms/{self.space_room_id}/hierarchy",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(401, channel.code, msg=channel.json_body)
|
||||||
|
self.assertEqual(Codes.MISSING_TOKEN, channel.json_body["errcode"])
|
||||||
|
|
||||||
|
def test_requester_is_no_admin(self) -> None:
|
||||||
|
"""
|
||||||
|
If the requester is not a server admin, an error 403 is returned.
|
||||||
|
"""
|
||||||
|
|
||||||
|
channel = self.make_request(
|
||||||
|
"GET",
|
||||||
|
f"/_synapse/admin/v1/rooms/{self.space_room_id}/hierarchy",
|
||||||
|
access_token=self.other_user_tok,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(403, channel.code, msg=channel.json_body)
|
||||||
|
self.assertEqual(Codes.FORBIDDEN, channel.json_body["errcode"])
|
||||||
|
|
||||||
|
def test_bad_request(self) -> None:
|
||||||
|
"""
|
||||||
|
Test that invalid param values raise an error
|
||||||
|
"""
|
||||||
|
channel = self.make_request(
|
||||||
|
"GET",
|
||||||
|
f"/_synapse/admin/v1/rooms/{self.space_room_id}/hierarchy?limit=ten",
|
||||||
|
access_token=self.admin_user_tok,
|
||||||
|
)
|
||||||
|
self.assertEqual(400, channel.code, msg=channel.json_body)
|
||||||
|
self.assertEqual(Codes.INVALID_PARAM, channel.json_body["errcode"])
|
||||||
|
|
||||||
|
channel = self.make_request(
|
||||||
|
"GET",
|
||||||
|
f"/_synapse/admin/v1/rooms/{self.space_room_id}/hierarchy?max_depth=four",
|
||||||
|
access_token=self.admin_user_tok,
|
||||||
|
)
|
||||||
|
self.assertEqual(400, channel.code, msg=channel.json_body)
|
||||||
|
self.assertEqual(Codes.INVALID_PARAM, channel.json_body["errcode"])
|
||||||
|
|
||||||
|
def test_room_summary(self) -> None:
|
||||||
|
"""
|
||||||
|
Test that details of room and details of children of room are
|
||||||
|
provided correctly
|
||||||
|
"""
|
||||||
|
|
||||||
|
channel = self.make_request(
|
||||||
|
"GET",
|
||||||
|
f"/_synapse/admin/v1/rooms/{self.space_room_id}/hierarchy",
|
||||||
|
access_token=self.admin_user_tok,
|
||||||
|
)
|
||||||
|
self.assertEqual(channel.code, 200, msg=channel.json_body)
|
||||||
|
rooms = channel.json_body["rooms"]
|
||||||
|
self.assertCountEqual(
|
||||||
|
{
|
||||||
|
self.room_id_to_human_name_map.get(
|
||||||
|
room["room_id"], f"Unknown room: {room['room_id']}"
|
||||||
|
)
|
||||||
|
for room in rooms
|
||||||
|
},
|
||||||
|
{"space_room", "room1", "room2", "room3"},
|
||||||
|
)
|
||||||
|
|
||||||
|
for room_result in rooms:
|
||||||
|
room_id = room_result["room_id"]
|
||||||
|
if room_id == self.room_id1:
|
||||||
|
self.assertEqual(room_result["name"], "nefarious")
|
||||||
|
self.assertEqual(room_result["topic"], "being bad")
|
||||||
|
self.assertEqual(room_result["join_rule"], "invite")
|
||||||
|
self.assertEqual(len(room_result["children_state"]), 0)
|
||||||
|
self.assertEqual(room_result["world_readable"], False)
|
||||||
|
self.assertEqual(room_result["guest_can_join"], True)
|
||||||
|
self.assertEqual(room_result["num_joined_members"], 1)
|
||||||
|
elif room_id == self.room_id2:
|
||||||
|
self.assertEqual(room_result["name"], "also nefarious")
|
||||||
|
self.assertEqual(room_result["join_rule"], "public")
|
||||||
|
self.assertEqual(len(room_result["children_state"]), 0)
|
||||||
|
self.assertEqual(room_result["world_readable"], False)
|
||||||
|
self.assertEqual(room_result["guest_can_join"], False)
|
||||||
|
self.assertEqual(room_result["num_joined_members"], 1)
|
||||||
|
elif room_id == self.room_id3:
|
||||||
|
self.assertEqual(room_result["name"], "not nefarious")
|
||||||
|
self.assertEqual(room_result["join_rule"], "invite")
|
||||||
|
self.assertEqual(room_result["topic"], "happy things")
|
||||||
|
self.assertEqual(len(room_result["children_state"]), 0)
|
||||||
|
self.assertEqual(room_result["world_readable"], False)
|
||||||
|
self.assertEqual(room_result["guest_can_join"], True)
|
||||||
|
self.assertEqual(room_result["num_joined_members"], 1)
|
||||||
|
elif room_id == self.not_in_space_room_id:
|
||||||
|
self.fail("this room should not have been returned")
|
||||||
|
elif room_id == self.space_room_id:
|
||||||
|
self.assertEqual(room_result["join_rule"], "public")
|
||||||
|
self.assertEqual(len(room_result["children_state"]), 4)
|
||||||
|
self.assertEqual(room_result["room_type"], "m.space")
|
||||||
|
self.assertEqual(room_result["world_readable"], False)
|
||||||
|
self.assertEqual(room_result["guest_can_join"], False)
|
||||||
|
self.assertEqual(room_result["num_joined_members"], 1)
|
||||||
|
self.assertEqual(room_result["name"], "space_room")
|
||||||
|
else:
|
||||||
|
self.fail("unknown room returned")
|
||||||
|
|
||||||
|
# Assert that a federation function to look up details about
|
||||||
|
# this room has not been called. We never expect the admin
|
||||||
|
# hierarchy endpoint to reach out over federation.
|
||||||
|
self._room_summary_handler._summarize_remote_room_hierarchy.assert_not_called() # type: ignore[attr-defined]
|
||||||
|
|
||||||
|
def test_room_summary_pagination(self) -> None:
|
||||||
|
"""
|
||||||
|
Test that details of room and details of children of room are provided
|
||||||
|
correctly when paginating
|
||||||
|
"""
|
||||||
|
|
||||||
|
channel = self.make_request(
|
||||||
|
"GET",
|
||||||
|
f"/_synapse/admin/v1/rooms/{self.space_room_id}/hierarchy?limit=2",
|
||||||
|
access_token=self.admin_user_tok,
|
||||||
|
)
|
||||||
|
self.assertEqual(channel.code, 200, msg=channel.json_body)
|
||||||
|
rooms = channel.json_body["rooms"]
|
||||||
|
self.assertCountEqual(
|
||||||
|
{
|
||||||
|
self.room_id_to_human_name_map.get(
|
||||||
|
room["room_id"], f"Unknown room: {room['room_id']}"
|
||||||
|
)
|
||||||
|
for room in rooms
|
||||||
|
},
|
||||||
|
{"space_room", "room1"},
|
||||||
|
)
|
||||||
|
next_batch = channel.json_body["next_batch"]
|
||||||
|
|
||||||
|
channel2 = self.make_request(
|
||||||
|
"GET",
|
||||||
|
f"/_synapse/admin/v1/rooms/{self.space_room_id}/hierarchy?from={next_batch}",
|
||||||
|
access_token=self.admin_user_tok,
|
||||||
|
)
|
||||||
|
self.assertEqual(channel2.code, 200, msg=channel2.json_body)
|
||||||
|
new_rooms = channel2.json_body["rooms"]
|
||||||
|
self.assertCountEqual(
|
||||||
|
{
|
||||||
|
self.room_id_to_human_name_map.get(
|
||||||
|
room["room_id"], f"Unknown room: {room['room_id']}"
|
||||||
|
)
|
||||||
|
for room in new_rooms
|
||||||
|
},
|
||||||
|
{"room2", "room3"},
|
||||||
|
)
|
||||||
|
|
||||||
|
rooms_to_check = rooms + new_rooms
|
||||||
|
for room_result in rooms_to_check:
|
||||||
|
room_id = room_result["room_id"]
|
||||||
|
if room_id == self.room_id1:
|
||||||
|
self.assertEqual(room_result["name"], "nefarious")
|
||||||
|
self.assertEqual(room_result["topic"], "being bad")
|
||||||
|
self.assertEqual(room_result["join_rule"], "invite")
|
||||||
|
self.assertEqual(len(room_result["children_state"]), 0)
|
||||||
|
self.assertEqual(room_result["world_readable"], False)
|
||||||
|
self.assertEqual(room_result["guest_can_join"], True)
|
||||||
|
self.assertEqual(room_result["num_joined_members"], 1)
|
||||||
|
elif room_id == self.room_id2:
|
||||||
|
self.assertEqual(room_result["name"], "also nefarious")
|
||||||
|
self.assertEqual(room_result["join_rule"], "public")
|
||||||
|
self.assertEqual(len(room_result["children_state"]), 0)
|
||||||
|
self.assertEqual(room_result["world_readable"], False)
|
||||||
|
self.assertEqual(room_result["guest_can_join"], False)
|
||||||
|
self.assertEqual(room_result["num_joined_members"], 1)
|
||||||
|
elif room_id == self.room_id3:
|
||||||
|
self.assertEqual(room_result["name"], "not nefarious")
|
||||||
|
self.assertEqual(room_result["join_rule"], "invite")
|
||||||
|
self.assertEqual(room_result["topic"], "happy things")
|
||||||
|
self.assertEqual(len(room_result["children_state"]), 0)
|
||||||
|
self.assertEqual(room_result["world_readable"], False)
|
||||||
|
self.assertEqual(room_result["guest_can_join"], True)
|
||||||
|
self.assertEqual(room_result["num_joined_members"], 1)
|
||||||
|
elif room_id == self.not_in_space_room_id:
|
||||||
|
self.fail("this room should not have been returned")
|
||||||
|
elif room_id == self.space_room_id:
|
||||||
|
self.assertEqual(room_result["join_rule"], "public")
|
||||||
|
self.assertEqual(len(room_result["children_state"]), 4)
|
||||||
|
self.assertEqual(room_result["room_type"], "m.space")
|
||||||
|
self.assertEqual(room_result["world_readable"], False)
|
||||||
|
self.assertEqual(room_result["guest_can_join"], False)
|
||||||
|
self.assertEqual(room_result["num_joined_members"], 1)
|
||||||
|
self.assertEqual(room_result["name"], "space_room")
|
||||||
|
else:
|
||||||
|
self.fail("unknown room returned")
|
||||||
|
|
||||||
|
# Assert that a federation function to look up details about
|
||||||
|
# this room has not been called. We never expect the admin
|
||||||
|
# hierarchy endpoint to reach out over federation.
|
||||||
|
self._room_summary_handler._summarize_remote_room_hierarchy.assert_not_called() # type: ignore[attr-defined]
|
||||||
|
|
||||||
|
|
||||||
class DeleteRoomTestCase(unittest.HomeserverTestCase):
|
class DeleteRoomTestCase(unittest.HomeserverTestCase):
|
||||||
servlets = [
|
servlets = [
|
||||||
synapse.rest.admin.register_servlets,
|
synapse.rest.admin.register_servlets,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user