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#[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 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 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 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, 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 if prev_event_is_only_room_create_event && *target_user == *creator {
165 return Ok(());
166 }
167
168 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 let current_membership = current_membership?;
178 if current_membership == MembershipState::Ban {
179 return Err!("banned user cannot join room");
180 }
181
182 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 if rules.restricted_join_rule && matches!(join_rule, JoinRule::Restricted)
196 || rules.knock_restricted_join_rule && matches!(join_rule, JoinRule::KnockRestricted)
197 {
198 if matches!(current_membership, MembershipState::Join | MembershipState::Invite) {
200 return Ok(());
201 }
202
203 let Some(authorized_via_user) = room_member_event.join_authorised_via_users_server()?
208 else {
209 return Err!(
211 "cannot join restricted room without `join_authorised_via_users_server` field \
212 if not invited"
213 );
214 };
215
216 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 if join_rule != JoinRule::Public {
242 return Err!("cannot join a room that is not `public`");
243 }
244
245 Ok(())
246}
247
248#[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 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 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 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 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#[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 if current_target_user_membership == MembershipState::Ban {
330 return Err!("cannot invite user that is banned");
331 }
332
333 let third_party_invite_token = third_party_invite.token()?;
336 let third_party_invite_mxid = third_party_invite.mxid()?;
337
338 if target_user != third_party_invite_mxid {
340 return Err!("third-party invite mxid does not match target user");
341 }
342
343 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 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 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 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 Err!(
413 "no signature on third-party invite matches a public key in `m.room.third_party_invite` \
414 event"
415 )
416}
417
418#[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 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 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 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 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#[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 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 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#[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 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 if room_member_event.sender() != target_user {
586 return Err!("cannot make another user knock, sender does not match target user");
587 }
588
589 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}