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

Commit

Permalink
Add knocking support (#81)
Browse files Browse the repository at this point in the history
Implement knocking as defined by matrix-org/matrix-spec-proposals#2403

This is the base knocking stuff, taken from matrix-org/synapse#6739
and does not include any public room directory changes.

While knocking hasn't merged yet on mainline due to waiting on getting Complement
into Synapse's CI, the code has been well-tested.
  • Loading branch information
anoadragon453 committed Feb 9, 2021
1 parent 2c896f1 commit 6bf58d8
Show file tree
Hide file tree
Showing 34 changed files with 1,567 additions and 117 deletions.
2 changes: 1 addition & 1 deletion .buildkite/pipeline.yml
Original file line number Diff line number Diff line change
Expand Up @@ -511,7 +511,7 @@ steps:
- "docker build -t complement-synapse -f complement-master/dockerfiles/Synapse.Dockerfile complement-master/dockerfiles"
# Finally, compile and run the tests.
- "cd complement-master"
- "COMPLEMENT_BASE_IMAGE=complement-synapse:latest go test -v -tags synapse_blacklist ./tests"
- "COMPLEMENT_BASE_IMAGE=complement-synapse:latest go test -v -tags synapse_blacklist,msc2403 ./tests"
label: "\U0001F9EA Complement"
agents:
queue: "medium"
Expand Down
1 change: 1 addition & 0 deletions changelog.d/6739.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Implement "room knocking" as per [MSC2403](https://github.com/matrix-org/matrix-doc/pull/2403). Contributed by Sorunome and anoa.
4 changes: 3 additions & 1 deletion docs/sample_config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1565,7 +1565,9 @@ metrics_flags:

## API Configuration ##

# A list of event types that will be included in the room_invite_state
# A list of event types from a room that will be given to users when they
# are invited to a room. This allows clients to display information about the
# room that they've been invited to, without actually being in the room yet.
#
#room_invite_state_types:
# - "m.room.join_rules"
Expand Down
4 changes: 2 additions & 2 deletions synapse/api/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ class Membership:

INVITE = "invite"
JOIN = "join"
KNOCK = "knock"
KNOCK = "xyz.amorgan.knock"
LEAVE = "leave"
BAN = "ban"
LIST = (INVITE, JOIN, KNOCK, LEAVE, BAN)
Expand All @@ -50,7 +50,7 @@ class PresenceState:

class JoinRules:
PUBLIC = "public"
KNOCK = "knock"
KNOCK = "xyz.amorgan.knock"
INVITE = "invite"
PRIVATE = "private"

Expand Down
43 changes: 42 additions & 1 deletion synapse/api/room_versions.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ class RoomVersion:
state_res = attr.ib() # int; one of the StateResolutionVersions
enforce_key_validity = attr.ib() # bool

# bool: before MSC2261/MSC2432, m.room.aliases had special auth rules and redaction rules
# Before MSC2432, m.room.aliases had special auth rules and redaction rules
special_case_aliases_auth = attr.ib(type=bool)
# Strictly enforce canonicaljson, do not allow:
# * Integers outside the range of [-2 ^ 53 + 1, 2 ^ 53 - 1]
Expand All @@ -67,6 +67,11 @@ class RoomVersion:
# bool: MSC2209: Check 'notifications' key while verifying
# m.room.power_levels auth rules.
limit_notifications_power_levels = attr.ib(type=bool)
# MSC2174/MSC2176: Apply updated redaction rules algorithm.
msc2176_redaction_rules = attr.ib(type=bool)
# MSC2403: Allows join_rules to be set to 'knock', changes auth rules to allow sending
# m.room.membership event with membership 'knock'.
allow_knocking = attr.ib(type=bool)


class RoomVersions:
Expand All @@ -79,6 +84,8 @@ class RoomVersions:
special_case_aliases_auth=True,
strict_canonicaljson=False,
limit_notifications_power_levels=False,
msc2176_redaction_rules=False,
allow_knocking=False,
)
V2 = RoomVersion(
"2",
Expand All @@ -89,6 +96,8 @@ class RoomVersions:
special_case_aliases_auth=True,
strict_canonicaljson=False,
limit_notifications_power_levels=False,
msc2176_redaction_rules=False,
allow_knocking=False,
)
V3 = RoomVersion(
"3",
Expand All @@ -99,6 +108,8 @@ class RoomVersions:
special_case_aliases_auth=True,
strict_canonicaljson=False,
limit_notifications_power_levels=False,
msc2176_redaction_rules=False,
allow_knocking=False,
)
V4 = RoomVersion(
"4",
Expand All @@ -109,6 +120,8 @@ class RoomVersions:
special_case_aliases_auth=True,
strict_canonicaljson=False,
limit_notifications_power_levels=False,
msc2176_redaction_rules=False,
allow_knocking=False,
)
V5 = RoomVersion(
"5",
Expand All @@ -119,6 +132,8 @@ class RoomVersions:
special_case_aliases_auth=True,
strict_canonicaljson=False,
limit_notifications_power_levels=False,
msc2176_redaction_rules=False,
allow_knocking=False,
)
V6 = RoomVersion(
"6",
Expand All @@ -129,6 +144,32 @@ class RoomVersions:
special_case_aliases_auth=False,
strict_canonicaljson=True,
limit_notifications_power_levels=True,
msc2176_redaction_rules=False,
allow_knocking=False,
)
MSC2176 = RoomVersion(
"org.matrix.msc2176",
RoomDisposition.UNSTABLE,
EventFormatVersions.V3,
StateResolutionVersions.V2,
enforce_key_validity=True,
special_case_aliases_auth=False,
strict_canonicaljson=True,
limit_notifications_power_levels=True,
msc2176_redaction_rules=True,
allow_knocking=False,
)
MSC2403_DEV = RoomVersion(
"xyz.amorgan.knock",
RoomDisposition.UNSTABLE,
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,
allow_knocking=True,
)


Expand Down
11 changes: 8 additions & 3 deletions synapse/appservice/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

from prometheus_client import Counter

from synapse.api.constants import EventTypes, ThirdPartyEntityKind
from synapse.api.constants import EventTypes, Membership, ThirdPartyEntityKind
from synapse.api.errors import CodeMessageException
from synapse.events import EventBase
from synapse.events.utils import serialize_event
Expand Down Expand Up @@ -249,9 +249,14 @@ def _serialize(self, service, events):
e,
time_now,
as_client_event=True,
is_invite=(
# If this is an invite or a knock membership event, and we're interested
# in this user, then include any stripped state alongside the event.
include_stripped_room_state=(
e.type == EventTypes.Member
and e.membership == "invite"
and (
e.membership == Membership.INVITE
or e.membership == Membership.KNOCK
)
and service.is_interested_in_user(e.state_key)
),
)
Expand Down
2 changes: 2 additions & 0 deletions synapse/config/_base.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ from synapse.config import (
consent_config,
database,
emailconfig,
experimental,
groups,
jwt_config,
key,
Expand Down Expand Up @@ -46,6 +47,7 @@ def path_exists(file_path: str): ...

class RootConfig:
server: server.ServerConfig
experimental: experimental.ExperimentalConfig
tls: tls.TlsConfig
database: database.DatabaseConfig
logging: logger.LoggingConfig
Expand Down
23 changes: 14 additions & 9 deletions synapse/config/api.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# Copyright 2015, 2016 OpenMarket Ltd
# Copyright 2020 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.
Expand All @@ -16,27 +17,31 @@

from ._base import Config

# The default types of room state to send to users to are invited to or knock on a room.
DEFAULT_ROOM_STATE_TYPES = [
EventTypes.JoinRules,
EventTypes.CanonicalAlias,
EventTypes.RoomAvatar,
EventTypes.RoomEncryption,
EventTypes.Name,
]


class ApiConfig(Config):
section = "api"

def read_config(self, config, **kwargs):
self.room_invite_state_types = config.get(
"room_invite_state_types",
[
EventTypes.JoinRules,
EventTypes.CanonicalAlias,
EventTypes.RoomAvatar,
EventTypes.RoomEncryption,
EventTypes.Name,
],
"room_invite_state_types", DEFAULT_ROOM_STATE_TYPES
)

def generate_config_section(cls, **kwargs):
return """\
## API Configuration ##
# A list of event types that will be included in the room_invite_state
# A list of event types from a room that will be given to users when they
# are invited to a room. This allows clients to display information about the
# room that they've been invited to, without actually being in the room yet.
#
#room_invite_state_types:
# - "{JoinRules}"
Expand Down
35 changes: 35 additions & 0 deletions synapse/config/experimental.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# -*- coding: utf-8 -*-
# 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 synapse.api.room_versions import KNOWN_ROOM_VERSIONS, RoomVersions
from synapse.config._base import Config
from synapse.types import JsonDict


class ExperimentalConfig(Config):
"""Config section for enabling experimental features"""

section = "experimental"

def read_config(self, config: JsonDict, **kwargs):
experimental = config.get("experimental_features") or {}

# MSC2403 (room knocking)
self.msc2403_enabled = experimental.get("msc2403_enabled", False) # type: bool
if self.msc2403_enabled:
# Enable the MSC2403 unstable room version
KNOWN_ROOM_VERSIONS.update(
{RoomVersions.MSC2403_DEV.identifier: RoomVersions.MSC2403_DEV}
)
2 changes: 2 additions & 0 deletions synapse/config/homeserver.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from .consent_config import ConsentConfig
from .database import DatabaseConfig
from .emailconfig import EmailConfig
from .experimental import ExperimentalConfig
from .federation import FederationConfig
from .groups import GroupsConfig
from .jwt_config import JWTConfig
Expand Down Expand Up @@ -57,6 +58,7 @@ class HomeServerConfig(RootConfig):

config_classes = [
ServerConfig,
ExperimentalConfig,
TlsConfig,
FederationConfig,
CacheConfig,
Expand Down
26 changes: 22 additions & 4 deletions synapse/event_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ def check(
if logger.isEnabledFor(logging.DEBUG):
logger.debug("Auth events: %s", [a.event_id for a in auth_events.values()])

# 5. If type is m.room.membership
if event.type == EventTypes.Member:
_is_membership_change_allowed(event, auth_events)
logger.debug("Allowing! %s", event)
Expand Down Expand Up @@ -247,6 +248,7 @@ def _is_membership_change_allowed(

caller_in_room = caller and caller.membership == Membership.JOIN
caller_invited = caller and caller.membership == Membership.INVITE
caller_knocked = caller and caller.membership == Membership.KNOCK

# get info about the target
key = (EventTypes.Member, target_user_id)
Expand Down Expand Up @@ -289,9 +291,12 @@ def _is_membership_change_allowed(
raise AuthError(403, "%s is banned from the room" % (target_user_id,))
return

if Membership.JOIN != membership:
# Require the user to be in the room for membership changes other than join/knock.
if Membership.JOIN != membership and Membership.KNOCK != membership:
# If the user has been invited or has knocked, they are allowed to change their
# membership event to leave
if (
caller_invited
(caller_invited or caller_knocked)
and Membership.LEAVE == membership
and target_user_id == event.user_id
):
Expand Down Expand Up @@ -324,7 +329,7 @@ def _is_membership_change_allowed(
raise AuthError(403, "You are banned from this room")
elif join_rule == JoinRules.PUBLIC:
pass
elif join_rule == JoinRules.INVITE:
elif join_rule in (JoinRules.INVITE, JoinRules.KNOCK):
if not caller_in_room and not caller_invited:
raise AuthError(403, "You are not invited to this room.")
else:
Expand All @@ -343,6 +348,19 @@ def _is_membership_change_allowed(
elif Membership.BAN == membership:
if user_level < ban_level or user_level <= target_level:
raise AuthError(403, "You don't have permission to ban")
elif Membership.KNOCK == membership:
if 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")
elif target_in_room:
raise AuthError(403, "You cannot knock on a room you are already in")
elif caller_knocked:
raise AuthError(403, "You already have a pending knock for this room")
elif caller_invited:
raise AuthError(403, "You are already invited to this room")
elif target_banned:
raise AuthError(403, "You are banned from this room")
else:
raise AuthError(500, "Unknown membership %s" % membership)

Expand Down Expand Up @@ -699,7 +717,7 @@ def auth_types_for_event(event: EventBase) -> Set[Tuple[str, str]]:

if event.type == EventTypes.Member:
membership = event.content["membership"]
if membership in [Membership.JOIN, Membership.INVITE]:
if membership in [Membership.JOIN, Membership.INVITE, Membership.KNOCK]:
auth_types.add((EventTypes.JoinRules, ""))

auth_types.add((EventTypes.Member, event.state_key))
Expand Down
19 changes: 12 additions & 7 deletions synapse/events/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,7 @@ def format_event_for_client_v1(d):
"replaces_state",
"prev_content",
"invite_room_state",
"knock_room_state",
)
for key in copy_keys:
if key in d["unsigned"]:
Expand Down Expand Up @@ -264,7 +265,7 @@ def serialize_event(
event_format=format_event_for_client_v1,
token_id=None,
only_event_fields=None,
is_invite=False,
include_stripped_room_state=False,
):
"""Serialize event for clients
Expand All @@ -275,8 +276,10 @@ def serialize_event(
event_format
token_id
only_event_fields
is_invite (bool): Whether this is an invite that is being sent to the
invitee
include_stripped_room_state (bool): Some events can have stripped room state
stored in the `unsigned` field. This is required for invite and knock
functionality. If this option is False, that state will be removed from the
event before it is returned. Otherwise, it will be kept.
Returns:
dict
Expand Down Expand Up @@ -308,11 +311,13 @@ def serialize_event(
if txn_id is not None:
d["unsigned"]["transaction_id"] = txn_id

# If this is an invite for somebody else, then we don't care about the
# invite_room_state as that's meant solely for the invitee. Other clients
# will already have the state since they're in the room.
if not is_invite:
# invite_room_state and knock_room_state are a list of stripped room state events
# that are meant to provide metadata about a room to an invitee/knocker. They are
# intended to only be included in specific circumstances, such as down sync, and
# should not be included in any other case.
if not include_stripped_room_state:
d["unsigned"].pop("invite_room_state", None)
d["unsigned"].pop("knock_room_state", None)

if as_client_event:
d = event_format(d)
Expand Down
Loading

0 comments on commit 6bf58d8

Please sign in to comment.