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

Update the MSC3083 support to verify if joins are from an authorized server #10254

Merged
merged 52 commits into from
Jul 26, 2021
Merged
Show file tree
Hide file tree
Changes from 45 commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
6a0dd93
Sign send_{join,leave,knock} requests.
clokep Jun 25, 2021
cb8aaed
Convert compute_auth_events to async.
clokep Jun 28, 2021
f9bfc19
Include another user's membership event in the auth events.
clokep Jun 24, 2021
fd37e76
Update the auth rules to inspect event signatures.
clokep Jun 22, 2021
59de557
Only perform checks when signature checking is enabled.
clokep Jun 25, 2021
2a074d3
Do not perform a local join if the local server is not authorized.
clokep Jun 25, 2021
d2fdc1b
Newsfragment
clokep Jun 25, 2021
441a9bb
Update the room version.
clokep Jul 1, 2021
aab6ae3
Merge remote-tracking branch 'origin/develop' into clokep/restricted-…
clokep Jul 13, 2021
111bbcf
Use get_domain_from_id.
clokep Jul 13, 2021
6d7e981
Consistently default to PL 0 for invite.
clokep Jul 13, 2021
80ce8f8
Include the authorising user ID in the event content.
clokep Jul 13, 2021
1a8f171
Revert "Convert compute_auth_events to async."
clokep Jul 13, 2021
5fbc307
Check signatures of the authorising server.
clokep Jul 13, 2021
fda81ad
Conditionally sign events in /send_join
clokep Jul 13, 2021
13cfdd7
Review comments.
clokep Jul 13, 2021
0da003c
Update the auth checks to use join_authorised_via_users_server.
clokep Jul 14, 2021
2c6a34c
Do not do remote joins if the user is invited/already joined.
clokep Jul 14, 2021
6b00541
Merge remote-tracking branch 'origin/develop' into clokep/restricted-…
clokep Jul 14, 2021
09599a2
Fix local joins to restricted rooms & abstract code.
clokep Jul 14, 2021
f90db62
Merge remote-tracking branch 'origin/develop' into clokep/restricted-…
clokep Jul 16, 2021
6997b6a
Check that signature exists in event auth.
clokep Jul 16, 2021
c71f2d6
Pull all state.
clokep Jul 16, 2021
83d95a0
Sign event before verifying.
clokep Jul 16, 2021
9cddd4b
Return the signed event from send_join and persist it.
clokep Jul 16, 2021
789fdc1
Remove unused parameter.
clokep Jul 16, 2021
6cf7890
Ensure we do not sign requests for other servers.
clokep Jul 16, 2021
ded8caa
Merge remote-tracking branch 'origin/develop' into clokep/restricted-…
clokep Jul 19, 2021
110fb19
Do not attempt to make an event object if no event data is returned.
clokep Jul 19, 2021
84d21d6
Use f-strings.
clokep Jul 19, 2021
858fb10
Add comments.
clokep Jul 19, 2021
bca8e73
Use attrs instead of TypedDict.
clokep Jul 19, 2021
d8eb84e
Inline logic used once.
clokep Jul 19, 2021
9f497a0
Backout unrealted change.
clokep Jul 19, 2021
fbe0038
Simplify logic to find user with maximum PL.
clokep Jul 19, 2021
b3a4b65
Only used the returned event from /send_join if the room version supp…
clokep Jul 19, 2021
8b2cac2
Fix copy & paste error.
clokep Jul 19, 2021
05e35ce
Raise an error if an authorising user cannot be found.
clokep Jul 19, 2021
a588b7b
Raise errors according to the spec.
clokep Jul 19, 2021
381cc8e
Update error codes.
clokep Jul 20, 2021
c82c0ce
Ensure that /send_join and /make_join go to the same server.
clokep Jul 20, 2021
0437602
Merge remote-tracking branch 'origin/develop' into clokep/restricted-…
clokep Jul 20, 2021
3549b5e
Fix the default power-level of invite and ensure the chosen user can …
clokep Jul 21, 2021
9970af8
Filter to local users.
clokep Jul 21, 2021
5aa985d
Lint
clokep Jul 21, 2021
8c82dcf
Fix typo.
clokep Jul 23, 2021
4cb62e8
Merge remote-tracking branch 'origin/develop' into clokep/restricted-…
clokep Jul 23, 2021
ba070ad
Prefix the event.
clokep Jul 23, 2021
549ca5b
Reduce logging level.
clokep Jul 23, 2021
af2c6a5
Pipe the room version into auth_types_for_event and use it.
clokep Jul 23, 2021
bc2677b
Move helper code closer to callers.
clokep Jul 23, 2021
6bc22bb
Fix tests.
clokep Jul 24, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
116 changes: 110 additions & 6 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")
Comment on lines +109 to +119
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm pretty sure we need to do this here and in _check_sigs_on_pdu (since that checks the signature is valid while this just checks that a signature exists). Anyway I matched what the 3pid code did.


# Implementation of https://matrix.org/docs/spec/rooms/v1#authorization-rules
#
# 1. If type is m.room.create:
Expand Down Expand Up @@ -285,7 +297,7 @@ 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?
invite_level = _get_named_level(auth_events, "invite", 0)
ban_level = _get_named_level(auth_events, "ban", 50)

logger.debug(
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 Down Expand Up @@ -640,6 +675,66 @@ def get_user_power_level(user_id: str, auth_events: StateMap[EventBase]) -> int:
return 0


def get_users_which_can_issue_invite(auth_events: StateMap[EventBase]) -> List[str]:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a bit worried that this is returning a list of potential servers? It feels like a recipe for different implementations to do signature checks on different servers and thus potentially come to different conclusions (though it seems fairly unlikely).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is now only used to find a list of candidate servers to attempt a remote join to.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd be tempted to lift these two functions out of here, as this file is otherwise stuff purely related to the auth rules check from the spec?

"""
Return the list of users which can issue invites.

This is done by exploring the joined users and comparing their power levels
to the necessyar power level to issue an invite.

Args:
auth_events: state in force at this point in the room

Returns:
The users which can issue invites.
"""
invite_level = _get_named_level(auth_events, "invite", 0)
users_default_level = _get_named_level(auth_events, "users_default", 0)
power_level_event = _get_power_level_event(auth_events)

# Custom power-levels for users.
if power_level_event:
users = power_level_event.content.get("users", {})
else:
users = {}

result = []

# Check which members are able to invite by ensuring they're joined and have
# the necessary power level.
for (event_type, state_key), event in auth_events.items():
if event_type != EventTypes.Member:
continue

if event.membership != Membership.JOIN:
continue

# Check if the user has a custom power level.
if users.get(state_key, users_default_level) >= invite_level:
result.append(state_key)

return result


def get_servers_from_users(users: List[str]) -> Set[str]:
"""
Resolve a list of users into their servers.

Args:
users: A list of users.

Returns:
A set of servers.
"""
servers = set()
for user in users:
try:
servers.add(get_domain_from_id(user))
except SynapseError:
pass
return servers


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

Expand Down Expand Up @@ -760,4 +855,13 @@ def auth_types_for_event(event: Union[EventBase, EventBuilder]) -> Set[Tuple[str
)
auth_types.add(key)

# TODO Should this be limited to only MSC3083 rooms.
clokep marked this conversation as resolved.
Show resolved Hide resolved
if 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"]
clokep marked this conversation as resolved.
Show resolved Hide resolved
)
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