Skip to content
This repository has been archived by the owner on Apr 26, 2024. It is now read-only.

Commit

Permalink
Tests: replace mocked Authenticator with the real thing (#11913)
Browse files Browse the repository at this point in the history
If we prepopulate the test homeserver with a key for a remote homeserver, we
can make federation requests to it without having to stub out the
authenticator. This has two advantages:

 * means that what we are testing is closer to reality (ie, we now have
   complete tests for the incoming-request-authorisation flow)

 * some tests require that other objects be signed by the remote server (eg,
   the event in `/send_join`), and doing that would require a whole separate
   set of mocking out. It's much simpler just to use real keys.
  • Loading branch information
richvdh authored Feb 11, 2022
1 parent d36943c commit c3db7a0
Show file tree
Hide file tree
Showing 7 changed files with 117 additions and 44 deletions.
1 change: 1 addition & 0 deletions changelog.d/11913.misc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Tests: replace mocked `Authenticator` with the real thing.
4 changes: 2 additions & 2 deletions tests/federation/test_complexity.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ def test_complexity_simple(self):
)

# Get the room complexity
channel = self.make_request(
channel = self.make_signed_federation_request(
"GET", "/_matrix/federation/unstable/rooms/%s/complexity" % (room_1,)
)
self.assertEquals(200, channel.code)
Expand All @@ -59,7 +59,7 @@ def test_complexity_simple(self):
store.get_current_state_event_counts = lambda x: make_awaitable(500 * 1.23)

# Get the room complexity again -- make sure it's our artificial value
channel = self.make_request(
channel = self.make_signed_federation_request(
"GET", "/_matrix/federation/unstable/rooms/%s/complexity" % (room_1,)
)
self.assertEquals(200, channel.code)
Expand Down
4 changes: 2 additions & 2 deletions tests/federation/test_federation_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ def test_without_event_id(self):
room_1 = self.helper.create_room_as(u1, tok=u1_token)
self.inject_room_member(room_1, "@user:other.example.com", "join")

channel = self.make_request(
channel = self.make_signed_federation_request(
"GET", "/_matrix/federation/v1/state/%s" % (room_1,)
)
self.assertEquals(200, channel.code, channel.result)
Expand Down Expand Up @@ -145,7 +145,7 @@ def test_needs_to_be_in_room(self):

room_1 = self.helper.create_room_as(u1, tok=u1_token)

channel = self.make_request(
channel = self.make_signed_federation_request(
"GET", "/_matrix/federation/v1/state/%s" % (room_1,)
)
self.assertEquals(403, channel.code, channel.result)
Expand Down
4 changes: 2 additions & 2 deletions tests/federation/transport/test_knocking.py
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@ def test_room_state_returned_when_knocking(self):
self.hs, room_id, user_id
)

channel = self.make_request(
channel = self.make_signed_federation_request(
"GET",
"/_matrix/federation/v1/make_knock/%s/%s?ver=%s"
% (
Expand Down Expand Up @@ -288,7 +288,7 @@ def test_room_state_returned_when_knocking(self):
)

# Send the signed knock event into the room
channel = self.make_request(
channel = self.make_signed_federation_request(
"PUT",
"/_matrix/federation/v1/send_knock/%s/%s"
% (room_id, signed_knock_event.event_id),
Expand Down
6 changes: 2 additions & 4 deletions tests/federation/transport/test_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,9 @@ def test_blocked_public_room_list_over_federation(self):
"""Test that unauthenticated requests to the public rooms directory 403 when
allow_public_rooms_over_federation is False.
"""
channel = self.make_request(
channel = self.make_signed_federation_request(
"GET",
"/_matrix/federation/v1/publicRooms",
federation_auth_origin=b"example.com",
)
self.assertEquals(403, channel.code)

Expand All @@ -34,9 +33,8 @@ def test_open_public_room_list_over_federation(self):
"""Test that unauthenticated requests to the public rooms directory 200 when
allow_public_rooms_over_federation is True.
"""
channel = self.make_request(
channel = self.make_signed_federation_request(
"GET",
"/_matrix/federation/v1/publicRooms",
federation_auth_origin=b"example.com",
)
self.assertEquals(200, channel.code)
6 changes: 2 additions & 4 deletions tests/rest/client/test_third_party_rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ async def _check_event_auth(origin, event, context, *args, **kwargs):
return hs

def prepare(self, reactor, clock, homeserver):
super().prepare(reactor, clock, homeserver)
# Create some users and a room to play with during the tests
self.user_id = self.register_user("kermit", "monkey")
self.invitee = self.register_user("invitee", "hackme")
Expand Down Expand Up @@ -473,8 +474,6 @@ def test_on_new_event(self):
def _send_event_over_federation(self) -> None:
"""Send a dummy event over federation and check that the request succeeds."""
body = {
"origin": self.hs.config.server.server_name,
"origin_server_ts": self.clock.time_msec(),
"pdus": [
{
"sender": self.user_id,
Expand All @@ -492,11 +491,10 @@ def _send_event_over_federation(self) -> None:
],
}

channel = self.make_request(
channel = self.make_signed_federation_request(
method="PUT",
path="/_matrix/federation/v1/send/1",
content=body,
federation_auth_origin=self.hs.config.server.server_name.encode("utf8"),
)

self.assertEqual(channel.code, 200, channel.result)
Expand Down
136 changes: 106 additions & 30 deletions tests/unittest.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import hashlib
import hmac
import inspect
import json
import logging
import secrets
import time
Expand All @@ -36,9 +37,11 @@
)
from unittest.mock import Mock, patch

from canonicaljson import json
import canonicaljson
import signedjson.key
import unpaddedbase64

from twisted.internet.defer import Deferred, ensureDeferred, succeed
from twisted.internet.defer import Deferred, ensureDeferred
from twisted.python.failure import Failure
from twisted.python.threadpool import ThreadPool
from twisted.test.proto_helpers import MemoryReactor
Expand All @@ -49,8 +52,7 @@
from synapse import events
from synapse.api.constants import EventTypes, Membership
from synapse.config.homeserver import HomeServerConfig
from synapse.config.ratelimiting import FederationRateLimitConfig
from synapse.federation.transport import server as federation_server
from synapse.federation.transport.server import TransportLayerServer
from synapse.http.server import JsonResource
from synapse.http.site import SynapseRequest, SynapseSite
from synapse.logging.context import (
Expand All @@ -61,10 +63,10 @@
)
from synapse.rest import RegisterServletsFunc
from synapse.server import HomeServer
from synapse.storage.keys import FetchKeyResult
from synapse.types import JsonDict, UserID, create_requester
from synapse.util import Clock
from synapse.util.httpresourcetree import create_resource_tree
from synapse.util.ratelimitutils import FederationRateLimiter

from tests.server import FakeChannel, get_clock, make_request, setup_test_homeserver
from tests.test_utils import event_injection, setup_awaitable_errors
Expand Down Expand Up @@ -755,42 +757,116 @@ def inject_room_member(self, room: str, user: str, membership: Membership) -> No

class FederatingHomeserverTestCase(HomeserverTestCase):
"""
A federating homeserver that authenticates incoming requests as `other.example.com`.
A federating homeserver, set up to validate incoming federation requests
"""

def create_resource_dict(self) -> Dict[str, Resource]:
d = super().create_resource_dict()
d["/_matrix/federation"] = TestTransportLayerServer(self.hs)
return d
OTHER_SERVER_NAME = "other.example.com"
OTHER_SERVER_SIGNATURE_KEY = signedjson.key.generate_signing_key("test")

def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer):
super().prepare(reactor, clock, hs)

class TestTransportLayerServer(JsonResource):
"""A test implementation of TransportLayerServer
# poke the other server's signing key into the key store, so that we don't
# make requests for it
verify_key = signedjson.key.get_verify_key(self.OTHER_SERVER_SIGNATURE_KEY)
verify_key_id = "%s:%s" % (verify_key.alg, verify_key.version)

authenticates incoming requests as `other.example.com`.
"""
self.get_success(
hs.get_datastore().store_server_verify_keys(
from_server=self.OTHER_SERVER_NAME,
ts_added_ms=clock.time_msec(),
verify_keys=[
(
self.OTHER_SERVER_NAME,
verify_key_id,
FetchKeyResult(
verify_key=verify_key,
valid_until_ts=clock.time_msec() + 1000,
),
)
],
)
)

def create_resource_dict(self) -> Dict[str, Resource]:
d = super().create_resource_dict()
d["/_matrix/federation"] = TransportLayerServer(self.hs)
return d

def __init__(self, hs):
super().__init__(hs)
def make_signed_federation_request(
self,
method: str,
path: str,
content: Optional[JsonDict] = None,
await_result: bool = True,
custom_headers: Optional[Iterable[Tuple[AnyStr, AnyStr]]] = None,
client_ip: str = "127.0.0.1",
) -> FakeChannel:
"""Make an inbound signed federation request to this server
class Authenticator:
def authenticate_request(self, request, content):
return succeed("other.example.com")
The request is signed as if it came from "other.example.com", which our HS
already has the keys for.
"""

authenticator = Authenticator()
if custom_headers is None:
custom_headers = []
else:
custom_headers = list(custom_headers)

custom_headers.append(
(
"Authorization",
_auth_header_for_request(
origin=self.OTHER_SERVER_NAME,
destination=self.hs.hostname,
signing_key=self.OTHER_SERVER_SIGNATURE_KEY,
method=method,
path=path,
content=content,
),
)
)

ratelimiter = FederationRateLimiter(
hs.get_clock(),
FederationRateLimitConfig(
window_size=1,
sleep_limit=1,
sleep_delay=1,
reject_limit=1000,
concurrent=1000,
),
return make_request(
self.reactor,
self.site,
method=method,
path=path,
content=content,
shorthand=False,
await_result=await_result,
custom_headers=custom_headers,
client_ip=client_ip,
)

federation_server.register_servlets(hs, self, authenticator, ratelimiter)

def _auth_header_for_request(
origin: str,
destination: str,
signing_key: signedjson.key.SigningKey,
method: str,
path: str,
content: Optional[JsonDict],
) -> str:
"""Build a suitable Authorization header for an outgoing federation request"""
request_description: JsonDict = {
"method": method,
"uri": path,
"destination": destination,
"origin": origin,
}
if content is not None:
request_description["content"] = content
signature_base64 = unpaddedbase64.encode_base64(
signing_key.sign(
canonicaljson.encode_canonical_json(request_description)
).signature
)
return (
f"X-Matrix origin={origin},"
f"key={signing_key.alg}:{signing_key.version},"
f"sig={signature_base64}"
)


def override_config(extra_config):
Expand Down

0 comments on commit c3db7a0

Please sign in to comment.