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

Commit

Permalink
Update the MSC3083 support to verify if joins are from an authorized …
Browse files Browse the repository at this point in the history
…server. (#10254)
  • Loading branch information
clokep authored Jul 26, 2021
1 parent 4fb92d9 commit 228decf
Show file tree
Hide file tree
Showing 17 changed files with 632 additions and 98 deletions.
1 change: 1 addition & 0 deletions changelog.d/10254.feature
Original file line number Diff line number Diff line change
@@ -0,0 +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.
3 changes: 3 additions & 0 deletions synapse/api/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ class Codes:
INVALID_SIGNATURE = "M_INVALID_SIGNATURE"
USER_DEACTIVATED = "M_USER_DEACTIVATED"
BAD_ALIAS = "M_BAD_ALIAS"
# For restricted join rules.
UNABLE_AUTHORISE_JOIN = "M_UNABLE_TO_AUTHORISE_JOIN"
UNABLE_TO_GRANT_JOIN = "M_UNABLE_TO_GRANT_JOIN"


class CodeMessageException(RuntimeError):
Expand Down
2 changes: 1 addition & 1 deletion synapse/api/room_versions.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ class RoomVersions:
msc2403_knocking=False,
)
MSC3083 = RoomVersion(
"org.matrix.msc3083",
"org.matrix.msc3083.v2",
RoomDisposition.UNSTABLE,
EventFormatVersions.V3,
StateResolutionVersions.V2,
Expand Down
77 changes: 61 additions & 16 deletions synapse/event_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,18 @@ def check(
if not event.signatures.get(event_id_domain):
raise AuthError(403, "Event not signed by sending server")

is_invite_via_allow_rule = (
event.type == EventTypes.Member
and event.membership == Membership.JOIN
and "join_authorised_via_users_server" in event.content
)
if is_invite_via_allow_rule:
authoriser_domain = get_domain_from_id(
event.content["join_authorised_via_users_server"]
)
if not event.signatures.get(authoriser_domain):
raise AuthError(403, "Event not signed by authorising server")

# Implementation of https://matrix.org/docs/spec/rooms/v1#authorization-rules
#
# 1. If type is m.room.create:
Expand Down Expand Up @@ -177,7 +189,7 @@ def check(
# https://github.com/vector-im/vector-web/issues/1208 hopefully
if event.type == EventTypes.ThirdPartyInvite:
user_level = get_user_power_level(event.user_id, auth_events)
invite_level = _get_named_level(auth_events, "invite", 0)
invite_level = get_named_level(auth_events, "invite", 0)

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

# FIXME (erikj): What should we do here as the default?
ban_level = _get_named_level(auth_events, "ban", 50)
invite_level = get_named_level(auth_events, "invite", 0)
ban_level = get_named_level(auth_events, "ban", 50)

logger.debug(
"_is_membership_change_allowed: %s",
Expand Down Expand Up @@ -336,25 +348,48 @@ def _is_membership_change_allowed(
elif target_in_room: # the target is already in the room.
raise AuthError(403, "%s is already in the room." % target_user_id)
else:
invite_level = _get_named_level(auth_events, "invite", 0)

if user_level < invite_level:
raise AuthError(403, "You don't have permission to invite users")
elif Membership.JOIN == membership:
# Joins are valid iff caller == target and:
# * They are not banned.
# * They are accepting a previously sent invitation.
# * They are already joined (it's a NOOP).
# * The room is public or restricted.
# * The room is public.
# * The room is restricted and the user meets the allows rules.
if event.user_id != target_user_id:
raise AuthError(403, "Cannot force another user to join.")
elif target_banned:
raise AuthError(403, "You are banned from this room")
elif join_rule == JoinRules.PUBLIC or (
elif join_rule == JoinRules.PUBLIC:
pass
elif (
room_version.msc3083_join_rules
and join_rule == JoinRules.MSC3083_RESTRICTED
):
pass
# This is the same as public, but the event must contain a reference
# to the server who authorised the join. If the event does not contain
# the proper content it is rejected.
#
# Note that if the caller is in the room or invited, then they do
# not need to meet the allow rules.
if not caller_in_room and not caller_invited:
authorising_user = event.content.get("join_authorised_via_users_server")

if authorising_user is None:
raise AuthError(403, "Join event is missing authorising user.")

# The authorising user must be in the room.
key = (EventTypes.Member, authorising_user)
member_event = auth_events.get(key)
_check_joined_room(member_event, authorising_user, event.room_id)

authorising_user_level = get_user_power_level(
authorising_user, auth_events
)
if authorising_user_level < invite_level:
raise AuthError(403, "Join event authorised by invalid server.")

elif join_rule == JoinRules.INVITE or (
room_version.msc2403_knocking and join_rule == JoinRules.KNOCK
):
Expand All @@ -369,7 +404,7 @@ def _is_membership_change_allowed(
if target_banned and user_level < ban_level:
raise AuthError(403, "You cannot unban user %s." % (target_user_id,))
elif target_user_id != event.user_id:
kick_level = _get_named_level(auth_events, "kick", 50)
kick_level = get_named_level(auth_events, "kick", 50)

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


def _can_send_event(event: EventBase, auth_events: StateMap[EventBase]) -> bool:
power_levels_event = _get_power_level_event(auth_events)
power_levels_event = get_power_level_event(auth_events)

send_level = get_send_level(event.type, event.get("state_key"), power_levels_event)
user_level = get_user_power_level(event.user_id, auth_events)
Expand Down Expand Up @@ -485,7 +520,7 @@ def check_redaction(
"""
user_level = get_user_power_level(event.user_id, auth_events)

redact_level = _get_named_level(auth_events, "redact", 50)
redact_level = get_named_level(auth_events, "redact", 50)

if user_level >= redact_level:
return False
Expand Down Expand Up @@ -600,7 +635,7 @@ def _check_power_levels(
)


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


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


def _get_named_level(auth_events: StateMap[EventBase], name: str, default: int) -> int:
power_level_event = _get_power_level_event(auth_events)
def get_named_level(auth_events: StateMap[EventBase], name: str, default: int) -> int:
power_level_event = get_power_level_event(auth_events)

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


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

if room_version.msc3083_join_rules and membership == Membership.JOIN:
if "join_authorised_via_users_server" in event.content:
key = (
EventTypes.Member,
event.content["join_authorised_via_users_server"],
)
auth_types.add(key)

return auth_types
28 changes: 28 additions & 0 deletions synapse/federation/federation_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,34 @@ async def _check_sigs_on_pdu(
)
raise SynapseError(403, errmsg, Codes.FORBIDDEN)

# If this is a join event for a restricted room it may have been authorised
# via a different server from the sending server. Check those signatures.
if (
room_version.msc3083_join_rules
and pdu.type == EventTypes.Member
and pdu.membership == Membership.JOIN
and "join_authorised_via_users_server" in pdu.content
):
authorising_server = get_domain_from_id(
pdu.content["join_authorised_via_users_server"]
)
try:
await keyring.verify_event_for_server(
authorising_server,
pdu,
pdu.origin_server_ts if room_version.enforce_key_validity else 0,
)
except Exception as e:
errmsg = (
"event id %s: unable to verify signature for authorising server %s: %s"
% (
pdu.event_id,
authorising_server,
e,
)
)
raise SynapseError(403, errmsg, Codes.FORBIDDEN)


def _is_invite_via_3pid(event: EventBase) -> bool:
return (
Expand Down
61 changes: 49 additions & 12 deletions synapse/federation/federation_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
import logging
from typing import (
TYPE_CHECKING,
Any,
Awaitable,
Callable,
Collection,
Expand Down Expand Up @@ -79,7 +78,15 @@ class InvalidResponseError(RuntimeError):
we couldn't parse
"""

pass

@attr.s(slots=True, frozen=True, auto_attribs=True)
class SendJoinResult:
# The event to persist.
event: EventBase
# A string giving the server the event was sent to.
origin: str
state: List[EventBase]
auth_chain: List[EventBase]


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

async def send_join(
self, destinations: Iterable[str], pdu: EventBase, room_version: RoomVersion
) -> Dict[str, Any]:
) -> SendJoinResult:
"""Sends a join event to one of a list of homeservers.
Doing so will cause the remote server to add the event to the graph,
Expand All @@ -691,18 +698,38 @@ async def send_join(
did the make_join)
Returns:
a dict with members ``origin`` (a string
giving the server the event was sent to, ``state`` (?) and
``auth_chain``.
The result of the send join request.
Raises:
SynapseError: if the chosen remote server returns a 300/400 code, or
no servers successfully handle the request.
"""

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

# If an event was returned (and expected to be returned):
#
# * Ensure it has the same event ID (note that the event ID is a hash
# of the event fields for versions which support MSC3083).
# * Ensure the signatures are good.
#
# Otherwise, fallback to the provided event.
if room_version.msc3083_join_rules and response.event:
event = response.event

valid_pdu = await self._check_sigs_and_hash_and_fetch_one(
pdu=event,
origin=destination,
outlier=True,
room_version=room_version,
)

if valid_pdu is None or event.event_id != pdu.event_id:
raise InvalidResponseError("Returned an invalid join event")
else:
event = pdu

state = response.state
auth_chain = response.auth_events

Expand Down Expand Up @@ -784,11 +811,21 @@ async def _execute(pdu: EventBase) -> None:
% (auth_chain_create_events,)
)

return {
"state": signed_state,
"auth_chain": signed_auth,
"origin": destination,
}
return SendJoinResult(
event=event,
state=signed_state,
auth_chain=signed_auth,
origin=destination,
)

if room_version.msc3083_join_rules:
# If the join is being authorised via allow rules, we need to send
# the /send_join back to the same server that was originally used
# with /make_join.
if "join_authorised_via_users_server" in pdu.content:
destinations = [
get_domain_from_id(pdu.content["join_authorised_via_users_server"])
]

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

Expand Down
Loading

0 comments on commit 228decf

Please sign in to comment.