|
20 | 20 | from typing import TYPE_CHECKING, Iterable, List, Optional, Tuple |
21 | 21 |
|
22 | 22 | from synapse import types |
23 | | -from synapse.api.constants import AccountDataTypes, EventTypes, Membership |
| 23 | +from synapse.api.constants import AccountDataTypes, EventTypes, JoinRules, Membership |
24 | 24 | from synapse.api.errors import ( |
25 | 25 | AuthError, |
26 | 26 | Codes, |
@@ -178,6 +178,60 @@ async def ratelimit_invite( |
178 | 178 |
|
179 | 179 | await self._invites_per_user_limiter.ratelimit(requester, invitee_user_id) |
180 | 180 |
|
| 181 | + async def _can_join_restricted_room( |
| 182 | + self, state_ids: StateMap[str], room_id: str, user_id: str |
| 183 | + ) -> bool: |
| 184 | + """ |
| 185 | + Check whether a user can join a restricted room. |
| 186 | +
|
| 187 | + Args: |
| 188 | + state_ids: The state of the room as it currently is. |
| 189 | + room_id: The room being joined. |
| 190 | + user_id: The user joining the room. |
| 191 | +
|
| 192 | + Returns: |
| 193 | + True if the user can join the room, false otherwise. |
| 194 | + """ |
| 195 | + # This only applies to room versions which support the new join rule. |
| 196 | + room_version = await self.store.get_room_version(room_id) |
| 197 | + if not room_version.msc3083_join_rules: |
| 198 | + return True |
| 199 | + |
| 200 | + # If there's no join rule, then it defaults to public (so this doesn't apply). |
| 201 | + join_rules_event_id = state_ids.get((EventTypes.JoinRules, ""), None) |
| 202 | + if not join_rules_event_id: |
| 203 | + return True |
| 204 | + |
| 205 | + # If the join rule is not restricted, this doesn't apply. |
| 206 | + join_rules_event = await self.store.get_event(join_rules_event_id) |
| 207 | + if join_rules_event.content.get("join_rule") != JoinRules.MSC3083_RESTRICTED: |
| 208 | + return True |
| 209 | + |
| 210 | + # If allowed is of the wrong form, then ignore it (and treat the room as public). |
| 211 | + allowed_spaces = join_rules_event.content.get("allow", []) |
| 212 | + if not isinstance(allowed_spaces, list): |
| 213 | + return True |
| 214 | + |
| 215 | + # Get the list of joined rooms and see if there's an overlap. |
| 216 | + joined_rooms = await self.store.get_rooms_for_user(user_id) |
| 217 | + |
| 218 | + # Pull out the other room IDs, invalid data gets filtered. |
| 219 | + for space in allowed_spaces: |
| 220 | + if not isinstance(space, dict): |
| 221 | + continue |
| 222 | + |
| 223 | + soace_id = space.get("space") |
| 224 | + if not isinstance(soace_id, str): |
| 225 | + continue |
| 226 | + |
| 227 | + # The user was joined to one of the spaces specified, they can join |
| 228 | + # this room! |
| 229 | + if soace_id in joined_rooms: |
| 230 | + return True |
| 231 | + |
| 232 | + # The user was not in any of the required spaces. |
| 233 | + return False |
| 234 | + |
181 | 235 | async def _local_membership_update( |
182 | 236 | self, |
183 | 237 | requester: Requester, |
@@ -239,6 +293,16 @@ async def _local_membership_update( |
239 | 293 | prev_member_event = await self.store.get_event(prev_member_event_id) |
240 | 294 | newly_joined = prev_member_event.membership != Membership.JOIN |
241 | 295 |
|
| 296 | + # If the member is not already in the room, check if they should be |
| 297 | + # allowed access via membership in a space. |
| 298 | + if newly_joined and not await self._can_join_restricted_room( |
| 299 | + prev_state_ids, room_id, user_id |
| 300 | + ): |
| 301 | + raise AuthError( |
| 302 | + 403, |
| 303 | + "You do not belong to any of the required spaces to join this room.", |
| 304 | + ) |
| 305 | + |
242 | 306 | # Only rate-limit if the user actually joined the room, otherwise we'll end |
243 | 307 | # up blocking profile updates. |
244 | 308 | if newly_joined and ratelimit: |
|
0 commit comments