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

Commit

Permalink
Proof of concept for MSC3613: Combinatorial join rules
Browse files Browse the repository at this point in the history
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)
  • Loading branch information
turt2live committed Dec 31, 2021
1 parent cbd82d0 commit 3e8b7a5
Show file tree
Hide file tree
Showing 11 changed files with 145 additions and 23 deletions.
32 changes: 32 additions & 0 deletions synapse/api/room_versions.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ class RoomVersion:
msc2716_historical = attr.ib(type=bool)
# MSC2716: Adds support for redacting "insertion", "chunk", and "marker" events
msc2716_redactions = attr.ib(type=bool)
# MSC3613: Allows for concurrent join rules in a simplified manner
msc3613_simplified_join_rules = attr.ib(type=bool)


class RoomVersions:
Expand All @@ -99,6 +101,7 @@ class RoomVersions:
msc2403_knocking=False,
msc2716_historical=False,
msc2716_redactions=False,
msc3613_simplified_join_rules=False,
)
V2 = RoomVersion(
"2",
Expand All @@ -115,6 +118,7 @@ class RoomVersions:
msc2403_knocking=False,
msc2716_historical=False,
msc2716_redactions=False,
msc3613_simplified_join_rules=False,
)
V3 = RoomVersion(
"3",
Expand All @@ -131,6 +135,7 @@ class RoomVersions:
msc2403_knocking=False,
msc2716_historical=False,
msc2716_redactions=False,
msc3613_simplified_join_rules=False,
)
V4 = RoomVersion(
"4",
Expand All @@ -147,6 +152,7 @@ class RoomVersions:
msc2403_knocking=False,
msc2716_historical=False,
msc2716_redactions=False,
msc3613_simplified_join_rules=False,
)
V5 = RoomVersion(
"5",
Expand All @@ -163,6 +169,7 @@ class RoomVersions:
msc2403_knocking=False,
msc2716_historical=False,
msc2716_redactions=False,
msc3613_simplified_join_rules=False,
)
V6 = RoomVersion(
"6",
Expand All @@ -179,6 +186,7 @@ class RoomVersions:
msc2403_knocking=False,
msc2716_historical=False,
msc2716_redactions=False,
msc3613_simplified_join_rules=False,
)
MSC2176 = RoomVersion(
"org.matrix.msc2176",
Expand All @@ -195,6 +203,7 @@ class RoomVersions:
msc2403_knocking=False,
msc2716_historical=False,
msc2716_redactions=False,
msc3613_simplified_join_rules=False,
)
V7 = RoomVersion(
"7",
Expand All @@ -211,6 +220,7 @@ class RoomVersions:
msc2403_knocking=True,
msc2716_historical=False,
msc2716_redactions=False,
msc3613_simplified_join_rules=False,
)
V8 = RoomVersion(
"8",
Expand All @@ -227,6 +237,7 @@ class RoomVersions:
msc2403_knocking=True,
msc2716_historical=False,
msc2716_redactions=False,
msc3613_simplified_join_rules=False,
)
V9 = RoomVersion(
"9",
Expand All @@ -243,6 +254,7 @@ class RoomVersions:
msc2403_knocking=True,
msc2716_historical=False,
msc2716_redactions=False,
msc3613_simplified_join_rules=False,
)
MSC2716v3 = RoomVersion(
"org.matrix.msc2716v3",
Expand All @@ -259,6 +271,25 @@ class RoomVersions:
msc2403_knocking=True,
msc2716_historical=True,
msc2716_redactions=True,
msc3613_simplified_join_rules=False,
)
MSC3613 = RoomVersion(
# v9 + MSC3613
"org.matrix.msc3613",
RoomDisposition.STABLE,
EventFormatVersions.V3,
StateResolutionVersions.V2,
enforce_key_validity=True,
special_case_aliases_auth=False,
strict_canonicaljson=True,
limit_notifications_power_levels=True,
msc2176_redaction_rules=False,
msc3083_join_rules=True,
msc3375_redaction_rules=True,
msc2403_knocking=True,
msc2716_historical=False,
msc2716_redactions=False,
msc3613_simplified_join_rules=True,
)


Expand All @@ -276,6 +307,7 @@ class RoomVersions:
RoomVersions.V8,
RoomVersions.V9,
RoomVersions.MSC2716v3,
RoomVersions.MSC3613,
)
}

