diff --git a/proposals/2403-knock.md b/proposals/2403-knock.md new file mode 100644 index 00000000000..8bad17662ae --- /dev/null +++ b/proposals/2403-knock.md @@ -0,0 +1,683 @@ +# MSC2403: Add "knock" feature +Many people are in invite-only rooms. Sometimes, someone wants to join such a +room and can't, as they aren't invited. This proposal adds a feature for a +user to indicate that they want to join a room. + +# Proposal +This proposal implements the reserved "knock" membership type for the +`m.room.member` state event. This state event indicates that when a user +knocks on a room, they are asking for permission to join. Like all membership +events, it contains an optional "reason" parameter to specify the reason you +want to join. Like other membership types, the parameters "displayname" and +"avatar_url" are optional. This membership can be sent by users who aren't +currently in said room. An example for the membership would look like the +following: +```json +{ + "membership": "knock", + "displayname": "Alice", + "avatar_url": "mxc://example.org/avatar", + "reason": "I want to join this room as I really love foxes!" +} +``` + +After a knock in a room, a member of the room can invite the knocker, or they +can decide to reject it instead. + +## Client-Server API +A new endpoint is introduced in the Client-Server API: `POST +/_matrix/client/r0/knock/{roomIdOrAlias}`. This allows the client to state +their intent to knock on a room. + +Additionally, extensions to the `GET /_matrix/client/r0/sync` endpoint are +introduced. These allow a client to receive information about the status of +their knock attempt. + +### `POST /_matrix/client/r0/knock/{roomIdOrAlias}` +Or the knocking equivalent of +[`POST +/_matrix/client/r0/join/{roomIdOrAlias}`](https://matrix.org/docs/spec/client_server/r0.6.1#post-matrix-client-r0-join-roomidoralias). + +The path parameter (`roomIdOrAlias`) is either the room ID or the alias of +the room you want to knock on. Additionally, several `server_name` parameters +can be specified via the query parameters. The post body accepts an optional +string parameter, `reason`, which is the reason you want to join the room. A +request could look as follows: + +```json +POST /_matrix/client/r0/knock/%23foxes%3Amatrix.org?server_name=matrix.org&server_name=elsewhere.ca HTTP/1.1 +Content-Type: application/json + +{ + "reason": "I want to join this room as I really love foxes!" +} +``` + +This endpoint requires authentication and can be rate limited. + + +#### Responses: +##### Status code 200: +The user knocked successfully. The room ID of the knocked on room is returned. Example +reply: +```json +{ + "room_id": "!ZclcEpFTORTjmWIrqH:matrix.org" +} +``` + +##### Status code 403: +The user wasn't allowed to knock (e.g. they are banned). Example error reply: +```json +{ + "errcode": "M_FORBIDDEN", + "error": "The user isn't allowed to knock in this room." +} +``` + +##### Status code 404: +The room was not found. Example error reply: +```json +{ + "errcode": "M_NOT_FOUND", + "error": "Unknown room." +} +``` + +### Extensions to `GET /_matrix/client/r0/sync` + +In [the response to +`/sync`](https://matrix.org/docs/spec/client_server/r0.6.1#get-matrix-client-r0-sync) +is a `rooms` field. This is a dictionary which currently contains keys +`join`, `invite` and `leave`, which each provide information to the client on +various membership states regarding the user. + +It is proposed to add a fourth possible key to `rooms`, called `knock`. Its +value is a mapping from room ID to room information. The room information is +a mapping from a key `knock_state` to another mapping with key `events` being +a list of `StrippedStateEvent`. `StrippedStateEvent`s are defined as state +events that only contain the `sender`, `type`, `state_key` and `content` +keys. + +Note that while `join` and `leave` keys in `/sync` use `state`, we use +`knock_state` here. This mirrors `invite`s use of `invite_state`. + +These stripped state events contain information about the room, most notably +the room's name and avatar. A client will need this information to show a +nice representation of pending knocked rooms. The recommended events to +include are the join rules, canonical alias, avatar, name and encryption +state of the room, rather than all room state. This behaviour matches the +information sent to remote homeservers when remote users are invited to a +room. + +This prevents unneeded state from the room leaking out, and also speeds +things up (think not sending over hundreds of membership events from big +rooms). + +Also note that like `invite_state`, state events from `knock_state` are +purely for giving the user some information about the current state of the +room that they have knocked on. If the user was previously in the room, the +state events in `knock_state` are not intended to overwrite any historical +state. This applies storage of state on both the homeserver and the client. + +The following is an example of knock state coming down `/sync`. + +Request: +``` +GET /_matrix/client/r0/sync HTTP/1.1 +Content-Type: application/json +``` + +Response: +```json +{ + ... + "rooms": { + "knock": { + "!abcdefghijklmo:example.com": { + "knock_state": { + "events": [ + { + "content": { + "join_rule": "knock" + }, + "sender": "@room_admin:example.com", + "state_key": "", + "type": "m.room.join_rules" + }, + { + "content": { + "name": "Some cool room" + }, + "sender": "@room_admin:example.com", + "state_key": "", + "type": "m.room.name" + }, + { + "content": { + "url": "mxc://example.com/xyz54321" + }, + "sender": "@room_admin:example.com", + "state_key": "", + "type": "m.room.avatar" + }, + { + "content": { + "avatar_url": "mxc://example.org/abc1234", + "displayname": "Knocking User", + "membership": "knock" + }, + "sender": "@knocking_user:example.org", + "state_key": "@knocking_user:example.org", + "type": "m.room.member", + } + ] + } + } + } + }, + ... +} +``` + +### Changes regarding the Public Rooms Directory + +A problem arises for discovery of knockable rooms. Ideally one wouldn't have +to send their colleagues a room ID for a room that they need to knock on. One +of these methods for room discovery is the [public rooms +directory](https://matrix.org/docs/spec/client_server/r0.6.1#get-matrix-client-r0-publicrooms), +which allows us to explore a list of rooms we may be able to join. + +The spec does not prevent us from adding rooms with 'knock' join_rules to the +public rooms directory. However, a user attempting +to join a room in the directory will not know whether to directly attempt a +join, or to knock first. The current content of a `PublicRoomsChunk` does not +contain this information: + +```json +{ + "aliases": [ + "#murrays:cheese.bar" + ], + "avatar_url": "mxc://bleeker.street/CHEDDARandBRIE", + "guest_can_join": false, + "name": "CHEESE", + "num_joined_members": 37, + "room_id": "!ol19s:bleecker.street", + "topic": "Tasty tasty cheese", + "world_readable": true +} +``` + +Therefore this proposal adds `join_rule` as a new, optional field to a +`PublicRoomsChunk`. The `join_rule` of knockable rooms will be `knock`, +thus giving clients the information they need to attempt entry of a +room when a client selects it. It also allows clients to display +knockable rooms differently than publicly joinable ones. + +For backwards compatibility with old servers, the default value of +`join_rule` is `public`. + +### Push Rules + +To help knocks be noticed earlier, it would be nice to send a push +notification to those in the room who can act on a knock when it +comes in, rather than everyone in the room. This would require a +push rule to fire only when that user's power level is high enough to +accept or reject a knock. + +With the current push rules implementation it is possible to place a +condition on the sender's power level, but unfortunately the same does +not exist for event recipients. + +This MSC thus does not propose any changes to push rules at this time, +but acknowledges that it would be useful for a future MSC to address when +the underlying push rules architecture can support it. + + +## Server-Server API +Similarly to [join](https://matrix.org/docs/spec/server_server/r0.1.4#joining-rooms) +and [leave](https://matrix.org/docs/spec/server_server/r0.1.4#leaving-rooms-rejecting-invites) +over federation, a ping-pong game with two new endpoints is introduced: `make_knock` +and `send_knock`. Both endpoints must be protected via server ACLs. + +### `GET /_matrix/federation/v1/make_knock/{roomId}/{userId}` + +Asks the receiving server to return information that the sending server will +need to prepare a knock event. + +Request format: + +| Parameter | Type | Description | +|-----------|------|-------------| +| Path parameters: +| roomId | string | Required. The room ID that should receive the knock. +| userId | string | Required. The user ID the knock event will be for. +| Query Parameters: +| ver | [string] | Required. The room versions the sending server has support for. + +Note that `GET /_matrix/federation/v1/make_join/{roomId}/{userId}` does not make `ver` +a required query parameter for backwards compatibility reasons. We have no such restrictions. + + +Response Format: + +| Parameter | Type | Description | +|-----------|------|-------------| +| room_version | string | The version of the room where the server is trying to knock. +| event | Event Template | An unsigned template event. May differ between room versions. + +#### Responses +##### Status code 200: +Returns a template to be used to knock on rooms. May depend on room version. +```json +{ + "room_version": "2", + "event": { + "type": "m.room.member", + "room_id": "!somewhere:example.org", + "content": { + "membership": "knock" + }, + "state_key": "@someone:example.org", + "origin": "example.org", + "origin_server_ts": 1549041175876, + "sender": "@someone:example.org" + } +} +``` + +##### Status code 400: +This request was invalid, e.g. bad JSON. Example reply: +```json +{ + "errcode": "M_INCOMPATIBLE_ROOM_VERSION", + "error": "Your homeserver does not support the features required to join this room", + "room_version": "3" +} +``` + +##### Status code 403: +This request is forbidden, e.g. the user is banned from the room. Example reply: +```json +{ + "errcode": "M_FORBIDDEN", + "error": "You are not allowed to knock on this room" +} +``` + +##### Status code 404: +The room is unknown to the remote server. Example reply: +```json +{ + "errcode": "M_NOT_FOUND", + "error": "Unknown room" +} +``` + +### `PUT /_matrix/federation/v1/send_knock/{roomId}/{eventId}` +Submits a signed knock event to the resident homeserver for it to accept into +the room's graph. Note that event format may differ between room versions. + +Note that in the past all `send_*` federation endpoints were updated to `/v2` +to remove a redundant HTTP error code from the return body. While we don't +have the same redundancy here, we start off at `/v1` for this new endpoint +as per +[MSC2844](https://github.com/matrix-org/matrix-doc/pull/2844). + +Request format: + +| Parameter | Type | Description | +|-----------|------|-------------| +| Path parameters: +| roomId | string | Required. The room ID that should receive the knock. +| eventId | string | Required. The event ID for the knock event. + +The JSON body is expected to be the full event. + +Response Format: + +| Parameter | Type | Description | +|-----------|------|-------------| +| `knock_room_state` | [StrippedStateEvent] | Required. State events providing public room metadata + +A request could look as follows: +```json +PUT /_matrix/federation/v1/send_knock/%21abc123%3Amatrix.org/%24abc123%3Aexample.org HTTP/1.1 +Content-Type: application/json + +{ + "sender": "@someone:example.org", + "origin": "matrix.org", + "origin_server_ts": 1234567890, + "type": "m.room.member", + "state_key": "@someone:example.org", + "content": { + "membership": "knock", + "displayname": "Alice", + "avatar_url": "mxc://example.org/avatar", + "reason": "I want to join this room as I really love foxes!" + } +} +``` + +#### Response: +##### Status code 200: +The event was successfully accepted into the graph by the homeserver that +received the knock. It must then send this knock into the room, before +responding to the knocking homeserver, indicating the knock succeeded. + +The response contains `StrippedStateEvent`s with room metadata (room name, +avatar ...) that the knocking homeserver can pass to the client. The event +types that can be sent here should match those of the `/sync` extensions +mentioned above. + +This is loosely based on the +[federated invite](https://matrix.org/docs/spec/server_server/r0.1.4#put-matrix-federation-v2-invite-roomid-eventid) +request content. +```json +{ + "knock_room_state": [ + { + "content": { + "join_rule": "knock" + }, + "sender": "@room_admin:example.com", + "state_key": "", + "type": "m.room.join_rules" + }, + { + "content": { + "name": "Some cool room" + }, + "sender": "@room_admin:example.com", + "state_key": "", + "type": "m.room.name" + }, + { + "content": { + "url": "mxc://example.com/xyz54321" + }, + "sender": "@room_admin:example.com", + "state_key": "", + "type": "m.room.avatar" + }, + { + "content": { + "avatar_url": "mxc://example.org/abc1234", + "displayname": "Knocking User", + "membership": "knock" + }, + "sender": "@knocking_user:example.org", + "state_key": "@knocking_user:example.org", + "type": "m.room.member", + } + ] +} +``` + +##### Status code 403: +This request is forbidden, e.g. the user is banned from the room. Example reply: +```json +{ + "errcode": "M_FORBIDDEN", + "error": "You are not allowed to knock on this room" +} +``` + +##### Status code 404: +The room is unknown to the remote server. Example reply: +```json +{ + "errcode": "M_NOT_FOUND", + "error": "Unknown room" +} +``` + +## Restrictions +There are restrictions to being able to set this membership, as well as +accepting or denying the knock. A formal description of the changes to the auth rules is given below; +first we summarise the semantics of the proposed changes. + +### Current membership +Only users without a current membership or with their current membership +set to "knock" or "leave" can knock on a room. This means that a user that +is banned, is invited or is currently in the room cannot knock on it. + +### Join Rules +This proposal makes use of the existing "knock" join rule. The value of +`join_rule` in the content of the `m.room.join_rules` state event for a room +must be set to "knock" for a knock to succeed. This means that existing rooms +will need to opt into allowing knocks in their rooms. Other than allowing +knocks, a join rule of "knock" is functionally equivalent to that of +"invite", except that it additionally allows external users to change their +membership to "knock" under certain conditions. + +### Auth rules + +Each room version defines the auth rules which should be applied in that room version. +This MSC proposes a new room version with the following changes to the [auth +rules from room version 6](https://matrix.org/docs/spec/rooms/v6#authorization-rules-for-events): + +* Under "5. If type is `m.room.member`", insert the following after "e. If membership is `ban`": + + ``` + f. If `membership` is `knock`: + i. If the `join_rule` is anything other than `knock`, reject. + ii. If `sender` does not match `state_key`, reject. + iii. If the `sender`'s membership is not `ban`, `invite` or `join`, allow. + iv. Otherwise, reject. + ``` + +Note that: + - Both the `sender` and `state_key` fields are set to the user ID of the knocking + user. This is different to an `invite` membership event, where the `sender` is the inviter and + the `state_key` is the invitee. + - f.ii is justified as one user should not be able to knock on behalf of + another user. + - f.iii is justified as knocks should not be allowed if the knocking user + has been banned from the room, is invited to the room or if they are already + in the room. + - Knocks are not restricted by power level like invites are. The `join_rules` + are already used to enforce whether someone can or cannot knock. However, + power level rules do apply when approving or denying the knock, as discussed + in the Membership Changes section below. + +Additionally, note that redactions of knock events are not a concern, as +`membership` keys are excluded from being redacted as defined by all current +room versions. + +## Membership changes +Once someone has sent a `knock` membership into the room, the membership for +that user can be transitioned to the following possible states: + - `invite`: In this case, the knock was accepted by someone inside the room + and they are inviting the knocker into the room. + - `leave`: In this case, similar to how kicks are handled, the knock has + been rejected. Alternatively, the knocking user has rescinded their knock. + - `ban`: In this case, the knock was rejected and the user has been prevented + from sending further knocks. + +Let's talk about each one of these in more detail. + +### Membership change to `invite` + +The knock has been accepted by someone in the room. + +The user who is accepting the knock must have the power level to perform +invites. The accepting user's homeserver will then send an invite - over federation if +necessary - to the knocking user. The knocking user may then join the room as +if they had been invited normally. + +To accept a knock, the client should call [`POST +/_matrix/client/r0/rooms/{roomId}/invite`](https://matrix.org/docs/spec/client_server/r0.6.1#post-matrix-client-r0-rooms-roomid-invite) +with the user ID of the knocking user in the JSON body. + +If the knocking user is on another homeserver, then the homeserver of the +accepting user will call [`PUT +/_matrix/federation/v2/invite/{roomId}/{eventId}`](https://matrix.org/docs/spec/server_server/r0.1.4#put-matrix-federation-v2-invite-roomid-eventid) +on the knocking homeserver to inform it that the knock has been accepted. + +The knocking homeserver should assume an invite to a room it has knocked on means +that its knock has been accepted, even if the invite was not explicitly +related to the knock attempt. + +Note that client or homeserver implementations are free to automatically +accept this invite given they're aware that it's the result of a previous +knock. In case of failing to auto-accept an invite on the homeserver, it's +recommended for homeservers to pass the invite down to the client so that it +may try at a later point (or reject the potentially broken invite) at the user's +discretion. + +### Membership change to `leave` via rejecting a knock + +The knock has been rejected by someone in the room. + +To reject a knock, the rejecting user's client must call [`POST +/_matrix/client/r0/rooms/{roomId}/kick`](https://matrix.org/docs/spec/client_server/r0.6.1#post-matrix-client-r0-rooms-roomid-kick) +with the user ID of the knocking user in the JSON body. Rejecting a knock +over federation has a slight catch, though. + +When the knocking user is on another homeserver, the homeserver of the +rejecting user needs to send the `leave` event over federation to the +knocking homeserver. However, this is a bit tricky as it is currently very +difficult to have events from a room propagate over federation when the +receiving homeserver is not in the room. This is due to the remote homeserver +being unable to verify that the event being sent is actually from a +homeserver in the room - and that the homeserver in the room had the required +power level to send it. This is a problem that currently affects other, +similar operations, such as disinviting or unbanning a federated user. In +both cases, they won't be notified as their homeserver is not in the room. + +While we could easily send the leave event as part of a generic +transaction to the remote homeserver, that homeserver would have no way to +validate the `prev_events` and `auth_events` that the event references. We +could send those events over as well, but those will also reference other +events that require validation and so on. + +A simple thing we could easily do here is to trust the leave event implicitly +if it is sent by the homeserver we originally knocked through. We know this +homeserver is (or at least was) in the room, so they're probably telling the +truth. This is almost an edge case though, as while you'll knock through one +homeserver in the room, there's no guarantee that the admin that denies your +knock will be on the same homeserver you knocked through. Perhaps the +homeserver you knocked through could listen for this and then send the event +back to you - but what if it goes offline in the meantime? + +As such, informing remote homeservers about the rejection of knocks over +federation is de-scoped for now, and left to a future MSC which can solve +this class of problem in a suitable way. Rejections should still work for the +homeservers that are in the room, as they can validate the leave event for +they have access to the events it references. + +### Membership change to `leave` via rescinding a knock +The knocking user has rescinded their knock. + +To rescind a knock, the knocking user's client must call [`POST +/_matrix/client/r0/rooms/{roomId}/leave`](https://matrix.org/docs/spec/client_server/r0.6.1#post-matrix-client-r0-rooms-roomid-leave). +To rescind a knock over federation, the knocking homeserver must complete +a [`make_leave`, `send_leave` dance]( +https://matrix.org/docs/spec/server_server/r0.1.4#leaving-rooms-rejecting-invites) +with a homeserver in the room. + +### Membership change to `ban` + +The knock has been rejected by someone in the room and the user has been +banned, and is unable to send further knocks. + +This one is fairly straightforward. Someone with the appropriate power levels +in the room bans the user. This will have the same effect as rejecting the +knock, and in addition prevent any further knocks by this user from being +allowed into the room. + +If the user is unbanned, they will be able to send a new knock which could be +accepted. + +To ban the user, the client should call [`POST +/_matrix/client/r0/rooms/{roomId}/ban`](https://matrix.org/docs/spec/client_server/r0.6.1#post-matrix-client-r0-rooms-roomid-ban) with the user ID of the knocking user in the JSON body. + +Informing the knocking user about the update is the same as rejecting the +knock. + +# Potential issues +This new feature would allow users to send events into rooms that they don't +partake in. That is why this proposal enables the `knock` join rule, in +order to allow room admins to opt in to this behaviour. + +# Alternatives +The possibility of initiating a knock by sending EDUs between users instead of sending +a membership state event into a room has been raised. This is an ongoing discussion +occurring at https://github.com/matrix-org/matrix-doc/pull/2403/files#r573180627. + +# Client UX recommendations +After a knock is received in a room, it is expected to be displayed in the +timeline, similar to other membership changes. Clients can optionally add a way +for users of a room to review all current knocks. + +Please also note the recommendations for clients in the "Security considerations" +section below. + +# Security considerations +Clients must take care when implementing this feature in order to prevent +simple abuse vectors that can be accomplished by individual users. For +instance, when a knock occurs, clients are advised to hide the reason until +the user interacts with the client in some way (e.g. clicking on a "show +reason" button). The user should reveal the reason only if they choose to. + +It is recommended to not display the reason by default as else this would +essentially allow outsiders to send messages into the room. + +It is still theoretically possible for a homeserver admin to create many users +with different user IDs or display names, all spelling out an abusive +message, and then having each of them knock in order. + +Clients should also do their best to prevent impersonation attacks. Similar to +joins, users can set any displayname or avatar URL they'd like when knocking on +a room. Clients SHOULD display further information to help identify the user, +such as User ID, encryption verification status, rooms you share with the user, +etc. Care should be taken to balance the importance of preventing attacks while +avoiding overloading the user with too much information or raising false +positives. + +Another abuse vector is allowed by the ability for users to rescind knocks. +This is to help users in case they knocked on a room accidentally, or simply +no longer want to join a room they've knocked on. While this is a useful +feature, it also allows users to spam a room by knocking and rescinding their +knocks over and over. Particularly note-worthy is that this will generate +state events that homeservers in the room will need to process. And while +join/leave state changes will do the same in a public room, the act of +knocking is much lighter than the act of joining a room. + +In both cases, room admins should employ typical abuse mitigation tools, such +as user bans and server ACLs. Clients are encouraged to make employing these +tools easy even if the offensive user or server is not present in the room. + +# Unstable prefix + +An unstable feature flag is not required due to this proposal's requirement +of a new room version. Clients can check for a room version that includes +knocking via the Client-Server API's [capabilities +endpoint](https://matrix.org/docs/spec/client_server/r0.6.1#get-matrix-client-r0-capabilities). +Experimental implementation should use `xyz.amorgan.knock` as a room version identifier. + +The new endpoints should contain an unstable prefix during experimental +implementation. The unstable counterpart for each endpoint is: + +C-S knock: + +* `POST /_matrix/client/knock/{roomIdOrAlias}` +* `POST /_matrix/client/unstable/xyz.amorgan.knock/knock/{roomIdOrAlias}` + +S-S make_knock: + +* `GET /_matrix/federation/v1/make_knock/{roomId}/{userId}` +* `GET /_matrix/federation/unstable/xyz.amorgan.knock/make_knock/{roomId}/{userId}` + +S-S send_knock: + +* `PUT /_matrix/federation/v1/send_knock/{roomId}/{eventId}` +* `PUT /_matrix/federation/unstable/xyz.amorgan.knock/send_knock/{roomId}/{eventId}` + +Finally, an unstable prefix is added to the key that comes down `/sync`, +the join rule for rooms and the `content.membership` key of the member +event sent into rooms when a user has knocked successfully. Instead of +`knock`, experimental implementations should use `xyz.amorgan.knock`.