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

Commit 3e8b7a5

Browse files
committed
Proof of concept for MSC3613: Combinatorial join rules
MSC: matrix-org/matrix-spec-proposals#3613 This is not a comprehensive implementation and was only built to prove that the idea works in a limited test case (knocking+restricted). The following still need to be considered/completed: * [ ] Make code quality on par with the rest of the project * [ ] Tests * [ ] Use the utility function in all the places * [ ] Update redaction algorithm * [ ] Remove auth rule changes (see MSC) * [ ] Fix handling of `allow` (see TODOs in code)
1 parent cbd82d0 commit 3e8b7a5

File tree

11 files changed

+145
-23
lines changed

11 files changed

+145
-23
lines changed

synapse/api/room_versions.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@ class RoomVersion:
8181
msc2716_historical = attr.ib(type=bool)
8282
# MSC2716: Adds support for redacting "insertion", "chunk", and "marker" events
8383
msc2716_redactions = attr.ib(type=bool)
84+
# MSC3613: Allows for concurrent join rules in a simplified manner
85+
msc3613_simplified_join_rules = attr.ib(type=bool)
8486

8587

8688
class RoomVersions:
@@ -99,6 +101,7 @@ class RoomVersions:
99101
msc2403_knocking=False,
100102
msc2716_historical=False,
101103
msc2716_redactions=False,
104+
msc3613_simplified_join_rules=False,
102105
)
103106
V2 = RoomVersion(
104107
"2",
@@ -115,6 +118,7 @@ class RoomVersions:
115118
msc2403_knocking=False,
116119
msc2716_historical=False,
117120
msc2716_redactions=False,
121+
msc3613_simplified_join_rules=False,
118122
)
119123
V3 = RoomVersion(
120124
"3",
@@ -131,6 +135,7 @@ class RoomVersions:
131135
msc2403_knocking=False,
132136
msc2716_historical=False,
133137
msc2716_redactions=False,
138+
msc3613_simplified_join_rules=False,
134139
)
135140
V4 = RoomVersion(
136141
"4",
@@ -147,6 +152,7 @@ class RoomVersions:
147152
msc2403_knocking=False,
148153
msc2716_historical=False,
149154
msc2716_redactions=False,
155+
msc3613_simplified_join_rules=False,
150156
)
151157
V5 = RoomVersion(
152158
"5",
@@ -163,6 +169,7 @@ class RoomVersions:
163169
msc2403_knocking=False,
164170
msc2716_historical=False,
165171
msc2716_redactions=False,
172+
msc3613_simplified_join_rules=False,
166173
)
167174
V6 = RoomVersion(
168175
"6",
@@ -179,6 +186,7 @@ class RoomVersions:
179186
msc2403_knocking=False,
180187
msc2716_historical=False,
181188
msc2716_redactions=False,
189+
msc3613_simplified_join_rules=False,
182190
)
183191
MSC2176 = RoomVersion(
184192
"org.matrix.msc2176",
@@ -195,6 +203,7 @@ class RoomVersions:
195203
msc2403_knocking=False,
196204
msc2716_historical=False,
197205
msc2716_redactions=False,
206+
msc3613_simplified_join_rules=False,
198207
)
199208
V7 = RoomVersion(
200209
"7",
@@ -211,6 +220,7 @@ class RoomVersions:
211220
msc2403_knocking=True,
212221
msc2716_historical=False,
213222
msc2716_redactions=False,
223+
msc3613_simplified_join_rules=False,
214224
)
215225
V8 = RoomVersion(
216226
"8",
@@ -227,6 +237,7 @@ class RoomVersions:
227237
msc2403_knocking=True,
228238
msc2716_historical=False,
229239
msc2716_redactions=False,
240+
msc3613_simplified_join_rules=False,
230241
)
231242
V9 = RoomVersion(
232243
"9",
@@ -243,6 +254,7 @@ class RoomVersions:
243254
msc2403_knocking=True,
244255
msc2716_historical=False,
245256
msc2716_redactions=False,
257+
msc3613_simplified_join_rules=False,
246258
)
247259
MSC2716v3 = RoomVersion(
248260
"org.matrix.msc2716v3",
@@ -259,6 +271,25 @@ class RoomVersions:
259271
msc2403_knocking=True,
260272
msc2716_historical=True,
261273
msc2716_redactions=True,
274+
msc3613_simplified_join_rules=False,
275+
)
276+
MSC3613 = RoomVersion(
277+
# v9 + MSC3613
278+
"org.matrix.msc3613",
279+
RoomDisposition.STABLE,
280+
EventFormatVersions.V3,
281+
StateResolutionVersions.V2,
282+
enforce_key_validity=True,
283+
special_case_aliases_auth=False,
284+
strict_canonicaljson=True,
285+
limit_notifications_power_levels=True,
286+
msc2176_redaction_rules=False,
287+
msc3083_join_rules=True,
288+
msc3375_redaction_rules=True,
289+
msc2403_knocking=True,
290+
msc2716_historical=False,
291+
msc2716_redactions=False,
292+
msc3613_simplified_join_rules=True,
262293
)
263294

