A resident authorises restricted joins only after checking the allow conditions¶
- Status: accepted
- Date: 2026-10-22
Technical Story: conformance audit 2026-10-17, finding R04
Context and Problem Statement¶
When this server is resident in a restricted (or knock_restricted) room, a user who is not
invited joins through an allow condition. The spec puts the check on the resident:
client-server-api.md, "Restricted rooms": "When joining without an invite, the server MUST verify
that the requesting user meets at least one of the conditions." server-server-api.md, "Restricted
rooms", names the refusals: 400 M_UNABLE_TO_AUTHORISE_JOIN when no condition can be validated,
400 M_UNABLE_TO_GRANT_JOIN when a condition holds but no local user can meet the auth rules for
join_authorised_via_users_server, 403 M_FORBIDDEN when every condition fails. The make_join
template must name "an arbitrary user ID belonging to the resident server ... that is able to
issue invites", and on send_join "the signature must be added before sending or persisting the
event". The auth rules only check that the named user is joined with invite power and that its
server signed the event (rooms/v10.md 4.2.1, 4.3.5): whether the user met a condition is checked
by nobody else.
Before 0.12.36 none of this was implemented: templates never named a user, send_join refused
such a join for lacking our signature, and local joins into local restricted rooms failed the
auth rules.
Decision Outcome¶
One decision function, homeserver::decide_restricted_join, used by the make_join template
provider, the send_join acceptor and every locally composed join:
- It reads only this server's current state, and a condition can be validated only while this
server is resident in the room it names (its
m.room.createis stored and one of its users is joined there). State kept after this server left a room is not current: the user may have left or been banned since. "Restricted rooms" puts that case underM_UNABLE_TO_AUTHORISE_JOIN: the resident "does not have state information about rooms required by the conditions". No condition met and at least one that cannot be validated isM_UNABLE_TO_AUTHORISE_JOIN, so the joining server tries another resident; all validated and none met isM_FORBIDDEN. The spec fixes the errcode only when no condition at all can be validated; the mixed case takes the retryable answer because another resident may hold the missing state. - The room version comes from the create event through
declared_room_version(ADR-0179). - At most
max_restricted_join_conditions(32) allow entries are considered. The list is room state another server's administrator can write, each condition costs a lookup underruntime.mutex, and an entry past the bound counts as absent, which can only refuse. - The authorising user is the lowest user ID among this server's joined users whose effective power level (room creators infinite from v12) is at least the invite level, so the same state always gives the same user.
- The acceptor co-signs (
sign_invite_event, main only; the worker never signs, ADR-0078) only asend_joinwhose named user is ours and for which the decision isauthorisedfor that named user, before the auth checks and before anything is stored. It first drops every signature the joining server filed under this server's name: thesignaturesobject is covered by no signature, so those entries are unverified, and this server's signature on the event must be only the one it adds. It refuses, unsigned and unstored, a join naming our user in a room that is not restricted or where the user is already invited or joined: our signature is not needed there and would stand as an authorisation if state resolution later judged the event under restricted join rules. The request layer and the worker relay skip the authorising-signature check only for asend_joinnaming this server (PduSignatureChecks::authorised_here), never for another endpoint or server. compose_signed_eventrefuses any locally composed join that names one of our users unless the decision isauthorisedfor that user. This server signs every event it composes, so without this gate a client could name an administrator throughPUT /state/m.room.memberand skip the condition check. A join by a user whose current membership is alreadyjoinis the exception: it needs no authorisation (rule 4.3.5.1), and clients copy the join's content into profile updates, so the field is dropped from it rather than checked. client-server-api.md, m.room.member: "When copying the membership event'scontent(for profile updates and similar) it is therefore encouraged to exclude this field in the copy". Refusing it would fail every per-room display name change in a restricted room.
Positive Consequences¶
- Remote users join restricted rooms this server hosts alone; local users join local restricted rooms through their conditions.
Negative Consequences¶
- A local user whose condition holds in a room where no local user can invite is refused (403); falling back to a remote join through another resident is not implemented.
- The resident does not yet send the accepted join to the room's other servers (a separate,
pre-existing gap of the
send_joinacceptor).