From e650fca1de7f4f7b33069b83a5dbc5c5115b37ca Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 7 Sep 2021 23:11:31 -0600 Subject: [PATCH] Room versions 8 and 9: Restricted rooms MSCs: * https://github.com/matrix-org/matrix-doc/pull/3083 * https://github.com/matrix-org/matrix-doc/pull/3289 * https://github.com/matrix-org/matrix-doc/pull/3375 --- content/_index.md | 4 + content/client-server-api/_index.md | 30 +++++++ content/rooms/_index.md | 2 + content/rooms/v8.md | 79 +++++++++++++++++++ content/rooms/v9.md | 52 ++++++++++++ content/server-server-api.md | 61 ++++++++++---- data/api/client-server/joining.yaml | 2 + data/api/server-server/joins-v1.yaml | 53 ++++++++++++- data/api/server-server/joins-v2.yaml | 53 ++++++++++++- .../m.room.join_rules$restricted.yaml | 18 +++++ ...mber$join_authorised_via_users_server.yaml | 12 +++ .../schema/m.room.join_rules.yaml | 49 +++++++++--- 12 files changed, 388 insertions(+), 27 deletions(-) create mode 100644 content/rooms/v8.md create mode 100644 content/rooms/v9.md create mode 100644 data/event-schemas/examples/m.room.join_rules$restricted.yaml create mode 100644 data/event-schemas/examples/m.room.member$join_authorised_via_users_server.yaml diff --git a/content/_index.md b/content/_index.md index 7d978fd41c3..502b901afff 100644 --- a/content/_index.md +++ b/content/_index.md @@ -523,6 +523,10 @@ The available room versions are: - [Version 6](/rooms/v6) - **Stable**. Alters several authorization rules for events. - [Version 7](/rooms/v7) - **Stable**. Introduces knocking. +- [Version 8](/rooms/v8) - **Stable**. Adds a join rule to allow members + of another room to join without invite. +- [Version 9](/rooms/v9) - **Stable**. Builds on v8 to fix issues when + redacting some membership events. ## Specification Versions diff --git a/content/client-server-api/_index.md b/content/client-server-api/_index.md index 1f50ae7a4a4..e5b8f992422 100644 --- a/content/client-server-api/_index.md +++ b/content/client-server-api/_index.md @@ -1734,6 +1734,12 @@ This room can only be joined if you were invited, and allows anyone to request an invite to the room. Note that this join rule is only available to rooms based upon [room version 7](/rooms/v7). +`restricted` +This room can be joined if you were invited or if you are a member of another +room listed in the join rules. If the server cannot verify membership for any +of the listed rooms then you can only join with an invite. Note that this join +rule is only available to rooms based upon [room version 8](/rooms/v8). + The allowable state transitions of membership are: ![membership-flow-diagram](/diagrams/membership.png) @@ -1781,6 +1787,30 @@ server chose to auto-accept. {{% http-api spec="client-server" api="knocking" %}} +##### Restricted rooms + +Restricted rooms are rooms with a `join_rule` of `restricted`. These rooms +are accompanied by "allow conditions" as described in the +[`m.room.join_rules`](#mroomjoin_rules) state event. + +If the user has an invite to the room then the restrictions will not affect +them. They should be able to join by simply accepting the invite. + +Currently there is only one condition available: `m.room_membership`. This +condition requires the user trying to join the room to be a *joined* member +of another room (specifically, the `room_id` accompanying the condition). + +When joining without an invite, the server MUST verify that the requesting +user meets at least one of the conditions. If no conditions can be verified +or no conditions are satisfied, the user will not be able to join. This +validation is additionally done over federation when using a remote server +to join the room. + +If the room is `restricted` but no valid conditions are presented then the +room is effectively invite only. The user does not need to maintain the +conditions in order to stay a member of the room: the conditions are only +checked/evaluated during the join process. + #### Leaving rooms A user can leave a room to stop receiving events for that room. A user diff --git a/content/rooms/_index.md b/content/rooms/_index.md index 80c688a211c..712e922f629 100644 --- a/content/rooms/_index.md +++ b/content/rooms/_index.md @@ -11,3 +11,5 @@ weight: 60 * [Room Version 5](v5) * [Room Version 6](v6) * [Room Version 7](v7) +* [Room Version 8](v8) +* [Room Version 9](v9) diff --git a/content/rooms/v8.md b/content/rooms/v8.md new file mode 100644 index 00000000000..a4469e9a163 --- /dev/null +++ b/content/rooms/v8.md @@ -0,0 +1,79 @@ +--- +title: Room Version 8 +type: docs +weight: 60 +--- + +This room version builds on [version 7](/rooms/v7) to introduce a new +join rule that allows members to join the room based on membership in +another room. + +{{% boxes/warning %}} +This room version is known to have issues relating to redactions of member +join events. [Room version 9](/rooms/v9) should be preferred over v8 when +creating rooms. +{{% /boxes/warning %}} + +## Client considerations + +Clients are encouraged to expose the option for the join rule in their +user interface for supported room versions. Specifically, this feature +is intended to be used primarily in conjunction with +[MSC1772-style Spaces](https://github.com/matrix-org/matrix-doc/pull/1772) +(allowing members of a space to join a given room), though any v8 capable +room will be able to support this newly introduced join rule. + +The new join rule, `restricted`, is described in the Client-Server API +under the [`m.room.join_rules`](/client-server-api/#mroomjoin_rules) section. + +## Server implementation components + +{{% boxes/warning %}} +The information contained in this section is strictly for server +implementors. Applications which use the Client-Server API are generally +unaffected by the intricacies contained here. The section above +regarding client considerations is the resource that Client-Server API +use cases should reference. +{{% /boxes/warning %}} + +Room version 8 adds a new join rule to allow members of a room to join another +room without invite. Otherwise, the room version inherits all properties of +[Room version 7](/rooms/v7). + +### Authorization rules for events + +`m.room.member` events for `membership` of `join` are now validated as such: + +1. If the only previous event is an `m.room.create` and the `state_key` is the + creator, allow. +2. If the `sender` does not match `state_key`, reject. +3. If the `sender` is banned, reject. +4. If the `join_rule` is `invite` then allow if membership state is `invite` or + `knock`. +5. **[New in this room version]** If the `join_rule` is `restricted`: + 1. If membership state is `join`, allow. + 2. If `content.join_authorised_via_users_server` is not a user with + sufficient permission to invite other users, reject. + 3. If the event is not validly signed by the server denoted by the user ID in + `content.join_authorised_via_users_server`, reject. + 4. Otherwise, allow. +6. If the `join_rule` is `public`, allow. +7. Otherwise, reject. + +The remaining rules are the same as in [room version 7](/rooms/v7#server-implementation-components). + +### Redactions + +Events of type `m.room.join_rules` now keep the following `content` properties +when the event is redacted: +* `join_rule` +* **[New in this room version]** `allow` + +{{% boxes/warning %}} +[Room version 9](/rooms/v9) adds additional cases of protected properties for behaviour +related to restricted rooms (the functionality introduced in v8). v9 is preferred over +v8 when creating new rooms. +{{% /boxes/warning %}} + +The remaining rules are the same as in [room version 6](/rooms/v6#redactions) (the +last room version to modify the redaction rules). diff --git a/content/rooms/v9.md b/content/rooms/v9.md new file mode 100644 index 00000000000..52057d16093 --- /dev/null +++ b/content/rooms/v9.md @@ -0,0 +1,52 @@ +--- +title: Room Version 9 +type: docs +weight: 60 +--- + +This room version builds on [version 8](/rooms/v8) to add additional redaction +rules that were unintentionally missed when incorporating v8. + +## Client considerations + +See [room version 8](/rooms/v8) for specific details regarding the addition of +restricted rooms. + +Clients which implement a local redaction algorithm are encouraged to read on. + +## Server implementation components + +{{% boxes/warning %}} +The information contained in this section is strictly for server +implementors. Applications which use the Client-Server API are generally +unaffected by the intricacies contained here. The section above +regarding client considerations is the resource that Client-Server API +use cases should reference. +{{% /boxes/warning %}} + +Room version 8 added a new `restricted` join rule to allow members of a room +to join another room without invite. Room version 9 is based upon v8 with the +following considerations. + +### Redactions + +Events of type `m.room.member` now keep the following `content` properties +when the event is redacted: +* `membership` +* **[New in this room version]** `join_authorised_via_users_server` + +The remaining rules are the same as in [room version 8](/rooms/v8#redactions). + +{{% boxes/rationale %}} +Without the `join_authorised_via_users_server` property redacted join events +can become invalid when verifying the auth chain of a given event, thus creating +a split-brain scenario where the user is able to speak from one server's +perspective but most others will continually reject their events. + +This can theoretically be worked around with a rejoin to the room, being careful +not to use the faulty events as `prev_events`, though instead it is encouraged +to use v9 rooms over v8 rooms to outright avoid the situation. + +[Issue #3373](https://github.com/matrix-org/matrix-doc/issues/3373) has further +information. +{{% /boxes/rationale %}} diff --git a/content/server-server-api.md b/content/server-server-api.md index 9c6552ab2d0..3b369643193 100644 --- a/content/server-server-api.md +++ b/content/server-server-api.md @@ -407,21 +407,26 @@ the sender permission to send the event. The `auth_events` for the `m.room.create` event in a room is empty; for other events, it should be the following subset of the room state: -- The `m.room.create` event. +- The `m.room.create` event. -- The current `m.room.power_levels` event, if any. +- The current `m.room.power_levels` event, if any. -- The sender's current `m.room.member` event, if any. +- The sender's current `m.room.member` event, if any. -- If type is `m.room.member`: +- If type is `m.room.member`: - - The target's current `m.room.member` event, if any. - - If `membership` is `join` or `invite`, the current - `m.room.join_rules` event, if any. - - If membership is `invite` and `content` contains a - `third_party_invite` property, the current - `m.room.third_party_invite` event with `state_key` matching - `content.third_party_invite.signed.token`, if any. + - The target's current `m.room.member` event, if any. + - If `membership` is `join` or `invite`, the current + `m.room.join_rules` event, if any. + - If membership is `invite` and `content` contains a + `third_party_invite` property, the current + `m.room.third_party_invite` event with `state_key` matching + `content.third_party_invite.signed.token`, if any. + - If `content.join_authorised_via_users_server` is present, + the `m.room.member` event with `state_key` matching + `content.join_authorised_via_users_server`. Due to the + auth rules for the event, the target membership event should + always be eligible for inclusion. #### Rejection @@ -721,15 +726,41 @@ To complete the join handshake, the joining server must now submit this new event to a resident homeserver, by using the `PUT /send_join` endpoint. -The resident homeserver then accepts this event into the room's event -graph, and responds to the joining server with the full set of state for -the newly-joined room. The resident server must also send the event to -other servers participating in the room. +the resident homeserver then adds its signature to this event and +accepts it into the room's event graph. The joining server receives +the full set of state for the newly-joined room. The resident server +must also send the event to other servers participating in the room. {{% http-api spec="server-server" api="joins-v1" %}} {{% http-api spec="server-server" api="joins-v2" %}} +### Restricted rooms + +Restricted rooms are described in detail in the +[client-server API](/client-server-api/#restricted-rooms) and are available +in room versions based on [v8](/rooms/v8). + +A resident server attempting to join a server to a restricted room must +ensure that the joining server satisfies at least one of the conditions +specified by `m.room.join_rules`. If no conditions are available, or none +match the required schema, then the joining server is considered to have +failed all conditions. + +The resident server uses a 400 `M_UNABLE_TO_AUTHORISE_JOIN` error on +`/make_join` and `/send_join` to denote that the resident server is unable +to validate any of the conditions, usually because the resident server +does not have state information about rooms required by the conditions. + +The resident server uses a 400 `M_UNABLE_TO_GRANT_JOIN` error on `/make_join` +and `/send_join` to denote that the joining server satisfies at least +one of the conditions, though the resident server would be unable to +meet the auth rules governing `join_authorised_via_users_server` on the +resulting `m.room.member` event. + +If the joining server fails all conditions then a 403 `M_FORBIDDEN` error +is used by the resident server. + ## Knocking upon a room Rooms can permit knocking through the join rules, and if permitted this diff --git a/data/api/client-server/joining.yaml b/data/api/client-server/joining.yaml index e56d6adac70..f097e8f77dc 100644 --- a/data/api/client-server/joining.yaml +++ b/data/api/client-server/joining.yaml @@ -94,6 +94,7 @@ paths: - The room is invite-only and the user was not invited. - The user has been banned from the room. + - The room is restricted and the user failed to satisfy any of the conditions. examples: application/json: { "errcode": "M_FORBIDDEN", "error": "You are not invited to this room."} @@ -180,6 +181,7 @@ paths: - The room is invite-only and the user was not invited. - The user has been banned from the room. + - The room is restricted and the user failed to satisfy any of the conditions. examples: application/json: { "errcode": "M_FORBIDDEN", "error": "You are not invited to this room."} diff --git a/data/api/server-server/joins-v1.yaml b/data/api/server-server/joins-v1.yaml index 696ca533352..dd4ead4bcb7 100644 --- a/data/api/server-server/joins-v1.yaml +++ b/data/api/server-server/joins-v1.yaml @@ -115,6 +115,14 @@ paths: type: string description: The value `join`. example: "join" + join_authorised_via_users_server: + type: string + description: |- + Required if the room is [restricted](/client-server-api/#restricted-rooms). + An arbitrary user ID belonging to the resident server in + the room being joined that is able to issue invites to other + users. This is used in later validation of the auth rules for + the `m.room.member` event. required: ['membership'] required: - state_key @@ -134,7 +142,8 @@ paths: "origin_server_ts": 1549041175876, "sender": "@someone:example.org", "content": { - "membership": "join" + "membership": "join", + "join_authorised_via_users_server": "@anyone:resident.example.org" } } } @@ -146,6 +155,19 @@ paths: The error should be passed through to clients so that they may give better feedback to users. + + If the room is [restricted](/client-server-api/#restricted-rooms) + and none of the conditions can be validated by the server then + the `errcode` `M_UNABLE_TO_AUTHORISE_JOIN` must be used. This can + happen if the server does not know about any of the rooms listed + as conditions, for example. + + If the room is [restricted](/client-server-api/#restricted-rooms) + and the user meets at least one of the conditions, but the server + does not have permission to send invites (a prerequisite for later + authorisation of the `m.room.member` event) then an `errcode` of + `M_UNABLE_TO_GRANT_JOIN` is returned. The joining server should + attempt another resident server. schema: allOf: - $ref: "../client-server/definitions/errors/error.yaml" @@ -162,7 +184,20 @@ paths: "error": "Your homeserver does not support the features required to join this room", "room_version": "3" } + 403: + schema: + $ref: "../client-server/definitions/errors/error.yaml" + description: |- + The room that the joining server is attempting to join does not permit the user + to join. + examples: + application/json: { + "errcode": "M_FORBIDDEN", + "error": "You are not invited to this room", + } 404: + schema: + $ref: "../client-server/definitions/errors/error.yaml" description: |- The room that the joining server is attempting to join is unknown to the receiving server. @@ -240,6 +275,19 @@ paths: type: string description: The value `join`. example: "join" + join_authorised_via_users_server: + type: string + description: |- + Required if the room is [restricted](/client-server-api/#restricted-rooms). + An arbitrary user ID belonging to the resident server in + the room being joined that is able to issue invites to other + users. This is used in later validation of the auth rules for + the `m.room.member` event. + + The resident server which owns the provided user ID must have a + valid signature on the event. If the resident server is receiving + the `/send_join` request, the signature must be added before sending + or persisting the event to other servers. required: ['membership'] required: - state_key @@ -256,7 +304,8 @@ paths: "origin_server_ts": 1549041175876, "sender": "@someone:example.org", "content": { - "membership": "join" + "membership": "join", + "join_authorised_via_users_server": "@anyone:resident.example.org" } } responses: diff --git a/data/api/server-server/joins-v2.yaml b/data/api/server-server/joins-v2.yaml index de5c57113e9..4b90bc2a070 100644 --- a/data/api/server-server/joins-v2.yaml +++ b/data/api/server-server/joins-v2.yaml @@ -103,6 +103,19 @@ paths: type: string description: The value `join`. example: "join" + join_authorised_via_users_server: + type: string + description: |- + Required if the room is [restricted](/client-server-api/#restricted-rooms). + An arbitrary user ID belonging to the resident server in + the room being joined that is able to issue invites to other + users. This is used in later validation of the auth rules for + the `m.room.member` event. + + The resident server which owns the provided user ID must have a + valid signature on the event. If the resident server is receiving + the `/send_join` request, the signature must be added before sending + or persisting the event to other servers. required: ['membership'] required: - state_key @@ -119,10 +132,48 @@ paths: "origin_server_ts": 1549041175876, "sender": "@someone:example.org", "content": { - "membership": "join" + "membership": "join", + "join_authorised_via_users_server": "@anyone:resident.example.org" } } responses: + 400: + description: |- + The request is invalid in some way. + + The error should be passed through to clients so that they + may give better feedback to users. + + If the room is [restricted](/client-server-api/#restricted-rooms) + and none of the conditions can be validated by the server then + the `errcode` `M_UNABLE_TO_AUTHORISE_JOIN` must be used. This can + happen if the server does not know about any of the rooms listed + as conditions, for example. + + If the room is [restricted](/client-server-api/#restricted-rooms) + and the user meets at least one of the conditions, but the server + does not have permission to send invites (a prerequisite for later + authorisation of the `m.room.member` event) then an `errcode` of + `M_UNABLE_TO_GRANT_JOIN` is returned. The joining server should + attempt another resident server. + schema: + $ref: "../client-server/definitions/errors/error.yaml" + examples: + application/json: { + "errcode": "M_UNABLE_TO_GRANT_JOIN", + "error": "This server cannot send invites to you." + } + 403: + schema: + $ref: "../client-server/definitions/errors/error.yaml" + description: |- + The room that the joining server is attempting to join does not permit the user + to join. + examples: + application/json: { + "errcode": "M_FORBIDDEN", + "error": "You are not invited to this room", + } 200: description: |- The full state for the room, having accepted the join event. diff --git a/data/event-schemas/examples/m.room.join_rules$restricted.yaml b/data/event-schemas/examples/m.room.join_rules$restricted.yaml new file mode 100644 index 00000000000..bf807e54f74 --- /dev/null +++ b/data/event-schemas/examples/m.room.join_rules$restricted.yaml @@ -0,0 +1,18 @@ +{ + "$ref": "core/state_event.json", + "type": "m.room.join_rules", + "state_key": "", + "content": { + "join_rule": "restricted", + "allow": [ + { + "type": "m.room_membership", + "room_id": "!other:example.org" + }, + { + "type": "m.room_membership", + "room_id": "!elsewhere:example.org" + } + ] + } +} diff --git a/data/event-schemas/examples/m.room.member$join_authorised_via_users_server.yaml b/data/event-schemas/examples/m.room.member$join_authorised_via_users_server.yaml new file mode 100644 index 00000000000..eb8c84bc802 --- /dev/null +++ b/data/event-schemas/examples/m.room.member$join_authorised_via_users_server.yaml @@ -0,0 +1,12 @@ +{ + "$ref": "m.room.member.yaml", + "content": { + "membership": "join", + "avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF", + "displayname": "Alice Margatroid", + "join_authorised_via_users_server": "@bob:other.example.org" + }, + "unsigned": { + "age": 1234 + } +} diff --git a/data/event-schemas/schema/m.room.join_rules.yaml b/data/event-schemas/schema/m.room.join_rules.yaml index 5f0e11afef7..8f0437cf496 100644 --- a/data/event-schemas/schema/m.room.join_rules.yaml +++ b/data/event-schemas/schema/m.room.join_rules.yaml @@ -2,15 +2,17 @@ allOf: - $ref: core-event-schema/state_event.yaml description: | - A room may be `public` meaning anyone can join the room without any prior action. - Alternatively, it can be `invite` meaning that a user who wishes to join the room - must first receive an invite to the room from someone already inside of the room. - `knock` means that users are able to ask for permission to join the room, where - they are either allowed (invited) or denied (kicked/banned) access. Join rules - of `knock` are otherwise the same as `invite`: the user needs an explicit invite - to join the room. - - Currently, `private` is a reserved keyword which is not implemented. + A room may have one of the following designations: + * `public` - anyone can join the room without any prior action. + * `invite` - a user must first receive an invite from someone already in the room + in order to join. + * `knock` - a user can request an invite to the room. They can be allowed (invited) + or denied (kicked/banned) access. Otherwise, users need to be invited in. Only + available in rooms based on [v7](/rooms/v7). + * `restricted` - anyone able to satisfy at least one of the allow conditions is + able to join the room without prior action. Otherwise, an invite is required. + Only available in rooms based on [v8](/rooms/v8). + * `private` - reserved without implementation. No significant meaning. properties: content: properties: @@ -21,7 +23,36 @@ properties: - knock - invite - private + - restricted type: string + allow: + description: |- + For `restricted` rooms, the conditions the user will be tested against. The + user needs only to satisfy one of the conditions to join the `restricted` + room. If the user fails to meet any condition, or the condition is unable + to be confirmed as satisfied, then the user requires an invite to join the + room. Improper or no `allow` conditions on a `restricted` join rule imply + the room is effectively invite-only (no conditions can be satisfied). + type: array + items: + type: object + title: AllowCondition + properties: + type: + type: string + description: |- + The type of condition: + * `m.room_membership` - the user satisfies the condition if they are + joined to the referenced room. + enum: ['m.room_membership'] + room_id: + type: string + description: |- + Required if `type` is `m.room_membership`. The room ID to check the + user's membership against. If the user is joined to this room, they + satisfy the condition and thus are permitted to join the `restricted` + room. + required: ['type'] required: - join_rule type: object