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#[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 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 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 match target_membership {
78 | 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 | 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 | 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 | 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 | MembershipState::Knock if rules.knocking =>
124 check_room_member_knock(room_member_event, target_user, rules, fetch_state).await,
125
126 | _ => Err!("unknown membership"),
128 }
129}
130
131#[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 if prev_event_is_only_room_create_event && *target_user == *creator {
169 return Ok(());
170 }
171
172 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 let current_membership = current_membership?;
182 if current_membership == MembershipState::Ban {
183 return Err!("banned user cannot join room");
184 }
185
186 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 if rules.restricted_join_rule && matches!(join_rule, JoinRule::Restricted)
200 || rules.knock_restricted_join_rule && matches!(join_rule, JoinRule::KnockRestricted)
201 {
202 if matches!(current_membership, MembershipState::Join | MembershipState::Invite) {
204 return Ok(());
205 }
206
207 let Some(authorized_via_user) = room_member_event.join_authorised_via_users_server()?
212 else {
213 return Err!(
215 "cannot join restricted room without `join_authorised_via_users_server` field \
216 if not invited"
217 );
218 };
219
220 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 if join_rule != JoinRule::Public {
248 return Err!("cannot join a room that is not `public`");
249 }
250
251 Ok(())
252}
253
254#[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 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 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 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 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#[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 if current_target_user_membership == MembershipState::Ban {
341 return Err!("cannot invite user that is banned");
342 }
343
344 let third_party_invite_token = third_party_invite.token()?;
347 let third_party_invite_mxid = third_party_invite.mxid()?;
348
349 if target_user != third_party_invite_mxid {
351 return Err!("third-party invite mxid does not match target user");
352 }
353
354 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 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 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 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 Err!(
427 "no signature on third-party invite matches a public key in `m.room.third_party_invite` \
428 event"
429 )
430}
431
432#[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 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 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 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 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#[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 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 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#[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 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 if room_member_event.sender() != target_user {
606 return Err!("cannot make another user knock, sender does not match target user");
607 }
608
609 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}