264295

@@ -276,6 +307,7 @@ class RoomVersions:
276307
RoomVersions.V8,
277308
RoomVersions.V9,
278309
RoomVersions.MSC2716v3,
310+
RoomVersions.MSC3613,
279311
)
280312
}
281313

synapse/event_auth.py

Lines changed: 35 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
from synapse.events import EventBase
3838
from synapse.events.builder import EventBuilder
3939
from synapse.types import StateMap, UserID, get_domain_from_id
40+
from synapse.util.join_rules import is_join_rule as is_join_rule_in_version
4041

4142
logger = logging.getLogger(__name__)
4243

@@ -225,6 +226,29 @@ def check_auth_rules_for_event(
225226

226227
_check_event_sender_in_room(event, auth_dict)
227228

229+
# MSC3613: Combination join rules
230+
if event.type == EventTypes.JoinRules and room_version_obj.msc3613_simplified_join_rules:
231+
if not event.is_state():
232+
raise AuthError(403, "Join rules event must be a state event")
233+
if event.state_key != "":
234+
raise AuthError(403, "Join rules event must have empty state_key")
235+
236+
# TODO: Function-ize this?
237+
238+
if not event.content.get("join_rule", None):
239+
raise AuthError(403, "A join_rule must be specified")
240+
241+
arr = event.content.get("join_rules", [])
242+
if arr and not isinstance(arr, list):
243+
raise AuthError(403, "join_rules must be an array")
244+
if arr:
245+
for rule in arr:
246+
if not rule.get("join_rule"):
247+
raise AuthError(403, "A join_rule must be specified for each rule")
248+
249+
# pass
250+
return
251+
228252
# Special case to allow m.room.third_party_invite events wherever
229253
# a user is allowed to issue invites. Fixes
230254
# https://github.com/vector-im/vector-web/issues/1208 hopefully
@@ -333,11 +357,10 @@ def _is_membership_change_allowed(
333357
target_banned = target and target.membership == Membership.BAN
334358

335359
key = (EventTypes.JoinRules, "")
336-
join_rule_event = auth_events.get(key)
337-
if join_rule_event:
338-
join_rule = join_rule_event.content.get("join_rule", JoinRules.INVITE)
339-
else:
340-
join_rule = JoinRules.INVITE
360+
join_rule_event: EventBase = auth_events.get(key)
361+
362+
def is_join_rule(rule: JoinRules) -> bool:
363+
return is_join_rule_in_version(room_version, join_rule_event, rule)
341364

342365
user_level = get_user_power_level(event.user_id, auth_events)
343366
target_level = get_user_power_level(target_user_id, auth_events)
@@ -354,7 +377,7 @@ def _is_membership_change_allowed(
354377
"target_banned": target_banned,
355378
"target_in_room": target_in_room,
356379
"membership": membership,
357-
"join_rule": join_rule,
380+
"join_rules": join_rule_event.content if join_rule_event else "unset",
358381
"target_user_id": target_user_id,
359382
"event.user_id": event.user_id,
360383
},
@@ -369,7 +392,7 @@ def _is_membership_change_allowed(
369392

370393
# Require the user to be in the room for membership changes other than join/knock.
371394
if Membership.JOIN != membership and (
372-
RoomVersion.msc2403_knocking and Membership.KNOCK != membership
395+
room_version.msc2403_knocking and Membership.KNOCK != membership
373396
):
374397
# If the user has been invited or has knocked, they are allowed to change their
375398
# membership event to leave
@@ -406,9 +429,9 @@ def _is_membership_change_allowed(
406429
raise AuthError(403, "Cannot force another user to join.")
407430
elif target_banned:
408431
raise AuthError(403, "You are banned from this room")
409-
elif join_rule == JoinRules.PUBLIC:
432+
elif is_join_rule(JoinRules.PUBLIC):
410433
pass
411-
elif room_version.msc3083_join_rules and join_rule == JoinRules.RESTRICTED:
434+
elif room_version.msc3083_join_rules and is_join_rule(JoinRules.RESTRICTED):
412435
# This is the same as public, but the event must contain a reference
413436
# to the server who authorised the join. If the event does not contain
414437
# the proper content it is rejected.
@@ -434,8 +457,8 @@ def _is_membership_change_allowed(
434457
if authorising_user_level < invite_level:
435458
raise AuthError(403, "Join event authorised by invalid server.")
436459

437-
elif join_rule == JoinRules.INVITE or (
438-
room_version.msc2403_knocking and join_rule == JoinRules.KNOCK
460+
elif is_join_rule(JoinRules.INVITE) or (
461+
room_version.msc2403_knocking and is_join_rule(JoinRules.KNOCK)
439462
):
440463
if not caller_in_room and not caller_invited:
441464
raise AuthError(403, "You are not invited to this room.")
@@ -456,7 +479,7 @@ def _is_membership_change_allowed(
456479
if user_level < ban_level or user_level <= target_level:
457480
raise AuthError(403, "You don't have permission to ban")
458481
elif room_version.msc2403_knocking and Membership.KNOCK == membership:
459-
if join_rule != JoinRules.KNOCK:
482+
if not is_join_rule(JoinRules.KNOCK):
460483
raise AuthError(403, "You don't have permission to knock")
461484
elif target_user_id != event.user_id:
462485
raise AuthError(403, "You cannot knock for other users")

synapse/handlers/event_auth.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
from synapse.events.builder import EventBuilder
2929
from synapse.events.snapshot import EventContext
3030
from synapse.types import StateMap, get_domain_from_id
31+
from synapse.util.join_rules import is_join_rule, get_all_allow_lists
3132
from synapse.util.metrics import Measure
3233

3334
if TYPE_CHECKING:
@@ -198,7 +199,7 @@ async def check_restricted_join_rules(
198199

199200
# Get the rooms which allow access to this room and check if the user is
200201
# in any of them.
201-
allowed_rooms = await self.get_rooms_that_allow_join(state_ids)
202+
allowed_rooms = await self.get_rooms_that_allow_join(state_ids, room_version)
202203
if not await self.is_user_in_rooms(allowed_rooms, user_id):
203204

204205
# If this is a remote request, the user might be in an allowed room
@@ -241,16 +242,17 @@ async def has_restricted_join_rules(
241242

242243
# If the join rule is not restricted, this doesn't apply.
243244
join_rules_event = await self._store.get_event(join_rules_event_id)
244-
return join_rules_event.content.get("join_rule") == JoinRules.RESTRICTED
245+
return is_join_rule(room_version, join_rules_event, JoinRules.RESTRICTED)
245246

246247
async def get_rooms_that_allow_join(
247-
self, state_ids: StateMap[str]
248+
self, state_ids: StateMap[str], room_version: RoomVersion,
248249
) -> Collection[str]:
249250
"""
250251
Generate a list of rooms in which membership allows access to a room.
251252
252253
Args:
253254
state_ids: The current state of the room the user wishes to join
255+
room_version: The version of the room
254256
255257
Returns:
256258
A collection of room IDs. Membership in any of the rooms in the list grants the ability to join the target room.
@@ -264,7 +266,7 @@ async def get_rooms_that_allow_join(
264266
join_rules_event = await self._store.get_event(join_rules_event_id)
265267

266268
# If allowed is of the wrong form, then only allow invited users.
267-
allow_list = join_rules_event.content.get("allow", [])
269+
allow_list = get_all_allow_lists(room_version, join_rules_event)
268270
if not isinstance(allow_list, list):
269271
return ()
270272

synapse/handlers/register.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
from synapse.spam_checker_api import RegistrationBehaviour
4242
from synapse.storage.state import StateFilter
4343
from synapse.types import RoomAlias, UserID, create_requester
44+
from synapse.util.join_rules import is_join_rule
4445

4546
if TYPE_CHECKING:
4647
from synapse.server import HomeServer
@@ -536,6 +537,7 @@ async def _join_rooms(self, user_id: str) -> None:
536537
event_id, allow_none=True
537538
)
538539
if join_rules_event:
540+
# TODO: Use is_join_rule utility
539541
join_rule = join_rules_event.content.get("join_rule", None)
540542
requires_invite = (
541543
join_rule and join_rule != JoinRules.PUBLIC

synapse/handlers/room_list.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
from synapse.types import JsonDict, ThirdPartyInstanceID
3636
from synapse.util.caches.descriptors import _CacheContext, cached
3737
from synapse.util.caches.response_cache import ResponseCache
38+
from synapse.util.join_rules import is_join_rule
3839

3940
if TYPE_CHECKING:
4041
from synapse.server import HomeServer
@@ -306,6 +307,7 @@ async def generate_room_entry(
306307

307308
join_rules_event = current_state.get((EventTypes.JoinRules, ""))
308309
if join_rules_event:
310+
# TODO: Use is_join_rule utility
309311
join_rule = join_rules_event.content.get("join_rule", None)
310312
if not allow_private and join_rule and join_rule != JoinRules.PUBLIC:
311313
return None

synapse/handlers/room_member.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1381,6 +1381,7 @@ async def _make_and_store_3pid_invite(
13811381
if room_create_event:
13821382
room_type = room_create_event.content.get(EventContentFields.ROOM_TYPE)
13831383

1384+
# TODO: Use is_join_rule utility
13841385
room_join_rules = ""
13851386
join_rules_event = room_state.get((EventTypes.JoinRules, ""))
13861387
if join_rules_event:

synapse/handlers/room_summary.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
from synapse.events import EventBase
4141
from synapse.types import JsonDict, Requester
4242
from synapse.util.caches.response_cache import ResponseCache
43+
from synapse.util.join_rules import is_join_rule
4344

4445
if TYPE_CHECKING:
4546
from synapse.server import HomeServer
@@ -843,10 +844,9 @@ async def _is_local_room_accessible(
843844
join_rules_event_id = state_ids.get((EventTypes.JoinRules, ""))
844845
if join_rules_event_id:
845846
join_rules_event = await self._store.get_event(join_rules_event_id)
846-
join_rule = join_rules_event.content.get("join_rule")
847-
if join_rule == JoinRules.PUBLIC or (
848-
room_version.msc2403_knocking and join_rule == JoinRules.KNOCK
849-
):
847+
is_public = is_join_rule(room_version, join_rules_event, JoinRules.PUBLIC)
848+
is_knock = is_join_rule(room_version, join_rules_event, JoinRules.KNOCK)
849+
if is_public or (room_version.msc2403_knocking and is_knock):
850850
return True
851851

852852
# Include the room if it is peekable.
@@ -875,7 +875,7 @@ async def _is_local_room_accessible(
875875
state_ids, room_version
876876
):
877877
allowed_rooms = (
878-
await self._event_auth_handler.get_rooms_that_allow_join(state_ids)
878+
await self._event_auth_handler.get_rooms_that_allow_join(state_ids, room_version)
879879
)
880880
if await self._event_auth_handler.is_user_in_rooms(
881881
allowed_rooms, requester
@@ -897,7 +897,7 @@ async def _is_local_room_accessible(
897897
state_ids, room_version
898898
):
899899
allowed_rooms = (
900-
await self._event_auth_handler.get_rooms_that_allow_join(state_ids)
900+
await self._event_auth_handler.get_rooms_that_allow_join(state_ids, room_version)
901901
)
902902
for space_id in allowed_rooms:
903903
if await self._event_auth_handler.check_host_in_room(
@@ -938,6 +938,7 @@ async def _is_remote_room_accessible(
938938
# The API doesn't return the room version so assume that a
939939
# join rule of knock is valid.
940940
if (
941+
# TODO: Use is_join_rule utility
941942
room.get("join_rules") in (JoinRules.PUBLIC, JoinRules.KNOCK)
942943
or room.get("world_readable") is True
943944
):
@@ -1006,7 +1007,8 @@ async def _build_room_entry(self, room_id: str, for_federation: bool) -> JsonDic
10061007
):
10071008
allowed_rooms = (
10081009
await self._event_auth_handler.get_rooms_that_allow_join(
1009-
current_state_ids
1010+
current_state_ids,
1011+
room_version
10101012
)
10111013
)
10121014
if allowed_rooms:

synapse/rest/admin/rooms.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -481,6 +481,7 @@ async def on_POST(
481481

482482
# send invite if room has "JoinRules.INVITE"
483483
room_state = await self.state_handler.get_current_state(room_id)
484+
# TODO: Use is_join_rule utility
484485
join_rules_event = room_state.get((EventTypes.JoinRules, ""))
485486
if join_rules_event:
486487
if not (join_rules_event.content.get("join_rule") == JoinRules.PUBLIC):
@@ -635,6 +636,7 @@ async def on_POST(
635636
if is_joined:
636637
return HTTPStatus.OK, {}
637638

639+
# TODO: Use is_join_rule utility
638640
join_rules = room_state.get((EventTypes.JoinRules, ""))
639641
is_public = False
640642
if join_rules:

synapse/storage/databases/main/stats.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -589,6 +589,7 @@ def _fetch_current_state_stats(
589589

590590
for event in state_event_map.values():
591591
if event.type == EventTypes.JoinRules:
592+
# TODO: Use is_join_rule utility?
592593
room_state["join_rules"] = event.content.get("join_rule")
593594
elif event.type == EventTypes.RoomHistoryVisibility:
594595
room_state["history_visibility"] = event.content.get(

0 commit comments

Comments
 (0)