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

Commit d924827

Browse files
authored
Check for space membership during a remote join of a restricted room (#9814)
When receiving a /send_join request for a room with join rules set to 'restricted', check if the user is a member of the spaces defined in the 'allow' key of the join rules. This only applies to an experimental room version, as defined in MSC3083.
1 parent 3853a7e commit d924827

File tree

6 files changed

+131
-68
lines changed

6 files changed

+131
-68
lines changed

changelog.d/9814.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Update experimental support for [MSC3083](https://github.com/matrix-org/matrix-doc/pull/3083): restricting room access via group membership.

synapse/api/auth.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ class Auth:
6565
"""
6666
FIXME: This class contains a mix of functions for authenticating users
6767
of our client-server API and authenticating events added to room graphs.
68+
The latter should be moved to synapse.handlers.event_auth.EventAuthHandler.
6869
"""
6970

7071
def __init__(self, hs):

synapse/handlers/event_auth.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# Copyright 2021 The Matrix.org Foundation C.I.C.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
from typing import TYPE_CHECKING
15+
16+
from synapse.api.constants import EventTypes, JoinRules
17+
from synapse.api.room_versions import RoomVersion
18+
from synapse.types import StateMap
19+
20+
if TYPE_CHECKING:
21+
from synapse.server import HomeServer
22+
23+
24+
class EventAuthHandler:
25+
"""
26+
This class contains methods for authenticating events added to room graphs.
27+
"""
28+
29+
def __init__(self, hs: "HomeServer"):
30+
self._store = hs.get_datastore()
31+
32+
async def can_join_without_invite(
33+
self, state_ids: StateMap[str], room_version: RoomVersion, user_id: str
34+
) -> bool:
35+
"""
36+
Check whether a user can join a room without an invite.
37+
38+
When joining a room with restricted joined rules (as defined in MSC3083),
39+
the membership of spaces must be checked during join.
40+
41+
Args:
42+
state_ids: The state of the room as it currently is.
43+
room_version: The room version of the room being joined.
44+
user_id: The user joining the room.
45+
46+
Returns:
47+
True if the user can join the room, false otherwise.
48+
"""
49+
# This only applies to room versions which support the new join rule.
50+
if not room_version.msc3083_join_rules:
51+
return True
52+
53+
# If there's no join rule, then it defaults to invite (so this doesn't apply).
54+
join_rules_event_id = state_ids.get((EventTypes.JoinRules, ""), None)
55+
if not join_rules_event_id:
56+
return True
57+
58+
# If the join rule is not restricted, this doesn't apply.
59+
join_rules_event = await self._store.get_event(join_rules_event_id)
60+
if join_rules_event.content.get("join_rule") != JoinRules.MSC3083_RESTRICTED:
61+
return True
62+
63+
# If allowed is of the wrong form, then only allow invited users.
64+
allowed_spaces = join_rules_event.content.get("allow", [])
65+
if not isinstance(allowed_spaces, list):
66+
return False
67+
68+
# Get the list of joined rooms and see if there's an overlap.
69+
joined_rooms = await self._store.get_rooms_for_user(user_id)
70+
71+
# Pull out the other room IDs, invalid data gets filtered.
72+
for space in allowed_spaces:
73+
if not isinstance(space, dict):
74+
continue
75+
76+
space_id = space.get("space")
77+
if not isinstance(space_id, str):
78+
continue
79+
80+
# The user was joined to one of the spaces specified, they can join
81+
# this room!
82+
if space_id in joined_rooms:
83+
return True
84+
85+
# The user was not in any of the required spaces.
86+
return False

synapse/handlers/federation.py

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ def __init__(self, hs: "HomeServer"):
146146
self.is_mine_id = hs.is_mine_id
147147
self.spam_checker = hs.get_spam_checker()
148148
self.event_creation_handler = hs.get_event_creation_handler()
149+
self._event_auth_handler = hs.get_event_auth_handler()
149150
self._message_handler = hs.get_message_handler()
150151
self._server_notices_mxid = hs.config.server_notices_mxid
151152
self.config = hs.config
@@ -1673,17 +1674,47 @@ async def on_send_join_request(self, origin: str, pdu: EventBase) -> JsonDict:
16731674
# would introduce the danger of backwards-compatibility problems.
16741675
event.internal_metadata.send_on_behalf_of = origin
16751676

1677+
# Calculate the event context.
16761678
context = await self.state_handler.compute_event_context(event)
1677-
context = await self._auth_and_persist_event(origin, event, context)
1679+
1680+
# Get the state before the new event.
1681+
prev_state_ids = await context.get_prev_state_ids()
1682+
1683+
# Check if the user is already in the room or invited to the room.
1684+
user_id = event.state_key
1685+
prev_member_event_id = prev_state_ids.get((EventTypes.Member, user_id), None)
1686+
newly_joined = True
1687+
user_is_invited = False
1688+
if prev_member_event_id:
1689+
prev_member_event = await self.store.get_event(prev_member_event_id)
1690+
newly_joined = prev_member_event.membership != Membership.JOIN
1691+
user_is_invited = prev_member_event.membership == Membership.INVITE
1692+
1693+
# If the member is not already in the room, and not invited, check if
1694+
# they should be allowed access via membership in a space.
1695+
if (
1696+
newly_joined
1697+
and not user_is_invited
1698+
and not await self._event_auth_handler.can_join_without_invite(
1699+
prev_state_ids,
1700+
event.room_version,
1701+
user_id,
1702+
)
1703+
):
1704+
raise AuthError(
1705+
403,
1706+
"You do not belong to any of the required spaces to join this room.",
1707+
)
1708+
1709+
# Persist the event.
1710+
await self._auth_and_persist_event(origin, event, context)
16781711

16791712
logger.debug(
16801713
"on_send_join_request: After _auth_and_persist_event: %s, sigs: %s",
16811714
event.event_id,
16821715
event.signatures,
16831716
)
16841717

1685-
prev_state_ids = await context.get_prev_state_ids()
1686-
16871718
state_ids = list(prev_state_ids.values())
16881719
auth_chain = await self.store.get_auth_chain(event.room_id, state_ids)
16891720

@@ -2006,7 +2037,7 @@ async def _auth_and_persist_event(
20062037
state: Optional[Iterable[EventBase]] = None,
20072038
auth_events: Optional[MutableStateMap[EventBase]] = None,
20082039
backfilled: bool = False,
2009-
) -> EventContext:
2040+
) -> None:
20102041
"""
20112042
Process an event by performing auth checks and then persisting to the database.
20122043
@@ -2028,9 +2059,6 @@ async def _auth_and_persist_event(
20282059
event is an outlier), may be the auth events claimed by the remote
20292060
server.
20302061
backfilled: True if the event was backfilled.
2031-
2032-
Returns:
2033-
The event context.
20342062
"""
20352063
context = await self._check_event_auth(
20362064
origin,
@@ -2060,8 +2088,6 @@ async def _auth_and_persist_event(
20602088
)
20612089
raise
20622090

2063-
return context
2064-
20652091
async def _auth_and_persist_events(
20662092
self,
20672093
origin: str,

synapse/handlers/room_member.py

Lines changed: 3 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
from typing import TYPE_CHECKING, Iterable, List, Optional, Tuple
2020

2121
from synapse import types
22-
from synapse.api.constants import AccountDataTypes, EventTypes, JoinRules, Membership
22+
from synapse.api.constants import AccountDataTypes, EventTypes, Membership
2323
from synapse.api.errors import (
2424
AuthError,
2525
Codes,
@@ -28,7 +28,6 @@
2828
SynapseError,
2929
)
3030
from synapse.api.ratelimiting import Ratelimiter
31-
from synapse.api.room_versions import RoomVersion
3231
from synapse.events import EventBase
3332
from synapse.events.snapshot import EventContext
3433
from synapse.types import JsonDict, Requester, RoomAlias, RoomID, StateMap, UserID
@@ -64,6 +63,7 @@ def __init__(self, hs: "HomeServer"):
6463
self.profile_handler = hs.get_profile_handler()
6564
self.event_creation_handler = hs.get_event_creation_handler()
6665
self.account_data_handler = hs.get_account_data_handler()
66+
self.event_auth_handler = hs.get_event_auth_handler()
6767

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

@@ -178,62 +178,6 @@ async def ratelimit_invite(
178178

179179
await self._invites_per_user_limiter.ratelimit(requester, invitee_user_id)
180180

181-
async def _can_join_without_invite(
182-
self, state_ids: StateMap[str], room_version: RoomVersion, user_id: str
183-
) -> bool:
184-
"""
185-
Check whether a user can join a room without an invite.
186-
187-
When joining a room with restricted joined rules (as defined in MSC3083),
188-
the membership of spaces must be checked during join.
189-
190-
Args:
191-
state_ids: The state of the room as it currently is.
192-
room_version: The room version of the room being joined.
193-
user_id: The user joining the room.
194-
195-
Returns:
196-
True if the user can join the room, false otherwise.
197-
"""
198-
# This only applies to room versions which support the new join rule.
199-
if not room_version.msc3083_join_rules:
200-
return True
201-
202-
# If there's no join rule, then it defaults to public (so this doesn't apply).
203-
join_rules_event_id = state_ids.get((EventTypes.JoinRules, ""), None)
204-
if not join_rules_event_id:
205-
return True
206-
207-
# If the join rule is not restricted, this doesn't apply.
208-
join_rules_event = await self.store.get_event(join_rules_event_id)
209-
if join_rules_event.content.get("join_rule") != JoinRules.MSC3083_RESTRICTED:
210-
return True
211-
212-
# If allowed is of the wrong form, then only allow invited users.
213-
allowed_spaces = join_rules_event.content.get("allow", [])
214-
if not isinstance(allowed_spaces, list):
215-
return False
216-
217-
# Get the list of joined rooms and see if there's an overlap.
218-
joined_rooms = await self.store.get_rooms_for_user(user_id)
219-
220-
# Pull out the other room IDs, invalid data gets filtered.
221-
for space in allowed_spaces:
222-
if not isinstance(space, dict):
223-
continue
224-
225-
space_id = space.get("space")
226-
if not isinstance(space_id, str):
227-
continue
228-
229-
# The user was joined to one of the spaces specified, they can join
230-
# this room!
231-
if space_id in joined_rooms:
232-
return True
233-
234-
# The user was not in any of the required spaces.
235-
return False
236-
237181
async def _local_membership_update(
238182
self,
239183
requester: Requester,
@@ -302,7 +246,7 @@ async def _local_membership_update(
302246
if (
303247
newly_joined
304248
and not user_is_invited
305-
and not await self._can_join_without_invite(
249+
and not await self.event_auth_handler.can_join_without_invite(
306250
prev_state_ids, event.room_version, user_id
307251
)
308252
):

synapse/server.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@
7777
from synapse.handlers.directory import DirectoryHandler
7878
from synapse.handlers.e2e_keys import E2eKeysHandler
7979
from synapse.handlers.e2e_room_keys import E2eRoomKeysHandler
80+
from synapse.handlers.event_auth import EventAuthHandler
8081
from synapse.handlers.events import EventHandler, EventStreamHandler
8182
from synapse.handlers.federation import FederationHandler
8283
from synapse.handlers.groups_local import GroupsLocalHandler, GroupsLocalWorkerHandler
@@ -746,6 +747,10 @@ def get_account_data_handler(self) -> AccountDataHandler:
746747
def get_space_summary_handler(self) -> SpaceSummaryHandler:
747748
return SpaceSummaryHandler(self)
748749

750+
@cache_in_self
751+
def get_event_auth_handler(self) -> EventAuthHandler:
752+
return EventAuthHandler(self)
753+
749754
@cache_in_self
750755
def get_external_cache(self) -> ExternalCache:
751756
return ExternalCache(self)

0 commit comments

Comments
 (0)