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 8 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.
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
106 changes: 100 additions & 6 deletions synapse/event_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ def check(
room_version_obj: the version of the room
event: the event being checked.
auth_events: the existing room state.
do_sig_check: True if it should be verified that the sending server
signed the event.
do_size_check: True if the size of the event fields should be verified.

Raises:
AuthError if the checks fail
Expand Down Expand Up @@ -163,7 +166,9 @@ def check(

# 5. If type is m.room.membership
if event.type == EventTypes.Member:
_is_membership_change_allowed(room_version_obj, event, auth_events)
_is_membership_change_allowed(
room_version_obj, event, auth_events, do_sig_check
)
logger.debug("Allowing! %s", event)
return

Expand Down Expand Up @@ -221,7 +226,10 @@ def _can_federate(event: EventBase, auth_events: StateMap[EventBase]) -> bool:


def _is_membership_change_allowed(
room_version: RoomVersion, event: EventBase, auth_events: StateMap[EventBase]
room_version: RoomVersion,
event: EventBase,
auth_events: StateMap[EventBase],
do_sig_check: bool,
) -> None:
"""
Confirms that the event which changes membership is an allowed change.
Expand All @@ -230,6 +238,8 @@ def _is_membership_change_allowed(
room_version: The version of the room.
event: The event to check.
auth_events: The current auth events of the room.
do_sig_check: True if it should be verified that the sending server
signed the event.

Raises:
AuthError if the event is not allowed.
Expand Down Expand Up @@ -282,7 +292,6 @@ 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)

logger.debug(
Expand Down Expand Up @@ -342,16 +351,41 @@ def _is_membership_change_allowed(
# * 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 be signed by a server
# whose users could issue invites.
#
# Signatures are only checked once the event is fully created, e.g.
# not during make_join,
#
# Note that if the caller is in the room or invited, then they do
# not need to meet the allow rules.
if do_sig_check and not caller_in_room and not caller_invited:
clokep marked this conversation as resolved.
Show resolved Hide resolved
# Find the servers of any users who could issue invites.
authorised_servers = get_servers_from_users(
get_users_which_can_issue_invite(auth_events)
)

# Ensure one of the signatures is from one of the authorised servers.
# Note that it was previously checked that the signatures are
# valid.
for signing_server in event.signatures:
if signing_server in authorised_servers:
break
clokep marked this conversation as resolved.
Show resolved Hide resolved
else:
# No valid servers were found!
raise AuthError(403, "Join event signed by invalid server.")
elif join_rule == JoinRules.INVITE or (
room_version.msc2403_knocking and join_rule == JoinRules.KNOCK
):
Expand Down Expand Up @@ -637,6 +671,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", 50)
clokep marked this conversation as resolved.
Show resolved Hide resolved
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(UserID.from_string(user).domain)
clokep marked this conversation as resolved.
Show resolved Hide resolved
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
2 changes: 1 addition & 1 deletion synapse/events/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ async def build(
state_ids = await self._state.get_current_state_ids(
self.room_id, prev_event_ids
)
auth_event_ids = self._event_auth_handler.compute_auth_events(
auth_event_ids = await self._event_auth_handler.compute_auth_events(
self, state_ids
)

Expand Down
2 changes: 1 addition & 1 deletion synapse/federation/federation_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -712,7 +712,7 @@ async def _on_send_membership_event(

event = await self._check_sigs_and_hash(room_version, event)

return await self.handler.on_send_membership_event(origin, event)
return await self.handler.on_send_membership_event(origin, event, room_version)

async def on_event_auth(
self, origin: str, room_id: str, event_id: str
Expand Down
65 changes: 63 additions & 2 deletions synapse/handlers/event_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
# 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 typing import TYPE_CHECKING, Collection, List, Optional, Union
from typing import TYPE_CHECKING, Collection, List, Optional, Tuple, Union

from synapse import event_auth
from synapse.api.constants import (
Expand Down Expand Up @@ -52,7 +52,7 @@ async def check_from_context(
room_version_obj, event, auth_events=auth_events, do_sig_check=do_sig_check
)

def compute_auth_events(
async def compute_auth_events(
clokep marked this conversation as resolved.
Show resolved Hide resolved
self,
event: Union[EventBase, EventBuilder],
current_state_ids: StateMap[str],
Expand Down Expand Up @@ -88,8 +88,69 @@ def compute_auth_events(
if auth_ev_id:
auth_ids.append(auth_ev_id)

# If the current room is using restricted join rules, an additional event
# must be included to assert that the server has the right to authorise
# a join event.
if event.type == EventTypes.Member:
if await self.has_restricted_join_rules(
current_state_ids, event.room_version
):
clokep marked this conversation as resolved.
Show resolved Hide resolved
additional_auth_id = await self._get_user_event_which_could_invite(
event.room_id,
current_state_ids,
)
if additional_auth_id:
auth_ids.append(additional_auth_id)

return auth_ids

async def _get_user_event_which_could_invite(
self, room_id: str, current_state_ids: StateMap[str]
) -> Optional[str]:
"""
Searches the room state for a local user who has the power level necessary
to invite other users.

Args:
room_id: The room ID under search.
current_state_ids: The current state of the room.

Returns:
The event ID of the member event.

Raises:
SynapseError if no appropriate user is found.
"""
power_level_event_id = current_state_ids.get((EventTypes.PowerLevels, ""))
invite_level = 50
users_default_level = 0
if power_level_event_id:
power_level_event = await self._store.get_event(power_level_event_id)
invite_level = power_level_event.content.get("invite", invite_level)
users_default_level = power_level_event.content.get(
"users_default", users_default_level
)
users = power_level_event.content.get("users", {})
else:
users = {}

# Find the user with the highest power level.
users_in_room = await self._store.get_users_in_room(room_id)
# A tuple of the chosen user's MXID and power level.
chosen_user: Optional[Tuple[str, int]] = None
for user in users_in_room:
user_level = users.get(user, users_default_level)
if user_level >= invite_level:
if chosen_user is None or user_level >= chosen_user[1]:
chosen_user = (user, user_level)
clokep marked this conversation as resolved.
Show resolved Hide resolved

# Add that user's event ID to the list of auth events.
if chosen_user:
return current_state_ids[(EventTypes.Member, chosen_user[0])]

# TODO What to do if no event is found?
return None
clokep marked this conversation as resolved.
Show resolved Hide resolved

async def check_host_in_room(self, room_id: str, host: str) -> bool:
with Measure(self._clock, "check_host_in_room"):
return await self._store.is_host_joined(room_id, host)
Expand Down
25 changes: 20 additions & 5 deletions synapse/handlers/federation.py
Original file line number Diff line number Diff line change
Expand Up @@ -1673,7 +1673,7 @@ async def on_make_join_request(

# checking the room version will check that we've actually heard of the room
# (and return a 404 otherwise)
room_version = await self.store.get_room_version_id(room_id)
room_version = await self.store.get_room_version(room_id)

# now check that we are *still* in the room
is_in_room = await self._event_auth_handler.check_host_in_room(
Expand All @@ -1689,7 +1689,7 @@ async def on_make_join_request(
event_content = {"membership": Membership.JOIN}

builder = self.event_builder_factory.new(
room_version,
room_version.identifier,
{
"type": EventTypes.Member,
"content": event_content,
Expand All @@ -1707,10 +1707,13 @@ async def on_make_join_request(
logger.warning("Failed to create join to %s because %s", room_id, e)
raise

# Ensure the user can even join the room.
await self._check_join_restrictions(context, event)

# The remote hasn't signed it yet, obviously. We'll do the full checks
# when we get the event back in `on_send_join_request`
await self._event_auth_handler.check_from_context(
room_version, event, context, do_sig_check=False
room_version.identifier, event, context, do_sig_check=False
)

return event
Expand Down Expand Up @@ -1954,7 +1957,7 @@ async def on_make_knock_request(

@log_function
async def on_send_membership_event(
self, origin: str, event: EventBase
self, origin: str, event: EventBase, room_version: RoomVersion
) -> EventContext:
"""
We have received a join/leave/knock event for a room via send_join/leave/knock.
Expand All @@ -1976,6 +1979,7 @@ async def on_send_membership_event(
Args:
origin: The homeserver of the remote (joining/invited/knocking) user.
event: The member event that has been signed by the remote homeserver.
room_version: The room version object for the event's room.

Returns:
The context of the event after inserting it into the room graph.
Expand Down Expand Up @@ -2009,6 +2013,17 @@ async def on_send_membership_event(
# the room, so we send it on their behalf.
event.internal_metadata.send_on_behalf_of = origin

# Sign the event since we're vouching on behalf of the remote server that
# the event is valid to be sent into the room.
event.signatures.update(
compute_event_signature(
room_version,
event.get_pdu_json(),
self.hs.hostname,
self.hs.signing_key,
)
)
clokep marked this conversation as resolved.
Show resolved Hide resolved

context = await self.state_handler.compute_event_context(event)
context = await self._check_event_auth(origin, event, context)
if context.rejected:
Expand Down Expand Up @@ -2568,7 +2583,7 @@ async def _check_event_auth(

if not auth_events:
prev_state_ids = await context.get_prev_state_ids()
auth_events_ids = self._event_auth_handler.compute_auth_events(
auth_events_ids = await self._event_auth_handler.compute_auth_events(
event, prev_state_ids, for_verification=True
)
auth_events_x = await self.store.get_events(auth_events_ids)
Expand Down
4 changes: 2 additions & 2 deletions synapse/handlers/message.py
Original file line number Diff line number Diff line change
Expand Up @@ -598,7 +598,7 @@ async def create_event(
(e.type, e.state_key): e.event_id for e in auth_events
}
# Actually strip down and use the necessary auth events
auth_event_ids = self._event_auth_handler.compute_auth_events(
auth_event_ids = await self._event_auth_handler.compute_auth_events(
event=temp_event,
current_state_ids=auth_event_state_map,
for_verification=False,
Expand Down Expand Up @@ -1384,7 +1384,7 @@ async def persist_and_notify_client_event(
raise AuthError(403, "Redacting server ACL events is not permitted")

prev_state_ids = await context.get_prev_state_ids()
auth_events_ids = self._event_auth_handler.compute_auth_events(
auth_events_ids = await self._event_auth_handler.compute_auth_events(
event, prev_state_ids, for_verification=True
)
auth_events_map = await self.store.get_events(auth_events_ids)
Expand Down
Loading