synapse/tests/federation/transport/test_knocking.py

305 lines
11 KiB
Python

# Copyright 2020 Matrix.org Federation 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 collections import OrderedDict
from http import HTTPStatus
from typing import Dict, List
from synapse.api.constants import EventTypes, JoinRules, Membership
from synapse.api.room_versions import RoomVersions
from synapse.events import builder
from synapse.rest import admin
from synapse.rest.client import login, room
from synapse.server import HomeServer
from synapse.types import RoomAlias
from tests.test_utils import event_injection
from tests.unittest import FederatingHomeserverTestCase, TestCase
class KnockingStrippedStateEventHelperMixin(TestCase):
def send_example_state_events_to_room(
self,
hs: "HomeServer",
room_id: str,
sender: str,
) -> OrderedDict:
"""Adds some state to a room. State events are those that should be sent to a knocking
user after they knock on the room, as well as some state that *shouldn't* be sent
to the knocking user.
Args:
hs: The homeserver of the sender.
room_id: The ID of the room to send state into.
sender: The ID of the user to send state as. Must be in the room.
Returns:
The OrderedDict of event types and content that a user is expected to see
after knocking on a room.
"""
# To set a canonical alias, we'll need to point an alias at the room first.
canonical_alias = "#fancy_alias:test"
self.get_success(
self.store.create_room_alias_association(
RoomAlias.from_string(canonical_alias), room_id, ["test"]
)
)
# Send some state that we *don't* expect to be given to knocking users
self.get_success(
event_injection.inject_event(
hs,
room_version=RoomVersions.V7.identifier,
room_id=room_id,
sender=sender,
type="com.example.secret",
state_key="",
content={"secret": "password"},
)
)
# We use an OrderedDict here to ensure that the knock membership appears last.
# Note that order only matters when sending stripped state to clients, not federated
# homeservers.
room_state = OrderedDict(
[
# We need to set the room's join rules to allow knocking
(
EventTypes.JoinRules,
{"content": {"join_rule": JoinRules.KNOCK}, "state_key": ""},
),
# Below are state events that are to be stripped and sent to clients
(
EventTypes.Name,
{"content": {"name": "A cool room"}, "state_key": ""},
),
(
EventTypes.RoomAvatar,
{
"content": {
"info": {
"h": 398,
"mimetype": "image/jpeg",
"size": 31037,
"w": 394,
},
"url": "mxc://example.org/JWEIFJgwEIhweiWJE",
},
"state_key": "",
},
),
(
EventTypes.RoomEncryption,
{"content": {"algorithm": "m.megolm.v1.aes-sha2"}, "state_key": ""},
),
(
EventTypes.CanonicalAlias,
{
"content": {"alias": canonical_alias, "alt_aliases": []},
"state_key": "",
},
),
(
EventTypes.Topic,
{
"content": {
"topic": "A really cool room",
},
"state_key": "",
},
),
]
)
for event_type, event_dict in room_state.items():
event_content = event_dict["content"]
state_key = event_dict["state_key"]
self.get_success(
event_injection.inject_event(
hs,
room_version=RoomVersions.V7.identifier,
room_id=room_id,
sender=sender,
type=event_type,
state_key=state_key,
content=event_content,
)
)
# Finally, we expect to see the m.room.create event of the room as part of the
# stripped state. We don't need to inject this event though.
room_state[EventTypes.Create] = {
"content": {
"creator": sender,
"room_version": RoomVersions.V7.identifier,
},
"state_key": "",
}
return room_state
def check_knock_room_state_against_room_state(
self,
knock_room_state: List[Dict],
expected_room_state: Dict,
) -> None:
"""Test a list of stripped room state events received over federation against a
dict of expected state events.
Args:
knock_room_state: The list of room state that was received over federation.
expected_room_state: A dict containing the room state we expect to see in
`knock_room_state`.
"""
for event in knock_room_state:
event_type = event["type"]
# Check that this event type is one of those that we expected.
# Note: This will also check that no excess state was included
self.assertIn(event_type, expected_room_state)
# Check the state content matches
self.assertEqual(
expected_room_state[event_type]["content"], event["content"]
)
# Check the state key is correct
self.assertEqual(
expected_room_state[event_type]["state_key"], event["state_key"]
)
# Ensure the event has been stripped
self.assertNotIn("signatures", event)
# Pop once we've found and processed a state event
expected_room_state.pop(event_type)
# Check that all expected state events were accounted for
self.assertEqual(len(expected_room_state), 0)
class FederationKnockingTestCase(
FederatingHomeserverTestCase, KnockingStrippedStateEventHelperMixin
):
servlets = [
admin.register_servlets,
room.register_servlets,
login.register_servlets,
]
def prepare(self, reactor, clock, homeserver):
self.store = homeserver.get_datastores().main
# We're not going to be properly signing events as our remote homeserver is fake,
# therefore disable event signature checks.
# Note that these checks are not relevant to this test case.
# Have this homeserver auto-approve all event signature checking.
async def approve_all_signature_checking(_, pdu):
return pdu
homeserver.get_federation_server()._check_sigs_and_hash = (
approve_all_signature_checking
)
# Have this homeserver skip event auth checks. This is necessary due to
# event auth checks ensuring that events were signed by the sender's homeserver.
async def _check_event_auth(origin, event, context, *args, **kwargs):
return context
homeserver.get_federation_event_handler()._check_event_auth = _check_event_auth
return super().prepare(reactor, clock, homeserver)
def test_room_state_returned_when_knocking(self):
"""
Tests that specific, stripped state events from a room are returned after
a remote homeserver successfully knocks on a local room.
"""
user_id = self.register_user("u1", "you the one")
user_token = self.login("u1", "you the one")
fake_knocking_user_id = "@user:other.example.com"
# Create a room with a room version that includes knocking
room_id = self.helper.create_room_as(
"u1",
is_public=False,
room_version=RoomVersions.V7.identifier,
tok=user_token,
)
# Update the join rules and add additional state to the room to check for later
expected_room_state = self.send_example_state_events_to_room(
self.hs, room_id, user_id
)
channel = self.make_signed_federation_request(
"GET",
"/_matrix/federation/v1/make_knock/%s/%s?ver=%s"
% (
room_id,
fake_knocking_user_id,
# Inform the remote that we support the room version of the room we're
# knocking on
RoomVersions.V7.identifier,
),
)
self.assertEqual(HTTPStatus.OK, channel.code, channel.result)
# Note: We don't expect the knock membership event to be sent over federation as
# part of the stripped room state, as the knocking homeserver already has that
# event. It is only done for clients during /sync
# Extract the generated knock event json
knock_event = channel.json_body["event"]
# Check that the event has things we expect in it
self.assertEqual(knock_event["room_id"], room_id)
self.assertEqual(knock_event["sender"], fake_knocking_user_id)
self.assertEqual(knock_event["state_key"], fake_knocking_user_id)
self.assertEqual(knock_event["type"], EventTypes.Member)
self.assertEqual(knock_event["content"]["membership"], Membership.KNOCK)
# Turn the event json dict into a proper event.
# We won't sign it properly, but that's OK as we stub out event auth in `prepare`
signed_knock_event = builder.create_local_event_from_event_dict(
self.clock,
self.hs.hostname,
self.hs.signing_key,
room_version=RoomVersions.V7,
event_dict=knock_event,
)
# Convert our proper event back to json dict format
signed_knock_event_json = signed_knock_event.get_pdu_json(
self.clock.time_msec()
)
# Send the signed knock event into the room
channel = self.make_signed_federation_request(
"PUT",
"/_matrix/federation/v1/send_knock/%s/%s"
% (room_id, signed_knock_event.event_id),
signed_knock_event_json,
)
self.assertEqual(HTTPStatus.OK, channel.code, channel.result)
# Check that we got the stripped room state in return
room_state_events = channel.json_body["knock_state_events"]
# Validate the stripped room state events
self.check_knock_room_state_against_room_state(
room_state_events, expected_room_state
)