Skip to content

Commit 43c2324

Browse files
authored
Merge pull request #3083 from matrix-org/clokep/restricted-rooms
MSC3083: Restricting room membership based on membership in other rooms
2 parents c116942 + 9699aa8 commit 43c2324

File tree

1 file changed

+289
-0
lines changed

1 file changed

+289
-0
lines changed

proposals/3083-restricted-rooms.md

Lines changed: 289 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,289 @@
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

Comments
 (0)