Skip to main content

tuwunel_service/rooms/state_res/event_auth/
room_member.rs

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