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:
@@ -31,7 +31,7 @@ from twisted.internet.task import deferLater
|
||||
from twisted.internet.testing import MemoryReactor
|
||||
|
||||
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.room_versions import RoomVersions
|
||||
from synapse.handlers.pagination import (
|
||||
@@ -56,6 +56,308 @@ from tests import unittest
|
||||
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):
|
||||
servlets = [
|
||||
synapse.rest.admin.register_servlets,
|
||||
|
||||
Reference in New Issue
Block a user