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

Check for space membership during a remote join of a restricted room #9814

Merged
merged 9 commits into from
Apr 23, 2021
1 change: 1 addition & 0 deletions changelog.d/9814.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Update experimental support for [MSC3083](https://github.com/matrix-org/matrix-doc/pull/3083): restricting room access via group membership.
1 change: 1 addition & 0 deletions synapse/api/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ class Auth:
"""
FIXME: This class contains a mix of functions for authenticating users
of our client-server API and authenticating events added to room graphs.
The latter should be moved to synapse.handlers.event_auth.EventAuthHandler.
"""

def __init__(self, hs):
Expand Down
86 changes: 86 additions & 0 deletions synapse/handlers/event_auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# Copyright 2021 The Matrix.org Foundation C.I.C.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# 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

from synapse.api.constants import EventTypes, JoinRules
from synapse.api.room_versions import RoomVersion
from synapse.types import StateMap

if TYPE_CHECKING:
from synapse.server import HomeServer


class EventAuthHandler:
clokep marked this conversation as resolved.
Show resolved Hide resolved
"""
This class contains methods for authenticating events added to room graphs.
"""

def __init__(self, hs: "HomeServer"):
self._store = hs.get_datastore()

async def can_join_without_invite(
self, state_ids: StateMap[str], room_version: RoomVersion, user_id: str
) -> bool:
Comment on lines +32 to +34
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 identical to the original version, except changing self.store to self._store.

"""
Check whether a user can join a room without an invite.

When joining a room with restricted joined rules (as defined in MSC3083),
the membership of spaces must be checked during join.

Args:
state_ids: The state of the room as it currently is.
room_version: The room version of the room being joined.
user_id: The user joining the room.

Returns:
True if the user can join the room, false otherwise.
"""
# This only applies to room versions which support the new join rule.
if not room_version.msc3083_join_rules:
return True

# If there's no join rule, then it defaults to invite (so this doesn't apply).
join_rules_event_id = state_ids.get((EventTypes.JoinRules, ""), None)
if not join_rules_event_id:
return True

# If the join rule is not restricted, this doesn't apply.
join_rules_event = await self._store.get_event(join_rules_event_id)
if join_rules_event.content.get("join_rule") != JoinRules.MSC3083_RESTRICTED:
return True

# If allowed is of the wrong form, then only allow invited users.
allowed_spaces = join_rules_event.content.get("allow", [])
if not isinstance(allowed_spaces, list):
return False

# Get the list of joined rooms and see if there's an overlap.
joined_rooms = await self._store.get_rooms_for_user(user_id)

# Pull out the other room IDs, invalid data gets filtered.
for space in allowed_spaces:
if not isinstance(space, dict):
continue

space_id = space.get("space")
if not isinstance(space_id, str):
continue

# The user was joined to one of the spaces specified, they can join
# this room!
if space_id in joined_rooms:
return True

# The user was not in any of the required spaces.
return False
44 changes: 35 additions & 9 deletions synapse/handlers/federation.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ def __init__(self, hs: "HomeServer"):
self.is_mine_id = hs.is_mine_id
self.spam_checker = hs.get_spam_checker()
self.event_creation_handler = hs.get_event_creation_handler()
self._event_auth_handler = hs.get_event_auth_handler()
self._message_handler = hs.get_message_handler()
self._server_notices_mxid = hs.config.server_notices_mxid
self.config = hs.config
Expand Down Expand Up @@ -1673,17 +1674,47 @@ async def on_send_join_request(self, origin: str, pdu: EventBase) -> JsonDict:
# would introduce the danger of backwards-compatibility problems.
event.internal_metadata.send_on_behalf_of = origin

# Calculate the event context.
context = await self.state_handler.compute_event_context(event)
context = await self._auth_and_persist_event(origin, event, context)

# Get the state before the new event.
prev_state_ids = await context.get_prev_state_ids()

# Check if the user is already in the room or invited to the room.
user_id = event.state_key
prev_member_event_id = prev_state_ids.get((EventTypes.Member, user_id), None)
newly_joined = True
user_is_invited = False
if prev_member_event_id:
prev_member_event = await self.store.get_event(prev_member_event_id)
newly_joined = prev_member_event.membership != Membership.JOIN
user_is_invited = prev_member_event.membership == Membership.INVITE

# If the member is not already in the room, and not invited, check if
# they should be allowed access via membership in a space.
if (
newly_joined
and not user_is_invited
and not await self._event_auth_handler.can_join_without_invite(
prev_state_ids,
event.room_version,
user_id,
)
):
raise AuthError(
403,
"You do not belong to any of the required spaces to join this room.",
)

# Persist the event.
await self._auth_and_persist_event(origin, event, context)

logger.debug(
"on_send_join_request: After _auth_and_persist_event: %s, sigs: %s",
event.event_id,
event.signatures,
)

prev_state_ids = await context.get_prev_state_ids()

state_ids = list(prev_state_ids.values())
auth_chain = await self.store.get_auth_chain(event.room_id, state_ids)

Expand Down Expand Up @@ -2006,7 +2037,7 @@ async def _auth_and_persist_event(
state: Optional[Iterable[EventBase]] = None,
auth_events: Optional[MutableStateMap[EventBase]] = None,
backfilled: bool = False,
) -> EventContext:
) -> None:
Copy link
Member Author