Expand Down
47 changes: 35 additions & 12 deletions synapse/event_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
from synapse.events import EventBase
from synapse.events.builder import EventBuilder
from synapse.types import StateMap, UserID, get_domain_from_id
from synapse.util.join_rules import is_join_rule as is_join_rule_in_version

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -225,6 +226,29 @@ def check_auth_rules_for_event(

_check_event_sender_in_room(event, auth_dict)

# MSC3613: Combination join rules
if event.type == EventTypes.JoinRules and room_version_obj.msc3613_simplified_join_rules:
if not event.is_state():
raise AuthError(403, "Join rules event must be a state event")
if event.state_key != "":
raise AuthError(403, "Join rules event must have empty state_key")

# TODO: Function-ize this?

if not event.content.get("join_rule", None):
raise AuthError(403, "A join_rule must be specified")

arr = event.content.get("join_rules", [])
if arr and not isinstance(arr, list):
raise AuthError(403, "join_rules must be an array")
if arr:
for rule in arr:
if not rule.get("join_rule"):
raise AuthError(403, "A join_rule must be specified for each rule")

# pass
return

# Special case to allow m.room.third_party_invite events wherever
# a user is allowed to issue invites. Fixes
# https://github.com/vector-im/vector-web/issues/1208 hopefully
Expand Down Expand Up @@ -333,11 +357,10 @@ def _is_membership_change_allowed(
target_banned = target and target.membership == Membership.BAN

key = (EventTypes.JoinRules, "")
join_rule_event = auth_events.get(key)
if join_rule_event:
join_rule = join_rule_event.content.get("join_rule", JoinRules.INVITE)
else:
join_rule = JoinRules.INVITE
join_rule_event: EventBase = auth_events.get(key)

def is_join_rule(rule: JoinRules) -> bool:
return is_join_rule_in_version(room_version, join_rule_event, rule)

user_level = get_user_power_level(event.user_id, auth_events)
target_level = get_user_power_level(target_user_id, auth_events)
Expand All @@ -354,7 +377,7 @@ def _is_membership_change_allowed(
"target_banned": target_banned,
"target_in_room": target_in_room,
"membership": membership,
"join_rule": join_rule,
"join_rules": join_rule_event.content if join_rule_event else "unset",
"target_user_id": target_user_id,
"event.user_id": event.user_id,
},
Expand All @@ -369,7 +392,7 @@ def _is_membership_change_allowed(

# Require the user to be in the room for membership changes other than join/knock.
if Membership.JOIN != membership and (
RoomVersion.msc2403_knocking and Membership.KNOCK != membership
room_version.msc2403_knocking and Membership.KNOCK != membership
):
# If the user has been invited or has knocked, they are allowed to change their
# membership event to leave
Expand Down Expand Up @@ -406,9 +429,9 @@ def _is_membership_change_allowed(
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:
elif is_join_rule(JoinRules.PUBLIC):
pass
elif room_version.msc3083_join_rules and join_rule == JoinRules.RESTRICTED:
elif room_version.msc3083_join_rules and is_join_rule(JoinRules.RESTRICTED):
# This is the same as public, but the event must contain a reference
# to the server who authorised the join. If the event does not contain
# the proper content it is rejected.
Expand All @@ -434,8 +457,8 @@ def _is_membership_change_allowed(
if authorising_user_level < invite_level:
raise AuthError(403, "Join event authorised by invalid server.")

elif join_rule == JoinRules.INVITE or (
room_version.msc2403_knocking and join_rule == JoinRules.KNOCK
elif is_join_rule(JoinRules.INVITE) or (
room_version.msc2403_knocking and is_join_rule(JoinRules.KNOCK)
):
if not caller_in_room and not caller_invited:
raise AuthError(403, "You are not invited to this room.")
Expand All @@ -456,7 +479,7 @@ def _is_membership_change_allowed(
if user_level < ban_level or user_level <= target_level:
raise AuthError(403, "You don't have permission to ban")
elif room_version.msc2403_knocking and Membership.KNOCK == membership:
if join_rule != JoinRules.KNOCK:
if not is_join_rule(JoinRules.KNOCK):
raise AuthError(403, "You don't have permission to knock")
elif target_user_id != event.user_id:
raise AuthError(403, "You cannot knock for other users")
Expand Down
10 changes: 6 additions & 4 deletions synapse/handlers/event_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
from synapse.events.builder import EventBuilder
from synapse.events.snapshot import EventContext
from synapse.types import StateMap, get_domain_from_id
from synapse.util.join_rules import is_join_rule, get_all_allow_lists
from synapse.util.metrics import Measure

if TYPE_CHECKING:
Expand Down Expand Up @@ -198,7 +199,7 @@ async def check_restricted_join_rules(

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

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

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

async def get_rooms_that_allow_join(
self, state_ids: StateMap[str]
self, state_ids: StateMap[str], room_version: RoomVersion,
) -> Collection[str]:
"""
Generate a list of rooms in which membership allows access to a room.
Args:
state_ids: The current state of the room the user wishes to join
room_version: The version of the room
Returns:
A collection of room IDs. Membership in any of the rooms in the list grants the ability to join the target room.
Expand All @@ -264,7 +266,7 @@ async def get_rooms_that_allow_join(
join_rules_event = await self._store.get_event(join_rules_event_id)

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

Expand Down
2 changes: 2 additions & 0 deletions synapse/handlers/register.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
from synapse.spam_checker_api import RegistrationBehaviour
from synapse.storage.state import StateFilter
from synapse.types import RoomAlias, UserID, create_requester
from synapse.util.join_rules import is_join_rule

if TYPE_CHECKING:
from synapse.server import HomeServer
Expand Down Expand Up @@ -536,6 +537,7 @@ async def _join_rooms(self, user_id: str) -> None:
event_id, allow_none=True
)
if join_rules_event:
# TODO: Use is_join_rule utility
join_rule = join_rules_event.content.get("join_rule", None)
requires_invite = (
join_rule and join_rule != JoinRules.PUBLIC
Expand Down
2 changes: 2 additions & 0 deletions synapse/handlers/room_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
from synapse.types import JsonDict, ThirdPartyInstanceID
from synapse.util.caches.descriptors import _CacheContext, cached
from synapse.util.caches.response_cache import ResponseCache
from synapse.util.join_rules import is_join_rule

if TYPE_CHECKING:
from synapse.server import HomeServer
Expand Down Expand Up @@ -306,6 +307,7 @@ async def generate_room_entry(

join_rules_event = current_state.get((EventTypes.JoinRules, ""))
if join_rules_event:
# TODO: Use is_join_rule utility
join_rule = join_rules_event.content.get("join_rule", None)
if not allow_private and join_rule and join_rule != JoinRules.PUBLIC:
return None
Expand Down
1 change: 1 addition & 0 deletions synapse/handlers/room_member.py
Original file line number Diff line number Diff line change
Expand Up @@ -1381,6 +1381,7 @@ async def _make_and_store_3pid_invite(
if room_create_event:
room_type = room_create_event.content.get(EventContentFields.ROOM_TYPE)

# TODO: Use is_join_rule utility
room_join_rules = ""
join_rules_event = room_state.get((EventTypes.JoinRules, ""))
if join_rules_event:
Expand Down
16 changes: 9 additions & 7 deletions synapse/handlers/room_summary.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
from synapse.events import EventBase
from synapse.types import JsonDict, Requester
from synapse.util.caches.response_cache import ResponseCache
from synapse.util.join_rules import is_join_rule

if TYPE_CHECKING:
from synapse.server import HomeServer
Expand Down Expand Up @@ -843,10 +844,9 @@ async def _is_local_room_accessible(
join_rules_event_id = state_ids.get((EventTypes.JoinRules, ""))
if join_rules_event_id:
join_rules_event = await self._store.get_event(join_rules_event_id)
join_rule = join_rules_event.content.get("join_rule")
if join_rule == JoinRules.PUBLIC or (
room_version.msc2403_knocking and join_rule == JoinRules.KNOCK
):
is_public = is_join_rule(room_version, join_rules_event, JoinRules.PUBLIC)
is_knock = is_join_rule(room_version, join_rules_event, JoinRules.KNOCK)
if is_public or (room_version.msc2403_knocking and is_knock):
return True

# Include the room if it is peekable.
Expand Down Expand Up @@ -875,7 +875,7 @@ async def _is_local_room_accessible(
state_ids, room_version
):
allowed_rooms = (
await self._event_auth_handler.get_rooms_that_allow_join(state_ids)
await self._event_auth_handler.get_rooms_that_allow_join(state_ids, room_version)
)
if await self._event_auth_handler.is_user_in_rooms(
allowed_rooms, requester
Expand All @@ -897,7 +897,7 @@ async def _is_local_room_accessible(
state_ids, room_version
):
allowed_rooms = (
await self._event_auth_handler.get_rooms_that_allow_join(state_ids)
await self._event_auth_handler.get_rooms_that_allow_join(state_ids, room_version)
)
for space_id in allowed_rooms:
if await self._event_auth_handler.check_host_in_room(
Expand Down Expand Up @@ -938,6 +938,7 @@ async def _is_remote_room_accessible(
# The API doesn't return the room version so assume that a
# join rule of knock is valid.
if (
# TODO: Use is_join_rule utility
room.get("join_rules") in (JoinRules.PUBLIC, JoinRules.KNOCK)
or room.get("world_readable") is True
):
Expand Down Expand Up @@ -1006,7 +1007,8 @@ async def _build_room_entry(self, room_id: str, for_federation: bool) -> JsonDic
):
allowed_rooms = (
await self._event_auth_handler.get_rooms_that_allow_join(
current_state_ids
current_state_ids,
room_version
)
)
if allowed_rooms:
Expand Down
2 changes: 2 additions & 0 deletions synapse/rest/admin/rooms.py
Original file line number Diff line number Diff line change
Expand Up @@ -481,6 +481,7 @@ async def on_POST(

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

# TODO: Use is_join_rule utility
join_rules = room_state.get((EventTypes.JoinRules, ""))
is_public = False
if join_rules:
Expand Down
1 change: 1 addition & 0 deletions synapse/storage/databases/main/stats.py
Original file line number Diff line number Diff line change
Expand Up @@ -589,6 +589,7 @@ def _fetch_current_state_stats(

for event in state_event_map.values():
if event.type == EventTypes.JoinRules:
# TODO: Use is_join_rule utility?
room_state["join_rules"] = event.content.get("join_rule")
elif event.type == EventTypes.RoomHistoryVisibility:
room_state["history_visibility"] = event.content.get(
Expand Down
Loading

0 comments on commit 3e8b7a5

Please sign in to comment.