From 302f535e6649744603116b0013448330b755c433 Mon Sep 17 00:00:00 2001 From: timedout Date: Sat, 19 Sep 2026 00:37:08 +0100 Subject: [PATCH] refactor(wip): Replace local state resolution with upstream Ruma's --- src/core/error/mod.rs | 2 +- src/core/matrix/mod.rs | 2 - src/core/matrix/pdu.rs | 35 + src/core/matrix/state_res/LICENSE | 17 - src/core/matrix/state_res/benches.rs | 554 ----- src/core/matrix/state_res/error.rs | 23 - src/core/matrix/state_res/event_auth.rs | 1971 ---------------- src/core/matrix/state_res/mod.rs | 2052 ----------------- src/core/matrix/state_res/outcomes.txt | 104 - src/core/matrix/state_res/power_levels.rs | 241 -- src/core/matrix/state_res/serde_backports.rs | 124 - src/core/matrix/state_res/test_utils.rs | 582 ----- src/core/mod.rs | 2 +- src/database/map/qry.rs | 17 + src/service/rooms/event_handler/fetch_auth.rs | 15 +- .../rooms/event_handler/handle_outlier_pdu.rs | 12 +- src/service/rooms/event_handler/mod.rs | 10 +- src/service/rooms/event_handler/pdu_checks.rs | 83 +- .../rooms/event_handler/resolve_state.rs | 101 +- .../event_handler/upgrade_outlier_pdu.rs | 24 +- src/service/rooms/short/mod.rs | 9 + src/service/rooms/state/mod.rs | 11 +- src/service/rooms/timeline/data.rs | 16 + src/service/rooms/timeline/mod.rs | 6 + 24 files changed, 233 insertions(+), 5780 deletions(-) delete mode 100644 src/core/matrix/state_res/LICENSE delete mode 100644 src/core/matrix/state_res/benches.rs delete mode 100644 src/core/matrix/state_res/error.rs delete mode 100644 src/core/matrix/state_res/event_auth.rs delete mode 100644 src/core/matrix/state_res/mod.rs delete mode 100644 src/core/matrix/state_res/outcomes.txt delete mode 100644 src/core/matrix/state_res/power_levels.rs delete mode 100644 src/core/matrix/state_res/serde_backports.rs delete mode 100644 src/core/matrix/state_res/test_utils.rs diff --git a/src/core/error/mod.rs b/src/core/error/mod.rs index ad3040fff..cd6e042ce 100644 --- a/src/core/error/mod.rs +++ b/src/core/error/mod.rs @@ -130,7 +130,7 @@ pub enum Error { #[error(transparent)] SignatureVerification(#[from] ruma::signatures::VerificationError), #[error(transparent)] - StateRes(#[from] crate::state_res::Error), + StateRes(#[from] ruma::state_res::Error), #[error("uiaa")] Uiaa(ruma::api::client::uiaa::UiaaInfo), diff --git a/src/core/matrix/mod.rs b/src/core/matrix/mod.rs index cab557411..420322865 100644 --- a/src/core/matrix/mod.rs +++ b/src/core/matrix/mod.rs @@ -3,10 +3,8 @@ pub mod event; pub mod pdu; pub mod state_key; -pub mod state_res; pub mod versions; pub use event::{Event, TypeExt as EventTypeExt}; pub use pdu::{PartialPdu, Pdu, PduCount, PduEvent, PduId, RawPduId, ShortId}; pub use state_key::StateKey; -pub use state_res::{StateMap, TypeStateKey}; diff --git a/src/core/matrix/pdu.rs b/src/core/matrix/pdu.rs index f7c0576d2..e72c32e38 100644 --- a/src/core/matrix/pdu.rs +++ b/src/core/matrix/pdu.rs @@ -73,6 +73,9 @@ pub struct Pdu { // BTreeMap, BTreeMap> #[serde(default, skip_serializing_if = "Option::is_none")] pub signatures: Option>, + + #[serde(default)] + pub rejected: bool, } /// Content hashes of a PDU. @@ -254,3 +257,35 @@ impl Ord for Pdu { impl PartialOrd for Pdu { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } + +impl ruma::state_res::Event for Pdu { + type Id = OwnedEventId; + + fn event_id(&self) -> &Self::Id { &self.event_id } + + fn room_id(&self) -> Option<&RoomId> { self.room_id.as_deref() } + + fn sender(&self) -> &UserId { self.sender.as_ref() } + + fn origin_server_ts(&self) -> MilliSecondsSinceUnixEpoch { + MilliSecondsSinceUnixEpoch(self.origin_server_ts) + } + + fn event_type(&self) -> &TimelineEventType { &self.kind } + + fn content(&self) -> &RawJsonValue { &self.content } + + fn state_key(&self) -> Option<&str> { self.state_key.as_deref() } + + fn prev_events(&self) -> Box + '_> { + Box::new(self.prev_events.iter()) + } + + fn auth_events(&self) -> Box + '_> { + Box::new(self.auth_events.iter()) + } + + fn redacts(&self) -> Option<&Self::Id> { self.redacts.as_ref() } + + fn rejected(&self) -> bool { self.rejected } +} diff --git a/src/core/matrix/state_res/LICENSE b/src/core/matrix/state_res/LICENSE deleted file mode 100644 index c103a044b..000000000 --- a/src/core/matrix/state_res/LICENSE +++ /dev/null @@ -1,17 +0,0 @@ -//! Permission is hereby granted, free of charge, to any person obtaining a copy -//! of this software and associated documentation files (the "Software"), to -//! deal in the Software without restriction, including without limitation the -//! rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -//! sell copies of the Software, and to permit persons to whom the Software is -//! furnished to do so, subject to the following conditions: - -//! The above copyright notice and this permission notice shall be included in -//! all copies or substantial portions of the Software. - -//! THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -//! IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -//! FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -//! AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -//! LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -//! FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS -//! IN THE SOFTWARE. diff --git a/src/core/matrix/state_res/benches.rs b/src/core/matrix/state_res/benches.rs deleted file mode 100644 index 31e0848d4..000000000 --- a/src/core/matrix/state_res/benches.rs +++ /dev/null @@ -1,554 +0,0 @@ -#[cfg(conduwuit_bench)] -extern crate test; - -use std::{ - borrow::Borrow, - collections::{HashMap, HashSet}, - sync::atomic::{AtomicU64, Ordering::SeqCst}, -}; - -use futures::{future, future::ready}; -use maplit::{btreemap, hashmap, hashset}; -use ruma::{ - EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, RoomId, RoomVersionId, Signatures, UserId, - events::{ - StateEventType, TimelineEventType, - room::{ - join_rules::{JoinRule, RoomJoinRulesEventContent}, - member::{MembershipState, RoomMemberEventContent}, - }, - }, - int, room_id, uint, user_id, -}; -use serde_json::{ - json, - value::{RawValue as RawJsonValue, to_raw_value as to_raw_json_value}, -}; - -use crate::{ - matrix::{Event, Pdu, pdu::EventHash}, - state_res::{self as state_res, Error, Result, StateMap}, -}; - -static SERVER_TIMESTAMP: AtomicU64 = AtomicU64::new(0); - -#[cfg(conduwuit_bench)] -#[cfg_attr(conduwuit_bench, bench)] -fn lexico_topo_sort(c: &mut test::Bencher) { - let graph = hashmap! { - event_id("l") => hashset![event_id("o")], - event_id("m") => hashset![event_id("n"), event_id("o")], - event_id("n") => hashset![event_id("o")], - event_id("o") => hashset![], // "o" has zero outgoing edges but 4 incoming edges - event_id("p") => hashset![event_id("o")], - }; - - c.iter(|| { - let _ = state_res::lexicographical_topological_sort(&graph, &|_| { - future::ok((int!(0), MilliSecondsSinceUnixEpoch(uint!(0)))) - }); - }); -} - -#[cfg(conduwuit_bench)] -#[cfg_attr(conduwuit_bench, bench)] -fn resolution_shallow_auth_chain(c: &mut test::Bencher) { - let mut store = TestStore(hashmap! {}); - - // build up the DAG - let (state_at_bob, state_at_charlie, _) = store.set_up(); - - c.iter(|| async { - let ev_map = store.0.clone(); - let state_sets = [&state_at_bob, &state_at_charlie]; - let fetch = |id: OwnedEventId| ready(ev_map.get(&id).map(ToOwned::to_owned)); - let exists = |id: OwnedEventId| ready(ev_map.get(&id).is_some()); - let auth_chain_sets: Vec> = state_sets - .iter() - .map(|map| { - store - .auth_event_ids(room_id(), map.values().cloned().collect()) - .unwrap() - }) - .collect(); - - let _ = match state_res::resolve( - &RoomVersionId::V6, - state_sets.into_iter(), - &auth_chain_sets, - &fetch, - &exists, - ) - .await - { - | Ok(state) => state, - | Err(e) => panic!("{e}"), - }; - }); -} - -#[cfg(conduwuit_bench)] -#[cfg_attr(conduwuit_bench, bench)] -fn resolve_deeper_event_set(c: &mut test::Bencher) { - let mut inner = INITIAL_EVENTS(); - let ban = BAN_STATE_SET(); - - inner.extend(ban); - let store = TestStore(inner.clone()); - - let state_set_a = [ - inner.get(&event_id("CREATE")).unwrap(), - inner.get(&event_id("IJR")).unwrap(), - inner.get(&event_id("IMA")).unwrap(), - inner.get(&event_id("IMB")).unwrap(), - inner.get(&event_id("IMC")).unwrap(), - inner.get(&event_id("MB")).unwrap(), - inner.get(&event_id("PA")).unwrap(), - ] - .iter() - .map(|ev| { - ( - (ev.event_type().clone().into(), ev.state_key().unwrap().into()), - ev.event_id().to_owned(), - ) - }) - .collect::>(); - - let state_set_b = [ - inner.get(&event_id("CREATE")).unwrap(), - inner.get(&event_id("IJR")).unwrap(), - inner.get(&event_id("IMA")).unwrap(), - inner.get(&event_id("IMB")).unwrap(), - inner.get(&event_id("IMC")).unwrap(), - inner.get(&event_id("IME")).unwrap(), - inner.get(&event_id("PA")).unwrap(), - ] - .iter() - .map(|ev| { - ( - (ev.event_type().clone().into(), ev.state_key().unwrap().into()), - ev.event_id().to_owned(), - ) - }) - .collect::>(); - - c.iter(|| async { - let state_sets = [&state_set_a, &state_set_b]; - let auth_chain_sets: Vec> = state_sets - .iter() - .map(|map| { - store - .auth_event_ids(room_id(), map.values().cloned().collect()) - .unwrap() - }) - .collect(); - - let fetch = |id: OwnedEventId| ready(inner.get(&id).map(ToOwned::to_owned)); - let exists = |id: OwnedEventId| ready(inner.get(&id).is_some()); - let _ = match state_res::resolve( - &RoomVersionId::V6, - state_sets.into_iter(), - &auth_chain_sets, - &fetch, - &exists, - ) - .await - { - | Ok(state) => state, - | Err(_) => panic!("resolution failed during benchmarking"), - }; - }); -} - -//*///////////////////////////////////////////////////////////////////// -// -// IMPLEMENTATION DETAILS AHEAD -// -/////////////////////////////////////////////////////////////////////*/ -struct TestStore(HashMap); - -#[allow(unused)] -impl TestStore { - fn get_event(&self, room_id: &RoomId, event_id: &EventId) -> Result { - self.0 - .get(event_id) - .cloned() - .ok_or_else(|| Error::NotFound(format!("{} not found", event_id))) - } - - /// Returns the events that correspond to the `event_ids` sorted in the same - /// order. - fn get_events(&self, room_id: &RoomId, event_ids: &[OwnedEventId]) -> Result> { - let mut events = vec![]; - for id in event_ids { - events.push(self.get_event(room_id, id)?); - } - Ok(events) - } - - /// Returns a Vec of the related auth events to the given `event`. - fn auth_event_ids( - &self, - room_id: &RoomId, - event_ids: Vec, - ) -> Result> { - let mut result = HashSet::new(); - let mut stack = event_ids; - - // DFS for auth event chain - while !stack.is_empty() { - let ev_id = stack.pop().unwrap(); - if result.contains(&ev_id) { - continue; - } - - result.insert(ev_id.clone()); - - let event = self.get_event(room_id, ev_id.borrow())?; - - stack.extend(event.auth_events().map(ToOwned::to_owned)); - } - - Ok(result) - } - - /// Returns a vector representing the difference in auth chains of the given - /// `events`. - fn auth_chain_diff( - &self, - room_id: &RoomId, - event_ids: Vec>, - ) -> Result> { - let mut auth_chain_sets = vec![]; - for ids in event_ids { - // TODO state store `auth_event_ids` returns self in the event ids - // list when an event returns `auth_event_ids` self is not - // contained - let chain = self - .auth_event_ids(room_id, ids)? - .into_iter() - .collect::>(); - auth_chain_sets.push(chain); - } - - if let Some(first) = auth_chain_sets.first().cloned() { - let common = auth_chain_sets - .iter() - .skip(1) - .fold(first, |a, b| a.intersection(b).cloned().collect::>()); - - Ok(auth_chain_sets - .into_iter() - .flatten() - .filter(|id| !common.contains(id)) - .collect()) - } else { - Ok(vec![]) - } - } -} - -impl TestStore { - #[allow(clippy::type_complexity)] - fn set_up( - &mut self, - ) -> (StateMap, StateMap, StateMap) { - let create_event = to_pdu_event::<&EventId>( - "CREATE", - alice(), - TimelineEventType::RoomCreate, - Some(""), - to_raw_json_value(&json!({ "creator": alice() })).unwrap(), - &[], - &[], - ); - let cre = create_event.event_id().to_owned(); - self.0.insert(cre.clone(), create_event.clone()); - - let alice_mem = to_pdu_event( - "IMA", - alice(), - TimelineEventType::RoomMember, - Some(alice().to_string().as_str()), - member_content_join(), - &[cre.clone()], - &[cre.clone()], - ); - self.0 - .insert(alice_mem.event_id().to_owned(), alice_mem.clone()); - - let join_rules = to_pdu_event( - "IJR", - alice(), - TimelineEventType::RoomJoinRules, - Some(""), - to_raw_json_value(&RoomJoinRulesEventContent::new(JoinRule::Public)).unwrap(), - &[cre.clone(), alice_mem.event_id().to_owned()], - &[alice_mem.event_id().to_owned()], - ); - self.0 - .insert(join_rules.event_id().to_owned(), join_rules.clone()); - - // Bob and Charlie join at the same time, so there is a fork - // this will be represented in the state_sets when we resolve - let bob_mem = to_pdu_event( - "IMB", - bob(), - TimelineEventType::RoomMember, - Some(bob().to_string().as_str()), - member_content_join(), - &[cre.clone(), join_rules.event_id().to_owned()], - &[join_rules.event_id().to_owned()], - ); - self.0 - .insert(bob_mem.event_id().to_owned(), bob_mem.clone()); - - let charlie_mem = to_pdu_event( - "IMC", - charlie(), - TimelineEventType::RoomMember, - Some(charlie().to_string().as_str()), - member_content_join(), - &[cre, join_rules.event_id().to_owned()], - &[join_rules.event_id().to_owned()], - ); - self.0 - .insert(charlie_mem.event_id().to_owned(), charlie_mem.clone()); - - let state_at_bob = [&create_event, &alice_mem, &join_rules, &bob_mem] - .iter() - .map(|ev| { - ( - (ev.event_type().to_string().into(), ev.state_key().unwrap().into()), - ev.event_id().to_owned(), - ) - }) - .collect::>(); - - let state_at_charlie = [&create_event, &alice_mem, &join_rules, &charlie_mem] - .iter() - .map(|ev| { - ( - (ev.event_type().to_string().into(), ev.state_key().unwrap().into()), - ev.event_id().to_owned(), - ) - }) - .collect::>(); - - let expected = [&create_event, &alice_mem, &join_rules, &bob_mem, &charlie_mem] - .iter() - .map(|ev| { - ( - (ev.event_type().to_string().into(), ev.state_key().unwrap().into()), - ev.event_id().to_owned(), - ) - }) - .collect::>(); - - (state_at_bob, state_at_charlie, expected) - } -} - -fn event_id(id: &str) -> OwnedEventId { - if id.contains('$') { - return id.try_into().unwrap(); - } - format!("${}:foo", id).try_into().unwrap() -} - -fn alice() -> &'static UserId { user_id!("@alice:foo") } - -fn bob() -> &'static UserId { user_id!("@bob:foo") } - -fn charlie() -> &'static UserId { user_id!("@charlie:foo") } - -fn ella() -> &'static UserId { user_id!("@ella:foo") } - -fn room_id() -> &'static RoomId { room_id!("!test:foo") } - -fn member_content_ban() -> Box { - to_raw_json_value(&RoomMemberEventContent::new(MembershipState::Ban)).unwrap() -} - -fn member_content_join() -> Box { - to_raw_json_value(&RoomMemberEventContent::new(MembershipState::Join)).unwrap() -} - -fn to_pdu_event( - id: &str, - sender: &UserId, - ev_type: TimelineEventType, - state_key: Option<&str>, - content: Box, - auth_events: &[S], - prev_events: &[S], -) -> Pdu -where - S: AsRef, -{ - // We don't care if the addition happens in order just that it is atomic - // (each event has its own value) - let ts = SERVER_TIMESTAMP.fetch_add(1, SeqCst); - let id = if id.contains('$') { - id.to_owned() - } else { - format!("${}:foo", id) - }; - let auth_events = auth_events - .iter() - .map(AsRef::as_ref) - .map(event_id) - .collect::>(); - let prev_events = prev_events - .iter() - .map(AsRef::as_ref) - .map(event_id) - .collect::>(); - - Pdu { - event_id: id.try_into().unwrap(), - room_id: Some(room_id().to_owned()), - sender: sender.to_owned(), - origin_server_ts: ts.try_into().unwrap(), - state_key: state_key.map(Into::into), - kind: ev_type, - content, - origin: None, - redacts: None, - sticky: None, - unsigned: None, - auth_events, - prev_events, - depth: uint!(0), - hashes: EventHash { sha256: String::new() }, - signatures: None, - } -} - -// all graphs start with these input events -#[allow(non_snake_case)] -fn INITIAL_EVENTS() -> HashMap { - vec![ - to_pdu_event::<&EventId>( - "CREATE", - alice(), - TimelineEventType::RoomCreate, - Some(""), - to_raw_json_value(&json!({ "creator": alice() })).unwrap(), - &[], - &[], - ), - to_pdu_event( - "IMA", - alice(), - TimelineEventType::RoomMember, - Some(alice().as_str()), - member_content_join(), - &["CREATE"], - &["CREATE"], - ), - to_pdu_event( - "IPOWER", - alice(), - TimelineEventType::RoomPowerLevels, - Some(""), - to_raw_json_value(&json!({ "users": { alice(): 100 } })).unwrap(), - &["CREATE", "IMA"], - &["IMA"], - ), - to_pdu_event( - "IJR", - alice(), - TimelineEventType::RoomJoinRules, - Some(""), - to_raw_json_value(&RoomJoinRulesEventContent::new(JoinRule::Public)).unwrap(), - &["CREATE", "IMA", "IPOWER"], - &["IPOWER"], - ), - to_pdu_event( - "IMB", - bob(), - TimelineEventType::RoomMember, - Some(bob().to_string().as_str()), - member_content_join(), - &["CREATE", "IJR", "IPOWER"], - &["IJR"], - ), - to_pdu_event( - "IMC", - charlie(), - TimelineEventType::RoomMember, - Some(charlie().to_string().as_str()), - member_content_join(), - &["CREATE", "IJR", "IPOWER"], - &["IMB"], - ), - to_pdu_event::<&EventId>( - "START", - charlie(), - TimelineEventType::RoomTopic, - Some(""), - to_raw_json_value(&json!({})).unwrap(), - &[], - &[], - ), - to_pdu_event::<&EventId>( - "END", - charlie(), - TimelineEventType::RoomTopic, - Some(""), - to_raw_json_value(&json!({})).unwrap(), - &[], - &[], - ), - ] - .into_iter() - .map(|ev| (ev.event_id().to_owned(), ev)) - .collect() -} - -// all graphs start with these input events -#[allow(non_snake_case)] -fn BAN_STATE_SET() -> HashMap { - vec![ - to_pdu_event( - "PA", - alice(), - TimelineEventType::RoomPowerLevels, - Some(""), - to_raw_json_value(&json!({ "users": { alice(): 100, bob(): 50 } })).unwrap(), - &["CREATE", "IMA", "IPOWER"], // auth_events - &["START"], // prev_events - ), - to_pdu_event( - "PB", - alice(), - TimelineEventType::RoomPowerLevels, - Some(""), - to_raw_json_value(&json!({ "users": { alice(): 100, bob(): 50 } })).unwrap(), - &["CREATE", "IMA", "IPOWER"], - &["END"], - ), - to_pdu_event( - "MB", - alice(), - TimelineEventType::RoomMember, - Some(ella().as_str()), - member_content_ban(), - &["CREATE", "IMA", "PB"], - &["PA"], - ), - to_pdu_event( - "IME", - ella(), - TimelineEventType::RoomMember, - Some(ella().as_str()), - member_content_join(), - &["CREATE", "IJR", "PA"], - &["MB"], - ), - ] - .into_iter() - .map(|ev| (ev.event_id().to_owned(), ev)) - .collect() -} diff --git a/src/core/matrix/state_res/error.rs b/src/core/matrix/state_res/error.rs deleted file mode 100644 index 7711d878d..000000000 --- a/src/core/matrix/state_res/error.rs +++ /dev/null @@ -1,23 +0,0 @@ -use serde_json::Error as JsonError; -use thiserror::Error; - -/// Represents the various errors that arise when resolving state. -#[derive(Error, Debug)] -#[non_exhaustive] -pub enum Error { - /// A deserialization error. - #[error(transparent)] - SerdeJson(#[from] JsonError), - - /// The given option or version is unsupported. - #[error("Unsupported room version: {0}")] - Unsupported(String), - - /// The given event was not found. - #[error("Not found error: {0}")] - NotFound(String), - - /// Invalid fields in the given PDU. - #[error("Invalid PDU: {0}")] - InvalidPdu(String), -} diff --git a/src/core/matrix/state_res/event_auth.rs b/src/core/matrix/state_res/event_auth.rs deleted file mode 100644 index b5d719742..000000000 --- a/src/core/matrix/state_res/event_auth.rs +++ /dev/null @@ -1,1971 +0,0 @@ -use std::collections::BTreeSet; - -use futures::{ - Future, - future::{OptionFuture, join, join3}, -}; -use ruma::{ - AnyKeyName, CanonicalJsonObject, Int, OwnedUserId, RoomVersionId, SigningKeyId, UserId, - events::room::{ - join_rules::{JoinRule, RoomJoinRulesEventContent}, - member::MembershipState, - power_levels::{RoomPowerLevelsEventContent, UserPowerLevel}, - third_party_invite::PublicKey, - }, - int, - room_version_rules::{RoomIdFormatVersion, RoomVersionRules}, - serde::{Raw, base64, base64::Standard}, - signatures, - signatures::to_canonical_json_string_for_signing, - third_party_invite::IdentityServerBase64PublicKey, -}; -use serde::{Deserialize, Serialize}; -use serde_json::{from_str as from_json_str, value::RawValue as RawJsonValue}; - -use super::{ - Error, Event, Result, StateEventType, StateKey, TimelineEventType, - power_levels::{ - deserialize_power_levels, deserialize_power_levels_content_invite, - deserialize_power_levels_content_redact, - }, -}; -use crate::{debug, err, error, trace, warn}; - -// FIXME: field extracting could be bundled for `content` -#[derive(Deserialize)] -struct GetMembership { - membership: MembershipState, -} - -#[derive(Deserialize, Debug)] -struct RoomMemberContentFields { - membership: Option>, - join_authorised_via_users_server: Option>, -} - -#[derive(Deserialize)] -struct RoomCreateContentFields { - room_version: Option>, - creator: Option>, - additional_creators: Option>>, - #[serde(rename = "m.federate", default = "ruma::serde::default_true")] - federate: bool, -} - -// Workaround for https://github.com/matrix-org/matrix-spec/issues/2471 -#[derive(Deserialize, Debug)] -struct RoomThirdPartyInviteContentFields { - public_key: Option, - public_keys: Option>, -} - -// same here sort of (maintainers see https://forgejo.ellis.link/continuwuation/continuwuity-sec/issues/41) -#[derive(Deserialize, Debug)] -struct MemberThirdPartyInviteSignedContent { - signed: ThirdPartyInviteSignedObject, -} - -#[derive(Serialize, Deserialize, Debug)] -struct ThirdPartyInviteSignedObject(CanonicalJsonObject); - -impl ThirdPartyInviteSignedObject { - #[inline] - fn mxid(&self) -> Option { - self.0 - .get("mxid")? - .as_str() - .and_then(|raw| UserId::parse(raw).ok()) - } - - #[inline] - fn token(&self) -> Option<&str> { self.0.get("token")?.as_str() } - - #[inline] - fn signatures(&self) -> Option { - self.0.get("signatures")?.as_object().map(ToOwned::to_owned) - } -} - -/// 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: &RoomVersionRules, -) -> crate::Result> { - if kind == &TimelineEventType::RoomCreate { - return Ok(vec![]); - } - - let mut auth_types = if room_version.room_id_format == RoomIdFormatVersion::V2 { - 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)] - #[allow(unused)] - 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() - .ok_or_else(|| { - err!(Request(BadJson( - "Third party invite content missing a token" - ))) - })? - .into(), - ); - if !auth_types.contains(&key) { - auth_types.push(key); - } - } - } - } - } - } - - Ok(auth_types) -} - -fn get_user_power_level( - room_version_rules: &RoomVersionRules, - creator: &UserId, - additional_creators: &[Raw], - power_levels: Option<&RoomPowerLevelsEventContent>, - user_id: &UserId, -) -> UserPowerLevel { - if room_version_rules - .authorization - .explicitly_privilege_room_creators - && (user_id == creator - || additional_creators - .iter() - .any(|c| c.deserialize().ok() == Some(user_id.to_owned()))) - { - return UserPowerLevel::Infinite; - } - - if let Some(pls) = power_levels { - UserPowerLevel::Int(pls.users.get(user_id).copied().unwrap_or(pls.users_default)) - } else { - UserPowerLevel::Int(if user_id == creator { int!(100) } else { int!(0) }) - } -} - -/// Authenticate the incoming `event`. -/// -/// The steps of authentication are: -/// -/// * check that the event is being authenticated for the correct room -/// * then there are checks for specific event types -/// -/// The `fetch_state` closure should gather state from a state snapshot. We need -/// to know if the event passes auth against some state not a recursive -/// collection of auth_events fields. -#[tracing::instrument( - level = "debug", - skip_all, - fields( - event_id = incoming_event.event_id().as_str(), - ) -)] -#[allow(clippy::suspicious_operation_groupings)] -pub async fn auth_check( - room_version: &RoomVersionRules, - incoming_event: &E, - fetch_state: F, - create_event: &E, -) -> Result -where - F: Fn(&StateEventType, &str) -> Fut + Send + Copy, - Fut: Future> + Send, - E: Event + Send + Sync, - for<'a> &'a E: Event + Send, -{ - debug!( - event_id = %incoming_event.event_id(), - event_type = ?incoming_event.event_type(), - "auth_check beginning" - ); - - // [synapse] check that all the events are in the same room as - // `incoming_event` - - // [synapse] do_sig_check check the event has valid signatures for member - // events - - let sender = incoming_event.sender(); - - // Implementation of https://spec.matrix.org/latest/rooms/v1/#authorization-rules - // - // 1. If type is m.room.create: - if *incoming_event.event_type() == TimelineEventType::RoomCreate { - debug!("start m.room.create check"); - - // If it has any previous events, reject - if incoming_event.prev_events().next().is_some() { - warn!("the room creation event had previous events"); - return Ok(false); - } - - // If the domain of the room_id does not match the domain of the sender, - // reject - if incoming_event.room_id().is_some() { - let Some(room_id_server_name) = incoming_event.room_id().unwrap().server_name() - else { - warn!("legacy room ID has no server name"); - return Ok(false); - }; - if room_id_server_name != sender.server_name() { - warn!( - expected = %sender.server_name(), - received = %room_id_server_name, - "server name of legacy room ID does not match server name of sender" - ); - return Ok(false); - } - } - - // If content.room_version is present and is not a recognized version, - // reject - let content: RoomCreateContentFields = from_json_str(incoming_event.content().get())?; - if content - .room_version - .is_some_and(|v| v.deserialize().is_err()) - { - warn!("unsupported room version found in m.room.create event"); - return Ok(false); - } - - if room_version.room_id_format == RoomIdFormatVersion::V2 - && incoming_event.room_id().is_some() - { - warn!("room create event incorrectly claims to have a room ID when it should not"); - return Ok(false); - } - - if !room_version.authorization.use_room_create_sender - && !room_version - .authorization - .explicitly_privilege_room_creators - { - // If content has no creator field, reject - if content.creator.is_none() { - warn!("m.room.create event incorrectly omits 'creator' field"); - return Ok(false); - } - } - - debug!("m.room.create event was allowed"); - return Ok(true); - } - - // NOTE(hydra): We always have a room ID from this point forward. - - /* - // TODO: In the past this code was commented as it caused problems with Synapse. This is no - // longer the case. This needs to be implemented. - // See also: https://github.com/ruma/ruma/pull/2064 - // - // 2. Reject if auth_events - // a. auth_events cannot have duplicate keys since it's a BTree - // b. All entries are valid auth events according to spec - let expected_auth = auth_types_for_event( - incoming_event.kind, - sender, - incoming_event.state_key, - incoming_event.content().clone(), - ); - - dbg!(&expected_auth); - - for ev_key in auth_events.keys() { - // (b) - if !expected_auth.contains(ev_key) { - warn!("auth_events contained invalid auth event"); - return Ok(false); - } - } - */ - - let (power_levels_event, sender_member_event) = join( - // fetch_state(&StateEventType::RoomCreate, ""), - fetch_state(&StateEventType::RoomPowerLevels, ""), - fetch_state(&StateEventType::RoomMember, sender.as_str()), - ) - .await; - - let room_create_event = create_event.clone(); - - // Get the content of the room create event, used later. - let room_create_content: RoomCreateContentFields = - from_json_str(room_create_event.content().get())?; - if room_create_content - .room_version - .is_some_and(|v| v.deserialize().is_err()) - { - warn!( - create_event_id = %room_create_event.event_id(), - "unsupported room version found in m.room.create event" - ); - return Ok(false); - } - let expected_room_id = room_create_event.room_id_or_hash(); - - if incoming_event.room_id().expect("event must have a room ID") != expected_room_id { - warn!( - expected = %expected_room_id, - received = %incoming_event.room_id().unwrap(), - "room_id of incoming event ({}) does not match that of the m.room.create event ({})", - incoming_event.room_id().unwrap(), - expected_room_id, - ); - return Ok(false); - } - - // If the create event is referenced in the event's auth events, and this is - // a v12 room, reject - let claims_create_event = incoming_event - .auth_events() - .any(|id| id == room_create_event.event_id()); - if room_version.room_id_format == RoomIdFormatVersion::V2 && claims_create_event { - warn!("event incorrectly references m.room.create event in auth events"); - return Ok(false); - } else if !(room_version.room_id_format == RoomIdFormatVersion::V2) && !claims_create_event { - // If the create event is not referenced in the event's auth events, and - // this is a v11 room, reject - warn!( - missing = %room_create_event.event_id(), - "event incorrectly did not reference an m.room.create in its auth events" - ); - return Ok(false); - } - - if let Some(ref pe) = power_levels_event { - if *pe.room_id().unwrap() != expected_room_id { - warn!( - expected = %expected_room_id, - received = %pe.room_id().unwrap(), - "room_id of referenced power levels event does not match that of the m.room.create event" - ); - return Ok(false); - } - } - - // If the create event content has the field m.federate set to false and the - // sender domain of the event does not match the sender domain of the create - // event, reject. - if !room_create_content.federate - && room_create_event.sender().server_name() != incoming_event.sender().server_name() - { - warn!( - sender = %incoming_event.sender(), - create_sender = %room_create_event.sender(), - "room is not federated and event's sender domain does not match create event's sender domain" - ); - return Ok(false); - } - - // Only in some room versions 6 and below - if room_version.authorization.special_case_room_aliases { - // 4. If type is m.room.aliases - if *incoming_event.event_type() == TimelineEventType::from("m.room.aliases") { - debug!("starting m.room.aliases check"); - - // If sender's domain doesn't matches state_key, reject - if incoming_event.state_key() != Some(sender.server_name().as_str()) { - warn!("state_key does not match sender"); - return Ok(false); - } - - debug!("m.room.aliases event was allowed"); - return Ok(true); - } - } - - // 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() { - | None => { - warn!("no state key in member event"); - 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, - fetch_state, - power_levels_event.as_ref(), - join_rules_event.as_ref(), - user_for_join_auth.as_deref(), - &user_for_join_auth_membership, - &room_create_event, - ) - .await? - { - 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 { - warn!( - ?sender_membership_event_content, - "Sender membership event content missing membership field" - ); - return Err(Error::InvalidPdu("Missing membership field".to_owned())); - }; - let membership_state = membership_state.deserialize()?; - - if !matches!(membership_state, MembershipState::Join) { - warn!( - %sender, - ?membership_state, - "sender cannot send events without being joined to the room" - ); - return Ok(false); - } - - let creator = if room_version.authorization.use_room_create_sender { - room_create_event.sender().to_owned() - } else { - room_create_content - .creator - .clone() - .ok_or_else(|| { - Error::InvalidPdu("Missing creator field in m.room.create event".to_owned()) - })? - .deserialize()? - }; - let power_levels: Option = power_levels_event - .as_ref() - .map(|pl| from_json_str(pl.content().get())) - .transpose()?; - let effective_power_levels = power_levels - .clone() - .unwrap_or_else(|| RoomPowerLevelsEventContent::new(&room_version.authorization)); - let sender_power_level = get_user_power_level( - room_version, - &creator, - room_create_content - .additional_creators - .as_deref() - .unwrap_or_default(), - power_levels.as_ref(), - sender, - ); - - // 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 { - if sender_power_level < effective_power_levels.invite { - warn!( - %sender, - has=?sender_power_level, - required=%effective_power_levels.invite, - "sender cannot send invites in this room" - ); - return Ok(false); - } - - debug!("m.room.third_party_invite event was allowed"); - 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) { - warn!( - %sender, - event_type=?incoming_event.kind(), - "sender cannot send 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 - .authorization - .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.authorization.special_case_room_redaction - && *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)? { - warn!( - %sender, - ?sender_power_level, - %redact_level, - "redaction event was not allowed" - ); - return Ok(false); - } - } - - debug!("allowing event passed all checks"); - Ok(true) -} - -// 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)] -async fn valid_membership_change( - room_version: &RoomVersionRules, - target_user: &UserId, - target_user_membership_event: Option<&E>, - sender: &UserId, - sender_membership_event: Option<&E>, - current_event: &E, - fetch_state: F, - 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, - F: Fn(&StateEventType, &str) -> Fut + Send + Copy, - Fut: Future> + Send, - 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 creator = if room_version.authorization.use_room_create_sender { - create_room.sender().to_owned() - } else { - create_content - .creator - .clone() - .ok_or_else(|| { - Error::InvalidPdu("Missing creator field in m.room.create event".to_owned()) - })? - .deserialize()? - }; - 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: Option = power_levels_event - .map(|ev| from_json_str(ev.content().get())) - .transpose()?; - let effective_power_levels: RoomPowerLevelsEventContent = power_levels - .clone() - .unwrap_or_else(|| RoomPowerLevelsEventContent::new(&room_version.authorization)); - - let sender_power = get_user_power_level( - room_version, - &creator, - create_content - .additional_creators - .as_deref() - .unwrap_or_default(), - power_levels.as_ref(), - sender, - ); - let target_power = get_user_power_level( - room_version, - &creator, - create_content - .additional_creators - .as_deref() - .unwrap_or_default(), - power_levels.as_ref(), - target_user, - ); - - 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 user_pl = get_user_power_level( - room_version, - &creator, - create_content - .additional_creators - .as_deref() - .unwrap_or_default(), - power_levels.as_ref(), - user_for_join_auth, - ); - - (user_pl, invite) - } else { - (UserPowerLevel::Int(int!(0)), int!(0)) - }; - let user_joined = user_for_join_auth_membership == &MembershipState::Join; - let okay_power = auth_user_pl >= invite_level; - user_joined && okay_power - } else { - // No auth user was given - trace!("No auth user given for join auth"); - false - }; - - 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 == create_room.event_id()); - let no_more_prev_events = prev_events.next().is_none(); - - if prev_event_is_create_event && no_more_prev_events { - if sender == 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.authorization.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.authorization.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 { - | Some(raw_tp_id) => - if target_user_current_membership == MembershipState::Ban { - warn!(?target_user_membership_event_id, "Can't invite banned user"); - false - } else { - let Ok(tp_id) = raw_tp_id.deserialize() else { - warn!("Invite third party content is invalid"); - return Ok(false); - }; - verify_third_party_invite(Some(target_user), sender, &tp_id, fetch_state) - .await - }, - | _ => - 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_power >= effective_power_levels.invite; - if !allow { - warn!( - %sender, - has=?sender_power, - required=?effective_power_levels.invite, - "sender does not have enough power to produce invites", - ); - } else { - trace!( - %sender, - ?sender_membership_event_id, - ?sender_membership, - ?target_user_membership_event_id, - ?target_user_current_membership, - sender_pl=?sender_power, - required_pl=?effective_power_levels.invite, - "allowing invite" - ); - } - allow - }, - } - }, - | MembershipState::Leave => { - if sender == target_user { - // If the sender matches state_key, allow if that user's current - // membership state is invite, join, or 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 { - // If the sender's current membership state is not join, reject. - warn!( - %sender, - ?sender_membership_event_id, - "sender cannot kick another user as they are not joined to the room", - ); - false - } else { - // If the target user's current membership state is ban, and the - // sender's power level is less than the ban level, reject. - // If the sender's power level is greater than or equal to - // the kick level, and the target user's power level is less - // than the sender's power level, allow. - let cannot_unban = target_user_current_membership == MembershipState::Ban - && sender_power < effective_power_levels.ban; - let can_kick = - sender_power >= effective_power_levels.kick && target_power < sender_power; - if cannot_unban { - 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 { - trace!(%sender, "allowing kick or unban"); - true - } else { - warn!( - %sender, - %target_user, - ?target_user_membership_event_id, - ?target_user_current_membership, - ?power_levels_event_id, - sender_pl=?sender_power, - target_pl=?target_power, - required_pl=?effective_power_levels.kick, - "sender does not have enough power to kick the target", - ); - false - } - } - }, - | MembershipState::Ban => - if !sender_is_joined { - // If the sender's current membership state is not join, reject. - warn!( - %sender, - ?sender_membership_event_id, - "sender cannot ban another user as they are not joined to the room", - ); - false - } else { - // If the sender's power level is greater than or equal to the - // ban level, and the target user's power level is less than - // the sender's power level, allow. Otherwise, reject. - let allow = - sender_power >= effective_power_levels.ban && 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.authorization.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.authorization.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 - } - }, - | _ => { - warn!( - %sender, - ?target_membership, - %target_user, - %target_user_current_membership, - "Unknown or invalid membership transition {} -> {}", - target_user_current_membership, - target_membership - ); - 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: UserPowerLevel, -) -> bool { - 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, - 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('@') && k != 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. -#[allow(clippy::cognitive_complexity)] -fn check_power_levels( - room_version: &RoomVersionRules, - power_event: &impl Event, - previous_power_event: Option<&impl Event>, - user_level: UserPowerLevel, - 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.map(|i| UserPowerLevel::Int(*i)) == 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.map(|i| UserPowerLevel::Int(*i)) > Some(user_level); - let new_level_too_big = new_level.map(|i| UserPowerLevel::Int(*i)) > 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 - } - } - - // 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.map(|i| UserPowerLevel::Int(*i)) > Some(user_level); - let new_level_too_big = new_level.map(|i| UserPowerLevel::Int(*i)) > 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.authorization.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: &RoomVersionRules, - redaction_event: &impl Event, - user_level: UserPowerLevel, - redact_level: Int, -) -> Result { - if user_level >= redact_level { - debug!("redaction allowed via power levels"); - return Ok(true); - } - - // 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); - } - - 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) }) -} - -async fn verify_third_party_invite( - target_user: Option<&UserId>, - sender: &UserId, - tp_id: &MemberThirdPartyInviteSignedContent, - fetch_state: F, -) -> bool -where - F: Fn(&StateEventType, &str) -> Fut + Send, - Fut: Future> + Send, - E: Event + Send + Sync, -{ - // If content.third_party_invite does not have a signed property, reject. - // ^ This would be a deserialization error before this function call, which - // equates to a rejection - let signed = &tp_id.signed; - // If signed does not have mxid and token properties, reject. - let Some(mxid) = signed.mxid() else { - warn!("Invite third party signed content is missing or has invalid `mxid` property"); - return false; - }; - let Some(token) = signed.token() else { - warn!("Invite third party signed content is missing or has invalid `token` property"); - return false; - }; - if let Err(e) = mxid.validate_historical() { - warn!(error=?e, ?mxid, "Invite third party signed content has invalid `mxid` property"); - return false; - } - // If mxid does not match state_key, reject. - // N.B. target_user is the state_key - if target_user.is_none_or(|u| u != mxid) { - warn!(?target_user, %mxid, "Invite third party mxid does not match membership state key"); - return false; - } - // If there is no m.room.third_party_invite event in the current room state - // with state_key matching token, reject. - let Some(current_third_party_invite) = - fetch_state(&StateEventType::RoomThirdPartyInvite, token).await - else { - warn!("No third party invite associated with the invite's third party invite found"); - return false; - }; - let Ok(tpi_content) = current_third_party_invite - .get_content::() - .inspect_err( - |e| warn!(error=?e, "Third party invite event content is invalid (cannot deserialize)"), - ) - else { - return false; - }; - if current_third_party_invite.state_key() != Some(token) { - warn!( - state_key=?current_third_party_invite.state_key(), - %token, - "Third party invite state key mismatch with signed invite token" - ); - return false; - } - // If sender does not match sender of the m.room.third_party_invite, reject. - if sender != current_third_party_invite.sender() { - warn!( - ?sender, - third_party_invite_sender=%current_third_party_invite.sender(), - "Third party invite sender does not match invite membership sender" - ); - return false; - } - // 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: - // - // A single public key in the public_key property. - // A list of public keys in the public_keys property. - - // NOTE: Technically the two below operations shouldn't be fallible at this - // point in execution, but if we can't de/serialize them now, we won't be - // able to in the future, so rejecting the event is the right way to go. - // We log an error though since this is unexpected. - let Ok(signed_json) = to_canonical_json_string_for_signing(&signed.0).inspect_err( - |e| error!(error=?e, "Canonical signed object cannot be cast to a signing string"), - ) else { - return false; - }; - let signatures = signed - .signatures() - .unwrap_or_default() // Empty sigs object will never pass so this is fine - .values() - .filter_map(|signatures| signatures.as_object()) - .flat_map(|signatures| { - signatures.iter().map(|(key, sig)| { - let key_id = SigningKeyId::::parse(key).ok()?; - base64::Base64::::parse(sig.as_str()?) - .map(|parsed| (key_id.algorithm(), parsed)) - .ok() - }) - }) - .flatten() - .collect::>(); - - for public_key_bytes in tpi_content - .public_key - .into_iter() - .chain( - tpi_content - .public_keys - .unwrap_or_default() - .iter() - .map(|pk| pk.public_key.clone()), - ) - .filter_map(|pk| pk.decode().ok()) - { - for (algorithm, signature) in &signatures { - if signatures::verify_canonical_json_bytes( - algorithm, - &public_key_bytes, - signature.as_bytes(), - signed_json.as_bytes(), - ) - .is_ok() - { - return true; - } - } - } - - warn!("Invite via third party was not signed by any associated public keys"); - false -} - -#[cfg(test)] -mod tests { - use ruma::{ - events::{ - StateEventType, TimelineEventType, - room::{ - join_rules::{AllowRule, JoinRule, Restricted, RoomJoinRulesEventContent}, - member::{MembershipState, RoomMemberEventContent}, - }, - }, - room::RoomMembership, - room_version_rules::RoomVersionRules, - }; - use serde_json::value::to_raw_value as to_raw_json_value; - - use crate::{ - matrix::{Event, EventTypeExt}, - state_res::{ - StateMap, - event_auth::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, - }, - }, - }; - - #[tokio::test] - async fn test_ban_pass() { - let _ = tracing::subscriber::set_default( - tracing_subscriber::fmt().with_test_writer().finish(), - ); - let events = INITIAL_EVENTS(); - - let auth_events = events - .values() - .map(|ev| (ev.event_type().with_state_key(ev.state_key().unwrap()), ev.clone())) - .collect::>(); - - let requester = to_pdu_event( - "HELLO", - alice(), - TimelineEventType::RoomMember, - Some(charlie().as_str()), - member_content_ban(), - &[], - &["IMC"], - ); - - let fetch_state = |ty, key| auth_events.get(&(ty, key)).cloned(); - let async_fetch_state = - |t: &StateEventType, k: &str| std::future::ready(fetch_state(t.to_owned(), k.into())); - let target_user = charlie(); - let sender = alice(); - - assert!( - valid_membership_change( - &RoomVersionRules::V6, - target_user, - fetch_state(StateEventType::RoomMember, target_user.as_str().into()).as_ref(), - sender, - fetch_state(StateEventType::RoomMember, sender.as_str().into()).as_ref(), - &requester, - &async_fetch_state, - fetch_state(StateEventType::RoomPowerLevels, "".into()).as_ref(), - fetch_state(StateEventType::RoomJoinRules, "".into()).as_ref(), - None, - &MembershipState::Leave, - &fetch_state(StateEventType::RoomCreate, "".into()).unwrap(), - ) - .await - .unwrap() - ); - } - - #[tokio::test] - async fn test_join_non_creator() { - let _ = tracing::subscriber::set_default( - tracing_subscriber::fmt().with_test_writer().finish(), - ); - let events = INITIAL_EVENTS_CREATE_ROOM(); - - let auth_events = events - .values() - .map(|ev| (ev.event_type().with_state_key(ev.state_key().unwrap()), ev.clone())) - .collect::>(); - - let requester = to_pdu_event( - "HELLO", - charlie(), - TimelineEventType::RoomMember, - Some(charlie().as_str()), - member_content_join(), - &["CREATE"], - &["CREATE"], - ); - - let fetch_state = |ty, key| auth_events.get(&(ty, key)).cloned(); - let async_fetch_state = - |t: &StateEventType, k: &str| std::future::ready(fetch_state(t.to_owned(), k.into())); - let target_user = charlie(); - let sender = charlie(); - - assert!( - !valid_membership_change( - &RoomVersionRules::V6, - target_user, - fetch_state(StateEventType::RoomMember, target_user.as_str().into()).as_ref(), - sender, - fetch_state(StateEventType::RoomMember, sender.as_str().into()).as_ref(), - &requester, - &async_fetch_state, - fetch_state(StateEventType::RoomPowerLevels, "".into()).as_ref(), - fetch_state(StateEventType::RoomJoinRules, "".into()).as_ref(), - None, - &MembershipState::Leave, - &fetch_state(StateEventType::RoomCreate, "".into()).unwrap(), - ) - .await - .unwrap() - ); - } - - #[tokio::test] - async fn test_join_creator() { - let _ = tracing::subscriber::set_default( - tracing_subscriber::fmt().with_test_writer().finish(), - ); - let events = INITIAL_EVENTS_CREATE_ROOM(); - - let auth_events = events - .values() - .map(|ev| (ev.event_type().with_state_key(ev.state_key().unwrap()), ev.clone())) - .collect::>(); - - let requester = to_pdu_event( - "HELLO", - alice(), - TimelineEventType::RoomMember, - Some(alice().as_str()), - member_content_join(), - &["CREATE"], - &["CREATE"], - ); - - let fetch_state = |ty, key| auth_events.get(&(ty, key)).cloned(); - let async_fetch_state = - |t: &StateEventType, k: &str| std::future::ready(fetch_state(t.to_owned(), k.into())); - let target_user = alice(); - let sender = alice(); - - assert!( - valid_membership_change( - &RoomVersionRules::V6, - target_user, - fetch_state(StateEventType::RoomMember, target_user.as_str().into()).as_ref(), - sender, - fetch_state(StateEventType::RoomMember, sender.as_str().into()).as_ref(), - &requester, - &async_fetch_state, - fetch_state(StateEventType::RoomPowerLevels, "".into()).as_ref(), - fetch_state(StateEventType::RoomJoinRules, "".into()).as_ref(), - None, - &MembershipState::Leave, - &fetch_state(StateEventType::RoomCreate, "".into()).unwrap(), - ) - .await - .unwrap() - ); - } - - #[tokio::test] - async fn test_ban_fail() { - let _ = tracing::subscriber::set_default( - tracing_subscriber::fmt().with_test_writer().finish(), - ); - let events = INITIAL_EVENTS(); - - let auth_events = events - .values() - .map(|ev| (ev.event_type().with_state_key(ev.state_key().unwrap()), ev.clone())) - .collect::>(); - - let requester = to_pdu_event( - "HELLO", - charlie(), - TimelineEventType::RoomMember, - Some(alice().as_str()), - member_content_ban(), - &[], - &["IMC"], - ); - - let fetch_state = |ty, key| auth_events.get(&(ty, key)).cloned(); - let async_fetch_state = - |t: &StateEventType, k: &str| std::future::ready(fetch_state(t.to_owned(), k.into())); - let target_user = alice(); - let sender = charlie(); - - assert!( - !valid_membership_change( - &RoomVersionRules::V6, - target_user, - fetch_state(StateEventType::RoomMember, target_user.as_str().into()).as_ref(), - sender, - fetch_state(StateEventType::RoomMember, sender.as_str().into()).as_ref(), - &requester, - &async_fetch_state, - fetch_state(StateEventType::RoomPowerLevels, "".into()).as_ref(), - fetch_state(StateEventType::RoomJoinRules, "".into()).as_ref(), - None, - &MembershipState::Leave, - &fetch_state(StateEventType::RoomCreate, "".into()).unwrap(), - ) - .await - .unwrap() - ); - } - - #[tokio::test] - async fn test_restricted_join_rule() { - let _ = tracing::subscriber::set_default( - tracing_subscriber::fmt().with_test_writer().finish(), - ); - let mut events = INITIAL_EVENTS(); - *events.get_mut(&event_id("IJR")).unwrap() = to_pdu_event( - "IJR", - alice(), - TimelineEventType::RoomJoinRules, - Some(""), - to_raw_json_value(&RoomJoinRulesEventContent::new(JoinRule::Restricted( - Restricted::new(vec![AllowRule::RoomMembership(RoomMembership::new( - room_id().to_owned(), - ))]), - ))) - .unwrap(), - &["CREATE", "IMA", "IPOWER"], - &["IPOWER"], - ); - - let mut member = RoomMemberEventContent::new(MembershipState::Join); - member.join_authorized_via_users_server = Some(alice().to_owned()); - - let auth_events = events - .values() - .map(|ev| (ev.event_type().with_state_key(ev.state_key().unwrap()), ev.clone())) - .collect::>(); - - let requester = to_pdu_event( - "HELLO", - ella(), - TimelineEventType::RoomMember, - Some(ella().as_str()), - to_raw_json_value(&RoomMemberEventContent::new(MembershipState::Join)).unwrap(), - &["CREATE", "IJR", "IPOWER", "new"], - &["new"], - ); - - let fetch_state = |ty, key| auth_events.get(&(ty, key)).cloned(); - let async_fetch_state = - |t: &StateEventType, k: &str| std::future::ready(fetch_state(t.to_owned(), k.into())); - let target_user = ella(); - let sender = ella(); - - assert!( - valid_membership_change( - &RoomVersionRules::V9, - target_user, - fetch_state(StateEventType::RoomMember, target_user.as_str().into()).as_ref(), - sender, - fetch_state(StateEventType::RoomMember, sender.as_str().into()).as_ref(), - &requester, - &async_fetch_state, - fetch_state(StateEventType::RoomPowerLevels, "".into()).as_ref(), - fetch_state(StateEventType::RoomJoinRules, "".into()).as_ref(), - Some(alice()), - &MembershipState::Join, - &fetch_state(StateEventType::RoomCreate, "".into()).unwrap(), - ) - .await - .unwrap() - ); - - assert!( - !valid_membership_change( - &RoomVersionRules::V9, - target_user, - fetch_state(StateEventType::RoomMember, target_user.as_str().into()).as_ref(), - sender, - fetch_state(StateEventType::RoomMember, sender.as_str().into()).as_ref(), - &requester, - &async_fetch_state, - fetch_state(StateEventType::RoomPowerLevels, "".into()).as_ref(), - fetch_state(StateEventType::RoomJoinRules, "".into()).as_ref(), - Some(ella()), - &MembershipState::Leave, - &fetch_state(StateEventType::RoomCreate, "".into()).unwrap(), - ) - .await - .unwrap() - ); - } - - #[tokio::test] - async fn test_knock() { - let _ = tracing::subscriber::set_default( - tracing_subscriber::fmt().with_test_writer().finish(), - ); - let mut events = INITIAL_EVENTS(); - *events.get_mut(&event_id("IJR")).unwrap() = to_pdu_event( - "IJR", - alice(), - TimelineEventType::RoomJoinRules, - Some(""), - to_raw_json_value(&RoomJoinRulesEventContent::new(JoinRule::Knock)).unwrap(), - &["CREATE", "IMA", "IPOWER"], - &["IPOWER"], - ); - - let auth_events = events - .values() - .map(|ev| (ev.event_type().with_state_key(ev.state_key().unwrap()), ev.clone())) - .collect::>(); - - let requester = to_pdu_event( - "HELLO", - ella(), - TimelineEventType::RoomMember, - Some(ella().as_str()), - to_raw_json_value(&RoomMemberEventContent::new(MembershipState::Knock)).unwrap(), - &[], - &["IMC"], - ); - - let fetch_state = |ty, key| auth_events.get(&(ty, key)).cloned(); - let async_fetch_state = - |t: &StateEventType, k: &str| std::future::ready(fetch_state(t.to_owned(), k.into())); - let target_user = ella(); - let sender = ella(); - - assert!( - valid_membership_change( - &RoomVersionRules::V7, - target_user, - fetch_state(StateEventType::RoomMember, target_user.as_str().into()).as_ref(), - sender, - fetch_state(StateEventType::RoomMember, sender.as_str().into()).as_ref(), - &requester, - &async_fetch_state, - fetch_state(StateEventType::RoomPowerLevels, "".into()).as_ref(), - fetch_state(StateEventType::RoomJoinRules, "".into()).as_ref(), - None, - &MembershipState::Leave, - &fetch_state(StateEventType::RoomCreate, "".into()).unwrap(), - ) - .await - .unwrap() - ); - } -} diff --git a/src/core/matrix/state_res/mod.rs b/src/core/matrix/state_res/mod.rs deleted file mode 100644 index 96a953ade..000000000 --- a/src/core/matrix/state_res/mod.rs +++ /dev/null @@ -1,2052 +0,0 @@ -#![cfg_attr(test, allow(warnings))] - -pub(crate) mod error; -pub mod event_auth; -mod power_levels; -mod serde_backports; - -#[cfg(test)] -mod test_utils; - -#[cfg(test)] -mod benches; - -use std::{ - borrow::Borrow, - cmp::{Ordering, Reverse}, - collections::{BinaryHeap, HashMap, HashSet}, - hash::{BuildHasher, Hash}, -}; - -use futures::{Future, FutureExt, Stream, StreamExt, TryFutureExt, TryStreamExt, future}; -use ruma::{ - EventId, Int, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedUserId, - events::{ - StateEventType, TimelineEventType, - room::member::{MembershipState, RoomMemberEventContent}, - }, - int, - room_version_rules::{RoomIdFormatVersion, RoomVersionRules, StateResolutionVersion}, - serde::Raw, -}; -use serde::Deserialize; -use serde_json::from_str as from_json_str; - -pub(crate) use self::error::Error; -pub use self::event_auth::{auth_check, auth_types_for_event}; -use self::power_levels::PowerLevelsContentFields; -use crate::{ - debug, debug_error, err, - matrix::{Event, StateKey}, - trace, - utils::stream::{BroadbandExt, IterStream, ReadyExt, TryWidebandExt, WidebandExt}, - warn, -}; - -/// A mapping of event type and state_key to some value `T`, usually an -/// `EventId`. -pub type StateMap = HashMap; -pub type StateMapItem = (TypeStateKey, T); -pub type TypeStateKey = (StateEventType, StateKey); - -type Result = crate::Result; - -/// Resolve sets of state events as they come in. -/// -/// Internally `StateResolution` builds a graph and an auth chain to allow for -/// state conflict resolution. -/// -/// ## Arguments -/// -/// * `state_sets` - The incoming state to resolve. Each `StateMap` represents a -/// possible fork in the state of a room. -/// -/// * `auth_chain_sets` - The full recursive set of `auth_events` for each event -/// in the `state_sets`. -/// -/// * `event_fetch` - Any event not found in the `event_map` will defer to this -/// closure to find the event. -/// -/// ## Invariants -/// -/// The caller of `resolve` must ensure that all the events are from the same -/// room. Although this function takes a `RoomId` it does not check that each -/// event is part of the same room. -//#[tracing::instrument(level = "debug", skip(state_sets, auth_chain_sets, -//#[tracing::instrument(level event_fetch))] -#[allow(clippy::cognitive_complexity)] -pub async fn resolve<'a, Pdu, Sets, SetIter, Hasher, Fetch, FetchFut, Exists, ExistsFut>( - room_version: &RoomVersionRules, - state_sets: Sets, - auth_chain_sets: &'a [HashSet], - event_fetch: &Fetch, - event_exists: &Exists, -) -> Result> -where - Fetch: Fn(OwnedEventId) -> FetchFut + Sync, - FetchFut: Future> + Send, - Exists: Fn(OwnedEventId) -> ExistsFut + Sync, - ExistsFut: Future + Send, - Sets: IntoIterator + Send, - SetIter: Iterator> + Clone + Send, - Hasher: BuildHasher + Send + Sync, - Pdu: Event + Clone + Send + Sync, - for<'b> &'b Pdu: Event + Send, -{ - let stateres_version = room_version.state_res; - debug!(version = ?stateres_version, "State resolution starting"); - - // Split non-conflicting and conflicting state - let (unconflicted, conflicting) = separate(state_sets.into_iter()); - - debug!(count = unconflicted.len(), "non-conflicting events"); - trace!(map = ?unconflicted, "non-conflicting events"); - - if conflicting.is_empty() { - debug!("no conflicting state found"); - return Ok(unconflicted); - } - - debug!(count = conflicting.len(), "conflicting events"); - trace!(map = ?conflicting, "conflicting events"); - let (conflicted_state_subgraph, initial_state) = if let StateResolutionVersion::V2(v2_rules) = - stateres_version - && v2_rules.consider_conflicted_state_subgraph - { - let csg = calculate_conflicted_subgraph(&conflicting, event_fetch) - .await - .ok_or_else(|| { - Error::InvalidPdu("Failed to calculate conflicted subgraph".to_owned()) - })?; - debug!(count = csg.len(), "conflicted subgraph"); - trace!(set = ?csg, "conflicted subgraph"); - (csg, HashMap::new()) - } else { - (HashSet::new(), unconflicted.clone()) - }; - - // `all_conflicted` contains unique items - // synapse says `full_set = {eid for eid in full_conflicted_set if eid in - // event_map}` - // Hydra: Also consider the conflicted state subgraph - let all_conflicted: HashSet<_> = get_auth_chain_diff(auth_chain_sets) - .chain(conflicting.into_values().flatten().stream()) - .broad_filter_map(async |id| event_exists(id.clone()).await.then_some(id)) - .chain(conflicted_state_subgraph.into_iter().stream()) - .collect() - .await; - - debug!(count = all_conflicted.len(), "full conflicted set"); - trace!(set = ?all_conflicted, "full conflicted set"); - - // We used to check that all events are events from the correct room - // this is now a check the caller of `resolve` must make. - - // Get only the control events with a state_key: "" or ban/kick event - // (sender != state_key) - let control_events: Vec<_> = all_conflicted - .iter() - .stream() - .wide_filter_map(async |id| { - is_power_event_id(id, &event_fetch) - .await - .then_some(id.clone()) - }) - .collect() - .await; - - // Sort the control events based on power_level/clock/event_id and - // outgoing/incoming edges - let sorted_control_levels = reverse_topological_power_sort( - control_events, - &all_conflicted, - room_version, - &event_fetch, - ) - .await?; - - debug!(count = sorted_control_levels.len(), "power events"); - trace!(list = ?sorted_control_levels, "sorted power events"); - - // Sequentially auth check each control event. - let resolved_control = iterative_auth_check( - room_version, - sorted_control_levels.iter().stream().map(AsRef::as_ref), - initial_state, - &event_fetch, - ) - .await?; - - debug!(count = resolved_control.len(), "resolved power events"); - trace!(map = ?resolved_control, "resolved power events"); - - // At this point the control_events have been resolved we now have to - // sort the remaining events using the mainline of the resolved power level. - let deduped_power_ev: HashSet<_> = sorted_control_levels.into_iter().collect(); - - debug!(count = deduped_power_ev.len(), "deduped power events"); - trace!(set = ?deduped_power_ev, "deduped power events"); - - // This removes the control events that passed auth and more importantly - // those that failed auth - let events_to_resolve: Vec<_> = all_conflicted - .iter() - .filter(|&id| !deduped_power_ev.contains(id)) - .cloned() - .collect(); - - debug!(count = events_to_resolve.len(), "events left to resolve"); - trace!(list = ?events_to_resolve, "events left to resolve"); - - // This "epochs" power level event - let power_levels_ty_sk = (StateEventType::RoomPowerLevels, StateKey::new()); - let power_event = resolved_control.get(&power_levels_ty_sk); - - trace!(event_id = ?power_event, "power event"); - - let sorted_left_events = - mainline_sort(&events_to_resolve, power_event.cloned(), &event_fetch).await?; - - trace!(list = ?sorted_left_events, "events left, sorted, running iterative auth check"); - - let mut resolved_state = iterative_auth_check( - room_version, - sorted_left_events.iter().stream().map(AsRef::as_ref), - resolved_control, // The control events are added to the final resolved state - &event_fetch, - ) - .await?; - - // Ensure unconflicting state is in the final state - resolved_state.extend(unconflicted); - - debug!("state resolution finished"); - trace!( map = ?resolved_state, "final resolved state" ); - - Ok(resolved_state) -} - -/// Split the events that have no conflicts from those that are conflicting. -/// -/// The return tuple looks like `(unconflicted, conflicted)`. -/// -/// State is determined to be conflicting if for the given key (StateEventType, -/// StateKey) there is not exactly one event ID. This includes missing events, -/// if one state_set includes an event that none of the other have this is a -/// conflicting event. -fn separate<'a, Id>( - state_sets_iter: impl Iterator>, -) -> (StateMap, StateMap>) -where - Id: Clone + Eq + Hash + 'a, -{ - let mut state_set_count: usize = 0; - let mut occurrences = HashMap::<_, HashMap<_, _>>::new(); - - let state_sets_iter = - state_sets_iter.inspect(|_| state_set_count = state_set_count.saturating_add(1)); - - for (k, v) in state_sets_iter.flatten() { - occurrences - .entry(k) - .or_default() - .entry(v) - .and_modify(|x: &mut usize| *x = x.saturating_add(1)) - .or_insert(1); - } - - let mut unconflicted_state = StateMap::new(); - let mut conflicted_state = StateMap::new(); - - for (k, v) in occurrences { - for (id, occurrence_count) in v { - if occurrence_count == state_set_count { - unconflicted_state.insert((k.0.clone(), k.1.clone()), id.clone()); - } else { - conflicted_state - .entry((k.0.clone(), k.1.clone())) - .and_modify(|x: &mut Vec<_>| x.push(id.clone())) - .or_insert_with(|| vec![id.clone()]); - } - } - } - - (unconflicted_state, conflicted_state) -} - -/// Calculate the conflicted subgraph -async fn calculate_conflicted_subgraph( - conflicted: &StateMap>, - fetch_event: &F, -) -> Option> -where - F: Fn(OwnedEventId) -> Fut + Sync, - Fut: Future> + Send, - E: Event + Send + Sync, -{ - let conflicted_events: HashSet<_> = conflicted.values().flatten().cloned().collect(); - let mut subgraph: HashSet = HashSet::new(); - let mut stack: Vec> = - vec![conflicted_events.iter().cloned().collect::>()]; - let mut path: Vec = Vec::new(); - let mut seen: HashSet = HashSet::new(); - let next_event = |stack: &mut Vec>, path: &mut Vec<_>| { - while stack.last().is_some_and(Vec::is_empty) { - stack.pop(); - path.pop(); - } - stack.last_mut().and_then(Vec::pop) - }; - while let Some(event_id) = next_event(&mut stack, &mut path) { - path.push(event_id.clone()); - if subgraph.contains(&event_id) { - if path.len() > 1 { - subgraph.extend(path.iter().cloned()); - } - path.pop(); - continue; - } - if conflicted_events.contains(&event_id) && path.len() > 1 { - subgraph.extend(path.iter().cloned()); - } - if seen.contains(&event_id) { - path.pop(); - continue; - } - trace!(event_id = event_id.as_str(), "fetching event for its auth events"); - let evt = fetch_event(event_id.clone()).await; - if evt.is_none() { - err!("could not fetch event {} to calculate conflicted subgraph", event_id); - path.pop(); - continue; - } - stack.push( - evt.expect("checked") - .auth_events() - .map(ToOwned::to_owned) - .collect(), - ); - seen.insert(event_id); - } - Some(subgraph) -} - -/// Returns a Vec of deduped EventIds that appear in some chains but not others. -#[allow(clippy::arithmetic_side_effects)] -fn get_auth_chain_diff( - auth_chain_sets: &[HashSet], -) -> impl Stream + Send + use -where - Id: Clone + Eq + Hash + Send, - Hasher: BuildHasher + Send + Sync, -{ - let num_sets = auth_chain_sets.len(); - let mut id_counts: HashMap = HashMap::new(); - for id in auth_chain_sets.iter().flatten() { - *id_counts.entry(id.clone()).or_default() += 1; - } - - id_counts - .into_iter() - .filter_map(move |(id, count)| (count < num_sets).then_some(id)) - .stream() -} - -/// Events are sorted from "earliest" to "latest". -/// -/// They are compared using the negative power level (reverse topological -/// ordering), the origin server timestamp and in case of a tie the `EventId`s -/// are compared lexicographically. -/// -/// 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( - events_to_sort: Vec, - auth_diff: &HashSet, - room_version: &RoomVersionRules, - fetch_event: &F, -) -> Result> -where - F: Fn(OwnedEventId) -> Fut + Sync, - Fut: Future> + Send, - E: Event + Send + Sync, -{ - debug!("reverse topological sort of power events"); - - let mut graph = HashMap::new(); - for event_id in events_to_sort { - add_event_and_auth_chain_to_graph(&mut graph, event_id, auth_diff, fetch_event).await; - } - - // This is used in the `key_fn` passed to the lexico_topo_sort fn - let event_to_pl: HashMap<_, _> = graph - .keys() - .cloned() - .stream() - .broad_filter_map(async |event_id| { - let pl = get_power_level_for_sender(&event_id, room_version, fetch_event) - .await - .ok()?; - - Some((event_id, pl)) - }) - .inspect(|(event_id, pl)| { - debug!( - event_id = event_id.as_str(), - power_level = i64::from(*pl), - "found the power level of an event's sender", - ); - }) - .collect() - .boxed() - .await; - - let fetcher = async |event_id: OwnedEventId| { - let pl = *event_to_pl - .get(&event_id) - .ok_or_else(|| Error::NotFound(String::new()))?; - - let ev = fetch_event(event_id) - .await - .ok_or_else(|| Error::NotFound(String::new()))?; - - Ok((pl, ev.origin_server_ts())) - }; - - lexicographical_topological_sort(&graph, &fetcher).await -} - -/// Sorts the event graph based on number of outgoing/incoming edges. -/// -/// `key_fn` is used as to obtain the power level and age of an event for -/// breaking ties (together with the event ID). -#[tracing::instrument(level = "debug", skip_all)] -pub async fn lexicographical_topological_sort( - graph: &HashMap, S>, - key_fn: &F, -) -> Result> -where - F: Fn(Id) -> Fut + Sync, - Fut: Future> + Send, - Id: Borrow + Clone + Eq + Hash + Ord + Send + Sync, - Hasher: BuildHasher + Default + Clone + Send + Sync, - S: BuildHasher + Clone + Send + Sync, -{ - #[derive(PartialEq, Eq)] - struct TieBreaker<'a, Id> { - power_level: Int, - origin_server_ts: MilliSecondsSinceUnixEpoch, - event_id: &'a Id, - } - - impl Ord for TieBreaker<'_, Id> - where - Id: Ord, - { - fn cmp(&self, other: &Self) -> Ordering { - // NOTE: the power level comparison is "backwards" intentionally. - // See the "Mainline ordering" section of the Matrix specification - // around where it says the following: - // - // > for events `x` and `y`, `x < y` if [...] - // - // - other - .power_level - .cmp(&self.power_level) - .then(self.origin_server_ts.cmp(&other.origin_server_ts)) - .then(self.event_id.cmp(other.event_id)) - } - } - - impl PartialOrd for TieBreaker<'_, Id> - where - Id: Ord, - { - fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } - } - - debug!("starting lexicographical topological sort"); - - // NOTE: an event that has no incoming edges happened most recently, - // and an event that has no outgoing edges happened least recently. - - // NOTE: this is basically Kahn's algorithm except we look at nodes with no - // outgoing edges, c.f. - // https://en.wikipedia.org/wiki/Topological_sorting#Kahn's_algorithm - - // outdegree_map is an event referring to the events before it, the - // more outdegree's the more recent the event. - let mut outdegree_map = graph.clone(); - - // The number of events that depend on the given event (the EventId key) - // How many events reference this event in the DAG as a parent - let mut reverse_graph: HashMap<_, HashSet<_, Hasher>> = HashMap::new(); - - // Vec of nodes that have zero out degree, least recent events. - let mut zero_outdegree = Vec::new(); - - for (node, edges) in graph { - if edges.is_empty() { - let (power_level, origin_server_ts) = key_fn(node.clone()).await?; - // The `Reverse` is because rusts `BinaryHeap` sorts largest -> - // smallest we need smallest -> largest - zero_outdegree.push(Reverse(TieBreaker { - power_level, - origin_server_ts, - event_id: node, - })); - } - - reverse_graph.entry(node).or_default(); - for edge in edges { - reverse_graph.entry(edge).or_default().insert(node); - } - } - - let mut heap = BinaryHeap::from(zero_outdegree); - - // We remove the oldest node (most incoming edges) and check against all - // other - let mut sorted = vec![]; - // Destructure the `Reverse` and take the smallest `node` each time - while let Some(Reverse(item)) = heap.pop() { - let node = item.event_id; - - for &parent in reverse_graph - .get(node) - .expect("EventId in heap is also in reverse_graph") - { - // The number of outgoing edges this node has - let out = outdegree_map - .get_mut(parent.borrow()) - .expect("outdegree_map knows of all referenced EventIds"); - - // Only push on the heap once older events have been cleared - out.remove(node.borrow()); - if out.is_empty() { - let (power_level, origin_server_ts) = key_fn(parent.clone()).await?; - heap.push(Reverse(TieBreaker { - power_level, - origin_server_ts, - event_id: parent, - })); - } - } - - // synapse yields we push then return the vec - sorted.push(node.clone()); - } - - Ok(sorted) -} - -/// Find the power level for the sender of `event_id`, falling back to the -/// room creator's power and then to `users_default`. -/// -/// 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, - room_version: &RoomVersionRules, - fetch_event: &F, -) -> serde_json::Result -where - F: Fn(OwnedEventId) -> Fut + Sync, - Fut: Future> + Send, - E: Event + Send + Sync, -{ - debug!("fetch event ({event_id}) senders power level"); - - let Some(event) = fetch_event(event_id.to_owned()).await else { - return Ok(int!(0)); - }; - - let auth_events: Vec = event - .auth_events() - .map(ToOwned::to_owned) - .stream() - .broadn_filter_map(5, fetch_event) - .collect() - .await; - - let sender_is_creator = - sender_is_creator(&event, room_version, &auth_events, fetch_event).await?; - - // Since v12 the room creators outrank any m.room.power_levels event, so - // their ordering position does not depend on one being in scope. - if sender_is_creator - && room_version - .authorization - .explicitly_privilege_room_creators - { - return Ok(Int::MAX); - } - - let pl = auth_events - .iter() - .find(|aev| is_type_and_key(*aev, &TimelineEventType::RoomPowerLevels, "")); - - let content: PowerLevelsContentFields = match pl { - // With no m.room.power_levels in scope the create event's defaults - // apply, under which the room creator holds 100 and everyone else 0. - | None => return Ok(if sender_is_creator { int!(100) } else { int!(0) }), - | Some(ev) => from_json_str(ev.content().get())?, - }; - - if let Some(&user_level) = content.get_user_power(event.sender()) { - debug!("found {} at power_level {user_level}", event.sender()); - return Ok(user_level); - } - - Ok(content.users_default) -} - -#[derive(Deserialize)] -struct RoomCreateContentFields { - creator: Option>, - additional_creators: Option>>, -} - -/// Whether the sender of `event` created the room, either by sending the -/// create event, by being named as the creator in it (room versions 1-10) or -/// by being named in its `additional_creators`. -async fn sender_is_creator( - event: &E, - room_version: &RoomVersionRules, - auth_events: &[E], - fetch_event: &F, -) -> serde_json::Result -where - F: Fn(OwnedEventId) -> Fut + Sync, - Fut: Future> + Send, - E: Event + Send + Sync, -{ - let create_event = auth_events - .iter() - .find(|aev| is_type_and_key(*aev, &TimelineEventType::RoomCreate, "")) - .cloned(); - - // Since v12 the create event is no longer listed in auth_events; the room - // ID is its event ID. - let create_event = match create_event { - | Some(create_event) => Some(create_event), - | None => - match OwnedEventId::try_from(event.room_id_or_hash().as_str().replace('!', "$")) { - | Ok(create_event_id) => fetch_event(create_event_id).await, - | Err(_) => None, - }, - }; - - let Some(create_event) = create_event else { - return Ok(false); - }; - - let content: RoomCreateContentFields = from_json_str(create_event.content().get())?; - - // Before v11 the creator is recorded in the create event's content and can - // differ from its sender. - let is_creator = if room_version.authorization.use_room_create_sender { - create_event.sender() == event.sender() - } else { - content - .creator - .iter() - .filter_map(|creator| creator.deserialize().ok()) - .any(|creator| creator == event.sender()) - }; - - if is_creator { - return Ok(true); - } - - if !room_version - .authorization - .explicitly_privilege_room_creators - { - return Ok(false); - } - - Ok(content - .additional_creators - .iter() - .flatten() - .filter_map(|creator| creator.deserialize().ok()) - .any(|creator| creator == event.sender())) -} - -/// Check the that each event is authenticated based on the events before it. -/// -/// ## Returns -/// -/// The `unconflicted_state` combined with the newly auth'ed events. So any -/// event that fails the `event_auth::auth_check` will be excluded from the -/// returned state map. -/// -/// For each `events_to_check` event we gather the events needed to auth it from -/// 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>( - room_version: &RoomVersionRules, - events_to_check: S, - unconflicted_state: StateMap, - fetch_event: &F, -) -> 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, -{ - debug!("starting iterative auth check"); - - let events_to_check: Vec<_> = events_to_check - .map(Result::Ok) - .wide_and_then(async |event_id| { - fetch_event(event_id.to_owned()) - .await - .ok_or_else(|| Error::NotFound(format!("Failed to find {event_id}"))) - }) - .try_collect() - .boxed() - .await?; - trace!(list = ?events_to_check, "events to check"); - if events_to_check.is_empty() { - debug!("no events to check, returning unconflicted state"); - return Ok(unconflicted_state); - } - - let auth_event_ids: HashSet = events_to_check - .iter() - .flat_map(|event: &E| 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 - .into_iter() - .stream() - .broad_filter_map(fetch_event) - .map(|auth_event| (auth_event.event_id().to_owned(), auth_event)) - .collect() - .boxed() - .await; - - trace!(map = ?auth_events.keys().collect::>(), "fetched auth events"); - - let auth_events = &auth_events; - // NOTE: in state resolution v2.1, auth checks should start with an empty - // state map. It is the caller's job to do this. Previously, this function - // would force an empty state map in this case, and this resulted in power - // events going missing from the resolved state as they'd be discarded - // here. - let mut resolved_state = unconflicted_state; - for event in events_to_check { - trace!(event_id = event.event_id().as_str(), "checking event"); - let state_key = event - .state_key() - .ok_or_else(|| Error::InvalidPdu("State event had no state key".to_owned()))?; - - let auth_types = auth_types_for_event( - event.event_type(), - event.sender(), - Some(state_key), - event.content(), - room_version, - ) - .map_err(|e| Error::InvalidPdu(e.to_string()))?; - 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_id_format == RoomIdFormatVersion::V2 { - 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) - .await - .ok_or_else(|| Error::NotFound("Failed to find create event".into()))?; - auth_state.insert(create_event.event_type().with_state_key(""), create_event); - } - 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 soft-failing - trace!(event_id = aid.as_str(), "found auth event"); - auth_state.insert( - ev.event_type() - .with_state_key(ev.state_key().ok_or_else(|| { - Error::InvalidPdu("State event had no state key".to_owned()) - })?), - ev.clone(), - ); - } else { - warn!(event_id = aid.as_str(), "missing auth event"); - } - } - - auth_types - .iter() - .stream() - .ready_filter_map(|key| Some((key, resolved_state.get(key)?))) - .filter_map(|(key, ev_id)| async move { - if let Some(event) = auth_events.get(ev_id) { - Some((key, event.clone())) - } else { - Some((key, fetch_event(ev_id.clone()).await?)) - } - }) - .ready_for_each(|(key, event)| { - //TODO: synapse checks "rejected_reason" is None here - auth_state.insert(key.to_owned(), event); - }) - .await; - trace!(map = ?auth_state.keys().collect::>(), event_id = event.event_id().as_str(), "auth state for event"); - - if *event.event_type() == StateEventType::RoomCreate.into() - && event.state_key().is_some_and(str::is_empty) - { - // Inject the create event so that the call to auth_check doesn't - // explode. It's not used otherwise. - auth_state - .entry(StateEventType::RoomCreate.with_state_key("")) - .or_insert_with(|| event.clone()); - } else { - // If this is not a create event, we must have the room create event - // in the auth state by now. - if !auth_state.contains_key(&StateEventType::RoomCreate.with_state_key("")) { - return Err(Error::NotFound(format!( - "m.room.create did not appear in the auth state for {}", - event.event_id() - ))); - } - } - - debug!(event_id = event.event_id().as_str(), "Running auth checks"); - - let fetch_state = |ty: &StateEventType, key: &str| { - future::ready( - auth_state - .get(&ty.with_state_key(key)) - .map(ToOwned::to_owned), - ) - }; - - let auth_result = auth_check( - room_version, - &event, - fetch_state, - &fetch_state(&StateEventType::RoomCreate, "") - .await - .expect("create event must exist"), - ) - .await; - - match auth_result { - | Ok(true) => { - // add event to resolved state map - trace!( - event_id = event.event_id().as_str(), - "event passed the authentication check, adding to resolved state" - ); - resolved_state.insert( - event.event_type().with_state_key(state_key), - event.event_id().to_owned(), - ); - trace!(map = ?resolved_state, "new resolved state"); - }, - | Ok(false) => { - // synapse passes here on AuthError. We do not add this event to - // resolved_state. - warn!("event {} failed the authentication check", event.event_id()); - }, - | Err(e) => { - debug_error!("event {} failed the authentication check: {e}", event.event_id()); - return Err(e); - }, - } - } - trace!(map = ?resolved_state, "final resolved state from iterative auth check"); - debug!("iterative auth check finished"); - Ok(resolved_state) -} - -/// Returns the sorted `to_sort` list of `EventId`s based on a mainline sort -/// using the depth of `resolved_power_level`, the server timestamp, and the -/// eventId. -/// -/// The depth of the given event is calculated based on the depth of it's -/// closest "parent" power_level event. If there have been two power events the -/// 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( - to_sort: &[OwnedEventId], - resolved_power_level: Option, - fetch_event: &F, -) -> Result> -where - F: Fn(OwnedEventId) -> Fut + Sync, - Fut: Future> + Send, - E: Event + Clone + Send + Sync, -{ - debug!("mainline sort of events"); - - // There are no EventId's to sort, bail. - if to_sort.is_empty() { - return Ok(vec![]); - } - - let mut mainline = vec![]; - let mut pl = resolved_power_level; - while let Some(p) = pl { - mainline.push(p.clone()); - - let event = fetch_event(p.clone()) - .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 - .ok_or_else(|| Error::NotFound(format!("Failed to find {aid}")))?; - - if is_type_and_key(&ev, &TimelineEventType::RoomPowerLevels, "") { - pl = Some(aid.to_owned()); - break; - } - } - } - - let mainline_map: HashMap<_, _> = mainline - .iter() - .rev() - .enumerate() - .map(|(idx, eid)| ((*eid).clone(), idx)) - .collect(); - - let order_map: HashMap<_, _> = to_sort - .iter() - .stream() - .broad_filter_map(async |ev_id| { - fetch_event(ev_id.clone()).await.map(|event| (event, ev_id)) - }) - .broad_filter_map(|(event, ev_id)| { - get_mainline_depth(Some(event.clone()), &mainline_map, fetch_event) - .map_ok(move |depth| (ev_id, (depth, event.origin_server_ts(), ev_id))) - .map(Result::ok) - }) - .collect() - .boxed() - .await; - - // Sort the event_ids by their depth, timestamp and EventId - // unwrap is OK order map and sort_event_ids are from to_sort (the same Vec) - let mut sort_event_ids: Vec<_> = order_map.keys().map(|&k| k.clone()).collect(); - - sort_event_ids.sort_by_key(|sort_id| &order_map[sort_id]); - - Ok(sort_event_ids) -} - -/// 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, - mainline_map: &HashMap, - fetch_event: &F, -) -> Result -where - F: Fn(OwnedEventId) -> Fut + Sync, - Fut: Future> + Send, - E: Event + Send + Sync, -{ - while let Some(sort_ev) = event { - debug!(event_id = sort_ev.event_id().as_str(), "mainline"); - - let id = sort_ev.event_id(); - if let Some(depth) = mainline_map.get(id) { - return Ok(*depth); - } - - event = None; - for aid in sort_ev.auth_events() { - let aev = fetch_event(aid.to_owned()) - .await - .ok_or_else(|| Error::NotFound(format!("Failed to find {aid}")))?; - - if is_type_and_key(&aev, &TimelineEventType::RoomPowerLevels, "") { - event = Some(aev); - break; - } - } - } - // Did not find a power level event so we default to zero - Ok(0) -} - -async fn add_event_and_auth_chain_to_graph( - graph: &mut HashMap>, - event_id: OwnedEventId, - auth_diff: &HashSet, - fetch_event: &F, -) where - F: Fn(OwnedEventId) -> Fut + Sync, - Fut: Future> + Send, - E: Event + Send + 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 auth_events = event.as_ref().map(Event::auth_events).into_iter().flatten(); - - // Prefer the store to event as the store filters dedups the events - for aid in auth_events { - if auth_diff.contains(aid) { - if !graph.contains_key(aid) { - state.push(aid.to_owned()); - } - - graph - .get_mut(&eid) - .expect("We just inserted this at the start of the while loop") - .insert(aid.to_owned()); - } - } - } -} - -async fn is_power_event_id(event_id: &EventId, fetch: &F) -> bool -where - F: Fn(OwnedEventId) -> Fut + Sync, - Fut: Future> + Send, - E: Event + Send, -{ - match fetch(event_id.to_owned()).await.as_ref() { - | Some(state) => is_power_event(state), - | _ => false, - } -} - -fn is_type_and_key(ev: &impl Event, ev_type: &TimelineEventType, state_key: &str) -> bool { - ev.event_type() == ev_type && ev.state_key() == Some(state_key) -} - -fn is_power_event(event: &impl Event) -> bool { - match event.event_type() { - | TimelineEventType::RoomPowerLevels - | TimelineEventType::RoomJoinRules - | TimelineEventType::RoomCreate => event.state_key() == Some(""), - | TimelineEventType::RoomMember => { - if let Ok(content) = from_json_str::(event.content().get()) { - if [MembershipState::Leave, MembershipState::Ban].contains(&content.membership) { - return Some(event.sender().as_str()) != event.state_key(); - } - } - - false - }, - | _ => false, - } -} - -/// Convenience trait for adding event type plus state key to state maps. -pub trait EventTypeExt { - fn with_state_key(self, state_key: impl Into) -> (StateEventType, StateKey); -} - -impl EventTypeExt for StateEventType { - fn with_state_key(self, state_key: impl Into) -> (StateEventType, StateKey) { - (self, state_key.into()) - } -} - -impl EventTypeExt for TimelineEventType { - fn with_state_key(self, state_key: impl Into) -> (StateEventType, StateKey) { - (self.to_string().into(), state_key.into()) - } -} - -impl EventTypeExt for &T -where - T: EventTypeExt + Clone, -{ - fn with_state_key(self, state_key: impl Into) -> (StateEventType, StateKey) { - self.to_owned().with_state_key(state_key) - } -} - -#[cfg(test)] -mod tests { - use std::collections::{HashMap, HashSet}; - - use maplit::{hashmap, hashset}; - use rand::seq::SliceRandom; - use ruma::{ - MilliSecondsSinceUnixEpoch, OwnedEventId, RoomVersionId, - events::{ - StateEventType, TimelineEventType, - room::join_rules::{JoinRule, RoomJoinRulesEventContent}, - }, - int, - room_version_rules::RoomVersionRules, - uint, - }; - use serde_json::{json, value::to_raw_value as to_raw_json_value}; - - use super::{ - StateMap, is_power_event, - test_utils::{ - INITIAL_EVENTS, TestStore, alice, bob, charlie, do_check, ella, event_id, - member_content_ban, member_content_join, room_id, to_init_pdu_event, to_pdu_event, - zara, - }, - }; - use crate::{ - debug, - matrix::{Event, EventTypeExt, Pdu as PduEvent}, - utils::stream::IterStream, - }; - - #[tokio::test] - async fn iterative_auth_check_keeps_event_order() { - let mut events = INITIAL_EVENTS(); - let initial_state = ["CREATE", "IPOWER", "IJR", "IMB"] - .into_iter() - .map(|id| { - let event = &events[&event_id(id)]; - ( - event - .event_type() - .with_state_key(event.state_key().unwrap()), - event.event_id.clone(), - ) - }) - .collect(); - let leave = to_pdu_event( - "LEAVE", - bob(), - TimelineEventType::RoomMember, - Some(bob().as_str()), - to_raw_json_value(&json!({ "membership": "leave" })).unwrap(), - &["CREATE", "IPOWER", "IMB"], - &["IMB"], - ); - let rejoin = to_pdu_event( - "REJOIN", - bob(), - TimelineEventType::RoomMember, - Some(bob().as_str()), - member_content_join(), - &["CREATE", "IPOWER", "IJR", "LEAVE"], - &["LEAVE"], - ); - let ordered_events = [leave.event_id.clone(), rejoin.event_id.clone()]; - for event in [leave, rejoin] { - events.insert(event.event_id.clone(), event); - } - let fetcher = |id: OwnedEventId| { - let events = &events; - async move { - // Let the rejoin fetch finish before the leave. - if id == event_id("LEAVE") { - tokio::task::yield_now().await; - } - events.get(&id).cloned() - } - }; - let resolved = super::iterative_auth_check( - &RoomVersionRules::V6, - ordered_events.iter().map(AsRef::as_ref).stream(), - initial_state, - &fetcher, - ) - .await - .unwrap(); - - assert_eq!( - resolved[&StateEventType::RoomMember.with_state_key(bob().as_str())], - event_id("REJOIN"), - ); - } - - /// Events at the start of a room have no `m.room.power_levels` in their - /// auth events, so the sender's power for sort ordering comes from the - /// create event instead. - #[tokio::test] - async fn power_level_for_sender_falls_back_to_creator() { - use futures::future::ready; - - let create = to_pdu_event::<&str>( - "CREATE", - alice(), - TimelineEventType::RoomCreate, - Some(""), - to_raw_json_value(&json!({ "creator": alice() })).unwrap(), - &[], - &[], - ); - - // alice created the room, and there is no m.room.power_levels yet. - let alice_join = to_pdu_event( - "IMA", - alice(), - TimelineEventType::RoomMember, - Some(alice().as_str()), - member_content_join(), - &["CREATE"], - &["CREATE"], - ); - - let events: HashMap = [create, alice_join] - .into_iter() - .map(|ev| (ev.event_id.clone(), ev)) - .collect(); - - let fetcher = |id: OwnedEventId| ready(events.get(&id).cloned()); - - let power_level = - super::get_power_level_for_sender(&event_id("IMA"), &RoomVersionRules::V6, &fetcher) - .await - .unwrap(); - - assert_eq!(power_level, int!(100), "the creator should sort with their default power"); - } - - #[tokio::test] - async fn power_level_for_sender_uses_create_content_creator() { - use futures::future::ready; - - // bob sent the create event, but alice is named as the creator in it. - let create = to_pdu_event::<&str>( - "CREATE", - bob(), - TimelineEventType::RoomCreate, - Some(""), - to_raw_json_value(&json!({ "creator": alice() })).unwrap(), - &[], - &[], - ); - - let alice_join = to_pdu_event( - "IMA", - alice(), - TimelineEventType::RoomMember, - Some(alice().as_str()), - member_content_join(), - &["CREATE"], - &["CREATE"], - ); - - let bob_join = to_pdu_event( - "IMB", - bob(), - TimelineEventType::RoomMember, - Some(bob().as_str()), - member_content_join(), - &["CREATE"], - &["CREATE"], - ); - - let events: HashMap = [create, alice_join, bob_join] - .into_iter() - .map(|ev| (ev.event_id.clone(), ev)) - .collect(); - - let fetcher = |id: OwnedEventId| ready(events.get(&id).cloned()); - - let alice_power = - super::get_power_level_for_sender(&event_id("IMA"), &RoomVersionRules::V6, &fetcher) - .await - .unwrap(); - - assert_eq!(alice_power, int!(100), "the content creator should sort with creator power"); - - let bob_power = - super::get_power_level_for_sender(&event_id("IMB"), &RoomVersionRules::V6, &fetcher) - .await - .unwrap(); - - assert_eq!(bob_power, int!(0), "the create event's sender is not the creator"); - } - - async fn test_event_sort() { - use futures::future::ready; - - let _ = tracing::subscriber::set_default( - tracing_subscriber::fmt().with_test_writer().finish(), - ); - let events = INITIAL_EVENTS(); - - let event_map = events - .values() - .map(|ev| (ev.event_type().with_state_key(ev.state_key().unwrap()), ev.clone())) - .collect::>(); - - let auth_chain: HashSet = HashSet::new(); - - let power_events = event_map - .values() - .filter(|&pdu| is_power_event(&*pdu)) - .map(|pdu| pdu.event_id.clone()) - .collect::>(); - - let fetcher = |id| ready(events.get(&id).cloned()); - let sorted_power_events = super::reverse_topological_power_sort( - power_events, - &auth_chain, - &RoomVersionRules::V6, - &fetcher, - ) - .await - .unwrap(); - - let resolved_power = super::iterative_auth_check( - &RoomVersionRules::V6, - sorted_power_events.iter().map(AsRef::as_ref).stream(), - HashMap::new(), // unconflicted events - &fetcher, - ) - .await - .expect("iterative auth check failed on resolved events"); - - // don't remove any events so we know it sorts them all correctly - let mut events_to_sort = events.keys().cloned().collect::>(); - - events_to_sort.shuffle(&mut rand::rng()); - - let power_level = resolved_power - .get(&(StateEventType::RoomPowerLevels, "".into())) - .cloned(); - - let sorted_event_ids = super::mainline_sort(&events_to_sort, power_level, &fetcher) - .await - .unwrap(); - - assert_eq!( - vec![ - "$CREATE:foo", - "$IMA:foo", - "$IPOWER:foo", - "$IJR:foo", - "$IMB:foo", - "$IMC:foo", - "$START:foo", - "$END:foo" - ], - sorted_event_ids - .iter() - .map(|id| id.to_string()) - .collect::>() - ); - } - - // NOTE(2025-09-17): Disabled due to unknown "create event must exist" bug - // #[tokio::test] - async fn test_sort() { - for _ in 0..20 { - // since we shuffle the eventIds before we sort them introducing - // randomness seems like we should test this a few times - test_event_sort().await; - } - } - - // NOTE(2025-09-17): Disabled due to unknown "create event must exist" bug - //#[tokio::test] - async fn ban_vs_power_level() { - let _ = tracing::subscriber::set_default( - tracing_subscriber::fmt().with_test_writer().finish(), - ); - - let events = &[ - to_init_pdu_event( - "PA", - alice(), - TimelineEventType::RoomPowerLevels, - Some(""), - to_raw_json_value(&json!({ "users": { alice(): 100, bob(): 50 } })).unwrap(), - ), - to_init_pdu_event( - "MA", - alice(), - TimelineEventType::RoomMember, - Some(alice().to_string().as_str()), - member_content_join(), - ), - to_init_pdu_event( - "MB", - alice(), - TimelineEventType::RoomMember, - Some(bob().to_string().as_str()), - member_content_ban(), - ), - to_init_pdu_event( - "PB", - bob(), - TimelineEventType::RoomPowerLevels, - Some(""), - to_raw_json_value(&json!({ "users": { alice(): 100, bob(): 50 } })).unwrap(), - ), - ]; - - let edges = vec![vec!["END", "MB", "MA", "PA", "START"], vec!["END", "PA", "PB"]] - .into_iter() - .map(|list| list.into_iter().map(event_id).collect::>()) - .collect::>(); - - let expected_state_ids = vec!["PA", "MA", "MB"] - .into_iter() - .map(event_id) - .collect::>(); - - do_check(events, edges, expected_state_ids).await; - } - - #[tokio::test] - async fn topic_basic() { - let _ = tracing::subscriber::set_default( - tracing_subscriber::fmt().with_test_writer().finish(), - ); - - let events = &[ - to_init_pdu_event( - "T1", - alice(), - TimelineEventType::RoomTopic, - Some(""), - to_raw_json_value(&json!({})).unwrap(), - ), - to_init_pdu_event( - "PA1", - alice(), - TimelineEventType::RoomPowerLevels, - Some(""), - to_raw_json_value(&json!({ "users": { alice(): 100, bob(): 50 } })).unwrap(), - ), - to_init_pdu_event( - "T2", - alice(), - TimelineEventType::RoomTopic, - Some(""), - to_raw_json_value(&json!({})).unwrap(), - ), - to_init_pdu_event( - "PA2", - alice(), - TimelineEventType::RoomPowerLevels, - Some(""), - to_raw_json_value(&json!({ "users": { alice(): 100, bob(): 0 } })).unwrap(), - ), - to_init_pdu_event( - "PB", - bob(), - TimelineEventType::RoomPowerLevels, - Some(""), - to_raw_json_value(&json!({ "users": { alice(): 100, bob(): 50 } })).unwrap(), - ), - to_init_pdu_event( - "T3", - bob(), - TimelineEventType::RoomTopic, - Some(""), - to_raw_json_value(&json!({})).unwrap(), - ), - ]; - - let edges = - vec![vec!["END", "PA2", "T2", "PA1", "T1", "START"], vec!["END", "T3", "PB", "PA1"]] - .into_iter() - .map(|list| list.into_iter().map(event_id).collect::>()) - .collect::>(); - - let expected_state_ids = vec!["PA2", "T2"] - .into_iter() - .map(event_id) - .collect::>(); - - do_check(events, edges, expected_state_ids).await; - } - - #[tokio::test] - async fn topic_reset() { - let _ = tracing::subscriber::set_default( - tracing_subscriber::fmt().with_test_writer().finish(), - ); - - let events = &[ - to_init_pdu_event( - "T1", - alice(), - TimelineEventType::RoomTopic, - Some(""), - to_raw_json_value(&json!({})).unwrap(), - ), - to_init_pdu_event( - "PA", - alice(), - TimelineEventType::RoomPowerLevels, - Some(""), - to_raw_json_value(&json!({ "users": { alice(): 100, bob(): 50 } })).unwrap(), - ), - to_init_pdu_event( - "T2", - bob(), - TimelineEventType::RoomTopic, - Some(""), - to_raw_json_value(&json!({})).unwrap(), - ), - to_init_pdu_event( - "MB", - alice(), - TimelineEventType::RoomMember, - Some(bob().to_string().as_str()), - member_content_ban(), - ), - ]; - - let edges = vec![vec!["END", "MB", "T2", "PA", "T1", "START"], vec!["END", "T1"]] - .into_iter() - .map(|list| list.into_iter().map(event_id).collect::>()) - .collect::>(); - - let expected_state_ids = vec!["T1", "MB", "PA"] - .into_iter() - .map(event_id) - .collect::>(); - - do_check(events, edges, expected_state_ids).await; - } - - #[tokio::test] - async fn join_rule_evasion() { - let _ = tracing::subscriber::set_default( - tracing_subscriber::fmt().with_test_writer().finish(), - ); - - let events = &[ - to_init_pdu_event( - "JR", - alice(), - TimelineEventType::RoomJoinRules, - Some(""), - to_raw_json_value(&RoomJoinRulesEventContent::new(JoinRule::Private)).unwrap(), - ), - to_init_pdu_event( - "ME", - ella(), - TimelineEventType::RoomMember, - Some(ella().to_string().as_str()), - member_content_join(), - ), - ]; - - let edges = vec![vec!["END", "JR", "START"], vec!["END", "ME", "START"]] - .into_iter() - .map(|list| list.into_iter().map(event_id).collect::>()) - .collect::>(); - - let expected_state_ids = vec![event_id("JR")]; - - do_check(events, edges, expected_state_ids).await; - } - - #[tokio::test] - async fn offtopic_power_level() { - let _ = tracing::subscriber::set_default( - tracing_subscriber::fmt().with_test_writer().finish(), - ); - - let events = &[ - to_init_pdu_event( - "PA", - alice(), - TimelineEventType::RoomPowerLevels, - Some(""), - to_raw_json_value(&json!({ "users": { alice(): 100, bob(): 50 } })).unwrap(), - ), - to_init_pdu_event( - "PB", - bob(), - TimelineEventType::RoomPowerLevels, - Some(""), - to_raw_json_value( - &json!({ "users": { alice(): 100, bob(): 50, charlie(): 50 } }), - ) - .unwrap(), - ), - to_init_pdu_event( - "PC", - charlie(), - TimelineEventType::RoomPowerLevels, - Some(""), - to_raw_json_value(&json!({ "users": { alice(): 100, bob(): 50, charlie(): 0 } })) - .unwrap(), - ), - ]; - - let edges = vec![vec!["END", "PC", "PB", "PA", "START"], vec!["END", "PA"]] - .into_iter() - .map(|list| list.into_iter().map(event_id).collect::>()) - .collect::>(); - - let expected_state_ids = vec!["PC"].into_iter().map(event_id).collect::>(); - - do_check(events, edges, expected_state_ids).await; - } - - #[tokio::test] - async fn topic_setting() { - let _ = tracing::subscriber::set_default( - tracing_subscriber::fmt().with_test_writer().finish(), - ); - - let events = &[ - to_init_pdu_event( - "T1", - alice(), - TimelineEventType::RoomTopic, - Some(""), - to_raw_json_value(&json!({})).unwrap(), - ), - to_init_pdu_event( - "PA1", - alice(), - TimelineEventType::RoomPowerLevels, - Some(""), - to_raw_json_value(&json!({ "users": { alice(): 100, bob(): 50 } })).unwrap(), - ), - to_init_pdu_event( - "T2", - alice(), - TimelineEventType::RoomTopic, - Some(""), - to_raw_json_value(&json!({})).unwrap(), - ), - to_init_pdu_event( - "PA2", - alice(), - TimelineEventType::RoomPowerLevels, - Some(""), - to_raw_json_value(&json!({ "users": { alice(): 100, bob(): 0 } })).unwrap(), - ), - to_init_pdu_event( - "PB", - bob(), - TimelineEventType::RoomPowerLevels, - Some(""), - to_raw_json_value(&json!({ "users": { alice(): 100, bob(): 50 } })).unwrap(), - ), - to_init_pdu_event( - "T3", - bob(), - TimelineEventType::RoomTopic, - Some(""), - to_raw_json_value(&json!({})).unwrap(), - ), - to_init_pdu_event( - "MZ1", - zara(), - TimelineEventType::RoomTopic, - Some(""), - to_raw_json_value(&json!({})).unwrap(), - ), - to_init_pdu_event( - "T4", - alice(), - TimelineEventType::RoomTopic, - Some(""), - to_raw_json_value(&json!({})).unwrap(), - ), - ]; - - let edges = vec![vec!["END", "T4", "MZ1", "PA2", "T2", "PA1", "T1", "START"], vec![ - "END", "MZ1", "T3", "PB", "PA1", - ]] - .into_iter() - .map(|list| list.into_iter().map(event_id).collect::>()) - .collect::>(); - - let expected_state_ids = vec!["T4", "PA2"] - .into_iter() - .map(event_id) - .collect::>(); - - do_check(events, edges, expected_state_ids).await; - } - - #[tokio::test] - async fn test_event_map_none() { - use futures::future::ready; - - let _ = tracing::subscriber::set_default( - tracing_subscriber::fmt().with_test_writer().finish(), - ); - - let mut store = TestStore::(hashmap! {}); - - // build up the DAG - let (state_at_bob, state_at_charlie, expected) = store.set_up(); - - let ev_map = store.0.clone(); - let fetcher = |id| ready(ev_map.get(&id).cloned()); - - let exists = |id: OwnedEventId| ready(ev_map.get(&*id).is_some()); - - let state_sets = [state_at_bob, state_at_charlie]; - let auth_chain: Vec<_> = state_sets - .iter() - .map(|map| { - store - .auth_event_ids(room_id(), map.values().cloned().collect()) - .unwrap() - }) - .collect(); - - let resolved = match super::resolve( - &RoomVersionRules::V2, - &state_sets, - &auth_chain, - &fetcher, - &exists, - ) - .await - { - | Ok(state) => state, - | Err(e) => panic!("{e}"), - }; - - assert_eq!(expected, resolved); - } - - #[tokio::test] - async fn test_lexicographical_sort() { - let _ = tracing::subscriber::set_default( - tracing_subscriber::fmt().with_test_writer().finish(), - ); - - let graph = hashmap! { - event_id("l") => hashset![event_id("o")], - event_id("m") => hashset![event_id("n"), event_id("o")], - event_id("n") => hashset![event_id("o")], - event_id("o") => hashset![], // "o" has zero outgoing edges but 4 incoming edges - event_id("p") => hashset![event_id("o")], - }; - - let res = super::lexicographical_topological_sort(&graph, &|_id| async { - Ok((int!(0), MilliSecondsSinceUnixEpoch(uint!(0)))) - }) - .await - .unwrap(); - - assert_eq!( - vec!["o", "l", "n", "m", "p"], - res.iter() - .map(ToString::to_string) - .map(|s| s.replace('$', "").replace(":foo", "")) - .collect::>() - ); - } - - #[tokio::test] - async fn ban_with_auth_chains() { - let _ = tracing::subscriber::set_default( - tracing_subscriber::fmt().with_test_writer().finish(), - ); - let ban = BAN_STATE_SET(); - - let edges = vec![vec!["END", "MB", "PA", "START"], vec!["END", "IME", "MB"]] - .into_iter() - .map(|list| list.into_iter().map(event_id).collect::>()) - .collect::>(); - - let expected_state_ids = vec!["PA", "MB"] - .into_iter() - .map(event_id) - .collect::>(); - - do_check(&ban.values().cloned().collect::>(), edges, expected_state_ids).await; - } - - #[tokio::test] - async fn ban_with_auth_chains2() { - use futures::future::ready; - - let _ = tracing::subscriber::set_default( - tracing_subscriber::fmt().with_test_writer().finish(), - ); - let init = INITIAL_EVENTS(); - let ban = BAN_STATE_SET(); - - let mut inner = init.clone(); - inner.extend(ban); - let store = TestStore(inner.clone()); - - let state_set_a = [ - inner.get(&event_id("CREATE")).unwrap(), - inner.get(&event_id("IJR")).unwrap(), - inner.get(&event_id("IMA")).unwrap(), - inner.get(&event_id("IMB")).unwrap(), - inner.get(&event_id("IMC")).unwrap(), - inner.get(&event_id("MB")).unwrap(), - inner.get(&event_id("PA")).unwrap(), - ] - .iter() - .map(|ev| (ev.event_type().with_state_key(ev.state_key().unwrap()), ev.event_id.clone())) - .collect::>(); - - let state_set_b = [ - inner.get(&event_id("CREATE")).unwrap(), - inner.get(&event_id("IJR")).unwrap(), - inner.get(&event_id("IMA")).unwrap(), - inner.get(&event_id("IMB")).unwrap(), - inner.get(&event_id("IMC")).unwrap(), - inner.get(&event_id("IME")).unwrap(), - inner.get(&event_id("PA")).unwrap(), - ] - .iter() - .map(|ev| (ev.event_type().with_state_key(ev.state_key().unwrap()), ev.event_id.clone())) - .collect::>(); - - let ev_map = &store.0; - let state_sets = [state_set_a, state_set_b]; - let auth_chain: Vec<_> = state_sets - .iter() - .map(|map| { - store - .auth_event_ids(room_id(), map.values().cloned().collect()) - .unwrap() - }) - .collect(); - - let fetcher = |id: OwnedEventId| ready(ev_map.get(&id).cloned()); - let exists = |id: OwnedEventId| ready(ev_map.get(&id).is_some()); - let resolved = match super::resolve( - &RoomVersionRules::V6, - &state_sets, - &auth_chain, - &fetcher, - &exists, - ) - .await - { - | Ok(state) => state, - | Err(e) => panic!("{e}"), - }; - - debug!( - resolved = ?resolved - .iter() - .map(|((ty, key), id)| format!("(({ty}{key:?}), {id})")) - .collect::>(), - "resolved state", - ); - - let expected = [ - "$CREATE:foo", - "$IJR:foo", - "$PA:foo", - "$IMA:foo", - "$IMB:foo", - "$IMC:foo", - "$MB:foo", - ]; - - for id in expected.iter().map(|i| event_id(i)) { - // make sure our resolved events are equal to the expected list - assert!(resolved.values().any(|eid| eid == &id) || init.contains_key(&id), "{id}"); - } - assert_eq!(expected.len(), resolved.len()); - } - - #[tokio::test] - async fn join_rule_with_auth_chain() { - let join_rule = JOIN_RULE(); - - let edges = vec![vec!["END", "JR", "START"], vec!["END", "IMZ", "START"]] - .into_iter() - .map(|list| list.into_iter().map(event_id).collect::>()) - .collect::>(); - - let expected_state_ids = vec!["JR"].into_iter().map(event_id).collect::>(); - - do_check(&join_rule.values().cloned().collect::>(), edges, expected_state_ids) - .await; - } - - #[allow(non_snake_case)] - fn BAN_STATE_SET() -> HashMap { - vec![ - to_pdu_event( - "PA", - alice(), - TimelineEventType::RoomPowerLevels, - Some(""), - to_raw_json_value(&json!({ "users": { alice(): 100, bob(): 50 } })).unwrap(), - &["CREATE", "IMA", "IPOWER"], // auth_events - &["START"], // prev_events - ), - to_pdu_event( - "PB", - alice(), - TimelineEventType::RoomPowerLevels, - Some(""), - to_raw_json_value(&json!({ "users": { alice(): 100, bob(): 50 } })).unwrap(), - &["CREATE", "IMA", "IPOWER"], - &["END"], - ), - to_pdu_event( - "MB", - alice(), - TimelineEventType::RoomMember, - Some(ella().as_str()), - member_content_ban(), - &["CREATE", "IMA", "PB"], - &["PA"], - ), - to_pdu_event( - "IME", - ella(), - TimelineEventType::RoomMember, - Some(ella().as_str()), - member_content_join(), - &["CREATE", "IJR", "PA"], - &["MB"], - ), - ] - .into_iter() - .map(|ev| (ev.event_id.clone(), ev)) - .collect() - } - - #[allow(non_snake_case)] - fn JOIN_RULE() -> HashMap { - vec![ - to_pdu_event( - "JR", - alice(), - TimelineEventType::RoomJoinRules, - Some(""), - to_raw_json_value(&json!({ "join_rule": "invite" })).unwrap(), - &["CREATE", "IMA", "IPOWER"], - &["START"], - ), - to_pdu_event( - "IMZ", - zara(), - TimelineEventType::RoomPowerLevels, - Some(zara().as_str()), - member_content_join(), - &["CREATE", "JR", "IPOWER"], - &["START"], - ), - ] - .into_iter() - .map(|ev| (ev.event_id.clone(), ev)) - .collect() - } - - macro_rules! state_set { - ($($kind:expr_2021 => $key:expr_2021 => $id:expr_2021),* $(,)?) => {{ - #[allow(unused_mut)] - let mut x = StateMap::new(); - $( - x.insert(($kind, $key.into()), $id); - )* - x - }}; - } - - #[test] - fn separate_unique_conflicted() { - let (unconflicted, conflicted) = super::separate( - [ - state_set![StateEventType::RoomMember => "@a:hs1" => 0], - state_set![StateEventType::RoomMember => "@b:hs1" => 1], - state_set![StateEventType::RoomMember => "@c:hs1" => 2], - ] - .iter(), - ); - - assert_eq!(unconflicted, StateMap::new()); - assert_eq!(conflicted, state_set![ - StateEventType::RoomMember => "@a:hs1" => vec![0], - StateEventType::RoomMember => "@b:hs1" => vec![1], - StateEventType::RoomMember => "@c:hs1" => vec![2], - ],); - } - - #[test] - fn separate_conflicted() { - let (unconflicted, mut conflicted) = super::separate( - [ - state_set![StateEventType::RoomMember => "@a:hs1" => 0], - state_set![StateEventType::RoomMember => "@a:hs1" => 1], - state_set![StateEventType::RoomMember => "@a:hs1" => 2], - ] - .iter(), - ); - - // HashMap iteration order is random, so sort this before asserting on - // it - for v in conflicted.values_mut() { - v.sort_unstable(); - } - - assert_eq!(unconflicted, StateMap::new()); - assert_eq!(conflicted, state_set![ - StateEventType::RoomMember => "@a:hs1" => vec![0, 1, 2], - ],); - } - - #[test] - fn separate_unconflicted() { - let (unconflicted, conflicted) = super::separate( - [ - state_set![StateEventType::RoomMember => "@a:hs1" => 0], - state_set![StateEventType::RoomMember => "@a:hs1" => 0], - state_set![StateEventType::RoomMember => "@a:hs1" => 0], - ] - .iter(), - ); - - assert_eq!(unconflicted, state_set![ - StateEventType::RoomMember => "@a:hs1" => 0, - ],); - assert_eq!(conflicted, StateMap::new()); - } - - #[test] - fn separate_mixed() { - let (unconflicted, conflicted) = super::separate( - [ - state_set![StateEventType::RoomMember => "@a:hs1" => 0], - state_set![ - StateEventType::RoomMember => "@a:hs1" => 0, - StateEventType::RoomMember => "@b:hs1" => 1, - ], - state_set![ - StateEventType::RoomMember => "@a:hs1" => 0, - StateEventType::RoomMember => "@c:hs1" => 2, - ], - ] - .iter(), - ); - - assert_eq!(unconflicted, state_set![ - StateEventType::RoomMember => "@a:hs1" => 0, - ],); - assert_eq!(conflicted, state_set![ - StateEventType::RoomMember => "@b:hs1" => vec![1], - StateEventType::RoomMember => "@c:hs1" => vec![2], - ],); - } -} diff --git a/src/core/matrix/state_res/outcomes.txt b/src/core/matrix/state_res/outcomes.txt deleted file mode 100644 index 0fa1c7346..000000000 --- a/src/core/matrix/state_res/outcomes.txt +++ /dev/null @@ -1,104 +0,0 @@ -11/29/2020 BRANCH: timo-spec-comp REV: d2a85669cc6056679ce6ca0fde4658a879ad2b08 -lexicographical topological sort - time: [1.7123 us 1.7157 us 1.7199 us] - change: [-1.7584% -1.5433% -1.3205%] (p = 0.00 < 0.05) - Performance has improved. -Found 8 outliers among 100 measurements (8.00%) - 2 (2.00%) low mild - 5 (5.00%) high mild - 1 (1.00%) high severe - -resolve state of 5 events one fork - time: [10.981 us 10.998 us 11.020 us] -Found 3 outliers among 100 measurements (3.00%) - 3 (3.00%) high mild - -resolve state of 10 events 3 conflicting - time: [26.858 us 26.946 us 27.037 us] - -11/29/2020 BRANCH: event-trait REV: f0eb1310efd49d722979f57f20bd1ac3592b0479 -lexicographical topological sort - time: [1.7686 us 1.7738 us 1.7810 us] - change: [-3.2752% -2.4634% -1.7635%] (p = 0.00 < 0.05) - Performance has improved. -Found 1 outliers among 100 measurements (1.00%) - 1 (1.00%) high severe - -resolve state of 5 events one fork - time: [10.643 us 10.656 us 10.669 us] - change: [-4.9990% -3.8078% -2.8319%] (p = 0.00 < 0.05) - Performance has improved. -Found 1 outliers among 100 measurements (1.00%) - 1 (1.00%) high severe - -resolve state of 10 events 3 conflicting - time: [29.149 us 29.252 us 29.375 us] - change: [-0.8433% -0.3270% +0.2656%] (p = 0.25 > 0.05) - No change in performance detected. -Found 1 outliers among 100 measurements (1.00%) - 1 (1.00%) high mild - -4/26/2020 BRANCH: fix-test-serde REV: -lexicographical topological sort - time: [1.6793 us 1.6823 us 1.6857 us] -Found 9 outliers among 100 measurements (9.00%) - 1 (1.00%) low mild - 4 (4.00%) high mild - 4 (4.00%) high severe - -resolve state of 5 events one fork - time: [9.9993 us 10.062 us 10.159 us] -Found 9 outliers among 100 measurements (9.00%) - 7 (7.00%) high mild - 2 (2.00%) high severe - -resolve state of 10 events 3 conflicting - time: [26.004 us 26.092 us 26.195 us] -Found 16 outliers among 100 measurements (16.00%) - 11 (11.00%) high mild - 5 (5.00%) high severe - -6/30/2021 BRANCH: state-closure REV: 174c3e2a72232ad75b3fb14b3551f5f746f4fe84 -lexicographical topological sort - time: [1.5496 us 1.5536 us 1.5586 us] -Found 9 outliers among 100 measurements (9.00%) - 1 (1.00%) low mild - 1 (1.00%) high mild - 7 (7.00%) high severe - -resolve state of 5 events one fork - time: [10.319 us 10.333 us 10.347 us] -Found 2 outliers among 100 measurements (2.00%) - 2 (2.00%) high severe - -resolve state of 10 events 3 conflicting - time: [25.770 us 25.805 us 25.839 us] -Found 7 outliers among 100 measurements (7.00%) - 5 (5.00%) high mild - 2 (2.00%) high severe - -7/20/2021 BRANCH stateres-result REV: -This marks the switch to HashSet/Map -lexicographical topological sort - time: [1.8122 us 1.8177 us 1.8233 us] - change: [+15.205% +15.919% +16.502%] (p = 0.00 < 0.05) - Performance has regressed. -Found 7 outliers among 100 measurements (7.00%) - 5 (5.00%) high mild - 2 (2.00%) high severe - -resolve state of 5 events one fork - time: [11.966 us 12.010 us 12.059 us] - change: [+16.089% +16.730% +17.469%] (p = 0.00 < 0.05) - Performance has regressed. -Found 7 outliers among 100 measurements (7.00%) - 3 (3.00%) high mild - 4 (4.00%) high severe - -resolve state of 10 events 3 conflicting - time: [29.092 us 29.201 us 29.311 us] - change: [+12.447% +12.847% +13.280%] (p = 0.00 < 0.05) - Performance has regressed. -Found 9 outliers among 100 measurements (9.00%) - 6 (6.00%) high mild - 3 (3.00%) high severe diff --git a/src/core/matrix/state_res/power_levels.rs b/src/core/matrix/state_res/power_levels.rs deleted file mode 100644 index d6b3c4108..000000000 --- a/src/core/matrix/state_res/power_levels.rs +++ /dev/null @@ -1,241 +0,0 @@ -use std::collections::BTreeMap; - -use ruma::{ - Int, OwnedUserId, UserId, - events::{TimelineEventType, room::power_levels::RoomPowerLevelsEventContent}, - power_levels::{NotificationPowerLevels, default_power_level}, - room_version_rules::{AuthorizationRules, RoomVersionRules}, - serde::deserialize_v1_powerlevel, -}; -use serde::Deserialize; -use serde_json::{Error, from_str as from_json_str}; - -use super::{ - Result, - serde_backports::{ - vec_deserialize_int_powerlevel_values, vec_deserialize_v1_powerlevel_values, - }, -}; -use crate::error; - -#[derive(Deserialize)] -struct IntRoomPowerLevelsEventContent { - #[serde(default = "default_power_level")] - ban: Int, - - #[serde(default)] - events: BTreeMap, - - #[serde(default)] - events_default: Int, - - #[serde(default)] - invite: Int, - - #[serde(default = "default_power_level")] - kick: Int, - - #[serde(default = "default_power_level")] - redact: Int, - - #[serde(default = "default_power_level")] - state_default: Int, - - #[serde(default)] - users: BTreeMap, - - #[serde(default)] - users_default: Int, - - #[serde(default)] - notifications: IntNotificationPowerLevels, -} - -impl IntRoomPowerLevelsEventContent { - fn into_room_power_levels_content( - self, - auth_rules: &AuthorizationRules, - ) -> RoomPowerLevelsEventContent { - let Self { - ban, - events, - events_default, - invite, - kick, - redact, - state_default, - users, - users_default, - notifications, - } = self; - - let mut pl = RoomPowerLevelsEventContent::new(auth_rules); - pl.ban = ban; - pl.events = events; - pl.events_default = events_default; - pl.invite = invite; - pl.kick = kick; - pl.redact = redact; - pl.state_default = state_default; - pl.users = users; - pl.users_default = users_default; - pl.notifications = notifications.into(); - - pl - } -} - -#[derive(Deserialize)] -struct IntNotificationPowerLevels { - #[serde(default = "default_power_level")] - room: Int, -} - -impl Default for IntNotificationPowerLevels { - fn default() -> Self { Self { room: default_power_level() } } -} - -impl From for NotificationPowerLevels { - fn from(int_notif: IntNotificationPowerLevels) -> Self { - let mut notif = Self::new(); - notif.room = int_notif.room; - - notif - } -} - -#[inline] -pub(crate) fn deserialize_power_levels( - content: &str, - room_version: &RoomVersionRules, -) -> Option { - if room_version.authorization.integer_power_levels { - deserialize_integer_power_levels(content, &room_version.authorization) - } else { - deserialize_legacy_power_levels(content) - } -} - -fn deserialize_integer_power_levels( - content: &str, - auth_rules: &AuthorizationRules, -) -> Option { - match from_json_str::(content) { - | Ok(content) => Some(content.into_room_power_levels_content(auth_rules)), - | Err(_) => { - error!("m.room.power_levels event is not valid with integer values"); - None - }, - } -} - -fn deserialize_legacy_power_levels(content: &str) -> Option { - match from_json_str(content) { - | Ok(content) => Some(content), - | Err(_) => { - error!( - "m.room.power_levels event is not valid with integer or string integer values" - ); - None - }, - } -} - -#[derive(Deserialize)] -pub(crate) struct PowerLevelsContentFields { - #[serde(default, deserialize_with = "vec_deserialize_v1_powerlevel_values")] - pub(crate) users: Vec<(OwnedUserId, Int)>, - - #[serde(default, deserialize_with = "deserialize_v1_powerlevel")] - pub(crate) users_default: Int, -} - -impl PowerLevelsContentFields { - pub(crate) fn get_user_power(&self, user_id: &UserId) -> Option<&Int> { - let comparator = |item: &(OwnedUserId, Int)| { - let item: &UserId = &item.0; - item.cmp(user_id) - }; - - self.users - .binary_search_by(comparator) - .ok() - .and_then(|idx| self.users.get(idx).map(|item| &item.1)) - } -} - -#[derive(Deserialize)] -struct IntPowerLevelsContentFields { - #[serde(default, deserialize_with = "vec_deserialize_int_powerlevel_values")] - users: Vec<(OwnedUserId, Int)>, - - #[serde(default)] - users_default: Int, -} - -impl From for PowerLevelsContentFields { - fn from(pl: IntPowerLevelsContentFields) -> Self { - let IntPowerLevelsContentFields { users, users_default } = pl; - Self { users, users_default } - } -} - -#[derive(Deserialize)] -pub(crate) struct PowerLevelsContentInvite { - #[serde(default, deserialize_with = "deserialize_v1_powerlevel")] - pub(crate) invite: Int, -} - -#[derive(Deserialize)] -struct IntPowerLevelsContentInvite { - #[serde(default)] - invite: Int, -} - -impl From for PowerLevelsContentInvite { - fn from(pl: IntPowerLevelsContentInvite) -> Self { - let IntPowerLevelsContentInvite { invite } = pl; - Self { invite } - } -} - -pub(crate) fn deserialize_power_levels_content_invite( - content: &str, - room_version: &RoomVersionRules, -) -> Result { - if room_version.authorization.integer_power_levels { - from_json_str::(content).map(Into::into) - } else { - from_json_str(content) - } -} - -#[derive(Deserialize)] -pub(crate) struct PowerLevelsContentRedact { - #[serde(default = "default_power_level", deserialize_with = "deserialize_v1_powerlevel")] - pub(crate) redact: Int, -} - -#[derive(Deserialize)] -pub(crate) struct IntPowerLevelsContentRedact { - #[serde(default = "default_power_level")] - redact: Int, -} - -impl From for PowerLevelsContentRedact { - fn from(pl: IntPowerLevelsContentRedact) -> Self { - let IntPowerLevelsContentRedact { redact } = pl; - Self { redact } - } -} - -pub(crate) fn deserialize_power_levels_content_redact( - content: &str, - room_version: &RoomVersionRules, -) -> Result { - if room_version.authorization.integer_power_levels { - from_json_str::(content).map(Into::into) - } else { - from_json_str(content) - } -} diff --git a/src/core/matrix/state_res/serde_backports.rs b/src/core/matrix/state_res/serde_backports.rs deleted file mode 100644 index ef6c4d90a..000000000 --- a/src/core/matrix/state_res/serde_backports.rs +++ /dev/null @@ -1,124 +0,0 @@ -//! These functions are copied from an old version of Ruma. power_levels.rs uses -//! them to lazily deserialize power level events. Upstream Ruma uses a much -//! more elegant approach in its state resolution code, which we may want -//! to look into at some point. - -use std::{fmt, marker::PhantomData}; - -use ruma::{Int, serde::deserialize_v1_powerlevel}; -use serde::{ - Deserialize, Deserializer, - de::{MapAccess, Visitor}, -}; - -/// Take a Map with values of either an integer number or a string and -/// deserialize those to integer numbers in a Vec of sorted pairs. -/// -/// To be used like this: -/// `#[serde(deserialize_with = "vec_deserialize_v1_powerlevel_values")]` -pub(super) fn vec_deserialize_v1_powerlevel_values<'de, D, T>( - de: D, -) -> Result, D::Error> -where - D: Deserializer<'de>, - T: Deserialize<'de> + Ord, -{ - #[repr(transparent)] - struct IntWrap(Int); - - impl<'de> Deserialize<'de> for IntWrap { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - deserialize_v1_powerlevel(deserializer).map(IntWrap) - } - } - - struct IntMapVisitor { - _phantom: PhantomData, - } - - impl IntMapVisitor { - fn new() -> Self { Self { _phantom: PhantomData } } - } - - impl<'de, T> Visitor<'de> for IntMapVisitor - where - T: Deserialize<'de> + Ord, - { - type Value = Vec<(T, Int)>; - - fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { - formatter.write_str("a map with integers or strings as values") - } - - fn visit_map>(self, mut map: A) -> Result { - let mut res = Vec::new(); - if let Some(hint) = map.size_hint() { - res.reserve(hint); - } - - while let Some((k, IntWrap(v))) = map.next_entry()? { - res.push((k, v)); - } - - res.sort_unstable(); - res.dedup_by(|a, b| a.0 == b.0); - - Ok(res) - } - } - - de.deserialize_map(IntMapVisitor::new()) -} - -/// Take a Map with integer values and deserialize those to a Vec of sorted -/// pairs -/// -/// To be used like this: -/// `#[serde(deserialize_with = "vec_deserialize_int_powerlevel_values")]` -pub(super) fn vec_deserialize_int_powerlevel_values<'de, D, T>( - de: D, -) -> Result, D::Error> -where - D: Deserializer<'de>, - T: Deserialize<'de> + Ord, -{ - struct IntMapVisitor { - _phantom: PhantomData, - } - - impl IntMapVisitor { - fn new() -> Self { Self { _phantom: PhantomData } } - } - - impl<'de, T> Visitor<'de> for IntMapVisitor - where - T: Deserialize<'de> + Ord, - { - type Value = Vec<(T, Int)>; - - fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { - formatter.write_str("a map with integers as values") - } - - fn visit_map>(self, mut map: A) -> Result { - let mut res = Vec::new(); - if let Some(hint) = map.size_hint() { - res.reserve(hint); - } - - while let Some(item) = map.next_entry()? { - res.push(item); - } - - res.sort_unstable(); - res.dedup_by(|a, b| a.0 == b.0); - - Ok(res) - } - } - - de.deserialize_map(IntMapVisitor::new()) -} diff --git a/src/core/matrix/state_res/test_utils.rs b/src/core/matrix/state_res/test_utils.rs deleted file mode 100644 index 4d2c9e51b..000000000 --- a/src/core/matrix/state_res/test_utils.rs +++ /dev/null @@ -1,582 +0,0 @@ -use std::{ - borrow::Borrow, - collections::{BTreeMap, HashMap, HashSet}, - sync::atomic::{AtomicU64, Ordering::SeqCst}, -}; - -use futures::future::ready; -use ruma::{ - EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, RoomId, RoomVersionId, ServerSignatures, - UserId, event_id, - events::{ - TimelineEventType, - room::{ - join_rules::{JoinRule, RoomJoinRulesEventContent}, - member::{MembershipState, RoomMemberEventContent}, - }, - }, - int, room_id, - room_version_rules::RoomVersionRules, - uint, user_id, -}; -use serde_json::{ - json, - value::{RawValue as RawJsonValue, to_raw_value as to_raw_json_value}, -}; - -use super::auth_types_for_event; -use crate::{ - Result, info, - matrix::{Event, EventTypeExt, Pdu, StateMap, pdu::EventHash}, -}; - -static SERVER_TIMESTAMP: AtomicU64 = AtomicU64::new(0); - -pub(crate) async fn do_check( - events: &[Pdu], - edges: Vec>, - expected_state_ids: Vec, -) { - // To activate logging use `RUST_LOG=debug cargo t` - - let init_events = INITIAL_EVENTS(); - - let mut store = TestStore( - init_events - .values() - .chain(events) - .map(|ev| (ev.event_id().to_owned(), ev.clone())) - .collect(), - ); - - // This will be lexi_topo_sorted for resolution - let mut graph = HashMap::new(); - // This is the same as in `resolve` event_id -> OriginalStateEvent - let mut fake_event_map = HashMap::new(); - - // Create the DB of events that led up to this point - // TODO maybe clean up some of these clones it is just tests but... - for ev in init_events.values().chain(events) { - graph.insert(ev.event_id().to_owned(), HashSet::new()); - fake_event_map.insert(ev.event_id().to_owned(), ev.clone()); - } - - for pair in INITIAL_EDGES().windows(2) { - if let [a, b] = &pair { - graph - .entry(a.to_owned()) - .or_insert_with(HashSet::new) - .insert(b.clone()); - } - } - - for edge_list in edges { - for pair in edge_list.windows(2) { - if let [a, b] = &pair { - graph - .entry(a.to_owned()) - .or_insert_with(HashSet::new) - .insert(b.clone()); - } - } - } - - // event_id -> Pdu - let mut event_map: HashMap = HashMap::new(); - // event_id -> StateMap - let mut state_at_event: HashMap> = HashMap::new(); - - // Resolve the current state and add it to the state_at_event map then - // continue on in "time" - for node in super::lexicographical_topological_sort(&graph, &|_id| async { - Ok((int!(0), MilliSecondsSinceUnixEpoch(uint!(0)))) - }) - .await - .unwrap() - { - let fake_event = fake_event_map.get(&node).unwrap(); - let event_id = fake_event.event_id().to_owned(); - - let prev_events = graph.get(&node).unwrap(); - - let state_before: StateMap = if prev_events.is_empty() { - HashMap::new() - } else if prev_events.len() == 1 { - state_at_event - .get(prev_events.iter().next().unwrap()) - .unwrap() - .clone() - } else { - let state_sets = prev_events - .iter() - .filter_map(|k| state_at_event.get(k)) - .collect::>(); - - info!( - "{:#?}", - state_sets - .iter() - .map(|map| map - .iter() - .map(|((ty, key), id)| format!("(({ty}{key:?}), {id})")) - .collect::>()) - .collect::>() - ); - - let auth_chain_sets: Vec<_> = state_sets - .iter() - .map(|map| { - store - .auth_event_ids(room_id(), map.values().cloned().collect()) - .unwrap() - }) - .collect(); - - let event_map = &event_map; - let fetch = |id: OwnedEventId| ready(event_map.get(&id).cloned()); - let exists = |id: OwnedEventId| ready(event_map.get(&id).is_some()); - let resolved = super::resolve( - &RoomVersionRules::V6, - state_sets, - &auth_chain_sets, - &fetch, - &exists, - ) - .await; - - match resolved { - | Ok(state) => state, - | Err(e) => panic!("resolution for {node} failed: {e}"), - } - }; - - let mut state_after = state_before.clone(); - - let ty = fake_event.event_type(); - let key = fake_event.state_key().unwrap(); - state_after.insert(ty.with_state_key(key), event_id.to_owned()); - - let auth_types = auth_types_for_event( - fake_event.event_type(), - fake_event.sender(), - fake_event.state_key(), - fake_event.content(), - &RoomVersionRules::V6, - ) - .unwrap(); - - let mut auth_events = vec![]; - for key in auth_types { - if state_before.contains_key(&key) { - auth_events.push(state_before[&key].clone()); - } - } - - // TODO The event is just remade, adding the auth_events and prev_events - // here the `to_pdu_event` was split into `init` and the fn below, - // could be better - let e = fake_event; - let ev_id = e.event_id(); - let event = to_pdu_event( - e.event_id().as_str(), - e.sender(), - e.event_type().clone(), - e.state_key(), - e.content().to_owned(), - &auth_events, - &prev_events.iter().cloned().collect::>(), - ); - - // We have to update our store, an actual user of this lib would - // be giving us state from a DB. - store.0.insert(ev_id.to_owned(), event.clone()); - - state_at_event.insert(node, state_after); - event_map.insert(event_id.to_owned(), store.0.get(ev_id).unwrap().clone()); - } - - let mut expected_state = StateMap::new(); - for node in expected_state_ids { - let ev = event_map.get(&node).unwrap_or_else(|| { - panic!( - "{node} not found in {:?}", - event_map - .keys() - .map(ToString::to_string) - .collect::>() - ) - }); - - let key = ev.event_type().with_state_key(ev.state_key().unwrap()); - - expected_state.insert(key, node); - } - - let start_state = state_at_event.get(event_id!("$START:foo")).unwrap(); - - let end_state = state_at_event - .get(event_id!("$END:foo")) - .unwrap() - .iter() - .filter(|(k, v)| { - expected_state.contains_key(k) - || start_state.get(k) != Some(*v) - // Filter out the dummy messages events. - // These act as points in time where there should be a known state to - // test against. - && **k != ("m.room.message".into(), "dummy".into()) - }) - .map(|(k, v)| (k.clone(), v.clone())) - .collect::>(); - - assert_eq!(expected_state, end_state); -} - -#[allow(clippy::exhaustive_structs)] -pub(crate) struct TestStore(pub(crate) HashMap); - -impl TestStore { - pub(crate) fn get_event(&self, _: &RoomId, event_id: &EventId) -> Result { - self.0 - .get(event_id) - .cloned() - .ok_or_else(|| super::Error::NotFound(format!("{event_id} not found"))) - .map_err(Into::into) - } - - /// Returns a Vec of the related auth events to the given `event`. - pub(crate) fn auth_event_ids( - &self, - room_id: &RoomId, - event_ids: Vec, - ) -> Result> { - let mut result = HashSet::new(); - let mut stack = event_ids; - - // DFS for auth event chain - while let Some(ev_id) = stack.pop() { - if result.contains(&ev_id) { - continue; - } - - result.insert(ev_id.clone()); - - let event = self.get_event(room_id, ev_id.borrow())?; - - stack.extend(event.auth_events().map(ToOwned::to_owned)); - } - - Ok(result) - } -} - -// A StateStore implementation for testing -#[allow(clippy::type_complexity)] -impl TestStore { - pub(crate) fn set_up( - &mut self, - ) -> (StateMap, StateMap, StateMap) { - let create_event = to_pdu_event::<&EventId>( - "CREATE", - alice(), - TimelineEventType::RoomCreate, - Some(""), - to_raw_json_value(&json!({ "creator": alice() })).unwrap(), - &[], - &[], - ); - let cre = create_event.event_id().to_owned(); - self.0.insert(cre.clone(), create_event.clone()); - - let alice_mem = to_pdu_event( - "IMA", - alice(), - TimelineEventType::RoomMember, - Some(alice().as_str()), - member_content_join(), - &[cre.clone()], - &[cre.clone()], - ); - self.0 - .insert(alice_mem.event_id().to_owned(), alice_mem.clone()); - - let join_rules = to_pdu_event( - "IJR", - alice(), - TimelineEventType::RoomJoinRules, - Some(""), - to_raw_json_value(&RoomJoinRulesEventContent::new(JoinRule::Public)).unwrap(), - &[cre.clone(), alice_mem.event_id().to_owned()], - &[alice_mem.event_id().to_owned()], - ); - self.0 - .insert(join_rules.event_id().to_owned(), join_rules.clone()); - - // Bob and Charlie join at the same time, so there is a fork - // this will be represented in the state_sets when we resolve - let bob_mem = to_pdu_event( - "IMB", - bob(), - TimelineEventType::RoomMember, - Some(bob().as_str()), - member_content_join(), - &[cre.clone(), join_rules.event_id().to_owned()], - &[join_rules.event_id().to_owned()], - ); - self.0 - .insert(bob_mem.event_id().to_owned(), bob_mem.clone()); - - let charlie_mem = to_pdu_event( - "IMC", - charlie(), - TimelineEventType::RoomMember, - Some(charlie().as_str()), - member_content_join(), - &[cre, join_rules.event_id().to_owned()], - &[join_rules.event_id().to_owned()], - ); - self.0 - .insert(charlie_mem.event_id().to_owned(), charlie_mem.clone()); - - let state_at_bob = [&create_event, &alice_mem, &join_rules, &bob_mem] - .iter() - .map(|e| { - (e.event_type().with_state_key(e.state_key().unwrap()), e.event_id().to_owned()) - }) - .collect::>(); - - let state_at_charlie = [&create_event, &alice_mem, &join_rules, &charlie_mem] - .iter() - .map(|e| { - (e.event_type().with_state_key(e.state_key().unwrap()), e.event_id().to_owned()) - }) - .collect::>(); - - let expected = [&create_event, &alice_mem, &join_rules, &bob_mem, &charlie_mem] - .iter() - .map(|e| { - (e.event_type().with_state_key(e.state_key().unwrap()), e.event_id().to_owned()) - }) - .collect::>(); - - (state_at_bob, state_at_charlie, expected) - } -} - -pub(crate) fn event_id(id: &str) -> OwnedEventId { - if id.contains('$') { - return id.try_into().unwrap(); - } - - format!("${id}:foo").try_into().unwrap() -} - -pub(crate) fn alice() -> &'static UserId { user_id!("@alice:foo") } - -pub(crate) fn bob() -> &'static UserId { user_id!("@bob:foo") } - -pub(crate) fn charlie() -> &'static UserId { user_id!("@charlie:foo") } - -pub(crate) fn ella() -> &'static UserId { user_id!("@ella:foo") } - -pub(crate) fn zara() -> &'static UserId { user_id!("@zara:foo") } - -pub(crate) fn room_id() -> &'static RoomId { room_id!("!test:foo") } - -pub(crate) fn member_content_ban() -> Box { - to_raw_json_value(&RoomMemberEventContent::new(MembershipState::Ban)).unwrap() -} - -pub(crate) fn member_content_join() -> Box { - to_raw_json_value(&RoomMemberEventContent::new(MembershipState::Join)).unwrap() -} - -pub(crate) fn to_init_pdu_event( - id: &str, - sender: &UserId, - ev_type: TimelineEventType, - state_key: Option<&str>, - content: Box, -) -> Pdu { - let ts = SERVER_TIMESTAMP.fetch_add(1, SeqCst); - let id = if id.contains('$') { - id.to_owned() - } else { - format!("${id}:foo") - }; - - Pdu { - event_id: id.try_into().unwrap(), - room_id: Some(room_id().to_owned()), - sender: sender.to_owned(), - origin_server_ts: ts.try_into().unwrap(), - state_key: state_key.map(Into::into), - kind: ev_type, - content, - origin: None, - redacts: None, - sticky: None, - unsigned: None, - auth_events: vec![], - prev_events: vec![], - depth: uint!(0), - hashes: EventHash { sha256: "".to_owned() }, - signatures: None, - } -} - -pub(crate) fn to_pdu_event( - id: &str, - sender: &UserId, - ev_type: TimelineEventType, - state_key: Option<&str>, - content: Box, - auth_events: &[S], - prev_events: &[S], -) -> Pdu -where - S: AsRef, -{ - let ts = SERVER_TIMESTAMP.fetch_add(1, SeqCst); - let id = if id.contains('$') { - id.to_owned() - } else { - format!("${id}:foo") - }; - let auth_events = auth_events - .iter() - .map(AsRef::as_ref) - .map(event_id) - .collect::>(); - let prev_events = prev_events - .iter() - .map(AsRef::as_ref) - .map(event_id) - .collect::>(); - - Pdu { - event_id: id.try_into().unwrap(), - room_id: Some(room_id().to_owned()), - sender: sender.to_owned(), - origin_server_ts: ts.try_into().unwrap(), - state_key: state_key.map(Into::into), - kind: ev_type, - content, - origin: None, - redacts: None, - sticky: None, - unsigned: None, - auth_events, - prev_events, - depth: uint!(0), - hashes: EventHash { sha256: "".to_owned() }, - signatures: None, - } -} - -// all graphs start with these input events -#[allow(non_snake_case)] -pub(crate) fn INITIAL_EVENTS() -> HashMap { - vec![ - to_pdu_event::<&EventId>( - "CREATE", - alice(), - TimelineEventType::RoomCreate, - Some(""), - to_raw_json_value(&json!({ "creator": alice() })).unwrap(), - &[], - &[], - ), - to_pdu_event( - "IMA", - alice(), - TimelineEventType::RoomMember, - Some(alice().as_str()), - member_content_join(), - &["CREATE"], - &["CREATE"], - ), - to_pdu_event( - "IPOWER", - alice(), - TimelineEventType::RoomPowerLevels, - Some(""), - to_raw_json_value(&json!({ "users": { alice(): 100 } })).unwrap(), - &["CREATE", "IMA"], - &["IMA"], - ), - to_pdu_event( - "IJR", - alice(), - TimelineEventType::RoomJoinRules, - Some(""), - to_raw_json_value(&RoomJoinRulesEventContent::new(JoinRule::Public)).unwrap(), - &["CREATE", "IMA", "IPOWER"], - &["IPOWER"], - ), - to_pdu_event( - "IMB", - bob(), - TimelineEventType::RoomMember, - Some(bob().as_str()), - member_content_join(), - &["CREATE", "IJR", "IPOWER"], - &["IJR"], - ), - to_pdu_event( - "IMC", - charlie(), - TimelineEventType::RoomMember, - Some(charlie().as_str()), - member_content_join(), - &["CREATE", "IJR", "IPOWER"], - &["IMB"], - ), - to_pdu_event::<&EventId>( - "START", - charlie(), - TimelineEventType::RoomMessage, - Some("dummy"), - to_raw_json_value(&json!({})).unwrap(), - &[], - &[], - ), - to_pdu_event::<&EventId>( - "END", - charlie(), - TimelineEventType::RoomMessage, - Some("dummy"), - to_raw_json_value(&json!({})).unwrap(), - &[], - &[], - ), - ] - .into_iter() - .map(|ev| (ev.event_id().to_owned(), ev)) - .collect() -} - -// all graphs start with these input events -#[allow(non_snake_case)] -pub(crate) fn INITIAL_EVENTS_CREATE_ROOM() -> HashMap { - vec![to_pdu_event::<&EventId>( - "CREATE", - alice(), - TimelineEventType::RoomCreate, - Some(""), - to_raw_json_value(&json!({ "creator": alice() })).unwrap(), - &[], - &[], - )] - .into_iter() - .map(|ev| (ev.event_id().to_owned(), ev)) - .collect() -} - -#[allow(non_snake_case)] -pub(crate) fn INITIAL_EDGES() -> Vec { - vec!["START", "IMC", "IMB", "IJR", "IPOWER", "IMA", "CREATE"] - .into_iter() - .map(event_id) - .collect::>() -} diff --git a/src/core/mod.rs b/src/core/mod.rs index f176593c4..b42afb62a 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -35,7 +35,7 @@ pub use conduwuit_build_metadata as build_metadata; pub use config::Config; pub use error::Error; pub use info::version::*; -pub use matrix::{Event, EventTypeExt, Pdu, PduCount, PduEvent, PduId, pdu, state_res}; +pub use matrix::{Event, EventTypeExt, Pdu, PduCount, PduEvent, PduId, pdu}; pub use parking_lot::{ Mutex as SyncMutex, RwLock as SyncRwLock, RwLockReadGuard as SyncRwLockReadGuard, RwLockWriteGuard as SyncRwLockWriteGuard, diff --git a/src/database/map/qry.rs b/src/database/map/qry.rs index 79f95dade..9aeb102f4 100644 --- a/src/database/map/qry.rs +++ b/src/database/map/qry.rs @@ -53,4 +53,21 @@ impl super::Map { let key = ser::serialize(buf, key).expect("failed to serialize query key"); self.get(key) } + + pub fn qry_blocking(self: &Arc, key: &K) -> Result> + where + K: Serialize + ?Sized + Debug, + { + let mut buf = KeyBuf::new(); + self.bqry_blocking(key, &mut buf) + } + + pub fn bqry_blocking(self: &Arc, key: &K, buf: &mut B) -> Result> + where + K: Serialize + ?Sized + Debug, + B: Write + AsRef<[u8]>, + { + let key = ser::serialize(buf, key).expect("failed to serialize query key"); + self.get_blocking(key) + } } diff --git a/src/service/rooms/event_handler/fetch_auth.rs b/src/service/rooms/event_handler/fetch_auth.rs index c807589dc..540b6b540 100644 --- a/src/service/rooms/event_handler/fetch_auth.rs +++ b/src/service/rooms/event_handler/fetch_auth.rs @@ -1,6 +1,8 @@ use std::collections::{HashMap, hash_map}; -use conduwuit::{Err, Event, EventTypeExt, PduEvent, Result, err, warn}; +use conduwuit::{ + Err, Event, EventTypeExt, PduEvent, Result, debug_info, err, utils::TryFutureExtExt, warn, +}; use ruma::{ OwnedEventId, ServerName, api::federation::authorization::get_event_authorization, room_version_rules::RoomVersionRules, @@ -191,15 +193,8 @@ impl super::Service { }, } } - if !self - .auth_state_check_4( - &pdu, - room_version_rules, - create_event.as_pdu(), - &auth_events_by_key, - ) - .await? - { + if let Some(msg) = self.auth_state_check_4(&pdu, room_version_rules) { + debug_info!("Rejecting auth chain event {event_id}: {msg}"); self.reject_and_persist(&event_id, &pdu.to_canonical_object()); } } diff --git a/src/service/rooms/event_handler/handle_outlier_pdu.rs b/src/service/rooms/event_handler/handle_outlier_pdu.rs index ee81a0c04..27ec23049 100644 --- a/src/service/rooms/event_handler/handle_outlier_pdu.rs +++ b/src/service/rooms/event_handler/handle_outlier_pdu.rs @@ -195,18 +195,10 @@ impl super::Service { } } - if !self - .auth_state_check_4( - &pdu_event, - &room_version_rules, - create_event.as_pdu(), - &auth_events_by_key, - ) - .await? - { + if let Some(msg) = self.auth_state_check_4(&pdu_event, &room_version_rules) { self.reject_and_persist(event_id, &incoming_pdu); return Err!(Request(Forbidden(debug_warn!( - "Event authorisation fails based on event's claimed auth events" + "Event authorisation fails based on event's claimed auth events: {msg}" )))); } diff --git a/src/service/rooms/event_handler/mod.rs b/src/service/rooms/event_handler/mod.rs index 75c12b73e..3691cf3b6 100644 --- a/src/service/rooms/event_handler/mod.rs +++ b/src/service/rooms/event_handler/mod.rs @@ -17,10 +17,14 @@ use std::{collections::HashMap, fmt::Write, sync::Arc, time::Instant}; use assign::assign; use async_trait::async_trait; -use conduwuit::{Err, Error, Event, PduEvent, Result, Server, SyncRwLock, utils::MutexMap}; +use conduwuit::{ + Err, Error, Event, PduEvent, Result, Server, SyncRwLock, + utils::{MutexMap, TryFutureExtExt}, +}; pub use fetch_and_handle_outliers::{ DagBuilderTree, GET_MISSING_EVENTS_MAX_BATCH_SIZE, build_local_dag, }; +use futures::future::ok; use http::StatusCode; use ruma::{ EventId, OwnedEventId, OwnedRoomId, OwnedServerName, @@ -131,8 +135,8 @@ impl Service { } /// Fetches a single PDU, returning None if there is an error. - async fn event_fetch(&self, event_id: OwnedEventId) -> Option { - self.services.timeline.get_pdu(&event_id).await.ok() + fn event_fetch(&self, event_id: &EventId) -> Option { + self.services.timeline.get_pdu_blocking(event_id).ok() } /// Returns a rate-limit error if the requested event had a recent failed diff --git a/src/service/rooms/event_handler/pdu_checks.rs b/src/service/rooms/event_handler/pdu_checks.rs index af0338a97..a45528488 100644 --- a/src/service/rooms/event_handler/pdu_checks.rs +++ b/src/service/rooms/event_handler/pdu_checks.rs @@ -3,7 +3,7 @@ use std::collections::HashMap; use base64::Engine; use conduwuit::{ Err, Event, EventTypeExt, PduEvent, Result, debug, debug::DebugInspect, debug_error, - debug_info, err, info, matrix::StateKey, state_res, trace, + debug_info, err, info, matrix::StateKey, trace, }; use futures::future::ready; use ruma::{ @@ -12,6 +12,8 @@ use ruma::{ canonical_json::redact, events::StateEventType, room_version_rules::{EventIdFormatVersion, RoomVersionRules}, + state_res, + state_res::check_state_independent_auth_rules, }; use crate::rooms::{ @@ -144,24 +146,21 @@ impl super::Service { /// Checks PDU check 4: Passes authorisation rules based on the event's auth /// events ([spec]). /// - /// If the auth check fails, false is returned, otherwise true. + /// If the auth check fails, the error message is returned, otherwise + /// `None`. /// /// [spec]: https://spec.matrix.org/v1.19/server-server-api/#checks-performed-on-receipt-of-a-pdu - pub async fn auth_state_check_4( + pub fn auth_state_check_4( &self, pdu: &PduEvent, room_version_rules: &RoomVersionRules, - create_event: &PduEvent, - auth_events_by_key: &HashMap<(StateEventType, StateKey), PduEvent>, - ) -> Result { - let state_fetch = |ty: &StateEventType, sk: &str| { - let key = (ty.to_owned(), sk.into()); - ready(auth_events_by_key.get(&key).map(ToOwned::to_owned)) - }; - - state_res::event_auth::auth_check(room_version_rules, pdu, state_fetch, create_event) - .await - .map_err(|e| err!("Event self-authentication failed: {e:?}")) + ) -> Option { + match check_state_independent_auth_rules(&room_version_rules.authorization, pdu, |e| { + self.event_fetch(e) + }) { + | Ok(_) => None, + | Err(msg) => Some(msg), + } } /// Checks that the event passes PDU check 5, which ensures that the event @@ -177,7 +176,7 @@ impl super::Service { room_version_rules: &RoomVersionRules, create_event: &PduEvent, origin: &ServerName, - ) -> Result<(bool, HashMap)> { + ) -> Result<(Option, HashMap)> { debug!( event_id = %incoming_pdu.event_id, "Resolving state at event" @@ -218,29 +217,27 @@ impl super::Service { } trace!(state_events = state_before.len(), "Calculated incoming state"); - let state_fetch_state = &state_before; - let state_fetch = |k: StateEventType, s: StateKey| async move { - let shortstatekey = self.services.short.get_shortstatekey(&k, &s).await.ok()?; - - let event_id = state_fetch_state.get(&shortstatekey)?; - self.services.timeline.get_pdu(event_id).await.ok() - }; - debug!( event_id = %incoming_pdu.event_id, "Running state-before auth check" ); // PDU check: 5 - let auth_check = state_res::event_auth::auth_check( - room_version_rules, + let state_fetch_state = &state_before; + let auth_result = state_res::check_state_dependent_auth_rules( + &room_version_rules.authorization, incoming_pdu, - |ty, sk| state_fetch(ty.clone(), sk.into()), - create_event.as_pdu(), - ) - .await - .map_err(|e| err!(Request(Forbidden("Auth check failed: {e:?}"))))?; - Ok((auth_check, state_before)) + |event_type, state_key| { + let ssk = self + .services + .short + .get_shortstatekey_blocking(event_type, state_key) + .ok()?; + let event_id = state_fetch_state.get(&ssk)?; + self.services.timeline.get_pdu_blocking(event_id).ok() + }, + ); + Ok((auth_result.err(), state_before)) } /// Checks that the event passes PDU check 6, which ensures that the event @@ -253,8 +250,7 @@ impl super::Service { &self, incoming_pdu: &PduEvent, room_version_rules: &RoomVersionRules, - create_event: &PduEvent, - ) -> Result { + ) -> Result<(), String> { debug!( event_id = %incoming_pdu.event_id, "Gathering auth events" @@ -270,25 +266,22 @@ impl super::Service { incoming_pdu.content(), room_version_rules, ) - .await?; - - let state_fetch = |k: &StateEventType, s: &str| { - let key = k.with_state_key(s); - ready(auth_events.get(&key).map(ToOwned::to_owned)) - }; + .await + .map_err(|e| e.to_string())?; debug!( event_id = %incoming_pdu.event_id, "Running current state auth check" ); - state_res::event_auth::auth_check( - room_version_rules, + state_res::check_state_dependent_auth_rules( + &room_version_rules.authorization, incoming_pdu, - state_fetch, - create_event.as_pdu(), + |t, s| { + auth_events + .get(&(t.to_owned(), s.to_owned())) + .map(ToOwned::to_owned) + }, ) - .await - .map_err(|e| err!(Request(Forbidden("Auth check failed: {e:?}")))) } /// Performs PDU check 7 - does the policy server allow this event. diff --git a/src/service/rooms/event_handler/resolve_state.rs b/src/service/rooms/event_handler/resolve_state.rs index 4f35ceecb..50a49a208 100644 --- a/src/service/rooms/event_handler/resolve_state.rs +++ b/src/service/rooms/event_handler/resolve_state.rs @@ -1,6 +1,6 @@ use std::{ borrow::Borrow, - collections::{HashMap, HashSet}, + collections::{HashMap, HashSet, VecDeque}, sync::Arc, }; @@ -9,13 +9,21 @@ use conduwuit::{ state_res::{self, StateMap}, trace, utils::stream::{IterStream, ReadyExt, TryWidebandExt, WidebandExt}, + warn, }; use futures::{FutureExt, StreamExt, TryFutureExt, TryStreamExt, future::try_join}; -use ruma::{OwnedEventId, RoomId, room_version_rules::RoomVersionRules}; +use itertools::cloned; +use ruma::{ + EventId, OwnedEventId, RoomId, + room_version_rules::RoomVersionRules, + state_res::{Event, utils::event_id_set::EventIdSet}, +}; use crate::rooms::state_compressor::CompressedState; impl super::Service { + /// Resolves the state based on the incoming fork states with the given room + /// version rules. pub async fn resolve_state( &self, room_id: &RoomId, @@ -48,7 +56,7 @@ impl super::Service { .event_ids_iter(room_id, state.values().map(Borrow::borrow)) .try_collect() }) - .try_collect::>>(); + .try_collect::>>(); let fork_states = fork_states .iter() @@ -60,19 +68,19 @@ impl super::Service { .short .multi_get_statekey_from_short(shortstatekeys) .zip(event_ids) - .ready_filter_map(|(ty_sk, id)| Some((ty_sk.ok()?, id))) + .ready_filter_map(|(ty_sk, id)| { + Some((ty_sk.ok().map(|(k, s)| (k, s.to_string()))?, id)) + }) .collect() }) .map(Ok::<_, Error>) - .try_collect::>>(); + .try_collect::>>(); let (fork_states, auth_chain_sets) = try_join(fork_states, auth_chain_sets).await?; trace!("Resolving state"); - let state = self - .state_resolution(room_version_rules, fork_states.iter(), &auth_chain_sets) - .boxed() - .await?; + let state = + self.state_resolution(room_version_rules, fork_states.as_slice(), auth_chain_sets)?; trace!("State resolution done."); let state_events: Vec<_> = state @@ -98,25 +106,72 @@ impl super::Service { Ok(Arc::new(new_room_state)) } - pub async fn state_resolution<'a, StateSets>( + /// Actually performs state resolution from the given state sets, auth chain + /// sets, and room version rules. + pub fn state_resolution<'a>( &'a self, room_version_rules: &'a RoomVersionRules, - state_sets: StateSets, - auth_chain_sets: &'a [HashSet], - ) -> Result> - where - StateSets: Iterator> + Clone + Send, - { + state_sets: &[ruma::state_res::StateMap], + auth_chain_sets: Vec>, + ) -> Result> { let event_fetch = |event_id| self.event_fetch(event_id); - let event_exists = |event_id| self.event_exists(event_id); - state_res::resolve( - room_version_rules, + let calc_subgraph = |ss| self.calculate_conflicted_state_subgraph(ss); + ruma::state_res::resolve( + &room_version_rules.authorization, + &room_version_rules + .state_res + .v2_rules() + .expect("can only use state resolution v2 rules"), state_sets, auth_chain_sets, - &event_fetch, - &event_exists, + event_fetch, + calc_subgraph, ) - .map_err(|e| err!(error!("State resolution failed: {e:?}"))) - .await + .map_err(|e| err!(error!("state resolution failed: {e:?}"))) + } + + /// Calculates the conflicted state subgraph for state resolution. + fn calculate_conflicted_state_subgraph<'a>( + &self, + state_set: &ruma::state_res::StateMap<&'a EventId>, + ) -> Result> { + let conflicted_events: HashSet<_> = state_set.values().cloned().collect(); + let mut subgraph: HashSet<_> = HashSet::new(); + let mut stack: Vec> = vec![conflicted_events.iter().cloned().collect::>()]; + let mut path: Vec<_> = Vec::new(); + let mut seen: HashSet<_> = HashSet::new(); + let next_event = |stack: &mut Vec>, path: &mut Vec<_>| { + while stack.last().is_some_and(Vec::is_empty) { + stack.pop(); + path.pop(); + } + stack.last_mut().and_then(Vec::pop) + }; + while let Some(event_id) = next_event(&mut stack, &mut path) { + path.push(event_id.clone()); + if subgraph.contains(&event_id) { + if path.len() > 1 { + subgraph.extend(path.iter().cloned()); + } + path.pop(); + continue; + } + if conflicted_events.contains(&event_id) && path.len() > 1 { + subgraph.extend(path.iter().cloned()); + } + if seen.contains(&event_id) { + path.pop(); + continue; + } + trace!(event_id = event_id.as_str(), "fetching event for its auth events"); + let Some(evt) = self.event_fetch(event_id.clone()) else { + warn!("could not fetch event {} to calculate conflicted subgraph", event_id); + path.pop(); + continue; + }; + stack.push(evt.auth_events().collect()); + seen.insert(event_id); + } + Ok(subgraph) } } diff --git a/src/service/rooms/event_handler/upgrade_outlier_pdu.rs b/src/service/rooms/event_handler/upgrade_outlier_pdu.rs index af3d5b7de..f66d272b8 100644 --- a/src/service/rooms/event_handler/upgrade_outlier_pdu.rs +++ b/src/service/rooms/event_handler/upgrade_outlier_pdu.rs @@ -65,14 +65,14 @@ impl super::Service { // perform PDU check 5 (event auth passes based on state before the // event). To do this, we either need to have all the prev events // locally, or ask a remote server for the state at the event. - let (passes_state_before, state_before) = self + let (state_before_failure_reason, state_before) = self .state_before_check_5(&incoming_pdu, &room_version_rules, create_event, origin) .await?; - if !passes_state_before { + if let Some(msg) = state_before_failure_reason { self.reject_and_persist(incoming_pdu.event_id(), &val); return Err!(Request(Forbidden(debug_warn!( - "Event authorisation fails based on the state before the event" + "Event authorisation fails based on the state before the event: {msg}" )))); } @@ -90,16 +90,16 @@ impl super::Service { ); let state_lock = self.services.state.mutex.lock(room_id).await; let passes_current_state = self - .current_state_check_6(&incoming_pdu, &room_version_rules, create_event) + .current_state_check_6(&incoming_pdu, &room_version_rules) .await - .inspect(|passes| { - if !*passes { - debug_warn!( - "Event authorisation fails based on the current room state - will be \ - soft-failed" - ); - } - })?; + .inspect_err(|reason| { + debug_warn!( + %reason, + "Event authorisation fails based on the current room state - will be \ + soft-failed" + ); + }) + .is_ok(); // Determine whether this PDU should be soft-failed. // If the auth check failed, invariably yes. Otherwise, only if the user diff --git a/src/service/rooms/short/mod.rs b/src/service/rooms/short/mod.rs index 53d70fe85..add982071 100644 --- a/src/service/rooms/short/mod.rs +++ b/src/service/rooms/short/mod.rs @@ -154,6 +154,15 @@ impl Service { .deserialized() } + pub fn get_shortstatekey_blocking( + &self, + event_type: &StateEventType, + state_key: &str, + ) -> Result { + let key = (event_type, state_key); + self.db.shortstatekey_statekey.qry() + } + /// Gets a full event ID from a short event ID. pub async fn get_eventid_from_short(&self, shorteventid: ShortEventId) -> Result where diff --git a/src/service/rooms/state/mod.rs b/src/service/rooms/state/mod.rs index 147e944a7..368a7216c 100644 --- a/src/service/rooms/state/mod.rs +++ b/src/service/rooms/state/mod.rs @@ -1,11 +1,10 @@ use std::{collections::HashMap, fmt::Write, sync::Arc}; use async_trait::async_trait; -use conduwuit::{debug, utils::stream::WidebandExt}; +use conduwuit::{debug, matrix::StateKey, smallstr::SmallString, utils::stream::WidebandExt}; use conduwuit_core::{ Event, PduEvent, Result, err, result::FlatOk, - state_res::{self, StateMap}, utils::{ IterStream, MutexMap, MutexMapGuard, ReadyExt, calculate_hash, stream::{BroadbandExt, TryIgnore}, @@ -21,6 +20,7 @@ use ruma::{ api::federation::membership::RawStrippedState, events::{StateEventType, TimelineEventType, room::create::RoomCreateEventContent}, room_version_rules::RoomVersionRules, + state_res, }; use crate::{ @@ -472,7 +472,7 @@ impl Service { state_key: Option<&str>, content: &serde_json::value::RawValue, room_version_rules: &RoomVersionRules, - ) -> Result> { + ) -> Result> { let Ok(shortstatehash) = self.get_room_shortstatehash(room_id).await else { return Ok(HashMap::new()); }; @@ -482,8 +482,9 @@ impl Service { sender, state_key, content, - room_version_rules, - )?; + &room_version_rules.authorization, + ) + .map_err(|e| err!("failed to select auth types for event: {e:?}"))?; debug!(?auth_types, "Auth types for event"); let sauthevents: HashMap<_, _> = auth_types .iter() diff --git a/src/service/rooms/timeline/data.rs b/src/service/rooms/timeline/data.rs index 560bbae84..e11f192ca 100644 --- a/src/service/rooms/timeline/data.rs +++ b/src/service/rooms/timeline/data.rs @@ -161,6 +161,22 @@ impl Data { select_ok([accepted, outlier]).await.map(at!(0)) } + pub(super) fn get_pdu_blocking(&self, event_id: &EventId) -> Result { + if let Ok(outlier) = self + .eventid_outlierpdu + .get_blocking(event_id)? + .deserialized() + { + return Ok(outlier); + }; + + let pdu_id = self + .eventid_pduid + .get_blocking(event_id) + .map(|h| RawPduId::from(&*h))?; + self.pduid_pdu.get_blocking(&pdu_id)?.deserialized() + } + /// Like get_non_outlier_pdu(), but without the expense of fetching and /// parsing the PduEvent #[inline] diff --git a/src/service/rooms/timeline/mod.rs b/src/service/rooms/timeline/mod.rs index 715d52855..10db534b0 100644 --- a/src/service/rooms/timeline/mod.rs +++ b/src/service/rooms/timeline/mod.rs @@ -211,6 +211,12 @@ impl Service { self.db.get_pdu(event_id).await } + /// Returns the PDU. Checks the outlier tree BEFORE the timeline tree, as + /// this operation is synchronous. + pub fn get_pdu_blocking(&self, event_id: &EventId) -> Result { + self.db.get_pdu_blocking(event_id) + } + /// Returns the pdu. /// /// This does __NOT__ check the outliers `Tree`.