Skip to main content

tuwunel_matrix/room/state/resolution/event_auth/
room_member.rs

1use std::borrow::Borrow;
2
3use futures::future::{join, join3};
4use ruma::{
5	AnyKeyName, EventId, SigningKeyId, UserId,
6	events::room::member::MembershipState,
7	room_version_rules::AuthorizationRules,
8	serde::{Base64, base64::Standard},
9	signatures::verify_canonical_json_bytes,
10};
11use tuwunel_core::{Err, Result, err};
12
13use crate::Event;
14
15#[cfg(test)]
16mod tests;
17
18#[cfg(test)]
19use super::test_utils;
20use super::{
21	FetchState, auth_input_error,
22	events::{
23		JoinRule, RoomCreateEvent, RoomMemberEvent, RoomPowerLevelsIntField,
24		member::ThirdPartyInvite, power_levels::RoomPowerLevelsEventOptionExt,
25	},
26};
27
28/// Check whether the given event passes the `m.room.member` authorization
29/// rules.
30///
31/// This assumes that `ruma_signatures::verify_event()` was called previously,
32/// as some authorization rules depend on the signatures being valid on the
33/// event.
34#[tracing::instrument(level = "trace", skip_all)]
35pub(super) async fn check_room_member<Fetch, Pdu>(
36	room_member_event: &RoomMemberEvent<Pdu>,
37	rules: &AuthorizationRules,
38	room_create_event: &RoomCreateEvent<Fetch::Pdu>,
39	fetch_state: Fetch,
40) -> Result
41where
42	Fetch: FetchState,
43	Pdu: Event,
44{
45	// Since v1, if there is no state_key property, or no membership property in
46	// content, reject.
47	let Some(state_key) = room_member_event.state_key() else {
48		return Err!("missing `state_key` field in `m.room.member` event");
49	};
50
51	let target_user = <&UserId>::try_from(state_key)
52		.map_err(|e| err!("invalid `state_key` field in `m.room.member` event: {e}"))?;
53
54	// MSC4361: in a non-federating room, reject members whose state_key
55	// belongs to a remote server. The existing `m.federate` check at the
56	// top of `auth_check` only covers the sender's domain.
57	if !room_create_event
58		.federate()
59		.map_err(auth_input_error)?
60		&& target_user.server_name() != room_create_event.sender().server_name()
61	{
62		return Err!(
63			"MSC4361: room is not federated and target user domain does not match \
64			 `m.room.create` event's sender domain"
65		);
66	}
67
68	let target_membership = room_member_event.membership()?;
69
70	// These checks are done `in ruma_signatures::verify_event()`:
71	//
72	// Since v8, if content has a join_authorised_via_users_server property:
73	//
74	// - Since v8, if the event is not validly signed by the homeserver of the user
75	//   ID denoted by the key, reject.
76
77	match target_membership {
78		// Since v1, if membership is join:
79		| MembershipState::Join =>
80			check_room_member_join(
81				room_member_event,
82				target_user,
83				rules,
84				room_create_event,
85				fetch_state,
86			)
87			.await,
88
89		// Since v1, if membership is invite:
90		| MembershipState::Invite =>
91			check_room_member_invite(
92				room_member_event,
93				target_user,
94				rules,
95				room_create_event,
96				fetch_state,
97			)
98			.await,
99
100		// Since v1, if membership is leave:
101		| MembershipState::Leave =>
102			check_room_member_leave(
103				room_member_event,
104				target_user,
105				rules,
106				room_create_event,
107				fetch_state,
108			)
109			.await,
110
111		// Since v1, if membership is ban:
112		| MembershipState::Ban =>
113			check_room_member_ban(
114				room_member_event,
115				target_user,
116				rules,
117				room_create_event,
118				fetch_state,
119			)
120			.await,
121
122		// Since v7, if membership is knock:
123		| MembershipState::Knock if rules.knocking =>
124			check_room_member_knock(room_member_event, target_user, rules, fetch_state).await,
125
126		// Since v1, otherwise, the membership is unknown. Reject.
127		| _ => Err!("unknown membership"),
128	}
129}
130
131/// Check whether the given event passes the `m.room.member` authorization rules
132/// with a membership of `join`.
133#[tracing::instrument(level = "trace", skip_all)]
134async fn check_room_member_join<Fetch, Pdu>(
135	room_member_event: &RoomMemberEvent<Pdu>,
136	target_user: &UserId,
137	rules: &AuthorizationRules,
138	room_create_event: &RoomCreateEvent<Fetch::Pdu>,
139	fetch_state: Fetch,
140) -> Result
141where
142	Fetch: FetchState,
143	Pdu: Event,
144{
145	let creator = room_create_event
146		.creator(rules)
147		.map_err(auth_input_error)?;
148
149	let creators = room_create_event
150		.creators(rules)
151		.map_err(auth_input_error)?;
152
153	let mut prev_events = room_member_event.prev_events();
154
155	let prev_event_is_room_create_event = prev_events.next().is_some_and(|event_id| {
156		<EventId as Borrow<str>>::borrow(event_id)
157			== <EventId as Borrow<str>>::borrow(room_create_event.event_id())
158	});
159
160	let prev_event_is_only_room_create_event =
161		prev_event_is_room_create_event && prev_events.next().is_none();
162
163	// v1-v10, if the only previous event is an m.room.create and the state_key is
164	// the creator, allow.
165	// Since v11, if the only previous event is an m.room.create and the state_key
166	// is the sender of the m.room.create, allow. additional_creators do not
167	// satisfy this rule; the spec names the create sender singular.
168	if prev_event_is_only_room_create_event && *target_user == *creator {
169		return Ok(());
170	}
171
172	// Since v1, if the sender does not match state_key, reject.
173	if room_member_event.sender() != target_user {
174		return Err!("sender of join event must match target user");
175	}
176
177	let (current_membership, join_rule) =
178		join(fetch_state.user_membership(target_user), fetch_state.join_rule()).await;
179
180	// Since v1, if the sender is banned, reject.
181	let current_membership = current_membership?;
182	if current_membership == MembershipState::Ban {
183		return Err!("banned user cannot join room");
184	}
185
186	// v1-v6, if the join_rule is invite then allow if membership state is invite or
187	// join.
188	// Since v7, if the join_rule is invite or knock then allow if membership state
189	// is invite or join.
190	let join_rule = join_rule?;
191	if (join_rule == JoinRule::Invite || rules.knocking && join_rule == JoinRule::Knock)
192		&& matches!(current_membership, MembershipState::Invite | MembershipState::Join)
193	{
194		return Ok(());
195	}
196
197	// v8-v9, if the join_rule is restricted:
198	// Since v10, if the join_rule is restricted or knock_restricted:
199	if rules.restricted_join_rule && matches!(join_rule, JoinRule::Restricted)
200		|| rules.knock_restricted_join_rule && matches!(join_rule, JoinRule::KnockRestricted)
201	{
202		// Since v8, if membership state is join or invite, allow.
203		if matches!(current_membership, MembershipState::Join | MembershipState::Invite) {
204			return Ok(());
205		}
206
207		// Since v8, if the join_authorised_via_users_server key in content is not a
208		// user with sufficient permission to invite other users, reject.
209		//
210		// Otherwise, allow.
211		let Some(authorized_via_user) = room_member_event.join_authorised_via_users_server()?
212		else {
213			// The field is absent, we cannot authorize.
214			return Err!(
215				"cannot join restricted room without `join_authorised_via_users_server` field \
216				 if not invited"
217			);
218		};
219
220		// The member needs to be in the room to have any kind of permission.
221		let authorized_via_user_membership = fetch_state
222			.user_membership(&authorized_via_user)
223			.await?;
224
225		if authorized_via_user_membership != MembershipState::Join {
226			return Err!("`join_authorised_via_users_server` is not joined");
227		}
228
229		let room_power_levels_event = fetch_state.room_power_levels_event().await?;
230
231		let authorized_via_user_power_level = room_power_levels_event
232			.user_power_level(&authorized_via_user, creators, rules)
233			.map_err(auth_input_error)?;
234
235		let invite_power_level = room_power_levels_event
236			.get_as_int_or_default(RoomPowerLevelsIntField::Invite, rules)
237			.map_err(auth_input_error)?;
238
239		if authorized_via_user_power_level < invite_power_level {
240			return Err!("`join_authorised_via_users_server` does not have enough power");
241		}
242
243		return Ok(());
244	}
245
246	// Since v1, if the join_rule is public, allow. Otherwise, reject.
247	if join_rule != JoinRule::Public {
248		return Err!("cannot join a room that is not `public`");
249	}
250
251	Ok(())
252}
253
254/// Check whether the given event passes the `m.room.member` authorization rules
255/// with a membership of `invite`.
256#[tracing::instrument(level = "trace", skip_all)]
257async fn check_room_member_invite<Fetch, Pdu>(
258	room_member_event: &RoomMemberEvent<Pdu>,
259	target_user: &UserId,
260	rules: &AuthorizationRules,
261	room_create_event: &RoomCreateEvent<Fetch::Pdu>,
262	fetch_state: Fetch,
263) -> Result
264where
265	Fetch: FetchState,
266	Pdu: Event,
267{
268	let third_party_invite = room_member_event.third_party_invite()?;
269
270	// Since v1, if content has a third_party_invite property:
271	if let Some(third_party_invite) = third_party_invite {
272		return check_third_party_invite(
273			room_member_event,
274			&third_party_invite,
275			target_user,
276			fetch_state,
277		)
278		.await;
279	}
280
281	let sender_user = room_member_event.sender();
282	let (sender_membership, current_target_user_membership, room_power_levels_event) = join3(
283		fetch_state.user_membership(sender_user),
284		fetch_state.user_membership(target_user),
285		fetch_state.room_power_levels_event(),
286	)
287	.await;
288
289	// Since v1, if the sender’s current membership state is not join, reject.
290	let sender_membership = sender_membership?;
291	if sender_membership != MembershipState::Join {
292		return Err!("cannot invite user if sender is not joined");
293	}
294
295	// Since v1, if target user’s current membership state is join or ban, reject.
296	let current_target_user_membership = current_target_user_membership?;
297	if matches!(current_target_user_membership, MembershipState::Join | MembershipState::Ban) {
298		return Err!("cannot invite user that is joined or banned");
299	}
300
301	let room_power_levels_event = room_power_levels_event?;
302
303	let creators = room_create_event
304		.creators(rules)
305		.map_err(auth_input_error)?;
306
307	let sender_power_level = room_power_levels_event
308		.user_power_level(room_member_event.sender(), creators, rules)
309		.map_err(auth_input_error)?;
310
311	let invite_power_level = room_power_levels_event
312		.get_as_int_or_default(RoomPowerLevelsIntField::Invite, rules)
313		.map_err(auth_input_error)?;
314
315	// Since v1, if the sender’s power level is greater than or equal to the invite
316	// level, allow. Otherwise, reject.
317	if sender_power_level < invite_power_level {
318		return Err!("sender does not have enough power to invite");
319	}
320
321	Ok(())
322}
323
324/// Check whether the `third_party_invite` from the `m.room.member` event passes
325/// the authorization rules.
326#[tracing::instrument(level = "trace", skip_all)]
327async fn check_third_party_invite<Fetch, Pdu>(
328	room_member_event: &RoomMemberEvent<Pdu>,
329	third_party_invite: &ThirdPartyInvite,
330	target_user: &UserId,
331	fetch_state: Fetch,
332) -> Result
333where
334	Fetch: FetchState,
335	Pdu: Event,
336{
337	let current_target_user_membership = fetch_state.user_membership(target_user).await?;
338
339	// Since v1, if target user is banned, reject.
340	if current_target_user_membership == MembershipState::Ban {
341		return Err!("cannot invite user that is banned");
342	}
343
344	// Since v1, if content.third_party_invite does not have a signed property,
345	// reject. Since v1, if signed does not have mxid and token properties, reject.
346	let third_party_invite_token = third_party_invite.token()?;
347	let third_party_invite_mxid = third_party_invite.mxid()?;
348
349	// Since v1, if mxid does not match state_key, reject.
350	if target_user != third_party_invite_mxid {
351		return Err!("third-party invite mxid does not match target user");
352	}
353
354	// Since v1, if there is no m.room.third_party_invite event in the current room
355	// state with state_key matching token, reject.
356	let Some(room_third_party_invite_event) = fetch_state
357		.room_third_party_invite_event(third_party_invite_token)
358		.await?
359	else {
360		return Err!("no `m.room.third_party_invite` in room state matches the token");
361	};
362
363	// Since v1, if sender does not match sender of the m.room.third_party_invite,
364	// reject.
365	if room_member_event.sender() != room_third_party_invite_event.sender() {
366		return Err!(
367			"sender of `m.room.third_party_invite` does not match sender of `m.room.member`"
368		);
369	}
370
371	let signatures = third_party_invite.signatures()?;
372	let public_keys = room_third_party_invite_event
373		.public_keys()
374		.map_err(auth_input_error)?;
375
376	let signed_canonical_json = third_party_invite.signed_canonical_json()?;
377
378	// Since v1, if any signature in signed matches any public key in the
379	// m.room.third_party_invite event, allow.
380	for entity_signatures_value in signatures.values() {
381		let Some(entity_signatures) = entity_signatures_value.as_object() else {
382			return Err!(Request(InvalidParam(
383				"unexpected format of `signatures` field in `third_party_invite.signed` of \
384				 `m.room.member` event: expected a map of string to object, got \
385				 {entity_signatures_value:?}"
386			)));
387		};
388
389		// We will ignore any error from now on, we just want to find a signature that
390		// can be verified from a public key.
391
392		for (key_id, signature_value) in entity_signatures {
393			let Ok(parsed_key_id) = <&SigningKeyId<AnyKeyName>>::try_from(key_id.as_str()) else {
394				continue;
395			};
396
397			let Some(signature_str) = signature_value.as_str() else {
398				continue;
399			};
400
401			let Ok(signature) = Base64::<Standard>::parse(signature_str) else {
402				continue;
403			};
404
405			let algorithm = parsed_key_id.algorithm();
406			for encoded_public_key in &public_keys {
407				let Ok(public_key) = encoded_public_key.decode() else {
408					continue;
409				};
410
411				if verify_canonical_json_bytes(
412					&algorithm,
413					&public_key,
414					signature.as_bytes(),
415					signed_canonical_json.as_bytes(),
416				)
417				.is_ok()
418				{
419					return Ok(());
420				}
421			}
422		}
423	}
424
425	// Otherwise, reject.
426	Err!(
427		"no signature on third-party invite matches a public key in `m.room.third_party_invite` \
428		 event"
429	)
430}
431
432/// Check whether the given event passes the `m.room.member` authorization rules
433/// with a membership of `leave`.
434#[tracing::instrument(level = "trace", skip_all)]
435async fn check_room_member_leave<Fetch, Pdu>(
436	room_member_event: &RoomMemberEvent<Pdu>,
437	target_user: &UserId,
438	rules: &AuthorizationRules,
439	room_create_event: &RoomCreateEvent<Fetch::Pdu>,
440	fetch_state: Fetch,
441) -> Result
442where
443	Fetch: FetchState,
444	Pdu: Event,
445{
446	let (sender_membership, room_power_levels_event, current_target_user_membership) = join3(
447		fetch_state.user_membership(room_member_event.sender()),
448		fetch_state.room_power_levels_event(),
449		fetch_state.user_membership(target_user),
450	)
451	.await;
452
453	let sender_membership = sender_membership?;
454
455	// v1-v6, if the sender matches state_key, allow if and only if that user’s
456	// current membership state is invite or join.
457	// Since v7, if the sender matches state_key, allow if and only if that user’s
458	// current membership state is invite, join, or knock.
459	if room_member_event.sender() == target_user {
460		let membership_is_invite_or_join =
461			matches!(sender_membership, MembershipState::Join | MembershipState::Invite);
462		let membership_is_knock = rules.knocking && sender_membership == MembershipState::Knock;
463
464		return if membership_is_invite_or_join || membership_is_knock {
465			Ok(())
466		} else {
467			Err!("cannot leave if not joined, invited or knocked")
468		};
469	}
470
471	// Since v1, if the sender’s current membership state is not join, reject.
472	if sender_membership != MembershipState::Join {
473		return Err!("cannot kick if sender is not joined");
474	}
475
476	let creators = room_create_event
477		.creators(rules)
478		.map_err(auth_input_error)?;
479
480	let current_target_user_membership = current_target_user_membership?;
481	let room_power_levels_event = room_power_levels_event?;
482
483	let sender_power_level = room_power_levels_event
484		.user_power_level(room_member_event.sender(), creators.clone(), rules)
485		.map_err(auth_input_error)?;
486
487	let ban_power_level = room_power_levels_event
488		.get_as_int_or_default(RoomPowerLevelsIntField::Ban, rules)
489		.map_err(auth_input_error)?;
490
491	// Since v1, if the target user’s current membership state is ban, and the
492	// sender’s power level is less than the ban level, reject.
493	if current_target_user_membership == MembershipState::Ban
494		&& sender_power_level < ban_power_level
495	{
496		return Err!("sender does not have enough power to unban");
497	}
498
499	let kick_power_level = room_power_levels_event
500		.get_as_int_or_default(RoomPowerLevelsIntField::Kick, rules)
501		.map_err(auth_input_error)?;
502
503	let target_user_power_level = room_power_levels_event
504		.user_power_level(target_user, creators, rules)
505		.map_err(auth_input_error)?;
506
507	// Since v1, if the sender’s power level is greater than or equal to the kick
508	// level, and the target user’s power level is less than the sender’s power
509	// level, allow.
510	//
511	// Otherwise, reject.
512	if sender_power_level >= kick_power_level && target_user_power_level < sender_power_level {
513		Ok(())
514	} else {
515		Err!("sender does not have enough power to kick target user")
516	}
517}
518
519/// Check whether the given event passes the `m.room.member` authorization rules
520/// with a membership of `ban`.
521#[tracing::instrument(level = "trace", skip_all)]
522async fn check_room_member_ban<Fetch, Pdu>(
523	room_member_event: &RoomMemberEvent<Pdu>,
524	target_user: &UserId,
525	rules: &AuthorizationRules,
526	room_create_event: &RoomCreateEvent<Fetch::Pdu>,
527	fetch_state: Fetch,
528) -> Result
529where
530	Fetch: FetchState,
531	Pdu: Event,
532{
533	let (sender_membership, room_power_levels_event) = join(
534		fetch_state.user_membership(room_member_event.sender()),
535		fetch_state.room_power_levels_event(),
536	)
537	.await;
538
539	// Since v1, if the sender’s current membership state is not join, reject.
540	let sender_membership = sender_membership?;
541	if sender_membership != MembershipState::Join {
542		return Err!("cannot ban if sender is not joined");
543	}
544
545	let room_power_levels_event = room_power_levels_event?;
546
547	let creators = room_create_event
548		.creators(rules)
549		.map_err(auth_input_error)?;
550
551	let sender_power_level = room_power_levels_event
552		.user_power_level(room_member_event.sender(), creators.clone(), rules)
553		.map_err(auth_input_error)?;
554
555	let ban_power_level = room_power_levels_event
556		.get_as_int_or_default(RoomPowerLevelsIntField::Ban, rules)
557		.map_err(auth_input_error)?;
558
559	let target_user_power_level = room_power_levels_event
560		.user_power_level(target_user, creators, rules)
561		.map_err(auth_input_error)?;
562
563	// If the sender’s power level is greater than or equal to the ban level, and
564	// the target user’s power level is less than the sender’s power level, allow.
565	//
566	// Otherwise, reject.
567	if sender_power_level >= ban_power_level && target_user_power_level < sender_power_level {
568		Ok(())
569	} else {
570		Err!("sender does not have enough power to ban target user")
571	}
572}
573
574/// Check whether the given event passes the `m.room.member` authorization rules
575/// with a membership of `knock`.
576#[tracing::instrument(level = "trace", skip_all)]
577async fn check_room_member_knock<Fetch, Pdu>(
578	room_member_event: &RoomMemberEvent<Pdu>,
579	target_user: &UserId,
580	rules: &AuthorizationRules,
581	fetch_state: Fetch,
582) -> Result
583where
584	Fetch: FetchState,
585	Pdu: Event,
586{
587	let sender = room_member_event.sender();
588	let (join_rule, sender_membership) =
589		join(fetch_state.join_rule(), fetch_state.user_membership(sender)).await;
590
591	// v7-v9, if the join_rule is anything other than knock, reject.
592	// Since v10, if the join_rule is anything other than knock or knock_restricted,
593	// reject.
594	let join_rule = join_rule?;
595	let supports_knock = matches!(join_rule, JoinRule::Knock)
596		|| (rules.knock_restricted_join_rule && matches!(join_rule, JoinRule::KnockRestricted));
597
598	if !supports_knock {
599		return Err!(
600			"join rule is not set to knock or knock_restricted, knocking is not allowed"
601		);
602	}
603
604	// Since v7, if sender does not match state_key, reject.
605	if room_member_event.sender() != target_user {
606		return Err!("cannot make another user knock, sender does not match target user");
607	}
608
609	// Since v7, if the sender’s current membership is not ban, invite, or join,
610	// allow. Otherwise, reject.
611	let sender_membership = sender_membership?;
612	if !matches!(
613		sender_membership,
614		MembershipState::Ban | MembershipState::Invite | MembershipState::Join
615	) {
616		Ok(())
617	} else {
618		Err!("cannot knock if user is banned, invited or joined")
619	}
620}