ruma_state_res/
event_auth.rs

1use std::{
2    borrow::Borrow,
3    collections::{BTreeMap, BTreeSet},
4};
5
6use js_int::Int;
7use ruma_common::UserId;
8use ruma_events::room::member::MembershipState;
9use serde_json::value::RawValue as RawJsonValue;
10use tracing::{debug, info, instrument, warn};
11
12mod room_member;
13#[cfg(test)]
14mod tests;
15
16use self::room_member::check_room_member;
17use crate::{
18    events::{
19        member::{RoomMemberEventContent, RoomMemberEventOptionExt},
20        power_levels::{RoomPowerLevelsEventOptionExt, RoomPowerLevelsIntField},
21        JoinRule, RoomCreateEvent, RoomJoinRulesEvent, RoomMemberEvent, RoomPowerLevelsEvent,
22        RoomThirdPartyInviteEvent,
23    },
24    room_version::RoomVersion,
25    Event, StateEventType, TimelineEventType,
26};
27
28// TODO: We need methods for all checks performed on receipt of a PDU, plus the following that are
29// not listed:
30//
31// - check that the event respects the size limits,
32// - check that all the auth events are in the same room as `incoming_event`.
33//
34// References:
35// - https://spec.matrix.org/latest/server-server-api/#checks-performed-on-receipt-of-a-pdu
36// - https://spec.matrix.org/latest/client-server-api/#size-limits
37// - https://github.com/element-hq/synapse/blob/9c5d08fff8d66a7cc0e2ecfeeb783f933a778c2f/synapse/event_auth.py
38// - https://github.com/matrix-org/matrix-spec/issues/365
39
40/// Get the list of [relevant auth event types] required to authorize the event of the given type.
41///
42/// Returns a list of `(event_type, state_key)` tuples.
43///
44/// # Errors
45///
46/// Returns an `Err(_)` if a field could not be deserialized because `content` does not respect the
47/// expected format for the `event_type`.
48///
49/// [relevant auth events]: https://spec.matrix.org/latest/server-server-api/#auth-events-selection
50pub fn auth_types_for_event(
51    event_type: &TimelineEventType,
52    sender: &UserId,
53    state_key: Option<&str>,
54    content: &RawJsonValue,
55    room_version: &RoomVersion,
56) -> Result<Vec<(StateEventType, String)>, String> {
57    // The `auth_events` for the `m.room.create` event in a room is empty.
58    if event_type == &TimelineEventType::RoomCreate {
59        return Ok(vec![]);
60    }
61
62    // For other events, it should be the following subset of the room state:
63    //
64    // - The `m.room.create` event.
65    // - The current `m.room.power_levels` event, if any.
66    // - The sender’s current `m.room.member` event, if any.
67    let mut auth_types = vec![
68        (StateEventType::RoomPowerLevels, "".to_owned()),
69        (StateEventType::RoomMember, sender.to_string()),
70        (StateEventType::RoomCreate, "".to_owned()),
71    ];
72
73    // If type is `m.room.member`:
74    if event_type == &TimelineEventType::RoomMember {
75        // The target’s current `m.room.member` event, if any.
76        let Some(state_key) = state_key else {
77            return Err("missing `state_key` field for `m.room.member` event".to_owned());
78        };
79        let key = (StateEventType::RoomMember, state_key.to_owned());
80        if !auth_types.contains(&key) {
81            auth_types.push(key);
82        }
83
84        let content = RoomMemberEventContent::new(content);
85        let membership = content.membership()?;
86
87        // If `membership` is `join`, `invite` or `knock`, the current `m.room.join_rules` event, if
88        // any.
89        if matches!(
90            membership,
91            MembershipState::Join | MembershipState::Invite | MembershipState::Knock
92        ) {
93            let key = (StateEventType::RoomJoinRules, "".to_owned());
94            if !auth_types.contains(&key) {
95                auth_types.push(key);
96            }
97        }
98
99        // If `membership` is `invite` and `content` contains a `third_party_invite` property, the
100        // current `m.room.third_party_invite` event with `state_key` matching
101        // `content.third_party_invite.signed.token`, if any.
102        if membership == MembershipState::Invite {
103            let third_party_invite = content.third_party_invite()?;
104
105            if let Some(third_party_invite) = third_party_invite {
106                let token = third_party_invite.token()?.to_owned();
107                let key = (StateEventType::RoomThirdPartyInvite, token);
108                if !auth_types.contains(&key) {
109                    auth_types.push(key);
110                }
111            }
112        }
113
114        // If `content.join_authorised_via_users_server` is present, and the room version supports
115        // restricted rooms, then the `m.room.member` event with `state_key` matching
116        // `content.join_authorised_via_users_server`.
117        //
118        // Note: And the membership is join (https://github.com/matrix-org/matrix-spec/pull/2100)
119        if membership == MembershipState::Join && room_version.restricted_join_rules {
120            let join_authorised_via_users_server = content.join_authorised_via_users_server()?;
121            if let Some(user_id) = join_authorised_via_users_server {
122                let key = (StateEventType::RoomMember, user_id.to_string());
123                if !auth_types.contains(&key) {
124                    auth_types.push(key);
125                }
126            }
127        }
128    }
129
130    Ok(auth_types)
131}
132
133/// Check whether the incoming event passes the [authorization rules] for the given room version.
134///
135/// The `fetch_state` closure should gather state from a state snapshot. We need to know if the
136/// event passes auth against some state not a recursive collection of auth_events fields.
137///
138/// This assumes that `ruma_signatures::verify_event()` was called previously, as some authorization
139/// rules depend on the signatures being valid on the event.
140///
141/// # Errors
142///
143/// If the check fails, this returns an `Err(_)` with a description of the check that failed.
144///
145/// [authorization rules]: https://spec.matrix.org/latest/server-server-api/#authorization-rules
146#[instrument(skip_all, fields(event_id = incoming_event.event_id().borrow().as_str()))]
147pub fn auth_check<E: Event>(
148    room_version: &RoomVersion,
149    incoming_event: impl Event,
150    fetch_state: impl Fn(&StateEventType, &str) -> Option<E>,
151) -> Result<(), String> {
152    debug!("starting auth check");
153
154    // Since v1, if type is m.room.create:
155    if *incoming_event.event_type() == TimelineEventType::RoomCreate {
156        let room_create_event = RoomCreateEvent::new(incoming_event);
157
158        return check_room_create(room_create_event, room_version);
159    }
160
161    /*
162    TODO: In the past this code caused problems federating with synapse, maybe this has been
163    resolved already. Needs testing.
164
165    // Since v1, considering auth_events:
166    //
167    // - Since v1, if there are duplicate entries for a given type and state_key pair, reject.
168    //
169    // - Since v1, if there are entries whose type and state_key don’t match those specified
170    //   by the auth events selection algorithm described in the server specification, reject.
171    let expected_auth = auth_types_for_event(
172        incoming_event.kind,
173        sender,
174        incoming_event.state_key,
175        incoming_event.content().clone(),
176    );
177
178    dbg!(&expected_auth);
179
180    for ev_key in auth_events.keys() {
181        // (b)
182        if !expected_auth.contains(ev_key) {
183            warn!("auth_events contained invalid auth event");
184            return Ok(false);
185        }
186    }
187    */
188
189    // TODO:
190    //
191    // Since v1, if there are entries which were themselves rejected under the checks performed on
192    // receipt of a PDU, reject.
193
194    let room_create_event = fetch_state.room_create_event()?;
195
196    // Since v1, if there is no m.room.create event among the entries, reject.
197    if !incoming_event.auth_events().any(|id| id.borrow() == room_create_event.event_id().borrow())
198    {
199        return Err("no `m.room.create` event in auth events".to_owned());
200    }
201
202    // Since v1, if the create event content has the field m.federate set to false and the sender
203    // domain of the event does not match the sender domain of the create event, reject.
204    let federate = room_create_event.federate()?;
205    if !federate
206        && room_create_event.sender().server_name() != incoming_event.sender().server_name()
207    {
208        return Err("\
209            room is not federated and event's sender domain \
210            does not match `m.room.create` event's sender domain"
211            .to_owned());
212    }
213
214    let sender = incoming_event.sender();
215
216    // v1-v5, if type is m.room.aliases:
217    if room_version.special_case_aliases_auth
218        && *incoming_event.event_type() == TimelineEventType::RoomAliases
219    {
220        debug!("starting m.room.aliases check");
221        // v1-v5, if event has no state_key, reject.
222        //
223        // v1-v5, if sender's domain doesn't match state_key, reject.
224        if incoming_event.state_key() != Some(sender.server_name().as_str()) {
225            return Err("\
226                server name of the `state_key` of `m.room.aliases` event \
227                does not match the server name of the sender"
228                .to_owned());
229        }
230
231        // Otherwise, allow.
232        info!("`m.room.aliases` event was allowed");
233        return Ok(());
234    }
235
236    // Since v1, if type is m.room.member:
237    if *incoming_event.event_type() == TimelineEventType::RoomMember {
238        let room_member_event = RoomMemberEvent::new(incoming_event);
239        return check_room_member(room_member_event, room_version, room_create_event, fetch_state);
240    }
241
242    // Since v1, if the sender's current membership state is not join, reject.
243    let sender_membership = fetch_state.user_membership(sender)?;
244
245    if sender_membership != MembershipState::Join {
246        return Err("sender's membership is not `join`".to_owned());
247    }
248
249    let creator = room_create_event.creator(room_version)?;
250    let current_room_power_levels_event = fetch_state.room_power_levels_event();
251
252    let sender_power_level =
253        current_room_power_levels_event.user_power_level(sender, &creator, room_version)?;
254
255    // Since v1, if type is m.room.third_party_invite:
256    if *incoming_event.event_type() == TimelineEventType::RoomThirdPartyInvite {
257        // Since v1, allow if and only if sender's current power level is greater than
258        // or equal to the invite level.
259        let invite_power_level = current_room_power_levels_event
260            .get_as_int_or_default(RoomPowerLevelsIntField::Invite, room_version)?;
261
262        if sender_power_level < invite_power_level {
263            return Err("sender does not have enough power to send invites in this room".to_owned());
264        }
265
266        info!("`m.room.third_party_invite` event was allowed");
267        return Ok(());
268    }
269
270    // Since v1, if the event type's required power level is greater than the sender's power level,
271    // reject.
272    let event_type_power_level = current_room_power_levels_event.event_power_level(
273        incoming_event.event_type(),
274        incoming_event.state_key(),
275        room_version,
276    )?;
277    if sender_power_level < event_type_power_level {
278        return Err(format!(
279            "sender does not have enough power to send event of type `{}`",
280            incoming_event.event_type()
281        ));
282    }
283
284    // Since v1, if the event has a state_key that starts with an @ and does not match the sender,
285    // reject.
286    if incoming_event.state_key().is_some_and(|k| k.starts_with('@'))
287        && incoming_event.state_key() != Some(incoming_event.sender().as_str())
288    {
289        return Err(
290            "sender cannot send event with `state_key` matching another user's ID".to_owned()
291        );
292    }
293
294    // If type is m.room.power_levels
295    if *incoming_event.event_type() == TimelineEventType::RoomPowerLevels {
296        let room_power_levels_event = RoomPowerLevelsEvent::new(incoming_event);
297        return check_room_power_levels(
298            room_power_levels_event,
299            current_room_power_levels_event,
300            room_version,
301            sender_power_level,
302        );
303    }
304
305    // v1-v2, if type is m.room.redaction:
306    if room_version.extra_redaction_checks
307        && *incoming_event.event_type() == TimelineEventType::RoomRedaction
308    {
309        return check_room_redaction(
310            incoming_event,
311            current_room_power_levels_event,
312            room_version,
313            sender_power_level,
314        );
315    }
316
317    // Otherwise, allow.
318    info!("allowing event passed all checks");
319    Ok(())
320}
321
322/// Check whether the given event passes the `m.room.create` authorization rules.
323fn check_room_create(
324    room_create_event: RoomCreateEvent<impl Event>,
325    room_version: &RoomVersion,
326) -> Result<(), String> {
327    debug!("start `m.room.create` check");
328
329    // Since v1, if it has any previous events, reject.
330    if room_create_event.prev_events().next().is_some() {
331        return Err("`m.room.create` event cannot have previous events".into());
332    }
333
334    // Since v1, if the domain of the room_id does not match the domain of the sender, reject.
335    let Some(room_id_server_name) = room_create_event.room_id().server_name() else {
336        return Err(
337            "invalid `room_id` field in `m.room.create` event: could not parse server name".into(),
338        );
339    };
340
341    if room_id_server_name != room_create_event.sender().server_name() {
342        return Err("invalid `room_id` field in `m.room.create` event: server name does not match sender's server name".into());
343    }
344
345    // Since v1, if `content.room_version` is present and is not a recognized version, reject.
346    //
347    // This check is assumed to be done before calling auth_check because we have a RoomVersion,
348    // which means that we recognized the version.
349
350    // v1-v10, if content has no creator field, reject.
351    if !room_version.use_room_create_sender && !room_create_event.has_creator()? {
352        return Err("missing `creator` field in `m.room.create` event".into());
353    }
354
355    // Otherwise, allow.
356    info!("`m.room.create` event was allowed");
357    Ok(())
358}
359
360/// Check whether the given event passes the `m.room.power_levels` authorization rules.
361fn check_room_power_levels(
362    room_power_levels_event: RoomPowerLevelsEvent<impl Event>,
363    current_room_power_levels_event: Option<RoomPowerLevelsEvent<impl Event>>,
364    room_version: &RoomVersion,
365    sender_power_level: Int,
366) -> Result<(), String> {
367    debug!("starting m.room.power_levels check");
368
369    // Since v10, if any of the properties users_default, events_default, state_default, ban,
370    // redact, kick, or invite in content are present and not an integer, reject.
371    let new_int_fields = room_power_levels_event.int_fields_map(room_version)?;
372
373    // Since v10, if either of the properties events or notifications in content are present and not
374    // a dictionary with values that are integers, reject.
375    let new_events = room_power_levels_event.events(room_version)?;
376    let new_notifications = room_power_levels_event.notifications(room_version)?;
377
378    // v1-v9, If the users property in content is not an object with keys that are valid user IDs
379    // with values that are integers (or a string that is an integer), reject.
380    // Since v10, if the users property in content is not an object with keys that are valid user
381    // IDs with values that are integers, reject.
382    let new_users = room_power_levels_event.users(room_version)?;
383
384    debug!("validation of power event finished");
385
386    // Since v1, if there is no previous m.room.power_levels event in the room, allow.
387    let Some(current_room_power_levels_event) = current_room_power_levels_event else {
388        info!("initial m.room.power_levels event allowed");
389        return Ok(());
390    };
391
392    // Since v1, for the properties users_default, events_default, state_default, ban, redact, kick,
393    // invite check if they were added, changed or removed. For each found alteration:
394    for field in RoomPowerLevelsIntField::ALL {
395        let current_power_level =
396            current_room_power_levels_event.get_as_int(*field, room_version)?;
397        let new_power_level = new_int_fields.get(field).copied();
398
399        if current_power_level == new_power_level {
400            continue;
401        }
402
403        // Since v1, if the current value is higher than the sender’s current power level,
404        // reject.
405        let current_power_level_too_big =
406            current_power_level.unwrap_or_else(|| field.default_value()) > sender_power_level;
407        // Since v1, if the new value is higher than the sender’s current power level, reject.
408        let new_power_level_too_big =
409            new_power_level.unwrap_or_else(|| field.default_value()) > sender_power_level;
410
411        if current_power_level_too_big || new_power_level_too_big {
412            return Err(format!(
413                "sender does not have enough power to change the power level of `{field}`"
414            ));
415        }
416    }
417
418    // Since v1, for each entry being added to, or changed in, the events property:
419    // - Since v1, if the new value is higher than the sender's current power level, reject.
420    let current_events = current_room_power_levels_event.events(room_version)?;
421    check_power_level_maps(
422        current_events.as_ref(),
423        new_events.as_ref(),
424        &sender_power_level,
425        |_, current_power_level| {
426            // Since v1, for each entry being changed in, or removed from, the events property:
427            // - Since v1, if the current value is higher than the sender's current power level,
428            //   reject.
429            current_power_level > sender_power_level
430        },
431        |ev_type| {
432            format!(
433            "sender does not have enough power to change the `{ev_type}` event type power level"
434        )
435        },
436    )?;
437
438    // Since v6, for each entry being added to, or changed in, the notifications property:
439    // - Since v6, if the new value is higher than the sender's current power level, reject.
440    if room_version.limit_notifications_power_levels {
441        let current_notifications = current_room_power_levels_event.notifications(room_version)?;
442        check_power_level_maps(
443            current_notifications.as_ref(),
444            new_notifications.as_ref(),
445            &sender_power_level,
446            |_, current_power_level| {
447                // Since v6, for each entry being changed in, or removed from, the notifications
448                // property:
449                // - Since v6, if the current value is higher than the sender's current power level,
450                //   reject.
451                current_power_level > sender_power_level
452            },
453            |key| {
454                format!(
455                "sender does not have enough power to change the `{key}` notification power level"
456            )
457            },
458        )?;
459    }
460
461    // Since v1, for each entry being added to, or changed in, the users property:
462    // - Since v1, if the new value is greater than the sender’s current power level, reject.
463    let current_users = current_room_power_levels_event.users(room_version)?;
464    check_power_level_maps(
465        current_users,
466        new_users,
467        &sender_power_level,
468        |user_id, current_power_level| {
469            // Since v1, for each entry being changed in, or removed from, the users property, other
470            // than the sender’s own entry:
471            // - Since v1, if the current value is greater than or equal to the sender’s current
472            //   power level, reject.
473            user_id != room_power_levels_event.sender() && current_power_level >= sender_power_level
474        },
475        |user_id| format!("sender does not have enough power to change `{user_id}`'s  power level"),
476    )?;
477
478    // Otherwise, allow.
479    info!("m.room.power_levels event allowed");
480    Ok(())
481}
482
483/// Check the power levels changes between the current and the new maps.
484///
485/// # Arguments
486///
487/// * `current`: the map with the current power levels.
488/// * `new`: the map with the new power levels.
489/// * `sender_power_level`: the power level of the sender of the new map.
490/// * `reject_current_power_level_change_fn`: the function to check if a power level change or
491///   removal must be rejected given its current value.
492///
493///   The arguments to the method are the key of the power level and the current value of the power
494///   level. It must return `true` if the change or removal is rejected.
495///
496///   Note that another check is done after this one to check if the change is allowed given the new
497///   value of the power level.
498/// * `error_fn`: the function to generate an error when the change for the given key is not
499///   allowed.
500fn check_power_level_maps<K: Ord>(
501    current: Option<&BTreeMap<K, Int>>,
502    new: Option<&BTreeMap<K, Int>>,
503    sender_power_level: &Int,
504    reject_current_power_level_change_fn: impl FnOnce(&K, Int) -> bool + Copy,
505    error_fn: impl FnOnce(&K) -> String,
506) -> Result<(), String> {
507    let keys_to_check = current
508        .iter()
509        .flat_map(|m| m.keys())
510        .chain(new.iter().flat_map(|m| m.keys()))
511        .collect::<BTreeSet<_>>();
512
513    for key in keys_to_check {
514        let current_power_level = current.as_ref().and_then(|m| m.get(key));
515        let new_power_level = new.as_ref().and_then(|m| m.get(key));
516
517        if current_power_level == new_power_level {
518            continue;
519        }
520
521        // For each entry being changed in, or removed from, the property.
522        let current_power_level_change_rejected = current_power_level
523            .is_some_and(|power_level| reject_current_power_level_change_fn(key, *power_level));
524
525        // For each entry being added to, or changed in, the property:
526        // - If the new value is higher than the sender's current power level, reject.
527        let new_power_level_too_big = new_power_level > Some(sender_power_level);
528
529        if current_power_level_change_rejected || new_power_level_too_big {
530            return Err(error_fn(key));
531        }
532    }
533
534    Ok(())
535}
536
537/// Check whether the given event passes the `m.room.redaction` authorization rules.
538fn check_room_redaction(
539    room_redaction_event: impl Event,
540    current_room_power_levels_event: Option<RoomPowerLevelsEvent<impl Event>>,
541    room_version: &RoomVersion,
542    sender_level: Int,
543) -> Result<(), String> {
544    let redact_level = current_room_power_levels_event
545        .get_as_int_or_default(RoomPowerLevelsIntField::Redact, room_version)?;
546
547    // v1-v2, if the sender’s power level is greater than or equal to the redact level, allow.
548    if sender_level >= redact_level {
549        info!("`m.room.redaction` event allowed via power levels");
550        return Ok(());
551    }
552
553    // v1-v2, if the domain of the event_id of the event being redacted is the same as the
554    // domain of the event_id of the m.room.redaction, allow.
555    if room_redaction_event.event_id().borrow().server_name()
556        == room_redaction_event.redacts().as_ref().and_then(|&id| id.borrow().server_name())
557    {
558        info!("`m.room.redaction` event allowed via room version 1 rules");
559        return Ok(());
560    }
561
562    // Otherwise, reject.
563    Err("`m.room.redaction` event did not pass any of the allow rules".to_owned())
564}
565
566trait FetchStateExt<E: Event> {
567    fn room_create_event(&self) -> Result<RoomCreateEvent<E>, String>;
568
569    fn user_membership(&self, user_id: &UserId) -> Result<MembershipState, String>;
570
571    fn room_power_levels_event(&self) -> Option<RoomPowerLevelsEvent<E>>;
572
573    fn join_rule(&self) -> Result<JoinRule, String>;
574
575    fn room_third_party_invite_event(&self, token: &str) -> Option<RoomThirdPartyInviteEvent<E>>;
576}
577
578impl<E, F> FetchStateExt<E> for F
579where
580    F: Fn(&StateEventType, &str) -> Option<E>,
581    E: Event,
582{
583    fn room_create_event(&self) -> Result<RoomCreateEvent<E>, String> {
584        self(&StateEventType::RoomCreate, "")
585            .map(RoomCreateEvent::new)
586            .ok_or_else(|| "no `m.room.create` event in current state".to_owned())
587    }
588
589    fn user_membership(&self, user_id: &UserId) -> Result<MembershipState, String> {
590        self(&StateEventType::RoomMember, user_id.as_str()).map(RoomMemberEvent::new).membership()
591    }
592
593    fn room_power_levels_event(&self) -> Option<RoomPowerLevelsEvent<E>> {
594        self(&StateEventType::RoomPowerLevels, "").map(RoomPowerLevelsEvent::new)
595    }
596
597    fn join_rule(&self) -> Result<JoinRule, String> {
598        self(&StateEventType::RoomJoinRules, "")
599            .map(RoomJoinRulesEvent::new)
600            .ok_or_else(|| "no `m.room.join_rules` event in current state".to_owned())?
601            .join_rule()
602    }
603
604    fn room_third_party_invite_event(&self, token: &str) -> Option<RoomThirdPartyInviteEvent<E>> {
605        self(&StateEventType::RoomThirdPartyInvite, token).map(RoomThirdPartyInviteEvent::new)
606    }
607}