diff --git a/changelogs/client_server/newsfragments/3387.feature b/changelogs/client_server/newsfragments/3387.feature new file mode 100644 index 00000000..34cc1ee4 --- /dev/null +++ b/changelogs/client_server/newsfragments/3387.feature @@ -0,0 +1 @@ +Add support for `restricted` rooms as per [MSC3083](https://github.com/matrix-org/matrix-doc/pull/3083), [MSC3289](https://github.com/matrix-org/matrix-doc/pull/3289), and [MSC3375](https://github.com/matrix-org/matrix-doc/pull/3375). diff --git a/changelogs/room_versions/newsfragments/3387.feature.1 b/changelogs/room_versions/newsfragments/3387.feature.1 new file mode 100644 index 00000000..324d935a --- /dev/null +++ b/changelogs/room_versions/newsfragments/3387.feature.1 @@ -0,0 +1 @@ +Add Room Version 8 as per [MSC3289](https://github.com/matrix-org/matrix-doc/pull/3289). diff --git a/changelogs/room_versions/newsfragments/3387.feature.2 b/changelogs/room_versions/newsfragments/3387.feature.2 new file mode 100644 index 00000000..7b1239ea --- /dev/null +++ b/changelogs/room_versions/newsfragments/3387.feature.2 @@ -0,0 +1 @@ +Add Room Version 9 as per [MSC3375](https://github.com/matrix-org/matrix-doc/pull/3375). diff --git a/changelogs/server_server/newsfragments/3387.feature b/changelogs/server_server/newsfragments/3387.feature new file mode 100644 index 00000000..34cc1ee4 --- /dev/null +++ b/changelogs/server_server/newsfragments/3387.feature @@ -0,0 +1 @@ +Add support for `restricted` rooms as per [MSC3083](https://github.com/matrix-org/matrix-doc/pull/3083), [MSC3289](https://github.com/matrix-org/matrix-doc/pull/3289), and [MSC3375](https://github.com/matrix-org/matrix-doc/pull/3375). diff --git a/content/client-server-api/_index.md b/content/client-server-api/_index.md index b4f783df..92ba7831 100644 --- a/content/client-server-api/_index.md +++ b/content/client-server-api/_index.md @@ -1984,6 +1984,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 in room versions [which support knocking](/rooms/#feature-matrix). +{{% added-in v="1.2" %}} `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 rule +is only expected to work in room versions [which support it](/rooms/#feature-matrix). + The allowable state transitions of membership are: ![membership-flow-diagram](/diagrams/membership.png) @@ -2033,6 +2039,51 @@ server chose to auto-accept. {{% http-api spec="client-server" api="knocking" %}} +##### Restricted rooms + +{{% added-in v="1.2" %}} + +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. + +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. When the +join is happening over federation, the remote server will check the conditions +before accepting the join. See the [Server-Server Spec](/server-server-api/#restricted-rooms) +for more information. + +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. + +###### Conditions + +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). For +example, if `!restricted:example.org` wanted to allow joined members of +`!other:example.org` to join, `!restricted:example.org` would have the following +`content` for [`m.room.join_rules`](#mroomjoin_rules): + +```json +{ + "join_rule": "restricted", + "allow": [ + { + "room_id": "!other:example.org", + "type": "m.room_membership" + } + ] +} +``` + #### 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 9efbde4c..4c6faae1 100644 --- a/content/rooms/_index.md +++ b/content/rooms/_index.md @@ -36,9 +36,10 @@ Alternatively, consider flipping the column/row organization to be features up top and versions on the left. --> -| Feature \ Version | 1 | 2 | 3 | 4 | 5 | 6 | 7 | -|-------------------|---|---|---|---|---|---|---| -| **Knocking** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✔ | +| Feature \ Version | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | +|-------------------|---|---|---|---|---|---|---|---|---| +| **Knocking** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✔ | ✔ | ✔ | +| **Restricted join rules** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✔ | ✔ | ## Complete list of room versions @@ -68,6 +69,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. ## Room version grammar diff --git a/content/rooms/fragments/v8-auth-rules.md b/content/rooms/fragments/v8-auth-rules.md new file mode 100644 index 00000000..2e9f39e4 --- /dev/null +++ b/content/rooms/fragments/v8-auth-rules.md @@ -0,0 +1,160 @@ +--- +toc_hide: true +--- + +Events must be signed by the server denoted by the `sender` key. + +`m.room.redaction` events are not explicitly part of the auth rules. +They are still subject to the minimum power level rules, but should always +fall into "10. Otherwise, allow". Instead of being authorized at the time +of receipt, they are authorized at a later stage: see the +[Redactions](#redactions) section below for more information. + +The types of state events that affect authorization are: + +- `m.room.create` +- `m.room.member` +- `m.room.join_rules` +- `m.room.power_levels` +- `m.room.third_party_invite` + +{{% boxes/note %}} +Power levels are inferred from defaults when not explicitly supplied. +For example, mentions of the `sender`'s power level can also refer to +the default power level for users in the room. +{{% /boxes/note %}} + +The rules are as follows: + +1. If type is `m.room.create`: + 1. If it has any previous events, reject. + 2. If the domain of the `room_id` does not match the domain of the + `sender`, reject. + 3. If `content.room_version` is present and is not a recognised + version, reject. + 4. If `content` has no `creator` field, reject. + 5. Otherwise, allow. +2. Reject if event has `auth_events` that: + 1. have duplicate entries for a given `type` and `state_key` pair + 2. have entries whose `type` and `state_key` don't match those + specified by the [auth events + selection](/server-server-api#auth-events-selection) + algorithm described in the server specification. +3. If event does not have a `m.room.create` in its `auth_events`, + reject. +4. If type is `m.room.member`: + 1. If no `state_key` key or `membership` key in `content`, reject. + 2. If `content` has a `join_authorised_via_users_server` + key: + 1. If the event is not validly signed by the user ID denoted + by the key, reject. + 3. If `membership` is `join`: + 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 `join`. + 5. If the `join_rule` is `restricted`: + 1. If membership state is `join` or `invite`, allow. + 2. If the `join_authorised_via_users_server` key in `content` + is not a user with sufficient permission to invite other + users, reject. + 3. Otherwise, allow. + 6. If the `join_rule` is `public`, allow. + 7. Otherwise, reject. + 4. If `membership` is `invite`: + 1. If `content` has `third_party_invite` key: + 1. If *target user* is banned, reject. + 2. If `content.third_party_invite` does not have a `signed` + key, reject. + 3. If `signed` does not have `mxid` and `token` keys, + reject. + 4. If `mxid` does not match `state_key`, reject. + 5. If there is no `m.room.third_party_invite` event in the + current room state with `state_key` matching `token`, + reject. + 6. If `sender` does not match `sender` of the + `m.room.third_party_invite`, reject. + 7. If any signature in `signed` matches any public key in + the `m.room.third_party_invite` event, allow. The public + keys are in `content` of `m.room.third_party_invite` as: + 1. A single public key in the `public_key` field. + 2. A list of public keys in the `public_keys` field. + 8. Otherwise, reject. + 2. If the `sender`'s current membership state is not `join`, + reject. + 3. If *target user*'s current membership state is `join` or + `ban`, reject. + 4. If the `sender`'s power level is greater than or equal to + the *invite level*, allow. + 5. Otherwise, reject. + 5. If `membership` is `leave`: + 1. If the `sender` matches `state_key`, allow if and only if + that user's current membership state is `invite` or `join`. + 2. If the `sender`'s current membership state is not `join`, + reject. + 3. If the *target user*'s current membership state is `ban`, + and the `sender`'s power level is less than the *ban level*, + reject. + 4. If the `sender`'s power level is greater than or equal to + the *kick level*, and the *target user*'s power level is + less than the `sender`'s power level, allow. + 5. Otherwise, reject. + 6. If `membership` is `ban`: + 1. If the `sender`'s current membership state is not `join`, + reject. + 2. If the `sender`'s power level is greater than or equal to + the *ban level*, and the *target user*'s power level is less + than the `sender`'s power level, allow. + 3. Otherwise, reject. + 7. If `membership` is `knock`: + 1. If the `join_rule` is anything other than `knock`, reject. + 2. If `sender` does not match `state_key`, reject. + 3. If the `sender`'s current membership is not `ban`, `invite`, + or `join`, allow. + 8. Otherwise, the membership is unknown. Reject. +5. If the `sender`'s current membership state is not `join`, reject. +6. If type is `m.room.third_party_invite`: + 1. Allow if and only if `sender`'s current power level is greater + than or equal to the *invite level*. +7. If the event type's *required power level* is greater than the + `sender`'s power level, reject. +8. If the event has a `state_key` that starts with an `@` and does not + match the `sender`, reject. +9. If type is `m.room.power_levels`: + 1. If `users` key in `content` is not a dictionary with keys that + are valid user IDs with values that are integers (or a string + that is an integer), reject. + 2. If there is no previous `m.room.power_levels` event in the room, + allow. + 3. For the keys `users_default`, `events_default`, `state_default`, + `ban`, `redact`, `kick`, `invite` check if they were added, + changed or removed. For each found alteration: + 1. If the current value is higher than the `sender`'s current + power level, reject. + 2. If the new value is higher than the `sender`'s current power + level, reject. + 4. For each entry being added, changed or removed in both the + `events`, `users`, and `notifications` keys: + 1. If the current value is higher than the `sender`'s current + power level, reject. + 2. If the new value is higher than the `sender`'s current power + level, reject. + 5. For each entry being changed under the `users` key, other than + the `sender`'s own entry: + 1. If the current value is equal to the `sender`'s current + power level, reject. + 6. Otherwise, allow. +10. Otherwise, allow. + +{{% boxes/note %}} +Some consequences of these rules: + +- Unless you are a member of the room, the only permitted operations + (apart from the initial create/join) are: joining a public room; + accepting or rejecting an invitation to a room. +- To unban somebody, you must have power level greater than or equal + to both the kick *and* ban levels, *and* greater than the target + user's power level. +{{% /boxes/note %}} \ No newline at end of file diff --git a/content/rooms/v7.md b/content/rooms/v7.md index 17ec223b..77775656 100644 --- a/content/rooms/v7.md +++ b/content/rooms/v7.md @@ -199,6 +199,10 @@ completeness. {{% rver-fragment name="v4-event-format" %}} +### Handling redactions + +{{% rver-fragment name="v3-handling-redactions" %}} + ### Canonical JSON {{% rver-fragment name="v6-canonical-json" %}} @@ -209,6 +213,4 @@ completeness. ### Redactions -{{% rver-fragment name="v3-handling-redactions" %}} - {{% rver-fragment name="v6-redactions" %}} diff --git a/content/rooms/v8.md b/content/rooms/v8.md new file mode 100644 index 00000000..63c18717 --- /dev/null +++ b/content/rooms/v8.md @@ -0,0 +1,123 @@ +--- +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. + +The new join rule, `restricted`, is described in the +[Client-Server API](/client-server-api/#restricted-rooms). + +Clients which implement the redaction algorithm locally should refer to the +[redactions](#redactions) section below for a full overview. + +### Redactions + +{{% added-in this=true %}} `m.room.join_rules` events now keep `allow` in addition to other +keys in `content` when being redacted. + +{{% 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 full redaction algorithm follows. + +Upon receipt of a redaction event, the server must strip off any keys +not in the following list: + +- `event_id` +- `type` +- `room_id` +- `sender` +- `state_key` +- `content` +- `hashes` +- `signatures` +- `depth` +- `prev_events` +- `prev_state` +- `auth_events` +- `origin` +- `origin_server_ts` +- `membership` + +The content object must also be stripped of all keys, unless it is one +of one of the following event types: + +- `m.room.member` allows key `membership`. +- `m.room.create` allows key `creator`. +- `m.room.join_rules` allows keys `join_rule`, `allow`. +- `m.room.power_levels` allows keys `ban`, `events`, `events_default`, + `kick`, `redact`, `state_default`, `users`, `users_default`. +- `m.room.history_visibility` allows key `history_visibility`. + +## 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 + +{{% added-in this=true %}} For checks performed upon `m.room.member` events, new +points for handling `content.join_authorised_via_users_server` are added (Rule 4.2 +and 4.3.5). + +{{% rver-fragment name="v8-auth-rules" %}} + +### Redactions + +[See above](#redactions). + +## Unchanged from v7 + +The following sections have not been modified since v7, but are included for +completeness. + +### State resolution + +{{% rver-fragment name="v2-state-res" %}} + +### Event IDs + +{{% rver-fragment name="v4-event-ids" %}} + +### Event format + +{{% rver-fragment name="v4-event-format" %}} + +### Handling redactions + +{{% rver-fragment name="v3-handling-redactions" %}} + +### Canonical JSON + +{{% rver-fragment name="v6-canonical-json" %}} + +### Signing key validity period + +{{% rver-fragment name="v5-signing-requirements" %}} diff --git a/content/rooms/v9.md b/content/rooms/v9.md new file mode 100644 index 00000000..f61c9af9 --- /dev/null +++ b/content/rooms/v9.md @@ -0,0 +1,119 @@ +--- +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 the redaction algorithm locally should refer to the +[redactions](#redactions) section below for a full overview. + +### Redactions + +{{% added-in this=true %}} `m.room.member` now keep `join_authorised_via_users_server` +in addition to other keys in `content` when being redacted. + +{{% 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 %}} + +The full redaction algorithm follows. + +{{% rver-fragment name="v3-handling-redactions" %}} + +Upon receipt of a redaction event, the server must strip off any keys +not in the following list: + +- `event_id` +- `type` +- `room_id` +- `sender` +- `state_key` +- `content` +- `hashes` +- `signatures` +- `depth` +- `prev_events` +- `prev_state` +- `auth_events` +- `origin` +- `origin_server_ts` +- `membership` + +The content object must also be stripped of all keys, unless it is one +of one of the following event types: + +- `m.room.member` allows keys `membership`, `join_authorised_via_users_server`. +- `m.room.create` allows key `creator`. +- `m.room.join_rules` allows keys `join_rule`, `allow`. +- `m.room.power_levels` allows keys `ban`, `events`, `events_default`, + `kick`, `redact`, `state_default`, `users`, `users_default`. +- `m.room.history_visibility` allows key `history_visibility`. + +## 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 + +[See above](#redactions). + +## Unchanged from v8 + +The following sections have not been modified since v8, but are included for +completeness. + +### State resolution + +{{% rver-fragment name="v2-state-res" %}} + +### Authorization rules + +{{% rver-fragment name="v8-auth-rules" %}} + +### Event IDs + +{{% rver-fragment name="v4-event-ids" %}} + +### Event format + +{{% rver-fragment name="v4-event-format" %}} + +### Handling redactions + +{{% rver-fragment name="v3-handling-redactions" %}} + +### Canonical JSON + +{{% rver-fragment name="v6-canonical-json" %}} + +### Signing key validity period + +{{% rver-fragment name="v5-signing-requirements" %}} diff --git a/content/server-server-api.md b/content/server-server-api.md index 1ba1f284..df8e5603 100644 --- a/content/server-server-api.md +++ b/content/server-server-api.md @@ -407,21 +407,25 @@ 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, + and the [room version supports restricted rooms](/rooms/#feature-matrix), + then the `m.room.member` event with `state_key` matching + `content.join_authorised_via_users_server`. #### Rejection @@ -744,19 +748,47 @@ The joining server is expected to add or replace the `origin`, `origin_server_ts`, and `event_id` on the templated event received by the resident server. This event is then signed by the joining server. -To complete the join handshake, the joining server must now submit this -new event to a resident homeserver, by using the `PUT /send_join` +To complete the join handshake, the joining server submits this new event +to the resident server it used for `GET /make_join`, 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 as well as the freshly +signed membership event. 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 [which support restricted join rules](/rooms/#feature-matrix). + +A resident server processing a request to join 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 should try a different +server. This is typically because the resident server can see that the +joining user satisfies 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 3a4b7fb2..70e7444e 100644 --- a/data/api/client-server/joining.yaml +++ b/data/api/client-server/joining.yaml @@ -95,6 +95,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."} @@ -182,6 +183,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/definitions/send_join_response.yaml b/data/api/server-server/definitions/send_join_response.yaml index cfa37910..744324ba 100644 --- a/data/api/server-server/definitions/send_join_response.yaml +++ b/data/api/server-server/definitions/send_join_response.yaml @@ -55,4 +55,14 @@ properties: properties: [] example: $ref: "../examples/minimal_pdu.json" + event: + type: object + title: SignedMembershipEvent + x-addedInMatrixVersion: "1.2" + description: |- + Required if the room version [supports restricted join rules](/rooms/#feature-matrix). The signed + copy of the membership event sent to other servers by the resident server, including the resident + server's signature. + example: + $ref: "../examples/minimal_pdu.json" required: ["auth_chain", "state", "origin"] diff --git a/data/api/server-server/examples/pdu_v4_join_membership.json b/data/api/server-server/examples/pdu_v4_join_membership.json new file mode 100644 index 00000000..1057ea94 --- /dev/null +++ b/data/api/server-server/examples/pdu_v4_join_membership.json @@ -0,0 +1,28 @@ +{ + "$ref": "unsigned_pdu_base.json", + "hashes": { + "sha256": "thishashcoversallfieldsincasethisisredacted" + }, + "signatures": { + "example.com": { + "ed25519:key_version": "these86bytesofbase64signaturecoveressentialfieldsincludinghashessocancheckredactedpdus" + }, + "resident.example.com": { + "ed25519:other_key_version": "a different signature" + } + }, + "auth_events": [ + "$urlsafe_base64_encoded_eventid", + "$a-different-event-id" + ], + "prev_events": [ + "$urlsafe_base64_encoded_eventid", + "$a-different-event-id" + ], + "type": "m.room.member", + "state_key": "@alice:example.com", + "content": { + "membership": "join", + "join_authorised_via_users_server": "@arbitrary:resident.example.com" + } +} diff --git a/data/api/server-server/joins-v1.yaml b/data/api/server-server/joins-v1.yaml index adf7298b..3da50f45 100644 --- a/data/api/server-server/joins-v1.yaml +++ b/data/api/server-server/joins-v1.yaml @@ -115,6 +115,18 @@ paths: type: string description: The value `join`. example: "join" + join_authorised_via_users_server: + type: string + x-addedInMatrixVersion: "1.2" + description: |- + Required if the room is [restricted](/client-server-api/#restricted-rooms) + and is joining through one of the conditions available. If the + user is responding to an invite, this is not required. + + 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,18 +146,37 @@ paths: "origin_server_ts": 1549041175876, "sender": "@someone:example.org", "content": { - "membership": "join" + "membership": "join", + "join_authorised_via_users_server": "@anyone:resident.example.org" } } } 400: description: |- - The request is invalid or the room the server is attempting + The request is invalid, the room the server is attempting to join has a version that is not listed in the `ver` - parameters. + parameters, or the server was unable to validate + [restricted room conditions](#restricted-rooms). The error should be passed through to clients so that they may give better feedback to users. + + New in `v1.2`, the following error conditions might happen: + + 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. + + `M_UNABLE_TO_GRANT_JOIN` is returned to denote that a different + server should be attempted for the join. This is typically because + the resident server can see that the joining user satisfies one or + more conditions, such as in the case of + [restricted rooms](/client-server-api/#restricted-rooms), but the + resident server would be unable to meet the auth rules governing + `join_authorised_via_users_server` on the resulting `m.room.member` + event. schema: allOf: - $ref: "../client-server/definitions/errors/error.yaml" @@ -162,7 +193,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 +284,23 @@ paths: type: string description: The value `join`. example: "join" + join_authorised_via_users_server: + type: string + x-addedInMatrixVersion: "1.2" + description: |- + Required if the room is [restricted](/client-server-api/#restricted-rooms) + and is joining through one of the conditions available. If the + user is responding to an invite, this is not required. + + 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 +317,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: @@ -278,6 +340,7 @@ paths: { "origin": "matrix.org", "auth_chain": [{"$ref": "examples/minimal_pdu.json"}], - "state": [{"$ref": "examples/minimal_pdu.json"}] + "state": [{"$ref": "examples/minimal_pdu.json"}], + "event": {"$ref": "examples/pdu_v4_join_membership.json"} } ] diff --git a/data/api/server-server/joins-v2.yaml b/data/api/server-server/joins-v2.yaml index b3ade854..d4b90994 100644 --- a/data/api/server-server/joins-v2.yaml +++ b/data/api/server-server/joins-v2.yaml @@ -103,6 +103,23 @@ paths: type: string description: The value `join`. example: "join" + join_authorised_via_users_server: + type: string + x-addedInMatrixVersion: "1.2" + description: |- + Required if the room is [restricted](/client-server-api/#restricted-rooms) + and is joining through one of the conditions available. If the + user is responding to an invite, this is not required. + + 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 +136,52 @@ 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. + + New in `v1.2`, the following error conditions might happen: + + 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. + + `M_UNABLE_TO_GRANT_JOIN` is returned to denote that a different + server should be attempted for the join. This is typically because + the resident server can see that the joining user satisfies one or + more conditions, such as in the case of + [restricted rooms](/client-server-api/#restricted-rooms), but the + resident server would be unable to meet the auth rules governing + `join_authorised_via_users_server` on the resulting `m.room.member` + event. + 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. @@ -132,5 +191,6 @@ paths: application/json: { "origin": "matrix.org", "auth_chain": [{"$ref": "examples/minimal_pdu.json"}], - "state": [{"$ref": "examples/minimal_pdu.json"}] + "state": [{"$ref": "examples/minimal_pdu.json"}], + "event": {"$ref": "examples/pdu_v4_join_membership.json"} } 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 00000000..bf807e54 --- /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 00000000..eb8c84bc --- /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 5f0e11af..dbac60d5 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 [which support knocking](/rooms/#feature-matrix). + * `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 [which support the join rule](/rooms/#feature-matrix). + * `private` - reserved without implementation. No significant meaning. properties: content: properties: @@ -21,7 +23,37 @@ properties: - knock - invite - private + - restricted type: string + allow: + x-addedInMatrixVersion: "1.2" + 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 diff --git a/data/event-schemas/schema/m.room.member.yaml b/data/event-schemas/schema/m.room.member.yaml index 9427d25f..7d196f4d 100644 --- a/data/event-schemas/schema/m.room.member.yaml +++ b/data/event-schemas/schema/m.room.member.yaml @@ -63,6 +63,18 @@ properties: is_direct: description: Flag indicating if the room containing this event was created with the intention of being a direct chat. See [Direct Messaging](/client-server-api/#direct-messaging). type: boolean + join_authorised_via_users_server: + x-addedInMatrixVersion: "1.2" + type: string + description: |- + Usually found on `join` events, this field is used to denote which homeserver (through representation of a user with sufficient power level) + authorised the user's join. More information about this field can be found in the [Restricted Rooms Specification](#restricted-rooms). + + Client and server implementations should be aware of the [signing implications](/rooms/v8/#authorization-rules) of including this + field in further events: in particular, the event must be signed by the server which + owns the user ID in the field. When copying the membership event's `content` + (for profile updates and similar) it is therefore encouraged to exclude this + field in the copy, as otherwise the event might fail event authorization. reason: x-addedInMatrixVersion: "1.1" type: string diff --git a/static/diagrams/membership.drawio b/static/diagrams/membership.drawio index 1c6708de..9eaa707b 100644 --- a/static/diagrams/membership.drawio +++ b/static/diagrams/membership.drawio @@ -1 +1 @@ -3Vvdb6M4EP9rIt09NMIGA3ncZtvbh73TSn243X1ZOYmT0BIcOaZJ9q8/E0z4cggEgulVqoqHsbHHv/nwjDsyp5vDXwxv13/TBfFH0FgcRubnEYTAAa74E1GOMWViopiwYt5CMqWEF+83kURDUkNvQXY5Rk6pz71tnjinQUDmPEfDjNF9nm1J/fxXt3hFSoSXOfbL1H+9BV/HVBc6Kf0L8Vbr5MvAnsRvNjhhlivZrfGC7jMk82lkThmlPH7aHKbEj4SXyCXu93zh7XlijAS8TodvBvr96euj/xL83G+Dn2gaTvgDkMO8Yz+UKxYjvFIvkJPmx0QSZCEEI5uU8TVd0QD7Tyn1kdEwWJDoc4ZopTxfKd0KIhDEV8L5Ue4yDjkVpDXf+PJt/M3oQxfXKEk7GrI5qVqYxApmK8Ir+OB5JwSECd0Qzo6iHyM+5t57fh5YYml15pNdPzGGjxmGrZAf32VG/hYRBINUCziRmJBKYdqFrWvGLx7iGSStzFJS0gkOTaBhqaDhEyykogEb5ODx75nnH9FQY4hk8/NBDn1qHJNGIGTwPcMZtX9kX6b9Tq2kY4dIhKgmFC01FCUGjDEwHDMeqhk6S3Cy3DycoDUZ25kfx8qPGM9cDpLCrinqLVD4rG1Wot4CqIo/j/q0dzIdulzuCC/06EYzSorhBe8eL6tFHvT7teB52eITWvbCXaqM3zthnByqQXcRI9ApSBjJ9j51XcCQtHXGbSV8KgzlpNdYVHaFETEoG0VO6/nNm799cH8DO1JysRduOyW/P/4hVG3qDGuJGRK/YIxRxjOA29wC6NEt1ASMWQmYB2NsI6ulW+gBMSXAKEPMRvZySQP+jDeeH23OVIjbI5E5+Yfs72NMLUu7MXVVehcGujUvF5HVDMhAVu1kr34Uz2xnqWsrWautNks7rdrkQekLQoVDiqldX5R+KrY8hreMVhfOfG8+grYv5vI4E/KwV9FTxPOLhT7Z6XVp8Caf5ug66lg1VavtobsdKsq6Jca5EMH3sduXNq7HYKT2xqFr0QgaeigCnEEGr05W0426mq4tqVEXMBei1340vZzAUmevBuVGzWKuT7sbVUadGu1lO53RFnfCSWdG1rVdq52dPddnCgkjB+WHiFdVSv31arCNjx3F5RPW1ccjLXBraaLrIcuy6yGrs4yDsrCly82nHrtRhHdLbksHhK7mqFxogrsYLGTVg1XTWgUo1CqQWV2hs41Kfn21CqhMwA/Ae9+SNGp6tM3Fal+I/064N8cdK0lHZycRzN+iIU2BbVr5ohqYoIEAVXkuG0qlqGWV4eOitrpeJUy7A4ZffjAHFQzcEBf+L6B0NUpwEGoZJfRgpspJhbeAKozSoJMK5zBKW1IhuRXT8SWKvouC+m9YJEF6RpBPB06YMEknl1k8FP8R0JOmiN8N2cwI+7NC4qChxMsmpl1JqXCGVKXCoELY9r2EbSpzEdIAxMmIU+MBz4QwB5qQKJSVaiebu/E8r+Fmmyw0oAFJSPGsHV1JkKtxjuvCm3JuTQP0wt08YFUfPM+3EtT8Gi/Jle2S/pjrVug3O3ZmIY7ZvGNI171OejXcmvRy4ISTwoETXcmkwEr+ewBaNNNr+DF7+s8M5tN/ \ No newline at end of file +1Vvbbts4EP0aA7sPMUTq6sfGTdIusrvFGtht+lLQNm0rlUWDpnzp15eyKOvGeEnJkpwAQcTRkBKHZ2YOh8rAHK8PTxRtVn+SOQ4G0JgfBubHAYTABR7/E0uOQmJAkEiW1J8LWSaY+D9xqiikkT/H24IiIyRg/qYonJEwxDNWkCFKyb6otiBB8akbtMQVwWSGgqr0P3/OVonUg24m/4T95Sp9MnBGyZ01SpXFTLYrNCf7nMh8GJhjSghLrtaHMQ5i66V2Sfo9vnH3/GIUh0ylwxfD/vnh+T6YhN/2m/CbPY5G7A6IYXYoiMSM+QivxA/FS7Njagk854YRTULZiixJiIKHTHpPSRTOcfw4g7cynWdCNlwIuPAVM3YUq4wiRrhoxdaBuFudkni9LYnoDF+ah4AGokvMLujBRC+eS+4BwmBPmKwxo0euQHGAmL8rggAJLC3PeqLrB0rRMaew4fZj29zIX2IBVxB+AUcCE8IrTKe0dHr6/CJ5g7SVm0omOsFBBxqWDBoBRtwqPWADH3z2NXf9Eg81hLZofjyIoU+NY9oIuQ2+5jTj9kv+Ztbv1Eo71kcif44aFC1FKAoMGEMLABFb9NBZgZPlFeEErdHQyf24VnHEZCpikAx2uqi3QOmxjnkR9RawL+kXUZ/1Tl+HLBZbzEo9ruMZFcfww53Pqm5RBP1+xXUmG3SCz57nS8Xgt8OU4YMKRqBbsrAt2vssdQFDyFa5tJXqyTBUsJ62qZwLQcQgdBAnrccf/uzH+8o3sC0nN0aO18zJ28c/hLJFnaJeOEOaF4yhncsMoF5aAO2lBUXAmHqAuTOGjm2ZN4+YCmCkFFMrXi5IyB7R2g/ixRlz+/s4Did/4X07wdSyeg+mnszvorBvzyswMleRkYG836XdWnE988qxWtnNGi22WVlr2TLflMfYdmmbAnv3GGmmSmKP4S8G0An4K9xPuRmcZXyFgoDsuT2hMT1W78Ydv9MowNvqvd820TTwZwM4jjHEZr/3mwxhrWzodrRJshRd8urb9WZoqvokH+cN7t/Far+1cO3RGOWFs7V5jH3rJAa4N0h7a5ZDiskXtpd8VQGjynuv7ul/YMcJJn9/frr/5/Hfw/dnRl8+S5JvvEsNSbxNleaN0707NOWLq5U1+k0Sbj5JGIrQqRVrSuvacvCx/i/WwAJPubv50FMtxcrrsDdFB81y1bp3OijdP/WYv5s5YjGGtxfC4ai9pO85ntXM+c5HjaXSp2sXh0imWSlid0ogDBn+crsR47z9KOeJeDdivJvtR5GUXOYkXeDy2txCDYKWowbBqxXZpGe5ffHTjC9o0YU65dwOIKRflvWgCVqJbLalBivd4zlQOp6zzcuH0o5xUb+/4zkoPXO6gTSvERVr12QKpO4TDnaY+TPUzEna2vSfeXi7X1uYVvEcGYzsGwGqtKAgmH05wfd9VtrwnO3dgFjzxDY+gGsW5tvHmXlTxKAGR6z9GU+fQNKnDK5tN6QMHcSsailC1MXeUynizKl6K0WkufLKHxF1fSje/xdGKYXPGfLhwDDlMemUMCsb5ZCcPIX/rvF6iml1p5xZHGhaXCnEaByoljaUpsTYUGJspy1jm9IKRr4wbqhWwvtkMLBW3fs6mec1Wm/SiYYkxKkoeWu3o4qIPsvxPFirUqfL1kvfpgLr8i70/FWOXL/Hj0Srcal/0tUJ9PMIR3TWDNGqX1Prs61RJ5tPOCptPm29qkpJvw0882b2XyiJevbPPObDLw== \ No newline at end of file diff --git a/static/diagrams/membership.png b/static/diagrams/membership.png index 891b338b..586bf28c 100644 Binary files a/static/diagrams/membership.png and b/static/diagrams/membership.png differ