|
| 1 | +# Restricting room membership based on membership in other rooms |
| 2 | + |
| 3 | +A desirable feature is to give room admins the power to restrict membership of |
| 4 | +their room based on the membership of one or more rooms. |
| 5 | + |
| 6 | +Potential usecases include: |
| 7 | + |
| 8 | +* Private spaces (allowing any member of a [MSC1772](https://github.com/matrix-org/matrix-doc/pull/1772) |
| 9 | + space to join child rooms in that space), for example: |
| 10 | + |
| 11 | + > members of the #doglovers:example.com space can join this room without an invitation<sup id="a1">[1](#f1)</sup> |
| 12 | +* Room upgrades for private rooms (instead of issuing invites to each user). |
| 13 | +* Allowing all users in a private room to be able to join a private breakout room. |
| 14 | + |
| 15 | +This does not preclude members from being directly invited to the room, which is |
| 16 | +still a useful discovery feature. |
| 17 | + |
| 18 | +## Proposal |
| 19 | + |
| 20 | +In a future room version a new `join_rule` (`restricted`) will be used to reflect |
| 21 | +a cross between `invite` and `public` join rules. The content of the join rules |
| 22 | +would include the rooms to trust for membership. For example: |
| 23 | + |
| 24 | +```json |
| 25 | +{ |
| 26 | + "type": "m.room.join_rules", |
| 27 | + "state_key": "", |
| 28 | + "content": { |
| 29 | + "join_rule": "restricted", |
| 30 | + "allow": [ |
| 31 | + { |
| 32 | + "type": "m.room_membership", |
| 33 | + "room_id": "!mods:example.org" |
| 34 | + }, |
| 35 | + { |
| 36 | + "type": "m.room_membership", |
| 37 | + "room_id": "!users:example.org" |
| 38 | + } |
| 39 | + ] |
| 40 | + } |
| 41 | +} |
| 42 | +``` |
| 43 | + |
| 44 | +This means that a user must be a member of the `!mods:example.org` room or |
| 45 | +`!users:example.org` room in order to join without an invite<sup id="a2">[2](#f2)</sup>. |
| 46 | +Membership in a single allowed room is enough. |
| 47 | + |
| 48 | +If the `allow` key is an empty list (or not a list at all), then no users are |
| 49 | +allowed to join without an invite. Each entry is expected to be an object with the |
| 50 | +following keys: |
| 51 | + |
| 52 | +* `type`: `"m.room_membership"` to describe that we are allowing access via room |
| 53 | + membership. Future MSCs may define other types. |
| 54 | +* `room_id`: The room ID to check the membership of. |
| 55 | + |
| 56 | +Any entries in the list which do not match the expected format are ignored. Thus, |
| 57 | +if all entries are invalid, the list behaves as if empty and all users without |
| 58 | +an invite are rejected. |
| 59 | + |
| 60 | +When a homeserver receives a `/join` request from a client or a `/make_join` / |
| 61 | +`/send_join` request from another homeserver, the request should only be permitted |
| 62 | +if the user is invited to this room, or is joined to one of the listed rooms. If |
| 63 | +the user is not a member of at least one of the rooms, the homeserver should return |
| 64 | +an error response with HTTP status code of 403 and an `errcode` of `M_FORBIDDEN`. |
| 65 | + |
| 66 | +It is possible for a resident homeserver (one which receives a `/make_join` / |
| 67 | +`/send_join` request) to not know if the user is in some of the allowed rooms (due |
| 68 | +to not participating in them). If the user is not in any of the allowed rooms that |
| 69 | +are known to the homeserver, and the homeserver is not participating in all listed |
| 70 | +rooms, then it should return an error response with HTTP status code of 400 with an `errcode` of `M_UNABLE_TO_AUTHORISE_JOIN`. The joining server should |
| 71 | +attempt to join via another resident homeserver. If the resident homeserver knows |
| 72 | +that the user is not in *any* of the allowed rooms it should return an error response |
| 73 | +with HTTP status code of 403 and an `errcode` of `M_FORBIDDEN`. Note that it is a |
| 74 | +configuration error if there are allowed rooms with no participating authorised |
| 75 | +servers. |
| 76 | + |
| 77 | +A chosen resident homeserver might also be unable to issue invites (which, as below, |
| 78 | +is a pre-requisite for generating a correctly-signed join event). In this case |
| 79 | +it should return an error response with HTTP status code of 400 and an `errcode` |
| 80 | +of `M_UNABLE_TO_GRANT_JOIN`. The joining server should attempt to join via another |
| 81 | +resident homeserver. |
| 82 | + |
| 83 | +From the perspective of the [auth rules](https://spec.matrix.org/unstable/rooms/v1/#authorization-rules), |
| 84 | +the `restricted` join rule has the same behavior as `public`, with the additional |
| 85 | +caveat that servers must ensure that, for `m.room.member` events with a `membership` of `join`: |
| 86 | + |
| 87 | +* The user's previous membership was `invite` or `join`, or |
| 88 | +* The join event has a valid signature from a homeserver whose users have the |
| 89 | + power to issue invites. |
| 90 | + |
| 91 | + When generating a join event for `/join` or `/make_join`, the server should |
| 92 | + include the MXID of a local user who could issue an invite in the content with |
| 93 | + the key `join_authorised_via_users_server`. The actual user chosen is arbitrary. |
| 94 | + |
| 95 | +The changes to the auth rules imply that: |
| 96 | + |
| 97 | +* A join event issued via `/send_join` is signed by not just the requesting |
| 98 | + server, but also the resident server.<sup id="a3">[3](#f3)</sup> |
| 99 | + |
| 100 | + In order for the joining server to receive the proper signatures the join |
| 101 | + event will be returned via `/send_join` in the `event` field. |
| 102 | +* The auth chain of the join event needs to include events which prove |
| 103 | + the homeserver can be issuing the join. This can be done by including: |
| 104 | + |
| 105 | + * The `m.room.power_levels` event. |
| 106 | + * The join event of the user specified in `join_authorised_via_users_server`. |
| 107 | + |
| 108 | + It should be confirmed that the authorising user is in the room. (This |
| 109 | + prevents situations where any homeserver could process the join, even if |
| 110 | + they weren't in the room, under certain power level conditions.) |
| 111 | + |
| 112 | +The above creates a new restriction on the relationship between the resident |
| 113 | +servers used for `/make_join` and `/send_join` -- they must now both go to |
| 114 | +the same server (since the `join_authorised_via_users_server` is added in |
| 115 | +the call to `/make_join`, while the final signature is added during |
| 116 | +the call to `/send_join`). If a request to `/send_join` is received that includes |
| 117 | +an event from a different resident server it should return an error response with |
| 118 | +HTTP status code of 400. |
| 119 | + |
| 120 | +Note that the homeservers whose users can issue invites are trusted to confirm |
| 121 | +that the `allow` rules were properly checked (since this cannot easily be |
| 122 | +enforced over federation by event authorisation).<sup id="a4">[4](#f4)</sup> |
| 123 | + |
| 124 | +To better cope with joining via aliases, homeservers should use the list of |
| 125 | +authorised servers (not the list of candidate servers) when a user attempts to |
| 126 | +join a room. |
| 127 | + |
| 128 | +## Summary of the behaviour of join rules |
| 129 | + |
| 130 | +See the [join rules](https://matrix.org/docs/spec/client_server/r0.6.1#m-room-join-rules) |
| 131 | +specification for full details; the summary below is meant to highlight the differences |
| 132 | +between `public`, `invite`, and `restricted` from a user perspective. Note that |
| 133 | +all join rules are subject to `ban` and `server_acls`. |
| 134 | + |
| 135 | +* `public`: anyone can join, as today. |
| 136 | +* `invite`: only people with membership `invite` can join, as today. |
| 137 | +* `knock`: the same as `invite`, except anyone can knock. See |
| 138 | + [MSC2403](https://github.com/matrix-org/matrix-doc/pull/2403). |
| 139 | +* `private`: This is reserved, but unspecified. |
| 140 | +* `restricted`: the same as `invite`, except users may also join if they are a |
| 141 | + member of a room listed in the `allow` rules. |
| 142 | + |
| 143 | +## Security considerations |
| 144 | + |
| 145 | +Increased trust to enforce the join rules during calls to `/join`, `/make_join`, |
| 146 | +and `/send_join` is placed in the homeservers whose users can issue invites. |
| 147 | +Although it is possible for those homeservers to issue a join event in bad faith, |
| 148 | +there is no real-world benefit to doing this as those homeservers could easily |
| 149 | +side-step the restriction by issuing an invite first anyway. |
| 150 | + |
| 151 | +## Unstable prefix |
| 152 | + |
| 153 | +The `restricted` join rule will be included in a future room version to allow |
| 154 | +servers and clients to opt-into the new functionality. |
| 155 | + |
| 156 | +During development, an unstable room version of `org.matrix.msc3083.v2` will be used. |
| 157 | +Since the room version namespaces the behaviour, the `allow` key and value, as well |
| 158 | +as the `restricted` join rule value do not need unstable prefixes. |
| 159 | + |
| 160 | +An unstable key of `org.matrix.msc3083.v2.event` will be used in the response |
| 161 | +from `/send_join` in place of `event` during development. |
| 162 | + |
| 163 | +## Alternatives |
| 164 | + |
| 165 | +It may seem that just having the `allow` key with `public` join rules is enough |
| 166 | +(as originally suggested in [MSC2962](https://github.com/matrix-org/matrix-doc/pull/2962)), |
| 167 | +but there are concerns that changing the behaviour of a pre-existing `public` |
| 168 | +join rule may cause security issues in older implementations (that do not yet |
| 169 | +understand the new behaviour). This could be solved by introducing a new room |
| 170 | +version, thus it seems clearer to introduce a new join rule -- `restricted`. |
| 171 | + |
| 172 | +Using an `allow` key with the `invite` join rules to broaden who can join was rejected |
| 173 | +as an option since it requires weakening the [auth rules](https://spec.matrix.org/unstable/rooms/v1/#authorization-rules). |
| 174 | +From the perspective of the auth rules, the `restricted` join rule is identical |
| 175 | +to `public` with additional checks on the signature of the event. |
| 176 | + |
| 177 | +## Future extensions |
| 178 | + |
| 179 | +### Checking room membership over federation |
| 180 | + |
| 181 | +If a homeserver is not in an allowed room (and thus doesn't know the |
| 182 | +membership of it) then the server cannot enforce the membership checks while |
| 183 | +generating a join event. Peeking over federation, as described in |
| 184 | +[MSC2444](https://github.com/matrix-org/matrix-doc/pull/2444), |
| 185 | +could be used to establish if the user is in any of the proper rooms. |
| 186 | + |
| 187 | +This would then delegate power out to a (potentially) untrusted server, giving that |
| 188 | +peek server significant power. For example, a poorly chosen peek |
| 189 | +server could lie about the room membership and add an `@evil_user:example.org` |
| 190 | +to an allowed room to gain membership to a room. |
| 191 | + |
| 192 | +As iterated above, this MSC recommends rejecting the join, potentially allowing |
| 193 | +the requesting homeserver to retry via another homeserver. |
| 194 | + |
| 195 | +### Kicking users out when they leave the allowed room |
| 196 | + |
| 197 | +In the above example, suppose `@bob:server.example` leaves `!users:example.org`: |
| 198 | +should they be removed from the room? Likely not, by analogy with what happens |
| 199 | +when you switch the join rules from `public` to `invite`. Join rules currently govern |
| 200 | +joins, not existing room membership. |
| 201 | + |
| 202 | +It is left to a future MSC to consider this, but some potential thoughts are |
| 203 | +given below. |
| 204 | + |
| 205 | +If you assume that a user *should* be removed in this case, one option is to |
| 206 | +leave the departure up to Bob's server `server.example`, but this places a |
| 207 | +relatively high level of trust in that server. Additionally, if `server.example` |
| 208 | +were offline, other users in the room would still see Bob in the room (and their |
| 209 | +servers would attempt to send message traffic to it). |
| 210 | + |
| 211 | +Another consideration is that users may have joined via a direct invite, not via |
| 212 | +access through a room. |
| 213 | + |
| 214 | +Fixing this is thorny. Some sort of annotation on the membership events might |
| 215 | +help, but it's unclear what the desired semantics are: |
| 216 | + |
| 217 | +* Assuming that users in an allowed room are *not* kicked when that room is |
| 218 | + removed from `allow`, are those users then given a pass to remain |
| 219 | + in the room indefinitely? What happens if the room is added back to |
| 220 | + `allow` and *then* the user leaves it? |
| 221 | +* Suppose a user joins a room via an allowed room (RoomA). Later, RoomB is added |
| 222 | + to the `allow` list and RoomA is removed. What should happen when the |
| 223 | + user leaves RoomB? Are they exempt from the kick? |
| 224 | + |
| 225 | +It is possible that completely different state should be kept, or a different |
| 226 | +`m.room.member` state could be used in a more reasonable way to track this. |
| 227 | + |
| 228 | +### Inheriting join rules |
| 229 | + |
| 230 | +If an allowed room is a space and you make a parent space invite-only, should that |
| 231 | +(optionally?) cascade into child rooms? This would have some of the same problems |
| 232 | +as inheriting power levels, as discussed in [MSC2962](https://github.com/matrix-org/matrix-doc/pull/2962). |
| 233 | + |
| 234 | +### Additional allow types |
| 235 | + |
| 236 | +Future MSCs may wish to define additional values for the `type` argument, potentially |
| 237 | +restricting access via: |
| 238 | + |
| 239 | +* MXIDs or servers. |
| 240 | +* A shared secret (room password). |
| 241 | + |
| 242 | +These are just examples and are not fully thought through for this MSC, but it should |
| 243 | +be possible to add these behaviors in the future. |
| 244 | + |
| 245 | +### Client considerations |
| 246 | + |
| 247 | +[MSC1772](https://github.com/matrix-org/matrix-doc/pull/1772) defines a `via` |
| 248 | +key in the content of `m.space.child` events: |
| 249 | + |
| 250 | +> the content must contain a via `key` which gives a list of candidate servers |
| 251 | +> that can be used to join the room. |
| 252 | +
|
| 253 | +It is possible for the list of candidate servers and the list of authorised |
| 254 | +servers to diverge. It may not be possible for a user to join a room if there's |
| 255 | +no overlap between these lists. |
| 256 | + |
| 257 | +If there is some overlap between the lists of servers the join request should |
| 258 | +complete successfully. |
| 259 | + |
| 260 | +Clients should also consider the authorised servers when generating candidate |
| 261 | +servers to embed in links to the room, e.g. via matrix.to. |
| 262 | + |
| 263 | +A future MSC may define a way to override or update the `via` key in a coherent |
| 264 | +manner. |
| 265 | + |
| 266 | +## Footnotes |
| 267 | + |
| 268 | +<a id="f1"/>[1]: The converse restriction, "anybody can join, provided they are not members |
| 269 | +of the #catlovers:example.com space" is less useful since: |
| 270 | + |
| 271 | +1. Users in the banned room could simply leave it at any time |
| 272 | +2. This functionality is already partially provided by |
| 273 | + [Moderation policy lists](https://matrix.org/docs/spec/client_server/r0.6.1#moderation-policy-lists). [↩](#a1) |
| 274 | + |
| 275 | +<a id="f2"/>[2]: Note that there is nothing stopping users sending and |
| 276 | +receiving invites in `public` rooms today, and they work as you might expect. |
| 277 | +The only difference is that you are not *required* to hold an invite when |
| 278 | +joining the room. [↩](#a2) |
| 279 | + |
| 280 | +<a id="f3"/>[3]: This seems like an improvement regardless since the resident server |
| 281 | +is accepting the event on behalf of the joining server and ideally this should be |
| 282 | +verifiable after the fact, even for current room versions. Requiring all events |
| 283 | +to be signed and verified in this way is left to a future MSC. [↩](#a3) |
| 284 | + |
| 285 | +<a id="f4"/>[4]: This has the downside of increased centralisation, as some |
| 286 | +homeservers that are already in the room may not issue a join event for another |
| 287 | +user on that server. (It must go through the `/make_join` / `/send_join` flow of |
| 288 | +a server whose users may issue invites.) This is considered a reasonable |
| 289 | +trade-off. [↩](#a4) |
0 commit comments