Choose a reason for hiding this comment

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

The return value here was now never used.

"""
Process an event by performing auth checks and then persisting to the database.

Expand All @@ -2028,9 +2059,6 @@ async def _auth_and_persist_event(
event is an outlier), may be the auth events claimed by the remote
server.
backfilled: True if the event was backfilled.

Returns:
The event context.
"""
context = await self._check_event_auth(
origin,
Expand Down Expand Up @@ -2060,8 +2088,6 @@ async def _auth_and_persist_event(
)
raise

return context

async def _auth_and_persist_events(
self,
origin: str,
Expand Down
62 changes: 3 additions & 59 deletions synapse/handlers/room_member.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
from typing import TYPE_CHECKING, Iterable, List, Optional, Tuple

from synapse import types
from synapse.api.constants import AccountDataTypes, EventTypes, JoinRules, Membership
from synapse.api.constants import AccountDataTypes, EventTypes, Membership
from synapse.api.errors import (
AuthError,
Codes,
Expand All @@ -28,7 +28,6 @@
SynapseError,
)
from synapse.api.ratelimiting import Ratelimiter
from synapse.api.room_versions import RoomVersion
from synapse.events import EventBase
from synapse.events.snapshot import EventContext
from synapse.types import JsonDict, Requester, RoomAlias, RoomID, StateMap, UserID
Expand Down Expand Up @@ -64,6 +63,7 @@ def __init__(self, hs: "HomeServer"):
self.profile_handler = hs.get_profile_handler()
self.event_creation_handler = hs.get_event_creation_handler()
self.account_data_handler = hs.get_account_data_handler()
self.event_auth_handler = hs.get_event_auth_handler()

self.member_linearizer = Linearizer(name="member")

Expand Down Expand Up @@ -178,62 +178,6 @@ async def ratelimit_invite(

await self._invites_per_user_limiter.ratelimit(requester, invitee_user_id)

async def _can_join_without_invite(
self, state_ids: StateMap[str], room_version: RoomVersion, user_id: str
) -> bool:
Comment on lines -181 to -183
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 moved to a separate handler to avoid an import loop between the RoomMemberHandler and FederationHandler.

"""
Check whether a user can join a room without an invite.

When joining a room with restricted joined rules (as defined in MSC3083),
the membership of spaces must be checked during join.

Args:
state_ids: The state of the room as it currently is.
room_version: The room version of the room being joined.
user_id: The user joining the room.

Returns:
True if the user can join the room, false otherwise.
"""
# This only applies to room versions which support the new join rule.
if not room_version.msc3083_join_rules:
return True

# If there's no join rule, then it defaults to public (so this doesn't apply).
join_rules_event_id = state_ids.get((EventTypes.JoinRules, ""), None)
if not join_rules_event_id:
return True

# If the join rule is not restricted, this doesn't apply.
join_rules_event = await self.store.get_event(join_rules_event_id)
if join_rules_event.content.get("join_rule") != JoinRules.MSC3083_RESTRICTED:
return True

# If allowed is of the wrong form, then only allow invited users.
allowed_spaces = join_rules_event.content.get("allow", [])
if not isinstance(allowed_spaces, list):
return False

# Get the list of joined rooms and see if there's an overlap.
joined_rooms = await self.store.get_rooms_for_user(user_id)

# Pull out the other room IDs, invalid data gets filtered.
for space in allowed_spaces:
if not isinstance(space, dict):
continue

space_id = space.get("space")
if not isinstance(space_id, str):
continue

# The user was joined to one of the spaces specified, they can join
# this room!
if space_id in joined_rooms:
return True

# The user was not in any of the required spaces.
return False

async def _local_membership_update(
self,
requester: Requester,
Expand Down Expand Up @@ -302,7 +246,7 @@ async def _local_membership_update(
if (
newly_joined
and not user_is_invited
and not await self._can_join_without_invite(
and not await self.event_auth_handler.can_join_without_invite(
prev_state_ids, event.room_version, user_id
)
):
Expand Down
5 changes: 5 additions & 0 deletions synapse/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@
from synapse.handlers.directory import DirectoryHandler
from synapse.handlers.e2e_keys import E2eKeysHandler
from synapse.handlers.e2e_room_keys import E2eRoomKeysHandler
from synapse.handlers.event_auth import EventAuthHandler
from synapse.handlers.events import EventHandler, EventStreamHandler
from synapse.handlers.federation import FederationHandler
from synapse.handlers.groups_local import GroupsLocalHandler, GroupsLocalWorkerHandler
Expand Down Expand Up @@ -746,6 +747,10 @@ def get_account_data_handler(self) -> AccountDataHandler:
def get_space_summary_handler(self) -> SpaceSummaryHandler:
return SpaceSummaryHandler(self)

@cache_in_self
def get_event_auth_handler(self) -> EventAuthHandler:
return EventAuthHandler(self)

@cache_in_self
def get_external_cache(self) -> ExternalCache:
return ExternalCache(self)
Expand Down