mirror of
https://github.com/element-hq/synapse.git
synced 2025-12-05 01:10:13 +00:00
Compare commits
17 Commits
patch-1
...
squah/leav
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2e2f5c043e | ||
|
|
07580acdc0 | ||
|
|
64c56177a4 | ||
|
|
94586c596f | ||
|
|
856656a8ee | ||
|
|
e5751a6350 | ||
|
|
b105beafdb | ||
|
|
f77da61ce8 | ||
|
|
8627a456e3 | ||
|
|
b43d085472 | ||
|
|
ab89c60702 | ||
|
|
b8c228ae98 | ||
|
|
3371ec0b85 | ||
|
|
75be1be9d5 | ||
|
|
f004687410 | ||
|
|
98873d7be3 | ||
|
|
17bc6167d6 |
1
changelog.d/11358.feature
Normal file
1
changelog.d/11358.feature
Normal file
@@ -0,0 +1 @@
|
||||
Add an admin API endpoint to force a local user to leave all non-public rooms in a space.
|
||||
@@ -61,6 +61,7 @@
|
||||
- [Registration Tokens](usage/administration/admin_api/registration_tokens.md)
|
||||
- [Manipulate Room Membership](admin_api/room_membership.md)
|
||||
- [Rooms](admin_api/rooms.md)
|
||||
- [Spaces](usage/administration/admin_api/spaces.md)
|
||||
- [Server Notices](admin_api/server_notices.md)
|
||||
- [Statistics](admin_api/statistics.md)
|
||||
- [Users](admin_api/user_admin_api.md)
|
||||
|
||||
57
docs/usage/administration/admin_api/spaces.md
Normal file
57
docs/usage/administration/admin_api/spaces.md
Normal file
@@ -0,0 +1,57 @@
|
||||
# Spaces API
|
||||
|
||||
This API allows a server administrator to manage spaces.
|
||||
|
||||
## Remove local user
|
||||
|
||||
This API forces a local user to leave all non-public rooms in a space.
|
||||
|
||||
The space itself is always left, regardless of whether it is public.
|
||||
|
||||
May succeed partially if the user fails to leave some rooms.
|
||||
|
||||
The API is:
|
||||
|
||||
```
|
||||
DELETE /_synapse/admin/v1/rooms/<room_id>/hierarchy/members/<user_id>
|
||||
```
|
||||
|
||||
with an optional body of:
|
||||
|
||||
```json
|
||||
{
|
||||
"include_remote_spaces": true,
|
||||
}
|
||||
```
|
||||
|
||||
`include_remote_spaces` controls whether to process subspaces that the
|
||||
local homeserver is not participating in. The listings of such subspaces
|
||||
have to be retrieved over federation and their accuracy cannot be
|
||||
guaranteed.
|
||||
|
||||
Returning:
|
||||
|
||||
```json
|
||||
{
|
||||
"left_rooms": ["!room1:example.net", "!room2:example.net", ...],
|
||||
"inaccessible_rooms": ["!subspace1:example.net", ...],
|
||||
"failed_rooms": {
|
||||
"!room4:example.net": "Failed to leave room.",
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`left_rooms`: A list of rooms that the user has been made to leave.
|
||||
|
||||
`inaccessible_rooms`: A list of rooms and spaces that the local
|
||||
homeserver is not in, and may have not been fully processed. Rooms may
|
||||
appear here if:
|
||||
* The room is a space that the local homeserver is not in, and so its
|
||||
full list of child rooms could not be determined.
|
||||
* The room is inaccessible to the local homeserver, and it is not
|
||||
known whether the room is a subspace containing further rooms.
|
||||
|
||||
`failed_rooms`: A dictionary of errors encountered when leaving rooms.
|
||||
The keys of the dictionary are room IDs and the values of the dictionary
|
||||
are error messages.
|
||||
@@ -1045,7 +1045,7 @@ class RoomSummaryHandler:
|
||||
|
||||
# filter out any events without a "via" (which implies it has been redacted),
|
||||
# and order to ensure we return stable results.
|
||||
return sorted(filter(_has_valid_via, events), key=_child_events_comparison_key)
|
||||
return sorted(filter(has_valid_via, events), key=child_events_comparison_key)
|
||||
|
||||
async def get_room_summary(
|
||||
self,
|
||||
@@ -1139,7 +1139,7 @@ class _RoomEntry:
|
||||
return result
|
||||
|
||||
|
||||
def _has_valid_via(e: EventBase) -> bool:
|
||||
def has_valid_via(e: EventBase) -> bool:
|
||||
via = e.content.get("via")
|
||||
if not via or not isinstance(via, Sequence):
|
||||
return False
|
||||
@@ -1162,7 +1162,7 @@ def _is_suggested_child_event(edge_event: EventBase) -> bool:
|
||||
_INVALID_ORDER_CHARS_RE = re.compile(r"[^\x20-\x7E]")
|
||||
|
||||
|
||||
def _child_events_comparison_key(
|
||||
def child_events_comparison_key(
|
||||
child: EventBase,
|
||||
) -> Tuple[bool, Optional[str], int, str]:
|
||||
"""
|
||||
|
||||
294
synapse/handlers/space_hierarchy.py
Normal file
294
synapse/handlers/space_hierarchy.py
Normal file
@@ -0,0 +1,294 @@
|
||||
# Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import logging
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Dict,
|
||||
Iterable,
|
||||
List,
|
||||
Mapping,
|
||||
Optional,
|
||||
Sequence,
|
||||
Tuple,
|
||||
)
|
||||
|
||||
from synapse.api.constants import EventContentFields, EventTypes, RoomTypes
|
||||
from synapse.api.errors import SynapseError
|
||||
from synapse.handlers.room_summary import child_events_comparison_key, has_valid_via
|
||||
from synapse.storage.state import StateFilter
|
||||
from synapse.types import JsonDict
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from synapse.server import HomeServer
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SpaceHierarchyHandler:
|
||||
"""Provides methods for walking over space hierarchies.
|
||||
|
||||
Also see `RoomSummaryHandler`, which has similar functionality.
|
||||
"""
|
||||
|
||||
def __init__(self, hs: "HomeServer"):
|
||||
self._store = hs.get_datastore()
|
||||
self._federation_client = hs.get_federation_client()
|
||||
|
||||
self._server_name = hs.hostname
|
||||
|
||||
async def get_space_descendants(
|
||||
self,
|
||||
space_id: str,
|
||||
via: Optional[Iterable[str]] = None,
|
||||
enable_federation: Optional[bool] = True,
|
||||
) -> Tuple[Sequence[Tuple[str, Iterable[str]]], Sequence[str]]:
|
||||
"""Gets the children of a space, recursively.
|
||||
|
||||
Args:
|
||||
space_id: The room ID of the space.
|
||||
via: A list of servers which may know about the space.
|
||||
enable_federation: A boolean controlling whether children of unknown rooms
|
||||
should be fetched over federation. Defaults to `True`.
|
||||
|
||||
Returns:
|
||||
A tuple containing:
|
||||
* A list of (room ID, via) tuples, representing the descendants of the
|
||||
space. `space_id` is included in the list.
|
||||
* A list of room IDs whose children could not be fully listed.
|
||||
Rooms in this list are either spaces not known locally, and thus require
|
||||
listing over federation, or are unknown rooms or subspaces completely
|
||||
inaccessible to the local homeserver which may contain further rooms.
|
||||
Subspaces requiring listing over federation are always included here,
|
||||
regardless of the value of the `enable_federation` flag.
|
||||
|
||||
This list is a subset of the previous list, except it may include
|
||||
`space_id`.
|
||||
"""
|
||||
via = via or []
|
||||
|
||||
# (room ID, via, federation room chunks)
|
||||
todo: List[Tuple[str, Iterable[str], Mapping[str, Optional[JsonDict]]]] = [
|
||||
(space_id, via, {})
|
||||
]
|
||||
# [(room ID, via)]
|
||||
descendants: List[Tuple[str, Iterable[str]]] = []
|
||||
|
||||
seen = {space_id}
|
||||
|
||||
inaccessible_room_ids: List[str] = []
|
||||
|
||||
while todo:
|
||||
space_id, via, federation_room_chunks = todo.pop()
|
||||
descendants.append((space_id, via))
|
||||
try:
|
||||
(
|
||||
is_in_room,
|
||||
children,
|
||||
federation_room_chunks,
|
||||
) = await self._get_space_children(
|
||||
space_id,
|
||||
via,
|
||||
federation_room_chunks,
|
||||
enable_federation=enable_federation,
|
||||
)
|
||||
except SynapseError:
|
||||
# Could not list children over federation
|
||||
inaccessible_room_ids.append(space_id)
|
||||
continue
|
||||
|
||||
# Children were retrieved over federation, which is not guaranteed to be
|
||||
# the full list.
|
||||
if not is_in_room:
|
||||
inaccessible_room_ids.append(space_id)
|
||||
|
||||
for child_room_id, child_via in reversed(children):
|
||||
if child_room_id in seen:
|
||||
continue
|
||||
|
||||
seen.add(child_room_id)
|
||||
|
||||
# Queue up the child for processing.
|
||||
# The child may not actually be a space, but that's checked by
|
||||
# `_get_space_children`.
|
||||
todo.append((child_room_id, child_via, federation_room_chunks))
|
||||
|
||||
return descendants, inaccessible_room_ids
|
||||
|
||||
async def _get_space_children(
|
||||
self,
|
||||
space_id: str,
|
||||
via: Optional[Iterable[str]] = None,
|
||||
federation_room_chunks: Optional[Mapping[str, Optional[JsonDict]]] = None,
|
||||
enable_federation: Optional[bool] = True,
|
||||
) -> Tuple[
|
||||
bool, Sequence[Tuple[str, Iterable[str]]], Mapping[str, Optional[JsonDict]]
|
||||
]:
|
||||
"""Gets the direct children of a space.
|
||||
|
||||
Args:
|
||||
space_id: The room ID of the space.
|
||||
via: A list of servers which may know about the space.
|
||||
federation_room_chunks: A cache of room chunks previously returned by
|
||||
`_get_space_children` that may be used to skip federation requests for
|
||||
inaccessible or non-space rooms.
|
||||
|
||||
Returns:
|
||||
A tuple containing:
|
||||
* A boolean indicating whether `space_id` is known to the local homeserver.
|
||||
* A list of (room ID, via) tuples, representing the children of the space,
|
||||
if `space_id` refers to a space; an empty list otherwise.
|
||||
* A dictionary of child room ID: `PublicRoomsChunk`s returned over
|
||||
federation:
|
||||
https://spec.matrix.org/latest/client-server-api/#get_matrixclientv3publicrooms
|
||||
These are supposed to include extra `room_type` and `allowed_room_ids`
|
||||
fields, as described in MSC2946.
|
||||
|
||||
Contains `None` for rooms to which the remote homeserver thinks we do not
|
||||
have access.
|
||||
|
||||
Local information about rooms should be trusted over data in this
|
||||
dictionary.
|
||||
|
||||
Raises:
|
||||
SynapseError: if `space_id` is not known locally and its children could not
|
||||
be retrieved over federation or `enable_federation` is `False`.
|
||||
"""
|
||||
via = via or []
|
||||
federation_room_chunks = federation_room_chunks or {}
|
||||
|
||||
is_in_room = await self._store.is_host_joined(space_id, self._server_name)
|
||||
if is_in_room:
|
||||
children = await self._get_space_children_local(space_id)
|
||||
return True, children, {}
|
||||
else:
|
||||
# Check the room chunks previously returned over federation to see if we
|
||||
# should really make a request.
|
||||
# `federation_room_chunks` is intentionally not used earlier since we want
|
||||
# to trust local data over data from federation.
|
||||
if space_id in federation_room_chunks:
|
||||
room_chunk = federation_room_chunks[space_id]
|
||||
if room_chunk is None:
|
||||
# `space_id` is inaccessible to the local homeserver according to
|
||||
# federation.
|
||||
raise SynapseError(
|
||||
502, f"{space_id} is not accessible to the local homeserver"
|
||||
)
|
||||
elif room_chunk.get("room_type") != RoomTypes.SPACE:
|
||||
# `space_id` is not a space according to federation.
|
||||
return False, [], {}
|
||||
|
||||
if not enable_federation:
|
||||
raise SynapseError(
|
||||
502, f"{space_id} is not accessible to the local homeserver"
|
||||
)
|
||||
|
||||
children, room_chunks = await self._get_space_children_remote(space_id, via)
|
||||
return False, children, room_chunks
|
||||
|
||||
async def _get_space_children_local(
|
||||
self, space_id: str
|
||||
) -> Sequence[Tuple[str, Iterable[str]]]:
|
||||
"""Gets the direct children of a space that the local homeserver is in.
|
||||
|
||||
Args:
|
||||
space_id: The room ID of the space.
|
||||
|
||||
Returns:
|
||||
A list of (room ID, via) tuples, representing the children of the space,
|
||||
if `space_id` refers to a space; an empty list otherwise.
|
||||
|
||||
Raises:
|
||||
ValueError: if `space_id` is not known locally.
|
||||
"""
|
||||
# Fetch the `m.room.create` and `m.space.child` events for `space_id`
|
||||
state_filter = StateFilter.from_types(
|
||||
[(EventTypes.Create, ""), (EventTypes.SpaceChild, None)]
|
||||
)
|
||||
current_state_ids = await self._store.get_filtered_current_state_ids(
|
||||
space_id, state_filter
|
||||
)
|
||||
state_events = await self._store.get_events_as_list(current_state_ids.values())
|
||||
assert len(state_events) == len(current_state_ids)
|
||||
|
||||
create_event_id = current_state_ids.get((EventTypes.Create, ""))
|
||||
if create_event_id is None:
|
||||
# The local homeserver is not in this room
|
||||
raise ValueError(f"{space_id} is not a room known locally.")
|
||||
|
||||
create_event = next(
|
||||
event for event in state_events if event.event_id == create_event_id
|
||||
)
|
||||
if create_event.content.get(EventContentFields.ROOM_TYPE) != RoomTypes.SPACE:
|
||||
# `space_id` is a regular room and not a space.
|
||||
# Ignore any `m.space.child` events.
|
||||
return []
|
||||
|
||||
child_events = [
|
||||
event
|
||||
for event in state_events
|
||||
# Ignore events with a missing or non-array `via`, as per MSC1772
|
||||
if event.event_id != create_event_id and has_valid_via(event)
|
||||
]
|
||||
child_events.sort(key=child_events_comparison_key)
|
||||
return [(event.state_key, event.content["via"]) for event in child_events]
|
||||
|
||||
async def _get_space_children_remote(
|
||||
self, space_id: str, via: Iterable[str]
|
||||
) -> Tuple[Sequence[Tuple[str, Iterable[str]]], Mapping[str, Optional[JsonDict]]]:
|
||||
"""Gets the direct children of a space over federation.
|
||||
|
||||
Args:
|
||||
space_id: The room ID of the space.
|
||||
via: A list of servers which may know about the space.
|
||||
|
||||
Returns:
|
||||
A tuple containing:
|
||||
* A list of (room ID, via) tuples, representing the children of the space,
|
||||
if `space_id` refers to a space; an empty list otherwise.
|
||||
* A dictionary of child room ID: `PublicRoomsChunk`s returned over
|
||||
federation:
|
||||
https://spec.matrix.org/latest/client-server-api/#get_matrixclientv3publicrooms
|
||||
These are supposed to include extra `room_type` and `allowed_room_ids`
|
||||
fields, as described in MSC2946.
|
||||
|
||||
Contains `None` for rooms to which the remote homeserver thinks we do not
|
||||
have access.
|
||||
|
||||
Raises:
|
||||
SynapseError: if none of the remote servers provided us with the space's
|
||||
children.
|
||||
"""
|
||||
(
|
||||
room,
|
||||
children_chunks,
|
||||
inaccessible_children,
|
||||
) = await self._federation_client.get_room_hierarchy(
|
||||
via, space_id, suggested_only=False
|
||||
)
|
||||
|
||||
child_events: List[JsonDict] = room["children_state"]
|
||||
children = [
|
||||
(child_event["room_id"], child_event["content"]["via"])
|
||||
for child_event in child_events
|
||||
]
|
||||
|
||||
room_chunks: Dict[str, Optional[JsonDict]] = {}
|
||||
room_chunks.update((room_id, None) for room_id in inaccessible_children)
|
||||
room_chunks.update(
|
||||
(room_chunk["room_id"], room_chunk) for room_chunk in children_chunks
|
||||
)
|
||||
|
||||
return children, room_chunks
|
||||
@@ -66,6 +66,7 @@ from synapse.rest.admin.rooms import (
|
||||
RoomStateRestServlet,
|
||||
)
|
||||
from synapse.rest.admin.server_notice_servlet import SendServerNoticeServlet
|
||||
from synapse.rest.admin.space import RemoveSpaceMemberRestServlet
|
||||
from synapse.rest.admin.statistics import UserMediaStatisticsRestServlet
|
||||
from synapse.rest.admin.username_available import UsernameAvailableRestServlet
|
||||
from synapse.rest.admin.users import (
|
||||
@@ -267,6 +268,7 @@ def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None:
|
||||
RegistrationTokenRestServlet(hs).register(http_server)
|
||||
DestinationsRestServlet(hs).register(http_server)
|
||||
ListDestinationsRestServlet(hs).register(http_server)
|
||||
RemoveSpaceMemberRestServlet(hs).register(http_server)
|
||||
|
||||
# Some servlets only get registered for the main process.
|
||||
if hs.config.worker.worker_app is None:
|
||||
|
||||
169
synapse/rest/admin/space.py
Normal file
169
synapse/rest/admin/space.py
Normal file
@@ -0,0 +1,169 @@
|
||||
# Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
import logging
|
||||
from http import HTTPStatus
|
||||
from typing import TYPE_CHECKING, Dict, List, Tuple
|
||||
|
||||
from synapse.api.constants import EventTypes, JoinRules, Membership
|
||||
from synapse.api.errors import SynapseError
|
||||
from synapse.http.servlet import (
|
||||
ResolveRoomIdMixin,
|
||||
RestServlet,
|
||||
parse_json_object_from_request,
|
||||
)
|
||||
from synapse.http.site import SynapseRequest
|
||||
from synapse.rest.admin._base import admin_patterns, assert_user_is_admin
|
||||
from synapse.storage.state import StateFilter
|
||||
from synapse.types import JsonDict, UserID, create_requester
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from synapse.server import HomeServer
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RemoveSpaceMemberRestServlet(ResolveRoomIdMixin, RestServlet):
|
||||
"""
|
||||
Puppets a local user to remove them from all rooms in a space.
|
||||
"""
|
||||
|
||||
PATTERNS = admin_patterns(
|
||||
"/rooms/(?P<space_id>[^/]+)/hierarchy/members/(?P<user_id>[^/]+)$"
|
||||
)
|
||||
|
||||
def __init__(self, hs: "HomeServer"):
|
||||
super().__init__(hs)
|
||||
self._hs = hs
|
||||
self._auth = hs.get_auth()
|
||||
self._store = hs.get_datastore()
|
||||
self._room_member_handler = hs.get_room_member_handler()
|
||||
self._space_hierarchy_handler = hs.get_space_hierarchy_handler()
|
||||
|
||||
async def on_DELETE(
|
||||
self, request: SynapseRequest, space_id: str, user_id: str
|
||||
) -> Tuple[int, JsonDict]:
|
||||
"""Forces a local user to leave all non-public rooms in a space.
|
||||
|
||||
The space itself is always left, regardless of whether it is public.
|
||||
|
||||
May succeed partially if the user fails to leave some rooms.
|
||||
|
||||
Returns:
|
||||
A tuple containing the HTTP status code and a JSON dictionary containing:
|
||||
* `left_rooms`: A list of rooms that the user has been made to leave.
|
||||
* `inaccessible_rooms`: A list of rooms and spaces that the local
|
||||
homeserver is not in, and may have not been fully processed. Rooms may
|
||||
appear here if:
|
||||
* The room is a space that the local homeserver is not in, and so its
|
||||
full list of child rooms could not be determined.
|
||||
* The room is inaccessible to the local homeserver, and it is not known
|
||||
whether the room is a subspace containing further rooms.
|
||||
* `failed_rooms`: A dictionary of errors encountered when leaving rooms.
|
||||
The keys of the dictionary are room IDs and the values of the dictionary
|
||||
are error messages.
|
||||
"""
|
||||
requester = await self._auth.get_user_by_req(request)
|
||||
await assert_user_is_admin(self._auth, requester.user)
|
||||
|
||||
content = parse_json_object_from_request(request, allow_empty_body=True)
|
||||
include_remote_spaces = content.get("include_remote_spaces", True)
|
||||
if not isinstance(include_remote_spaces, bool):
|
||||
raise SynapseError(
|
||||
HTTPStatus.BAD_REQUEST,
|
||||
"'include_remote_spaces' parameter must be a boolean",
|
||||
)
|
||||
|
||||
space_id, _ = await self.resolve_room_id(space_id)
|
||||
|
||||
target_user = UserID.from_string(user_id)
|
||||
|
||||
if not self._hs.is_mine(target_user):
|
||||
raise SynapseError(
|
||||
HTTPStatus.BAD_REQUEST,
|
||||
"This endpoint can only be used with local users",
|
||||
)
|
||||
|
||||
# Fetch the list of rooms the target user is currently in
|
||||
user_rooms = await self._store.get_rooms_for_local_user_where_membership_is(
|
||||
user_id, [Membership.INVITE, Membership.JOIN, Membership.KNOCK]
|
||||
)
|
||||
user_room_ids = {room.room_id for room in user_rooms}
|
||||
|
||||
# Fetch the list of rooms in the space hierarchy
|
||||
(
|
||||
descendants,
|
||||
inaccessible_room_ids,
|
||||
) = await self._space_hierarchy_handler.get_space_descendants(
|
||||
space_id, enable_federation=include_remote_spaces
|
||||
)
|
||||
|
||||
# Determine which rooms to leave by checking join rules
|
||||
rooms_to_leave: List[str] = []
|
||||
state_filter = StateFilter.from_types([(EventTypes.JoinRules, "")])
|
||||
for room_id, _via in descendants:
|
||||
if room_id not in user_room_ids:
|
||||
# The user is not in this room. There is nothing to do here.
|
||||
continue
|
||||
|
||||
current_state_ids = await self._store.get_filtered_current_state_ids(
|
||||
room_id, state_filter
|
||||
)
|
||||
join_rules_event_id = current_state_ids.get((EventTypes.JoinRules, ""))
|
||||
if join_rules_event_id is not None:
|
||||
join_rules_event = await self._store.get_event(join_rules_event_id)
|
||||
join_rules = join_rules_event.content.get("join_rule")
|
||||
else:
|
||||
# The user is invited to or has knocked on a room that is not known
|
||||
# locally. Assume that such rooms are not public and should be left.
|
||||
# If it turns out that the room is actually public, then we've not
|
||||
# actually prevented the user from joining it.
|
||||
join_rules = None
|
||||
|
||||
# Leave the room if it is not public, or it is the root space.
|
||||
if join_rules != JoinRules.PUBLIC or room_id == space_id:
|
||||
rooms_to_leave.append(room_id)
|
||||
|
||||
# Now start leaving rooms
|
||||
failures: Dict[str, str] = {}
|
||||
left_rooms: List[str] = []
|
||||
|
||||
fake_requester = create_requester(
|
||||
target_user, authenticated_entity=requester.user.to_string()
|
||||
)
|
||||
|
||||
for room_id in rooms_to_leave:
|
||||
# There is a race condition here where the user may have left or been kicked
|
||||
# from a room since their list of memberships was fetched.
|
||||
# `update_membership` will raise if the user is no longer in the room,
|
||||
# but it's tricky to distinguish from other failure modes.
|
||||
|
||||
try:
|
||||
await self._room_member_handler.update_membership(
|
||||
requester=fake_requester,
|
||||
target=target_user,
|
||||
room_id=room_id,
|
||||
action=Membership.LEAVE,
|
||||
content={},
|
||||
ratelimit=False,
|
||||
require_consent=False,
|
||||
)
|
||||
left_rooms.append(room_id)
|
||||
except Exception as e:
|
||||
failures[room_id] = str(e)
|
||||
|
||||
return 200, {
|
||||
"left_rooms": left_rooms,
|
||||
"inaccessible_rooms": inaccessible_room_ids,
|
||||
"failed_rooms": failures,
|
||||
}
|
||||
@@ -107,6 +107,7 @@ from synapse.handlers.room_summary import RoomSummaryHandler
|
||||
from synapse.handlers.search import SearchHandler
|
||||
from synapse.handlers.send_email import SendEmailHandler
|
||||
from synapse.handlers.set_password import SetPasswordHandler
|
||||
from synapse.handlers.space_hierarchy import SpaceHierarchyHandler
|
||||
from synapse.handlers.sso import SsoHandler
|
||||
from synapse.handlers.stats import StatsHandler
|
||||
from synapse.handlers.sync import SyncHandler
|
||||
@@ -795,6 +796,10 @@ class HomeServer(metaclass=abc.ABCMeta):
|
||||
def get_account_data_handler(self) -> AccountDataHandler:
|
||||
return AccountDataHandler(self)
|
||||
|
||||
@cache_in_self
|
||||
def get_space_hierarchy_handler(self) -> SpaceHierarchyHandler:
|
||||
return SpaceHierarchyHandler(self)
|
||||
|
||||
@cache_in_self
|
||||
def get_room_summary_handler(self) -> RoomSummaryHandler:
|
||||
return RoomSummaryHandler(self)
|
||||
|
||||
@@ -28,7 +28,7 @@ from synapse.api.constants import (
|
||||
from synapse.api.errors import AuthError, NotFoundError, SynapseError
|
||||
from synapse.api.room_versions import RoomVersions
|
||||
from synapse.events import make_event_from_dict
|
||||
from synapse.handlers.room_summary import _child_events_comparison_key, _RoomEntry
|
||||
from synapse.handlers.room_summary import _RoomEntry, child_events_comparison_key
|
||||
from synapse.rest import admin
|
||||
from synapse.rest.client import login, room
|
||||
from synapse.server import HomeServer
|
||||
@@ -48,7 +48,7 @@ def _create_event(room_id: str, order: Optional[Any] = None, origin_server_ts: i
|
||||
|
||||
|
||||
def _order(*events):
|
||||
return sorted(events, key=_child_events_comparison_key)
|
||||
return sorted(events, key=child_events_comparison_key)
|
||||
|
||||
|
||||
class TestSpaceSummarySort(unittest.TestCase):
|
||||
|
||||
239
tests/handlers/test_space_hierarchy.py
Normal file
239
tests/handlers/test_space_hierarchy.py
Normal file
@@ -0,0 +1,239 @@
|
||||
# Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from typing import Dict, Iterable, Mapping, NoReturn, Optional, Sequence, Tuple
|
||||
from unittest import mock
|
||||
|
||||
from twisted.test.proto_helpers import MemoryReactor
|
||||
|
||||
from synapse.api.constants import EventContentFields, EventTypes, RoomTypes
|
||||
from synapse.handlers.space_hierarchy import SpaceHierarchyHandler
|
||||
from synapse.rest import admin
|
||||
from synapse.rest.client import login, room
|
||||
from synapse.server import HomeServer
|
||||
from synapse.types import JsonDict
|
||||
from synapse.util import Clock
|
||||
|
||||
from tests import unittest
|
||||
|
||||
|
||||
class SpaceDescendantsTestCase(unittest.HomeserverTestCase):
|
||||
"""Tests iteration over the descendants of a space."""
|
||||
|
||||
servlets = [
|
||||
admin.register_servlets_for_client_rest_resource,
|
||||
login.register_servlets,
|
||||
room.register_servlets,
|
||||
]
|
||||
|
||||
def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer):
|
||||
self.hs = hs
|
||||
self.handler = self.hs.get_space_hierarchy_handler()
|
||||
|
||||
# Create a user.
|
||||
self.user = self.register_user("user", "pass")
|
||||
self.token = self.login("user", "pass")
|
||||
|
||||
# Create a space and a child room.
|
||||
self.space = self.helper.create_room_as(
|
||||
self.user,
|
||||
tok=self.token,
|
||||
extra_content={
|
||||
"creation_content": {EventContentFields.ROOM_TYPE: RoomTypes.SPACE}
|
||||
},
|
||||
)
|
||||
self.room = self.helper.create_room_as(self.user, tok=self.token)
|
||||
self._add_child(self.space, self.room)
|
||||
|
||||
def _add_child(
|
||||
self, space_id: str, room_id: str, order: Optional[str] = None
|
||||
) -> None:
|
||||
"""Adds a room to a space."""
|
||||
content: JsonDict = {"via": [self.hs.hostname]}
|
||||
if order is not None:
|
||||
content["order"] = order
|
||||
self.helper.send_state(
|
||||
space_id,
|
||||
event_type=EventTypes.SpaceChild,
|
||||
body=content,
|
||||
tok=self.token,
|
||||
state_key=room_id,
|
||||
)
|
||||
|
||||
def _create_space(self) -> str:
|
||||
"""Creates a space."""
|
||||
return self._create_room(
|
||||
extra_content={
|
||||
"creation_content": {EventContentFields.ROOM_TYPE: RoomTypes.SPACE}
|
||||
},
|
||||
)
|
||||
|
||||
def _create_room(self, extra_content: Optional[Dict] = None) -> str:
|
||||
"""Creates a room."""
|
||||
return self.helper.create_room_as(
|
||||
self.user,
|
||||
tok=self.token,
|
||||
extra_content=extra_content,
|
||||
)
|
||||
|
||||
def test_empty_space(self):
|
||||
"""Tests iteration over an empty space."""
|
||||
space_id = self._create_space()
|
||||
|
||||
descendants, inaccessible_room_ids = self.get_success(
|
||||
self.handler.get_space_descendants(space_id)
|
||||
)
|
||||
|
||||
self.assertEqual(descendants, [(space_id, [])])
|
||||
self.assertEqual(inaccessible_room_ids, [])
|
||||
|
||||
def test_invalid_space(self):
|
||||
"""Tests iteration over an inaccessible space."""
|
||||
space_id = f"!invalid:{self.hs.hostname}"
|
||||
|
||||
descendants, inaccessible_room_ids = self.get_success(
|
||||
self.handler.get_space_descendants(space_id)
|
||||
)
|
||||
|
||||
self.assertEqual(descendants, [(space_id, [])])
|
||||
self.assertEqual(inaccessible_room_ids, [space_id])
|
||||
|
||||
def test_invalid_room(self):
|
||||
"""Tests iteration over a space containing an inaccessible room."""
|
||||
space_id = self._create_space()
|
||||
room_id = f"!invalid:{self.hs.hostname}"
|
||||
self._add_child(space_id, room_id)
|
||||
|
||||
descendants, inaccessible_room_ids = self.get_success(
|
||||
self.handler.get_space_descendants(space_id)
|
||||
)
|
||||
|
||||
self.assertEqual(descendants, [(space_id, []), (room_id, [self.hs.hostname])])
|
||||
self.assertEqual(inaccessible_room_ids, [room_id])
|
||||
|
||||
def test_remote_space_with_federation_enabled(self):
|
||||
"""Tests iteration over a remote space with federation enabled."""
|
||||
space_id = "!space:remote"
|
||||
room_id = "!room:remote"
|
||||
|
||||
async def _get_space_children_remote(
|
||||
_self: SpaceHierarchyHandler, space_id: str, via: Iterable[str]
|
||||
) -> Tuple[
|
||||
Sequence[Tuple[str, Iterable[str]]], Mapping[str, Optional[JsonDict]]
|
||||
]:
|
||||
if space_id == "!space:remote":
|
||||
self.assertEqual(via, ["remote"])
|
||||
return [("!room:remote", ["remote"])], {}
|
||||
elif space_id == "!room:remote":
|
||||
self.assertEqual(via, ["remote"])
|
||||
return [], {}
|
||||
else:
|
||||
self.fail(
|
||||
f"Unexpected _get_space_children_remote({space_id!r}, {via!r}) call"
|
||||
)
|
||||
raise # `fail` is missing type hints
|
||||
|
||||
with mock.patch(
|
||||
"synapse.handlers.space_hierarchy.SpaceHierarchyHandler._get_space_children_remote",
|
||||
new=_get_space_children_remote,
|
||||
):
|
||||
descendants, inaccessible_room_ids = self.get_success(
|
||||
self.handler.get_space_descendants(
|
||||
space_id, via=["remote"], enable_federation=True
|
||||
)
|
||||
)
|
||||
|
||||
self.assertEqual(descendants, [(space_id, ["remote"]), (room_id, ["remote"])])
|
||||
self.assertEqual(inaccessible_room_ids, [space_id, room_id])
|
||||
|
||||
def test_remote_space_with_federation_disabled(self):
|
||||
"""Tests iteration over a remote space with federation disabled."""
|
||||
space_id = "!space:remote"
|
||||
|
||||
async def _get_space_children_remote(
|
||||
_self: SpaceHierarchyHandler, space_id: str, via: Iterable[str]
|
||||
) -> NoReturn:
|
||||
self.fail(
|
||||
f"Unexpected _get_space_children_remote({space_id!r}, {via!r}) call"
|
||||
)
|
||||
raise # `fail` is missing type hints
|
||||
|
||||
with mock.patch(
|
||||
"synapse.handlers.space_hierarchy.SpaceHierarchyHandler._get_space_children_remote",
|
||||
new=_get_space_children_remote,
|
||||
):
|
||||
descendants, inaccessible_room_ids = self.get_success(
|
||||
self.handler.get_space_descendants(
|
||||
space_id, via=["remote"], enable_federation=False
|
||||
)
|
||||
)
|
||||
|
||||
self.assertEqual(descendants, [(space_id, ["remote"])])
|
||||
self.assertEqual(inaccessible_room_ids, [space_id])
|
||||
|
||||
def test_cycle(self):
|
||||
"""Tests iteration over a cyclic space."""
|
||||
# space_id
|
||||
# - subspace_id
|
||||
# - space_id
|
||||
space_id = self._create_space()
|
||||
subspace_id = self._create_space()
|
||||
self._add_child(space_id, subspace_id)
|
||||
self._add_child(subspace_id, space_id)
|
||||
|
||||
descendants, inaccessible_room_ids = self.get_success(
|
||||
self.handler.get_space_descendants(space_id)
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
descendants, [(space_id, []), (subspace_id, [self.hs.hostname])]
|
||||
)
|
||||
self.assertEqual(inaccessible_room_ids, [])
|
||||
|
||||
def test_duplicates(self):
|
||||
"""Tests iteration over a space with repeated rooms."""
|
||||
# space_id
|
||||
# - subspace_id
|
||||
# - duplicate_room_1_id
|
||||
# - duplicate_room_2_id
|
||||
# - room_id
|
||||
# - duplicate_room_1_id
|
||||
# - duplicate_room_2_id
|
||||
space_id = self._create_space()
|
||||
subspace_id = self._create_space()
|
||||
room_id = self._create_room()
|
||||
duplicate_room_1_id = self._create_room()
|
||||
duplicate_room_2_id = self._create_room()
|
||||
self._add_child(space_id, subspace_id, order="1")
|
||||
self._add_child(space_id, duplicate_room_1_id, order="2")
|
||||
self._add_child(space_id, duplicate_room_2_id, order="3")
|
||||
self._add_child(subspace_id, duplicate_room_1_id, order="1")
|
||||
self._add_child(subspace_id, duplicate_room_2_id, order="2")
|
||||
self._add_child(subspace_id, room_id, order="3")
|
||||
|
||||
descendants, inaccessible_room_ids = self.get_success(
|
||||
self.handler.get_space_descendants(space_id)
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
descendants,
|
||||
[
|
||||
(space_id, []),
|
||||
(subspace_id, [self.hs.hostname]),
|
||||
(room_id, [self.hs.hostname]),
|
||||
(duplicate_room_1_id, [self.hs.hostname]),
|
||||
(duplicate_room_2_id, [self.hs.hostname]),
|
||||
],
|
||||
)
|
||||
self.assertEqual(inaccessible_room_ids, [])
|
||||
399
tests/rest/admin/test_space.py
Normal file
399
tests/rest/admin/test_space.py
Normal file
@@ -0,0 +1,399 @@
|
||||
# Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from typing import (
|
||||
Dict,
|
||||
Iterable,
|
||||
List,
|
||||
Mapping,
|
||||
NoReturn,
|
||||
Optional,
|
||||
Sequence,
|
||||
Tuple,
|
||||
Union,
|
||||
)
|
||||
from unittest import mock
|
||||
|
||||
from typing_extensions import Literal
|
||||
|
||||
from twisted.test.proto_helpers import MemoryReactor
|
||||
|
||||
import synapse.rest.admin
|
||||
from synapse.api.constants import (
|
||||
EventContentFields,
|
||||
EventTypes,
|
||||
JoinRules,
|
||||
Membership,
|
||||
RestrictedJoinRuleTypes,
|
||||
RoomTypes,
|
||||
)
|
||||
from synapse.api.room_versions import RoomVersions
|
||||
from synapse.handlers.space_hierarchy import SpaceHierarchyHandler
|
||||
from synapse.rest.client import login, room
|
||||
from synapse.server import HomeServer
|
||||
from synapse.types import JsonDict
|
||||
from synapse.util import Clock
|
||||
|
||||
from tests import unittest
|
||||
|
||||
|
||||
class RemoveSpaceMemberTestCase(unittest.HomeserverTestCase):
|
||||
"""Tests removal of a user from a space."""
|
||||
|
||||
servlets = [
|
||||
synapse.rest.admin.register_servlets,
|
||||
login.register_servlets,
|
||||
room.register_servlets,
|
||||
]
|
||||
|
||||
def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer):
|
||||
self.store = hs.get_datastore()
|
||||
|
||||
# Create users
|
||||
self.admin_user = self.register_user("admin", "pass", admin=True)
|
||||
self.admin_user_tok = self.login("admin", "pass")
|
||||
self.space_owner_user = self.register_user("space_owner", "pass")
|
||||
self.space_owner_user_tok = self.login("space_owner", "pass")
|
||||
self.target_user = self.register_user("user", "pass")
|
||||
self.target_user_tok = self.login("user", "pass")
|
||||
|
||||
# Create a space hierarchy for testing:
|
||||
# space, invite-only
|
||||
# * subspace, restricted
|
||||
self.space_id = self._create_space(JoinRules.INVITE)
|
||||
|
||||
# Make the target user a member of the space
|
||||
self.helper.invite(
|
||||
self.space_id,
|
||||
src=self.space_owner_user,
|
||||
targ=self.target_user,
|
||||
tok=self.space_owner_user_tok,
|
||||
)
|
||||
self.helper.join(self.space_id, self.target_user, tok=self.target_user_tok)
|
||||
|
||||
self.subspace_id = self._create_space((JoinRules.RESTRICTED, self.space_id))
|
||||
self._add_child(self.space_id, self.subspace_id)
|
||||
|
||||
def _add_child(
|
||||
self, space_id: str, room_id: str, via: Optional[List[str]] = None
|
||||
) -> None:
|
||||
"""Adds a room to a space."""
|
||||
if via is None:
|
||||
via = [self.hs.hostname]
|
||||
|
||||
self.helper.send_state(
|
||||
space_id,
|
||||
event_type=EventTypes.SpaceChild,
|
||||
body={"via": via},
|
||||
tok=self.space_owner_user_tok,
|
||||
state_key=room_id,
|
||||
)
|
||||
|
||||
def _create_space(
|
||||
self,
|
||||
join_rules: Union[
|
||||
Literal["public", "invite", "knock"],
|
||||
Tuple[Literal["restricted"], str],
|
||||
],
|
||||
) -> str:
|
||||
"""Creates a space."""
|
||||
return self._create_room(
|
||||
join_rules,
|
||||
extra_content={
|
||||
"creation_content": {EventContentFields.ROOM_TYPE: RoomTypes.SPACE}
|
||||
},
|
||||
)
|
||||
|
||||
def _create_room(
|
||||
self,
|
||||
join_rules: Union[
|
||||
Literal["public", "invite", "knock"],
|
||||
Tuple[Literal["restricted"], str],
|
||||
],
|
||||
extra_content: Optional[Dict] = None,
|
||||
) -> str:
|
||||
"""Creates a room."""
|
||||
room_id = self.helper.create_room_as(
|
||||
self.space_owner_user,
|
||||
room_version=RoomVersions.V8.identifier,
|
||||
tok=self.space_owner_user_tok,
|
||||
extra_content=extra_content,
|
||||
)
|
||||
|
||||
if isinstance(join_rules, str):
|
||||
self.helper.send_state(
|
||||
room_id,
|
||||
event_type=EventTypes.JoinRules,
|
||||
body={"join_rule": join_rules},
|
||||
tok=self.space_owner_user_tok,
|
||||
)
|
||||
else:
|
||||
_, space_id = join_rules
|
||||
self.helper.send_state(
|
||||
room_id,
|
||||
event_type=EventTypes.JoinRules,
|
||||
body={
|
||||
"join_rule": JoinRules.RESTRICTED,
|
||||
"allow": [
|
||||
{
|
||||
"type": RestrictedJoinRuleTypes.ROOM_MEMBERSHIP,
|
||||
"room_id": space_id,
|
||||
"via": [self.hs.hostname],
|
||||
}
|
||||
],
|
||||
},
|
||||
tok=self.space_owner_user_tok,
|
||||
)
|
||||
|
||||
return room_id
|
||||
|
||||
def _remove_from_space(
|
||||
self,
|
||||
user_id: str,
|
||||
space_id: Optional[str] = None,
|
||||
include_remote_spaces: Optional[bool] = None,
|
||||
) -> JsonDict:
|
||||
"""Removes the given user from the test space."""
|
||||
if space_id is None:
|
||||
space_id = self.space_id
|
||||
|
||||
content: Union[bytes, JsonDict] = b""
|
||||
if include_remote_spaces is not None:
|
||||
content = {"include_remote_spaces": include_remote_spaces}
|
||||
|
||||
url = f"/_synapse/admin/v1/rooms/{self.space_id}/hierarchy/members/{user_id}"
|
||||
channel = self.make_request(
|
||||
"DELETE",
|
||||
url.encode("ascii"),
|
||||
access_token=self.admin_user_tok,
|
||||
content=content,
|
||||
)
|
||||
|
||||
self.assertEqual(200, channel.code, channel.json_body)
|
||||
|
||||
return channel.json_body
|
||||
|
||||
def test_public_space(self) -> None:
|
||||
"""Tests that the user is removed from the space, even if public."""
|
||||
self.helper.send_state(
|
||||
self.space_id,
|
||||
event_type=EventTypes.JoinRules,
|
||||
body={"join_rule": JoinRules.PUBLIC},
|
||||
tok=self.space_owner_user_tok,
|
||||
)
|
||||
|
||||
response = self._remove_from_space(self.target_user)
|
||||
self.assertEqual(
|
||||
response,
|
||||
{
|
||||
"left_rooms": [self.space_id],
|
||||
"inaccessible_rooms": [],
|
||||
"failed_rooms": {},
|
||||
},
|
||||
)
|
||||
|
||||
membership, _ = self.get_success(
|
||||
self.store.get_local_current_membership_for_user_in_room(
|
||||
self.target_user, self.space_id
|
||||
)
|
||||
)
|
||||
self.assertEqual(membership, Membership.LEAVE)
|
||||
|
||||
def test_public_room(self) -> None:
|
||||
"""Tests that the user is not removed from public rooms."""
|
||||
public_room_id = self._create_room(JoinRules.PUBLIC)
|
||||
self._add_child(self.subspace_id, public_room_id)
|
||||
|
||||
self.helper.join(public_room_id, self.target_user, tok=self.target_user_tok)
|
||||
|
||||
response = self._remove_from_space(self.target_user)
|
||||
self.assertEqual(
|
||||
response,
|
||||
{
|
||||
"left_rooms": [self.space_id],
|
||||
"inaccessible_rooms": [],
|
||||
"failed_rooms": {},
|
||||
},
|
||||
)
|
||||
|
||||
membership, _ = self.get_success(
|
||||
self.store.get_local_current_membership_for_user_in_room(
|
||||
self.target_user, public_room_id
|
||||
)
|
||||
)
|
||||
self.assertEqual(membership, Membership.JOIN)
|
||||
|
||||
def test_invited(self) -> None:
|
||||
"""Tests that the user is made to decline invites to rooms in the space."""
|
||||
invite_only_room_id = self._create_room(JoinRules.INVITE)
|
||||
self._add_child(self.subspace_id, invite_only_room_id)
|
||||
|
||||
self.helper.invite(
|
||||
invite_only_room_id,
|
||||
src=self.space_owner_user,
|
||||
targ=self.target_user,
|
||||
tok=self.space_owner_user_tok,
|
||||
)
|
||||
|
||||
response = self._remove_from_space(self.target_user)
|
||||
self.assertEqual(
|
||||
response,
|
||||
{
|
||||
"left_rooms": [self.space_id, invite_only_room_id],
|
||||
"inaccessible_rooms": [],
|
||||
"failed_rooms": {},
|
||||
},
|
||||
)
|
||||
|
||||
membership, _ = self.get_success(
|
||||
self.store.get_local_current_membership_for_user_in_room(
|
||||
self.target_user, invite_only_room_id
|
||||
)
|
||||
)
|
||||
self.assertEqual(membership, Membership.LEAVE)
|
||||
|
||||
def test_invite_only_room(self) -> None:
|
||||
"""Tests that the user is made to leave invite-only rooms."""
|
||||
invite_only_room_id = self._create_room(JoinRules.INVITE)
|
||||
self._add_child(self.subspace_id, invite_only_room_id)
|
||||
|
||||
self.helper.invite(
|
||||
invite_only_room_id,
|
||||
src=self.space_owner_user,
|
||||
targ=self.target_user,
|
||||
tok=self.space_owner_user_tok,
|
||||
)
|
||||
self.helper.join(
|
||||
invite_only_room_id, self.target_user, tok=self.target_user_tok
|
||||
)
|
||||
|
||||
response = self._remove_from_space(self.target_user)
|
||||
self.assertEqual(
|
||||
response,
|
||||
{
|
||||
"left_rooms": [self.space_id, invite_only_room_id],
|
||||
"inaccessible_rooms": [],
|
||||
"failed_rooms": {},
|
||||
},
|
||||
)
|
||||
|
||||
membership, _ = self.get_success(
|
||||
self.store.get_local_current_membership_for_user_in_room(
|
||||
self.target_user, invite_only_room_id
|
||||
)
|
||||
)
|
||||
self.assertEqual(membership, Membership.LEAVE)
|
||||
|
||||
def test_restricted_room(self) -> None:
|
||||
"""Tests that the user is made to leave restricted rooms."""
|
||||
restricted_room_id = self._create_room((JoinRules.RESTRICTED, self.space_id))
|
||||
self._add_child(self.subspace_id, restricted_room_id)
|
||||
self.helper.join(restricted_room_id, self.target_user, tok=self.target_user_tok)
|
||||
|
||||
response = self._remove_from_space(self.target_user)
|
||||
self.assertEqual(
|
||||
response,
|
||||
{
|
||||
"left_rooms": [self.space_id, restricted_room_id],
|
||||
"inaccessible_rooms": [],
|
||||
"failed_rooms": {},
|
||||
},
|
||||
)
|
||||
|
||||
membership, _ = self.get_success(
|
||||
self.store.get_local_current_membership_for_user_in_room(
|
||||
self.target_user, restricted_room_id
|
||||
)
|
||||
)
|
||||
self.assertEqual(membership, Membership.LEAVE)
|
||||
|
||||
def test_remote_space(self) -> None:
|
||||
"""Tests that the user is made to leave rooms in a remote space."""
|
||||
remote_space_id = "!space:remote"
|
||||
self._add_child(self.subspace_id, remote_space_id, via=["remote"])
|
||||
|
||||
restricted_room_id = self._create_room((JoinRules.RESTRICTED, self.space_id))
|
||||
self.helper.join(restricted_room_id, self.target_user, tok=self.target_user_tok)
|
||||
|
||||
async def _get_space_children_remote(
|
||||
_self: SpaceHierarchyHandler, space_id: str, via: Iterable[str]
|
||||
) -> Tuple[
|
||||
Sequence[Tuple[str, Iterable[str]]], Mapping[str, Optional[JsonDict]]
|
||||
]:
|
||||
self.assertEqual(space_id, remote_space_id)
|
||||
self.assertEqual(via, ["remote"])
|
||||
|
||||
return [(restricted_room_id, [self.hs.hostname])], {}
|
||||
|
||||
with mock.patch(
|
||||
"synapse.handlers.space_hierarchy.SpaceHierarchyHandler._get_space_children_remote",
|
||||
new=_get_space_children_remote,
|
||||
):
|
||||
response = self._remove_from_space(
|
||||
self.target_user, space_id="!space:remote", include_remote_spaces=True
|
||||
)
|
||||
self.assertEqual(
|
||||
response,
|
||||
{
|
||||
"left_rooms": [self.space_id, restricted_room_id],
|
||||
"inaccessible_rooms": [remote_space_id],
|
||||
"failed_rooms": {},
|
||||
},
|
||||
)
|
||||
|
||||
membership, _ = self.get_success(
|
||||
self.store.get_local_current_membership_for_user_in_room(
|
||||
self.target_user, restricted_room_id
|
||||
)
|
||||
)
|
||||
self.assertEqual(membership, Membership.LEAVE)
|
||||
|
||||
def test_remote_spaces_excluded(self) -> None:
|
||||
"""Tests the exclusion of remote spaces."""
|
||||
remote_space_id = "!space:remote"
|
||||
self._add_child(self.subspace_id, remote_space_id, via=["remote"])
|
||||
|
||||
restricted_room_id = self._create_room((JoinRules.RESTRICTED, self.space_id))
|
||||
self.helper.join(restricted_room_id, self.target_user, tok=self.target_user_tok)
|
||||
|
||||
async def _get_space_children_remote(
|
||||
_self: SpaceHierarchyHandler, space_id: str, via: Iterable[str]
|
||||
) -> NoReturn:
|
||||
self.fail(
|
||||
f"Unexpected _get_space_children_remote({space_id!r}, {via!r}) call"
|
||||
)
|
||||
raise # `fail` is missing type hints
|
||||
|
||||
with mock.patch(
|
||||
"synapse.handlers.space_hierarchy.SpaceHierarchyHandler._get_space_children_remote",
|
||||
new=_get_space_children_remote,
|
||||
):
|
||||
response = self._remove_from_space(
|
||||
self.target_user, space_id="!space:remote", include_remote_spaces=False
|
||||
)
|
||||
self.assertEqual(
|
||||
response,
|
||||
{
|
||||
"left_rooms": [self.space_id],
|
||||
"inaccessible_rooms": [remote_space_id],
|
||||
"failed_rooms": {},
|
||||
},
|
||||
)
|
||||
|
||||
membership, _ = self.get_success(
|
||||
self.store.get_local_current_membership_for_user_in_room(
|
||||
self.target_user, restricted_room_id
|
||||
)
|
||||
)
|
||||
self.assertEqual(membership, Membership.JOIN)
|
||||
Reference in New Issue
Block a user