diff --git a/src/core/matrix/state_res/event_auth/auth_events.rs b/src/core/matrix/state_res/event_auth/auth_events.rs index 322542dd..5a4397b3 100644 --- a/src/core/matrix/state_res/event_auth/auth_events.rs +++ b/src/core/matrix/state_res/event_auth/auth_events.rs @@ -1,31 +1,107 @@ //! Auth checks relevant to any event's `auth_events`. //! //! See: https://spec.matrix.org/v1.16/rooms/v12/#authorization-rules -use std::{ - collections::{HashMap, HashSet}, - future::Future, -}; +use std::collections::{HashMap, HashSet}; -use ruma::{EventId, OwnedEventId, RoomId, events::StateEventType}; +use ruma::{ + EventId, OwnedEventId, RoomId, UserId, + events::{ + StateEventType, TimelineEventType, + room::member::{MembershipState, RoomMemberEventContent, ThirdPartyInvite}, + }, +}; use crate::{Event, EventTypeExt, Pdu, RoomVersion, matrix::StateKey, state_res::Error, warn}; -// Checks for duplicate auth events in the `auth_events` field of an event. -// Note: the caller should already have all of the auth events fetched. -// -// If there are multiple auth events of the same type and state key, this -// returns an error. Otherwise, it returns a map of (type, state_key) to the -// corresponding auth event. -pub async fn check_duplicate_auth_events( +/// For the given event `kind` what are the relevant auth events that are needed +/// to authenticate this `content`. +/// +/// # Errors +/// +/// This function will return an error if the supplied `content` is not a JSON +/// object. +pub fn auth_types_for_event( + room_version: &RoomVersion, + event_type: &TimelineEventType, + state_key: Option<&StateKey>, + sender: &UserId, + member_content: Option, +) -> serde_json::Result> { + if event_type == &TimelineEventType::RoomCreate { + // Create events never have auth events + return Ok(vec![]); + } + let mut auth_types = if room_version.room_ids_as_hashes { + vec![ + StateEventType::RoomMember.with_state_key(sender.as_str()), + StateEventType::RoomPowerLevels.with_state_key(""), + ] + } else { + // For room versions that do not use room IDs as hashes, include the + // RoomCreate event as an auth event. + vec![ + StateEventType::RoomMember.with_state_key(sender.as_str()), + StateEventType::RoomPowerLevels.with_state_key(""), + StateEventType::RoomCreate.with_state_key(""), + ] + }; + + if event_type == &TimelineEventType::RoomMember { + let member_content = + member_content.expect("member_content must be provided for RoomMember events"); + + // Include the target's membership (if available) + auth_types.push(( + StateEventType::RoomMember, + state_key + .expect("state_key must be provided for RoomMember events") + .to_owned(), + )); + + if matches!( + member_content.membership, + MembershipState::Join | MembershipState::Invite | MembershipState::Knock + ) { + // Include the join rules + auth_types.push(StateEventType::RoomJoinRules.with_state_key("")); + } + + if matches!(member_content.membership, MembershipState::Invite) { + // If this is an invite, include the third party invite if it exists + if let Some(ThirdPartyInvite { signed, .. }) = member_content.third_party_invite { + auth_types + .push(StateEventType::RoomThirdPartyInvite.with_state_key(signed.token)); + } + } + + if matches!(member_content.membership, MembershipState::Join) + && room_version.restricted_join_rules + { + // If this is a restricted join, include the authorizing user's membership + if let Some(authorizing_user) = member_content.join_authorized_via_users_server { + auth_types + .push(StateEventType::RoomMember.with_state_key(authorizing_user.as_str())); + } + } + } + + Ok(auth_types) +} + +/// Checks for duplicate auth events in the `auth_events` field of an event. +/// Note: the caller should already have all of the auth events fetched. +/// +/// If there are multiple auth events of the same type and state key, this +/// returns an error. Otherwise, it returns a map of (type, state_key) to the +/// corresponding auth event. +pub async fn check_duplicate_auth_events( auth_events: &[OwnedEventId], - fetch_event: impl Fn(&EventId) -> Fut + Send, -) -> Result, Error> + fetch_event: FE, +) -> Result, Error> where - Fut: Future, Error>> + Send, - E: Event + Send + Sync, - for<'a> &'a E: Event + Send, + FE: AsyncFn(&EventId) -> Result, Error>, { - let mut seen: HashMap<(StateEventType, StateKey), E> = HashMap::new(); + let mut seen: HashMap<(StateEventType, StateKey), Pdu> = HashMap::new(); // Considering all of the event's auth events: for auth_event_id in auth_events { @@ -79,23 +155,15 @@ pub fn check_unnecessary_auth_events( // Checks that all provided auth events were not rejected previously. // // TODO: this is currently a no-op and always returns Ok(()). -pub fn check_all_auth_events_accepted( - _auth_events: &HashMap<(StateEventType, StateKey), E>, -) -> Result<(), Error> -where - E: Event + Send + Sync, - for<'a> &'a E: Event + Send, -{ +pub fn check_all_auth_events_accepted( + _auth_events: &HashMap<(StateEventType, StateKey), Pdu>, +) -> Result<(), Error> { Ok(()) } // Checks that all auth events are from the same room as the event being // validated. -pub fn check_auth_same_room(auth_events: &Vec, room_id: &RoomId) -> bool -where - E: Event + Send + Sync, - for<'a> &'a E: Event + Send, -{ +pub fn check_auth_same_room(auth_events: &Vec, room_id: &RoomId) -> bool { for auth_event in auth_events { if let Some(auth_room_id) = &auth_event.room_id() { if auth_room_id.as_str() != room_id.as_str() { @@ -115,17 +183,15 @@ where true } -// Performs all auth event checks for the given event. -pub async fn check_auth_events( +/// Performs all auth event checks for the given event. +pub async fn check_auth_events( event: &Pdu, room_id: &RoomId, room_version: &RoomVersion, - fetch_event: impl Fn(&EventId) -> Fut + Send, -) -> Result, Error> + fetch_event: &FE, +) -> Result, Error> where - Fut: Future, Error>> + Send, - E: Event + Send + Sync, - for<'a> &'a E: Event + Send, + FE: AsyncFn(&EventId) -> Result, Error>, { // If there are duplicate entries for a given type and state_key pair, reject. let auth_events_map = check_duplicate_auth_events(&event.auth_events, fetch_event).await?; @@ -135,12 +201,19 @@ where // If there are entries whose type and state_key don’t match those specified by // the auth events selection algorithm described in the server specification, // reject. - let expected_auth_events = crate::state_res::auth_types_for_event( - event.kind(), - event.sender(), - event.state_key(), - event.content(), + let member_event_content = match event.kind() { + | TimelineEventType::RoomMember => + Some(event.get_content::().map_err(|e| { + Error::InvalidPdu(format!("Failed to parse m.room.member content: {}", e)) + })?), + | _ => None, + }; + let expected_auth_events = auth_types_for_event( room_version, + event.kind(), + event.state_key.as_ref(), + event.sender(), + member_event_content, )?; if let Err(e) = check_unnecessary_auth_events(&auth_events_set, &expected_auth_events) { return Err(e); @@ -154,7 +227,7 @@ where // If any event in auth_events has a room_id which does not match that of the // event being authorised, reject. - let auth_event_refs: Vec = auth_events_map.values().cloned().collect(); + let auth_event_refs: Vec = auth_events_map.values().cloned().collect(); if !check_auth_same_room(&auth_event_refs, room_id) { return Err(Error::InvalidPdu( "One or more auth events are from a different room".to_owned(), diff --git a/src/core/matrix/state_res/event_auth/context.rs b/src/core/matrix/state_res/event_auth/context.rs index 97f42bfd..2279b45a 100644 --- a/src/core/matrix/state_res/event_auth/context.rs +++ b/src/core/matrix/state_res/event_auth/context.rs @@ -61,6 +61,7 @@ where Ok(vec![create_event.sender().to_owned()]) } else { // Have to check the event content + #[allow(deprecated)] if let Some(creator) = content.creator { Ok(vec![creator]) } else { diff --git a/src/core/matrix/state_res/event_auth/iterative_auth_checks.rs b/src/core/matrix/state_res/event_auth/iterative_auth_checks.rs index 057bbca4..ae3fefe0 100644 --- a/src/core/matrix/state_res/event_auth/iterative_auth_checks.rs +++ b/src/core/matrix/state_res/event_auth/iterative_auth_checks.rs @@ -1,38 +1,28 @@ -use std::{borrow::Borrow, collections::BTreeSet}; - -use futures::{ - Future, - future::{OptionFuture, join, join3}, -}; use ruma::{ - EventId, Int, OwnedUserId, RoomVersionId, UserId, - events::room::{ - create::RoomCreateEventContent, - join_rules::{JoinRule, RoomJoinRulesEventContent}, - member::{MembershipState, ThirdPartyInvite}, - power_levels::RoomPowerLevelsEventContent, - third_party_invite::RoomThirdPartyInviteEventContent, + EventId, OwnedUserId, RoomVersionId, + events::{ + StateEventType, TimelineEventType, + room::{create::RoomCreateEventContent, member::MembershipState}, }, int, - serde::{Base64, Raw}, + serde::Raw, }; -use serde::{ - Deserialize, - de::{Error as _, IgnoredAny}, -}; -use serde_json::{from_str as from_json_str, value::RawValue as RawJsonValue}; +use serde::{Deserialize, de::IgnoredAny}; +use serde_json::from_str as from_json_str; -use super::super::{ - Error, Event, Result, StateEventType, StateKey, TimelineEventType, - power_levels::{ - deserialize_power_levels, deserialize_power_levels_content_fields, - deserialize_power_levels_content_invite, deserialize_power_levels_content_redact, - }, - room_version::RoomVersion, -}; use crate::{ - Pdu, debug, error, - state_res::event_auth::{auth_events::check_auth_events, create_event::check_room_create}, + Event, EventTypeExt, Pdu, RoomVersion, debug, error, + matrix::StateKey, + state_res::{ + error::Error, + event_auth::{ + auth_events::check_auth_events, + context::{UserPower, calculate_creators, get_rank}, + create_event::check_room_create, + member_event::check_member_event, + power_levels::check_power_levels, + }, + }, trace, warn, }; @@ -57,89 +47,6 @@ struct RoomCreateContentFields { federate: bool, } -/// For the given event `kind` what are the relevant auth events that are needed -/// to authenticate this `content`. -/// -/// # Errors -/// -/// This function will return an error if the supplied `content` is not a JSON -/// object. -pub fn auth_types_for_event( - kind: &TimelineEventType, - sender: &UserId, - state_key: Option<&str>, - content: &RawJsonValue, - room_version: &RoomVersion, -) -> serde_json::Result> { - if kind == &TimelineEventType::RoomCreate { - return Ok(vec![]); - } - - let mut auth_types = if room_version.room_ids_as_hashes { - vec![ - (StateEventType::RoomPowerLevels, StateKey::new()), - (StateEventType::RoomMember, sender.as_str().into()), - ] - } else { - vec![ - (StateEventType::RoomPowerLevels, StateKey::new()), - (StateEventType::RoomMember, sender.as_str().into()), - (StateEventType::RoomCreate, StateKey::new()), - ] - }; - - if kind == &TimelineEventType::RoomMember { - #[derive(Deserialize)] - struct RoomMemberContentFields { - membership: Option>, - third_party_invite: Option>, - join_authorised_via_users_server: Option>, - } - - if let Some(state_key) = state_key { - let content: RoomMemberContentFields = from_json_str(content.get())?; - - if let Some(Ok(membership)) = content.membership.map(|m| m.deserialize()) { - if [MembershipState::Join, MembershipState::Invite, MembershipState::Knock] - .contains(&membership) - { - let key = (StateEventType::RoomJoinRules, StateKey::new()); - if !auth_types.contains(&key) { - auth_types.push(key); - } - - if let Some(Ok(u)) = content - .join_authorised_via_users_server - .map(|m| m.deserialize()) - { - let key = (StateEventType::RoomMember, u.as_str().into()); - if !auth_types.contains(&key) { - auth_types.push(key); - } - } - } - - let key = (StateEventType::RoomMember, state_key.into()); - if !auth_types.contains(&key) { - auth_types.push(key); - } - - if membership == MembershipState::Invite { - if let Some(Ok(t_id)) = content.third_party_invite.map(|t| t.deserialize()) { - let key = - (StateEventType::RoomThirdPartyInvite, t_id.signed.token.into()); - if !auth_types.contains(&key) { - auth_types.push(key); - } - } - } - } - } - } - - Ok(auth_types) -} - /// Authenticate the incoming `event`. /// /// The steps of authentication are: @@ -158,22 +65,21 @@ pub fn auth_types_for_event( ) )] #[allow(clippy::suspicious_operation_groupings)] -pub async fn iterative_auth_check( +pub async fn auth_check( room_version: &RoomVersion, incoming_event: &Pdu, - current_third_party_invite: Option<&Pdu>, - fetch_event: impl Fn(&EventId) -> Fut + Send, - create_event: &Pdu, + fetch_event: &FE, + fetch_state: &FS, + create_event: Option<&Pdu>, ) -> Result where - Fut: Future, Error>> + Send, - E: Event + Send + Sync, - for<'a> &'a E: Event + Send, + FE: AsyncFn(&EventId) -> Result, Error>, + FS: AsyncFn((StateEventType, StateKey)) -> Result, Error>, { debug!("auth_check beginning"); let sender = incoming_event.sender(); - // If type is m.room.create: + // Since v1, If type is m.room.create: if *incoming_event.event_type() == TimelineEventType::RoomCreate { debug!("start m.room.create check"); if let Err(e) = check_room_create(incoming_event) { @@ -184,13 +90,15 @@ where debug!("m.room.create event was allowed"); return Ok(true); } + let Some(create_event) = create_event else { + error!("no create event provided for auth check"); + return Err(Error::InvalidPdu("missing create event".to_owned())); + }; // TODO: we need to know if events have previously been rejected or soft failed // For now, we'll just assume the create_event is valid. let create_content = from_json_str::(create_event.content().get()) .expect("provided create event must be valid"); - let room_version = RoomVersion::new(&create_content.room_version) - .expect("valid create event must have a valid room version"); // Since v12, If the event’s room_id is not an event ID for an accepted (not // rejected) m.room.create event, with the sigil ! instead of $, reject. @@ -213,16 +121,20 @@ where let room_id = incoming_event.room_id().expect("event must have a room ID"); - // Considering the event's auth_events - let auth_map = check_auth_events(incoming_event, room_id, &room_version, fetch_event).await; - if let Err(e) = auth_map { - warn!("event's auth events are invalid: {}", e); - return Ok(false); - } + let auth_map = + match check_auth_events(incoming_event, room_id, &room_version, fetch_event).await { + | Ok(map) => map, + | Err(e) => { + warn!("event's auth events are invalid: {}", e); + return Ok(false); + }, + }; - // If the content of the m.room.create event in the room state has the property - // m.federate set to false, and the sender domain of the event does not match - // the sender domain of the create event, reject. + // Considering the event's auth_events + + // Since v1, If the content of the m.room.create event in the room state has the + // property m.federate set to false, and the sender domain of the event does + // not match the sender domain of the create event, reject. if !create_content.federate { if create_event.sender().server_name() != incoming_event.sender().server_name() { warn!( @@ -234,7 +146,7 @@ where } } - // Only in room versions 5 and below + // From v1 to v5, If type is m.room.aliases if room_version.special_case_aliases_auth && *incoming_event.event_type() == TimelineEventType::RoomAliases { @@ -247,103 +159,34 @@ where // Otherwise, allow return Ok(true); } + // If event has no state_key, reject. warn!("m.room.alias event has no state key"); return Ok(false); } - // If type is m.room.member + // From v1, If type is m.room.member if *incoming_event.event_type() == TimelineEventType::RoomMember { - debug!("starting m.room.member check"); - let state_key = match incoming_event.state_key() { + if let Err(e) = + check_member_event(&room_version, incoming_event, fetch_event, fetch_state).await + { + warn!("m.room.member event has been rejected: {}", e); + return Ok(false); + } + } + + // From v1, If the sender's current membership state is not join, reject + let sender_member_event = + match auth_map.get(&StateEventType::RoomMember.with_state_key(sender.as_str())) { + | Some(ev) => ev, | None => { - warn!("no state key in member event"); + warn!( + %sender, + "sender is not joined - no membership event found for sender in auth events" + ); return Ok(false); }, - | Some(s) => s, }; - let content: RoomMemberContentFields = from_json_str(incoming_event.content().get())?; - if content - .membership - .as_ref() - .and_then(|m| m.deserialize().ok()) - .is_none() - { - warn!("no valid membership field found for m.room.member event content"); - return Ok(false); - } - - let target_user = - <&UserId>::try_from(state_key).map_err(|e| Error::InvalidPdu(format!("{e}")))?; - - let user_for_join_auth = content - .join_authorised_via_users_server - .as_ref() - .and_then(|u| u.deserialize().ok()); - - let user_for_join_auth_event: OptionFuture<_> = user_for_join_auth - .as_ref() - .map(|auth_user| fetch_state(&StateEventType::RoomMember, auth_user.as_str())) - .into(); - - let target_user_member_event = - fetch_state(&StateEventType::RoomMember, target_user.as_str()); - - let join_rules_event = fetch_state(&StateEventType::RoomJoinRules, ""); - - let (join_rules_event, target_user_member_event, user_for_join_auth_event) = - join3(join_rules_event, target_user_member_event, user_for_join_auth_event).await; - - let user_for_join_auth_membership = user_for_join_auth_event - .and_then(|mem| from_json_str::(mem?.content().get()).ok()) - .map_or(MembershipState::Leave, |mem| mem.membership); - - if !valid_membership_change( - room_version, - target_user, - target_user_member_event.as_ref(), - sender, - sender_member_event.as_ref(), - incoming_event, - current_third_party_invite, - power_levels_event.as_ref(), - join_rules_event.as_ref(), - user_for_join_auth.as_deref(), - &user_for_join_auth_membership, - &room_create_event, - )? { - return Ok(false); - } - - debug!("m.room.member event was allowed"); - return Ok(true); - } - - // If the sender's current membership state is not join, reject - #[allow(clippy::manual_let_else)] - let sender_member_event = match sender_member_event { - | Some(mem) => mem, - | None => { - warn!("sender has no membership event"); - return Ok(false); - }, - }; - - if sender_member_event - .room_id() - .expect("we have a room ID for non create events") - != expected_room_id - { - warn!( - "room_id of incoming event ({}) does not match that of the m.room.create event ({})", - sender_member_event - .room_id() - .expect("event must have a room ID"), - expected_room_id - ); - return Ok(false); - } - let sender_membership_event_content: RoomMemberContentFields = from_json_str(sender_member_event.content().get())?; let Some(membership_state) = sender_membership_event_content.membership else { @@ -355,7 +198,7 @@ where }; let membership_state = membership_state.deserialize()?; - if !matches!(membership_state, MembershipState::Join) { + if membership_state != MembershipState::Join { warn!( %sender, ?membership_state, @@ -364,65 +207,25 @@ where return Ok(false); } - // If type is m.room.third_party_invite - let mut sender_power_level = match &power_levels_event { - | Some(pl) => { - let content = - deserialize_power_levels_content_fields(pl.content().get(), room_version)?; - match content.get_user_power(sender) { - | Some(level) => *level, - | _ => content.users_default, - } - }, - | _ => { - // If no power level event found the creator gets 100 everyone else gets 0 - let is_creator = if room_version.use_room_create_sender { - room_create_event.sender() == sender - } else { - #[allow(deprecated)] - from_json_str::(room_create_event.content().get()) - .is_ok_and(|create| create.creator.unwrap() == *sender) - }; - - if is_creator { int!(100) } else { int!(0) } - }, - }; - if room_version.explicitly_privilege_room_creators { - // If the user sent the create event, or is listed in additional_creators, just - // give them Int::MAX - if sender == room_create_event.sender() - || room_create_content - .additional_creators - .as_ref() - .is_some_and(|creators| { - creators - .iter() - .any(|c| c.deserialize().is_ok_and(|c| c == *sender)) - }) { - trace!("privileging room creator or additional creator"); - // This user is the room creator or an additional creator, give them max power - // level - sender_power_level = Int::MAX; - } - } + // From v1, If type is m.room.third_party_invite + let (rank, sender_pl, pl_evt) = get_rank(&room_version, fetch_state, sender).await?; // Allow if and only if sender's current power level is greater than // or equal to the invite level if *incoming_event.event_type() == TimelineEventType::RoomThirdPartyInvite { - let invite_level = match &power_levels_event { - | Some(power_levels) => - deserialize_power_levels_content_invite( - power_levels.content().get(), - room_version, - )? - .invite, + if rank == UserPower::Creator { + trace!("sender is room creator, allowing m.room.third_party_invite"); + return Ok(true); + } + let invite_level = match &pl_evt { + | Some(power_levels) => power_levels.invite, | None => int!(0), }; - if sender_power_level < invite_level { + if sender_pl < invite_level { warn!( %sender, - has=%sender_power_level, + has=%sender_pl, required=%invite_level, "sender cannot send invites in this room" ); @@ -433,1017 +236,75 @@ where return Ok(true); } - // If the event type's required power level is greater than the sender's power - // level, reject If the event has a state_key that starts with an @ and does - // not match the sender, reject. - if !can_send_event(incoming_event, power_levels_event.as_ref(), sender_power_level) { + // Since v1, if the event type’s required power level is greater than the + // sender’s power level, reject. + let required_level = match &pl_evt { + | Some(power_levels) => power_levels + .events + .get(incoming_event.kind()) + .unwrap_or_else(|| { + if incoming_event.state_key.is_some() { + &power_levels.state_default + } else { + &power_levels.events_default + } + }), + | None => &int!(0), + }; + if rank != UserPower::Creator && sender_pl < *required_level { warn!( %sender, - event_type=?incoming_event.kind(), - "sender cannot send event" + has=%sender_pl, + required=%required_level, + "sender does not have enough power level to send this event" ); return Ok(false); } - // If type is m.room.power_levels - if *incoming_event.event_type() == TimelineEventType::RoomPowerLevels { - debug!("starting m.room.power_levels check"); - let mut creators = BTreeSet::new(); - if room_version.explicitly_privilege_room_creators { - creators.insert(create_event.sender().to_owned()); - for creator in room_create_content.additional_creators.iter().flatten() { - creators.insert(creator.deserialize()?); - } - } - match check_power_levels( - room_version, - incoming_event, - power_levels_event.as_ref(), - sender_power_level, - &creators, - ) { - | Some(required_pwr_lvl) => - if !required_pwr_lvl { - warn!("m.room.power_levels was not allowed"); - return Ok(false); - }, - | _ => { - warn!("m.room.power_levels was not allowed"); - return Ok(false); - }, - } - debug!("m.room.power_levels event allowed"); - } - - // Room version 3: Redaction events are always accepted (provided the event is - // allowed by `events` and `events_default` in the power levels). However, - // servers should not apply or send redaction's to clients until both the - // redaction event and original event have been seen, and are valid. Servers - // should only apply redaction's to events where the sender's domains match, or - // the sender of the redaction has the appropriate permissions per the - // power levels. - - if room_version.extra_redaction_checks - && *incoming_event.event_type() == TimelineEventType::RoomRedaction - { - let redact_level = match power_levels_event { - | Some(pl) => - deserialize_power_levels_content_redact(pl.content().get(), room_version)?.redact, - | None => int!(50), - }; - - if !check_redaction(room_version, incoming_event, sender_power_level, redact_level)? { + // Since v1, If the event has a state_key that starts with an @ and does not + // match the sender, reject. + if let Some(state_key) = incoming_event.state_key() { + if state_key.starts_with('@') && state_key != sender.as_str() { warn!( %sender, - %sender_power_level, - %redact_level, - "redaction event was not allowed" + %state_key, + "event's state key starts with @ and does not match sender" ); return Ok(false); } } - debug!("allowing event passed all checks"); - Ok(true) -} - -fn is_creator( - v: &RoomVersion, - c: &BTreeSet, - ce: &EV, - user_id: &UserId, - have_pls: bool, -) -> bool -where - EV: Event + Send + Sync, -{ - if v.explicitly_privilege_room_creators { - c.contains(user_id) - } else if v.use_room_create_sender && !have_pls { - ce.sender() == user_id - } else if !have_pls { - #[allow(deprecated)] - let creator = from_json_str::(ce.content().get()) - .unwrap() - .creator - .ok_or_else(|| serde_json::Error::missing_field("creator")) - .unwrap(); - - creator == user_id - } else { - false - } -} - -// TODO deserializing the member, power, join_rules event contents is done in -// conduit just before this is called. Could they be passed in? -/// Does the user who sent this member event have required power levels to do -/// so. -/// -/// * `user` - Information about the membership event and user making the -/// request. -/// * `auth_events` - The set of auth events that relate to a membership event. -/// -/// This is generated by calling `auth_types_for_event` with the membership -/// event and the current State. -#[allow(clippy::too_many_arguments)] -#[allow(clippy::cognitive_complexity)] -fn valid_membership_change( - room_version: &RoomVersion, - target_user: &UserId, - target_user_membership_event: Option<&E>, - sender: &UserId, - sender_membership_event: Option<&E>, - current_event: &E, - current_third_party_invite: Option<&E>, - power_levels_event: Option<&E>, - join_rules_event: Option<&E>, - user_for_join_auth: Option<&UserId>, - user_for_join_auth_membership: &MembershipState, - create_room: &E, -) -> Result -where - E: Event + Send + Sync, - for<'a> &'a E: Event + Send, -{ - #[derive(Deserialize)] - struct GetThirdPartyInvite { - third_party_invite: Option>, - } - let create_content = from_json_str::(create_room.content().get())?; - let content = current_event.content(); - - let target_membership = from_json_str::(content.get())?.membership; - let third_party_invite = - from_json_str::(content.get())?.third_party_invite; - - let sender_membership = match &sender_membership_event { - | Some(pdu) => from_json_str::(pdu.content().get())?.membership, - | None => MembershipState::Leave, - }; - let sender_is_joined = sender_membership == MembershipState::Join; - - let target_user_current_membership = match &target_user_membership_event { - | Some(pdu) => from_json_str::(pdu.content().get())?.membership, - | None => MembershipState::Leave, - }; - - let power_levels: RoomPowerLevelsEventContent = match &power_levels_event { - | Some(ev) => from_json_str(ev.content().get())?, - | None => RoomPowerLevelsEventContent::default(), - }; - - let mut sender_power = power_levels - .users - .get(sender) - .or_else(|| sender_is_joined.then_some(&power_levels.users_default)); - - let mut target_power = power_levels.users.get(target_user).or_else(|| { - (target_membership == MembershipState::Join).then_some(&power_levels.users_default) - }); - - let mut creators = BTreeSet::new(); - creators.insert(create_room.sender().to_owned()); - if room_version.explicitly_privilege_room_creators { - // Explicitly privilege room creators - // If the sender sent the create event, or in additional_creators, give them - // Int::MAX. Same case for target. - if let Some(additional_creators) = &create_content.additional_creators { - for c in additional_creators { - if let Ok(c) = c.deserialize() { - creators.insert(c); - } - } - } - if creators.contains(sender) { - sender_power = Some(&Int::MAX); - } - if creators.contains(target_user) { - target_power = Some(&Int::MAX); - } - } - trace!(?creators, "creators for room"); - - let join_rules = if let Some(jr) = &join_rules_event { - from_json_str::(jr.content().get())?.join_rule - } else { - JoinRule::Invite - }; - - let power_levels_event_id = power_levels_event.as_ref().map(Event::event_id); - let sender_membership_event_id = sender_membership_event.as_ref().map(Event::event_id); - let target_user_membership_event_id = - target_user_membership_event.as_ref().map(Event::event_id); - - let user_for_join_auth_is_valid = if let Some(user_for_join_auth) = user_for_join_auth { - // Is the authorised user allowed to invite users into this room - let (auth_user_pl, invite_level) = if let Some(pl) = &power_levels_event { - // TODO Refactor all powerlevel parsing - let invite = - deserialize_power_levels_content_invite(pl.content().get(), room_version)?.invite; - - let content = - deserialize_power_levels_content_fields(pl.content().get(), room_version)?; - let user_pl = match content.get_user_power(user_for_join_auth) { - | Some(level) => *level, - | _ => content.users_default, - }; - - (user_pl, invite) - } else { - (int!(0), int!(0)) - }; - let user_joined = user_for_join_auth_membership == &MembershipState::Join; - let okay_power = is_creator( - room_version, - &creators, - create_room, - user_for_join_auth, - power_levels_event.as_ref().is_some(), - ) || auth_user_pl >= invite_level; - trace!( - %auth_user_pl, - %auth_user_pl, - %invite_level, - %user_joined, - %okay_power, - passing=%(user_joined && okay_power), - "user for join auth is valid check details" - ); - user_joined && okay_power - } else { - // No auth user was given - trace!("No auth user given for join auth"); - false - }; - let sender_creator = is_creator( - room_version, - &creators, - create_room, - sender, - power_levels_event.as_ref().is_some(), - ); - let target_creator = is_creator( - room_version, - &creators, - create_room, - target_user, - power_levels_event.as_ref().is_some(), - ); - - Ok(match target_membership { - | MembershipState::Join => { - trace!("starting target_membership=join check"); - // 1. If the only previous event is an m.room.create and the state_key is the - // creator, - // allow - let mut prev_events = current_event.prev_events(); - - let prev_event_is_create_event = prev_events - .next() - .is_some_and(|event_id| event_id.borrow() == create_room.event_id().borrow()); - let no_more_prev_events = prev_events.next().is_none(); - - if prev_event_is_create_event && no_more_prev_events { - trace!( - %sender, - target_user = %target_user, - ?sender_creator, - ?target_creator, - "checking if sender is a room creator for initial membership event" - ); - let is_creator = sender_creator && target_creator; - - if is_creator { - debug!("sender is room creator, allowing join"); - return Ok(true); - } - trace!("sender is not room creator, proceeding with normal auth checks"); - } - let membership_allows_join = matches!( - target_user_current_membership, - MembershipState::Join | MembershipState::Invite - ); - if sender != target_user { - // If the sender does not match state_key, reject. - warn!( - %sender, - target_user = %target_user, - "sender cannot join on behalf of another user" - ); - false - } else if target_user_current_membership == MembershipState::Ban { - // If the sender is banned, reject. - warn!( - %sender, - membership_event_id = ?target_user_membership_event_id, - "sender cannot join as they are banned from the room" - ); - false - } else { - match join_rules { - | JoinRule::Invite => - if !membership_allows_join { - warn!( - %sender, - membership_event_id = ?target_user_membership_event_id, - membership = ?target_user_current_membership, - "sender cannot join as they are not invited to the invite-only room" - ); - false - } else { - trace!(sender=%sender, "sender is invited to room, allowing join"); - true - }, - | JoinRule::Knock if !room_version.allow_knocking => { - warn!("Join rule is knock but room version does not allow knocking"); - false - }, - | JoinRule::Knock => - if !membership_allows_join { - warn!( - %sender, - membership_event_id = ?target_user_membership_event_id, - membership=?target_user_current_membership, - "sender cannot join a knock room without being invited or already joined" - ); - false - } else { - trace!(sender=%sender, "sender is invited or already joined to room, allowing join"); - true - }, - | JoinRule::KnockRestricted(_) if !room_version.knock_restricted_join_rule => - { - warn!( - "Join rule is knock_restricted but room version does not support it" - ); - false - }, - | JoinRule::KnockRestricted(_) => { - if membership_allows_join || user_for_join_auth_is_valid { - trace!( - %sender, - %membership_allows_join, - %user_for_join_auth_is_valid, - "sender is invited, already joined to, or authorised to join the room, allowing join" - ); - true - } else { - warn!( - %sender, - membership_event_id = ?target_user_membership_event_id, - membership=?target_user_current_membership, - %user_for_join_auth_is_valid, - ?user_for_join_auth, - "sender cannot join as they are not invited nor already joined to the room, nor was a \ - valid authorising user given to permit the join" - ); - false - } - }, - | JoinRule::Restricted(_) => { - if membership_allows_join || user_for_join_auth_is_valid { - trace!( - %sender, - %membership_allows_join, - %user_for_join_auth_is_valid, - "sender is invited, already joined to, or authorised to join the room, allowing join" - ); - true - } else { - warn!( - %sender, - membership_event_id = ?target_user_membership_event_id, - membership=?target_user_current_membership, - %user_for_join_auth_is_valid, - ?user_for_join_auth, - "sender cannot join as they are not invited nor already joined to the room, nor was a \ - valid authorising user given to permit the join" - ); - false - } - }, - | JoinRule::Public => { - trace!(%sender, "join rule is public, allowing join"); - true - }, - | _ => { - warn!( - join_rule=?join_rules, - "Join rule is unknown, or the rule's conditions were not met" - ); - false - }, - } - } - }, - | MembershipState::Invite => { - // If content has third_party_invite key - trace!("starting target_membership=invite check"); - match third_party_invite.and_then(|i| i.deserialize().ok()) { - | Some(tp_id) => - if target_user_current_membership == MembershipState::Ban { - warn!(?target_user_membership_event_id, "Can't invite banned user"); - false - } else { - let allow = verify_third_party_invite( - Some(target_user), - sender, - &tp_id, - current_third_party_invite, - ); - if !allow { - warn!("Third party invite invalid"); - } - allow - }, - | _ => - if !sender_is_joined { - warn!( - %sender, - ?sender_membership_event_id, - ?sender_membership, - "sender cannot produce an invite without being joined to the room", - ); - false - } else if matches!( - target_user_current_membership, - MembershipState::Join | MembershipState::Ban - ) { - warn!( - ?target_user_membership_event_id, - ?target_user_current_membership, - "cannot invite a user who is banned or already joined", - ); - false - } else { - let allow = sender_creator - || sender_power - .filter(|&p| p >= &power_levels.invite) - .is_some(); - if !allow { - warn!( - %sender, - has=?sender_power, - required=?power_levels.invite, - "sender does not have enough power to produce invites", - ); - } - trace!( - %sender, - ?sender_membership_event_id, - ?sender_membership, - ?target_user_membership_event_id, - ?target_user_current_membership, - sender_pl=?sender_power, - required_pl=?power_levels.invite, - "allowing invite" - ); - allow - }, - } - }, - | MembershipState::Leave => { - let can_unban = if target_user_current_membership == MembershipState::Ban { - sender_creator || sender_power.filter(|&p| p >= &power_levels.ban).is_some() - } else { - true - }; - let can_kick = if !matches!( - target_user_current_membership, - MembershipState::Ban | MembershipState::Leave - ) { - if sender_creator { - // sender is a creator - true - } else if sender_power.filter(|&p| p >= &power_levels.kick).is_none() { - // sender lacks kick power level - false - } else if let Some(sp) = sender_power { - if let Some(tp) = target_power { - // sender must have more power than target - sp > tp - } else { - // target has default power level - true - } - } else { - // sender has default power level - false - } - } else { - true - }; - if sender == target_user { - // self-leave - // let allow = target_user_current_membership == MembershipState::Join - // || target_user_current_membership == MembershipState::Invite - // || target_user_current_membership == MembershipState::Knock; - let allow = matches!( - target_user_current_membership, - MembershipState::Join | MembershipState::Invite | MembershipState::Knock - ); - if !allow { - warn!( - %sender, - current_membership_event_id=?target_user_membership_event_id, - current_membership=?target_user_current_membership, - "sender cannot leave as they are not already knocking on, invited to, or joined to the room" - ); - } - trace!(sender=%sender, "allowing leave"); - allow - } else if !sender_is_joined { - warn!( - %sender, - ?sender_membership_event_id, - "sender cannot kick another user as they are not joined to the room", - ); - false - } else if !(can_unban && can_kick) { - // If the target is banned, only a room creator or someone with ban power - // level can unban them - warn!( - %sender, - ?target_user_membership_event_id, - ?power_levels_event_id, - "sender lacks the power level required to unban users", - ); - false - } else if !can_kick { - warn!( - %sender, - %target_user, - ?target_user_membership_event_id, - ?target_user_current_membership, - ?power_levels_event_id, - "sender does not have enough power to kick the target", - ); - false - } else { - trace!( - %sender, - %target_user, - ?target_user_membership_event_id, - ?target_user_current_membership, - sender_pl=?sender_power, - target_pl=?target_power, - required_pl=?power_levels.kick, - "allowing kick/unban", - ); - true - } - }, - | MembershipState::Ban => - if !sender_is_joined { - warn!( - %sender, - ?sender_membership_event_id, - "sender cannot ban another user as they are not joined to the room", - ); - false - } else { - let allow = sender_creator - || (sender_power.filter(|&p| p >= &power_levels.ban).is_some() - && target_power < sender_power); - if !allow { - warn!( - %sender, - %target_user, - ?target_user_membership_event_id, - ?power_levels_event_id, - "sender does not have enough power to ban the target", - ); - } - allow - }, - | MembershipState::Knock if room_version.allow_knocking => { - // 1. If the `join_rule` is anything other than `knock` or `knock_restricted`, - // reject. - if !matches!(join_rules, JoinRule::KnockRestricted(_) | JoinRule::Knock) { - warn!( - "Join rule is not set to knock or knock_restricted, knocking is not allowed" - ); - false - } else if matches!(join_rules, JoinRule::KnockRestricted(_)) - && !room_version.knock_restricted_join_rule - { - // 2. If the `join_rule` is `knock_restricted`, but the room does not support - // `knock_restricted`, reject. - warn!( - "Join rule is set to knock_restricted but room version does not support \ - knock_restricted, knocking is not allowed" - ); - false - } else if sender != target_user { - // 3. If `sender` does not match `state_key`, reject. - warn!( - %sender, - %target_user, - "sender cannot knock on behalf of another user", - ); - false - } else if matches!( - sender_membership, - MembershipState::Ban | MembershipState::Invite | MembershipState::Join - ) { - // 4. If the `sender`'s current membership is not `ban`, `invite`, or `join`, - // allow. - // 5. Otherwise, reject. - warn!( - ?target_user_membership_event_id, - ?sender_membership, - "Knocking with a membership state of ban, invite or join is invalid", - ); - false - } else { - trace!(%sender, "allowing knock"); - true - } - }, - | _ => { + // Since v1, If type is m.room.power_levels + if *incoming_event.event_type() == TimelineEventType::RoomPowerLevels { + let creators = calculate_creators(&room_version, fetch_state).await?; + if let Err(e) = + check_power_levels(&room_version, incoming_event, pl_evt.as_ref(), creators).await + { warn!( %sender, - ?target_membership, - %target_user, - %target_user_current_membership, - "Unknown or invalid membership transition {} -> {}", - target_user_current_membership, - target_membership + "m.room.power_levels event has been rejected: {}", e ); - false - }, - }) -} - -/// Is the user allowed to send a specific event based on the rooms power -/// levels. -/// -/// Does the event have the correct userId as its state_key if it's not the "" -/// state_key. -fn can_send_event(event: &impl Event, ple: Option<&impl Event>, user_level: Int) -> bool { - // TODO(hydra): This function does not care about creators! - let event_type_power_level = get_send_level(event.event_type(), event.state_key(), ple); - - debug!( - required_level = i64::from(event_type_power_level), - user_level = i64::from(user_level), - state_key = ?event.state_key(), - power_level_event_id = ?ple.map(|e| e.event_id().as_str()), - "permissions factors", - ); - - if user_level < event_type_power_level { - return false; - } - - if event.state_key().is_some_and(|k| k.starts_with('@')) - && event.state_key() != Some(event.sender().as_str()) - { - warn!( - %user_level, - required=%event_type_power_level, - state_key=?event.state_key(), - sender=%event.sender(), - "state_key starts with @ but does not match sender", - ); - return false; // permission required to post in this room - } - - true -} - -/// Confirm that the event sender has the required power levels. -fn check_power_levels( - room_version: &RoomVersion, - power_event: &impl Event, - previous_power_event: Option<&impl Event>, - user_level: Int, - creators: &BTreeSet, -) -> Option { - match power_event.state_key() { - | Some("") => {}, - | Some(key) => { - error!(state_key = key, "m.room.power_levels event has non-empty state key"); - return None; - }, - | None => { - error!("check_power_levels requires an m.room.power_levels *state* event argument"); - return None; - }, - } - - // - If any of the keys users_default, events_default, state_default, ban, - // redact, kick, or invite in content are present and not an integer, reject. - // - If either of the keys events or notifications in content are present and - // not a dictionary with values that are integers, reject. - // - If users key in content is not a dictionary with keys that are valid user - // IDs with values that are integers, reject. - let user_content: RoomPowerLevelsEventContent = - deserialize_power_levels(power_event.content().get(), room_version)?; - - // Validation of users is done in Ruma, synapse for loops validating user_ids - // and integers here - debug!("validation of power event finished"); - - #[allow(clippy::manual_let_else)] - let current_state = match previous_power_event { - | Some(current_state) => current_state, - // If there is no previous m.room.power_levels event in the room, allow - | None => return Some(true), - }; - - let current_content: RoomPowerLevelsEventContent = - deserialize_power_levels(current_state.content().get(), room_version)?; - - let mut user_levels_to_check = BTreeSet::new(); - let old_list = ¤t_content.users; - let user_list = &user_content.users; - for user in old_list.keys().chain(user_list.keys()) { - let user: &UserId = user; - user_levels_to_check.insert(user); - } - - trace!(set = ?user_levels_to_check, "user levels to check"); - - let mut event_levels_to_check = BTreeSet::new(); - let old_list = ¤t_content.events; - let new_list = &user_content.events; - for ev_id in old_list.keys().chain(new_list.keys()) { - event_levels_to_check.insert(ev_id); - } - - trace!(set = ?event_levels_to_check, "event levels to check"); - - let old_state = ¤t_content; - let new_state = &user_content; - - // synapse does not have to split up these checks since we can't combine UserIds - // and EventTypes we do 2 loops - - // UserId loop - for user in user_levels_to_check { - let old_level = old_state.users.get(user); - let new_level = new_state.users.get(user); - if new_level.is_some() && creators.contains(user) { - warn!("creators cannot appear in the users list of m.room.power_levels"); - return Some(false); // cannot alter creator power level - } - if old_level.is_some() && new_level.is_some() && old_level == new_level { - continue; - } - - // If the current value is equal to the sender's current power level, reject - if user != power_event.sender() && old_level == Some(&user_level) { - warn!( - ?old_level, - ?new_level, - ?user, - %user_level, - sender=%power_event.sender(), - "cannot alter the power level of a user with the same power level as sender's own" - ); - return Some(false); // cannot remove ops level == to own - } - - // If the current value is higher than the sender's current power level, reject - // If the new value is higher than the sender's current power level, reject - let old_level_too_big = old_level > Some(&user_level); - let new_level_too_big = new_level > Some(&user_level); - if old_level_too_big { - warn!( - ?old_level, - ?new_level, - ?user, - %user_level, - sender=%power_event.sender(), - "cannot alter the power level of a user with a higher power level than sender's own" - ); - return Some(false); // cannot add ops greater than own - } - if new_level_too_big { - warn!( - ?old_level, - ?new_level, - ?user, - %user_level, - sender=%power_event.sender(), - "cannot set the power level of a user to a level higher than sender's own" - ); - return Some(false); // cannot add ops greater than own + return Ok(false); } } - // EventType loop - for ev_type in event_levels_to_check { - let old_level = old_state.events.get(ev_type); - let new_level = new_state.events.get(ev_type); - if old_level.is_some() && new_level.is_some() && old_level == new_level { - continue; - } - - // If the current value is higher than the sender's current power level, reject - // If the new value is higher than the sender's current power level, reject - let old_level_too_big = old_level > Some(&user_level); - let new_level_too_big = new_level > Some(&user_level); - if old_level_too_big { - warn!( - ?old_level, - ?new_level, - ?ev_type, - %user_level, - sender=%power_event.sender(), - "cannot alter the power level of an event with a higher power level than sender's own" - ); - return Some(false); // cannot add ops greater than own - } - if new_level_too_big { - warn!( - ?old_level, - ?new_level, - ?ev_type, - %user_level, - sender=%power_event.sender(), - "cannot set the power level of an event to a level higher than sender's own" - ); - return Some(false); // cannot add ops greater than own - } - } - - // Notifications, currently there is only @room - if room_version.limit_notifications_power_levels { - let old_level = old_state.notifications.room; - let new_level = new_state.notifications.room; - if old_level != new_level { - // If the current value is higher than the sender's current power level, reject - // If the new value is higher than the sender's current power level, reject - let old_level_too_big = old_level > user_level; - let new_level_too_big = new_level > user_level; - if old_level_too_big || new_level_too_big { - warn!( - ?old_level, - ?new_level, - %user_level, - sender=%power_event.sender(), - "cannot alter the power level of notifications greater than sender's own" - ); - return Some(false); // cannot add ops greater than own - } - } - } - - let levels = [ - "users_default", - "events_default", - "state_default", - "ban", - "redact", - "kick", - "invite", - ]; - let old_state = serde_json::to_value(old_state).unwrap(); - let new_state = serde_json::to_value(new_state).unwrap(); - for lvl_name in &levels { - if let Some((old_lvl, new_lvl)) = get_deserialize_levels(&old_state, &new_state, lvl_name) - { - let old_level_too_big = old_lvl > user_level; - let new_level_too_big = new_lvl > user_level; - - if old_level_too_big || new_level_too_big { - warn!( - ?old_lvl, - ?new_lvl, - %user_level, - sender=%power_event.sender(), - action=%lvl_name, - "cannot alter the power level of action greater than sender's own", - ); - return Some(false); - } - } - } - - Some(true) -} - -fn get_deserialize_levels( - old: &serde_json::Value, - new: &serde_json::Value, - name: &str, -) -> Option<(Int, Int)> { - Some(( - serde_json::from_value(old.get(name)?.clone()).ok()?, - serde_json::from_value(new.get(name)?.clone()).ok()?, - )) -} - -/// Does the event redacting come from a user with enough power to redact the -/// given event. -fn check_redaction( - _room_version: &RoomVersion, - redaction_event: &impl Event, - user_level: Int, - redact_level: Int, -) -> Result { - if user_level >= redact_level { - debug!("redaction allowed via power levels"); - return Ok(true); - } - + // From v1 to v2: If type is m.room.redaction: + // If the sender’s power level is greater than or equal to the redact level, + // allow. // If the domain of the event_id of the event being redacted is the same as the - // domain of the event_id of the m.room.redaction, allow - if redaction_event.event_id().server_name() - == redaction_event - .redacts() - .as_ref() - .and_then(|&id| id.server_name()) - { - debug!("redaction event allowed via room version 1 rules"); - return Ok(true); + // domain of the event_id of the m.room.redaction, allow. + // Otherwise, reject. + if room_version.extra_redaction_checks { + // We'll panic here, since while we don't theoretically support the room + // versions that require this, we don't want to incorrectly permit an event + // that should be rejected in this theoretically impossible scenario. + unreachable!( + "continuwuity does not support room versions that require extra redaction checks" + ); } - Ok(false) -} - -/// Helper function to fetch the power level needed to send an event of type -/// `e_type` based on the rooms "m.room.power_level" event. -fn get_send_level( - e_type: &TimelineEventType, - state_key: Option<&str>, - power_lvl: Option<&impl Event>, -) -> Int { - power_lvl - .and_then(|ple| { - from_json_str::(ple.content().get()) - .map(|content| { - content.events.get(e_type).copied().unwrap_or_else(|| { - if state_key.is_some() { - content.state_default - } else { - content.events_default - } - }) - }) - .ok() - }) - .unwrap_or_else(|| if state_key.is_some() { int!(50) } else { int!(0) }) -} - -fn verify_third_party_invite( - target_user: Option<&UserId>, - sender: &UserId, - tp_id: &ThirdPartyInvite, - current_third_party_invite: Option<&impl Event>, -) -> bool { - // 1. Check for user being banned happens before this is called - // checking for mxid and token keys is done by ruma when deserializing - - // The state key must match the invitee - if target_user != Some(&tp_id.signed.mxid) { - return false; - } - - // If there is no m.room.third_party_invite event in the current room state with - // state_key matching token, reject - #[allow(clippy::manual_let_else)] - let current_tpid = match current_third_party_invite { - | Some(id) => id, - | None => return false, - }; - - if current_tpid.state_key() != Some(&tp_id.signed.token) { - return false; - } - - if sender != current_tpid.sender() { - return false; - } - - // If any signature in signed matches any public key in the - // m.room.third_party_invite event, allow - #[allow(clippy::manual_let_else)] - let tpid_ev = - match from_json_str::(current_tpid.content().get()) { - | Ok(ev) => ev, - | Err(_) => return false, - }; - - #[allow(clippy::manual_let_else)] - let decoded_invite_token = match Base64::parse(&tp_id.signed.token) { - | Ok(tok) => tok, - // FIXME: Log a warning? - | Err(_) => return false, - }; - - // A list of public keys in the public_keys field - for key in tpid_ev.public_keys.unwrap_or_default() { - if key.public_key == decoded_invite_token { - return true; - } - } - - // A single public key in the public_key field - tpid_ev.public_key == decoded_invite_token + debug!("allowing event passed all checks"); + Ok(true) } #[cfg(test)] @@ -1463,7 +324,9 @@ mod tests { matrix::{Event, EventTypeExt, Pdu as PduEvent}, state_res::{ RoomVersion, StateMap, - event_auth::valid_membership_change, + event_auth::{ + iterative_auth_checks::valid_membership_change, valid_membership_change, + }, test_utils::{ INITIAL_EVENTS, INITIAL_EVENTS_CREATE_ROOM, alice, charlie, ella, event_id, member_content_ban, member_content_join, room_id, to_pdu_event, diff --git a/src/core/matrix/state_res/event_auth/member_event.rs b/src/core/matrix/state_res/event_auth/member_event.rs index 23b5df44..a421bcfd 100644 --- a/src/core/matrix/state_res/event_auth/member_event.rs +++ b/src/core/matrix/state_res/event_auth/member_event.rs @@ -14,7 +14,6 @@ use ruma::{ serde::Base64, signatures::{PublicKeyMap, PublicKeySet, verify_json}, }; -use serde::Deserializer; use crate::{ Event, EventTypeExt, Pdu, RoomVersion, @@ -200,7 +199,123 @@ where } } -async fn check_invite_event( +/// Checks a third-party invite is valid. +async fn check_third_party_invite( + target_current_membership: PartialMembershipObject, + raw_third_party_invite: &serde_json::Value, + target: &UserId, + event: &Pdu, + fetch_state: impl AsyncFn((StateEventType, StateKey)) -> Result, Error>, +) -> Result<(), Error> { + // 4.1.1: If target user is banned, reject. + if target_current_membership + .membership + .is_some_and(|m| m == "ban") + { + return Err(Error::AuthConditionFailed("invite target is banned".to_owned())); + } + // 4.1.2: If content.third_party_invite does not have a signed property, reject. + let signed = raw_third_party_invite.get("signed").ok_or_else(|| { + Error::AuthConditionFailed( + "invite event third_party_invite missing signed property".to_owned(), + ) + })?; + // 4.2.3: If signed does not have mxid and token properties, reject. + let mxid = signed.get("mxid").and_then(|v| v.as_str()).ok_or_else(|| { + Error::AuthConditionFailed( + "invite event third_party_invite signed missing/invalid mxid property".to_owned(), + ) + })?; + let token = signed + .get("token") + .and_then(|v| v.as_str()) + .ok_or_else(|| { + Error::AuthConditionFailed( + "invite event third_party_invite signed missing token property".to_owned(), + ) + })?; + // 4.2.4: If mxid does not match state_key, reject. + if mxid != target.as_str() { + return Err(Error::AuthConditionFailed( + "invite event third_party_invite signed mxid does not match state_key".to_owned(), + )); + } + // 4.2.5: If there is no m.room.third_party_invite event in the room + // state matching the token, reject. + let Some(third_party_invite_event) = + fetch_state(StateEventType::RoomThirdPartyInvite.with_state_key(token)).await? + else { + return Err(Error::AuthConditionFailed( + "invite event third_party_invite token has no matching m.room.third_party_invite" + .to_owned(), + )); + }; + // 4.2.6: If sender does not match sender of the m.room.third_party_invite, + // reject. + if third_party_invite_event.sender() != event.sender() { + return Err(Error::AuthConditionFailed( + "invite event sender does not match m.room.third_party_invite sender".to_owned(), + )); + } + // 4.2.7: If any signature in signed matches any public key in the + // m.room.third_party_invite event, allow. The public keys are in + // content of m.room.third_party_invite as: + // 1. A single public key in the public_key property. + // 2. A list of public keys in the public_keys property. + let tpi_content = third_party_invite_event + .get_content::() + .or_else(|_| { + Err(Error::InvalidPdu( + "m.room.third_party_invite event has invalid content".to_owned(), + )) + })?; + let mut public_keys = tpi_content.public_keys.unwrap_or_default(); + public_keys.push(PublicKey { + public_key: tpi_content.public_key, + key_validity_url: None, + }); + + let signatures = signed + .get("signatures") + .and_then(|v| v.as_object()) + .ok_or_else(|| { + Error::InvalidPdu( + "invite event third_party_invite signed missing/invalid signatures".to_owned(), + ) + })?; + let mut public_key_map = PublicKeyMap::new(); + for (server_name, sig_map) in signatures { + let mut pk_set = PublicKeySet::new(); + if let Some(sig_map) = sig_map.as_object() { + for (key_id, sig) in sig_map { + let sig_b64 = Base64::parse(sig.as_str().ok_or(Error::InvalidPdu( + "invite event third_party_invite signature is not a string".to_owned(), + ))?) + .map_err(|_| { + Error::InvalidPdu( + "invite event third_party_invite signature is not valid Base64" + .to_owned(), + ) + })?; + pk_set.insert(key_id.clone(), sig_b64); + } + } + public_key_map.insert(server_name.clone(), pk_set); + } + verify_json( + &public_key_map, + to_canonical_object(signed).expect("signed was already validated"), + ) + .map_err(|e| { + Error::AuthConditionFailed(format!( + "invite event third_party_invite signature verification failed: {e}" + )) + })?; + // If there was no error, there was a valid signature, so allow. + Ok(()) +} + +async fn check_invite_event( room_version: &RoomVersion, event: &Pdu, membership: &PartialMembershipObject, @@ -208,120 +323,20 @@ async fn check_invite_event( fetch_state: &FS, ) -> Result<(), Error> where - FE: AsyncFn(&EventId) -> Result, Error>, FS: AsyncFn((StateEventType, StateKey)) -> Result, Error>, { let target_current_membership = fetch_membership(fetch_state, target).await?; // 4.1: If content has a third_party_invite property: if let Some(raw_third_party_invite) = &membership.third_party_invite { - // 4.1.1: If target user is banned, reject. - if target_current_membership - .membership - .is_some_and(|m| m == "ban") - { - return Err(Error::AuthConditionFailed("invite target is banned".to_owned())); - } - // 4.1.2: If content.third_party_invite does not have a signed property, reject. - let signed = raw_third_party_invite.get("signed").ok_or_else(|| { - Error::AuthConditionFailed( - "invite event third_party_invite missing signed property".to_owned(), - ) - })?; - // 4.2.3: If signed does not have mxid and token properties, reject. - let mxid = signed.get("mxid").and_then(|v| v.as_str()).ok_or_else(|| { - Error::AuthConditionFailed( - "invite event third_party_invite signed missing/invalid mxid property".to_owned(), - ) - })?; - let token = signed - .get("token") - .and_then(|v| v.as_str()) - .ok_or_else(|| { - Error::AuthConditionFailed( - "invite event third_party_invite signed missing token property".to_owned(), - ) - })?; - // 4.2.4: If mxid does not match state_key, reject. - if mxid != target.as_str() { - return Err(Error::AuthConditionFailed( - "invite event third_party_invite signed mxid does not match state_key".to_owned(), - )); - } - // 4.2.5: If there is no m.room.third_party_invite event in the room - // state matching the token, reject. - let Some(third_party_invite_event) = - fetch_state(StateEventType::RoomThirdPartyInvite.with_state_key(token)).await? - else { - return Err(Error::AuthConditionFailed( - "invite event third_party_invite token has no matching m.room.third_party_invite" - .to_owned(), - )); - }; - // 4.2.6: If sender does not match sender of the m.room.third_party_invite, - // reject. - if third_party_invite_event.sender() != event.sender() { - return Err(Error::AuthConditionFailed( - "invite event sender does not match m.room.third_party_invite sender".to_owned(), - )); - } - // 4.2.7: If any signature in signed matches any public key in the - // m.room.third_party_invite event, allow. The public keys are in - // content of m.room.third_party_invite as: - // 1. A single public key in the public_key property. - // 2. A list of public keys in the public_keys property. - let tpi_content = third_party_invite_event - .get_content::() - .or_else(|_| { - Err(Error::InvalidPdu( - "m.room.third_party_invite event has invalid content".to_owned(), - )) - })?; - let mut public_keys = tpi_content.public_keys.unwrap_or_default(); - public_keys.push(PublicKey { - public_key: tpi_content.public_key, - key_validity_url: None, - }); - - let signatures = signed - .get("signatures") - .and_then(|v| v.as_object()) - .ok_or_else(|| { - Error::InvalidPdu( - "invite event third_party_invite signed missing/invalid signatures" - .to_owned(), - ) - })?; - let mut public_key_map = PublicKeyMap::new(); - for (server_name, sig_map) in signatures { - let mut pk_set = PublicKeySet::new(); - if let Some(sig_map) = sig_map.as_object() { - for (key_id, sig) in sig_map { - let sig_b64 = Base64::parse(sig.as_str().ok_or(Error::InvalidPdu( - "invite event third_party_invite signature is not a string".to_owned(), - ))?) - .map_err(|_| { - Error::InvalidPdu( - "invite event third_party_invite signature is not valid Base64" - .to_owned(), - ) - })?; - pk_set.insert(key_id.clone(), sig_b64); - } - } - public_key_map.insert(server_name.clone(), pk_set); - } - verify_json( - &public_key_map, - to_canonical_object(signed).expect("signed was already validated"), + return check_third_party_invite( + target_current_membership, + raw_third_party_invite, + target, + event, + fetch_state, ) - .map_err(|e| { - Error::AuthConditionFailed(format!( - "invite event third_party_invite signature verification failed: {e}" - )) - })?; - // If there was no error, there was a valid signature, so allow. - return Ok(()); + .await; } // 4.2: If the sender’s current membership state is not join, reject. @@ -354,7 +369,7 @@ where } pub async fn check_member_event( - room_version: RoomVersion, + room_version: &RoomVersion, event: &Pdu, fetch_event: FE, fetch_state: FS, @@ -395,10 +410,10 @@ where match content.membership.as_deref().unwrap() { | "join" => - check_join_event(&room_version, event, &content, &target, &fetch_event, &fetch_state) + check_join_event(room_version, event, &content, &target, &fetch_event, &fetch_state) .await?, | "invite" => - check_invite_event(&room_version, event, &content, &target, &fetch_state).await?, + check_invite_event(room_version, event, &content, &target, &fetch_state).await?, | _ => { todo!() }, diff --git a/src/core/matrix/state_res/event_auth/mod.rs b/src/core/matrix/state_res/event_auth/mod.rs index daf224d0..f0e5a7a7 100644 --- a/src/core/matrix/state_res/event_auth/mod.rs +++ b/src/core/matrix/state_res/event_auth/mod.rs @@ -3,3 +3,4 @@ mod context; pub mod create_event; pub mod iterative_auth_checks; pub mod member_event; +mod power_levels; diff --git a/src/core/matrix/state_res/event_auth/power_levels.rs b/src/core/matrix/state_res/event_auth/power_levels.rs new file mode 100644 index 00000000..9bb13220 --- /dev/null +++ b/src/core/matrix/state_res/event_auth/power_levels.rs @@ -0,0 +1,157 @@ +use ruma::{OwnedUserId, events::room::power_levels::RoomPowerLevelsEventContent}; + +use crate::{ + Event, Pdu, RoomVersion, + state_res::{Error, event_auth::context::UserPower}, +}; + +/// Verifies that a m.room.power_levels event is well-formed according to the +/// Matrix specification. +/// +/// Creators must contain the m.room.create sender and any additional creators. +pub async fn check_power_levels( + room_version: &RoomVersion, + event: &Pdu, + current_power_levels: Option<&RoomPowerLevelsEventContent>, + creators: Vec, +) -> Result<(), Error> { + let content = event + .get_content::() + .map_err(|e| { + Error::InvalidPdu(format!("m.room.power_levels event has invalid content: {}", e)) + })?; + + // If any of the properties users_default, events_default, state_default, ban, + // redact, kick, or invite in content are present and not an integer, reject. + // + // If either of the properties events or notifications in content are present + // and not an object with values that are integers, reject. + // + // NOTE: Deserialisation fails if this is not the case, so we don't need to + // check these here. + + // If the users property in content is not an object with keys that are valid + // user IDs with values that are integers (or a string that is an integer), + // reject. + while let Some(user_id) = content.users.keys().next() { + // NOTE: Deserialisation fails if the power level is not an integer, so we don't + // need to check that here. + + if let Err(e) = user_id.validate_historical() { + return Err(Error::InvalidPdu(format!( + "m.room.power_levels event has invalid user ID in users map: {}", + e + ))); + } + // Since v12, If the users property in content contains the sender of the + // m.room.create event or any of the additional_creators array (if present) + // from the content of the m.room.create event, reject. + if room_version.explicitly_privilege_room_creators && creators.contains(user_id) { + return Err(Error::InvalidPdu( + "m.room.power_levels event users map contains a room creator".to_string(), + )); + } + } + + // If there is no previous m.room.power_levels event in the room, allow. + if current_power_levels.is_none() { + return Ok(()); + } + let current_power_levels = current_power_levels.unwrap(); + + // For the properties users_default, events_default, state_default, ban, redact, + // kick, invite check if they were added, changed or removed. For each found + // alteration: + // If the current value is higher than the sender’s current power level, reject. + // If the new value is higher than the sender’s current power level, reject. + let sender = event.sender(); + let rank = if room_version.explicitly_privilege_room_creators { + if creators.contains(&sender.to_owned()) { + UserPower::Creator + } else { + UserPower::Standard + } + } else { + UserPower::Standard + }; + let sender_pl = current_power_levels + .users + .get(sender) + .unwrap_or(¤t_power_levels.users_default); + + if rank != UserPower::Creator { + let checks = [ + ("users_default", current_power_levels.users_default, content.users_default), + ("events_default", current_power_levels.events_default, content.events_default), + ("state_default", current_power_levels.state_default, content.state_default), + ("ban", current_power_levels.ban, content.ban), + ("redact", current_power_levels.redact, content.redact), + ("kick", current_power_levels.kick, content.kick), + ("invite", current_power_levels.invite, content.invite), + ]; + + for (name, old_value, new_value) in checks.iter() { + if old_value != new_value { + if *old_value > *sender_pl { + return Err(Error::AuthConditionFailed(format!( + "sender cannot change level for {}", + name + ))); + } + if *new_value > *sender_pl { + return Err(Error::AuthConditionFailed(format!( + "sender cannot raise level for {} to {}", + name, new_value + ))); + } + } + } + + // For each entry being changed in, or removed from, the events + // property: + // If the current value is greater than the sender’s current power level, + // reject. + for (event_type, new_value) in content.events.iter() { + let old_value = current_power_levels.events.get(event_type); + if old_value != Some(new_value) { + let old_pl = old_value.unwrap_or(¤t_power_levels.events_default); + if *old_pl > *sender_pl { + return Err(Error::AuthConditionFailed(format!( + "sender cannot change event level for {}", + event_type + ))); + } + if *new_value > *sender_pl { + return Err(Error::AuthConditionFailed(format!( + "sender cannot raise event level for {} to {}", + event_type, new_value + ))); + } + } + } + + // For each entry being changed in, or removed from, the events or + // notifications properties: + // If the current value is greater than the sender’s current power + // level, reject. + // If the new value is greater than the sender’s current power level, + // reject. + // TODO after making ruwuma's notifications value a BTreeMap + + // For each entry being added to, or changed in, the users property: + // If the new value is greater than the sender’s current power level, reject. + for (user_id, new_value) in content.users.iter() { + let old_value = current_power_levels.users.get(user_id); + if old_value != Some(new_value) { + if *new_value > *sender_pl { + return Err(Error::AuthConditionFailed(format!( + "sender cannot raise user level for {} to {}", + user_id, new_value + ))); + } + } + } + } + + Ok(()) +} diff --git a/src/core/matrix/state_res/mod.rs b/src/core/matrix/state_res/mod.rs index 2b3ddea4..ded63800 100644 --- a/src/core/matrix/state_res/mod.rs +++ b/src/core/matrix/state_res/mod.rs @@ -15,7 +15,8 @@ use std::{ hash::{BuildHasher, Hash}, }; -use futures::{Future, FutureExt, Stream, StreamExt, TryFutureExt, TryStreamExt, future}; +use futures::{Future, FutureExt, Stream, StreamExt, TryFutureExt, TryStreamExt}; +use itertools::Itertools; use ruma::{ EventId, Int, MilliSecondsSinceUnixEpoch, OwnedEventId, RoomVersionId, events::{ @@ -28,14 +29,13 @@ use serde_json::from_str as from_json_str; pub(crate) use self::error::Error; use self::power_levels::PowerLevelsContentFields; -pub use self::{ - event_auth::iterative_auth_checks::{auth_types_for_event, iterative_auth_check}, - room_version::RoomVersion, -}; +pub use self::{event_auth::iterative_auth_checks::auth_check, room_version::RoomVersion}; use crate::{ - debug, debug_error, err, + Pdu, debug, err, error as log_error, matrix::{Event, StateKey}, - state_res::room_version::StateResolutionVersion, + state_res::{ + event_auth::auth_events::auth_types_for_event, room_version::StateResolutionVersion, + }, trace, utils::stream::{BroadbandExt, IterStream, ReadyExt, TryBroadbandExt, WidebandExt}, warn, @@ -72,23 +72,19 @@ type Result = crate::Result; /// event is part of the same room. //#[tracing::instrument(level = "debug", skip(state_sets, auth_chain_sets, //#[tracing::instrument(level event_fetch))] -pub async fn resolve<'a, Pdu, Sets, SetIter, Hasher, Fetch, FetchFut, Exists, ExistsFut>( +pub async fn resolve<'a, Sets, SetIter, Hasher, FE, Exists>( room_version: &RoomVersionId, state_sets: Sets, auth_chain_sets: &'a [HashSet], - event_fetch: &Fetch, + event_fetch: &FE, event_exists: &Exists, ) -> Result> where - Fetch: Fn(OwnedEventId) -> FetchFut + Sync, - FetchFut: Future> + Send, - Exists: Fn(OwnedEventId) -> ExistsFut + Sync, - ExistsFut: Future + Send, + FE: AsyncFn(OwnedEventId) -> Result> + Sync, + Exists: AsyncFn(OwnedEventId) -> bool + Sync, Sets: IntoIterator + Send, SetIter: Iterator> + Clone + Send, Hasher: BuildHasher + Send + Sync, - Pdu: Event + Clone + Send + Sync, - for<'b> &'b Pdu: Event + Send, { use RoomVersionId::*; let stateres_version = match room_version { @@ -166,7 +162,7 @@ where // Sequentially auth check each control event. let resolved_control = iterative_auth_check( &room_version, - sorted_control_levels.iter().stream().map(AsRef::as_ref), + sorted_control_levels.iter().stream().map(ToOwned::to_owned), initial_state, &event_fetch, ) @@ -206,7 +202,7 @@ where let mut resolved_state = iterative_auth_check( &room_version, - sorted_left_events.iter().stream().map(AsRef::as_ref), + sorted_left_events.iter().stream(), resolved_control, // The control events are added to the final resolved state &event_fetch, ) @@ -270,14 +266,12 @@ where } /// Calculate the conflicted subgraph -async fn calculate_conflicted_subgraph( +async fn calculate_conflicted_subgraph( conflicted: &StateMap>, - fetch_event: &F, + fetch_event: &FE, ) -> Option> where - F: Fn(OwnedEventId) -> Fut + Sync, - Fut: Future> + Send, - E: Event + Send + Sync, + FE: AsyncFn(OwnedEventId) -> Result> + Sync, { let conflicted_events: HashSet<_> = conflicted.values().flatten().cloned().collect(); let mut subgraph: HashSet = HashSet::new(); @@ -309,7 +303,17 @@ where continue; } trace!(event_id = event_id.as_str(), "fetching event for its auth events"); - let evt = fetch_event(event_id.clone()).await; + let evt = fetch_event(event_id.clone()) + .await + .inspect_err(|e| { + log_error!( + "error fetching event {} for conflicted state subgraph: {}", + event_id, + e + ) + }) + .ok() + .flatten(); if evt.is_none() { err!("could not fetch event {} to calculate conflicted subgraph", event_id); path.pop(); @@ -356,15 +360,13 @@ where /// The power level is negative because a higher power level is equated to an /// earlier (further back in time) origin server timestamp. #[tracing::instrument(level = "debug", skip_all)] -async fn reverse_topological_power_sort( +async fn reverse_topological_power_sort( events_to_sort: Vec, auth_diff: &HashSet, - fetch_event: &F, + fetch_event: &FE, ) -> Result> where - F: Fn(OwnedEventId) -> Fut + Sync, - Fut: Future> + Send, - E: Event + Send + Sync, + FE: AsyncFn(OwnedEventId) -> Result> + Sync, { debug!("reverse topological sort of power events"); @@ -402,7 +404,7 @@ where .ok_or_else(|| Error::NotFound(String::new()))?; let ev = fetch_event(event_id) - .await + .await? .ok_or_else(|| Error::NotFound(String::new()))?; Ok((pl, ev.origin_server_ts())) @@ -541,18 +543,13 @@ where /// Do NOT use this any where but topological sort, we find the power level for /// the eventId at the eventId's generation (we walk backwards to `EventId`s /// most recent previous power level event). -async fn get_power_level_for_sender( - event_id: &EventId, - fetch_event: &F, -) -> serde_json::Result +async fn get_power_level_for_sender(event_id: &EventId, fetch_event: &FE) -> Result where - F: Fn(OwnedEventId) -> Fut + Sync, - Fut: Future> + Send, - E: Event + Send, + FE: AsyncFn(OwnedEventId) -> Result> + Sync, { debug!("fetch event ({event_id}) senders power level"); - let event = fetch_event(event_id.to_owned()).await; + let event = fetch_event(event_id.to_owned()).await?; let auth_events = event.as_ref().map(Event::auth_events); @@ -591,27 +588,23 @@ where /// the the `fetch_event` closure and verify each event using the /// `event_auth::auth_check` function. #[tracing::instrument(level = "trace", skip_all)] -async fn iterative_auth_check<'a, E, F, Fut, S>( +async fn iterative_auth_check( room_version: &RoomVersion, events_to_check: S, unconflicted_state: StateMap, - fetch_event: &F, + fetch_event: &FE, ) -> Result> where - F: Fn(OwnedEventId) -> Fut + Sync, - Fut: Future> + Send, - S: Stream + Send + 'a, - E: Event + Clone + Send + Sync, - for<'b> &'b E: Event + Send, + FE: AsyncFn(&EventId) -> Result, Error> + Sync + Send, + S: Stream + Send, { debug!("starting iterative auth check"); let events_to_check: Vec<_> = events_to_check - .map(Result::Ok) - .broad_and_then(async |event_id| { - fetch_event(event_id.to_owned()) - .await - .ok_or_else(|| Error::NotFound(format!("Failed to find {event_id}"))) + .map(Ok::) + .broad_and_then(async |event_id| match fetch_event(&event_id).await { + | Ok(Some(e)) => Ok(e), + | _ => Err(Error::NotFound(format!("could not find {event_id}")))?, }) .try_collect() .boxed() @@ -624,16 +617,20 @@ where let auth_event_ids: HashSet = events_to_check .iter() - .flat_map(|event: &E| event.auth_events().map(ToOwned::to_owned)) + .flat_map(|event: &Pdu| event.auth_events().map(ToOwned::to_owned)) .collect(); trace!(set = ?auth_event_ids, "auth event IDs to fetch"); - let auth_events: HashMap = auth_event_ids + let auth_events: HashMap = auth_event_ids .into_iter() .stream() - .broad_filter_map(fetch_event) - .map(|auth_event| (auth_event.event_id().to_owned(), auth_event)) + .broad_filter_map(async |event_id| { + fetch_event(&event_id) + .await + .map(|ev_opt| ev_opt.map(|ev| (event_id.clone(), ev))) + .unwrap_or_default() + }) .collect() .boxed() .await; @@ -652,29 +649,23 @@ where .state_key() .ok_or_else(|| Error::InvalidPdu("State event had no state key".to_owned()))?; + let member_event_content = match event.kind() { + | TimelineEventType::RoomMember => + Some(event.get_content::().map_err(|e| { + Error::InvalidPdu(format!("Failed to parse m.room.member content: {}", e)) + })?), + | _ => None, + }; let auth_types = auth_types_for_event( - event.event_type(), - event.sender(), - Some(state_key), - event.content(), room_version, + event.kind(), + event.state_key().map(StateKey::from_str).as_ref(), + event.sender(), + member_event_content, )?; trace!(list = ?auth_types, event_id = event.event_id().as_str(), "auth types for event"); - let mut auth_state = StateMap::new(); - if room_version.room_ids_as_hashes { - trace!("room version uses hashed IDs, manually fetching create event"); - let create_event_id_raw = event.room_id_or_hash().as_str().replace('!', "$"); - let create_event_id = EventId::parse(&create_event_id_raw).map_err(|e| { - Error::InvalidPdu(format!( - "Failed to parse create event ID from room ID/hash: {e}" - )) - })?; - let create_event = fetch_event(create_event_id.into()) - .await - .ok_or_else(|| Error::NotFound("Failed to find create event".into()))?; - auth_state.insert(create_event.event_type().with_state_key(""), create_event); - } + let mut auth_state = StateMap::with_capacity(event.auth_events.len()); for aid in event.auth_events() { if let Some(ev) = auth_events.get(aid) { //TODO: synapse checks "rejected_reason" which is most likely related to @@ -700,7 +691,13 @@ where if let Some(event) = auth_events.get(ev_id) { Some((key, event.clone())) } else { - Some((key, fetch_event(ev_id.clone()).await?)) + match fetch_event(ev_id).await { + | Ok(Some(event)) => Some((key, event)), + | _ => { + warn!(event_id = ev_id.as_str(), "unable to fetch auth event"); + None + }, + } } }) .ready_for_each(|(key, event)| { @@ -712,30 +709,16 @@ where debug!(event_id = event.event_id().as_str(), "Running auth checks"); - // The key for this is (eventType + a state_key of the signed token not sender) - // so search for it - let current_third_party = auth_state.iter().find_map(|(_, pdu)| { - (*pdu.event_type() == TimelineEventType::RoomThirdPartyInvite).then_some(pdu) - }); - - let fetch_state = |ty: &StateEventType, key: &str| { - future::ready( - auth_state - .get(&ty.with_state_key(key)) - .map(ToOwned::to_owned), - ) + let fetch_state = async |t: (StateEventType, StateKey)| { + Ok(auth_state + .get(&t.0.with_state_key(t.1.as_str())) + .map(ToOwned::to_owned)) }; - let auth_result = iterative_auth_check( - room_version, - &event, - current_third_party, - fetch_state, - &fetch_state(&StateEventType::RoomCreate, "") - .await - .expect("create event must exist"), - ) - .await; + let create_event = fetch_state((StateEventType::RoomCreate, StateKey::new())).await?; + let auth_result = + auth_check(room_version, &event, fetch_event, &fetch_state, create_event.as_ref()) + .await; match auth_result { | Ok(true) => { @@ -755,7 +738,7 @@ where warn!("event {} failed the authentication check", event.event_id()); }, | Err(e) => { - debug_error!("event {} failed the authentication check: {e}", event.event_id()); + log_error!("event {} failed the authentication check: {e}", event.event_id()); return Err(e); }, } @@ -774,15 +757,13 @@ where /// after the most recent are depth 0, the events before (with the first power /// level as a parent) will be marked as depth 1. depth 1 is "older" than depth /// 0. -async fn mainline_sort( +async fn mainline_sort( to_sort: &[OwnedEventId], resolved_power_level: Option, - fetch_event: &F, + fetch_event: &FE, ) -> Result> where - F: Fn(OwnedEventId) -> Fut + Sync, - Fut: Future> + Send, - E: Event + Clone + Send + Sync, + FE: AsyncFn(OwnedEventId) -> Result> + Sync, { debug!("mainline sort of events"); @@ -797,13 +778,13 @@ where mainline.push(p.clone()); let event = fetch_event(p.clone()) - .await + .await? .ok_or_else(|| Error::NotFound(format!("Failed to find {p}")))?; pl = None; for aid in event.auth_events() { let ev = fetch_event(aid.to_owned()) - .await + .await? .ok_or_else(|| Error::NotFound(format!("Failed to find {aid}")))?; if is_type_and_key(&ev, &TimelineEventType::RoomPowerLevels, "") { @@ -824,7 +805,11 @@ where .iter() .stream() .broad_filter_map(async |ev_id| { - fetch_event(ev_id.clone()).await.map(|event| (event, ev_id)) + fetch_event(ev_id.clone()) + .await + .ok() + .flatten() + .map(|event| (event, ev_id)) }) .broad_filter_map(|(event, ev_id)| { get_mainline_depth(Some(event.clone()), &mainline_map, fetch_event) @@ -846,15 +831,13 @@ where /// Get the mainline depth from the `mainline_map` or finds a power_level event /// that has an associated mainline depth. -async fn get_mainline_depth( - mut event: Option, +async fn get_mainline_depth( + mut event: Option, mainline_map: &HashMap, - fetch_event: &F, + fetch_event: &FE, ) -> Result where - F: Fn(OwnedEventId) -> Fut + Sync, - Fut: Future> + Send, - E: Event + Send + Sync, + FE: AsyncFn(OwnedEventId) -> Result> + Sync, { while let Some(sort_ev) = event { debug!(event_id = sort_ev.event_id().as_str(), "mainline"); @@ -867,7 +850,7 @@ where event = None; for aid in sort_ev.auth_events() { let aev = fetch_event(aid.to_owned()) - .await + .await? .ok_or_else(|| Error::NotFound(format!("Failed to find {aid}")))?; if is_type_and_key(&aev, &TimelineEventType::RoomPowerLevels, "") { @@ -880,20 +863,18 @@ where Ok(0) } -async fn add_event_and_auth_chain_to_graph( +async fn add_event_and_auth_chain_to_graph( graph: &mut HashMap>, event_id: OwnedEventId, auth_diff: &HashSet, - fetch_event: &F, + fetch_event: &FE, ) where - F: Fn(OwnedEventId) -> Fut + Sync, - Fut: Future> + Send, - E: Event + Send + Sync, + FE: AsyncFn(OwnedEventId) -> Result> + Sync, { let mut state = vec![event_id]; while let Some(eid) = state.pop() { graph.entry(eid.clone()).or_default(); - let event = fetch_event(eid.clone()).await; + let event = fetch_event(eid.clone()).await.ok().flatten(); let auth_events = event.as_ref().map(Event::auth_events).into_iter().flatten(); // Prefer the store to event as the store filters dedups the events @@ -912,14 +893,12 @@ async fn add_event_and_auth_chain_to_graph( } } -async fn is_power_event_id(event_id: &EventId, fetch: &F) -> bool +async fn is_power_event_id(event_id: &EventId, fetch: &FE) -> bool where - F: Fn(OwnedEventId) -> Fut + Sync, - Fut: Future> + Send, - E: Event + Send, + FE: AsyncFn(OwnedEventId) -> Result> + Sync, { match fetch(event_id.to_owned()).await.as_ref() { - | Some(state) => is_power_event(state), + | Ok(Some(state)) => is_power_event(state), | _ => false, } } @@ -976,6 +955,7 @@ where mod tests { use std::collections::{HashMap, HashSet}; + use itertools::Itertools; use maplit::{hashmap, hashset}; use rand::seq::SliceRandom; use ruma::{ @@ -1031,7 +1011,7 @@ mod tests { .await .unwrap(); - let resolved_power = super::iterative_auth_check( + let resolved_power = super::auth_check( &RoomVersion::V6, sorted_power_events.iter().map(AsRef::as_ref).stream(), HashMap::new(), // unconflicted events diff --git a/src/service/rooms/timeline/create.rs b/src/service/rooms/timeline/create.rs index ccb2d6e7..40e41b08 100644 --- a/src/service/rooms/timeline/create.rs +++ b/src/service/rooms/timeline/create.rs @@ -236,7 +236,7 @@ pub async fn create_hash_and_sign_event( | _ => create_pdu.as_ref().unwrap().as_pdu(), }; - let auth_check = state_res::iterative_auth_check( + let auth_check = state_res::auth_check( &room_version, &pdu, None, // TODO: third_party_invite