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

Commit 228decf

Browse files
authored
Update the MSC3083 support to verify if joins are from an authorized server. (#10254)
1 parent 4fb92d9 commit 228decf

File tree

17 files changed

+632
-98
lines changed

17 files changed

+632
-98
lines changed

changelog.d/10254.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Update support for [MSC3083](https://github.com/matrix-org/matrix-doc/pull/3083) to consider changes in the MSC around which servers can issue join events.

synapse/api/errors.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,9 @@ class Codes:
7575
INVALID_SIGNATURE = "M_INVALID_SIGNATURE"
7676
USER_DEACTIVATED = "M_USER_DEACTIVATED"
7777
BAD_ALIAS = "M_BAD_ALIAS"
78+
# For restricted join rules.
79+
UNABLE_AUTHORISE_JOIN = "M_UNABLE_TO_AUTHORISE_JOIN"
80+
UNABLE_TO_GRANT_JOIN = "M_UNABLE_TO_GRANT_JOIN"
7881

7982

8083
class CodeMessageException(RuntimeError):

synapse/api/room_versions.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ class RoomVersions:
168168
msc2403_knocking=False,
169169
)
170170
MSC3083 = RoomVersion(
171-
"org.matrix.msc3083",
171+
"org.matrix.msc3083.v2",
172172
RoomDisposition.UNSTABLE,
173173
EventFormatVersions.V3,
174174
StateResolutionVersions.V2,

synapse/event_auth.py

Lines changed: 61 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,18 @@ def check(
106106
if not event.signatures.get(event_id_domain):
107107
raise AuthError(403, "Event not signed by sending server")
108108

109+
is_invite_via_allow_rule = (
110+
event.type == EventTypes.Member
111+
and event.membership == Membership.JOIN
112+
and "join_authorised_via_users_server" in event.content
113+
)
114+
if is_invite_via_allow_rule:
115+
authoriser_domain = get_domain_from_id(
116+
event.content["join_authorised_via_users_server"]
117+
)
118+
if not event.signatures.get(authoriser_domain):
119+
raise AuthError(403, "Event not signed by authorising server")
120+
109121
# Implementation of https://matrix.org/docs/spec/rooms/v1#authorization-rules
110122
#
111123
# 1. If type is m.room.create:
@@ -177,7 +189,7 @@ def check(
177189
# https://github.com/vector-im/vector-web/issues/1208 hopefully
178190
if event.type == EventTypes.ThirdPartyInvite:
179191
user_level = get_user_power_level(event.user_id, auth_events)
180-
invite_level = _get_named_level(auth_events, "invite", 0)
192+
invite_level = get_named_level(auth_events, "invite", 0)
181193

182194
if user_level < invite_level:
183195
raise AuthError(403, "You don't have permission to invite users")
@@ -285,8 +297,8 @@ def _is_membership_change_allowed(
285297
user_level = get_user_power_level(event.user_id, auth_events)
286298
target_level = get_user_power_level(target_user_id, auth_events)
287299

288-
# FIXME (erikj): What should we do here as the default?
289-
ban_level = _get_named_level(auth_events, "ban", 50)
300+
invite_level = get_named_level(auth_events, "invite", 0)
301+
ban_level = get_named_level(auth_events, "ban", 50)
290302

291303
logger.debug(
292304
"_is_membership_change_allowed: %s",
@@ -336,25 +348,48 @@ def _is_membership_change_allowed(
336348
elif target_in_room: # the target is already in the room.
337349
raise AuthError(403, "%s is already in the room." % target_user_id)
338350
else:
339-
invite_level = _get_named_level(auth_events, "invite", 0)
340-
341351
if user_level < invite_level:
342352
raise AuthError(403, "You don't have permission to invite users")
343353
elif Membership.JOIN == membership:
344354
# Joins are valid iff caller == target and:
345355
# * They are not banned.
346356
# * They are accepting a previously sent invitation.
347357
# * They are already joined (it's a NOOP).
348-
# * The room is public or restricted.
358+
# * The room is public.
359+
# * The room is restricted and the user meets the allows rules.
349360
if event.user_id != target_user_id:
350361
raise AuthError(403, "Cannot force another user to join.")
351362
elif target_banned:
352363
raise AuthError(403, "You are banned from this room")
353-
elif join_rule == JoinRules.PUBLIC or (
364+
elif join_rule == JoinRules.PUBLIC:
365+
pass
366+
elif (
354367
room_version.msc3083_join_rules
355368
and join_rule == JoinRules.MSC3083_RESTRICTED
356369
):
357-
pass
370+
# This is the same as public, but the event must contain a reference
371+
# to the server who authorised the join. If the event does not contain
372+
# the proper content it is rejected.
373+
#
374+
# Note that if the caller is in the room or invited, then they do
375+
# not need to meet the allow rules.
376+
if not caller_in_room and not caller_invited:
377+
authorising_user = event.content.get("join_authorised_via_users_server")
378+
379+
if authorising_user is None:
380+
raise AuthError(403, "Join event is missing authorising user.")
381+
382+
# The authorising user must be in the room.
383+
key = (EventTypes.Member, authorising_user)
384+
member_event = auth_events.get(key)
385+
_check_joined_room(member_event, authorising_user, event.room_id)
386+
387+
authorising_user_level = get_user_power_level(
388+
authorising_user, auth_events
389+
)
390+
if authorising_user_level < invite_level:
391+
raise AuthError(403, "Join event authorised by invalid server.")
392+
358393
elif join_rule == JoinRules.INVITE or (
359394
room_version.msc2403_knocking and join_rule == JoinRules.KNOCK
360395
):
@@ -369,7 +404,7 @@ def _is_membership_change_allowed(
369404
if target_banned and user_level < ban_level:
370405
raise AuthError(403, "You cannot unban user %s." % (target_user_id,))
371406
elif target_user_id != event.user_id:
372-
kick_level = _get_named_level(auth_events, "kick", 50)
407+
kick_level = get_named_level(auth_events, "kick", 50)
373408

374409
if user_level < kick_level or user_level <= target_level:
375410
raise AuthError(403, "You cannot kick user %s." % target_user_id)
@@ -445,7 +480,7 @@ def get_send_level(
445480

446481

447482
def _can_send_event(event: EventBase, auth_events: StateMap[EventBase]) -> bool:
448-
power_levels_event = _get_power_level_event(auth_events)
483+
power_levels_event = get_power_level_event(auth_events)
449484

450485
send_level = get_send_level(event.type, event.get("state_key"), power_levels_event)
451486
user_level = get_user_power_level(event.user_id, auth_events)
@@ -485,7 +520,7 @@ def check_redaction(
485520
"""
486521
user_level = get_user_power_level(event.user_id, auth_events)
487522

488-
redact_level = _get_named_level(auth_events, "redact", 50)
523+
redact_level = get_named_level(auth_events, "redact", 50)
489524

490525
if user_level >= redact_level:
491526
return False
@@ -600,7 +635,7 @@ def _check_power_levels(
600635
)
601636

602637

603-
def _get_power_level_event(auth_events: StateMap[EventBase]) -> Optional[EventBase]:
638+
def get_power_level_event(auth_events: StateMap[EventBase]) -> Optional[EventBase]:
604639
return auth_events.get((EventTypes.PowerLevels, ""))
605640

606641

@@ -616,7 +651,7 @@ def get_user_power_level(user_id: str, auth_events: StateMap[EventBase]) -> int:
616651
Returns:
617652
the user's power level in this room.
618653
"""
619-
power_level_event = _get_power_level_event(auth_events)
654+
power_level_event = get_power_level_event(auth_events)
620655
if power_level_event:
621656
level = power_level_event.content.get("users", {}).get(user_id)
622657
if not level:
@@ -640,8 +675,8 @@ def get_user_power_level(user_id: str, auth_events: StateMap[EventBase]) -> int:
640675
return 0
641676

642677

643-
def _get_named_level(auth_events: StateMap[EventBase], name: str, default: int) -> int:
644-
power_level_event = _get_power_level_event(auth_events)
678+
def get_named_level(auth_events: StateMap[EventBase], name: str, default: int) -> int:
679+
power_level_event = get_power_level_event(auth_events)
645680

646681
if not power_level_event:
647682
return default
@@ -728,7 +763,9 @@ def get_public_keys(invite_event: EventBase) -> List[Dict[str, Any]]:
728763
return public_keys
729764

730765

731-
def auth_types_for_event(event: Union[EventBase, EventBuilder]) -> Set[Tuple[str, str]]:
766+
def auth_types_for_event(
767+
room_version: RoomVersion, event: Union[EventBase, EventBuilder]
768+
) -> Set[Tuple[str, str]]:
732769
"""Given an event, return a list of (EventType, StateKey) that may be
733770
needed to auth the event. The returned list may be a superset of what
734771
would actually be required depending on the full state of the room.
@@ -760,4 +797,12 @@ def auth_types_for_event(event: Union[EventBase, EventBuilder]) -> Set[Tuple[str
760797
)
761798
auth_types.add(key)
762799

800+
if room_version.msc3083_join_rules and membership == Membership.JOIN:
801+
if "join_authorised_via_users_server" in event.content:
802+
key = (
803+
EventTypes.Member,
804+
event.content["join_authorised_via_users_server"],
805+
)
806+
auth_types.add(key)
807+
763808
return auth_types

synapse/federation/federation_base.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,34 @@ async def _check_sigs_on_pdu(
178178
)
179179
raise SynapseError(403, errmsg, Codes.FORBIDDEN)
180180

181+
# If this is a join event for a restricted room it may have been authorised
182+
# via a different server from the sending server. Check those signatures.
183+
if (
184+
room_version.msc3083_join_rules
185+
and pdu.type == EventTypes.Member
186+
and pdu.membership == Membership.JOIN
187+
and "join_authorised_via_users_server" in pdu.content
188+
):
189+
authorising_server = get_domain_from_id(
190+
pdu.content["join_authorised_via_users_server"]
191+
)
192+
try:
193+
await keyring.verify_event_for_server(
194+
authorising_server,
195+
pdu,
196+
pdu.origin_server_ts if room_version.enforce_key_validity else 0,
197+
)
198+
except Exception as e:
199+
errmsg = (
200+
"event id %s: unable to verify signature for authorising server %s: %s"
201+
% (
202+
pdu.event_id,
203+
authorising_server,
204+
e,
205+
)
206+
)
207+
raise SynapseError(403, errmsg, Codes.FORBIDDEN)
208+
181209

182210
def _is_invite_via_3pid(event: EventBase) -> bool:
183211
return (

synapse/federation/federation_client.py

Lines changed: 49 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
import logging
2020
from typing import (
2121
TYPE_CHECKING,
22-
Any,
2322
Awaitable,
2423
Callable,
2524
Collection,
@@ -79,7 +78,15 @@ class InvalidResponseError(RuntimeError):
7978
we couldn't parse
8079
"""
8180

82-
pass
81+
82+
@attr.s(slots=True, frozen=True, auto_attribs=True)
83+
class SendJoinResult:
84+
# The event to persist.
85+
event: EventBase
86+
# A string giving the server the event was sent to.
87+
origin: str
88+
state: List[EventBase]
89+
auth_chain: List[EventBase]
8390

8491

8592
class FederationClient(FederationBase):
@@ -677,7 +684,7 @@ async def send_request(destination: str) -> Tuple[str, EventBase, RoomVersion]:
677684

678685
async def send_join(
679686
self, destinations: Iterable[str], pdu: EventBase, room_version: RoomVersion
680-
) -> Dict[str, Any]:
687+
) -> SendJoinResult:
681688
"""Sends a join event to one of a list of homeservers.
682689
683690
Doing so will cause the remote server to add the event to the graph,
@@ -691,18 +698,38 @@ async def send_join(
691698
did the make_join)
692699
693700
Returns:
694-
a dict with members ``origin`` (a string
695-
giving the server the event was sent to, ``state`` (?) and
696-
``auth_chain``.
701+
The result of the send join request.
697702
698703
Raises:
699704
SynapseError: if the chosen remote server returns a 300/400 code, or
700705
no servers successfully handle the request.
701706
"""
702707

703-
async def send_request(destination) -> Dict[str, Any]:
708+
async def send_request(destination) -> SendJoinResult:
704709
response = await self._do_send_join(room_version, destination, pdu)
705710

711+
# If an event was returned (and expected to be returned):
712+
#
713+
# * Ensure it has the same event ID (note that the event ID is a hash
714+
# of the event fields for versions which support MSC3083).
715+
# * Ensure the signatures are good.
716+
#
717+
# Otherwise, fallback to the provided event.
718+
if room_version.msc3083_join_rules and response.event:
719+
event = response.event
720+
721+
valid_pdu = await self._check_sigs_and_hash_and_fetch_one(
722+
pdu=event,
723+
origin=destination,
724+
outlier=True,
725+
room_version=room_version,
726+
)
727+
728+
if valid_pdu is None or event.event_id != pdu.event_id:
729+
raise InvalidResponseError("Returned an invalid join event")
730+
else:
731+
event = pdu
732+
706733
state = response.state
707734
auth_chain = response.auth_events
708735

@@ -784,11 +811,21 @@ async def _execute(pdu: EventBase) -> None:
784811
% (auth_chain_create_events,)
785812
)
786813

787-
return {
788-
"state": signed_state,
789-
"auth_chain": signed_auth,
790-
"origin": destination,
791-
}
814+
return SendJoinResult(
815+
event=event,
816+
state=signed_state,
817+
auth_chain=signed_auth,
818+
origin=destination,
819+
)
820+
821+
if room_version.msc3083_join_rules:
822+
# If the join is being authorised via allow rules, we need to send
823+
# the /send_join back to the same server that was originally used
824+
# with /make_join.
825+
if "join_authorised_via_users_server" in pdu.content:
826+
destinations = [
827+
get_domain_from_id(pdu.content["join_authorised_via_users_server"])
828+
]
792829

793830
return await self._try_destination_list("send_join", destinations, send_request)
794831

0 commit comments

Comments
 (0)