Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
2 changes: 2 additions & 0 deletions changelog.d/19021.feature
Original file line number Diff line number Diff line change
@@ -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 search for room details and any children in a provided room.
74 changes: 74 additions & 0 deletions docs/admin_api/rooms.md
Original file line number Diff line number Diff line change
Expand Up @@ -1115,3 +1115,77 @@ Example response:
]
}
```

# Admin Room Hierarchy Endpoint

This API allows an admin to fetch the room hierarchy for a given room, returning details about that room and any children
the room may have. This is functionally similar to the [CS Hierarchy](https://spec.matrix.org/v1.16/client-server-api/#get_matrixclientv1roomsroomidhierarchy) endpoint but does not return information about any remote
rooms that the server is not currently participating in and does not check for room membership when returning room summaries.
Comment thread
H-Shay marked this conversation as resolved.
Outdated

**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. If not provided will recurse
into the space tree without limit.
Comment thread
H-Shay marked this conversation as resolved.
Outdated

Request:

```http
GET /_synapse/admin/v1/rooms/<room_id>/hierarchy
```

Response:

```json
{
"rooms":
[
{"aliases": [],
Comment thread
H-Shay marked this conversation as resolved.
Outdated
"children_state": [
{
"content": {
"via": ["local_test_server"]
},
"origin_server_ts": 1500,
"sender": "@user:test",
"state_key": "!QrMkkqBSwYRIFNFCso:test",
"type": "m.space.child"
}
],
"creation_event_id": "$bVkNVtm4aDw4c0LRf_U5Ad7mZSo4WKzzQKImrk_rQcg",
Comment thread
H-Shay marked this conversation as resolved.
Outdated
"creator": "@user:test",
Comment thread
H-Shay marked this conversation as resolved.
Outdated
"guest_can_join": false,
"is_space": true,
Comment thread
H-Shay marked this conversation as resolved.
Outdated
"join_rule": "public",
"name": null,
"num_joined_members": 1,
"power_users": ["@user:test"],
Comment thread
H-Shay marked this conversation as resolved.
Outdated
"room_creation_ts": 1400,
Comment thread
H-Shay marked this conversation as resolved.
Outdated
"room_id": "!sPOpNyMHbZAoAOsOFL:test",
"room_type": "m.space",
"topic": null,
"world_readable": false
},

{
"aliases": [],
"children_state": [],
"creation_event_id": "$kymNeN-gA5kzLwZ6FEQUu0_2MfeenYKINSO3dUuLYf8",
"creator": "@user:test",
"guest_can_join": true,
"is_space": false,
"join_rule": "invite",
"name": "nefarious",
"num_joined_members": 1,
"power_users": ["@user:test"],
"room_creation_ts": 999,
"room_id": "!QrMkkqBSwYRIFNFCso:test",
"topic": "being bad",
"world_readable": false}
],
Comment thread
MadLittleMods marked this conversation as resolved.
"next_batch": "KUYmRbeSpAoaAIgOKGgyaCEn"
}
```
105 changes: 64 additions & 41 deletions synapse/handlers/room_summary.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,8 @@ def __init__(self, hs: "HomeServer"):
str,
str,
bool,
bool,
bool,
Optional[int],
Optional[int],
Optional[str],
Expand All @@ -133,6 +135,8 @@ async def get_room_hierarchy(
requester: Requester,
requested_room_id: str,
suggested_only: bool = False,
omit_remote_rooms: bool = False,
admin_skip_room_check: bool = False,
Comment thread
H-Shay marked this conversation as resolved.
Outdated
max_depth: Optional[int] = None,
limit: Optional[int] = None,
from_token: Optional[str] = None,
Expand All @@ -146,6 +150,9 @@ async def get_room_hierarchy(
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"
flag set.
omit_remote_rooms: Whether to omit rooms which the server is not currently participating in
Comment thread
MadLittleMods marked this conversation as resolved.
Outdated
admin_skip_room_check: Whether to skip checking if the room can be accessed by the requester,
used when the requester is a server admin
max_depth: The maximum depth in the tree to explore, must be a
non-negative integer.

Expand Down Expand Up @@ -173,6 +180,8 @@ async def get_room_hierarchy(
requester.user.to_string(),
requested_room_id,
suggested_only,
omit_remote_rooms,
admin_skip_room_check,
max_depth,
limit,
from_token,
Expand All @@ -182,6 +191,8 @@ async def get_room_hierarchy(
requester.user.to_string(),
requested_room_id,
suggested_only,
omit_remote_rooms,
admin_skip_room_check,
max_depth,
limit,
from_token,
Expand All @@ -193,6 +204,8 @@ async def _get_room_hierarchy(
requester: str,
requested_room_id: str,
suggested_only: bool = False,
omit_remote_rooms: bool = False,
admin_skip_room_check: bool = False,
max_depth: Optional[int] = None,
limit: Optional[int] = None,
from_token: Optional[str] = None,
Expand All @@ -204,17 +217,18 @@ async def _get_room_hierarchy(
local_room = await self._store.is_host_joined(
requested_room_id, self._server_name
)
if local_room and not await self._is_local_room_accessible(
requested_room_id, requester
):
raise UnstableSpecAuthError(
403,
"User %s not in room %s, and room previews are disabled"
% (requester, requested_room_id),
errcode=Codes.NOT_JOINED,
)
if not admin_skip_room_check:
if local_room and not await self._is_local_room_accessible(
requested_room_id, requester
):
raise UnstableSpecAuthError(
403,
"User %s not in room %s, and room previews are disabled"
% (requester, requested_room_id),
errcode=Codes.NOT_JOINED,
)

if not local_room:
if not local_room and not omit_remote_rooms:
room_hierarchy = await self._summarize_remote_room_hierarchy(
_RoomQueueEntry(requested_room_id, remote_room_hosts or ()),
False,
Expand Down Expand Up @@ -247,6 +261,8 @@ async def _get_room_hierarchy(
or requested_room_id != pagination_session["room_id"]
or suggested_only != pagination_session["suggested_only"]
or max_depth != pagination_session["max_depth"]
or omit_remote_rooms != pagination_session["omit_remote_rooms"]
or admin_skip_room_check != pagination_session["admin_skip_room_check"]
):
raise SynapseError(400, "Unknown pagination token", Codes.INVALID_PARAM)

Expand Down Expand Up @@ -301,42 +317,44 @@ async def _get_room_hierarchy(
None,
room_id,
suggested_only,
admin_skip_room_check=admin_skip_room_check,
)

# Otherwise, attempt to use information for federation.
else:
# A previous call might have included information for this room.
# It can be used if either:
#
# 1. The room is not a space.
# 2. The maximum depth has been achieved (since no children
# information is needed).
if queue_entry.remote_room and (
queue_entry.remote_room.get("room_type") != RoomTypes.SPACE
or (max_depth is not None and current_depth >= max_depth)
):
room_entry = _RoomEntry(
queue_entry.room_id, queue_entry.remote_room
)
if not omit_remote_rooms:
Comment thread
H-Shay marked this conversation as resolved.
Outdated
# A previous call might have included information for this room.
# It can be used if either:
#
# 1. The room is not a space.
# 2. The maximum depth has been achieved (since no children
# information is needed).
if queue_entry.remote_room and (
queue_entry.remote_room.get("room_type") != RoomTypes.SPACE
or (max_depth is not None and current_depth >= max_depth)
):
room_entry = _RoomEntry(
queue_entry.room_id, queue_entry.remote_room
)

# If the above isn't true, attempt to fetch the room
# information over federation.
else:
(
room_entry,
children_room_entries,
inaccessible_children,
) = await self._summarize_remote_room_hierarchy(
queue_entry,
suggested_only,
)
# If the above isn't true, attempt to fetch the room
# information over federation.
else:
(
room_entry,
children_room_entries,
inaccessible_children,
) = await self._summarize_remote_room_hierarchy(
queue_entry,
suggested_only,
)

# Ensure this room is accessible to the requester (and not just
# the homeserver).
if room_entry and not await self._is_remote_room_accessible(
requester, queue_entry.room_id, room_entry.room
):
room_entry = None
# Ensure this room is accessible to the requester (and not just
# the homeserver).
if room_entry and not await self._is_remote_room_accessible(
requester, queue_entry.room_id, room_entry.room
):
room_entry = None

# This room has been processed and should be ignored if it appears
# elsewhere in the hierarchy.
Expand Down Expand Up @@ -378,6 +396,8 @@ async def _get_room_hierarchy(
"room_id": requested_room_id,
"suggested_only": suggested_only,
"max_depth": max_depth,
"omit_remote_rooms": omit_remote_rooms,
"admin_skip_room_check": admin_skip_room_check,
# The stored state.
"room_queue": [
attr.astuple(room_entry) for room_entry in room_queue
Expand Down Expand Up @@ -460,6 +480,7 @@ async def _summarize_local_room(
room_id: str,
suggested_only: bool,
include_children: bool = True,
admin_skip_room_check: bool = False,
) -> Optional["_RoomEntry"]:
"""
Generate a room entry and a list of event entries for a given room.
Expand All @@ -480,7 +501,9 @@ async def _summarize_local_room(
Returns:
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_check and not await self._is_local_room_accessible(
room_id, requester, origin
):
return None

room_entry = await self._build_room_entry(room_id, for_federation=bool(origin))
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 @@ -74,6 +74,7 @@
RegistrationTokenRestServlet,
)
from synapse.rest.admin.rooms import (
AdminRoomHierarchy,
BlockRoomRestServlet,
DeleteRoomStatusByDeleteIdRestServlet,
DeleteRoomStatusByRoomIdRestServlet,
Expand Down Expand Up @@ -342,6 +343,7 @@ def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None:
ExperimentalFeaturesRestServlet(hs).register(http_server)
SuspendAccountRestServlet(hs).register(http_server)
ScheduledTasksRestServlet(hs).register(http_server)
AdminRoomHierarchy(hs).register(http_server)
EventRestServlet(hs).register(http_server)


Expand Down
90 changes: 90 additions & 0 deletions synapse/rest/admin/rooms.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,96 @@
logger = logging.getLogger(__name__)


class AdminRoomHierarchy(RestServlet):
"""
Given a room returns room details on that room and any children of the provided room.
Does not return information about remote rooms which the server is not currently
participating in
Comment thread
H-Shay marked this conversation as resolved.
Outdated
"""

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,
omit_remote_rooms=True,
Comment thread
H-Shay marked this conversation as resolved.
Outdated
Comment thread
MadLittleMods marked this conversation as resolved.
Outdated
admin_skip_room_check=True,
max_depth=max_depth,
limit=limit,
from_token=parse_string(request, "from"),
Comment thread
H-Shay marked this conversation as resolved.
)

async def _fetch_additional_details(room_id: str) -> JsonDict:
try:
creation_event = await self._store.get_create_event_for_room(room_id)
except NotFoundError:
creation_event = None
aliases = await self._store.get_aliases_for_room(room_id)

pl_event = await self._storage_controllers.state.get_current_state_event(
room_id, EventTypes.PowerLevels, ""
)
pl_users = []
if pl_event is not None:
users = pl_event.content.get("users", {})
for user, level in users.items():
if level >= 25:
pl_users.append(user)
Comment thread
MadLittleMods marked this conversation as resolved.
Outdated
if (
creation_event
and creation_event.room_version.msc4289_creator_power_enabled
):
additional_creators = creation_event.content.get(
"additional_creators", []
)
pl_users.extend(additional_creators)
pl_users.append(creation_event.sender)
additional_details = {
"aliases": aliases,
Comment thread
H-Shay marked this conversation as resolved.
Outdated
"power_users": pl_users,
"room_creation_ts": creation_event.origin_server_ts
if creation_event
else None,
"creator": creation_event.sender if creation_event else None,
"creation_event_id": creation_event.event_id
Comment thread
MadLittleMods marked this conversation as resolved.
Outdated
Comment thread
H-Shay marked this conversation as resolved.
Outdated
if creation_event
else None,
}
return additional_details

rooms = room_entry_summary.get("rooms")
if rooms is not None:
for room in rooms:
room_id = room.get("room_id")
assert room_id is not None
# fetch additional details of interest not provided by room summary
additional_details = await _fetch_additional_details(room_id)
Comment thread
H-Shay marked this conversation as resolved.
Outdated
room.update(additional_details)
# add any missing fields
if not room.get("name"):
room.update({"name": None})
Comment thread
H-Shay marked this conversation as resolved.
Outdated
Comment thread
H-Shay marked this conversation as resolved.
Outdated
if not room.get("topic"):
room.update({"topic": None})
is_space = len(room.get("children_state")) > 0
room.update({"is_space": is_space})
return HTTPStatus.OK, room_entry_summary


class RoomRestV2Servlet(RestServlet):
"""Delete a room from server asynchronously with a background task.

Expand Down
Loading
Loading