diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 00000000..9a782c56 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,2 @@ +[env] +RUMA_UNSTABLE_EXHAUSTIVE_TYPES = "true" diff --git a/Cargo.lock b/Cargo.lock index 850067ff..2f7b15c3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -720,6 +720,18 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "console" +version = "0.15.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8" +dependencies = [ + "encode_unicode", + "libc", + "once_cell", + "windows-sys 0.59.0", +] + [[package]] name = "const-oid" version = "0.9.6" @@ -1143,6 +1155,12 @@ dependencies = [ "serde", ] +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + [[package]] name = "enum-as-inner" version = "0.6.1" @@ -1989,6 +2007,18 @@ version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8fae54786f62fb2918dcfae3d568594e50eb9b5c25bf04371af6fe7516452fb" +[[package]] +name = "insta" +version = "1.43.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "154934ea70c58054b556dd430b99a98c2a7ff5309ac9891597e339b5c28f4371" +dependencies = [ + "console", + "once_cell", + "serde", + "similar", +] + [[package]] name = "interpolate_name" version = "0.2.4" @@ -3439,8 +3469,8 @@ dependencies = [ [[package]] name = "ruma" -version = "0.10.1" -source = "git+https://github.com/matrix-construct/ruma?rev=0155c2b33233bec9dece79d5134a9574b347f4c1#0155c2b33233bec9dece79d5134a9574b347f4c1" +version = "0.12.6" +source = "git+https://github.com/matrix-construct/ruma?rev=3d3acddfcf96891f1203ca3c36d8f41932ede50f#3d3acddfcf96891f1203ca3c36d8f41932ede50f" dependencies = [ "assign", "js_int", @@ -3451,7 +3481,6 @@ dependencies = [ "ruma-events", "ruma-federation-api", "ruma-identifiers-validation", - "ruma-identity-service-api", "ruma-push-gateway-api", "ruma-signatures", "web-time", @@ -3459,8 +3488,8 @@ dependencies = [ [[package]] name = "ruma-appservice-api" -version = "0.10.0" -source = "git+https://github.com/matrix-construct/ruma?rev=0155c2b33233bec9dece79d5134a9574b347f4c1#0155c2b33233bec9dece79d5134a9574b347f4c1" +version = "0.12.2" +source = "git+https://github.com/matrix-construct/ruma?rev=3d3acddfcf96891f1203ca3c36d8f41932ede50f#3d3acddfcf96891f1203ca3c36d8f41932ede50f" dependencies = [ "js_int", "ruma-common", @@ -3471,8 +3500,8 @@ dependencies = [ [[package]] name = "ruma-client-api" -version = "0.18.0" -source = "git+https://github.com/matrix-construct/ruma?rev=0155c2b33233bec9dece79d5134a9574b347f4c1#0155c2b33233bec9dece79d5134a9574b347f4c1" +version = "0.20.4" +source = "git+https://github.com/matrix-construct/ruma?rev=3d3acddfcf96891f1203ca3c36d8f41932ede50f#3d3acddfcf96891f1203ca3c36d8f41932ede50f" dependencies = [ "as_variant", "assign", @@ -3494,8 +3523,8 @@ dependencies = [ [[package]] name = "ruma-common" -version = "0.13.0" -source = "git+https://github.com/matrix-construct/ruma?rev=0155c2b33233bec9dece79d5134a9574b347f4c1#0155c2b33233bec9dece79d5134a9574b347f4c1" +version = "0.15.4" +source = "git+https://github.com/matrix-construct/ruma?rev=3d3acddfcf96891f1203ca3c36d8f41932ede50f#3d3acddfcf96891f1203ca3c36d8f41932ede50f" dependencies = [ "as_variant", "base64", @@ -3522,12 +3551,13 @@ dependencies = [ "uuid", "web-time", "wildmatch", + "zeroize", ] [[package]] name = "ruma-events" -version = "0.28.1" -source = "git+https://github.com/matrix-construct/ruma?rev=0155c2b33233bec9dece79d5134a9574b347f4c1#0155c2b33233bec9dece79d5134a9574b347f4c1" +version = "0.30.5" +source = "git+https://github.com/matrix-construct/ruma?rev=3d3acddfcf96891f1203ca3c36d8f41932ede50f#3d3acddfcf96891f1203ca3c36d8f41932ede50f" dependencies = [ "as_variant", "indexmap", @@ -3547,12 +3577,13 @@ dependencies = [ "url", "web-time", "wildmatch", + "zeroize", ] [[package]] name = "ruma-federation-api" -version = "0.9.0" -source = "git+https://github.com/matrix-construct/ruma?rev=0155c2b33233bec9dece79d5134a9574b347f4c1#0155c2b33233bec9dece79d5134a9574b347f4c1" +version = "0.11.2" +source = "git+https://github.com/matrix-construct/ruma?rev=3d3acddfcf96891f1203ca3c36d8f41932ede50f#3d3acddfcf96891f1203ca3c36d8f41932ede50f" dependencies = [ "bytes", "headers", @@ -3573,27 +3604,17 @@ dependencies = [ [[package]] name = "ruma-identifiers-validation" -version = "0.9.5" -source = "git+https://github.com/matrix-construct/ruma?rev=0155c2b33233bec9dece79d5134a9574b347f4c1#0155c2b33233bec9dece79d5134a9574b347f4c1" +version = "0.10.1" +source = "git+https://github.com/matrix-construct/ruma?rev=3d3acddfcf96891f1203ca3c36d8f41932ede50f#3d3acddfcf96891f1203ca3c36d8f41932ede50f" dependencies = [ "js_int", "thiserror 2.0.12", ] -[[package]] -name = "ruma-identity-service-api" -version = "0.9.0" -source = "git+https://github.com/matrix-construct/ruma?rev=0155c2b33233bec9dece79d5134a9574b347f4c1#0155c2b33233bec9dece79d5134a9574b347f4c1" -dependencies = [ - "js_int", - "ruma-common", - "serde", -] - [[package]] name = "ruma-macros" -version = "0.13.0" -source = "git+https://github.com/matrix-construct/ruma?rev=0155c2b33233bec9dece79d5134a9574b347f4c1#0155c2b33233bec9dece79d5134a9574b347f4c1" +version = "0.15.2" +source = "git+https://github.com/matrix-construct/ruma?rev=3d3acddfcf96891f1203ca3c36d8f41932ede50f#3d3acddfcf96891f1203ca3c36d8f41932ede50f" dependencies = [ "cfg-if", "proc-macro-crate", @@ -3607,8 +3628,8 @@ dependencies = [ [[package]] name = "ruma-push-gateway-api" -version = "0.9.0" -source = "git+https://github.com/matrix-construct/ruma?rev=0155c2b33233bec9dece79d5134a9574b347f4c1#0155c2b33233bec9dece79d5134a9574b347f4c1" +version = "0.11.0" +source = "git+https://github.com/matrix-construct/ruma?rev=3d3acddfcf96891f1203ca3c36d8f41932ede50f#3d3acddfcf96891f1203ca3c36d8f41932ede50f" dependencies = [ "js_int", "ruma-common", @@ -3619,8 +3640,8 @@ dependencies = [ [[package]] name = "ruma-signatures" -version = "0.15.0" -source = "git+https://github.com/matrix-construct/ruma?rev=0155c2b33233bec9dece79d5134a9574b347f4c1#0155c2b33233bec9dece79d5134a9574b347f4c1" +version = "0.17.1" +source = "git+https://github.com/matrix-construct/ruma?rev=3d3acddfcf96891f1203ca3c36d8f41932ede50f#3d3acddfcf96891f1203ca3c36d8f41932ede50f" dependencies = [ "base64", "ed25519-dalek", @@ -4206,6 +4227,12 @@ dependencies = [ "quote", ] +[[package]] +name = "similar" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" + [[package]] name = "simple_asn1" version = "0.6.3" @@ -4977,6 +5004,7 @@ dependencies = [ "hardened_malloc-rs", "http", "http-body-util", + "insta", "ipaddress", "itertools 0.14.0", "jsonwebtoken", @@ -4996,6 +5024,7 @@ dependencies = [ "serde_json", "serde_regex", "serde_yaml", + "similar", "smallstr", "smallvec", "thiserror 2.0.12", diff --git a/Cargo.toml b/Cargo.toml index f69e59fb..3a225798 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -219,6 +219,12 @@ features = [ "webp", ] +[workspace.dependencies.insta] +version = "1.43.1" +features = [ + "json", +] + [workspace.dependencies.ipaddress] version = "0.1" @@ -311,20 +317,18 @@ default-features = false [workspace.dependencies.ruma] git = "https://github.com/matrix-construct/ruma" -rev = "0155c2b33233bec9dece79d5134a9574b347f4c1" +rev = "3d3acddfcf96891f1203ca3c36d8f41932ede50f" features = [ - "compat", + "__compat", "rand", "appservice-api-c", "client-api", "federation-api", "markdown", "push-gateway-api-c", - "unstable-exhaustive-types", "ring-compat", "compat-upload-signatures", "identifiers-validation", - "unstable-unspecified", "unstable-msc2448", "unstable-msc2666", "unstable-msc2867", @@ -332,10 +336,8 @@ features = [ "unstable-msc3026", "unstable-msc3061", "unstable-msc3245", - "unstable-msc3266", "unstable-msc3381", # polls "unstable-msc3489", # beacon / live location - "unstable-msc3575", "unstable-msc3930", # polls push rules "unstable-msc4075", "unstable-msc4095", @@ -343,9 +345,9 @@ features = [ "unstable-msc4125", "unstable-msc4186", "unstable-msc4203", # sending to-device events to appservices - "unstable-msc4210", # remove legacy mentions + "unstable-msc4311", "unstable-extensible-events", - "unstable-pdu", + "unstable-hydra", ] [workspace.dependencies.rustls] @@ -425,6 +427,9 @@ default-features = false version = "0.10" default-features = false +[workspace.dependencies.similar] +version = "2.7.0" + [workspace.dependencies.smallstr] version = "0.3" features = [ diff --git a/src/admin/debug/commands.rs b/src/admin/debug/commands.rs index 20ac7a59..b83369bf 100644 --- a/src/admin/debug/commands.rs +++ b/src/admin/debug/commands.rs @@ -87,9 +87,12 @@ pub(super) async fn parse_pdu(&self) -> Result { } let string = self.body[1..self.body.len().saturating_sub(1)].join("\n"); + let rules = RoomVersionId::V6 + .rules() + .expect("rules for V6 rooms"); match serde_json::from_str(&string) { | Err(e) => return Err!("Invalid json in command body: {e}"), - | Ok(value) => match ruma::signatures::reference_hash(&value, &RoomVersionId::V6) { + | Ok(value) => match ruma::signatures::reference_hash(&value, &rules) { | Err(e) => return Err!("Could not parse PDU JSON: {e:?}"), | Ok(hash) => { let event_id = OwnedEventId::parse(format!("${hash}")); @@ -252,7 +255,6 @@ pub(super) async fn get_remote_pdu( .sending .send_federation_request(&server, ruma::api::federation::event::get_event::v1::Request { event_id: event_id.clone(), - include_unredacted_content: None, }) .await { diff --git a/src/admin/user/commands.rs b/src/admin/user/commands.rs index 15678692..e243eaa0 100644 --- a/src/admin/user/commands.rs +++ b/src/admin/user/commands.rs @@ -195,6 +195,7 @@ pub(super) async fn create_user(&self, username: String, password: Option = self + let room_power_levels: Option = self .services .rooms .state_accessor - .room_state_get_content(&room_id, &StateEventType::RoomPowerLevels, "") + .get_power_levels(&room_id) .await .ok(); - let user_can_demote_self = room_power_levels + let user_can_change_self = room_power_levels .as_ref() - .is_some_and(|power_levels_content| { - RoomPowerLevels::from(power_levels_content.clone()) - .user_can_change_user_power_level(&user_id, &user_id) - }) || self - .services - .rooms - .state_accessor - .room_state_get(&room_id, &StateEventType::RoomCreate, "") - .await - .is_ok_and(|event| event.sender() == user_id); + .is_some_and(|power_levels| { + power_levels.user_can_change_user_power_level(&user_id, &user_id) + }); + + let user_can_demote_self = user_can_change_self + || self + .services + .rooms + .state_accessor + .room_state_get(&room_id, &StateEventType::RoomCreate, "") + .await + .is_ok_and(|event| event.sender() == user_id); if !user_can_demote_self { - return Err!("User is not allowed to modify their own power levels in the room.",); + return Err!("User is not allowed to modify their own power levels in the room."); } - let mut power_levels_content = room_power_levels.unwrap_or_default(); + let mut power_levels_content: RoomPowerLevelsEventContent = room_power_levels + .map(TryInto::try_into) + .transpose()? + .unwrap_or_default(); + power_levels_content.users.remove(&user_id); let event_id = self @@ -783,6 +790,7 @@ pub(super) async fn make_user_admin(&self, user_id: String) -> Result { self.services .admin .make_user_admin(&user_id) + .boxed() .await?; self.write_str(&format!("{user_id} has been granted admin privileges.",)) diff --git a/src/api/client/account.rs b/src/api/client/account.rs index a7b56e2b..cf828d78 100644 --- a/src/api/client/account.rs +++ b/src/api/client/account.rs @@ -11,10 +11,7 @@ use ruma::{ }, uiaa::{AuthFlow, AuthType, UiaaInfo}, }, - events::{ - StateEventType, - room::power_levels::{RoomPowerLevels, RoomPowerLevelsEventContent}, - }, + events::{StateEventType, room::power_levels::RoomPowerLevelsEventContent}, }; use tuwunel_core::{ Err, Error, Result, err, info, @@ -60,10 +57,7 @@ pub(crate) async fn change_password_route( let mut uiaainfo = UiaaInfo { flows: vec![AuthFlow { stages: vec![AuthType::Password] }], - completed: Vec::new(), - params: Box::default(), - session: None, - auth_error: None, + ..Default::default() }; match &body.auth { @@ -189,10 +183,7 @@ pub(crate) async fn deactivate_route( let mut uiaainfo = UiaaInfo { flows: vec![AuthFlow { stages: vec![AuthType::Password] }], - completed: Vec::new(), - params: Box::default(), - session: None, - auth_error: None, + ..Default::default() }; match &body.auth { @@ -331,21 +322,18 @@ pub async fn full_user_deactivate( let room_power_levels = services .rooms .state_accessor - .room_state_get_content::( - room_id, - &StateEventType::RoomPowerLevels, - "", - ) + .get_power_levels(room_id) .await .ok(); - let user_can_demote_self = - room_power_levels - .as_ref() - .is_some_and(|power_levels_content| { - RoomPowerLevels::from(power_levels_content.clone()) - .user_can_change_user_power_level(user_id, user_id) - }) || services + let user_can_change_self = room_power_levels + .as_ref() + .is_some_and(|power_levels| { + power_levels.user_can_change_user_power_level(user_id, user_id) + }); + + let user_can_demote_self = user_can_change_self + || services .rooms .state_accessor .room_state_get(room_id, &StateEventType::RoomCreate, "") @@ -353,7 +341,11 @@ pub async fn full_user_deactivate( .is_ok_and(|event| event.sender() == user_id); if user_can_demote_self { - let mut power_levels_content = room_power_levels.unwrap_or_default(); + let mut power_levels_content: RoomPowerLevelsEventContent = room_power_levels + .map(TryInto::try_into) + .transpose()? + .unwrap_or_default(); + power_levels_content.users.remove(user_id); // ignore errors so deactivation doesn't fail diff --git a/src/api/client/capabilities.rs b/src/api/client/capabilities.rs index ada2ff87..8eb9bb10 100644 --- a/src/api/client/capabilities.rs +++ b/src/api/client/capabilities.rs @@ -3,9 +3,12 @@ use std::collections::BTreeMap; use axum::extract::State; use ruma::{ RoomVersionId, - api::client::discovery::get_capabilities::{ - self, Capabilities, GetLoginTokenCapability, RoomVersionStability, - RoomVersionsCapability, ThirdPartyIdChangesCapability, + api::client::discovery::{ + get_capabilities, + get_capabilities::v3::{ + Capabilities, GetLoginTokenCapability, RoomVersionStability, RoomVersionsCapability, + ThirdPartyIdChangesCapability, + }, }, }; use serde_json::json; diff --git a/src/api/client/device.rs b/src/api/client/device.rs index 3f9876eb..443597a8 100644 --- a/src/api/client/device.rs +++ b/src/api/client/device.rs @@ -145,10 +145,7 @@ pub(crate) async fn delete_device_route( // UIAA let mut uiaainfo = UiaaInfo { flows: vec![AuthFlow { stages: vec![AuthType::Password] }], - completed: Vec::new(), - params: Box::default(), - session: None, - auth_error: None, + ..Default::default() }; match &body.auth { @@ -224,10 +221,7 @@ pub(crate) async fn delete_devices_route( // UIAA let mut uiaainfo = UiaaInfo { flows: vec![AuthFlow { stages: vec![AuthType::Password] }], - completed: Vec::new(), - params: Box::default(), - session: None, - auth_error: None, + ..Default::default() }; match &body.auth { diff --git a/src/api/client/directory.rs b/src/api/client/directory.rs index 76bf5269..f870040e 100644 --- a/src/api/client/directory.rs +++ b/src/api/client/directory.rs @@ -16,18 +16,15 @@ use ruma::{ }, federation, }, - directory::{Filter, PublicRoomJoinRule, PublicRoomsChunk, RoomNetwork, RoomTypeFilter}, + directory::{Filter, PublicRoomsChunk, RoomNetwork, RoomTypeFilter}, events::{ StateEventType, - room::{ - join_rules::{JoinRule, RoomJoinRulesEventContent}, - power_levels::{RoomPowerLevels, RoomPowerLevelsEventContent}, - }, + room::join_rules::{JoinRule, RoomJoinRulesEventContent}, }, uint, }; use tuwunel_core::{ - Err, Result, err, info, + Err, Result, err, info, is_true, matrix::Event, utils::{ TryFutureExtExt, @@ -51,7 +48,7 @@ pub(crate) async fn get_public_rooms_filtered_route( InsecureClientIp(client): InsecureClientIp, body: Ruma, ) -> Result { - check_banned(&services, body.server.as_deref())?; + check_server_banned(&services, body.server.as_deref())?; let response = get_public_rooms_filtered_helper( &services, @@ -80,7 +77,7 @@ pub(crate) async fn get_public_rooms_route( InsecureClientIp(client): InsecureClientIp, body: Ruma, ) -> Result { - check_banned(&services, body.server.as_deref())?; + check_server_banned(&services, body.server.as_deref())?; let response = get_public_rooms_filtered_helper( &services, @@ -393,15 +390,11 @@ async fn user_can_publish_room( match services .rooms .state_accessor - .room_state_get(room_id, &StateEventType::RoomPowerLevels, "") + .get_power_levels(room_id) .await { - | Ok(event) => serde_json::from_str(event.content().get()) - .map_err(|_| err!(Database("Invalid event content for m.room.power_levels"))) - .map(|content: RoomPowerLevelsEventContent| { - RoomPowerLevels::from(content) - .user_can_send_state(user_id, StateEventType::RoomHistoryVisibility) - }), + | Ok(power_levels) => + Ok(power_levels.user_can_send_state(user_id, StateEventType::RoomHistoryVisibility)), | _ => { match services .rooms @@ -453,7 +446,7 @@ async fn public_rooms_chunk(services: &Services, room_id: OwnedRoomId) -> Public .state_accessor .room_state_get_content(&room_id, &StateEventType::RoomJoinRules, "") .map_ok(|c: RoomJoinRulesEventContent| match c.join_rule { - | JoinRule::Public => PublicRoomJoinRule::Public, + | JoinRule::Public => "public".into(), | JoinRule::Knock => "knock".into(), | JoinRule::KnockRestricted(_) => "knock_restricted".into(), | _ => "invite".into(), @@ -497,24 +490,25 @@ async fn public_rooms_chunk(services: &Services, room_id: OwnedRoomId) -> Public } } -fn check_banned(services: &Services, server: Option<&ServerName>) -> Result { +fn check_server_banned(services: &Services, server: Option<&ServerName>) -> Result { let Some(server) = server else { return Ok(()); }; - let forbidden_remote_directory = services - .config - .forbidden_remote_room_directory_server_names - .is_match(server.host()); + let conditions = [ + services + .config + .forbidden_remote_room_directory_server_names + .is_match(server.host()), + services + .config + .forbidden_remote_server_names + .is_match(server.host()), + ]; - let forbidden_remote_server = services - .config - .forbidden_remote_server_names - .is_match(server.host()); - - if forbidden_remote_directory || forbidden_remote_server { - Err!(Request(Forbidden("Server is banned on this homeserver."))) - } else { - Ok(()) + if conditions.iter().any(is_true!()) { + return Err!(Request(Forbidden("Server is banned on this homeserver."))); } + + Ok(()) } diff --git a/src/api/client/keys.rs b/src/api/client/keys.rs index 56d484a3..41543bac 100644 --- a/src/api/client/keys.rs +++ b/src/api/client/keys.rs @@ -3,7 +3,8 @@ use std::collections::{BTreeMap, HashMap, HashSet}; use axum::extract::State; use futures::{StreamExt, stream::FuturesUnordered}; use ruma::{ - OneTimeKeyAlgorithm, OwnedDeviceId, OwnedUserId, UserId, + CanonicalJsonObject, CanonicalJsonValue, OneTimeKeyAlgorithm, OwnedDeviceId, OwnedUserId, + UserId, api::{ client::{ error::ErrorKind, @@ -162,10 +163,7 @@ pub(crate) async fn upload_signing_keys_route( // UIAA let mut uiaainfo = UiaaInfo { flows: vec![AuthFlow { stages: vec![AuthType::Password] }], - completed: Vec::new(), - params: Box::default(), - session: None, - auth_error: None, + ..Default::default() }; match check_for_new_keys( @@ -599,18 +597,19 @@ fn add_unsigned_device_display_name( include_display_names: bool, ) -> Result { if let Some(display_name) = metadata.display_name { - let mut object = keys.deserialize_as::>()?; + let mut object = keys.deserialize_as_unchecked::()?; let unsigned = object - .entry("unsigned") - .or_insert_with(|| json!({})); - if let serde_json::Value::Object(unsigned_object) = unsigned { + .entry("unsigned".into()) + .or_insert_with(CanonicalJsonValue::default); + + if let CanonicalJsonValue::Object(unsigned_object) = unsigned { if include_display_names { unsigned_object.insert("device_display_name".to_owned(), display_name.into()); } else { unsigned_object.insert( "device_display_name".to_owned(), - Some(metadata.device_id.as_str().to_owned()).into(), + CanonicalJsonValue::String(metadata.device_id.as_str().to_owned()), ); } } diff --git a/src/api/client/membership/invite.rs b/src/api/client/membership/invite.rs index c510d077..b5dce1d0 100644 --- a/src/api/client/membership/invite.rs +++ b/src/api/client/membership/invite.rs @@ -7,8 +7,9 @@ use ruma::{ events::room::member::{MembershipState, RoomMemberEventContent}, }; use tuwunel_core::{ - Err, Result, debug_error, err, info, + Err, Result, err, matrix::{event::gen_event_id_canonical_json, pdu::PduBuilder}, + warn, }; use tuwunel_service::Services; @@ -27,8 +28,8 @@ pub(crate) async fn invite_user_route( let sender_user = body.sender_user(); if !services.users.is_admin(sender_user).await && services.config.block_non_admin_invites { - debug_error!( - "User {sender_user} is not an admin and attempted to send an invite to room {}", + warn!( + "{sender_user} is not an admin and attempted to send an invite to {}", &body.room_id ); return Err!(Request(Forbidden("Invites are not allowed on this server."))); @@ -104,10 +105,7 @@ pub(crate) async fn invite_helper( is_direct: bool, ) -> Result { if !services.users.is_admin(sender_user).await && services.config.block_non_admin_invites { - info!( - "User {sender_user} is not an admin and attempted to send an invite to room \ - {room_id}" - ); + warn!("{sender_user} is not an admin and attempted to send an invite to {room_id}"); return Err!(Request(Forbidden("Invites are not allowed on this server."))); } @@ -156,7 +154,10 @@ pub(crate) async fn invite_helper( .sending .convert_to_outgoing_federation_event(pdu_json.clone()) .await, - invite_room_state, + invite_room_state: invite_room_state + .into_iter() + .map(Into::into) + .collect(), via: services .rooms .state_cache diff --git a/src/api/client/membership/join.rs b/src/api/client/membership/join.rs index ab0ec75c..fd70b9d5 100644 --- a/src/api/client/membership/join.rs +++ b/src/api/client/membership/join.rs @@ -25,10 +25,9 @@ use ruma::{ use tuwunel_core::{ Err, Result, debug, debug_info, debug_warn, err, error, info, matrix::{ - StateKey, event::{gen_event_id, gen_event_id_canonical_json}, pdu::{PduBuilder, PduEvent}, - state_res, + room_version, state_res, }, result::FlatOk, trace, @@ -551,7 +550,13 @@ async fn join_room_by_id_helper_remote( }) .ready_filter_map(Result::ok) .fold(HashMap::new(), async |mut state, (event_id, value)| { - let pdu = match PduEvent::from_id_val(&event_id, value.clone()) { + let pdu = if value["type"] == "m.room.create" { + PduEvent::from_rid_val(room_id, &event_id, value.clone()) + } else { + PduEvent::from_id_val(&event_id, value.clone()) + }; + + let pdu = match pdu { | Ok(pdu) => pdu, | Err(e) => { debug_warn!("Invalid PDU in send_join response: {e:?}: {value:#?}"); @@ -604,36 +609,26 @@ async fn join_room_by_id_helper_remote( drop(cork); debug!("Running send_join auth check"); - let fetch_state = &state; - let state_fetch = |k: StateEventType, s: StateKey| async move { - let shortstatekey = services - .rooms - .short - .get_shortstatekey(&k, &s) - .await - .ok()?; - - let event_id = fetch_state.get(&shortstatekey)?; - services - .rooms - .timeline - .get_pdu(event_id) - .await - .ok() - }; - - let auth_check = state_res::event_auth::auth_check( - &state_res::RoomVersion::new(&room_version_id)?, + state_res::auth_check( + &room_version::rules(&room_version_id)?, &parsed_join_pdu, - None, // TODO: third party invite - |k, s| state_fetch(k.clone(), s.into()), - ) - .await - .map_err(|e| err!(Request(Forbidden(warn!("Auth check failed: {e:?}")))))?; + &async |event_id| services.rooms.timeline.get_pdu(&event_id).await, + &async |event_type, state_key| { + let shortstatekey = services + .rooms + .short + .get_shortstatekey(&event_type, state_key.as_str()) + .await?; - if !auth_check { - return Err!(Request(Forbidden("Auth check failed"))); - } + let event_id = state.get(&shortstatekey).ok_or_else(|| { + err!(Request(NotFound("Missing fetch_state {shortstatekey:?}"))) + })?; + + services.rooms.timeline.get_pdu(event_id).await + }, + ) + .boxed() + .await?; info!("Compressing state from send_join"); let compressed: CompressedState = services diff --git a/src/api/client/membership/knock.rs b/src/api/client/membership/knock.rs index c3c540b1..6421ca43 100644 --- a/src/api/client/membership/knock.rs +++ b/src/api/client/membership/knock.rs @@ -8,7 +8,10 @@ use ruma::{ RoomVersionId, UserId, api::{ client::knock::knock_room, - federation::{self}, + federation::{ + membership::RawStrippedState, + {self}, + }, }, canonical_json::to_canonical_value, events::{ @@ -17,7 +20,7 @@ use ruma::{ }, }; use tuwunel_core::{ - Err, Result, debug, debug_info, debug_warn, err, info, + Err, Result, debug, debug_info, debug_warn, err, extract_variant, info, matrix::{ event::{Event, gen_event_id}, pdu::{PduBuilder, PduEvent}, @@ -346,7 +349,7 @@ async fn knock_room_helper_local( let knock_event = knock_event_stub; info!("Asking {remote_server} for send_knock in room {room_id}"); - let send_knock_request = federation::knock::send_knock::v1::Request { + let send_knock_request = federation::membership::create_knock_event::v1::Request { room_id: room_id.to_owned(), event_id: event_id.clone(), pdu: services @@ -384,7 +387,13 @@ async fn knock_room_helper_local( .get_content::() .expect("we just created this"), sender_user, - Some(send_knock_response.knock_room_state), + Some( + send_knock_response + .knock_room_state + .into_iter() + .filter_map(|s| extract_variant!(s, RawStrippedState::Stripped)) + .collect(), + ), None, false, ) @@ -477,7 +486,7 @@ async fn knock_room_helper_remote( let knock_event = knock_event_stub; info!("Asking {remote_server} for send_knock in room {room_id}"); - let send_knock_request = federation::knock::send_knock::v1::Request { + let send_knock_request = federation::membership::create_knock_event::v1::Request { room_id: room_id.to_owned(), event_id: event_id.clone(), pdu: services @@ -507,7 +516,14 @@ async fn knock_room_helper_remote( let state = send_knock_response .knock_room_state .iter() - .map(|event| serde_json::from_str::(event.clone().into_json().get())) + .map(|event| { + serde_json::from_str::( + extract_variant!(event.clone(), RawStrippedState::Stripped) + .expect("Raw") + .json() + .get(), + ) + }) .filter_map(Result::ok); let mut state_map: HashMap = HashMap::new(); @@ -594,7 +610,13 @@ async fn knock_room_helper_remote( .get_content::() .expect("we just created this"), sender_user, - Some(send_knock_response.knock_room_state), + Some( + send_knock_response + .knock_room_state + .into_iter() + .filter_map(|s| extract_variant!(s, RawStrippedState::Stripped)) + .collect(), + ), None, false, ) @@ -628,7 +650,7 @@ async fn make_knock_request( sender_user: &UserId, room_id: &RoomId, servers: &[OwnedServerName], -) -> Result<(federation::knock::create_knock_event_template::v1::Response, OwnedServerName)> { +) -> Result<(federation::membership::prepare_knock_event::v1::Response, OwnedServerName)> { let mut make_knock_response_and_server = Err!(BadServerResponse("No server available to assist in knocking.")); @@ -645,7 +667,7 @@ async fn make_knock_request( .sending .send_federation_request( remote_server, - federation::knock::create_knock_event_template::v1::Request { + federation::membership::prepare_knock_event::v1::Request { room_id: room_id.to_owned(), user_id: sender_user.to_owned(), ver: services diff --git a/src/api/client/membership/mod.rs b/src/api/client/membership/mod.rs index f3372043..57fcb29b 100644 --- a/src/api/client/membership/mod.rs +++ b/src/api/client/membership/mod.rs @@ -70,15 +70,16 @@ pub(crate) async fn banned_room_check( if let Some(room_id) = room_id { if services.rooms.metadata.is_banned(room_id).await - || services - .config - .forbidden_remote_server_names - .is_match( - room_id - .server_name() - .expect("legacy room mxid") - .host(), - ) { + || (room_id.server_name().is_some() + && services + .config + .forbidden_remote_server_names + .is_match( + room_id + .server_name() + .expect("legacy room mxid") + .host(), + )) { warn!( "User {user_id} who is not an admin attempted to send an invite for or \ attempted to join a banned room or banned room server name: {room_id}" diff --git a/src/api/client/mod.rs b/src/api/client/mod.rs index bb761ace..c1e8b61a 100644 --- a/src/api/client/mod.rs +++ b/src/api/client/mod.rs @@ -60,7 +60,7 @@ pub(super) use message::*; pub(super) use openid::*; pub(super) use presence::*; pub(super) use profile::*; -pub use profile::{update_all_rooms, update_avatar_url, update_displayname}; +pub use profile::{update_avatar_url, update_displayname}; pub(super) use push::*; pub(super) use read_marker::*; pub(super) use redact::*; diff --git a/src/api/client/profile.rs b/src/api/client/profile.rs index 71057cff..066d2972 100644 --- a/src/api/client/profile.rs +++ b/src/api/client/profile.rs @@ -2,7 +2,7 @@ use std::collections::BTreeMap; use axum::extract::State; use futures::{ - StreamExt, TryStreamExt, + FutureExt, StreamExt, TryStreamExt, future::{join, join3, join4}, }; use ruma::{ @@ -368,7 +368,9 @@ pub async fn update_displayname( .collect() .await; - update_all_rooms(services, all_joined_rooms, user_id).await; + update_all_rooms(services, all_joined_rooms, user_id) + .boxed() + .await; } pub async fn update_avatar_url( @@ -421,10 +423,12 @@ pub async fn update_avatar_url( .collect() .await; - update_all_rooms(services, all_joined_rooms, user_id).await; + update_all_rooms(services, all_joined_rooms, user_id) + .boxed() + .await; } -pub async fn update_all_rooms( +async fn update_all_rooms( services: &Services, all_joined_rooms: Vec<(PduBuilder, &OwnedRoomId)>, user_id: &UserId, diff --git a/src/api/client/register.rs b/src/api/client/register.rs index cfda5a6f..31906d61 100644 --- a/src/api/client/register.rs +++ b/src/api/client/register.rs @@ -316,7 +316,7 @@ pub(crate) async fn register_route( stages: vec![AuthType::RegistrationToken], }], completed: Vec::new(), - params: Box::default(), + params: Default::default(), session: None, auth_error: None, }; @@ -326,7 +326,7 @@ pub(crate) async fn register_route( uiaainfo = UiaaInfo { flows: vec![AuthFlow { stages: vec![AuthType::Dummy] }], completed: Vec::new(), - params: Box::default(), + params: Default::default(), session: None, auth_error: None, }; @@ -523,7 +523,11 @@ pub(crate) async fn register_route( .await .is_ok_and(is_equal_to!(1)) { - services.admin.make_user_admin(&user_id).await?; + services + .admin + .make_user_admin(&user_id) + .boxed() + .await?; warn!("Granting {user_id} admin privileges as the first user"); } diff --git a/src/api/client/report.rs b/src/api/client/report.rs index 009c71ed..28862052 100644 --- a/src/api/client/report.rs +++ b/src/api/client/report.rs @@ -15,6 +15,8 @@ use tuwunel_service::Services; use crate::Ruma; +const REASON_MAX_LEN: usize = 750; + /// # `POST /_matrix/client/v3/rooms/{roomId}/report` /// /// Reports an abusive room to homeserver admins @@ -29,18 +31,13 @@ pub(crate) async fn report_room_route( info!( "Received room report by user {sender_user} for room {} with reason: \"{}\"", - body.room_id, - body.reason.as_deref().unwrap_or("") + body.room_id, body.reason, ); - if body - .reason - .as_ref() - .is_some_and(|s| s.len() > 750) - { - return Err!(Request( - InvalidParam("Reason too long, should be 750 characters or fewer",) - )); + if body.reason.len().gt(&REASON_MAX_LEN) { + return Err!(Request(InvalidParam( + "Reason too long, should be {REASON_MAX_LEN} characters or fewer" + ))); } delay_response().await; @@ -64,7 +61,7 @@ pub(crate) async fn report_room_route( "@room Room report received from {} -\n\nRoom ID: {}\n\nReport Reason: {}", sender_user.to_owned(), body.room_id, - body.reason.as_deref().unwrap_or("") + body.reason, ))) .await .ok(); diff --git a/src/api/client/room/create.rs b/src/api/client/room/create.rs index 5fe7735f..87fc8ba8 100644 --- a/src/api/client/room/create.rs +++ b/src/api/client/room/create.rs @@ -1,10 +1,13 @@ use std::collections::BTreeMap; use axum::extract::State; -use futures::FutureExt; +use futures::{FutureExt, future::OptionFuture}; use ruma::{ CanonicalJsonObject, Int, OwnedRoomAliasId, OwnedRoomId, OwnedUserId, RoomId, RoomVersionId, - api::client::room::{self, create_room}, + api::client::room::{ + self, create_room, + create_room::v3::{CreationContent, RoomPreset}, + }, events::{ TimelineEventType, room::{ @@ -16,19 +19,21 @@ use ruma::{ member::{MembershipState, RoomMemberEventContent}, name::RoomNameEventContent, power_levels::RoomPowerLevelsEventContent, - topic::RoomTopicEventContent, + topic::{RoomTopicEventContent, TopicContentBlock}, }, }, int, + room_version_rules::{RoomIdFormatVersion, RoomVersionRules}, serde::{JsonObject, Raw}, }; use serde_json::{json, value::to_raw_value}; use tuwunel_core::{ Err, Result, debug_info, debug_warn, err, info, - matrix::{StateKey, pdu::PduBuilder}, + matrix::{StateKey, pdu::PduBuilder, room_version}, + utils::BoolExt, warn, }; -use tuwunel_service::{Services, appservice::RegistrationInfo}; +use tuwunel_service::{Services, appservice::RegistrationInfo, rooms::state::RoomMutexGuard}; use crate::{Ruma, client::invite_helper}; @@ -53,164 +58,61 @@ pub(crate) async fn create_room_route( State(services): State, body: Ruma, ) -> Result { - use create_room::v3::RoomPreset; + can_create_room_check(&services, &body).await?; + can_publish_directory_check(&services, &body).await?; - let sender_user = body.sender_user(); + // Figure out preset. We need it for preset specific events + let preset = body + .preset + .clone() + .unwrap_or(match &body.visibility { + | room::Visibility::Public => RoomPreset::PublicChat, + | _ => RoomPreset::PrivateChat, // Room visibility should not be custom + }); - if !services.globals.allow_room_creation() - && body.appservice_info.is_none() - && !services.users.is_admin(sender_user).await - { - return Err!(Request(Forbidden("Room creation has been disabled.",))); - } + let alias: OptionFuture<_> = body + .room_alias_name + .as_ref() + .map(|alias| room_alias_check(&services, alias, body.appservice_info.as_ref())) + .into(); - let room_id: OwnedRoomId = match &body.room_id { - | Some(custom_room_id) => custom_room_id_check(&services, custom_room_id)?, - | _ => RoomId::new(&services.server.name), - }; - - // check if room ID doesn't already exist instead of erroring on auth check - if services - .rooms - .short - .get_shortroomid(&room_id) - .await - .is_ok() - { - return Err!(Request(RoomInUse("Room with that custom room ID already exists",))); - } - - if body.visibility == room::Visibility::Public - && services - .server - .config - .lockdown_public_room_directory - && !services.users.is_admin(sender_user).await - && body.appservice_info.is_none() - { - warn!( - "Non-admin user {sender_user} tried to publish {room_id} to the room directory \ - while \"lockdown_public_room_directory\" is enabled" - ); - - if services.server.config.admin_room_notices { + // Determine room version + let (room_version, version_rules) = body + .room_version + .as_ref() + .map_or(Ok(&services.server.config.default_room_version), |version| { services - .admin - .notice(&format!( - "Non-admin user {sender_user} tried to publish {room_id} to the room \ - directory while \"lockdown_public_room_directory\" is enabled" - )) - .await; - } - - return Err!(Request(Forbidden("Publishing rooms to the room directory is not allowed"))); - } - - let _short_id = services - .rooms - .short - .get_or_create_shortroomid(&room_id) - .await; - - let state_lock = services.rooms.state.mutex.lock(&room_id).await; - - let alias: Option = match body.room_alias_name.as_ref() { - | Some(alias) => - Some(room_alias_check(&services, alias, body.appservice_info.as_ref()).await?), - | _ => None, - }; - - let room_version = match body.room_version.clone() { - | Some(room_version) => - if services .server - .supported_room_version(&room_version) - { - room_version - } else { - return Err!(Request(UnsupportedRoomVersion( - "This server does not support that room version." - ))); - }, - | None => services - .server - .config - .default_room_version - .clone(), - }; + .supported_room_version(version) + .then_ok_or_else(version, || { + err!(Request(UnsupportedRoomVersion( + "This server does not support room version {version:?}" + ))) + }) + }) + .and_then(|version| Ok((version, room_version::rules(version)?)))?; - let create_content = match &body.creation_content { - | Some(content) => { - use RoomVersionId::*; - - let mut content = content - .deserialize_as::() - .map_err(|e| { - err!(Request(BadJson(error!( - "Failed to deserialise content as canonical JSON: {e}" - )))) - })?; - - match room_version { - | V1 | V2 | V3 | V4 | V5 | V6 | V7 | V8 | V9 | V10 => { - content.insert( - "creator".into(), - json!(&sender_user).try_into().map_err(|e| { - err!(Request(BadJson(debug_error!("Invalid creation content: {e}")))) - })?, - ); - }, - | _ => { - // V11+ removed the "creator" key - }, - } - content.insert( - "room_version".into(), - json!(room_version.as_str()) - .try_into() - .map_err(|e| err!(Request(BadJson("Invalid creation content: {e}"))))?, - ); - content - }, - | None => { - use RoomVersionId::*; - - let content = match room_version { - | V1 | V2 | V3 | V4 | V5 | V6 | V7 | V8 | V9 | V10 => - RoomCreateEventContent::new_v1(sender_user.to_owned()), - | _ => RoomCreateEventContent::new_v11(), - }; - let mut content = - serde_json::from_str::(to_raw_value(&content)?.get()) - .unwrap(); - content.insert("room_version".into(), json!(room_version.as_str()).try_into()?); - content - }, - }; + // Error on existing alias before committing to creation. + let alias = alias.await.transpose()?; // Increment and hold the counter; the room will sync atomically to clients // which is preferable. let next_count = services.globals.next_count(); - // 1. The room create event - services - .rooms - .timeline - .build_and_append_pdu( - PduBuilder { - event_type: TimelineEventType::RoomCreate, - content: to_raw_value(&create_content)?, - state_key: Some(StateKey::new()), - ..Default::default() - }, - sender_user, - &room_id, - &state_lock, - ) - .boxed() - .await?; + // 1. Create the create event. + let (room_id, state_lock) = match version_rules.room_id_format { + | RoomIdFormatVersion::V1 => + create_create_event_legacy(&services, &body, room_version, &version_rules).await?, + | RoomIdFormatVersion::V2 => + create_create_event(&services, &body, &preset, room_version, &version_rules) + .await + .map_err(|e| { + err!(Request(InvalidParam("Error while creating m.room.create event: {e}"))) + })?, + }; // 2. Let the room creator join + let sender_user = body.sender_user(); services .rooms .timeline @@ -230,17 +132,14 @@ pub(crate) async fn create_room_route( .await?; // 3. Power levels - - // Figure out preset. We need it for preset specific events - let preset = body - .preset - .clone() - .unwrap_or(match &body.visibility { - | room::Visibility::Public => RoomPreset::PublicChat, - | _ => RoomPreset::PrivateChat, // Room visibility should not be custom - }); - - let mut users = BTreeMap::from_iter([(sender_user.to_owned(), int!(100))]); + let mut users = if !version_rules + .authorization + .explicitly_privilege_room_creators + { + BTreeMap::from_iter([(sender_user.to_owned(), int!(100))]) + } else { + BTreeMap::new() + }; if preset == RoomPreset::TrustedPrivateChat { for invite in &body.invite { @@ -260,11 +159,17 @@ pub(crate) async fn create_room_route( continue; } - users.insert(invite.clone(), int!(100)); + if !version_rules + .authorization + .additional_room_creators + { + users.insert(invite.clone(), int!(100)); + } } } let power_levels_content = default_power_levels_content( + &version_rules, body.power_level_content_override.as_ref(), &body.visibility, users, @@ -365,7 +270,7 @@ pub(crate) async fn create_room_route( // 6. Events listed in initial_state for event in &body.initial_state { let mut pdu_builder = event - .deserialize_as::() + .deserialize_as_unchecked::() .map_err(|e| { err!(Request(InvalidParam(warn!("Invalid initial state event: {e:?}")))) })?; @@ -421,7 +326,10 @@ pub(crate) async fn create_room_route( .rooms .timeline .build_and_append_pdu( - PduBuilder::state(String::new(), &RoomTopicEventContent { topic: topic.clone() }), + PduBuilder::state(String::new(), &RoomTopicEventContent { + topic: topic.clone(), + topic_block: TopicContentBlock::default(), + }), sender_user, &room_id, &state_lock, @@ -488,25 +396,226 @@ pub(crate) async fn create_room_route( Ok(create_room::v3::Response::new(room_id)) } +async fn create_create_event( + services: &Services, + body: &Ruma, + preset: &RoomPreset, + room_version: &RoomVersionId, + version_rules: &RoomVersionRules, +) -> Result<(OwnedRoomId, RoomMutexGuard)> { + let _sender_user = body.sender_user(); + + let mut create_content = match &body.creation_content { + | Some(content) => { + let mut content = content + .deserialize_as_unchecked::() + .map_err(|e| { + err!(Request(BadJson(error!( + "Failed to deserialise content as canonical JSON: {e}" + )))) + })?; + + content.insert( + "room_version".into(), + json!(room_version.as_str()) + .try_into() + .map_err(|e| err!(Request(BadJson("Invalid creation content: {e}"))))?, + ); + + content + }, + | None => { + let content = RoomCreateEventContent::new_v11(); + + let mut content = + serde_json::from_str::(to_raw_value(&content)?.get())?; + + content.insert("room_version".into(), json!(room_version.as_str()).try_into()?); + content + }, + }; + + if version_rules + .authorization + .additional_room_creators + { + let mut additional_creators = body + .creation_content + .as_ref() + .and_then(|c| { + c.deserialize_as_unchecked::() + .ok() + }) + .unwrap_or_default() + .additional_creators; + + if *preset == RoomPreset::TrustedPrivateChat { + additional_creators.extend(body.invite.clone()); + } + + additional_creators.sort(); + additional_creators.dedup(); + if !additional_creators.is_empty() { + create_content + .insert("additional_creators".into(), json!(additional_creators).try_into()?); + } + } + + // 1. The room create event, using a placeholder room_id + let room_id = ruma::room_id!("!thiswillbereplaced").to_owned(); + let state_lock = services.rooms.state.mutex.lock(&room_id).await; + let create_event_id = services + .rooms + .timeline + .build_and_append_pdu( + PduBuilder { + event_type: TimelineEventType::RoomCreate, + content: to_raw_value(&create_content)?, + state_key: Some(StateKey::new()), + ..Default::default() + }, + body.sender_user(), + &room_id, + &state_lock, + ) + .boxed() + .await?; + + drop(state_lock); + + // The real room_id is now the event_id. + let room_id = OwnedRoomId::from_parts('!', create_event_id.localpart(), None)?; + let state_lock = services.rooms.state.mutex.lock(&room_id).await; + + Ok((room_id, state_lock)) +} + +async fn create_create_event_legacy( + services: &Services, + body: &Ruma, + room_version: &RoomVersionId, + _version_rules: &RoomVersionRules, +) -> Result<(OwnedRoomId, RoomMutexGuard)> { + let room_id: OwnedRoomId = match &body.room_id { + | None => RoomId::new_v1(&services.server.name), + | Some(custom_id) => custom_room_id_check(services, custom_id).await?, + }; + + let state_lock = services.rooms.state.mutex.lock(&room_id).await; + + let _short_id = services + .rooms + .short + .get_or_create_shortroomid(&room_id) + .await; + + let create_content = match &body.creation_content { + | Some(content) => { + use RoomVersionId::*; + + let mut content = content + .deserialize_as_unchecked::() + .map_err(|e| { + err!(Request(BadJson(error!( + "Failed to deserialise content as canonical JSON: {e}" + )))) + })?; + + match room_version { + | V1 | V2 | V3 | V4 | V5 | V6 | V7 | V8 | V9 | V10 => { + content.insert( + "creator".into(), + json!(body.sender_user()) + .try_into() + .map_err(|e| { + err!(Request(BadJson(debug_error!( + "Invalid creation content: {e}" + )))) + })?, + ); + }, + | _ => { + // V11+ removed the "creator" key + }, + } + + content.insert( + "room_version".into(), + json!(room_version.as_str()) + .try_into() + .map_err(|e| err!(Request(BadJson("Invalid creation content: {e}"))))?, + ); + + content + }, + | None => { + use RoomVersionId::*; + + let content = match room_version { + | V1 | V2 | V3 | V4 | V5 | V6 | V7 | V8 | V9 | V10 => + RoomCreateEventContent::new_v1(body.sender_user().to_owned()), + | _ => RoomCreateEventContent::new_v11(), + }; + + let mut content = + serde_json::from_str::(to_raw_value(&content)?.get())?; + + content.insert("room_version".into(), json!(room_version.as_str()).try_into()?); + content + }, + }; + + // 1. The room create event + services + .rooms + .timeline + .build_and_append_pdu( + PduBuilder { + event_type: TimelineEventType::RoomCreate, + content: to_raw_value(&create_content)?, + state_key: Some(StateKey::new()), + ..Default::default() + }, + body.sender_user(), + &room_id, + &state_lock, + ) + .boxed() + .await?; + + Ok((room_id, state_lock)) +} + /// creates the power_levels_content for the PDU builder fn default_power_levels_content( + version_rules: &RoomVersionRules, power_level_content_override: Option<&Raw>, visibility: &room::Visibility, users: BTreeMap, ) -> Result { use serde_json::to_value; - let mut power_levels_content = - to_value(RoomPowerLevelsEventContent { users, ..Default::default() })?; + let mut power_levels_content = RoomPowerLevelsEventContent::new(&version_rules.authorization); + power_levels_content.users = users; + + let mut power_levels_content = to_value(power_levels_content)?; // secure proper defaults of sensitive/dangerous permissions that moderators // (power level 50) should not have easy access to power_levels_content["events"]["m.room.power_levels"] = to_value(100)?; power_levels_content["events"]["m.room.server_acl"] = to_value(100)?; - power_levels_content["events"]["m.room.tombstone"] = to_value(100)?; power_levels_content["events"]["m.room.encryption"] = to_value(100)?; power_levels_content["events"]["m.room.history_visibility"] = to_value(100)?; + if version_rules + .authorization + .explicitly_privilege_room_creators + { + power_levels_content["events"]["m.room.tombstone"] = to_value(150)?; + } else { + power_levels_content["events"]["m.room.tombstone"] = to_value(100)?; + } + // always allow users to respond (not post new) to polls. this is primarily // useful in read-only announcement rooms that post a public poll. power_levels_content["events"]["org.matrix.msc3381.poll.response"] = to_value(0)?; @@ -599,7 +708,7 @@ async fn room_alias_check( } /// if a room is being created with a custom room ID, run our checks against it -fn custom_room_id_check(services: &Services, custom_room_id: &str) -> Result { +async fn custom_room_id_check(services: &Services, custom_room_id: &str) -> Result { // apply forbidden room alias checks to custom room IDs too if services .globals @@ -623,8 +732,65 @@ fn custom_room_id_check(services: &Services, custom_room_id: &str) -> Result, +) -> Result { + if !services + .server + .config + .lockdown_public_room_directory + || body.appservice_info.is_some() + || body.visibility != room::Visibility::Public + || services.users.is_admin(body.sender_user()).await + { + return Ok(()); + } + + let msg = format!( + "Non-admin user {} tried to publish new to the directory while \ + lockdown_public_room_directory is enabled", + body.sender_user(), + ); + + warn!("{msg}"); + if services.server.config.admin_room_notices { + services.admin.notice(&msg).await; + } + + Err!(Request(Forbidden("Publishing rooms to the room directory is not allowed"))) +} + +async fn can_create_room_check( + services: &Services, + body: &Ruma, +) -> Result { + if !services.globals.allow_room_creation() + && body.appservice_info.is_none() + && !services.users.is_admin(body.sender_user()).await + { + return Err!(Request(Forbidden("Room creation has been disabled.",))); + } + + Ok(()) } diff --git a/src/api/client/room/summary.rs b/src/api/client/room/summary.rs index 33b4ef65..5f5d38c5 100644 --- a/src/api/client/room/summary.rs +++ b/src/api/client/room/summary.rs @@ -12,7 +12,7 @@ use ruma::{ federation::space::{SpaceHierarchyParentSummary, get_hierarchy}, }, events::room::member::MembershipState, - space::SpaceRoomJoinRule::{self, *}, + room::{JoinRuleSummary, RoomSummary}, }; use tuwunel_core::{ Err, Result, debug_warn, trace, @@ -34,8 +34,8 @@ use crate::{Ruma, RumaResponse}; pub(crate) async fn get_room_summary_legacy( State(services): State, InsecureClientIp(client): InsecureClientIp, - body: Ruma, -) -> Result> { + body: Ruma, +) -> Result> { get_room_summary(State(services), InsecureClientIp(client), body) .boxed() .await @@ -51,8 +51,8 @@ pub(crate) async fn get_room_summary_legacy( pub(crate) async fn get_room_summary( State(services): State, InsecureClientIp(client): InsecureClientIp, - body: Ruma, -) -> Result { + body: Ruma, +) -> Result { let (room_id, servers) = services .rooms .alias @@ -73,7 +73,7 @@ async fn room_summary_response( room_id: &RoomId, servers: &[OwnedServerName], sender_user: Option<&UserId>, -) -> Result { +) -> Result { if services .rooms .state_cache @@ -85,23 +85,12 @@ async fn room_summary_response( .await; } - let room = - remote_room_summary_hierarchy_response(services, room_id, servers, sender_user).await?; + let summary = remote_room_summary_hierarchy_response(services, room_id, servers, sender_user) + .await? + .summary; - Ok(get_summary::msc3266::Response { - room_id: room_id.to_owned(), - canonical_alias: room.canonical_alias, - avatar_url: room.avatar_url, - guest_can_join: room.guest_can_join, - name: room.name, - num_joined_members: room.num_joined_members, - topic: room.topic, - world_readable: room.world_readable, - join_rule: room.join_rule, - room_type: room.room_type, - room_version: room.room_version, - encryption: room.encryption, - allowed_room_ids: room.allowed_room_ids, + Ok(get_summary::v1::Response { + summary, membership: sender_user .is_some() .then_some(MembershipState::Leave), @@ -112,7 +101,7 @@ async fn local_room_summary_response( services: &Services, room_id: &RoomId, sender_user: Option<&UserId>, -) -> Result { +) -> Result { trace!(?sender_user, "Sending local room summary response for {room_id:?}"); let join_rule = services .rooms @@ -139,7 +128,7 @@ async fn local_room_summary_response( &join_rule.clone().into(), guest_can_join, world_readable, - join_rule.allowed_rooms(), + join_rule.allowed_room_ids(), sender_user, ) .await?; @@ -224,24 +213,22 @@ async fn local_room_summary_response( membership, ); - Ok(get_summary::msc3266::Response { - room_id: room_id.to_owned(), - canonical_alias, - avatar_url, - guest_can_join, - name, - num_joined_members: num_joined_members.try_into().unwrap_or_default(), - topic, - world_readable, - room_type, - room_version, - encryption, + Ok(get_summary::v1::Response { + summary: RoomSummary { + room_id: room_id.to_owned(), + canonical_alias, + avatar_url, + guest_can_join, + name, + num_joined_members: num_joined_members.try_into().unwrap_or_default(), + topic, + world_readable, + room_type, + room_version, + encryption, + join_rule: join_rule.into(), + }, membership, - allowed_room_ids: join_rule - .allowed_rooms() - .map(Into::into) - .collect(), - join_rule: join_rule.into(), }) } @@ -277,10 +264,11 @@ async fn remote_room_summary_hierarchy_response( while let Some(Ok(response)) = requests.next().await { trace!("{response:?}"); let room = response.room.clone(); - if room.room_id != room_id { + let summary = &room.summary; + if summary.room_id != room_id { debug_warn!( "Room ID {} returned does not belong to the requested room ID {}", - room.room_id, + summary.room_id, room_id ); continue; @@ -289,10 +277,10 @@ async fn remote_room_summary_hierarchy_response( return user_can_see_summary( services, room_id, - &room.join_rule, - room.guest_can_join, - room.world_readable, - room.allowed_room_ids.iter().map(AsRef::as_ref), + &summary.join_rule, + summary.guest_can_join, + summary.world_readable, + summary.join_rule.allowed_room_ids(), sender_user, ) .await @@ -308,7 +296,7 @@ async fn remote_room_summary_hierarchy_response( async fn user_can_see_summary<'a, I>( services: &Services, room_id: &RoomId, - join_rule: &SpaceRoomJoinRule, + join_rule: &JoinRuleSummary, guest_can_join: bool, world_readable: bool, allowed_room_ids: I, @@ -317,17 +305,23 @@ async fn user_can_see_summary<'a, I>( where I: Iterator + Send, { - let is_public_room = matches!(join_rule, Public | Knock | KnockRestricted); + let is_public_room = matches!( + join_rule, + JoinRuleSummary::Public | JoinRuleSummary::Knock | JoinRuleSummary::KnockRestricted(_) + ); + match sender_user { | Some(sender_user) => { let user_can_see_state_events = services .rooms .state_accessor .user_can_see_state_events(sender_user, room_id); + let is_guest = services .users .is_deactivated(sender_user) .unwrap_or(false); + let user_in_allowed_restricted_room = allowed_room_ids.stream().any(|room| { services .rooms diff --git a/src/api/client/room/upgrade.rs b/src/api/client/room/upgrade.rs index a652e52f..48fcb04a 100644 --- a/src/api/client/room/upgrade.rs +++ b/src/api/client/room/upgrade.rs @@ -4,7 +4,7 @@ use axum::extract::State; use futures::StreamExt; use ruma::{ CanonicalJsonObject, RoomId, RoomVersionId, - api::client::{error::ErrorKind, room::upgrade_room}, + api::client::room::upgrade_room, events::{ StateEventType, TimelineEventType, room::{ @@ -14,11 +14,12 @@ use ruma::{ }, }, int, + room_version_rules::RoomIdFormatVersion, }; use serde_json::{json, value::to_raw_value}; use tuwunel_core::{ - Err, Error, Result, err, - matrix::{Event, StateKey, pdu::PduBuilder}, + Err, Result, err, + matrix::{Event, StateKey, pdu::PduBuilder, room_version}, }; use crate::Ruma; @@ -61,14 +62,23 @@ pub(crate) async fn upgrade_room_route( .server .supported_room_version(&body.new_version) { - return Err(Error::BadRequest( - ErrorKind::UnsupportedRoomVersion, + return Err!(Request(UnsupportedRoomVersion( "This server does not support that room version.", - )); + ))); } + if matches!(body.new_version, RoomVersionId::V12) { + return Err!(Request(UnsupportedRoomVersion( + "Upgrading to version 12 is still under development.", + ))); + } + + let room_version_rules = room_version::rules(&body.new_version)?; + let room_id_format = &room_version_rules.room_id_format; + assert!(*room_id_format == RoomIdFormatVersion::V1, "TODO"); + // Create a replacement room - let replacement_room = RoomId::new(services.globals.server_name()); + let replacement_room = RoomId::new_v1(services.globals.server_name()); let _short_id = services .rooms diff --git a/src/api/client/session/ldap.rs b/src/api/client/session/ldap.rs index 8e80e7b6..00a77ee6 100644 --- a/src/api/client/session/ldap.rs +++ b/src/api/client/session/ldap.rs @@ -1,3 +1,4 @@ +use futures::FutureExt; use ruma::{OwnedUserId, UserId}; use tuwunel_core::{Err, Result, debug}; use tuwunel_service::Services; @@ -63,6 +64,7 @@ pub(super) async fn ldap_login( services .admin .make_user_admin(lowercased_user_id) + .boxed() .await?; } else if !is_ldap_admin && is_tuwunel_admin { services diff --git a/src/api/client/session/token.rs b/src/api/client/session/token.rs index 2dae0fd3..b01c4bd1 100644 --- a/src/api/client/session/token.rs +++ b/src/api/client/session/token.rs @@ -56,10 +56,7 @@ pub(crate) async fn login_token_route( let mut uiaainfo = uiaa::UiaaInfo { flows: vec![password_flow], - completed: Vec::new(), - params: Box::default(), - session: None, - auth_error: None, + ..Default::default() }; match &body.auth { diff --git a/src/api/client/state.rs b/src/api/client/state.rs index 13792647..3f1a82b4 100644 --- a/src/api/client/state.rs +++ b/src/api/client/state.rs @@ -2,7 +2,7 @@ use axum::extract::State; use futures::{FutureExt, TryFutureExt, TryStreamExt}; use ruma::{ OwnedEventId, RoomId, UserId, - api::client::state::{get_state_events, get_state_events_for_key, send_state_event}, + api::client::state::{get_state_event_for_key, get_state_events, send_state_event}, events::{ AnyStateEventContent, StateEventType, room::{ @@ -107,8 +107,8 @@ pub(crate) async fn get_state_events_route( /// readable pub(crate) async fn get_state_events_for_key_route( State(services): State, - body: Ruma, -) -> Result { + body: Ruma, +) -> Result { let sender_user = body.sender_user(); if !services @@ -140,7 +140,7 @@ pub(crate) async fn get_state_events_for_key_route( .as_ref() .is_some_and(|f| f.to_lowercase().eq("event")); - Ok(get_state_events_for_key::v3::Response { + Ok(get_state_event_for_key::v3::Response { content: event_format.or(|| event.get_content_as_value()), event: event_format.then(|| { json!({ @@ -167,8 +167,8 @@ pub(crate) async fn get_state_events_for_key_route( /// readable pub(crate) async fn get_state_events_for_empty_key_route( State(services): State, - body: Ruma, -) -> Result> { + body: Ruma, +) -> Result> { get_state_events_for_key_route(State(services), body) .await .map(RumaResponse) @@ -222,7 +222,7 @@ async fn allowed_to_send_state_event( | StateEventType::RoomServerAcl => { // prevents common ACL paw-guns as ACL management is difficult and prone to // irreversible mistakes - match json.deserialize_as::() { + match json.deserialize_as_unchecked::() { | Ok(acl_content) => { if acl_content.allow_is_empty() { return Err!(Request(BadJson(debug_warn!( @@ -282,7 +282,7 @@ async fn allowed_to_send_state_event( // admin room is a sensitive room, it should not ever be made public if let Ok(admin_room_id) = services.admin.get_admin_room().await { if admin_room_id == room_id { - match json.deserialize_as::() { + match json.deserialize_as_unchecked::() { | Ok(join_rule) => if join_rule.join_rule == JoinRule::Public { return Err!(Request(Forbidden( @@ -301,7 +301,7 @@ async fn allowed_to_send_state_event( | StateEventType::RoomHistoryVisibility => { // admin room is a sensitive room, it should not ever be made world readable if let Ok(admin_room_id) = services.admin.get_admin_room().await { - match json.deserialize_as::() { + match json.deserialize_as_unchecked::() { | Ok(visibility_content) => { if admin_room_id == room_id && visibility_content.history_visibility @@ -322,7 +322,7 @@ async fn allowed_to_send_state_event( } }, | StateEventType::RoomCanonicalAlias => { - match json.deserialize_as::() { + match json.deserialize_as_unchecked::() { | Ok(canonical_alias_content) => { let mut aliases = canonical_alias_content.alt_aliases.clone(); @@ -354,52 +354,53 @@ async fn allowed_to_send_state_event( }, } }, - | StateEventType::RoomMember => match json.deserialize_as::() { - | Ok(membership_content) => { - let Ok(_state_key) = UserId::parse(state_key) else { - return Err!(Request(BadJson( - "Membership event has invalid or non-existent state key" - ))); - }; - - if let Some(authorising_user) = - membership_content.join_authorized_via_users_server - { - if membership_content.membership != MembershipState::Join { + | StateEventType::RoomMember => + match json.deserialize_as_unchecked::() { + | Ok(membership_content) => { + let Ok(_state_key) = UserId::parse(state_key) else { return Err!(Request(BadJson( - "join_authorised_via_users_server is only for member joins" + "Membership event has invalid or non-existent state key" ))); - } + }; - if !services.globals.user_is_local(&authorising_user) { - return Err!(Request(InvalidParam( - "Authorising user {authorising_user} does not belong to this \ - homeserver" - ))); - } + if let Some(authorising_user) = + membership_content.join_authorized_via_users_server + { + if membership_content.membership != MembershipState::Join { + return Err!(Request(BadJson( + "join_authorised_via_users_server is only for member joins" + ))); + } - services - .rooms - .state_cache - .is_joined(&authorising_user, room_id) - .map(is_false!()) - .map(BoolExt::into_result) - .map_err(|()| { - err!(Request(InvalidParam( - "Authorising user {authorising_user} is not in the room. They \ - cannot authorise the join." - ))) - }) - .await?; - } + if !services.globals.user_is_local(&authorising_user) { + return Err!(Request(InvalidParam( + "Authorising user {authorising_user} does not belong to this \ + homeserver" + ))); + } + + services + .rooms + .state_cache + .is_joined(&authorising_user, room_id) + .map(is_false!()) + .map(BoolExt::into_result) + .map_err(|()| { + err!(Request(InvalidParam( + "Authorising user {authorising_user} is not in the room. \ + They cannot authorise the join." + ))) + }) + .await?; + } + }, + | Err(e) => { + return Err!(Request(BadJson( + "Membership content must have a valid JSON body with at least a valid \ + membership state: {e}" + ))); + }, }, - | Err(e) => { - return Err!(Request(BadJson( - "Membership content must have a valid JSON body with at least a valid \ - membership state: {e}" - ))); - }, - }, | _ => (), } diff --git a/src/api/client/sync/v3.rs b/src/api/client/sync/v3.rs index 96628c95..9ab81919 100644 --- a/src/api/client/sync/v3.rs +++ b/src/api/client/sync/v3.rs @@ -14,11 +14,11 @@ use ruma::{ api::client::{ filter::FilterDefinition, sync::sync_events::{ - self, DeviceLists, UnreadNotificationsCount, + self, DeviceLists, StrippedState, UnreadNotificationsCount, v3::{ Ephemeral, Filter, GlobalAccountData, InviteState, InvitedRoom, JoinedRoom, KnockState, KnockedRoom, LeftRoom, Presence, RoomAccountData, RoomSummary, Rooms, - State as RoomState, Timeline, ToDevice, + State as RoomState, StateEvents, Timeline, ToDevice, }, }, uiaa::UiaaResponse, @@ -295,7 +295,12 @@ async fn build_sync_events( } let invited_room = InvitedRoom { - invite_state: InviteState { events: invite_state }, + invite_state: InviteState { + events: invite_state + .into_iter() + .map(Raw::cast::) + .collect(), + }, }; invited_rooms.insert(room_id, invited_room); @@ -320,7 +325,12 @@ async fn build_sync_events( } let knocked_room = KnockedRoom { - knock_state: KnockState { events: knock_state }, + knock_state: KnockState { + events: knock_state + .into_iter() + .map(Raw::cast::) + .collect(), + }, }; knocked_rooms.insert(room_id, knocked_room); @@ -540,7 +550,7 @@ async fn handle_left_room( prev_batch: Some(next_batch.to_string()), events: Vec::new(), }, - state: RoomState { events: vec![event.into_format()] }, + state: RoomState::Before(StateEvents { events: vec![event.into_format()] }), })); } @@ -635,7 +645,7 @@ async fn handle_left_room( prev_batch: Some(next_batch.to_string()), events: Vec::new(), // and so we dont need to set this to empty vec }, - state: RoomState { events: left_state_events }, + state: RoomState::Before(StateEvents { events: left_state_events }), })) } @@ -1042,7 +1052,7 @@ async fn load_joined_room( let joined_room = JoinedRoom { account_data: RoomAccountData { events: account_data_events }, ephemeral: Ephemeral { events: edus }, - state: RoomState { events: state_events }, + state: RoomState::Before(StateEvents { events: state_events }), summary: RoomSummary { joined_member_count: joined_member_count.map(ruma_from_u64), invited_member_count: invited_member_count.map(ruma_from_u64), diff --git a/src/api/client/sync/v5.rs b/src/api/client/sync/v5.rs index 558aa5d9..e4b67cee 100644 --- a/src/api/client/sync/v5.rs +++ b/src/api/client/sync/v5.rs @@ -13,7 +13,10 @@ use futures::{ }; use ruma::{ DeviceId, OwnedEventId, OwnedRoomId, RoomId, UInt, UserId, - api::client::sync::sync_events::{self, DeviceLists, UnreadNotificationsCount}, + api::client::sync::sync_events::{ + self, DeviceLists, StrippedState, UnreadNotificationsCount, + v5::request::ExtensionRoomConfig, + }, directory::RoomTypeFilter, events::{ AnyRawAccountDataEvent, AnySyncEphemeralRoomEvent, StateEventType, TimelineEventType, @@ -647,7 +650,11 @@ where name: room_name.or(hero_name), initial: Some(roomsince == &0), is_dm: None, - invite_state, + invite_state: invite_state.map(|s| { + s.into_iter() + .map(Raw::cast::) + .collect() + }), unread_notifications: UnreadNotificationsCount { highlight_count: Some( services @@ -727,7 +734,10 @@ async fn collect_account_data( .await; if let Some(rooms) = &body.extensions.account_data.rooms { - for room in rooms { + for room in rooms + .iter() + .filter_map(|erc| extract_variant!(erc, ExtensionRoomConfig::Room)) + { account_data.rooms.insert( room.clone(), services diff --git a/src/api/client/unstable.rs b/src/api/client/unstable.rs index cff2e96f..5c17c763 100644 --- a/src/api/client/unstable.rs +++ b/src/api/client/unstable.rs @@ -146,7 +146,7 @@ pub(crate) async fn set_profile_key_route( ))); } - let Some(profile_key_value) = body.kv_pair.get(&body.key_name) else { + let Some(profile_key_value) = body.kv_pair.get(&body.key) else { return Err!(Request(BadJson( "The key does not match the URL field key, or JSON body is empty (use DELETE)" ))); @@ -164,7 +164,7 @@ pub(crate) async fn set_profile_key_route( return Err!(Request(BadJson("Key names cannot be longer than 128 bytes"))); } - if body.key_name == "displayname" { + if body.key == "displayname" { let all_joined_rooms: Vec = services .rooms .state_cache @@ -180,7 +180,7 @@ pub(crate) async fn set_profile_key_route( &all_joined_rooms, ) .await; - } else if body.key_name == "avatar_url" { + } else if body.key == "avatar_url" { let mxc = ruma::OwnedMxcUri::from(profile_key_value.to_string()); let all_joined_rooms: Vec = services @@ -193,11 +193,9 @@ pub(crate) async fn set_profile_key_route( update_avatar_url(&services, &body.user_id, Some(mxc), None, &all_joined_rooms).await; } else { - services.users.set_profile_key( - &body.user_id, - &body.key_name, - Some(profile_key_value.clone()), - ); + services + .users + .set_profile_key(&body.user_id, &body.key, Some(profile_key_value.clone())); } if services.config.allow_local_presence { @@ -233,7 +231,7 @@ pub(crate) async fn delete_profile_key_route( ))); } - if body.key_name == "displayname" { + if body.key == "displayname" { let all_joined_rooms: Vec = services .rooms .state_cache @@ -243,7 +241,7 @@ pub(crate) async fn delete_profile_key_route( .await; update_displayname(&services, &body.user_id, None, &all_joined_rooms).await; - } else if body.key_name == "avatar_url" { + } else if body.key == "avatar_url" { let all_joined_rooms: Vec = services .rooms .state_cache @@ -256,7 +254,7 @@ pub(crate) async fn delete_profile_key_route( } else { services .users - .set_profile_key(&body.user_id, &body.key_name, None); + .set_profile_key(&body.user_id, &body.key, None); } if services.config.allow_local_presence { @@ -379,14 +377,12 @@ pub(crate) async fn get_profile_key_route( .users .set_timezone(&body.user_id, response.tz.clone()); - match response.custom_profile_fields.get(&body.key_name) { + match response.custom_profile_fields.get(&body.key) { | Some(value) => { - profile_key_value.insert(body.key_name.clone(), value.clone()); - services.users.set_profile_key( - &body.user_id, - &body.key_name, - Some(value.clone()), - ); + profile_key_value.insert(body.key.clone(), value.clone()); + services + .users + .set_profile_key(&body.user_id, &body.key, Some(value.clone())); }, | _ => { return Err!(Request(NotFound("The requested profile key does not exist."))); @@ -409,11 +405,11 @@ pub(crate) async fn get_profile_key_route( match services .users - .profile_key(&body.user_id, &body.key_name) + .profile_key(&body.user_id, &body.key) .await { | Ok(value) => { - profile_key_value.insert(body.key_name.clone(), value); + profile_key_value.insert(body.key.clone(), value); }, | _ => { return Err!(Request(NotFound("The requested profile key does not exist."))); diff --git a/src/api/client/well_known.rs b/src/api/client/well_known.rs index db968d72..4dff3b3e 100644 --- a/src/api/client/well_known.rs +++ b/src/api/client/well_known.rs @@ -1,12 +1,9 @@ use axum::{Json, extract::State, response::IntoResponse}; -use ruma::api::client::{ - discovery::{ - discover_homeserver::{self, HomeserverInfo, SlidingSyncProxyInfo}, - discover_support::{self, Contact}, - }, - error::ErrorKind, +use ruma::api::client::discovery::{ + discover_homeserver::{self, HomeserverInfo}, + discover_support::{self, Contact}, }; -use tuwunel_core::{Error, Result}; +use tuwunel_core::{Err, Result}; use crate::Ruma; @@ -19,13 +16,12 @@ pub(crate) async fn well_known_client( ) -> Result { let client_url = match services.server.config.well_known.client.as_ref() { | Some(url) => url.to_string(), - | None => return Err(Error::BadRequest(ErrorKind::NotFound, "Not found.")), + | None => return Err!(Request(NotFound("Not found."))), }; Ok(discover_homeserver::Response { - homeserver: HomeserverInfo { base_url: client_url.clone() }, + homeserver: HomeserverInfo { base_url: client_url }, identity_server: None, - sliding_sync_proxy: Some(SlidingSyncProxyInfo { url: client_url }), tile_server: None, }) } @@ -54,7 +50,7 @@ pub(crate) async fn well_known_support( // support page or role must be either defined for this to be valid if support_page.is_none() && role.is_none() { - return Err(Error::BadRequest(ErrorKind::NotFound, "Not found.")); + return Err!(Request(NotFound("Not found."))); } let email_address = services @@ -63,6 +59,7 @@ pub(crate) async fn well_known_support( .well_known .support_email .clone(); + let matrix_id = services .server .config @@ -72,7 +69,7 @@ pub(crate) async fn well_known_support( // if a role is specified, an email address or matrix id is required if role.is_some() && (email_address.is_none() && matrix_id.is_none()) { - return Err(Error::BadRequest(ErrorKind::NotFound, "Not found.")); + return Err!(Request(NotFound("Not found."))); } // TODO: support defining multiple contacts in the config @@ -86,7 +83,7 @@ pub(crate) async fn well_known_support( // support page or role+contacts must be either defined for this to be valid if contacts.is_empty() && support_page.is_none() { - return Err(Error::BadRequest(ErrorKind::NotFound, "Not found.")); + return Err!(Request(NotFound("Not found."))); } Ok(discover_support::Response { contacts, support_page }) @@ -103,7 +100,7 @@ pub(crate) async fn syncv3_client_server_json( | Some(url) => url.to_string(), | None => match services.server.config.well_known.server.as_ref() { | Some(url) => url.to_string(), - | None => return Err(Error::BadRequest(ErrorKind::NotFound, "Not found.")), + | None => return Err!(Request(NotFound("Not found."))), }, }; diff --git a/src/api/mod.rs b/src/api/mod.rs index 5684c7f2..0409b2d8 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -1,4 +1,4 @@ -#![type_length_limit = "65536"] //TODO: reduce me +#![type_length_limit = "262144"] //TODO: REDUCE ME #![allow(clippy::toplevel_ref_arg)] pub mod client; diff --git a/src/api/router/auth.rs b/src/api/router/auth.rs index 6ea6c2a8..42eb2a85 100644 --- a/src/api/router/auth.rs +++ b/src/api/router/auth.rs @@ -69,7 +69,10 @@ pub(super) async fn auth( json_body: Option<&CanonicalJsonValue>, metadata: &Metadata, ) -> Result { - use AuthScheme::{AccessToken, AccessTokenOptional, AppserviceToken, ServerSignatures}; + use AuthScheme::{ + AccessToken, AccessTokenOptional, AppserviceToken, AppserviceTokenOptional, + ServerSignatures, + }; use Error::BadRequest; use ErrorKind::UnknownToken; use Token::{Appservice, Expired, Invalid, User}; @@ -129,7 +132,7 @@ pub(super) async fn auth( | (AccessToken, Appservice(info)) => Ok(auth_appservice(services, request, info).await?), - | (AccessToken, Token::None) => match metadata { + | (AccessToken | AppserviceToken, Token::None) => match metadata { | &get_turn_server_info::v3::Request::METADATA if services.server.config.turn_allow_guests => Ok(Auth::default()), @@ -137,22 +140,25 @@ pub(super) async fn auth( | _ => Err!(Request(MissingToken("Missing access token."))), }, - | (AccessToken | AccessTokenOptional | AuthScheme::None, User(user)) => Ok(Auth { + | ( + AccessToken | AccessTokenOptional | AppserviceTokenOptional | AuthScheme::None, + User(user), + ) => Ok(Auth { sender_user: Some(user.0), sender_device: Some(user.1), _expires_at: user.2, ..Auth::default() }), - //TODO: add AppserviceTokenOptional - | (AccessTokenOptional | AppserviceToken | AuthScheme::None, Appservice(info)) => - Ok(Auth { - appservice_info: Some(*info), - ..Auth::default() - }), + | ( + AccessTokenOptional | AppserviceTokenOptional | AppserviceToken | AuthScheme::None, + Appservice(info), + ) => Ok(Auth { + appservice_info: Some(*info), + ..Auth::default() + }), - //TODO: add AppserviceTokenOptional - | (AccessTokenOptional | AppserviceToken | AuthScheme::None, Token::None) => + | (AccessTokenOptional | AppserviceTokenOptional | AuthScheme::None, Token::None) => Ok(Auth::default()), } } @@ -306,7 +312,7 @@ async fn auth_server( let keys: PubKeys = [(x_matrix.key.to_string(), key.key)].into(); let keys: PubKeyMap = [(origin.as_str().into(), keys)].into(); - if let Err(e) = ruma::signatures::verify_json(&keys, authorization) { + if let Err(e) = ruma::signatures::verify_json(&keys, &authorization) { debug_error!("Failed to verify federation request from {origin}: {e}"); if request.parts.uri.to_string().contains('@') { warn!( diff --git a/src/api/server/hierarchy.rs b/src/api/server/hierarchy.rs index d5bc62d8..95c98b5a 100644 --- a/src/api/server/hierarchy.rs +++ b/src/api/server/hierarchy.rs @@ -64,17 +64,18 @@ pub(crate) async fn get_hierarchy_route( }) .unzip() .map(|(children, inaccessible_children): (Vec<_>, Vec<_>)| { - ( - children - .into_iter() - .flatten() - .map(Into::into) - .collect(), - inaccessible_children - .into_iter() - .flatten() - .collect(), - ) + let children = children + .into_iter() + .flatten() + .map(|parent| parent.summary) + .collect(); + + let inaccessible_children = inaccessible_children + .into_iter() + .flatten() + .collect(); + + (children, inaccessible_children) }) .await; diff --git a/src/api/server/invite.rs b/src/api/server/invite.rs index 12fdbe35..752178e4 100644 --- a/src/api/server/invite.rs +++ b/src/api/server/invite.rs @@ -3,12 +3,15 @@ use axum_client_ip::InsecureClientIp; use base64::{Engine as _, engine::general_purpose}; use ruma::{ CanonicalJsonValue, OwnedUserId, UserId, - api::{client::error::ErrorKind, federation::membership::create_invite}, + api::{ + client::error::ErrorKind, + federation::membership::{RawStrippedState, create_invite}, + }, events::room::member::{MembershipState, RoomMemberEventContent}, serde::JsonObject, }; use tuwunel_core::{ - Err, Error, Result, err, + Err, Error, Result, err, extract_variant, matrix::{Event, PduEvent, event::gen_event_id}, utils, utils::hash::sha256, @@ -119,7 +122,12 @@ pub(crate) async fn create_invite_route( return Err!(Request(Forbidden("This server does not allow room invites."))); } - let mut invite_state = body.invite_room_state.clone(); + let mut invite_state: Vec<_> = body + .invite_room_state + .clone() + .into_iter() + .filter_map(|s| extract_variant!(s, RawStrippedState::Stripped)) + .collect(); let mut event: JsonObject = serde_json::from_str(body.event.get()) .map_err(|e| err!(Request(BadJson("Invalid invite event PDU: {e}"))))?; diff --git a/src/api/server/make_knock.rs b/src/api/server/make_knock.rs index 310bbd0e..77d1db94 100644 --- a/src/api/server/make_knock.rs +++ b/src/api/server/make_knock.rs @@ -2,7 +2,7 @@ use RoomVersionId::*; use axum::extract::State; use ruma::{ RoomVersionId, - api::{client::error::ErrorKind, federation::knock::create_knock_event_template}, + api::{client::error::ErrorKind, federation::membership::prepare_knock_event}, events::room::member::{MembershipState, RoomMemberEventContent}, }; use serde_json::value::to_raw_value; @@ -15,8 +15,8 @@ use crate::Ruma; /// Creates a knock template. pub(crate) async fn create_knock_event_template_route( State(services): State, - body: Ruma, -) -> Result { + body: Ruma, +) -> Result { if !services .rooms .metadata @@ -124,7 +124,7 @@ pub(crate) async fn create_knock_event_template_route( // room v3 and above removed the "event_id" field from remote PDU format super::maybe_strip_event_id(&mut pdu_json, &room_version_id)?; - Ok(create_knock_event_template::v1::Response { + Ok(prepare_knock_event::v1::Response { room_version: room_version_id, event: to_raw_value(&pdu_json).expect("CanonicalJson can be serialized to JSON"), }) diff --git a/src/api/server/send_join.rs b/src/api/server/send_join.rs index b6b1cd10..f1260270 100644 --- a/src/api/server/send_join.rs +++ b/src/api/server/send_join.rs @@ -354,6 +354,7 @@ pub(crate) async fn create_join_event_v2_route( create_join_event(&services, body.origin(), &body.room_id, &body.pdu) .boxed() .await?; + let room_state = create_join_event::v2::RoomState { members_omitted: false, auth_chain, diff --git a/src/api/server/send_knock.rs b/src/api/server/send_knock.rs index b2b5425f..89f0b952 100644 --- a/src/api/server/send_knock.rs +++ b/src/api/server/send_knock.rs @@ -3,7 +3,7 @@ use futures::FutureExt; use ruma::{ OwnedServerName, OwnedUserId, RoomVersionId::*, - api::federation::knock::send_knock, + api::federation::membership::create_knock_event, events::{ StateEventType, room::member::{MembershipState, RoomMemberEventContent}, @@ -23,8 +23,8 @@ use crate::Ruma; /// Submits a signed knock event. pub(crate) async fn create_knock_event_v1_route( State(services): State, - body: Ruma, -) -> Result { + body: Ruma, +) -> Result { if services .config .forbidden_remote_server_names @@ -189,7 +189,14 @@ pub(crate) async fn create_knock_event_v1_route( .send_pdu_room(&body.room_id, &pdu_id) .await?; - let knock_room_state = services.rooms.state.summary_stripped(&pdu).await; - - Ok(send_knock::v1::Response { knock_room_state }) + Ok(create_knock_event::v1::Response { + knock_room_state: services + .rooms + .state + .summary_stripped(&pdu) + .await + .into_iter() + .map(Into::into) + .collect(), + }) } diff --git a/src/core/Cargo.toml b/src/core/Cargo.toml index b54b6c9e..72e545fc 100644 --- a/src/core/Cargo.toml +++ b/src/core/Cargo.toml @@ -119,7 +119,9 @@ hardened_malloc-rs.workspace = true hardened_malloc-rs.optional = true [dev-dependencies] +insta.workspace = true maplit.workspace = true +similar.workspace = true [lints] workspace = true diff --git a/src/core/config/mod.rs b/src/core/config/mod.rs index bba01ba5..962f40a3 100644 --- a/src/core/config/mod.rs +++ b/src/core/config/mod.rs @@ -665,20 +665,33 @@ pub struct Config { pub allow_room_creation: bool, /// Set to false to disable users from joining or creating room versions - /// that aren't officially supported by tuwunel. + /// that aren't officially supported by tuwunel. Unstable room versions may + /// have flawed specifications or our implementation may be non-conforming. + /// Correct operation may not be guaranteed, but incorrect operation may be + /// tolerable and unnoticed. /// - /// tuwunel officially supports room versions 6 - 11. + /// tuwunel officially supports room versions 6+. tuwunel has slightly + /// experimental (though works fine in practice) support for versions 3 - 5. /// - /// tuwunel has slightly experimental (though works fine in practice) - /// support for versions 3 - 5. + /// default: true #[serde(default = "true_fn")] pub allow_unstable_room_versions: bool, + /// Set to true to enable experimental room versions. + /// + /// Unlike unstable room versions these versions are either under + /// development, protype spec-changes, or somehow present a serious risk to + /// the server's operation or database corruption. This is for developer use + /// only. + #[serde(default)] + pub allow_experimental_room_versions: bool, + /// Default room version tuwunel will create rooms with. /// - /// Per spec, room version 11 is the default. - /// - /// default: 11 + /// The default is prescribed by the spec, but may be selected by developer + /// recommendation. To prevent stale documentation we no longer list it + /// here. It is only advised to override this if you know what you are + /// doing, and by doing so, updates with new versions are precluded. #[serde(default = "default_default_room_version")] pub default_room_version: RoomVersionId, @@ -1885,6 +1898,30 @@ pub struct Config { #[serde(default)] pub allow_invalid_tls_certificates: bool, + /// Backport state-reset security fixes to all room versions. + /// + /// This option applies the State Resolution 2.1 mitigation developed during + /// project Hydra for room version 12 to all prior State Resolution 2.0 room + /// versions (all room versions supported by this server). These mitigations + /// increase resilience to state-resets without any new definition of + /// correctness; therefor it is safe to set this to true for existing rooms. + /// + /// Furthermore, state-reset attacks are not consistent as they result in + /// rooms without any single consensus, therefor it is unnecessary to set + /// this to false to match other servers which set this to false or simply + /// lack support; even if replicating the post-reset state suffered by other + /// servers is somehow desired. + /// + /// This option exists for developer and debug use, and as a failsafe in + /// lieu of hardcoding it. + /// + /// This currently defaults to false as a matter of development until + /// real-world testing can shake out any implementation issues rather than + /// jeopardize existing rooms, but otherwise will default to true at the + /// next point release or patch. + #[serde(default)] + pub hydra_backports: bool, + // external structure; separate section #[serde(default)] pub blurhashing: BlurhashConfig, diff --git a/src/core/error/mod.rs b/src/core/error/mod.rs index 713a8513..7a12519b 100644 --- a/src/core/error/mod.rs +++ b/src/core/error/mod.rs @@ -118,6 +118,8 @@ pub enum Error { Mxc(#[from] ruma::MxcUriError), #[error(transparent)] Mxid(#[from] ruma::IdParseError), + #[error(transparent)] + PowerLevels(#[from] ruma::events::room::power_levels::PowerLevelsError), #[error("from {0}: {1}")] Redaction(ruma::OwnedServerName, ruma::canonical_json::RedactionError), #[error("{0}: {1}")] @@ -126,8 +128,6 @@ pub enum Error { Ruma(#[from] ruma::api::client::error::Error), #[error(transparent)] Signatures(#[from] ruma::signatures::Error), - #[error(transparent)] - StateRes(#[from] crate::state_res::Error), #[error("uiaa")] Uiaa(ruma::api::client::uiaa::UiaaInfo), diff --git a/src/core/error/response.rs b/src/core/error/response.rs index 7d07b5a1..c41d542c 100644 --- a/src/core/error/response.rs +++ b/src/core/error/response.rs @@ -41,10 +41,7 @@ impl From for UiaaResponse { message: error.message(), }; - Self::MatrixError(ruma::api::client::error::Error { - status_code: error.status_code(), - body, - }) + Self::MatrixError(ruma::api::client::error::Error::new(error.status_code(), body)) } } diff --git a/src/core/info/room_version.rs b/src/core/info/room_version.rs index e29eda2b..3bf60b1a 100644 --- a/src/core/info/room_version.rs +++ b/src/core/info/room_version.rs @@ -2,10 +2,19 @@ use std::iter::once; -use ruma::{RoomVersionId, api::client::discovery::get_capabilities::RoomVersionStability}; +use ruma::{RoomVersionId, api::client::discovery::get_capabilities::v3::RoomVersionStability}; use crate::{at, is_equal_to}; +/// Partially supported non-compliant room versions +pub const UNSTABLE_ROOM_VERSIONS: &[RoomVersionId] = &[ + RoomVersionId::V1, + RoomVersionId::V2, + RoomVersionId::V3, + RoomVersionId::V4, + RoomVersionId::V5, +]; + /// Supported and stable room versions pub const STABLE_ROOM_VERSIONS: &[RoomVersionId] = &[ RoomVersionId::V6, @@ -16,9 +25,8 @@ pub const STABLE_ROOM_VERSIONS: &[RoomVersionId] = &[ RoomVersionId::V11, ]; -/// Experimental, partially supported room versions -pub const UNSTABLE_ROOM_VERSIONS: &[RoomVersionId] = - &[RoomVersionId::V2, RoomVersionId::V3, RoomVersionId::V4, RoomVersionId::V5]; +/// Experimental and prototype room versions under development. +pub const EXPERIMENTAL_ROOM_VERSIONS: &[RoomVersionId] = &[RoomVersionId::V12]; type RoomVersion = (RoomVersionId, RoomVersionStability); @@ -31,8 +39,15 @@ impl crate::Server { #[inline] pub fn supported_room_versions(&self) -> impl Iterator + '_ { + let experimental_room_versions = EXPERIMENTAL_ROOM_VERSIONS + .iter() + .cloned() + .zip(once(RoomVersionStability::Unstable).cycle()) + .filter(|_| self.config.allow_experimental_room_versions); + Self::available_room_versions() .filter(|(_, stability)| self.supported_stability(stability)) + .chain(experimental_room_versions) .map(at!(0)) } diff --git a/src/core/matrix/event.rs b/src/core/matrix/event.rs index 35a79966..a9b958bf 100644 --- a/src/core/matrix/event.rs +++ b/src/core/matrix/event.rs @@ -4,6 +4,7 @@ mod format; mod id; mod redact; mod relation; +pub mod state_key; mod type_ext; mod unsigned; @@ -16,12 +17,23 @@ use ruma::{ use serde::Deserialize; use serde_json::{Value as JsonValue, value::RawValue as RawJsonValue}; -pub use self::{filter::Matches, id::*, relation::RelationTypeEqual, type_ext::TypeExt}; -use super::{pdu::Pdu, state_key::StateKey}; +pub use self::{ + filter::Matches, + id::*, + relation::RelationTypeEqual, + state_key::{StateKey, TypeStateKey}, + type_ext::TypeExt, +}; +use super::pdu::Pdu; use crate::{Result, utils}; /// Abstraction of a PDU so users can have their own PDU types. pub trait Event: Clone + Debug + Send + Sync { + #[inline] + fn is_type_and_state_key(&self, kind: &TimelineEventType, state_key: &str) -> bool { + self.kind() == kind && self.state_key() == Some(state_key) + } + /// Serialize into a Ruma JSON format, consuming. #[inline] fn into_format(self) -> T @@ -152,6 +164,11 @@ pub trait Event: Clone + Debug + Send + Sync { /// All the authenticating events for this event. fn auth_events(&self) -> impl DoubleEndedIterator + Clone + Send + '_; + /// All the authenticating events for this event. + fn auth_events_into( + self, + ) -> impl IntoIterator> + Send; + /// The event's content. fn content(&self) -> &RawJsonValue; @@ -167,6 +184,9 @@ pub trait Event: Clone + Debug + Send + Sync { /// If this event is a redaction event this is the event it redacts. fn redacts(&self) -> Option<&EventId>; + /// see: https://spec.matrix.org/v1.14/rooms/v11/#rejected-events + fn rejected(&self) -> bool; + /// The `RoomId` of this event. fn room_id(&self) -> &RoomId; diff --git a/src/core/matrix/event/format.rs b/src/core/matrix/event/format.rs index 988cf4f0..20a048b1 100644 --- a/src/core/matrix/event/format.rs +++ b/src/core/matrix/event/format.rs @@ -1,8 +1,8 @@ use ruma::{ events::{ - AnyMessageLikeEvent, AnyStateEvent, AnyStrippedStateEvent, AnySyncStateEvent, - AnySyncTimelineEvent, AnyTimelineEvent, StateEvent, room::member::RoomMemberEventContent, - space::child::HierarchySpaceChildEvent, + AnyMessageLikeEvent, AnyStateEvent, AnyStrippedStateEvent, AnySyncMessageLikeEvent, + AnySyncStateEvent, AnySyncTimelineEvent, AnyTimelineEvent, StateEvent, + room::member::RoomMemberEventContent, space::child::HierarchySpaceChildEvent, }, serde::Raw, }; @@ -106,6 +106,36 @@ impl<'a, E: Event> From> for Raw { } } +impl From> for Raw { + fn from(event: Owned) -> Self { Ref(&event.0).into() } +} + +impl<'a, E: Event> From> for Raw { + fn from(event: Ref<'a, E>) -> Self { + let event = event.0; + let (redacts, content) = redact::copy(event); + let mut json = json!({ + "content": content, + "event_id": event.event_id(), + "origin_server_ts": event.origin_server_ts(), + "sender": event.sender(), + "type": event.kind(), + }); + + if let Some(redacts) = &redacts { + json["redacts"] = json!(redacts); + } + if let Some(state_key) = event.state_key() { + json["state_key"] = json!(state_key); + } + if let Some(unsigned) = event.unsigned() { + json["unsigned"] = json!(unsigned); + } + + serde_json::from_value(json).expect("Failed to serialize Event value") + } +} + impl From> for Raw { fn from(event: Owned) -> Self { Ref(&event.0).into() } } diff --git a/src/core/matrix/event/id.rs b/src/core/matrix/event/id.rs index e9d868b1..6ac9a878 100644 --- a/src/core/matrix/event/id.rs +++ b/src/core/matrix/event/id.rs @@ -1,7 +1,7 @@ use ruma::{CanonicalJsonObject, OwnedEventId, RoomVersionId}; use serde_json::value::RawValue as RawJsonValue; -use crate::{Result, err}; +use crate::{Result, err, matrix::room_version}; /// Generates a correct eventId for the incoming pdu. /// @@ -24,8 +24,8 @@ pub fn gen_event_id( value: &CanonicalJsonObject, room_version_id: &RoomVersionId, ) -> Result { - let reference_hash = ruma::signatures::reference_hash(value, room_version_id)?; - let event_id: OwnedEventId = format!("${reference_hash}").try_into()?; + let room_version_rules = room_version::rules(room_version_id)?; + let reference_hash = ruma::signatures::reference_hash(value, &room_version_rules)?; - Ok(event_id) + OwnedEventId::from_parts('$', &reference_hash, None).map_err(Into::into) } diff --git a/src/core/matrix/event/state_key.rs b/src/core/matrix/event/state_key.rs new file mode 100644 index 00000000..05d51460 --- /dev/null +++ b/src/core/matrix/event/state_key.rs @@ -0,0 +1,17 @@ +use std::cmp::Ordering; + +use ruma::events::StateEventType; +use smallstr::SmallString; + +pub type TypeStateKey = (StateEventType, StateKey); +pub type StateKey = SmallString<[u8; INLINE_SIZE]>; + +const INLINE_SIZE: usize = 48; + +#[inline] +#[must_use] +pub fn cmp(a: &TypeStateKey, b: &TypeStateKey) -> Ordering { a.0.cmp(&b.0).then(a.1.cmp(&b.1)) } + +#[inline] +#[must_use] +pub fn rcmp(a: &TypeStateKey, b: &TypeStateKey) -> Ordering { b.0.cmp(&a.0).then(b.1.cmp(&a.1)) } diff --git a/src/core/matrix/event/type_ext.rs b/src/core/matrix/event/type_ext.rs index 9b824d41..9044665c 100644 --- a/src/core/matrix/event/type_ext.rs +++ b/src/core/matrix/event/type_ext.rs @@ -21,12 +21,12 @@ impl TypeExt for &StateEventType { impl TypeExt for TimelineEventType { fn with_state_key(self, state_key: impl Into) -> (StateEventType, StateKey) { - (self.into(), state_key.into()) + (self.to_cow_str().into(), state_key.into()) } } impl TypeExt for &TimelineEventType { fn with_state_key(self, state_key: impl Into) -> (StateEventType, StateKey) { - (self.clone().into(), state_key.into()) + (self.to_cow_str().into(), state_key.into()) } } diff --git a/src/core/matrix/mod.rs b/src/core/matrix/mod.rs index b38d4c9a..aab42398 100644 --- a/src/core/matrix/mod.rs +++ b/src/core/matrix/mod.rs @@ -2,10 +2,10 @@ pub mod event; pub mod pdu; -pub mod state_key; +pub mod room_version; pub mod state_res; -pub use event::{Event, TypeExt as EventTypeExt}; -pub use pdu::{Pdu, PduBuilder, PduCount, PduEvent, PduId, RawPduId, ShortId}; -pub use state_key::StateKey; -pub use state_res::{RoomVersion, StateMap, TypeStateKey}; +pub use event::{Event, StateKey, TypeExt as EventTypeExt, TypeStateKey, state_key}; +pub use pdu::{EventHash, Pdu, PduBuilder, PduCount, PduEvent, PduId, RawPduId, ShortId}; +pub use room_version::{RoomVersion, RoomVersionRules}; +pub use state_res::{StateMap, events}; diff --git a/src/core/matrix/pdu.rs b/src/core/matrix/pdu.rs index 9d4975b5..3c5bcf6b 100644 --- a/src/core/matrix/pdu.rs +++ b/src/core/matrix/pdu.rs @@ -67,8 +67,25 @@ pub struct Pdu { // BTreeMap, BTreeMap> #[serde(default, skip_serializing_if = "Option::is_none")] pub signatures: Option>, + + //TODO: https://spec.matrix.org/v1.14/rooms/v11/#rejected-events + #[cfg(test)] + #[serde(default, skip_serializing)] + pub rejected: bool, } +/// The [maximum size allowed] for a PDU. +/// [maximum size allowed]: https://spec.matrix.org/latest/client-server-api/#size-limits +pub const MAX_PDU_BYTES: usize = 65_535; + +/// The [maximum length allowed] for the `prev_events` array of a PDU. +/// [maximum length allowed]: https://spec.matrix.org/latest/rooms/v1/#event-format +pub const MAX_PREV_EVENTS: usize = 20; + +/// The [maximum length allowed] for the `auth_events` array of a PDU. +/// [maximum length allowed]: https://spec.matrix.org/latest/rooms/v1/#event-format +pub const MAX_AUTH_EVENTS: usize = 10; + impl Pdu { pub fn from_id_val(event_id: &EventId, mut json: CanonicalJsonObject) -> Result { let event_id = CanonicalJsonValue::String(event_id.into()); @@ -77,6 +94,20 @@ impl Pdu { .and_then(serde_json::from_value) .map_err(Into::into) } + + pub fn from_rid_val( + room_id: &RoomId, + event_id: &EventId, + mut json: CanonicalJsonObject, + ) -> Result { + let event_id = CanonicalJsonValue::String(event_id.into()); + let room_id = CanonicalJsonValue::String(room_id.into()); + json.insert("event_id".into(), event_id); + json.insert("room_id".into(), room_id); + serde_json::to_value(json) + .and_then(serde_json::from_value) + .map_err(Into::into) + } } impl Event for Pdu @@ -88,6 +119,13 @@ where self.auth_events.iter().map(AsRef::as_ref) } + #[inline] + fn auth_events_into( + self, + ) -> impl IntoIterator> + Send { + self.auth_events.into_iter() + } + #[inline] fn content(&self) -> &RawJsonValue { &self.content } @@ -107,6 +145,14 @@ where #[inline] fn redacts(&self) -> Option<&EventId> { self.redacts.as_deref() } + #[cfg(test)] + #[inline] + fn rejected(&self) -> bool { self.rejected } + + #[cfg(not(test))] + #[inline] + fn rejected(&self) -> bool { false } + #[inline] fn room_id(&self) -> &RoomId { &self.room_id } @@ -144,6 +190,13 @@ where self.auth_events.iter().map(AsRef::as_ref) } + #[inline] + fn auth_events_into( + self, + ) -> impl IntoIterator> + Send { + self.auth_events.iter().map(ToOwned::to_owned) + } + #[inline] fn content(&self) -> &RawJsonValue { &self.content } @@ -163,6 +216,14 @@ where #[inline] fn redacts(&self) -> Option<&EventId> { self.redacts.as_deref() } + #[cfg(test)] + #[inline] + fn rejected(&self) -> bool { self.rejected } + + #[cfg(not(test))] + #[inline] + fn rejected(&self) -> bool { false } + #[inline] fn room_id(&self) -> &RoomId { &self.room_id } diff --git a/src/core/matrix/pdu/builder.rs b/src/core/matrix/pdu/builder.rs index 5aa0c9ca..38ba3f44 100644 --- a/src/core/matrix/pdu/builder.rs +++ b/src/core/matrix/pdu/builder.rs @@ -2,7 +2,7 @@ use std::collections::BTreeMap; use ruma::{ MilliSecondsSinceUnixEpoch, OwnedEventId, - events::{EventContent, MessageLikeEventType, StateEventType, TimelineEventType}, + events::{MessageLikeEventContent, StateEventContent, TimelineEventType}, }; use serde::Deserialize; use serde_json::value::{RawValue as RawJsonValue, to_raw_value}; @@ -33,7 +33,7 @@ type Unsigned = BTreeMap; impl Builder { pub fn state(state_key: S, content: &T) -> Self where - T: EventContent, + T: StateEventContent, S: Into, { Self { @@ -47,7 +47,7 @@ impl Builder { pub fn timeline(content: &T) -> Self where - T: EventContent, + T: MessageLikeEventContent, { Self { event_type: content.event_type().into(), diff --git a/src/core/matrix/pdu/redact.rs b/src/core/matrix/pdu/redact.rs index 896e03f8..a025db2c 100644 --- a/src/core/matrix/pdu/redact.rs +++ b/src/core/matrix/pdu/redact.rs @@ -10,7 +10,13 @@ pub fn redact(&mut self, room_version_id: &RoomVersionId, reason: JsonValue) -> let mut content = serde_json::from_str(self.content.get()) .map_err(|e| err!(Request(BadJson("Failed to deserialize content into type: {e}"))))?; - redact_content_in_place(&mut content, room_version_id, self.kind.to_string()) + let room_version_rules = room_version_id.rules().ok_or_else(|| { + err!(Request(UnsupportedRoomVersion( + "Cannot redact event for unknown room version {room_version_id:?}." + ))) + })?; + + redact_content_in_place(&mut content, &room_version_rules.redaction, self.kind.to_string()) .map_err(|e| Error::Redaction(self.sender.server_name().to_owned(), e))?; let reason = serde_json::to_value(reason).expect("Failed to preserialize reason"); diff --git a/src/core/matrix/room_version.rs b/src/core/matrix/room_version.rs new file mode 100644 index 00000000..2fb9242f --- /dev/null +++ b/src/core/matrix/room_version.rs @@ -0,0 +1,23 @@ +use ruma::{RoomVersionId, events::room::create::RoomCreateEventContent}; +pub use ruma::{RoomVersionId as RoomVersion, room_version_rules::RoomVersionRules}; + +use crate::{Result, err, matrix::Event}; + +pub fn rules(room_version_id: &RoomVersionId) -> Result { + room_version_id.rules().ok_or_else(|| { + err!(Request(UnsupportedRoomVersion( + "Unknown or unsupported room version {room_version_id:?}.", + ))) + }) +} + +pub fn from_create_event(create_event: &Pdu) -> Result { + let content: RoomCreateEventContent = create_event.get_content()?; + Ok(from_create_content(&content).clone()) +} + +#[inline] +#[must_use] +pub fn from_create_content(content: &RoomCreateEventContent) -> &RoomVersionId { + &content.room_version +} diff --git a/src/core/matrix/state_key.rs b/src/core/matrix/state_key.rs deleted file mode 100644 index 06d614f8..00000000 --- a/src/core/matrix/state_key.rs +++ /dev/null @@ -1,5 +0,0 @@ -use smallstr::SmallString; - -pub type StateKey = SmallString<[u8; INLINE_SIZE]>; - -const INLINE_SIZE: usize = 48; diff --git a/src/core/matrix/state_res/benches.rs b/src/core/matrix/state_res/benches.rs index 1da4da40..2abf9011 100644 --- a/src/core/matrix/state_res/benches.rs +++ b/src/core/matrix/state_res/benches.rs @@ -1,18 +1,18 @@ +#![cfg_attr(not(tuwunel_bench), allow(unused_imports, dead_code))] + #[cfg(tuwunel_bench)] extern crate test; use std::{ borrow::Borrow, - collections::{HashMap, HashSet}, + collections::HashMap, sync::atomic::{AtomicU64, Ordering::SeqCst}, }; -use futures::{future, future::ready}; -use maplit::{btreemap, hashmap, hashset}; use ruma::{ - EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, RoomId, RoomVersionId, Signatures, UserId, + EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, RoomId, RoomVersionId, UserId, events::{ - StateEventType, TimelineEventType, + TimelineEventType, room::{ join_rules::{JoinRule, RoomJoinRulesEventContent}, member::{MembershipState, RoomMemberEventContent}, @@ -25,9 +25,11 @@ use serde_json::{ value::{RawValue as RawJsonValue, to_raw_value as to_raw_json_value}, }; +use super::{AuthSet, StateMap, test_utils::not_found}; use crate::{ - matrix::{Event, Pdu, pdu::EventHash}, - state_res::{self as state_res, Error, Result, StateMap}, + Result, + matrix::{Event, EventHash, PduEvent, event::TypeExt}, + utils::stream::IterStream, }; static SERVER_TIMESTAMP: AtomicU64 = AtomicU64::new(0); @@ -35,6 +37,12 @@ static SERVER_TIMESTAMP: AtomicU64 = AtomicU64::new(0); #[cfg(tuwunel_bench)] #[cfg_attr(tuwunel_bench, bench)] fn lexico_topo_sort(c: &mut test::Bencher) { + use maplit::{hashmap, hashset}; + + let rt = tokio::runtime::Builder::new_current_thread() + .build() + .unwrap(); + let graph = hashmap! { event_id("l") => hashset![event_id("o")], event_id("m") => hashset![event_id("n"), event_id("o")], @@ -43,9 +51,12 @@ fn lexico_topo_sort(c: &mut test::Bencher) { event_id("p") => hashset![event_id("o")], }; - c.iter(|| { - let _ = state_res::lexicographical_topological_sort(&graph, &|_| { - future::ok((int!(0), MilliSecondsSinceUnixEpoch(uint!(0)))) + c.iter(move || { + rt.block_on(async { + _ = super::topological_sort(&graph, &async |_id| { + Ok((int!(0).into(), MilliSecondsSinceUnixEpoch(uint!(0)))) + }) + .await; }); }); } @@ -53,43 +64,56 @@ fn lexico_topo_sort(c: &mut test::Bencher) { #[cfg(tuwunel_bench)] #[cfg_attr(tuwunel_bench, bench)] fn resolution_shallow_auth_chain(c: &mut test::Bencher) { - let mut store = TestStore(hashmap! {}); + let rt = tokio::runtime::Builder::new_current_thread() + .build() + .unwrap(); + + let mut store = TestStore(maplit::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 rules = RoomVersionId::V6.rules().unwrap(); + let ev_map = store.0.clone(); + let state_sets = [state_at_bob, state_at_charlie]; + let auth_chains = 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, + let func = async || { + if let Err(e) = super::resolve( + &rules, + state_sets.clone().into_iter().stream(), + auth_chains.clone().into_iter().stream(), + &async |id| ev_map.get(&id).cloned().ok_or_else(not_found), + &async |id| ev_map.contains_key(&id), + false, ) .await { - | Ok(state) => state, - | Err(e) => panic!("{e}"), - }; + panic!("{e}") + } + }; + + c.iter(move || { + rt.block_on(async { + func().await; + }); }); } #[cfg(tuwunel_bench)] #[cfg_attr(tuwunel_bench, bench)] fn resolve_deeper_event_set(c: &mut test::Bencher) { + let rt = tokio::runtime::Builder::new_current_thread() + .build() + .unwrap(); + let mut inner = INITIAL_EVENTS(); let ban = BAN_STATE_SET(); @@ -97,66 +121,73 @@ fn resolve_deeper_event_set(c: &mut test::Bencher) { 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(), + &inner[&event_id("CREATE")], + &inner[&event_id("IJR")], + &inner[&event_id("IMA")], + &inner[&event_id("IMB")], + &inner[&event_id("IMC")], + &inner[&event_id("MB")], + &inner[&event_id("PA")], ] .iter() .map(|ev| { ( - (ev.event_type().clone().into(), ev.state_key().unwrap().into()), + ev.event_type() + .with_state_key(ev.state_key().unwrap()), 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(), + &inner[&event_id("CREATE")], + &inner[&event_id("IJR")], + &inner[&event_id("IMA")], + &inner[&event_id("IMB")], + &inner[&event_id("IMC")], + &inner[&event_id("IME")], + &inner[&event_id("PA")], ] .iter() .map(|ev| { ( - (ev.event_type().clone().into(), ev.state_key().unwrap().into()), + ev.event_type() + .with_state_key(ev.state_key().unwrap()), 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 rules = RoomVersionId::V6.rules().unwrap(); + let state_sets = [state_set_a, state_set_b]; + let auth_chains = 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, + let func = async || { + if let Err(e) = super::resolve( + &rules, + state_sets.clone().into_iter().stream(), + auth_chains.clone().into_iter().stream(), + &async |id| inner.get(&id).cloned().ok_or_else(not_found), + &async |id| inner.contains_key(&id), + false, ) .await { - | Ok(state) => state, - | Err(_) => panic!("resolution failed during benchmarking"), - }; + panic!("{e}") + } + }; + + c.iter(move || { + rt.block_on(async { + func().await; + }); }); } @@ -168,12 +199,12 @@ fn resolve_deeper_event_set(c: &mut test::Bencher) { struct TestStore(HashMap); #[allow(unused)] -impl TestStore { +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))) + .ok_or_else(not_found) } /// Returns the events that correspond to the `event_ids` sorted in the same @@ -191,13 +222,12 @@ impl TestStore { &self, room_id: &RoomId, event_ids: Vec, - ) -> Result> { - let mut result = HashSet::new(); + ) -> Result> { + let mut result = AuthSet::new(); let mut stack = event_ids; // DFS for auth event chain - while !stack.is_empty() { - let ev_id = stack.pop().unwrap(); + while let Some(ev_id) = stack.pop() { if result.contains(&ev_id) { continue; } @@ -226,7 +256,8 @@ impl TestStore { let chain = self .auth_event_ids(room_id, ids)? .into_iter() - .collect::>(); + .collect::>(); + auth_chain_sets.push(chain); } @@ -234,7 +265,7 @@ impl TestStore { let common = auth_chain_sets .iter() .skip(1) - .fold(first, |a, b| a.intersection(b).cloned().collect::>()); + .fold(first, |a, b| a.intersection(b).cloned().collect::>()); Ok(auth_chain_sets .into_iter() @@ -247,7 +278,7 @@ impl TestStore { } } -impl TestStore { +impl TestStore { #[allow(clippy::type_complexity)] fn set_up( &mut self, @@ -261,8 +292,9 @@ impl TestStore { &[], &[], ); - let cre = create_event.event_id().to_owned(); - self.0.insert(cre.clone(), create_event.clone()); + let cre = create_event.event_id(); + self.0 + .insert(cre.to_owned(), create_event.clone()); let alice_mem = to_pdu_event( "IMA", @@ -270,8 +302,8 @@ impl TestStore { TimelineEventType::RoomMember, Some(alice().to_string().as_str()), member_content_join(), - &[cre.clone()], - &[cre.clone()], + &[cre.to_owned()], + &[cre.to_owned()], ); self.0 .insert(alice_mem.event_id().to_owned(), alice_mem.clone()); @@ -282,7 +314,7 @@ impl TestStore { TimelineEventType::RoomJoinRules, Some(""), to_raw_json_value(&RoomJoinRulesEventContent::new(JoinRule::Public)).unwrap(), - &[cre.clone(), alice_mem.event_id().to_owned()], + &[cre.to_owned(), alice_mem.event_id().to_owned()], &[alice_mem.event_id().to_owned()], ); self.0 @@ -296,7 +328,7 @@ impl TestStore { TimelineEventType::RoomMember, Some(bob().to_string().as_str()), member_content_join(), - &[cre.clone(), join_rules.event_id().to_owned()], + &[cre.to_owned(), join_rules.event_id().to_owned()], &[join_rules.event_id().to_owned()], ); self.0 @@ -308,7 +340,7 @@ impl TestStore { TimelineEventType::RoomMember, Some(charlie().to_string().as_str()), member_content_join(), - &[cre, join_rules.event_id().to_owned()], + &[cre.to_owned(), join_rules.event_id().to_owned()], &[join_rules.event_id().to_owned()], ); self.0 @@ -316,30 +348,33 @@ impl TestStore { let state_at_bob = [&create_event, &alice_mem, &join_rules, &bob_mem] .iter() - .map(|ev| { + .map(|e| { ( - (ev.event_type().clone().into(), ev.state_key().unwrap().into()), - ev.event_id().to_owned(), + 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(|ev| { + .map(|e| { ( - (ev.event_type().clone().into(), ev.state_key().unwrap().into()), - ev.event_id().to_owned(), + 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(|ev| { + .map(|e| { ( - (ev.event_type().clone().into(), ev.state_key().unwrap().into()), - ev.event_id().to_owned(), + e.event_type() + .with_state_key(e.state_key().unwrap()), + e.event_id().to_owned(), ) }) .collect::>(); @@ -352,7 +387,7 @@ fn event_id(id: &str) -> OwnedEventId { if id.contains('$') { return id.try_into().unwrap(); } - format!("${}:foo", id).try_into().unwrap() + format!("${id}:foo").try_into().unwrap() } fn alice() -> &'static UserId { user_id!("@alice:foo") } @@ -381,7 +416,7 @@ fn to_pdu_event( content: Box, auth_events: &[S], prev_events: &[S], -) -> Pdu +) -> PduEvent where S: AsRef, { @@ -391,28 +426,31 @@ where let id = if id.contains('$') { id.to_owned() } else { - format!("${}:foo", id) + 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 { + let state_key = state_key.map(ToOwned::to_owned); + PduEvent { event_id: id.try_into().unwrap(), room_id: room_id().to_owned(), sender: sender.to_owned(), + origin: None, origin_server_ts: ts.try_into().unwrap(), state_key: state_key.map(Into::into), kind: ev_type, content, - origin: None, redacts: None, unsigned: None, auth_events, @@ -420,12 +458,14 @@ where depth: uint!(0), hashes: EventHash::default(), signatures: None, + #[cfg(test)] + rejected: false, } } // all graphs start with these input events #[allow(non_snake_case)] -fn INITIAL_EVENTS() -> HashMap { +fn INITIAL_EVENTS() -> HashMap { vec![ to_pdu_event::<&EventId>( "CREATE", @@ -507,7 +547,7 @@ fn INITIAL_EVENTS() -> HashMap { // all graphs start with these input events #[allow(non_snake_case)] -fn BAN_STATE_SET() -> HashMap { +fn BAN_STATE_SET() -> HashMap { vec![ to_pdu_event( "PA", diff --git a/src/core/matrix/state_res/error.rs b/src/core/matrix/state_res/error.rs deleted file mode 100644 index 7711d878..00000000 --- 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 index bf89c5bd..949887e2 100644 --- a/src/core/matrix/state_res/event_auth.rs +++ b/src/core/matrix/state_res/event_auth.rs @@ -1,1526 +1,707 @@ -use std::{borrow::Borrow, collections::BTreeSet}; +mod auth_types; +mod room_member; +#[cfg(test)] +mod tests; + +use std::collections::HashSet; use futures::{ - Future, - future::{OptionFuture, join3}, + TryFutureExt, TryStreamExt, + future::{join3, try_join}, }; use ruma::{ - Int, OwnedUserId, RoomVersionId, UserId, - events::room::{ - create::RoomCreateEventContent, - join_rules::{JoinRule, RoomJoinRulesEventContent}, - member::{MembershipState, ThirdPartyInvite}, - power_levels::RoomPowerLevelsEventContent, - third_party_invite::RoomThirdPartyInviteEventContent, + EventId, Int, OwnedEventId, OwnedUserId, + api::client::error::ErrorKind::InvalidParam, + events::{ + StateEventType, TimelineEventType, + room::{member::MembershipState, power_levels::UserPowerLevel}, }, - int, - serde::{Base64, Raw}, + room_version_rules::{AuthorizationRules, RoomVersionRules}, }; -use serde::{ - Deserialize, - de::{Error as _, IgnoredAny}, -}; -use serde_json::{from_str as from_json_str, value::RawValue as RawJsonValue}; +pub use self::auth_types::{AuthTypes, auth_types_for_event}; +use self::room_member::check_room_member; +#[cfg(test)] +use super::test_utils; use super::{ - Error, Event, Result, StateEventType, StateKey, TimelineEventType, - power_levels::{ - deserialize_power_levels, deserialize_power_levels_content_fields, - deserialize_power_levels_content_invite, deserialize_power_levels_content_redact, + FetchStateExt, TypeStateKey, events, + events::{ + RoomCreateEvent, RoomMemberEvent, RoomPowerLevelsEvent, + power_levels::{self, RoomPowerLevelsEventOptionExt, RoomPowerLevelsIntField}, }, - room_version::RoomVersion, }; -use crate::{debug, error, trace, warn}; +use crate::{ + Err, Error, Result, err, + matrix::{Event, StateKey}, + trace, + utils::stream::{IterStream, TryReadyExt}, +}; -// 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>, -} - -/// 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, -) -> serde_json::Result> { - if kind == &TimelineEventType::RoomCreate { - return Ok(vec![]); - } - - let mut auth_types = vec![ - (StateEventType::RoomPowerLevels, StateKey::new()), - (StateEventType::RoomMember, sender.as_str().into()), - (StateEventType::RoomCreate, StateKey::new()), - ]; - - if kind == &TimelineEventType::RoomMember { - #[derive(Deserialize)] - struct RoomMemberContentFields { - membership: Option>, - third_party_invite: Option>, - join_authorised_via_users_server: Option>, - } - - if let Some(state_key) = state_key { - let content: RoomMemberContentFields = from_json_str(content.get())?; - - if let Some(Ok(membership)) = content.membership.map(|m| m.deserialize()) { - if [MembershipState::Join, MembershipState::Invite, MembershipState::Knock] - .contains(&membership) - { - let key = (StateEventType::RoomJoinRules, StateKey::new()); - if !auth_types.contains(&key) { - auth_types.push(key); - } - - if let Some(Ok(u)) = content - .join_authorised_via_users_server - .map(|m| m.deserialize()) - { - let key = (StateEventType::RoomMember, u.as_str().into()); - if !auth_types.contains(&key) { - auth_types.push(key); - } - } - } - - let key = (StateEventType::RoomMember, state_key.into()); - if !auth_types.contains(&key) { - auth_types.push(key); - } - - if membership == MembershipState::Invite { - if let Some(Ok(t_id)) = content - .third_party_invite - .map(|t| t.deserialize()) - { - let key = - (StateEventType::RoomThirdPartyInvite, t_id.signed.token.into()); - if !auth_types.contains(&key) { - auth_types.push(key); - } - } - } - } - } - } - - Ok(auth_types) -} - -/// Authenticate the incoming `event`. -/// -/// The steps of authentication are: -/// -/// * 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(), + event_id = ?incoming_event.event_id(), ) )] -pub async fn auth_check( - room_version: &RoomVersion, - incoming_event: &E, - current_third_party_invite: Option<&E>, - fetch_state: F, -) -> Result +pub async fn auth_check( + rules: &RoomVersionRules, + incoming_event: &Pdu, + fetch_event: &FetchEvent, + fetch_state: &FetchState, +) -> Result where - F: Fn(&StateEventType, &str) -> Fut + Send, - Fut: Future> + Send, - E: Event + Send + Sync, - for<'a> &'a E: Event + Send, + FetchEvent: Fn(OwnedEventId) -> EventFut + Sync, + EventFut: Future> + Send, + FetchState: Fn(StateEventType, StateKey) -> StateFut + Sync, + StateFut: Future> + Send, + Pdu: Event, { - debug!( - event_id = ?incoming_event.event_id(), - event_type = ?incoming_event.event_type(), - "auth_check beginning" - ); + let dependent = check_state_dependent_auth_rules(rules, incoming_event, fetch_state); - // [synapse] check that all the events are in the same room as `incoming_event` + let independent = check_state_independent_auth_rules(rules, incoming_event, fetch_event); - // [synapse] do_sig_check check the event has valid signatures for member events + match try_join(independent, dependent).await { + | Err(e) if matches!(e, Error::Request(InvalidParam, ..)) => Err(e), + | Err(e) => Err!(Request(Forbidden("Auth check failed: {e}"))), + | Ok(_) => Ok(()), + } +} - // TODO do_size_check is false when called by `iterative_auth_check` - // do_size_check is also mostly accomplished by ruma with the exception of - // checking event_type, state_key, and json are below a certain size (255 and - // 65_536 respectively) - - let sender = incoming_event.sender(); - - // Implementation of https://spec.matrix.org/latest/rooms/v1/#authorization-rules - // - // 1. If type is m.room.create: +/// Check whether the incoming event passes the state-independent [authorization +/// rules] for the given room version rules. +/// +/// The state-independent rules are the first few authorization rules that check +/// an incoming `m.room.create` event (which cannot have `auth_events`), and the +/// list of `auth_events` of other events. +/// +/// This method only needs to be called once, when the event is received. +/// +/// # Errors +/// +/// If the check fails, this returns an `Err(_)` with a description of the check +/// that failed. +/// +/// [authorization rules]: https://spec.matrix.org/latest/server-server-api/#authorization-rules +#[tracing::instrument( + name = "independent", + level = "debug", + skip_all, + fields( + sender = ?incoming_event.sender(), + ) +)] +pub(super) async fn check_state_independent_auth_rules( + rules: &RoomVersionRules, + incoming_event: &Pdu, + fetch_event: &Fetch, +) -> Result +where + Fetch: Fn(OwnedEventId) -> Fut + Sync, + Fut: Future> + Send, + Pdu: Event, +{ + // Since v1, if type is m.room.create: if *incoming_event.event_type() == TimelineEventType::RoomCreate { - #[derive(Deserialize)] - struct RoomCreateContentFields { - room_version: Option>, - creator: Option>, - } + let room_create_event = RoomCreateEvent::new(incoming_event.clone()); + return check_room_create(&room_create_event, &rules.authorization); + } - debug!("start m.room.create check"); + let expected_auth_types: HashSet<_> = auth_types_for_event( + incoming_event.event_type(), + incoming_event.sender(), + incoming_event.state_key(), + incoming_event.content(), + &rules.authorization, + false, + )? + .into_iter() + .collect(); - // 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); - } + let room_id = incoming_event.room_id(); - // If the domain of the room_id does not match the domain of the sender, reject - let Some(room_id_server_name) = incoming_event.room_id().server_name() else { - warn!("room ID has no servername"); - return Ok(false); + // Since v1, considering auth_events: + let seen_auth_types = incoming_event + .auth_events() + .try_stream() + .and_then(|event_id: &EventId| { + fetch_event(event_id.to_owned()) + .map_err(|_| err!(Request(NotFound("failed to find auth event")))) + }) + .ready_try_fold_default(|mut seen_auth_types: HashSet, auth_event| { + let event_id = auth_event.event_id(); + + // The auth event must be in the same room as the incoming event. + if auth_event.room_id() != room_id { + return Err!("auth event {event_id} not in the same room"); + } + + let state_key = auth_event + .state_key() + .ok_or_else(|| err!("auth event {event_id} has no `state_key`"))?; + + let event_type = auth_event.event_type(); + let key = (event_type.to_cow_str().into(), state_key.into()); + + // Since v1, if there are duplicate entries for a given type and state_key pair, + // reject. + if seen_auth_types.contains(&key) { + return Err!( + "duplicate auth event {event_id} for ({event_type}, {state_key}) pair" + ); + } + + // Since v1, if there are entries whose type and state_key don’t match those + // specified by the auth events selection algorithm described in the server + // specification, reject. + if !expected_auth_types.contains(&key) { + return Err!( + "unexpected auth event {event_id} with ({event_type}, {state_key}) pair" + ); + } + + // Since v1, if there are entries which were themselves rejected under the + // checks performed on receipt of a PDU, reject. + if auth_event.rejected() { + return Err!("rejected auth event {event_id}"); + } + + seen_auth_types.insert(key); + Ok(seen_auth_types) + }) + .await?; + + // Since v1, if there is no m.room.create event among the entries, reject. + if !rules + .authorization + .room_create_event_id_as_room_id + && !seen_auth_types + .iter() + .any(|(event_type, _)| *event_type == StateEventType::RoomCreate) + { + return Err!("no `m.room.create` event in auth events"); + } + + // Since `org.matrix.hydra.11`, the room_id must be the reference hash of an + // accepted m.room.create event. + if rules + .authorization + .room_create_event_id_as_room_id + { + let room_create_event_id = room_id.as_event_id().map_err(|e| { + err!(Request(InvalidParam( + "could not construct `m.room.create` event ID from room ID: {e}" + ))) + })?; + + let Ok(room_create_event) = fetch_event(room_create_event_id.clone()).await else { + return Err!(Request(NotFound( + "failed to find `m.room.create` event {room_create_event_id}" + ))); }; - if room_id_server_name != sender.server_name() { - warn!("servername of room ID does not match servername 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!("invalid room version found in m.room.create event"); - return Ok(false); - } - - if !room_version.use_room_create_sender { - // If content has no creator field, reject - if content.creator.is_none() { - warn!("no creator field found in m.room.create content"); - return Ok(false); - } - } - - debug!("m.room.create event was allowed"); - return Ok(true); - } - - /* - // TODO: In the past this code caused problems federating with synapse, maybe this has been - // resolved already. Needs testing. - // - // 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); + if room_create_event.rejected() { + return Err!("rejected `m.room.create` event {room_create_event_id}"); } } - */ - let (room_create_event, power_levels_event, sender_member_event) = join3( - fetch_state(&StateEventType::RoomCreate, ""), - fetch_state(&StateEventType::RoomPowerLevels, ""), - fetch_state(&StateEventType::RoomMember, sender.as_str()), + Ok(()) +} + +/// Check whether the incoming event passes the state-dependent [authorization +/// rules] for the given room version rules. +/// +/// The state-dependent rules are all the remaining rules not checked by +/// [`check_state_independent_auth_rules()`]. +/// +/// This method should be called several times for an event, to perform the +/// [checks on receipt of a PDU]. +/// +/// 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. +/// +/// This assumes that `ruma_signatures::verify_event()` was called previously, +/// as some authorization rules depend on the signatures being valid on the +/// event. +/// +/// # Errors +/// +/// If the check fails, this returns an `Err(_)` with a description of the check +/// that failed. +/// +/// [authorization rules]: https://spec.matrix.org/latest/server-server-api/#authorization-rules +/// [checks on receipt of a PDU]: https://spec.matrix.org/latest/server-server-api/#checks-performed-on-receipt-of-a-pdu +#[tracing::instrument( + name = "dependent", + level = "debug", + skip_all, + fields( + sender = ?incoming_event.sender(), + ) +)] +pub(super) async fn check_state_dependent_auth_rules( + rules: &RoomVersionRules, + incoming_event: &Pdu, + fetch_state: &Fetch, +) -> Result +where + Fetch: Fn(StateEventType, StateKey) -> Fut + Sync, + Fut: Future> + Send, + Pdu: Event, +{ + // There are no state-dependent auth rules for create events. + if *incoming_event.event_type() == TimelineEventType::RoomCreate { + trace!("allowing `m.room.create` event"); + return Ok(()); + } + + let sender = incoming_event.sender(); + let (room_create_event, sender_membership, current_room_power_levels_event) = join3( + fetch_state.room_create_event(), + fetch_state.user_membership(sender), + fetch_state.room_power_levels_event(), ) .await; - let room_create_event = match room_create_event { - | None => { - warn!("no m.room.create event in auth chain"); - return Ok(false); - }, - | Some(e) => e, - }; - - // 3. If event does not have m.room.create in auth_events reject - if !incoming_event - .auth_events() - .any(|id| id == room_create_event.event_id()) - { - warn!("no m.room.create event in auth events"); - 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. - #[derive(Deserialize)] - #[allow(clippy::items_after_statements)] - struct RoomCreateContentFederate { - #[serde(rename = "m.federate", default = "ruma::serde::default_true")] - federate: bool, - } - let room_create_content: RoomCreateContentFederate = - from_json_str(room_create_event.content().get())?; - if !room_create_content.federate + // Since v1, 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. + let room_create_event = room_create_event?; + let federate = room_create_event.federate()?; + if !federate && room_create_event.sender().server_name() != incoming_event.sender().server_name() { - warn!( - "room is not federated and event's sender domain does not match create event's \ - sender domain" + return Err!( + "room is not federated and event's sender domain does not match `m.room.create` \ + event's sender domain" ); - return Ok(false); } - // Only in some room versions 6 and below - if room_version.special_case_aliases_auth { - // 4. If type is m.room.aliases - if *incoming_event.event_type() == TimelineEventType::RoomAliases { - 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); + // v1-v5, if type is m.room.aliases: + if rules.authorization.special_case_room_aliases + && *incoming_event.event_type() == TimelineEventType::RoomAliases + { + trace!("starting m.room.aliases check"); + // v1-v5, if event has no state_key, reject. + // + // v1-v5, if sender's domain doesn't match state_key, reject. + if incoming_event.state_key() != Some(sender.server_name().as_str()) { + return Err!( + "server name of the `state_key` of `m.room.aliases` event does not match the \ + server name of the sender" + ); } + + // Otherwise, allow. + trace!("`m.room.aliases` event was allowed"); + return Ok(()); } - // If type is m.room.member + // Since v1, if type is m.room.member: if *incoming_event.event_type() == TimelineEventType::RoomMember { - debug!("starting m.room.member check"); - let state_key = match incoming_event.state_key() { - | None => { - warn!("no statekey 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, - current_third_party_invite, - power_levels_event.as_ref(), - join_rules_event.as_ref(), - user_for_join_auth.as_deref(), - &user_for_join_auth_membership, + let room_member_event = RoomMemberEvent::new(incoming_event.clone()); + return check_room_member( + &room_member_event, + &rules.authorization, &room_create_event, - )? { - return Ok(false); - } - - debug!("m.room.member event was allowed"); - return Ok(true); + fetch_state, + ) + .await; } - // 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 not found in room"); - 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!( - event_id = ?incoming_event.event_id(), - content = ?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's membership is not join"); - return Ok(false); + // Since v1, if the sender's current membership state is not join, reject. + let sender_membership = sender_membership?; + if sender_membership != MembershipState::Join { + return Err!("sender's membership `{sender_membership}` is not `join`"); } - // If type is m.room.third_party_invite - let sender_power_level = match &power_levels_event { - | Some(pl) => { - let content = - deserialize_power_levels_content_fields(pl.content().get(), room_version)?; - match content.get_user_power(sender) { - | Some(level) => *level, - | _ => content.users_default, - } - }, - | _ => { - // If no power level event found the creator gets 100 everyone else gets 0 - let is_creator = if room_version.use_room_create_sender { - room_create_event.sender() == sender - } else { - #[allow(deprecated)] - from_json_str::(room_create_event.content().get()) - .is_ok_and(|create| create.creator.unwrap() == *sender) - }; + let creators = room_create_event.creators(&rules.authorization)?; + let sender_power_level = current_room_power_levels_event.user_power_level( + sender, + creators.clone(), + &rules.authorization, + )?; - if is_creator { int!(100) } else { int!(0) } - }, - }; - - // Allow if and only if sender's current power level is greater than - // or equal to the invite level + // Since v1, if type is m.room.third_party_invite: if *incoming_event.event_type() == TimelineEventType::RoomThirdPartyInvite { - let invite_level = match &power_levels_event { - | Some(power_levels) => - deserialize_power_levels_content_invite( - power_levels.content().get(), - room_version, - )? - .invite, - | None => int!(0), - }; + // Since v1, allow if and only if sender's current power level is greater than + // or equal to the invite level. + let invite_power_level = current_room_power_levels_event + .get_as_int_or_default(RoomPowerLevelsIntField::Invite, &rules.authorization)?; - if sender_power_level < invite_level { - warn!("sender's cannot send invites in this room"); - return Ok(false); + if sender_power_level < invite_power_level { + return Err!( + "sender does not have enough power ({sender_power_level:?}) to send invites \ + ({invite_power_level}) in this room" + ); } - debug!("m.room.third_party_invite event was allowed"); - return Ok(true); + trace!("`m.room.third_party_invite` event was allowed"); + return Ok(()); } - // 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!("user cannot send event"); - return Ok(false); + // Since v1, if the event type's required power level is greater than the + // sender's power level, reject. + let event_type_power_level = current_room_power_levels_event.event_power_level( + incoming_event.event_type(), + incoming_event.state_key(), + &rules.authorization, + )?; + + if sender_power_level < event_type_power_level { + return Err!( + "sender does not have enough power ({sender_power_level:?}) for `{}` event type \ + ({event_type_power_level})", + incoming_event.event_type() + ); + } + + // Since v1, if the event has a state_key that starts with an @ and does not + // match the sender, reject. + if incoming_event + .state_key() + .is_some_and(|k| k.starts_with('@')) + && incoming_event.state_key() != Some(incoming_event.sender().as_str()) + { + return Err!("sender cannot send event with `state_key` matching another user's ID"); } // If type is m.room.power_levels if *incoming_event.event_type() == TimelineEventType::RoomPowerLevels { - debug!("starting m.room.power_levels check"); - - match check_power_levels( - room_version, - incoming_event, - power_levels_event.as_ref(), + let room_power_levels_event = RoomPowerLevelsEvent::new(incoming_event.clone()); + return check_room_power_levels( + &room_power_levels_event, + current_room_power_levels_event.as_ref(), + &rules.authorization, sender_power_level, - ) { - | 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"); + creators, + ); } - // Room version 3: Redaction events are always accepted (provided the event is - // allowed by `events` and `events_default` in the power levels). However, - // servers should not apply or send redaction's to clients until both the - // redaction event and original event have been seen, and are valid. Servers - // should only apply redaction's to events where the sender's domains match, or - // the sender of the redaction has the appropriate permissions per the - // power levels. - - if room_version.extra_redaction_checks + // v1-v2, if type is m.room.redaction: + if rules.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)? { - return Ok(false); - } + return check_room_redaction( + incoming_event, + current_room_power_levels_event.as_ref(), + &rules.authorization, + sender_power_level, + ); } - debug!("allowing event passed all checks"); - Ok(true) + // Otherwise, allow. + trace!("allowing event passed all checks"); + Ok(()) } -// TODO deserializing the member, power, join_rules event contents is done in -// conduit just before this is called. Could they be passed in? -/// Does the user who sent this member event have required power levels to do -/// so. -/// -/// * `user` - Information about the membership event and user making the -/// request. -/// * `auth_events` - The set of auth events that relate to a membership event. -/// -/// This is generated by calling `auth_types_for_event` with the membership -/// event and the current State. -#[allow(clippy::too_many_arguments)] -#[allow(clippy::cognitive_complexity)] -fn valid_membership_change( - room_version: &RoomVersion, - target_user: &UserId, - target_user_membership_event: Option<&E>, - sender: &UserId, - sender_membership_event: Option<&E>, - current_event: &E, - current_third_party_invite: Option<&E>, - power_levels_event: Option<&E>, - join_rules_event: Option<&E>, - user_for_join_auth: Option<&UserId>, - user_for_join_auth_membership: &MembershipState, - create_room: &E, -) -> Result +/// Check whether the given event passes the `m.room.create` authorization +/// rules. +#[tracing::instrument(level = "trace", skip_all)] +fn check_room_create( + room_create_event: &RoomCreateEvent, + rules: &AuthorizationRules, +) -> Result where - E: Event + Send + Sync, - for<'a> &'a E: Event + Send, + Pdu: Event, { - #[derive(Deserialize)] - struct GetThirdPartyInvite { - third_party_invite: Option>, + // Since v1, if it has any previous events, reject. + if room_create_event.prev_events().next().is_some() { + return Err!("`m.room.create` event cannot have previous events"); } - let content = current_event.content(); - let target_membership = from_json_str::(content.get())?.membership; - let third_party_invite = - from_json_str::(content.get())?.third_party_invite; - - let sender_membership = match &sender_membership_event { - | Some(pdu) => from_json_str::(pdu.content().get())?.membership, - | None => MembershipState::Leave, - }; - let sender_is_joined = sender_membership == MembershipState::Join; - - let target_user_current_membership = match &target_user_membership_event { - | Some(pdu) => from_json_str::(pdu.content().get())?.membership, - | None => MembershipState::Leave, - }; - - let power_levels: RoomPowerLevelsEventContent = match &power_levels_event { - | Some(ev) => from_json_str(ev.content().get())?, - | None => RoomPowerLevelsEventContent::default(), - }; - - let sender_power = power_levels - .users - .get(sender) - .or_else(|| sender_is_joined.then_some(&power_levels.users_default)); - - let target_power = power_levels.users.get(target_user).or_else(|| { - (target_membership == MembershipState::Join).then_some(&power_levels.users_default) - }); - - let join_rules = if let Some(jr) = &join_rules_event { - from_json_str::(jr.content().get())?.join_rule - } else { - JoinRule::Invite - }; - - let power_levels_event_id = power_levels_event.as_ref().map(Event::event_id); - let sender_membership_event_id = sender_membership_event - .as_ref() - .map(Event::event_id); - let target_user_membership_event_id = target_user_membership_event - .as_ref() - .map(Event::event_id); - - let user_for_join_auth_is_valid = if let Some(user_for_join_auth) = user_for_join_auth { - // Is the authorised user allowed to invite users into this room - let (auth_user_pl, invite_level) = if let Some(pl) = &power_levels_event { - // TODO Refactor all powerlevel parsing - let invite = - deserialize_power_levels_content_invite(pl.content().get(), room_version)?.invite; - - let content = - deserialize_power_levels_content_fields(pl.content().get(), room_version)?; - let user_pl = match content.get_user_power(user_for_join_auth) { - | Some(level) => *level, - | _ => content.users_default, - }; - - (user_pl, invite) - } else { - (int!(0), int!(0)) + if rules.room_create_event_id_as_room_id { + let Ok(room_create_event_id) = room_create_event.room_id().as_event_id() else { + return Err!(Request(InvalidParam( + "Failed to create `event_id` out of `m.room.create` synthetic `room_id`" + ))); }; - (user_for_join_auth_membership == &MembershipState::Join) - && (auth_user_pl >= invite_level) + + if room_create_event_id != room_create_event.event_id() { + return Err!(Request(InvalidParam( + "`m.room.create` has mismatching synthetic `room_id` and `event_id`" + ))); + } } else { - // No auth user was given - false - }; + // v1-v11, if the domain of the room_id does not match the domain of the sender, + // reject. + let Some(room_id_server_name) = room_create_event.room_id().server_name() else { + return Err!("Invalid `ServerName` for `room_id` in `m.room.create` event"); + }; - Ok(match target_membership { - | MembershipState::Join => { - // 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(); + if room_id_server_name != room_create_event.sender().server_name() { + return Err!( + "Mismatched `ServerName` for `room_id` in `m.room.create` with `sender`" + ); + } + } - let prev_event_is_create_event = prev_events - .next() - .is_some_and(|event_id| event_id.borrow() == create_room.event_id().borrow()); - let no_more_prev_events = prev_events.next().is_none(); + // Since v1, if `content.room_version` is present and is not a recognized + // version, reject. + // + // This check is assumed to be done before calling auth_check because we have an + // AuthorizationRules, which means that we recognized the version. - if prev_event_is_create_event && no_more_prev_events { - let is_creator = if room_version.use_room_create_sender { - let creator = create_room.sender(); + // v1-v10, if content has no creator field, reject. + if !rules.use_room_create_sender && !room_create_event.has_creator()? { + return Err!("missing `creator` field in `m.room.create` event"); + } - creator == sender && creator == target_user - } else { - #[allow(deprecated)] - let creator = from_json_str::(create_room.content().get())? - .creator - .ok_or_else(|| serde_json::Error::missing_field("creator"))?; - - creator == sender && creator == target_user - }; - - if is_creator { - return Ok(true); - } - } - - if sender != target_user { - // If the sender does not match state_key, reject. - warn!("Can't make other user join"); - false - } else if target_user_current_membership == MembershipState::Ban { - // If the sender is banned, reject. - warn!(?target_user_membership_event_id, "Banned user can't join"); - false - } else if (join_rules == JoinRule::Invite - || room_version.allow_knocking && (join_rules == JoinRule::Knock || matches!(join_rules, JoinRule::KnockRestricted(_)))) - // If the join_rule is invite then allow if membership state is invite or join - && (target_user_current_membership == MembershipState::Join - || target_user_current_membership == MembershipState::Invite) - { - true - } else if room_version.restricted_join_rules - && matches!(join_rules, JoinRule::Restricted(_)) - || room_version.knock_restricted_join_rule - && matches!(join_rules, JoinRule::KnockRestricted(_)) - { - // If the join_rule is restricted or knock_restricted - if matches!( - target_user_current_membership, - MembershipState::Invite | MembershipState::Join - ) { - // If membership state is join or invite, allow. - true - } else { - // If the join_authorised_via_users_server key in content is not a user with - // sufficient permission to invite other users, reject. - // Otherwise, allow. - user_for_join_auth_is_valid - } - } else { - // If the join_rule is public, allow. - // Otherwise, reject. - join_rules == JoinRule::Public - } - }, - | MembershipState::Invite => { - // If content has third_party_invite key - match third_party_invite.and_then(|i| i.deserialize().ok()) { - | Some(tp_id) => - if target_user_current_membership == MembershipState::Ban { - warn!(?target_user_membership_event_id, "Can't invite banned user"); - false - } else { - let allow = verify_third_party_invite( - Some(target_user), - sender, - &tp_id, - current_third_party_invite, - ); - if !allow { - warn!("Third party invite invalid"); - } - allow - }, - | _ => { - if !sender_is_joined - || target_user_current_membership == MembershipState::Join - || target_user_current_membership == MembershipState::Ban - { - warn!( - ?target_user_membership_event_id, - ?sender_membership_event_id, - "Can't invite user if sender not joined or the user is currently \ - joined or banned", - ); - false - } else { - let allow = sender_power - .filter(|&p| p >= &power_levels.invite) - .is_some(); - if !allow { - warn!( - ?target_user_membership_event_id, - ?power_levels_event_id, - "User does not have enough power to invite", - ); - } - allow - } - }, - } - }, - | MembershipState::Leave => - if sender == target_user { - let allow = target_user_current_membership == MembershipState::Join - || target_user_current_membership == MembershipState::Invite - || target_user_current_membership == MembershipState::Knock; - if !allow { - warn!( - ?target_user_membership_event_id, - ?target_user_current_membership, - "Can't leave if sender is not already invited, knocked, or joined" - ); - } - allow - } else if !sender_is_joined - || target_user_current_membership == MembershipState::Ban - && sender_power - .filter(|&p| p < &power_levels.ban) - .is_some() - { - warn!( - ?target_user_membership_event_id, - ?sender_membership_event_id, - "Can't kick if sender not joined or user is already banned", - ); - false - } else { - let allow = sender_power - .filter(|&p| p >= &power_levels.kick) - .is_some() && target_power < sender_power; - if !allow { - warn!( - ?target_user_membership_event_id, - ?power_levels_event_id, - "User does not have enough power to kick", - ); - } - allow - }, - | MembershipState::Ban => - if !sender_is_joined { - warn!(?sender_membership_event_id, "Can't ban user if sender is not joined"); - false - } else { - let allow = sender_power - .filter(|&p| p >= &power_levels.ban) - .is_some() && target_power < sender_power; - if !allow { - warn!( - ?target_user_membership_event_id, - ?power_levels_event_id, - "User does not have enough power to ban", - ); - } - allow - }, - | MembershipState::Knock if room_version.allow_knocking => { - // 1. If the `join_rule` is anything other than `knock` or `knock_restricted`, - // reject. - if !matches!(join_rules, JoinRule::KnockRestricted(_) | JoinRule::Knock) { - warn!( - "Join rule is not set to knock or knock_restricted, knocking is not allowed" - ); - false - } else if matches!(join_rules, JoinRule::KnockRestricted(_)) - && !room_version.knock_restricted_join_rule - { - // 2. If the `join_rule` is `knock_restricted`, but the room does not support - // `knock_restricted`, reject. - warn!( - "Join rule is set to knock_restricted but room version does not support \ - knock_restricted, knocking is not allowed" - ); - false - } else if sender != target_user { - // 3. If `sender` does not match `state_key`, reject. - warn!( - ?sender, - ?target_user, - "Can't make another user knock, sender did not match target" - ); - 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, - "Knocking with a membership state of ban, invite or join is invalid", - ); - false - } else { - true - } - }, - | _ => { - warn!("Unknown membership transition"); - false - }, - }) + // Otherwise, allow. + trace!("`m.room.create` event was allowed"); + Ok(()) } -/// Is the user allowed to send a specific event based on the rooms power -/// levels. +/// Check whether the given event passes the `m.room.power_levels` authorization +/// rules. +#[tracing::instrument(level = "trace", skip_all)] +fn check_room_power_levels<'a, Creators, Pdu>( + room_power_levels_event: &RoomPowerLevelsEvent, + current_room_power_levels_event: Option<&RoomPowerLevelsEvent>, + rules: &AuthorizationRules, + sender_power_level: impl Into, + mut room_creators: Creators, +) -> Result +where + Creators: Iterator + Clone, + Pdu: Event, +{ + let sender_power_level = sender_power_level.into(); + + // Since v10, if any of the properties users_default, events_default, + // state_default, ban, redact, kick, or invite in content are present and not + // an integer, reject. + let new_int_fields = room_power_levels_event.int_fields_map(rules)?; + + // Since v10, if either of the properties events or notifications in content are + // present and not a dictionary with values that are integers, reject. + let new_events = room_power_levels_event.events(rules)?; + let new_notifications = room_power_levels_event.notifications(rules)?; + + // v1-v9, If the users property in content is not an object with keys that are + // valid user IDs with values that are integers (or a string that is an + // integer), reject. Since v10, if the users property in content is not an + // object with keys that are valid user IDs with values that are integers, + // reject. + let new_users = room_power_levels_event.users(rules)?; + + // Since `org.matrix.hydra.11`, if the `users` property in `content` contains + // the `sender` of + + // the `m.room.create` event or any of the user IDs in the create event's + // `content.additional_creators`, reject. + if rules.explicitly_privilege_room_creators + && new_users.as_ref().is_some_and(|new_users| { + room_creators.any(|creator| power_levels::contains_key(new_users, &creator)) + }) { + return Err!(Request(InvalidParam( + "creator user IDs are not allowed in the `users` field" + ))); + } + + trace!("validation of power event finished"); + + // Since v1, if there is no previous m.room.power_levels event in the room, + // allow. + let Some(current_room_power_levels_event) = current_room_power_levels_event else { + trace!("initial m.room.power_levels event allowed"); + return Ok(()); + }; + + // Since v1, for the properties users_default, events_default, state_default, + // ban, redact, kick, invite check if they were added, changed or removed. For + // each found alteration: + for field in RoomPowerLevelsIntField::ALL { + let current_power_level = current_room_power_levels_event.get_as_int(*field, rules)?; + let new_power_level = power_levels::get_value(&new_int_fields, field).copied(); + + if current_power_level == new_power_level { + continue; + } + + // Since v1, if the current value is higher than the sender’s current power + // level, reject. + let current_power_level_too_big = + current_power_level.unwrap_or_else(|| field.default_value()) > sender_power_level; + + // Since v1, if the new value is higher than the sender’s current power level, + // reject. + let new_power_level_too_big = + new_power_level.unwrap_or_else(|| field.default_value()) > sender_power_level; + + if current_power_level_too_big || new_power_level_too_big { + return Err!( + "sender does not have enough power to change the power level of `{field}`" + ); + } + } + + // Since v1, for each entry being added to, or changed in, the events property: + // - Since v1, if the new value is higher than the sender's current power level, + // reject. + let current_events = current_room_power_levels_event.events(rules)?; + check_power_level_maps( + current_events.as_deref(), + new_events.as_deref(), + sender_power_level, + |_, current_power_level| { + // Since v1, for each entry being changed in, or removed from, the events + // property: + // - Since v1, if the current value is higher than the sender's current power + // level, reject. + current_power_level > sender_power_level + }, + |ev_type| { + err!( + "sender does not have enough power to change the `{ev_type}` event type power \ + level" + ) + }, + )?; + + // Since v6, for each entry being added to, or changed in, the notifications + // property: + // - Since v6, if the new value is higher than the sender's current power level, + // reject. + if rules.limit_notifications_power_levels { + let current_notifications = current_room_power_levels_event.notifications(rules)?; + check_power_level_maps( + current_notifications.as_deref(), + new_notifications.as_deref(), + sender_power_level, + |_, current_power_level| { + // Since v6, for each entry being changed in, or removed from, the notifications + // property: + // - Since v6, if the current value is higher than the sender's current power + // level, reject. + current_power_level > sender_power_level + }, + |key| { + err!( + "sender does not have enough power to change the `{key}` notification power \ + level" + ) + }, + )?; + } + + // Since v1, for each entry being added to, or changed in, the users property: + // - Since v1, if the new value is greater than the sender’s current power + // level, reject. + let current_users = current_room_power_levels_event.users(rules)?; + check_power_level_maps( + current_users.as_deref(), + new_users.as_deref(), + sender_power_level, + |user_id, current_power_level| { + // Since v1, for each entry being changed in, or removed from, the users + // property, other than the sender’s own entry: + // - Since v1, if the current value is greater than or equal to the sender’s + // current power level, reject. + user_id != room_power_levels_event.sender() + && current_power_level >= sender_power_level + }, + |user_id| err!("sender does not have enough power to change `{user_id}`'s power level"), + )?; + + // Otherwise, allow. + trace!("m.room.power_levels event allowed"); + Ok(()) +} + +/// Check the power levels changes between the current and the new maps. /// -/// Does the event have the correct userId as its state_key if it's not the "" -/// state_key. -fn can_send_event(event: &impl Event, ple: Option<&impl Event>, user_level: Int) -> bool { - let event_type_power_level = get_send_level(event.event_type(), event.state_key(), ple); +/// # Arguments +/// +/// * `current`: the map with the current power levels. +/// * `new`: the map with the new power levels. +/// * `sender_power_level`: the power level of the sender of the new map. +/// * `reject_current_power_level_change_fn`: the function to check if a power +/// level change or removal must be rejected given its current value. +/// +/// The arguments to the method are the key of the power level and the current +/// value of the power level. It must return `true` if the change or removal +/// is rejected. +/// +/// Note that another check is done after this one to check if the change is +/// allowed given the new value of the power level. +/// * `error_fn`: the function to generate an error when the change for the +/// given key is not allowed. +fn check_power_level_maps<'a, K>( + current: Option<&'a [(K, Int)]>, + new: Option<&'a [(K, Int)]>, + sender_power_level: UserPowerLevel, + reject_current_power_level_change_fn: impl FnOnce(&K, Int) -> bool + Copy, + error_fn: impl FnOnce(&K) -> Error, +) -> Result +where + K: Ord, +{ + let keys_to_check = current + .iter() + .flat_map(|m| m.iter().map(|(k, _)| k)) + .chain(new.iter().flat_map(|m| m.iter().map(|(k, _)| k))); - debug!( - required_level = i64::from(event_type_power_level), - user_level = i64::from(user_level), - state_key = ?event.state_key(), - "permissions factors", - ); + for key in keys_to_check { + let current_power_level = current.and_then(|m| power_levels::get_value(m, key)); + let new_power_level = new.and_then(|m| power_levels::get_value(m, key)); - if user_level < event_type_power_level { - return false; - } - - if event - .state_key() - .is_some_and(|k| k.starts_with('@')) - && event.state_key() != Some(event.sender().as_str()) - { - return false; // permission required to post in this room - } - - true -} - -/// Confirm that the event sender has the required power levels. -fn check_power_levels( - room_version: &RoomVersion, - power_event: &impl Event, - previous_power_event: Option<&impl Event>, - user_level: Int, -) -> 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 old_level.is_some() && new_level.is_some() && old_level == new_level { + if current_power_level == new_power_level { continue; } - // If the current value is equal to the sender's current power level, reject - if user != power_event.sender() && old_level == Some(&user_level) { - warn!("m.room.power_level cannot remove ops == to own"); - return Some(false); // cannot remove ops level == to own - } + // For each entry being changed in, or removed from, the property. + let current_power_level_change_rejected = current_power_level + .is_some_and(|power_level| reject_current_power_level_change_fn(key, *power_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 > Some(&user_level); - let new_level_too_big = new_level > Some(&user_level); - if old_level_too_big || new_level_too_big { - warn!("m.room.power_level failed to add ops > than own"); - return Some(false); // cannot add ops greater than own + // For each entry being added to, or changed in, the property: + // - If the new value is higher than the sender's current power level, reject. + let new_power_level_too_big = + new_power_level.is_some_and(|&new_power_level| new_power_level > sender_power_level); + + if current_power_level_change_rejected || new_power_level_too_big { + return Err(error_fn(key)); } } - // EventType loop - for ev_type in event_levels_to_check { - let old_level = old_state.events.get(ev_type); - let new_level = new_state.events.get(ev_type); - if old_level.is_some() && new_level.is_some() && old_level == new_level { - continue; - } - - // If the current value is higher than the sender's current power level, reject - // If the new value is higher than the sender's current power level, reject - let old_level_too_big = old_level > Some(&user_level); - let new_level_too_big = new_level > Some(&user_level); - if old_level_too_big || new_level_too_big { - warn!("m.room.power_level failed to add ops > than own"); - return Some(false); // cannot add ops greater than own - } - } - - // Notifications, currently there is only @room - if room_version.limit_notifications_power_levels { - let old_level = old_state.notifications.room; - let new_level = new_state.notifications.room; - if old_level != new_level { - // If the current value is higher than the sender's current power level, reject - // If the new value is higher than the sender's current power level, reject - let old_level_too_big = old_level > user_level; - let new_level_too_big = new_level > user_level; - if old_level_too_big || new_level_too_big { - warn!("m.room.power_level failed to add ops > than 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!("cannot add ops > than own"); - return Some(false); - } - } - } - - Some(true) + Ok(()) } -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()?, - )) -} +/// Check whether the given event passes the `m.room.redaction` authorization +/// rules. +fn check_room_redaction( + room_redaction_event: &Pdu, + current_room_power_levels_event: Option<&RoomPowerLevelsEvent>, + rules: &AuthorizationRules, + sender_level: UserPowerLevel, +) -> Result +where + Pdu: Event, +{ + let redact_level = current_room_power_levels_event + .cloned() + .get_as_int_or_default(RoomPowerLevelsIntField::Redact, rules)?; -/// Does the event redacting come from a user with enough power to redact the -/// given event. -fn check_redaction( - _room_version: &RoomVersion, - redaction_event: &impl Event, - user_level: Int, - redact_level: Int, -) -> Result { - if user_level >= redact_level { - debug!("redaction allowed via power levels"); - return Ok(true); + // v1-v2, if the sender’s power level is greater than or equal to the redact + // level, allow. + if sender_level >= redact_level { + trace!("`m.room.redaction` event allowed via power levels"); + return Ok(()); } - // 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 + // v1-v2, 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 room_redaction_event.event_id().server_name() + == room_redaction_event .redacts() .as_ref() .and_then(|&id| id.server_name()) { - debug!("redaction event allowed via room version 1 rules"); - return Ok(true); + trace!("`m.room.redaction` event allowed via room version 1 rules"); + return Ok(()); } - Ok(false) -} - -/// Helper function to fetch the power level needed to send an event of type -/// `e_type` based on the rooms "m.room.power_level" event. -fn get_send_level( - e_type: &TimelineEventType, - state_key: Option<&str>, - power_lvl: Option<&impl Event>, -) -> Int { - power_lvl - .and_then(|ple| { - from_json_str::(ple.content().get()) - .map(|content| { - content - .events - .get(e_type) - .copied() - .unwrap_or_else(|| { - if state_key.is_some() { - content.state_default - } else { - content.events_default - } - }) - }) - .ok() - }) - .unwrap_or_else(|| if state_key.is_some() { int!(50) } else { int!(0) }) -} - -fn verify_third_party_invite( - target_user: Option<&UserId>, - sender: &UserId, - tp_id: &ThirdPartyInvite, - current_third_party_invite: Option<&impl Event>, -) -> bool { - // 1. Check for user being banned happens before this is called - // checking for mxid and token keys is done by ruma when deserializing - - // The state key must match the invitee - if target_user != Some(&tp_id.signed.mxid) { - return false; - } - - // If there is no m.room.third_party_invite event in the current room state with - // state_key matching token, reject - #[allow(clippy::manual_let_else)] - let current_tpid = match current_third_party_invite { - | Some(id) => id, - | None => return false, - }; - - if current_tpid.state_key() != Some(&tp_id.signed.token) { - return false; - } - - if sender != current_tpid.sender() { - return false; - } - - // If any signature in signed matches any public key in the - // m.room.third_party_invite event, allow - #[allow(clippy::manual_let_else)] - let tpid_ev = - match from_json_str::(current_tpid.content().get()) { - | Ok(ev) => ev, - | Err(_) => return false, - }; - - #[allow(clippy::manual_let_else)] - let decoded_invite_token = match Base64::parse(&tp_id.signed.token) { - | Ok(tok) => tok, - // FIXME: Log a warning? - | Err(_) => return false, - }; - - // A list of public keys in the public_keys field - for key in tpid_ev.public_keys.unwrap_or_default() { - if key.public_key == decoded_invite_token { - return true; - } - } - - // A single public key in the public_key field - tpid_ev.public_key == decoded_invite_token -} - -#[cfg(test)] -mod tests { - use ruma::events::{ - StateEventType, TimelineEventType, - room::{ - join_rules::{ - AllowRule, JoinRule, Restricted, RoomJoinRulesEventContent, RoomMembership, - }, - member::{MembershipState, RoomMemberEventContent}, - }, - }; - use serde_json::value::to_raw_value as to_raw_json_value; - - use crate::{ - matrix::{Event, EventTypeExt, Pdu as PduEvent}, - state_res::{ - RoomVersion, 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, - }, - }, - }; - - #[test] - 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 target_user = charlie(); - let sender = alice(); - - assert!( - valid_membership_change( - &RoomVersion::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, - None::<&PduEvent>, - fetch_state(StateEventType::RoomPowerLevels, "".into()).as_ref(), - fetch_state(StateEventType::RoomJoinRules, "".into()).as_ref(), - None, - &MembershipState::Leave, - &fetch_state(StateEventType::RoomCreate, "".into()).unwrap(), - ) - .unwrap() - ); - } - - #[test] - 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 target_user = charlie(); - let sender = charlie(); - - assert!( - !valid_membership_change( - &RoomVersion::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, - None::<&PduEvent>, - fetch_state(StateEventType::RoomPowerLevels, "".into()).as_ref(), - fetch_state(StateEventType::RoomJoinRules, "".into()).as_ref(), - None, - &MembershipState::Leave, - &fetch_state(StateEventType::RoomCreate, "".into()).unwrap(), - ) - .unwrap() - ); - } - - #[test] - 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 target_user = alice(); - let sender = alice(); - - assert!( - valid_membership_change( - &RoomVersion::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, - None::<&PduEvent>, - fetch_state(StateEventType::RoomPowerLevels, "".into()).as_ref(), - fetch_state(StateEventType::RoomJoinRules, "".into()).as_ref(), - None, - &MembershipState::Leave, - &fetch_state(StateEventType::RoomCreate, "".into()).unwrap(), - ) - .unwrap() - ); - } - - #[test] - 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 target_user = alice(); - let sender = charlie(); - - assert!( - !valid_membership_change( - &RoomVersion::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, - None::<&PduEvent>, - fetch_state(StateEventType::RoomPowerLevels, "".into()).as_ref(), - fetch_state(StateEventType::RoomJoinRules, "".into()).as_ref(), - None, - &MembershipState::Leave, - &fetch_state(StateEventType::RoomCreate, "".into()).unwrap(), - ) - .unwrap() - ); - } - - #[test] - 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 target_user = ella(); - let sender = ella(); - - assert!( - valid_membership_change( - &RoomVersion::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, - None::<&PduEvent>, - fetch_state(StateEventType::RoomPowerLevels, "".into()).as_ref(), - fetch_state(StateEventType::RoomJoinRules, "".into()).as_ref(), - Some(alice()), - &MembershipState::Join, - &fetch_state(StateEventType::RoomCreate, "".into()).unwrap(), - ) - .unwrap() - ); - - assert!( - !valid_membership_change( - &RoomVersion::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, - None::<&PduEvent>, - fetch_state(StateEventType::RoomPowerLevels, "".into()).as_ref(), - fetch_state(StateEventType::RoomJoinRules, "".into()).as_ref(), - Some(ella()), - &MembershipState::Leave, - &fetch_state(StateEventType::RoomCreate, "".into()).unwrap(), - ) - .unwrap() - ); - } - - #[test] - 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 target_user = ella(); - let sender = ella(); - - assert!( - valid_membership_change( - &RoomVersion::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, - None::<&PduEvent>, - fetch_state(StateEventType::RoomPowerLevels, "".into()).as_ref(), - fetch_state(StateEventType::RoomJoinRules, "".into()).as_ref(), - None, - &MembershipState::Leave, - &fetch_state(StateEventType::RoomCreate, "".into()).unwrap(), - ) - .unwrap() - ); - } + // Otherwise, reject. + Err!("`m.room.redaction` event did not pass any of the allow rules") } diff --git a/src/core/matrix/state_res/event_auth/auth_types.rs b/src/core/matrix/state_res/event_auth/auth_types.rs new file mode 100644 index 00000000..ce6a54ee --- /dev/null +++ b/src/core/matrix/state_res/event_auth/auth_types.rs @@ -0,0 +1,119 @@ +use ruma::{ + UserId, + events::{StateEventType, TimelineEventType, room::member::MembershipState}, + room_version_rules::AuthorizationRules, +}; +use serde_json::value::RawValue as RawJsonValue; + +use super::super::{TypeStateKey, events::member::RoomMemberEventContent}; +use crate::{Err, Result, arrayvec::ArrayVec, matrix::pdu::MAX_AUTH_EVENTS}; + +pub type AuthTypes = ArrayVec; + +/// Get the list of [relevant auth events] required to authorize the event of +/// the given type. +/// +/// Returns a list of `(event_type, state_key)` tuples. +/// +/// # Errors +/// +/// Returns an `Err(_)` if a field could not be deserialized because `content` +/// does not respect the expected format for the `event_type`. +/// +/// [relevant auth events]: https://spec.matrix.org/latest/server-server-api/#auth-events-selection +pub fn auth_types_for_event( + event_type: &TimelineEventType, + sender: &UserId, + state_key: Option<&str>, + content: &RawJsonValue, + rules: &AuthorizationRules, + always_create: bool, +) -> Result { + let mut auth_types = AuthTypes::new(); + + // The `auth_events` for the `m.room.create` event in a room is empty. + // For other events, it should be the following subset of the room state: + // + // - The `m.room.create` event. + // - The current `m.room.power_levels` event, if any. + // - The sender’s current `m.room.member` event, if any. + if *event_type != TimelineEventType::RoomCreate { + // v1-v11, the `m.room.create` event. + if !rules.room_create_event_id_as_room_id || always_create { + auth_types.push((StateEventType::RoomCreate, "".into())); + } + + auth_types.push((StateEventType::RoomPowerLevels, "".into())); + auth_types.push((StateEventType::RoomMember, sender.as_str().into())); + } + + // If type is `m.room.member`: + if *event_type == TimelineEventType::RoomMember { + auth_types_for_member_event(&mut auth_types, state_key, content, rules)?; + } + + Ok(auth_types) +} + +fn auth_types_for_member_event( + auth_types: &mut AuthTypes, + state_key: Option<&str>, + content: &RawJsonValue, + rules: &AuthorizationRules, +) -> Result { + // The target’s current `m.room.member` event, if any. + let Some(state_key) = state_key else { + return Err!("missing `state_key` field for `m.room.member` event"); + }; + + let key = (StateEventType::RoomMember, state_key.into()); + if !auth_types.contains(&key) { + auth_types.push(key); + } + + let content = RoomMemberEventContent::new(content); + let membership = content.membership()?; + + // If `membership` is `join`, `invite` or `knock`, the current + // `m.room.join_rules` event, if any. + if matches!( + membership, + MembershipState::Join | MembershipState::Invite | MembershipState::Knock + ) { + let key = (StateEventType::RoomJoinRules, "".into()); + if !auth_types.contains(&key) { + auth_types.push(key); + } + } + + // If `membership` is `invite` and `content` contains a `third_party_invite` + // property, the current `m.room.third_party_invite` event with `state_key` + // matching `content.third_party_invite.signed.token`, if any. + if membership == MembershipState::Invite { + let third_party_invite = content.third_party_invite()?; + if let Some(third_party_invite) = third_party_invite { + let token = third_party_invite.token()?.into(); + let key = (StateEventType::RoomThirdPartyInvite, token); + if !auth_types.contains(&key) { + auth_types.push(key); + } + } + } + + // If `content.join_authorised_via_users_server` is present, and the room + // version supports restricted rooms, then the `m.room.member` event with + // `state_key` matching `content.join_authorised_via_users_server`. + // + // Note: And the membership is join (https://github.com/matrix-org/matrix-spec/pull/2100) + if membership == MembershipState::Join && rules.restricted_join_rule { + let join_authorised_via_users_server = content.join_authorised_via_users_server()?; + if let Some(user_id) = join_authorised_via_users_server { + let key = (StateEventType::RoomMember, user_id.as_str().into()); + if !auth_types.contains(&key) { + auth_types.push(key); + } + } + } + + Ok(()) +} diff --git a/src/core/matrix/state_res/event_auth/room_member.rs b/src/core/matrix/state_res/event_auth/room_member.rs new file mode 100644 index 00000000..facf74a3 --- /dev/null +++ b/src/core/matrix/state_res/event_auth/room_member.rs @@ -0,0 +1,587 @@ +use std::borrow::Borrow; + +use futures::future::{join, join3}; +use ruma::{ + AnyKeyName, SigningKeyId, UserId, + events::{StateEventType, room::member::MembershipState}, + room_version_rules::AuthorizationRules, + serde::{Base64, base64::Standard}, + signatures::verify_canonical_json_bytes, +}; + +use crate::{ + Err, Result, debug, err, is_equal_to, + matrix::{Event, StateKey}, +}; + +#[cfg(test)] +mod tests; + +#[cfg(test)] +use super::test_utils; +use super::{ + FetchStateExt, + events::{ + JoinRule, RoomCreateEvent, RoomMemberEvent, RoomPowerLevelsIntField, + member::ThirdPartyInvite, power_levels::RoomPowerLevelsEventOptionExt, + }, +}; + +/// Check whether the given event passes the `m.room.roomber` authorization +/// rules. +/// +/// This assumes that `ruma_signatures::verify_event()` was called previously, +/// as some authorization rules depend on the signatures being valid on the +/// event. +#[tracing::instrument(level = "trace", skip_all)] +pub(super) async fn check_room_member( + room_member_event: &RoomMemberEvent, + rules: &AuthorizationRules, + room_create_event: &RoomCreateEvent, + fetch_state: &Fetch, +) -> Result +where + Fetch: Fn(StateEventType, StateKey) -> Fut + Sync, + Fut: Future> + Send, + Pdu: Event, +{ + debug!("starting m.room.member check"); + + // Since v1, if there is no state_key property, or no membership property in + // content, reject. + let Some(state_key) = room_member_event.state_key() else { + return Err!("missing `state_key` field in `m.room.member` event"); + }; + + let target_user = <&UserId>::try_from(state_key) + .map_err(|e| err!("invalid `state_key` field in `m.room.member` event: {e}"))?; + + let target_membership = room_member_event.membership()?; + + // These checks are done `in ruma_signatures::verify_event()`: + // + // Since v8, if content has a join_authorised_via_users_server property: + // + // - Since v8, if the event is not validly signed by the homeserver of the user + // ID denoted by the key, reject. + + match target_membership { + // Since v1, if membership is join: + | MembershipState::Join => + check_room_member_join( + room_member_event, + target_user, + rules, + room_create_event, + fetch_state, + ) + .await, + + // Since v1, if membership is invite: + | MembershipState::Invite => + check_room_member_invite( + room_member_event, + target_user, + rules, + room_create_event, + fetch_state, + ) + .await, + + // Since v1, if membership is leave: + | MembershipState::Leave => + check_room_member_leave( + room_member_event, + target_user, + rules, + room_create_event, + fetch_state, + ) + .await, + + // Since v1, if membership is ban: + | MembershipState::Ban => + check_room_member_ban( + room_member_event, + target_user, + rules, + room_create_event, + fetch_state, + ) + .await, + + // Since v7, if membership is knock: + | MembershipState::Knock if rules.knocking => + check_room_member_knock(room_member_event, target_user, rules, fetch_state).await, + + // Since v1, otherwise, the membership is unknown. Reject. + | _ => Err!("unknown membership"), + } +} + +/// Check whether the given event passes the `m.room.member` authorization rules +/// with a membership of `join`. +#[tracing::instrument(level = "trace", skip_all)] +async fn check_room_member_join( + room_member_event: &RoomMemberEvent, + target_user: &UserId, + rules: &AuthorizationRules, + room_create_event: &RoomCreateEvent, + fetch_state: &Fetch, +) -> Result +where + Fetch: Fn(StateEventType, StateKey) -> Fut + Sync, + Fut: Future> + Send, + Pdu: Event, +{ + let mut creators = room_create_event.creators(rules)?; + + let mut prev_events = room_member_event.prev_events(); + + let prev_event_is_room_create_event = prev_events + .next() + .is_some_and(|event_id| event_id.borrow() == room_create_event.event_id().borrow()); + + let prev_event_is_only_room_create_event = + prev_event_is_room_create_event && prev_events.next().is_none(); + + // v1-v10, if the only previous event is an m.room.create and the state_key is + // the creator, allow. + // Since v11, if the only previous event is an m.room.create and the state_key + // is the sender of the m.room.create, allow. + if prev_event_is_only_room_create_event && creators.any(is_equal_to!(*target_user)) { + return Ok(()); + } + + // Since v1, if the sender does not match state_key, reject. + if room_member_event.sender() != target_user { + return Err!("sender of join event must match target user"); + } + + let (current_membership, join_rule) = + join(fetch_state.user_membership(target_user), fetch_state.join_rule()).await; + + // Since v1, if the sender is banned, reject. + let current_membership = current_membership?; + if current_membership == MembershipState::Ban { + return Err!("banned user cannot join room"); + } + + // v1-v6, if the join_rule is invite then allow if membership state is invite or + // join. + // Since v7, if the join_rule is invite or knock then allow if membership state + // is invite or join. + let join_rule = join_rule?; + if (join_rule == JoinRule::Invite || rules.knocking && join_rule == JoinRule::Knock) + && matches!(current_membership, MembershipState::Invite | MembershipState::Join) + { + return Ok(()); + } + + // v8-v9, if the join_rule is restricted: + // Since v10, if the join_rule is restricted or knock_restricted: + if rules.restricted_join_rule && matches!(join_rule, JoinRule::Restricted) + || rules.knock_restricted_join_rule && matches!(join_rule, JoinRule::KnockRestricted) + { + // Since v8, if membership state is join or invite, allow. + if matches!(current_membership, MembershipState::Join | MembershipState::Invite) { + return Ok(()); + } + + // Since v8, if the join_authorised_via_users_server key in content is not a + // user with sufficient permission to invite other users, reject. + // + // Otherwise, allow. + let Some(authorized_via_user) = room_member_event.join_authorised_via_users_server()? + else { + // The field is absent, we cannot authorize. + return Err!( + "cannot join restricted room without `join_authorised_via_users_server` field \ + if not invited" + ); + }; + + // The member needs to be in the room to have any kind of permission. + let authorized_via_user_membership = fetch_state + .user_membership(&authorized_via_user) + .await?; + + if authorized_via_user_membership != MembershipState::Join { + return Err!("`join_authorised_via_users_server` is not joined"); + } + + let room_power_levels_event = fetch_state.room_power_levels_event().await; + + let authorized_via_user_power_level = + room_power_levels_event.user_power_level(&authorized_via_user, creators, rules)?; + + let invite_power_level = room_power_levels_event + .get_as_int_or_default(RoomPowerLevelsIntField::Invite, rules)?; + + if authorized_via_user_power_level < invite_power_level { + return Err!("`join_authorised_via_users_server` does not have enough power"); + } + + return Ok(()); + } + + // Since v1, if the join_rule is public, allow. Otherwise, reject. + if join_rule != JoinRule::Public { + return Err!("cannot join a room that is not `public`"); + } + + Ok(()) +} + +/// Check whether the given event passes the `m.room.member` authorization rules +/// with a membership of `invite`. +#[tracing::instrument(level = "trace", skip_all)] +async fn check_room_member_invite( + room_member_event: &RoomMemberEvent, + target_user: &UserId, + rules: &AuthorizationRules, + room_create_event: &RoomCreateEvent, + fetch_state: &Fetch, +) -> Result +where + Fetch: Fn(StateEventType, StateKey) -> Fut + Sync, + Fut: Future> + Send, + Pdu: Event, +{ + let third_party_invite = room_member_event.third_party_invite()?; + + // Since v1, if content has a third_party_invite property: + if let Some(third_party_invite) = third_party_invite { + return check_third_party_invite( + room_member_event, + &third_party_invite, + target_user, + fetch_state, + ) + .await; + } + + let sender_user = room_member_event.sender(); + let (sender_membership, current_target_user_membership, room_power_levels_event) = join3( + fetch_state.user_membership(sender_user), + fetch_state.user_membership(target_user), + fetch_state.room_power_levels_event(), + ) + .await; + + // Since v1, if the sender’s current membership state is not join, reject. + let sender_membership = sender_membership?; + if sender_membership != MembershipState::Join { + return Err!("cannot invite user if sender is not joined"); + } + + // Since v1, if target user’s current membership state is join or ban, reject. + let current_target_user_membership = current_target_user_membership?; + if matches!(current_target_user_membership, MembershipState::Join | MembershipState::Ban) { + return Err!("cannot invite user that is joined or banned"); + } + + let creators = room_create_event.creators(rules)?; + let sender_power_level = + room_power_levels_event.user_power_level(room_member_event.sender(), creators, rules)?; + + let invite_power_level = + room_power_levels_event.get_as_int_or_default(RoomPowerLevelsIntField::Invite, rules)?; + + // Since v1, if the sender’s power level is greater than or equal to the invite + // level, allow. Otherwise, reject. + if sender_power_level < invite_power_level { + return Err!("sender does not have enough power to invite"); + } + + Ok(()) +} + +/// Check whether the `third_party_invite` from the `m.room.member` event passes +/// the authorization rules. +#[tracing::instrument(level = "trace", skip_all)] +async fn check_third_party_invite( + room_member_event: &RoomMemberEvent, + third_party_invite: &ThirdPartyInvite, + target_user: &UserId, + fetch_state: &Fetch, +) -> Result +where + Fetch: Fn(StateEventType, StateKey) -> Fut + Sync, + Fut: Future> + Send, + Pdu: Event, +{ + let current_target_user_membership = fetch_state.user_membership(target_user).await?; + + // Since v1, if target user is banned, reject. + if current_target_user_membership == MembershipState::Ban { + return Err!("cannot invite user that is banned"); + } + + // Since v1, if content.third_party_invite does not have a signed property, + // reject. Since v1, if signed does not have mxid and token properties, reject. + let third_party_invite_token = third_party_invite.token()?; + let third_party_invite_mxid = third_party_invite.mxid()?; + + // Since v1, if mxid does not match state_key, reject. + if target_user != third_party_invite_mxid { + return Err!("third-party invite mxid does not match target user"); + } + + // Since v1, if there is no m.room.third_party_invite event in the current room + // state with state_key matching token, reject. + let Some(room_third_party_invite_event) = fetch_state + .room_third_party_invite_event(third_party_invite_token) + .await + else { + return Err!("no `m.room.third_party_invite` in room state matches the token"); + }; + + // Since v1, if sender does not match sender of the m.room.third_party_invite, + // reject. + if room_member_event.sender() != room_third_party_invite_event.sender() { + return Err!( + "sender of `m.room.third_party_invite` does not match sender of `m.room.member`" + ); + } + + let signatures = third_party_invite.signatures()?; + let public_keys = room_third_party_invite_event.public_keys()?; + let signed_canonical_json = third_party_invite.signed_canonical_json()?; + + // Since v1, if any signature in signed matches any public key in the + // m.room.third_party_invite event, allow. + for entity_signatures_value in signatures.values() { + let Some(entity_signatures) = entity_signatures_value.as_object() else { + return Err!(Request(InvalidParam( + "unexpected format of `signatures` field in `third_party_invite.signed` of \ + `m.room.member` event: expected a map of string to object, got \ + {entity_signatures_value:?}" + ))); + }; + + // We will ignore any error from now on, we just want to find a signature that + // can be verified from a public key. + + for (key_id, signature_value) in entity_signatures { + let Ok(parsed_key_id) = <&SigningKeyId>::try_from(key_id.as_str()) else { + continue; + }; + + let Some(signature_str) = signature_value.as_str() else { + continue; + }; + + let Ok(signature) = Base64::::parse(signature_str) else { + continue; + }; + + let algorithm = parsed_key_id.algorithm(); + for encoded_public_key in &public_keys { + let Ok(public_key) = encoded_public_key.decode() else { + continue; + }; + + if verify_canonical_json_bytes( + &algorithm, + &public_key, + signature.as_bytes(), + signed_canonical_json.as_bytes(), + ) + .is_ok() + { + return Ok(()); + } + } + } + } + + // Otherwise, reject. + Err!( + "no signature on third-party invite matches a public key in `m.room.third_party_invite` \ + event" + ) +} + +/// Check whether the given event passes the `m.room.member` authorization rules +/// with a membership of `leave`. +#[tracing::instrument(level = "trace", skip_all)] +async fn check_room_member_leave( + room_member_event: &RoomMemberEvent, + target_user: &UserId, + rules: &AuthorizationRules, + room_create_event: &RoomCreateEvent, + fetch_state: &Fetch, +) -> Result +where + Fetch: Fn(StateEventType, StateKey) -> Fut + Sync, + Fut: Future> + Send, + Pdu: Event, +{ + let (sender_membership, room_power_levels_event, current_target_user_membership) = join3( + fetch_state.user_membership(room_member_event.sender()), + fetch_state.room_power_levels_event(), + fetch_state.user_membership(target_user), + ) + .await; + + let sender_membership = sender_membership?; + + // v1-v6, if the sender matches state_key, allow if and only if that user’s + // current membership state is invite or join. + // Since v7, if the sender matches state_key, allow if and only if that user’s + // current membership state is invite, join, or knock. + if room_member_event.sender() == target_user { + let membership_is_invite_or_join = + matches!(sender_membership, MembershipState::Join | MembershipState::Invite); + let membership_is_knock = rules.knocking && sender_membership == MembershipState::Knock; + + return if membership_is_invite_or_join || membership_is_knock { + Ok(()) + } else { + Err!("cannot leave if not joined, invited or knocked") + }; + } + + // Since v1, if the sender’s current membership state is not join, reject. + if sender_membership != MembershipState::Join { + return Err!("cannot kick if sender is not joined"); + } + + let creators = room_create_event.creators(rules)?; + let current_target_user_membership = current_target_user_membership?; + + let sender_power_level = room_power_levels_event.user_power_level( + room_member_event.sender(), + creators.clone(), + rules, + )?; + + let ban_power_level = + room_power_levels_event.get_as_int_or_default(RoomPowerLevelsIntField::Ban, rules)?; + + // Since v1, if the target user’s current membership state is ban, and the + // sender’s power level is less than the ban level, reject. + if current_target_user_membership == MembershipState::Ban + && sender_power_level < ban_power_level + { + return Err!("sender does not have enough power to unban"); + } + + let kick_power_level = + room_power_levels_event.get_as_int_or_default(RoomPowerLevelsIntField::Kick, rules)?; + + let target_user_power_level = + room_power_levels_event.user_power_level(target_user, creators, rules)?; + + // Since v1, 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. + // + // Otherwise, reject. + if sender_power_level >= kick_power_level && target_user_power_level < sender_power_level { + Ok(()) + } else { + Err!("sender does not have enough power to kick target user") + } +} + +/// Check whether the given event passes the `m.room.member` authorization rules +/// with a membership of `ban`. +#[tracing::instrument(level = "trace", skip_all)] +async fn check_room_member_ban( + room_member_event: &RoomMemberEvent, + target_user: &UserId, + rules: &AuthorizationRules, + room_create_event: &RoomCreateEvent, + fetch_state: &Fetch, +) -> Result +where + Fetch: Fn(StateEventType, StateKey) -> Fut + Sync, + Fut: Future> + Send, + Pdu: Event, +{ + let (sender_membership, room_power_levels_event) = join( + fetch_state.user_membership(room_member_event.sender()), + fetch_state.room_power_levels_event(), + ) + .await; + + // Since v1, if the sender’s current membership state is not join, reject. + let sender_membership = sender_membership?; + if sender_membership != MembershipState::Join { + return Err!("cannot ban if sender is not joined"); + } + + let creators = room_create_event.creators(rules)?; + + let sender_power_level = room_power_levels_event.user_power_level( + room_member_event.sender(), + creators.clone(), + rules, + )?; + + let ban_power_level = + room_power_levels_event.get_as_int_or_default(RoomPowerLevelsIntField::Ban, rules)?; + + let target_user_power_level = + room_power_levels_event.user_power_level(target_user, creators, rules)?; + + // 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. + if sender_power_level >= ban_power_level && target_user_power_level < sender_power_level { + Ok(()) + } else { + Err!("sender does not have enough power to ban target user") + } +} + +/// Check whether the given event passes the `m.room.member` authorization rules +/// with a membership of `knock`. +#[tracing::instrument(level = "trace", skip_all)] +async fn check_room_member_knock( + room_member_event: &RoomMemberEvent, + target_user: &UserId, + rules: &AuthorizationRules, + fetch_state: &Fetch, +) -> Result +where + Fetch: Fn(StateEventType, StateKey) -> Fut + Sync, + Fut: Future> + Send, + Pdu: Event, +{ + let sender = room_member_event.sender(); + let (join_rule, sender_membership) = + join(fetch_state.join_rule(), fetch_state.user_membership(sender)).await; + + // v7-v9, if the join_rule is anything other than knock, reject. + // Since v10, if the join_rule is anything other than knock or knock_restricted, + // reject. + let join_rule = join_rule?; + if join_rule != JoinRule::Knock + && (rules.knock_restricted_join_rule && !matches!(join_rule, JoinRule::KnockRestricted)) + { + return Err!( + "join rule is not set to knock or knock_restricted, knocking is not allowed" + ); + } + + // Since v7, if sender does not match state_key, reject. + if room_member_event.sender() != target_user { + return Err!("cannot make another user knock, sender does not match target user"); + } + + // Since v7, if the sender’s current membership is not ban, invite, or join, + // allow. Otherwise, reject. + let sender_membership = sender_membership?; + if !matches!( + sender_membership, + MembershipState::Ban | MembershipState::Invite | MembershipState::Join + ) { + Ok(()) + } else { + Err!("cannot knock if user is banned, invited or joined") + } +} diff --git a/src/core/matrix/state_res/event_auth/room_member/tests.rs b/src/core/matrix/state_res/event_auth/room_member/tests.rs new file mode 100644 index 00000000..736ed7d6 --- /dev/null +++ b/src/core/matrix/state_res/event_auth/room_member/tests.rs @@ -0,0 +1,2605 @@ +use ruma::{ + Signatures, + events::{ + TimelineEventType, + room::{ + join_rules::{JoinRule, Restricted, RoomJoinRulesEventContent}, + member::{MembershipState, RoomMemberEventContent, SignedContent, ThirdPartyInvite}, + third_party_invite::RoomThirdPartyInviteEventContent, + }, + }, + room_version_rules::AuthorizationRules, + serde::Raw, + third_party_invite::IdentityServerBase64PublicKey, +}; +use serde_json::{json, value::to_raw_value as to_raw_json_value}; + +use super::{ + super::events::RoomMemberEvent, + check_room_member, + test_utils::{ + INITIAL_EVENTS, INITIAL_EVENTS_CREATE_ROOM, TestStateMap, alice, bob, charlie, ella, + event_id, init_subscriber, member_content_ban, member_content_join, + room_third_party_invite, to_pdu_event, zara, + }, +}; + +#[tokio::test] +async fn missing_state_key() { + let _guard = init_subscriber(); + + let incoming_event = to_pdu_event( + "HELLO", + charlie(), + TimelineEventType::RoomMember, + None, + member_content_join(), + &["CREATE", "IMA", "IPOWER"], + &["IPOWER"], + ); + + let init_events = INITIAL_EVENTS(); + let auth_events = TestStateMap::new(&init_events); + let fetch_state = auth_events.fetch_state_fn(); + let room_create_event = auth_events.room_create_event(); + + // Event should have a state key. + check_room_member( + &RoomMemberEvent::new(incoming_event), + &AuthorizationRules::V6, + &room_create_event, + &fetch_state, + ) + .await + .unwrap_err(); +} + +#[tokio::test] +async fn missing_membership() { + let _guard = init_subscriber(); + + let incoming_event = to_pdu_event( + "HELLO", + charlie(), + TimelineEventType::RoomMember, + Some(charlie().as_str()), + to_raw_json_value(&json!({})).unwrap(), + &["CREATE", "IMA", "IPOWER"], + &["IPOWER"], + ); + + let init_events = INITIAL_EVENTS(); + let auth_events = TestStateMap::new(&init_events); + let fetch_state = auth_events.fetch_state_fn(); + let room_create_event = auth_events.room_create_event(); + + // Content should at least include `membership`. + check_room_member( + &RoomMemberEvent::new(incoming_event), + &AuthorizationRules::V6, + &room_create_event, + &fetch_state, + ) + .await + .unwrap_err(); +} + +#[tokio::test] +async fn join_after_create_creator_match() { + let _guard = init_subscriber(); + + let incoming_event = to_pdu_event( + "HELLO", + alice(), + TimelineEventType::RoomMember, + Some(alice().as_str()), + member_content_join(), + &["CREATE"], + &["CREATE"], + ); + + let init_events = INITIAL_EVENTS_CREATE_ROOM(); + let auth_events = TestStateMap::new(&init_events); + let fetch_state = auth_events.fetch_state_fn(); + let room_create_event = auth_events.room_create_event(); + + // Before v11, the `creator` of `m.room.create` must be the same as the state + // key. + check_room_member( + &RoomMemberEvent::new(incoming_event), + &AuthorizationRules::V6, + &room_create_event, + &fetch_state, + ) + .await + .unwrap(); +} + +#[tokio::test] +async fn join_after_create_creator_mismatch() { + let _guard = init_subscriber(); + + let incoming_event = to_pdu_event( + "HELLO", + charlie(), + TimelineEventType::RoomMember, + Some(charlie().as_str()), + member_content_join(), + &["CREATE"], + &["CREATE"], + ); + + let init_events = INITIAL_EVENTS_CREATE_ROOM(); + let auth_events = TestStateMap::new(&init_events); + let fetch_state = auth_events.fetch_state_fn(); + let room_create_event = auth_events.room_create_event(); + + // Before v11, the `creator` of `m.room.create` must be the same as the state + // key. + check_room_member( + &RoomMemberEvent::new(incoming_event), + &AuthorizationRules::V6, + &room_create_event, + &fetch_state, + ) + .await + .unwrap_err(); +} + +#[tokio::test] +async fn join_after_create_sender_match() { + let _guard = init_subscriber(); + + let incoming_event = to_pdu_event( + "HELLO", + alice(), + TimelineEventType::RoomMember, + Some(alice().as_str()), + member_content_join(), + &["CREATE"], + &["CREATE"], + ); + + let init_events = INITIAL_EVENTS_CREATE_ROOM(); + let auth_events = TestStateMap::new(&init_events); + let fetch_state = auth_events.fetch_state_fn(); + let room_create_event = auth_events.room_create_event(); + + // Since v11, the `sender` of `m.room.create` must be the same as the state key. + check_room_member( + &RoomMemberEvent::new(incoming_event), + &AuthorizationRules::V11, + &room_create_event, + &fetch_state, + ) + .await + .unwrap(); +} + +#[tokio::test] +async fn join_after_create_sender_mismatch() { + let _guard = init_subscriber(); + + let incoming_event = to_pdu_event( + "HELLO", + charlie(), + TimelineEventType::RoomMember, + Some(charlie().as_str()), + member_content_join(), + &["CREATE"], + &["CREATE"], + ); + + let init_events = INITIAL_EVENTS_CREATE_ROOM(); + let auth_events = TestStateMap::new(&init_events); + let fetch_state = auth_events.fetch_state_fn(); + let room_create_event = auth_events.room_create_event(); + + // Since v11, the `sender` of `m.room.create` must be the same as the state key. + check_room_member( + &RoomMemberEvent::new(incoming_event), + &AuthorizationRules::V11, + &room_create_event, + &fetch_state, + ) + .await + .unwrap_err(); +} + +#[tokio::test] +async fn join_sender_state_key_mismatch() { + let _guard = init_subscriber(); + + let incoming_event = to_pdu_event( + "HELLO", + charlie(), + TimelineEventType::RoomMember, + Some(alice().as_str()), + member_content_join(), + &["CREATE", "IMA", "IPOWER"], + &["IPOWER"], + ); + + let init_events = INITIAL_EVENTS(); + let auth_events = TestStateMap::new(&init_events); + let fetch_state = auth_events.fetch_state_fn(); + let room_create_event = auth_events.room_create_event(); + + // For join events, the sender must be the same as the state key. + check_room_member( + &RoomMemberEvent::new(incoming_event), + &AuthorizationRules::V6, + &room_create_event, + &fetch_state, + ) + .await + .unwrap_err(); +} + +#[tokio::test] +async fn join_banned() { + let _guard = init_subscriber(); + + let incoming_event = to_pdu_event( + "HELLO", + charlie(), + TimelineEventType::RoomMember, + Some(charlie().as_str()), + member_content_join(), + &["CREATE", "IMA", "IPOWER"], + &["IPOWER"], + ); + + let mut init_events = INITIAL_EVENTS(); + *init_events.get_mut(&event_id("IMC")).unwrap() = to_pdu_event( + "IMC", + charlie(), + TimelineEventType::RoomMember, + Some(charlie().as_str()), + member_content_ban(), + &["CREATE", "IJR", "IPOWER"], + &["IMB"], + ); + + let auth_events = TestStateMap::new(&init_events); + let fetch_state = auth_events.fetch_state_fn(); + let room_create_event = auth_events.room_create_event(); + + // A user cannot join if they are banned. + check_room_member( + &RoomMemberEvent::new(incoming_event), + &AuthorizationRules::V6, + &room_create_event, + &fetch_state, + ) + .await + .unwrap_err(); +} + +#[tokio::test] +async fn join_invite_join_rule_already_joined() { + let _guard = init_subscriber(); + + let incoming_event = to_pdu_event( + "HELLO", + charlie(), + TimelineEventType::RoomMember, + Some(charlie().as_str()), + member_content_join(), + &["CREATE", "IMA", "IPOWER"], + &["IPOWER"], + ); + + let mut init_events = INITIAL_EVENTS(); + *init_events.get_mut(&event_id("IJR")).unwrap() = to_pdu_event( + "IJR", + alice(), + TimelineEventType::RoomJoinRules, + Some(""), + to_raw_json_value(&RoomJoinRulesEventContent::new(JoinRule::Invite)).unwrap(), + &["CREATE", "IMA", "IPOWER"], + &["IPOWER"], + ); + + let auth_events = TestStateMap::new(&init_events); + let fetch_state = auth_events.fetch_state_fn(); + let room_create_event = auth_events.room_create_event(); + + // A user can send a join event in a room with `invite` join rule if they + // already joined. + check_room_member( + &RoomMemberEvent::new(incoming_event), + &AuthorizationRules::V6, + &room_create_event, + &fetch_state, + ) + .await + .unwrap(); +} + +#[tokio::test] +async fn join_knock_join_rule_already_invited() { + let _guard = init_subscriber(); + + let incoming_event = to_pdu_event( + "HELLO", + charlie(), + TimelineEventType::RoomMember, + Some(charlie().as_str()), + member_content_join(), + &["CREATE", "IMA", "IPOWER"], + &["IPOWER"], + ); + + let mut init_events = INITIAL_EVENTS(); + *init_events.get_mut(&event_id("IMC")).unwrap() = to_pdu_event( + "IMC", + charlie(), + TimelineEventType::RoomMember, + Some(charlie().as_str()), + to_raw_json_value(&RoomMemberEventContent::new(MembershipState::Invite)).unwrap(), + &["CREATE", "IJR", "IPOWER"], + &["IMB"], + ); + *init_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 = TestStateMap::new(&init_events); + let fetch_state = auth_events.fetch_state_fn(); + let room_create_event = auth_events.room_create_event(); + + // Since v7, a user can send a join event in a room with `knock` join rule if + // they are were invited. + check_room_member( + &RoomMemberEvent::new(incoming_event), + &AuthorizationRules::V7, + &room_create_event, + &fetch_state, + ) + .await + .unwrap(); +} + +#[tokio::test] +async fn join_knock_join_rule_not_supported() { + let _guard = init_subscriber(); + + let incoming_event = to_pdu_event( + "HELLO", + charlie(), + TimelineEventType::RoomMember, + Some(charlie().as_str()), + member_content_join(), + &["CREATE", "IMA", "IPOWER"], + &["IPOWER"], + ); + + let mut init_events = INITIAL_EVENTS(); + *init_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 = TestStateMap::new(&init_events); + let fetch_state = auth_events.fetch_state_fn(); + let room_create_event = auth_events.room_create_event(); + + // Before v7, a user CANNOT send a join event in a room with `knock` join rule. + // Servers should not allow that join rule if it's not supported by the room + // version, but this is good for coverage. + check_room_member( + &RoomMemberEvent::new(incoming_event), + &AuthorizationRules::V6, + &room_create_event, + &fetch_state, + ) + .await + .unwrap_err(); +} + +#[tokio::test] +async fn join_restricted_join_rule_not_supported() { + let _guard = init_subscriber(); + + let incoming_event = to_pdu_event( + "HELLO", + charlie(), + TimelineEventType::RoomMember, + Some(charlie().as_str()), + member_content_join(), + &["CREATE", "IMA", "IPOWER"], + &["IPOWER"], + ); + + let mut init_events = INITIAL_EVENTS(); + *init_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![]), + ))) + .unwrap(), + &["CREATE", "IMA", "IPOWER"], + &["IPOWER"], + ); + + let auth_events = TestStateMap::new(&init_events); + let fetch_state = auth_events.fetch_state_fn(); + let room_create_event = auth_events.room_create_event(); + + // Before v8, a user CANNOT send a join event in a room with `restricted` join + // rule. Servers should not allow that join rule if it's not supported by the + // room version, but this is good for coverage. + check_room_member( + &RoomMemberEvent::new(incoming_event), + &AuthorizationRules::V6, + &room_create_event, + &fetch_state, + ) + .await + .unwrap_err(); +} + +#[tokio::test] +async fn join_knock_restricted_join_rule_not_supported() { + let _guard = init_subscriber(); + + let incoming_event = to_pdu_event( + "HELLO", + charlie(), + TimelineEventType::RoomMember, + Some(charlie().as_str()), + member_content_join(), + &["CREATE", "IMA", "IPOWER"], + &["IPOWER"], + ); + + let mut init_events = INITIAL_EVENTS(); + *init_events.get_mut(&event_id("IJR")).unwrap() = to_pdu_event( + "IJR", + alice(), + TimelineEventType::RoomJoinRules, + Some(""), + to_raw_json_value(&RoomJoinRulesEventContent::new(JoinRule::KnockRestricted( + Restricted::new(vec![]), + ))) + .unwrap(), + &["CREATE", "IMA", "IPOWER"], + &["IPOWER"], + ); + + let auth_events = TestStateMap::new(&init_events); + let fetch_state = auth_events.fetch_state_fn(); + let room_create_event = auth_events.room_create_event(); + + // Before v10, a user CANNOT send a join event in a room with `knock_restricted` + // join rule. Servers should not allow that join rule if it's not supported by + // the room version, but this is good for coverage. + check_room_member( + &RoomMemberEvent::new(incoming_event), + &AuthorizationRules::V6, + &room_create_event, + &fetch_state, + ) + .await + .unwrap_err(); +} + +#[tokio::test] +async fn join_restricted_join_rule_already_joined() { + let _guard = init_subscriber(); + + let incoming_event = to_pdu_event( + "HELLO", + charlie(), + TimelineEventType::RoomMember, + Some(charlie().as_str()), + member_content_join(), + &["CREATE", "IMA", "IPOWER"], + &["IPOWER"], + ); + + let mut init_events = INITIAL_EVENTS(); + *init_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![]), + ))) + .unwrap(), + &["CREATE", "IMA", "IPOWER"], + &["IPOWER"], + ); + + let auth_events = TestStateMap::new(&init_events); + let fetch_state = auth_events.fetch_state_fn(); + let room_create_event = auth_events.room_create_event(); + + // Since v8, a user can send a join event in a room with `restricted` join rule + // if they already joined. + check_room_member( + &RoomMemberEvent::new(incoming_event), + &AuthorizationRules::V8, + &room_create_event, + &fetch_state, + ) + .await + .unwrap(); +} + +#[tokio::test] +async fn join_knock_restricted_join_rule_already_invited() { + let _guard = init_subscriber(); + + let incoming_event = to_pdu_event( + "HELLO", + charlie(), + TimelineEventType::RoomMember, + Some(charlie().as_str()), + member_content_join(), + &["CREATE", "IMA", "IPOWER"], + &["IPOWER"], + ); + + let mut init_events = INITIAL_EVENTS(); + *init_events.get_mut(&event_id("IMC")).unwrap() = to_pdu_event( + "IMC", + charlie(), + TimelineEventType::RoomMember, + Some(charlie().as_str()), + to_raw_json_value(&RoomMemberEventContent::new(MembershipState::Invite)).unwrap(), + &["CREATE", "IJR", "IPOWER"], + &["IMB"], + ); + *init_events.get_mut(&event_id("IJR")).unwrap() = to_pdu_event( + "IJR", + alice(), + TimelineEventType::RoomJoinRules, + Some(""), + to_raw_json_value(&RoomJoinRulesEventContent::new(JoinRule::KnockRestricted( + Restricted::new(vec![]), + ))) + .unwrap(), + &["CREATE", "IMA", "IPOWER"], + &["IPOWER"], + ); + + let auth_events = TestStateMap::new(&init_events); + let fetch_state = auth_events.fetch_state_fn(); + let room_create_event = auth_events.room_create_event(); + + // Since v10, a user can send a join event in a room with `knock_restricted` + // join rule if they were invited. + check_room_member( + &RoomMemberEvent::new(incoming_event), + &AuthorizationRules::V10, + &room_create_event, + &fetch_state, + ) + .await + .unwrap(); +} + +#[tokio::test] +async fn join_restricted_join_rule_missing_join_authorised_via_users_server() { + let _guard = init_subscriber(); + + let incoming_event = to_pdu_event( + "HELLO", + ella(), + TimelineEventType::RoomMember, + Some(ella().as_str()), + member_content_join(), + &["CREATE", "IJR", "IPOWER"], + &["IPOWER"], + ); + + let mut init_events = INITIAL_EVENTS(); + *init_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![]), + ))) + .unwrap(), + &["CREATE", "IMA", "IPOWER"], + &["IPOWER"], + ); + + let auth_events = TestStateMap::new(&init_events); + let fetch_state = auth_events.fetch_state_fn(); + let room_create_event = auth_events.room_create_event(); + + // Since v8, a user CANNOT join event in a room with `restricted` join rule if + // there is no `join_authorised_via_users_server` property. + check_room_member( + &RoomMemberEvent::new(incoming_event), + &AuthorizationRules::V8, + &room_create_event, + &fetch_state, + ) + .await + .unwrap_err(); +} + +#[tokio::test] +async fn join_restricted_join_rule_authorised_via_user_not_in_room() { + let _guard = init_subscriber(); + + let mut content = RoomMemberEventContent::new(MembershipState::Join); + content.join_authorized_via_users_server = Some(zara().to_owned()); + + let incoming_event = to_pdu_event( + "HELLO", + ella(), + TimelineEventType::RoomMember, + Some(ella().as_str()), + to_raw_json_value(&content).unwrap(), + &["CREATE", "IJR", "IPOWER"], + &["IPOWER"], + ); + + let mut init_events = INITIAL_EVENTS(); + *init_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![]), + ))) + .unwrap(), + &["CREATE", "IMA", "IPOWER"], + &["IPOWER"], + ); + + let auth_events = TestStateMap::new(&init_events); + let fetch_state = auth_events.fetch_state_fn(); + let room_create_event = auth_events.room_create_event(); + + // Since v8, a user CANNOT join event in a room with `restricted` join rule if + // they were authorized by a user not in the room. + check_room_member( + &RoomMemberEvent::new(incoming_event), + &AuthorizationRules::V8, + &room_create_event, + &fetch_state, + ) + .await + .unwrap_err(); +} + +#[tokio::test] +async fn join_restricted_join_rule_authorised_via_user_with_not_enough_power() { + let _guard = init_subscriber(); + + let mut content = RoomMemberEventContent::new(MembershipState::Join); + content.join_authorized_via_users_server = Some(charlie().to_owned()); + + let incoming_event = to_pdu_event( + "HELLO", + ella(), + TimelineEventType::RoomMember, + Some(ella().as_str()), + to_raw_json_value(&content).unwrap(), + &["CREATE", "IJR", "IPOWER"], + &["IPOWER"], + ); + + let mut init_events = INITIAL_EVENTS(); + *init_events.get_mut(&event_id("IPOWER")).unwrap() = to_pdu_event( + "IPOWER", + alice(), + TimelineEventType::RoomPowerLevels, + Some(""), + to_raw_json_value(&json!({ "users": { alice(): 100 }, "invite": 50 })).unwrap(), + &["CREATE", "IMA"], + &["IMA"], + ); + *init_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![]), + ))) + .unwrap(), + &["CREATE", "IMA", "IPOWER"], + &["IPOWER"], + ); + + let auth_events = TestStateMap::new(&init_events); + let fetch_state = auth_events.fetch_state_fn(); + let room_create_event = auth_events.room_create_event(); + + // Since v8, a user CANNOT join event in a room with `restricted` join rule if + // they were authorized by a user with not enough power. + check_room_member( + &RoomMemberEvent::new(incoming_event), + &AuthorizationRules::V8, + &room_create_event, + &fetch_state, + ) + .await + .unwrap_err(); +} + +#[tokio::test] +async fn join_restricted_join_rule_authorised_via_user() { + let _guard = init_subscriber(); + + // Check various contents that might not match the definition of + // `m.room.join_rules` in the spec, to ensure that we only care about the + // `join_rule` field. + let join_rules_to_check = [ + // Valid content, but we don't care about the allow rules. + to_raw_json_value(&RoomJoinRulesEventContent::new(JoinRule::Restricted( + Restricted::new(vec![]), + ))) + .unwrap(), + // Invalid room ID, real-life example from . + to_raw_json_value(&json!({ + "allow": [ + { + "room_id": "", + "type": "m.room_membership", + }, + ], + "join_rule": "restricted", + })) + .unwrap(), + // Missing room ID. + to_raw_json_value(&json!({ + "allow": [ + { + "type": "m.room_membership", + }, + ], + "join_rule": "restricted", + })) + .unwrap(), + ]; + + let mut content = RoomMemberEventContent::new(MembershipState::Join); + content.join_authorized_via_users_server = Some(charlie().to_owned()); + + let incoming_event = to_pdu_event( + "HELLO", + ella(), + TimelineEventType::RoomMember, + Some(ella().as_str()), + to_raw_json_value(&content).unwrap(), + &["CREATE", "IJR", "IPOWER"], + &["IPOWER"], + ); + + let mut init_events = INITIAL_EVENTS(); + + for join_rule_content in join_rules_to_check { + *init_events.get_mut(&event_id("IJR")).unwrap() = to_pdu_event( + "IJR", + alice(), + TimelineEventType::RoomJoinRules, + Some(""), + join_rule_content, + &["CREATE", "IMA", "IPOWER"], + &["IPOWER"], + ); + + let auth_events = TestStateMap::new(&init_events); + let fetch_state = auth_events.fetch_state_fn(); + let room_create_event = auth_events.room_create_event(); + + // Since v8, a user can join event in a room with `restricted` join rule if they + // were authorized by a user with enough power. + check_room_member( + &RoomMemberEvent::new(incoming_event.clone()), + &AuthorizationRules::V8, + &room_create_event, + &fetch_state, + ) + .await + .unwrap(); + } +} + +#[tokio::test] +async fn join_public_join_rule() { + let _guard = init_subscriber(); + + // Check various contents that might not match the definition of `m.room.member` + // in the spec, to ensure that we only care about a few fields. + let contents_to_check = [ + // Valid content. + member_content_join(), + // Invalid displayname. + to_raw_json_value(&json!({ + "membership": "join", + "displayname": 203, + })) + .unwrap(), + // Invalid is_direct. + to_raw_json_value(&json!({ + "membership": "join", + "is_direct": "yes", + })) + .unwrap(), + ]; + + let init_events = INITIAL_EVENTS(); + let auth_events = TestStateMap::new(&init_events); + let fetch_state = auth_events.fetch_state_fn(); + let room_create_event = auth_events.room_create_event(); + + for content in contents_to_check { + let incoming_event = to_pdu_event( + "HELLO", + ella(), + TimelineEventType::RoomMember, + Some(ella().as_str()), + content, + &["CREATE", "IMA", "IPOWER"], + &["IPOWER"], + ); + + // A user can join a room with a `public` join rule. + check_room_member( + &RoomMemberEvent::new(incoming_event), + &AuthorizationRules::V8, + &room_create_event, + &fetch_state, + ) + .await + .unwrap(); + } +} + +#[tokio::test] +async fn invite_via_third_party_invite_banned() { + let _guard = init_subscriber(); + + let mut content = RoomMemberEventContent::new(MembershipState::Invite); + content.third_party_invite = Some(ThirdPartyInvite::new( + "e..@p..".to_owned(), + Raw::new(&SignedContent::new( + Signatures::new(), + ella().to_owned(), + "somerandomtoken".to_owned(), + )) + .unwrap(), + )); + + let incoming_event = to_pdu_event( + "HELLO", + ella(), + TimelineEventType::RoomMember, + Some(ella().as_str()), + to_raw_json_value(&content).unwrap(), + &["CREATE", "BAN", "IPOWER"], + &["BAN"], + ); + + let mut init_events = INITIAL_EVENTS(); + init_events.insert( + event_id("BAN"), + to_pdu_event( + "BAN", + alice(), + TimelineEventType::RoomMember, + Some(ella().as_str()), + member_content_ban(), + &["CREATE", "IJR", "IPOWER"], + &["IPOWER"], + ), + ); + + let auth_events = TestStateMap::new(&init_events); + let fetch_state = auth_events.fetch_state_fn(); + let room_create_event = auth_events.room_create_event(); + + // A user cannot be invited via third party invite if they were banned. + check_room_member( + &RoomMemberEvent::new(incoming_event), + &AuthorizationRules::V8, + &room_create_event, + &fetch_state, + ) + .await + .unwrap_err(); +} + +#[tokio::test] +async fn invite_via_third_party_invite_missing_signed() { + let _guard = init_subscriber(); + + let content = json!({ + "membership": "invite", + "third_party_invite": { + "display_name": "e..@p..", + }, + }); + + let incoming_event = to_pdu_event( + "HELLO", + charlie(), + TimelineEventType::RoomMember, + Some(ella().as_str()), + to_raw_json_value(&content).unwrap(), + &["CREATE", "IJR", "IPOWER"], + &["IPOWER"], + ); + + let init_events = INITIAL_EVENTS(); + let auth_events = TestStateMap::new(&init_events); + let fetch_state = auth_events.fetch_state_fn(); + let room_create_event = auth_events.room_create_event(); + + // Third party invite content must have a `joined` property. + check_room_member( + &RoomMemberEvent::new(incoming_event), + &AuthorizationRules::V8, + &room_create_event, + &fetch_state, + ) + .await + .unwrap_err(); +} + +#[tokio::test] +async fn invite_via_third_party_invite_missing_mxid() { + let _guard = init_subscriber(); + + let content = json!({ + "membership": "invite", + "third_party_invite": { + "display_name": "e..@p..", + "signed": { + "token": "somerandomtoken", + }, + }, + }); + + let incoming_event = to_pdu_event( + "HELLO", + charlie(), + TimelineEventType::RoomMember, + Some(ella().as_str()), + to_raw_json_value(&content).unwrap(), + &["CREATE", "IJR", "IPOWER"], + &["IPOWER"], + ); + + let init_events = INITIAL_EVENTS(); + let auth_events = TestStateMap::new(&init_events); + let fetch_state = auth_events.fetch_state_fn(); + let room_create_event = auth_events.room_create_event(); + + // Third party invite content must have a `joined.mxid` property. + check_room_member( + &RoomMemberEvent::new(incoming_event), + &AuthorizationRules::V8, + &room_create_event, + &fetch_state, + ) + .await + .unwrap_err(); +} + +#[tokio::test] +async fn invite_via_third_party_invite_missing_token() { + let _guard = init_subscriber(); + + let content = json!({ + "membership": "invite", + "third_party_invite": { + "display_name": "e..@p..", + "signed": { + "mxid": ella(), + }, + }, + }); + + let incoming_event = to_pdu_event( + "HELLO", + charlie(), + TimelineEventType::RoomMember, + Some(ella().as_str()), + to_raw_json_value(&content).unwrap(), + &["CREATE", "IJR", "IPOWER"], + &["IPOWER"], + ); + + let init_events = INITIAL_EVENTS(); + let auth_events = TestStateMap::new(&init_events); + let fetch_state = auth_events.fetch_state_fn(); + let room_create_event = auth_events.room_create_event(); + + // Third party invite content must have a `joined.token` property. + check_room_member( + &RoomMemberEvent::new(incoming_event), + &AuthorizationRules::V8, + &room_create_event, + &fetch_state, + ) + .await + .unwrap_err(); +} + +#[tokio::test] +async fn invite_via_third_party_invite_mxid_mismatch() { + let _guard = init_subscriber(); + + let mut content = RoomMemberEventContent::new(MembershipState::Invite); + content.third_party_invite = Some(ThirdPartyInvite::new( + "z..@p..".to_owned(), + Raw::new(&SignedContent::new( + Signatures::new(), + zara().to_owned(), + "somerandomtoken".to_owned(), + )) + .unwrap(), + )); + + let incoming_event = to_pdu_event( + "HELLO", + charlie(), + TimelineEventType::RoomMember, + Some(ella().as_str()), + to_raw_json_value(&content).unwrap(), + &["CREATE", "IJR", "IPOWER"], + &["IPOWER"], + ); + + let init_events = INITIAL_EVENTS(); + let auth_events = TestStateMap::new(&init_events); + let fetch_state = auth_events.fetch_state_fn(); + let room_create_event = auth_events.room_create_event(); + + // `mxid` of third party invite must match state key. + check_room_member( + &RoomMemberEvent::new(incoming_event), + &AuthorizationRules::V8, + &room_create_event, + &fetch_state, + ) + .await + .unwrap_err(); +} + +#[tokio::test] +async fn invite_via_third_party_invite_missing_room_third_party_invite() { + let _guard = init_subscriber(); + + let mut content = RoomMemberEventContent::new(MembershipState::Invite); + content.third_party_invite = Some(ThirdPartyInvite::new( + "e..@p..".to_owned(), + Raw::new(&SignedContent::new( + Signatures::new(), + ella().to_owned(), + "somerandomtoken".to_owned(), + )) + .unwrap(), + )); + + let incoming_event = to_pdu_event( + "HELLO", + charlie(), + TimelineEventType::RoomMember, + Some(ella().as_str()), + to_raw_json_value(&content).unwrap(), + &["CREATE", "IJR", "IPOWER", "THIRDPARTY"], + &["THIRDPARTY"], + ); + + let mut init_events = INITIAL_EVENTS(); + init_events.insert( + event_id("THIRD_PARTY"), + to_pdu_event( + "THIRDPARTY", + charlie(), + TimelineEventType::RoomThirdPartyInvite, + Some("wrong_token"), + to_raw_json_value(&RoomThirdPartyInviteEventContent::new( + "e..@p..".to_owned(), + "http://host.local/check/public_key".to_owned(), + IdentityServerBase64PublicKey::new(b"public_key"), + )) + .unwrap(), + &["CREATE", "IJR", "IPOWER"], + &["IPOWER"], + ), + ); + + let auth_events = TestStateMap::new(&init_events); + let fetch_state = auth_events.fetch_state_fn(); + let room_create_event = auth_events.room_create_event(); + + // There must be an `m.room.third_party_invite` event with the same token in the + // state. + check_room_member( + &RoomMemberEvent::new(incoming_event), + &AuthorizationRules::V8, + &room_create_event, + &fetch_state, + ) + .await + .unwrap_err(); +} + +#[tokio::test] +async fn invite_via_third_party_invite_room_third_party_invite_sender_mismatch() { + let _guard = init_subscriber(); + + let mut content = RoomMemberEventContent::new(MembershipState::Invite); + content.third_party_invite = Some(ThirdPartyInvite::new( + "e..@p..".to_owned(), + Raw::new(&SignedContent::new( + Signatures::new(), + ella().to_owned(), + "somerandomtoken".to_owned(), + )) + .unwrap(), + )); + + let incoming_event = to_pdu_event( + "HELLO", + charlie(), + TimelineEventType::RoomMember, + Some(ella().as_str()), + to_raw_json_value(&content).unwrap(), + &["CREATE", "IJR", "IPOWER"], + &["IPOWER"], + ); + + let mut init_events = INITIAL_EVENTS(); + init_events.insert(event_id("THIRD_PARTY"), room_third_party_invite(bob())); + + let auth_events = TestStateMap::new(&init_events); + let fetch_state = auth_events.fetch_state_fn(); + let room_create_event = auth_events.room_create_event(); + + // `mxid` of third party invite must match state key. + check_room_member( + &RoomMemberEvent::new(incoming_event), + &AuthorizationRules::V8, + &room_create_event, + &fetch_state, + ) + .await + .unwrap_err(); +} + +#[tokio::test] +async fn invite_via_third_party_invite_with_room_missing_signatures() { + let _guard = init_subscriber(); + + let content = json!({ + "membership": "invite", + "third_party_invite": { + "display_name": "e...@p...", + "signed": { + "mxid": "@ella:foo", + "sender": "@charlie:foo", + "token": "somerandomtoken", + } + } + }); + + let incoming_event = to_pdu_event( + "HELLO", + charlie(), + TimelineEventType::RoomMember, + Some(ella().as_str()), + to_raw_json_value(&content).unwrap(), + &["CREATE", "IJR", "IPOWER", "THIRDPARTY"], + &["THIRDPARTY"], + ); + + let mut init_events = INITIAL_EVENTS(); + init_events.insert(event_id("THIRD_PARTY"), room_third_party_invite(charlie())); + + let auth_events = TestStateMap::new(&init_events); + let fetch_state = auth_events.fetch_state_fn(); + let room_create_event = auth_events.room_create_event(); + + // `signed` must have a `signatures` field. + check_room_member( + &RoomMemberEvent::new(incoming_event), + &AuthorizationRules::V8, + &room_create_event, + &fetch_state, + ) + .await + .unwrap_err(); +} + +#[tokio::test] +async fn invite_via_third_party_invite_with_room_empty_signatures() { + let _guard = init_subscriber(); + + let content = json!({ + "membership": "invite", + "third_party_invite": { + "display_name": "e...@p...", + "signed": { + "mxid": "@ella:foo", + "sender": "@charlie:foo", + "token": "somerandomtoken", + "signatures": [], + } + } + }); + + let incoming_event = to_pdu_event( + "HELLO", + charlie(), + TimelineEventType::RoomMember, + Some(ella().as_str()), + to_raw_json_value(&content).unwrap(), + &["CREATE", "IJR", "IPOWER", "THIRDPARTY"], + &["THIRDPARTY"], + ); + + let mut init_events = INITIAL_EVENTS(); + init_events.insert(event_id("THIRD_PARTY"), room_third_party_invite(charlie())); + + let auth_events = TestStateMap::new(&init_events); + let fetch_state = auth_events.fetch_state_fn(); + let room_create_event = auth_events.room_create_event(); + + // There is no signature to verify, we need at least one. + check_room_member( + &RoomMemberEvent::new(incoming_event), + &AuthorizationRules::V8, + &room_create_event, + &fetch_state, + ) + .await + .unwrap_err(); +} + +#[tokio::test] +async fn invite_via_third_party_invite_with_wrong_signature() { + let _guard = init_subscriber(); + + let content = json!({ + "membership": "invite", + "third_party_invite": { + "display_name": "e...@p...", + "signed": { + "mxid": "@ella:foo", + "sender": "@charlie:foo", + "token": "somerandomtoken", + "signatures": { + "identity.local": { + "ed25519:0": "ClearlyWrongSignature", + } + }, + } + } + }); + + let incoming_event = to_pdu_event( + "HELLO", + charlie(), + TimelineEventType::RoomMember, + Some(ella().as_str()), + to_raw_json_value(&content).unwrap(), + &["CREATE", "IJR", "IPOWER", "THIRDPARTY"], + &["THIRDPARTY"], + ); + + let mut init_events = INITIAL_EVENTS(); + init_events.insert(event_id("THIRD_PARTY"), room_third_party_invite(charlie())); + + let auth_events = TestStateMap::new(&init_events); + let fetch_state = auth_events.fetch_state_fn(); + let room_create_event = auth_events.room_create_event(); + + // No public key will manage to verify the signature. + check_room_member( + &RoomMemberEvent::new(incoming_event), + &AuthorizationRules::V8, + &room_create_event, + &fetch_state, + ) + .await + .unwrap_err(); +} + +#[tokio::test] +async fn invite_via_third_party_invite_with_wrong_signing_algorithm() { + let _guard = init_subscriber(); + + let content = json!({ + "membership": "invite", + "third_party_invite": { + "display_name": "e...@p...", + "signed": { + "mxid": "@ella:foo", + "sender": "@charlie:foo", + "token": "somerandomtoken", + "signatures": { + "identity.local": { + "unknown:0": "EyW7uaJagmhIQg7DUFpiJv9ur8h8DkDSjyV6f5MlROJrrkg8JElBFKr2iTQY9x+A6OauQdNy7L9T4xgzIZVbCA" + } + }, + } + } + }); + + let incoming_event = to_pdu_event( + "HELLO", + charlie(), + TimelineEventType::RoomMember, + Some(ella().as_str()), + to_raw_json_value(&content).unwrap(), + &["CREATE", "IJR", "IPOWER", "THIRDPARTY"], + &["THIRDPARTY"], + ); + + let mut init_events = INITIAL_EVENTS(); + init_events.insert(event_id("THIRD_PARTY"), room_third_party_invite(charlie())); + + let auth_events = TestStateMap::new(&init_events); + let fetch_state = auth_events.fetch_state_fn(); + let room_create_event = auth_events.room_create_event(); + + // Can't verify a signature with an unsupported algorithm, so there is no + // signature to verify. + check_room_member( + &RoomMemberEvent::new(incoming_event), + &AuthorizationRules::V8, + &room_create_event, + &fetch_state, + ) + .await + .unwrap_err(); +} + +#[tokio::test] +async fn invite_via_third_party_invite() { + let _guard = init_subscriber(); + + let content = json!({ + "membership": "invite", + "third_party_invite": { + "display_name": "e...@p...", + "signed": { + "mxid": "@ella:foo", + "sender": "@charlie:foo", + "token": "somerandomtoken", + "signatures": { + "identity.local": { + // This signature will be ignored because the algorithm is unsupported. + "unknown:0": "SomeSignature", + // This signature will fail the verification. + "ed25519:0": "ClearlyWrongSignature", + // This signature will pass verification! + "ed25519:1": "EyW7uaJagmhIQg7DUFpiJv9ur8h8DkDSjyV6f5MlROJrrkg8JElBFKr2iTQY9x+A6OauQdNy7L9T4xgzIZVbCA" + } + }, + } + } + }); + + let incoming_event = to_pdu_event( + "HELLO", + charlie(), + TimelineEventType::RoomMember, + Some(ella().as_str()), + to_raw_json_value(&content).unwrap(), + &["CREATE", "IJR", "IPOWER", "THIRDPARTY"], + &["THIRDPARTY"], + ); + + let mut init_events = INITIAL_EVENTS(); + init_events.insert(event_id("THIRD_PARTY"), room_third_party_invite(charlie())); + + let auth_events = TestStateMap::new(&init_events); + let fetch_state = auth_events.fetch_state_fn(); + let room_create_event = auth_events.room_create_event(); + + // Valid third party invite works. + check_room_member( + &RoomMemberEvent::new(incoming_event), + &AuthorizationRules::V8, + &room_create_event, + &fetch_state, + ) + .await + .unwrap(); +} + +#[tokio::test] +async fn invite_sender_not_joined() { + let _guard = init_subscriber(); + + let incoming_event = to_pdu_event( + "HELLO", + zara(), + TimelineEventType::RoomMember, + Some(ella().as_str()), + to_raw_json_value(&RoomMemberEventContent::new(MembershipState::Invite)).unwrap(), + &["CREATE", "IJR", "IPOWER"], + &["IPOWER"], + ); + + let init_events = INITIAL_EVENTS(); + let auth_events = TestStateMap::new(&init_events); + let fetch_state = auth_events.fetch_state_fn(); + let room_create_event = auth_events.room_create_event(); + + // The sender of the invite must have joined the room. + check_room_member( + &RoomMemberEvent::new(incoming_event), + &AuthorizationRules::V8, + &room_create_event, + &fetch_state, + ) + .await + .unwrap_err(); +} + +#[tokio::test] +async fn invite_banned() { + let _guard = init_subscriber(); + + let incoming_event = to_pdu_event( + "HELLO", + alice(), + TimelineEventType::RoomMember, + Some(charlie().as_str()), + to_raw_json_value(&RoomMemberEventContent::new(MembershipState::Invite)).unwrap(), + &["CREATE", "IJR", "IPOWER"], + &["IPOWER"], + ); + + let mut init_events = INITIAL_EVENTS(); + *init_events.get_mut(&event_id("IMC")).unwrap() = to_pdu_event( + "IMC", + charlie(), + TimelineEventType::RoomMember, + Some(charlie().as_str()), + member_content_ban(), + &["CREATE", "IJR", "IPOWER"], + &["IMB"], + ); + + let auth_events = TestStateMap::new(&init_events); + let fetch_state = auth_events.fetch_state_fn(); + let room_create_event = auth_events.room_create_event(); + + // The sender of the invite must have joined the room. + check_room_member( + &RoomMemberEvent::new(incoming_event), + &AuthorizationRules::V8, + &room_create_event, + &fetch_state, + ) + .await + .unwrap_err(); +} + +#[tokio::test] +async fn invite_already_joined() { + let _guard = init_subscriber(); + + let incoming_event = to_pdu_event( + "HELLO", + alice(), + TimelineEventType::RoomMember, + Some(charlie().as_str()), + to_raw_json_value(&RoomMemberEventContent::new(MembershipState::Invite)).unwrap(), + &["CREATE", "IJR", "IPOWER"], + &["IPOWER"], + ); + + let init_events = INITIAL_EVENTS(); + let auth_events = TestStateMap::new(&init_events); + let fetch_state = auth_events.fetch_state_fn(); + let room_create_event = auth_events.room_create_event(); + + // The sender of the invite must have joined the room. + check_room_member( + &RoomMemberEvent::new(incoming_event), + &AuthorizationRules::V8, + &room_create_event, + &fetch_state, + ) + .await + .unwrap_err(); +} + +#[tokio::test] +async fn invite_sender_not_enough_power() { + let _guard = init_subscriber(); + + let incoming_event = to_pdu_event( + "HELLO", + charlie(), + TimelineEventType::RoomMember, + Some(ella().as_str()), + to_raw_json_value(&RoomMemberEventContent::new(MembershipState::Invite)).unwrap(), + &["CREATE", "IJR", "IPOWER"], + &["IPOWER"], + ); + + let mut init_events = INITIAL_EVENTS(); + *init_events.get_mut(&event_id("IPOWER")).unwrap() = to_pdu_event( + "IPOWER", + alice(), + TimelineEventType::RoomPowerLevels, + Some(""), + to_raw_json_value(&json!({ "users": { alice(): 100 }, "invite": 50 })).unwrap(), + &["CREATE", "IMA"], + &["IMA"], + ); + + let auth_events = TestStateMap::new(&init_events); + let fetch_state = auth_events.fetch_state_fn(); + let room_create_event = auth_events.room_create_event(); + + // The sender must have enough power to invite in the room. + check_room_member( + &RoomMemberEvent::new(incoming_event), + &AuthorizationRules::V8, + &room_create_event, + &fetch_state, + ) + .await + .unwrap_err(); +} + +#[tokio::test] +async fn invite() { + let _guard = init_subscriber(); + + let incoming_event = to_pdu_event( + "HELLO", + charlie(), + TimelineEventType::RoomMember, + Some(ella().as_str()), + to_raw_json_value(&RoomMemberEventContent::new(MembershipState::Invite)).unwrap(), + &["CREATE", "IJR", "IPOWER"], + &["IPOWER"], + ); + + let init_events = INITIAL_EVENTS(); + let auth_events = TestStateMap::new(&init_events); + let fetch_state = auth_events.fetch_state_fn(); + let room_create_event = auth_events.room_create_event(); + + // The invite is valid. + check_room_member( + &RoomMemberEvent::new(incoming_event), + &AuthorizationRules::V8, + &room_create_event, + &fetch_state, + ) + .await + .unwrap(); +} + +#[tokio::test] +async fn leave_after_leave() { + let _guard = init_subscriber(); + + let incoming_event = to_pdu_event( + "HELLO", + ella(), + TimelineEventType::RoomMember, + Some(ella().as_str()), + to_raw_json_value(&RoomMemberEventContent::new(MembershipState::Leave)).unwrap(), + &["CREATE", "IJR", "IPOWER"], + &["IPOWER"], + ); + + let init_events = INITIAL_EVENTS(); + let auth_events = TestStateMap::new(&init_events); + let fetch_state = auth_events.fetch_state_fn(); + let room_create_event = auth_events.room_create_event(); + + // User can only leave after `invite`, `join` or `knock`. + check_room_member( + &RoomMemberEvent::new(incoming_event), + &AuthorizationRules::V8, + &room_create_event, + &fetch_state, + ) + .await + .unwrap_err(); +} + +#[tokio::test] +async fn leave_after_join() { + let _guard = init_subscriber(); + + let incoming_event = to_pdu_event( + "HELLO", + charlie(), + TimelineEventType::RoomMember, + Some(charlie().as_str()), + to_raw_json_value(&RoomMemberEventContent::new(MembershipState::Leave)).unwrap(), + &["CREATE", "IJR", "IPOWER"], + &["IPOWER"], + ); + + let init_events = INITIAL_EVENTS(); + let auth_events = TestStateMap::new(&init_events); + let fetch_state = auth_events.fetch_state_fn(); + let room_create_event = auth_events.room_create_event(); + + // User can leave after join. + check_room_member( + &RoomMemberEvent::new(incoming_event), + &AuthorizationRules::V8, + &room_create_event, + &fetch_state, + ) + .await + .unwrap(); +} + +#[tokio::test] +async fn leave_after_invite() { + let _guard = init_subscriber(); + + let incoming_event = to_pdu_event( + "HELLO", + charlie(), + TimelineEventType::RoomMember, + Some(charlie().as_str()), + to_raw_json_value(&RoomMemberEventContent::new(MembershipState::Leave)).unwrap(), + &["CREATE", "IJR", "IPOWER"], + &["IPOWER"], + ); + + let mut init_events = INITIAL_EVENTS(); + *init_events.get_mut(&event_id("IMC")).unwrap() = to_pdu_event( + "IMC", + charlie(), + TimelineEventType::RoomMember, + Some(charlie().as_str()), + to_raw_json_value(&RoomMemberEventContent::new(MembershipState::Invite)).unwrap(), + &["CREATE", "IJR", "IPOWER"], + &["IMB"], + ); + + let auth_events = TestStateMap::new(&init_events); + let fetch_state = auth_events.fetch_state_fn(); + let room_create_event = auth_events.room_create_event(); + + // User can leave after invite. + check_room_member( + &RoomMemberEvent::new(incoming_event), + &AuthorizationRules::V8, + &room_create_event, + &fetch_state, + ) + .await + .unwrap(); +} + +#[tokio::test] +async fn leave_after_knock() { + let _guard = init_subscriber(); + + let incoming_event = to_pdu_event( + "HELLO", + charlie(), + TimelineEventType::RoomMember, + Some(charlie().as_str()), + to_raw_json_value(&RoomMemberEventContent::new(MembershipState::Leave)).unwrap(), + &["CREATE", "IJR", "IPOWER"], + &["IPOWER"], + ); + + let mut init_events = INITIAL_EVENTS(); + *init_events.get_mut(&event_id("IMC")).unwrap() = to_pdu_event( + "IMC", + charlie(), + TimelineEventType::RoomMember, + Some(charlie().as_str()), + to_raw_json_value(&RoomMemberEventContent::new(MembershipState::Knock)).unwrap(), + &["CREATE", "IJR", "IPOWER"], + &["IMB"], + ); + + let auth_events = TestStateMap::new(&init_events); + let fetch_state = auth_events.fetch_state_fn(); + let room_create_event = auth_events.room_create_event(); + + // User can leave after knock. + check_room_member( + &RoomMemberEvent::new(incoming_event), + &AuthorizationRules::V8, + &room_create_event, + &fetch_state, + ) + .await + .unwrap(); +} + +#[tokio::test] +async fn leave_after_knock_not_supported() { + let _guard = init_subscriber(); + + let incoming_event = to_pdu_event( + "HELLO", + charlie(), + TimelineEventType::RoomMember, + Some(charlie().as_str()), + to_raw_json_value(&RoomMemberEventContent::new(MembershipState::Leave)).unwrap(), + &["CREATE", "IJR", "IPOWER"], + &["IPOWER"], + ); + + let mut init_events = INITIAL_EVENTS(); + *init_events.get_mut(&event_id("IMC")).unwrap() = to_pdu_event( + "IMC", + charlie(), + TimelineEventType::RoomMember, + Some(charlie().as_str()), + to_raw_json_value(&RoomMemberEventContent::new(MembershipState::Knock)).unwrap(), + &["CREATE", "IJR", "IPOWER"], + &["IMB"], + ); + + let auth_events = TestStateMap::new(&init_events); + let fetch_state = auth_events.fetch_state_fn(); + let room_create_event = auth_events.room_create_event(); + + // User can't leave if the room version does not support knocking. Servers + // should not allow that membership if it's not supported by the room version, + // but this is good for coverage. + check_room_member( + &RoomMemberEvent::new(incoming_event), + &AuthorizationRules::V6, + &room_create_event, + &fetch_state, + ) + .await + .unwrap_err(); +} + +#[tokio::test] +async fn leave_kick_sender_left() { + let _guard = init_subscriber(); + + let incoming_event = to_pdu_event( + "HELLO", + zara(), + TimelineEventType::RoomMember, + Some(charlie().as_str()), + to_raw_json_value(&RoomMemberEventContent::new(MembershipState::Leave)).unwrap(), + &["CREATE", "IJR", "IPOWER"], + &["IPOWER"], + ); + + let init_events = INITIAL_EVENTS(); + let auth_events = TestStateMap::new(&init_events); + let fetch_state = auth_events.fetch_state_fn(); + let room_create_event = auth_events.room_create_event(); + + // User can't kick if not joined. + check_room_member( + &RoomMemberEvent::new(incoming_event), + &AuthorizationRules::V6, + &room_create_event, + &fetch_state, + ) + .await + .unwrap_err(); +} + +#[tokio::test] +async fn leave_unban_not_enough_power() { + let _guard = init_subscriber(); + + let incoming_event = to_pdu_event( + "HELLO", + bob(), + TimelineEventType::RoomMember, + Some(charlie().as_str()), + to_raw_json_value(&RoomMemberEventContent::new(MembershipState::Leave)).unwrap(), + &["CREATE", "IJR", "IPOWER"], + &["IPOWER"], + ); + + let mut init_events = INITIAL_EVENTS(); + *init_events.get_mut(&event_id("IMC")).unwrap() = to_pdu_event( + "IMC", + charlie(), + TimelineEventType::RoomMember, + Some(charlie().as_str()), + member_content_ban(), + &["CREATE", "IJR", "IPOWER"], + &["IMB"], + ); + + let auth_events = TestStateMap::new(&init_events); + let fetch_state = auth_events.fetch_state_fn(); + let room_create_event = auth_events.room_create_event(); + + // User can't unban if not enough power. + check_room_member( + &RoomMemberEvent::new(incoming_event), + &AuthorizationRules::V6, + &room_create_event, + &fetch_state, + ) + .await + .unwrap_err(); +} + +#[tokio::test] +async fn leave_unban() { + let _guard = init_subscriber(); + + let incoming_event = to_pdu_event( + "HELLO", + alice(), + TimelineEventType::RoomMember, + Some(charlie().as_str()), + to_raw_json_value(&RoomMemberEventContent::new(MembershipState::Leave)).unwrap(), + &["CREATE", "IJR", "IPOWER"], + &["IPOWER"], + ); + + let mut init_events = INITIAL_EVENTS(); + *init_events.get_mut(&event_id("IMC")).unwrap() = to_pdu_event( + "IMC", + charlie(), + TimelineEventType::RoomMember, + Some(charlie().as_str()), + member_content_ban(), + &["CREATE", "IJR", "IPOWER"], + &["IMB"], + ); + + let auth_events = TestStateMap::new(&init_events); + let fetch_state = auth_events.fetch_state_fn(); + let room_create_event = auth_events.room_create_event(); + + // User can unban with enough power. + check_room_member( + &RoomMemberEvent::new(incoming_event), + &AuthorizationRules::V6, + &room_create_event, + &fetch_state, + ) + .await + .unwrap(); +} + +#[tokio::test] +async fn leave_kick_not_enough_power() { + let _guard = init_subscriber(); + + let incoming_event = to_pdu_event( + "HELLO", + bob(), + TimelineEventType::RoomMember, + Some(charlie().as_str()), + to_raw_json_value(&RoomMemberEventContent::new(MembershipState::Leave)).unwrap(), + &["CREATE", "IJR", "IPOWER"], + &["IPOWER"], + ); + + let init_events = INITIAL_EVENTS(); + let auth_events = TestStateMap::new(&init_events); + let fetch_state = auth_events.fetch_state_fn(); + let room_create_event = auth_events.room_create_event(); + + // User can't kick if not enough power for it. + check_room_member( + &RoomMemberEvent::new(incoming_event), + &AuthorizationRules::V6, + &room_create_event, + &fetch_state, + ) + .await + .unwrap_err(); +} + +#[tokio::test] +async fn leave_kick_greater_power() { + let _guard = init_subscriber(); + + let incoming_event = to_pdu_event( + "HELLO", + bob(), + TimelineEventType::RoomMember, + Some(charlie().as_str()), + to_raw_json_value(&RoomMemberEventContent::new(MembershipState::Leave)).unwrap(), + &["CREATE", "IJR", "IPOWER"], + &["IPOWER"], + ); + + let mut init_events = INITIAL_EVENTS(); + *init_events.get_mut(&event_id("IPOWER")).unwrap() = to_pdu_event( + "IPOWER", + alice(), + TimelineEventType::RoomPowerLevels, + Some(""), + to_raw_json_value(&json!({ + "users": { + alice(): 100, + bob(): 50, + charlie(): 60, + }, + })) + .unwrap(), + &["CREATE", "IMA"], + &["IMA"], + ); + + let auth_events = TestStateMap::new(&init_events); + let fetch_state = auth_events.fetch_state_fn(); + let room_create_event = auth_events.room_create_event(); + + // Can't kick user with greater power level. + check_room_member( + &RoomMemberEvent::new(incoming_event), + &AuthorizationRules::V6, + &room_create_event, + &fetch_state, + ) + .await + .unwrap_err(); +} + +#[tokio::test] +async fn leave_kick_same_power() { + let _guard = init_subscriber(); + + let incoming_event = to_pdu_event( + "HELLO", + bob(), + TimelineEventType::RoomMember, + Some(charlie().as_str()), + to_raw_json_value(&RoomMemberEventContent::new(MembershipState::Leave)).unwrap(), + &["CREATE", "IJR", "IPOWER"], + &["IPOWER"], + ); + + let mut init_events = INITIAL_EVENTS(); + *init_events.get_mut(&event_id("IPOWER")).unwrap() = to_pdu_event( + "IPOWER", + alice(), + TimelineEventType::RoomPowerLevels, + Some(""), + to_raw_json_value(&json!({ + "users": { + alice(): 100, + bob(): 50, + charlie(): 50, + }, + })) + .unwrap(), + &["CREATE", "IMA"], + &["IMA"], + ); + + let auth_events = TestStateMap::new(&init_events); + let fetch_state = auth_events.fetch_state_fn(); + let room_create_event = auth_events.room_create_event(); + + // Can't kick user with same power level. + check_room_member( + &RoomMemberEvent::new(incoming_event), + &AuthorizationRules::V6, + &room_create_event, + &fetch_state, + ) + .await + .unwrap_err(); +} + +#[tokio::test] +async fn leave_kick() { + let _guard = init_subscriber(); + + let incoming_event = to_pdu_event( + "HELLO", + alice(), + TimelineEventType::RoomMember, + Some(charlie().as_str()), + to_raw_json_value(&RoomMemberEventContent::new(MembershipState::Leave)).unwrap(), + &["CREATE", "IJR", "IPOWER"], + &["IPOWER"], + ); + + let init_events = INITIAL_EVENTS(); + let auth_events = TestStateMap::new(&init_events); + let fetch_state = auth_events.fetch_state_fn(); + let room_create_event = auth_events.room_create_event(); + + // Can kick user with enough power. + check_room_member( + &RoomMemberEvent::new(incoming_event), + &AuthorizationRules::V6, + &room_create_event, + &fetch_state, + ) + .await + .unwrap(); +} + +#[tokio::test] +async fn ban_sender_not_joined() { + let _guard = init_subscriber(); + + let incoming_event = to_pdu_event( + "HELLO", + ella(), + TimelineEventType::RoomMember, + Some(charlie().as_str()), + member_content_ban(), + &["CREATE", "IJR", "IPOWER"], + &["IPOWER"], + ); + + let init_events = INITIAL_EVENTS(); + let auth_events = TestStateMap::new(&init_events); + let fetch_state = auth_events.fetch_state_fn(); + let room_create_event = auth_events.room_create_event(); + + // Can't ban user if not in room. + check_room_member( + &RoomMemberEvent::new(incoming_event), + &AuthorizationRules::V6, + &room_create_event, + &fetch_state, + ) + .await + .unwrap_err(); +} + +#[tokio::test] +async fn ban_not_enough_power() { + let _guard = init_subscriber(); + + let incoming_event = to_pdu_event( + "HELLO", + charlie(), + TimelineEventType::RoomMember, + Some(bob().as_str()), + member_content_ban(), + &["CREATE", "IJR", "IPOWER"], + &["IPOWER"], + ); + + let init_events = INITIAL_EVENTS(); + let auth_events = TestStateMap::new(&init_events); + let fetch_state = auth_events.fetch_state_fn(); + let room_create_event = auth_events.room_create_event(); + + // Can't ban user if not enough power. + check_room_member( + &RoomMemberEvent::new(incoming_event), + &AuthorizationRules::V6, + &room_create_event, + &fetch_state, + ) + .await + .unwrap_err(); +} + +#[tokio::test] +async fn ban_greater_power() { + let _guard = init_subscriber(); + + let incoming_event = to_pdu_event( + "HELLO", + charlie(), + TimelineEventType::RoomMember, + Some(bob().as_str()), + member_content_ban(), + &["CREATE", "IJR", "IPOWER"], + &["IPOWER"], + ); + + let mut init_events = INITIAL_EVENTS(); + *init_events.get_mut(&event_id("IPOWER")).unwrap() = to_pdu_event( + "IPOWER", + alice(), + TimelineEventType::RoomPowerLevels, + Some(""), + to_raw_json_value(&json!({ + "users": { + alice(): 100, + bob(): 60, + charlie(): 50, + }, + })) + .unwrap(), + &["CREATE", "IMA"], + &["IMA"], + ); + + let auth_events = TestStateMap::new(&init_events); + let fetch_state = auth_events.fetch_state_fn(); + let room_create_event = auth_events.room_create_event(); + + // Can't ban user with greater power level. + check_room_member( + &RoomMemberEvent::new(incoming_event), + &AuthorizationRules::V6, + &room_create_event, + &fetch_state, + ) + .await + .unwrap_err(); +} + +#[tokio::test] +async fn ban_same_power() { + let _guard = init_subscriber(); + + let incoming_event = to_pdu_event( + "HELLO", + charlie(), + TimelineEventType::RoomMember, + Some(bob().as_str()), + member_content_ban(), + &["CREATE", "IJR", "IPOWER"], + &["IPOWER"], + ); + + let mut init_events = INITIAL_EVENTS(); + *init_events.get_mut(&event_id("IPOWER")).unwrap() = to_pdu_event( + "IPOWER", + alice(), + TimelineEventType::RoomPowerLevels, + Some(""), + to_raw_json_value(&json!({ + "users": { + alice(): 100, + bob(): 50, + charlie(): 50, + }, + })) + .unwrap(), + &["CREATE", "IMA"], + &["IMA"], + ); + + let auth_events = TestStateMap::new(&init_events); + let fetch_state = auth_events.fetch_state_fn(); + let room_create_event = auth_events.room_create_event(); + + // Can't ban user with same power level. + check_room_member( + &RoomMemberEvent::new(incoming_event), + &AuthorizationRules::V6, + &room_create_event, + &fetch_state, + ) + .await + .unwrap_err(); +} + +#[tokio::test] +async fn ban() { + let _guard = init_subscriber(); + + let incoming_event = to_pdu_event( + "HELLO", + alice(), + TimelineEventType::RoomMember, + Some(bob().as_str()), + member_content_ban(), + &["CREATE", "IJR", "IPOWER"], + &["IPOWER"], + ); + + let init_events = INITIAL_EVENTS(); + let auth_events = TestStateMap::new(&init_events); + let fetch_state = auth_events.fetch_state_fn(); + let room_create_event = auth_events.room_create_event(); + + // Can ban user with enough power. + check_room_member( + &RoomMemberEvent::new(incoming_event), + &AuthorizationRules::V6, + &room_create_event, + &fetch_state, + ) + .await + .unwrap(); +} + +#[tokio::test] +async fn knock_public_join_rule() { + let _guard = init_subscriber(); + + let incoming_event = to_pdu_event( + "HELLO", + ella(), + TimelineEventType::RoomMember, + Some(ella().as_str()), + to_raw_json_value(&RoomMemberEventContent::new(MembershipState::Knock)).unwrap(), + &["CREATE", "IJR", "IPOWER"], + &["IPOWER"], + ); + + let init_events = INITIAL_EVENTS(); + let auth_events = TestStateMap::new(&init_events); + let fetch_state = auth_events.fetch_state_fn(); + let room_create_event = auth_events.room_create_event(); + + // User can't knock if join rule is not `knock` or `knock_restricted`. + check_room_member( + &RoomMemberEvent::new(incoming_event), + &AuthorizationRules::V11, + &room_create_event, + &fetch_state, + ) + .await + .unwrap_err(); +} + +#[tokio::test] +async fn knock_knock_join_rule() { + let _guard = init_subscriber(); + + let incoming_event = to_pdu_event( + "HELLO", + ella(), + TimelineEventType::RoomMember, + Some(ella().as_str()), + to_raw_json_value(&RoomMemberEventContent::new(MembershipState::Knock)).unwrap(), + &["CREATE", "IJR", "IPOWER"], + &["IPOWER"], + ); + + let mut init_events = INITIAL_EVENTS(); + *init_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 = TestStateMap::new(&init_events); + let fetch_state = auth_events.fetch_state_fn(); + let room_create_event = auth_events.room_create_event(); + + // User can knock if room version supports it. + check_room_member( + &RoomMemberEvent::new(incoming_event), + &AuthorizationRules::V7, + &room_create_event, + &fetch_state, + ) + .await + .unwrap(); +} + +#[tokio::test] +async fn knock_knock_join_rule_not_supported() { + let _guard = init_subscriber(); + + let incoming_event = to_pdu_event( + "HELLO", + ella(), + TimelineEventType::RoomMember, + Some(ella().as_str()), + to_raw_json_value(&RoomMemberEventContent::new(MembershipState::Knock)).unwrap(), + &["CREATE", "IJR", "IPOWER"], + &["IPOWER"], + ); + + let mut init_events = INITIAL_EVENTS(); + *init_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 = TestStateMap::new(&init_events); + let fetch_state = auth_events.fetch_state_fn(); + let room_create_event = auth_events.room_create_event(); + + // User CANNOT knock if room version doesn't support it. + check_room_member( + &RoomMemberEvent::new(incoming_event), + &AuthorizationRules::V3, + &room_create_event, + &fetch_state, + ) + .await + .unwrap_err(); +} + +#[tokio::test] +async fn knock_knock_restricted_join_rule() { + let _guard = init_subscriber(); + + let incoming_event = to_pdu_event( + "HELLO", + ella(), + TimelineEventType::RoomMember, + Some(ella().as_str()), + to_raw_json_value(&RoomMemberEventContent::new(MembershipState::Knock)).unwrap(), + &["CREATE", "IJR", "IPOWER"], + &["IPOWER"], + ); + + let mut init_events = INITIAL_EVENTS(); + *init_events.get_mut(&event_id("IJR")).unwrap() = to_pdu_event( + "IJR", + alice(), + TimelineEventType::RoomJoinRules, + Some(""), + to_raw_json_value(&RoomJoinRulesEventContent::new(JoinRule::KnockRestricted( + Restricted::new(vec![]), + ))) + .unwrap(), + &["CREATE", "IMA", "IPOWER"], + &["IPOWER"], + ); + + let auth_events = TestStateMap::new(&init_events); + let fetch_state = auth_events.fetch_state_fn(); + let room_create_event = auth_events.room_create_event(); + + // User can knock if room version supports it. + check_room_member( + &RoomMemberEvent::new(incoming_event), + &AuthorizationRules::V10, + &room_create_event, + &fetch_state, + ) + .await + .unwrap(); +} + +#[tokio::test] +async fn knock_knock_restricted_join_rule_not_supported() { + let _guard = init_subscriber(); + + let incoming_event = to_pdu_event( + "HELLO", + ella(), + TimelineEventType::RoomMember, + Some(ella().as_str()), + to_raw_json_value(&RoomMemberEventContent::new(MembershipState::Knock)).unwrap(), + &["CREATE", "IJR", "IPOWER"], + &["IPOWER"], + ); + + let mut init_events = INITIAL_EVENTS(); + *init_events.get_mut(&event_id("IJR")).unwrap() = to_pdu_event( + "IJR", + alice(), + TimelineEventType::RoomJoinRules, + Some(""), + to_raw_json_value(&RoomJoinRulesEventContent::new(JoinRule::KnockRestricted( + Restricted::new(vec![]), + ))) + .unwrap(), + &["CREATE", "IMA", "IPOWER"], + &["IPOWER"], + ); + + let auth_events = TestStateMap::new(&init_events); + let fetch_state = auth_events.fetch_state_fn(); + let room_create_event = auth_events.room_create_event(); + + // User CANNOT knock if room version doesn't support it. + check_room_member( + &RoomMemberEvent::new(incoming_event), + &AuthorizationRules::V3, + &room_create_event, + &fetch_state, + ) + .await + .unwrap_err(); +} + +#[tokio::test] +async fn knock_sender_state_key_mismatch() { + let _guard = init_subscriber(); + + let incoming_event = to_pdu_event( + "HELLO", + zara(), + TimelineEventType::RoomMember, + Some(ella().as_str()), + to_raw_json_value(&RoomMemberEventContent::new(MembershipState::Knock)).unwrap(), + &["CREATE", "IJR", "IPOWER"], + &["IPOWER"], + ); + + let mut init_events = INITIAL_EVENTS(); + *init_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 = TestStateMap::new(&init_events); + let fetch_state = auth_events.fetch_state_fn(); + let room_create_event = auth_events.room_create_event(); + + // User cannot knock if state key doesn't match sender. + check_room_member( + &RoomMemberEvent::new(incoming_event), + &AuthorizationRules::V7, + &room_create_event, + &fetch_state, + ) + .await + .unwrap_err(); +} + +#[tokio::test] +async fn knock_after_ban() { + let _guard = init_subscriber(); + + let incoming_event = to_pdu_event( + "HELLO", + charlie(), + TimelineEventType::RoomMember, + Some(charlie().as_str()), + to_raw_json_value(&RoomMemberEventContent::new(MembershipState::Knock)).unwrap(), + &["CREATE", "IJR", "IPOWER"], + &["IPOWER"], + ); + + let mut init_events = INITIAL_EVENTS(); + *init_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"], + ); + *init_events.get_mut(&event_id("IMC")).unwrap() = to_pdu_event( + "IMC", + charlie(), + TimelineEventType::RoomMember, + Some(charlie().as_str()), + member_content_ban(), + &["CREATE", "IJR", "IPOWER"], + &["IMB"], + ); + + let auth_events = TestStateMap::new(&init_events); + let fetch_state = auth_events.fetch_state_fn(); + let room_create_event = auth_events.room_create_event(); + + // User cannot knock if banned. + check_room_member( + &RoomMemberEvent::new(incoming_event), + &AuthorizationRules::V7, + &room_create_event, + &fetch_state, + ) + .await + .unwrap_err(); +} + +#[tokio::test] +async fn knock_after_invite() { + let _guard = init_subscriber(); + + let incoming_event = to_pdu_event( + "HELLO", + charlie(), + TimelineEventType::RoomMember, + Some(charlie().as_str()), + to_raw_json_value(&RoomMemberEventContent::new(MembershipState::Knock)).unwrap(), + &["CREATE", "IJR", "IPOWER"], + &["IPOWER"], + ); + + let mut init_events = INITIAL_EVENTS(); + *init_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"], + ); + *init_events.get_mut(&event_id("IMC")).unwrap() = to_pdu_event( + "IMC", + charlie(), + TimelineEventType::RoomMember, + Some(charlie().as_str()), + to_raw_json_value(&RoomMemberEventContent::new(MembershipState::Invite)).unwrap(), + &["CREATE", "IJR", "IPOWER"], + &["IMB"], + ); + + let auth_events = TestStateMap::new(&init_events); + let fetch_state = auth_events.fetch_state_fn(); + let room_create_event = auth_events.room_create_event(); + + // User cannot knock after being invited. + check_room_member( + &RoomMemberEvent::new(incoming_event), + &AuthorizationRules::V7, + &room_create_event, + &fetch_state, + ) + .await + .unwrap_err(); +} + +#[tokio::test] +async fn knock_after_join() { + let _guard = init_subscriber(); + + let incoming_event = to_pdu_event( + "HELLO", + charlie(), + TimelineEventType::RoomMember, + Some(charlie().as_str()), + to_raw_json_value(&RoomMemberEventContent::new(MembershipState::Knock)).unwrap(), + &["CREATE", "IJR", "IPOWER"], + &["IPOWER"], + ); + + let mut init_events = INITIAL_EVENTS(); + *init_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 = TestStateMap::new(&init_events); + let fetch_state = auth_events.fetch_state_fn(); + let room_create_event = auth_events.room_create_event(); + + // User cannot knock after being invited. + check_room_member( + &RoomMemberEvent::new(incoming_event), + &AuthorizationRules::V7, + &room_create_event, + &fetch_state, + ) + .await + .unwrap_err(); +} diff --git a/src/core/matrix/state_res/event_auth/tests.rs b/src/core/matrix/state_res/event_auth/tests.rs new file mode 100644 index 00000000..1e746ffc --- /dev/null +++ b/src/core/matrix/state_res/event_auth/tests.rs @@ -0,0 +1,1054 @@ +use ruma::{ + events::{ + TimelineEventType, + room::{ + aliases::RoomAliasesEventContent, message::RoomMessageEventContent, + redaction::RoomRedactionEventContent, + }, + }, + int, owned_event_id, owned_room_alias_id, owned_room_id, + room_version_rules::{AuthorizationRules, RoomVersionRules}, + uint, user_id, +}; +use serde_json::{json, value::to_raw_value as to_raw_json_value}; + +mod room_power_levels; + +use self::room_power_levels::default_room_power_levels; +use super::{ + check_room_create, check_room_redaction, check_state_dependent_auth_rules, + check_state_independent_auth_rules, + events::{RoomCreateEvent, RoomPowerLevelsEvent}, + test_utils::{ + INITIAL_EVENTS, INITIAL_HYDRA_EVENTS, TestStateMap, alice, charlie, ella, event_id, + init_subscriber, member_content_join, not_found, room_create_hydra_pdu_event, room_id, + room_redaction_pdu_event, room_third_party_invite, to_hydra_pdu_event, to_init_pdu_event, + to_pdu_event, + }, +}; +use crate::matrix::{EventHash, PduEvent, StateKey}; + +#[test] +fn valid_room_create() { + // Minimal fields valid for room v1. + let content = json!({ + "creator": alice(), + }); + let event = to_init_pdu_event( + "CREATE", + alice(), + TimelineEventType::RoomCreate, + Some(""), + to_raw_json_value(&content).unwrap(), + ); + check_room_create(&RoomCreateEvent::new(event), &AuthorizationRules::V1).unwrap(); + + // Same, with room version. + let content = json!({ + "creator": alice(), + "room_version": "2", + }); + let event = to_init_pdu_event( + "CREATE", + alice(), + TimelineEventType::RoomCreate, + Some(""), + to_raw_json_value(&content).unwrap(), + ); + check_room_create(&RoomCreateEvent::new(event), &AuthorizationRules::V1).unwrap(); + + // With a room version that does not need the creator. + let content = json!({ + "room_version": "11", + }); + let event = to_init_pdu_event( + "CREATE", + alice(), + TimelineEventType::RoomCreate, + Some(""), + to_raw_json_value(&content).unwrap(), + ); + check_room_create(&RoomCreateEvent::new(event), &AuthorizationRules::V11).unwrap(); + + // Check various contents that might not match the definition of `m.room.create` + // in the spec, to ensure that we only care about a few fields. + let contents_to_check = vec![ + // With an invalid predecessor, but we don't care about it. Inspired by a real-life + // example. + json!({ + "room_version": "11", + "predecessor": "!XPoLiaavxVgyMSiRwK:localhost", + }), + // With an invalid type, but we don't care about it. + json!({ + "room_version": "11", + "type": true, + }), + ]; + + for content in contents_to_check { + let event = to_init_pdu_event( + "CREATE", + alice(), + TimelineEventType::RoomCreate, + Some(""), + to_raw_json_value(&content).unwrap(), + ); + check_room_create(&RoomCreateEvent::new(event), &AuthorizationRules::V11).unwrap(); + } + + // Check `additional_creators` is allowed to contain invalid user IDs if the + // room version doesn't acknowledge them. + let content = json!({ + "room_version": "11", + "additional_creators": ["@::example.org"] + }); + let event = to_init_pdu_event( + "CREATE", + alice(), + TimelineEventType::RoomCreate, + Some(""), + to_raw_json_value(&content).unwrap(), + ); + check_room_create(&RoomCreateEvent::new(event), &AuthorizationRules::V11).unwrap(); + + // Check `additional_creators` only contains valid user IDs. + let content = json!({ + "room_version": "12", + "additional_creators": ["@alice:example.org"] + }); + let event = + room_create_hydra_pdu_event("CREATE", alice(), to_raw_json_value(&content).unwrap()); + check_room_create(&RoomCreateEvent::new(event), &AuthorizationRules::V12).unwrap(); +} + +#[test] +fn invalid_room_create() { + // With a prev event. + let content = json!({ + "creator": alice(), + }); + let event = to_pdu_event( + "CREATE", + alice(), + TimelineEventType::RoomCreate, + Some(""), + to_raw_json_value(&content).unwrap(), + &["OTHER_CREATE"], + &["OTHER_CREATE"], + ); + check_room_create(&RoomCreateEvent::new(event), &AuthorizationRules::V1).unwrap_err(); + + // Sender with a different domain. + let creator = user_id!("@bot:bar"); + let content = json!({ + "creator": creator, + }); + let event = to_init_pdu_event( + "CREATE", + creator, + TimelineEventType::RoomCreate, + Some(""), + to_raw_json_value(&content).unwrap(), + ); + check_room_create(&RoomCreateEvent::new(event), &AuthorizationRules::V1).unwrap_err(); + + // No creator in v1. + let content = json!({}); + let event = to_init_pdu_event( + "CREATE", + alice(), + TimelineEventType::RoomCreate, + Some(""), + to_raw_json_value(&content).unwrap(), + ); + check_room_create(&RoomCreateEvent::new(event), &AuthorizationRules::V1).unwrap_err(); +} + +#[test] +fn redact_higher_power_level() { + let _guard = init_subscriber(); + + let incoming_event = room_redaction_pdu_event( + "HELLO", + charlie(), + owned_event_id!("$redacted_event:other.server"), + to_raw_json_value(&RoomRedactionEventContent::new_v1()).unwrap(), + &["CREATE", "IMA", "IPOWER"], + &["IPOWER"], + ); + + let room_power_levels_event = Some(default_room_power_levels()); + + // Cannot redact if redact level is higher than user's. + check_room_redaction( + &incoming_event, + room_power_levels_event.as_ref(), + &AuthorizationRules::V1, + int!(0).into(), + ) + .unwrap_err(); +} + +#[test] +fn redact_same_power_level() { + let _guard = init_subscriber(); + + let incoming_event = room_redaction_pdu_event( + "HELLO", + charlie(), + owned_event_id!("$redacted_event:other.server"), + to_raw_json_value(&RoomRedactionEventContent::new_v1()).unwrap(), + &["CREATE", "IMA", "IPOWER"], + &["IPOWER"], + ); + + let room_power_levels_event = Some(RoomPowerLevelsEvent::new(to_pdu_event( + "IPOWER", + alice(), + TimelineEventType::RoomPowerLevels, + Some(""), + to_raw_json_value(&json!({ "users": { alice(): 100, charlie(): 50 } })).unwrap(), + &["CREATE", "IMA"], + &["IMA"], + ))); + + // Can redact if redact level is same as user's. + check_room_redaction( + &incoming_event, + room_power_levels_event.as_ref(), + &AuthorizationRules::V1, + int!(50).into(), + ) + .unwrap(); +} + +#[test] +fn redact_same_server() { + let _guard = init_subscriber(); + + let incoming_event = room_redaction_pdu_event( + "HELLO", + charlie(), + event_id("redacted_event"), + to_raw_json_value(&RoomRedactionEventContent::new_v1()).unwrap(), + &["CREATE", "IMA", "IPOWER"], + &["IPOWER"], + ); + + let room_power_levels_event = Some(default_room_power_levels()); + + // Can redact if redact level is same as user's. + check_room_redaction( + &incoming_event, + room_power_levels_event.as_ref(), + &AuthorizationRules::V1, + int!(0).into(), + ) + .unwrap(); +} + +#[tokio::test] +async fn missing_room_create_in_state() { + let _guard = init_subscriber(); + + let incoming_event = to_pdu_event( + "HELLO", + alice(), + TimelineEventType::RoomMessage, + None, + to_raw_json_value(&RoomMessageEventContent::text_plain("Hi!")).unwrap(), + &["IMA", "IPOWER"], + &["IPOWER"], + ); + + let mut init_events = INITIAL_EVENTS(); + init_events.remove(&event_id("CREATE")); + + // Cannot accept event if no `m.room.create` in state. + check_state_independent_auth_rules( + &RoomVersionRules::V6, + &incoming_event, + &async |event_id| { + init_events + .get(&event_id) + .cloned() + .ok_or_else(not_found) + }, + ) + .await + .unwrap_err(); +} + +#[tokio::test] +async fn reject_missing_room_create_auth_events() { + let _guard = init_subscriber(); + + let incoming_event = to_pdu_event( + "HELLO", + alice(), + TimelineEventType::RoomMessage, + None, + to_raw_json_value(&RoomMessageEventContent::text_plain("Hi!")).unwrap(), + &["IMA", "IPOWER"], + &["IPOWER"], + ); + + let init_events = INITIAL_EVENTS(); + + // Cannot accept event if no `m.room.create` in auth events. + check_state_independent_auth_rules( + &RoomVersionRules::V6, + &incoming_event, + &async |event_id| { + init_events + .get(&event_id) + .cloned() + .ok_or_else(not_found) + }, + ) + .await + .unwrap_err(); +} + +#[tokio::test] +async fn no_federate_different_server() { + let _guard = init_subscriber(); + + let sender = user_id!("@aya:other.server"); + let incoming_event = to_pdu_event( + "AYA_JOIN", + sender, + TimelineEventType::RoomMember, + Some(sender.as_str()), + member_content_join(), + &["CREATE", "IJR", "IPOWER"], + &["IMB"], + ); + + let mut init_events = INITIAL_EVENTS(); + *init_events.get_mut(&event_id("CREATE")).unwrap() = to_init_pdu_event( + "CREATE", + alice(), + TimelineEventType::RoomCreate, + Some(""), + to_raw_json_value(&json!({ + "creator": alice(), + "m.federate": false, + })) + .unwrap(), + ); + + let auth_events = TestStateMap::new(&init_events); + let fetch_state = auth_events.fetch_state_fn(); + + // Cannot accept event if not federating and different server. + check_state_dependent_auth_rules(&RoomVersionRules::V6, &incoming_event, &fetch_state) + .await + .unwrap_err(); +} + +#[tokio::test] +async fn no_federate_same_server() { + let _guard = init_subscriber(); + + let sender = user_id!("@aya:foo"); + let incoming_event = to_pdu_event( + "AYA_JOIN", + sender, + TimelineEventType::RoomMember, + Some(sender.as_str()), + member_content_join(), + &["CREATE", "IJR", "IPOWER"], + &["IMB"], + ); + + let mut init_events = INITIAL_EVENTS(); + *init_events.get_mut(&event_id("CREATE")).unwrap() = to_init_pdu_event( + "CREATE", + alice(), + TimelineEventType::RoomCreate, + Some(""), + to_raw_json_value(&json!({ + "creator": alice(), + "m.federate": false, + })) + .unwrap(), + ); + + let auth_events = TestStateMap::new(&init_events); + let fetch_state = auth_events.fetch_state_fn(); + + // Accept event if not federating and same server. + check_state_dependent_auth_rules(&RoomVersionRules::V6, &incoming_event, &fetch_state) + .await + .unwrap(); +} + +#[tokio::test] +async fn room_aliases_no_state_key() { + let _guard = init_subscriber(); + + let incoming_event = to_pdu_event( + "ALIASES", + alice(), + TimelineEventType::RoomAliases, + None, + to_raw_json_value(&RoomAliasesEventContent::new(vec![ + owned_room_alias_id!("#room:foo"), + owned_room_alias_id!("#room_alt:foo"), + ])) + .unwrap(), + &["CREATE", "IJR", "IPOWER"], + &["IMB"], + ); + + let init_events = INITIAL_EVENTS(); + let auth_events = TestStateMap::new(&init_events); + let fetch_state = auth_events.fetch_state_fn(); + + // Cannot accept `m.room.aliases` without state key. + check_state_dependent_auth_rules(&RoomVersionRules::V3, &incoming_event, &fetch_state) + .await + .unwrap_err(); + + // `m.room.aliases` is not checked since v6. + check_state_dependent_auth_rules(&RoomVersionRules::V8, &incoming_event, &fetch_state) + .await + .unwrap(); +} + +#[tokio::test] +async fn room_aliases_other_server() { + let _guard = init_subscriber(); + + let incoming_event = to_pdu_event( + "ALIASES", + alice(), + TimelineEventType::RoomAliases, + Some("bar"), + to_raw_json_value(&RoomAliasesEventContent::new(vec![ + owned_room_alias_id!("#room:bar"), + owned_room_alias_id!("#room_alt:bar"), + ])) + .unwrap(), + &["CREATE", "IJR", "IPOWER"], + &["IMB"], + ); + + let init_events = INITIAL_EVENTS(); + let auth_events = TestStateMap::new(&init_events); + let fetch_state = auth_events.fetch_state_fn(); + + // Cannot accept `m.room.aliases` with different server name than sender. + check_state_dependent_auth_rules(&RoomVersionRules::V3, &incoming_event, &fetch_state) + .await + .unwrap_err(); + + // `m.room.aliases` is not checked since v6. + check_state_dependent_auth_rules(&RoomVersionRules::V8, &incoming_event, &fetch_state) + .await + .unwrap(); +} + +#[tokio::test] +async fn room_aliases_same_server() { + let _guard = init_subscriber(); + + let incoming_event = to_pdu_event( + "ALIASES", + alice(), + TimelineEventType::RoomAliases, + Some("foo"), + to_raw_json_value(&RoomAliasesEventContent::new(vec![ + owned_room_alias_id!("#room:foo"), + owned_room_alias_id!("#room_alt:foo"), + ])) + .unwrap(), + &["CREATE", "IJR", "IPOWER"], + &["IMB"], + ); + + let init_events = INITIAL_EVENTS(); + let auth_events = TestStateMap::new(&init_events); + let fetch_state = auth_events.fetch_state_fn(); + + // Accept `m.room.aliases` with same server name as sender. + check_state_dependent_auth_rules(&RoomVersionRules::V3, &incoming_event, &fetch_state) + .await + .unwrap(); + + // `m.room.aliases` is not checked since v6. + check_state_dependent_auth_rules(&RoomVersionRules::V8, &incoming_event, &fetch_state) + .await + .unwrap(); +} + +#[tokio::test] +async fn sender_not_in_room() { + let _guard = init_subscriber(); + + let incoming_event = to_pdu_event( + "HELLO", + ella(), + TimelineEventType::RoomMessage, + None, + to_raw_json_value(&RoomMessageEventContent::text_plain("Hi!")).unwrap(), + &["IMA", "IPOWER", "CREATE"], + &["IPOWER"], + ); + + let init_events = INITIAL_EVENTS(); + let auth_events = TestStateMap::new(&init_events); + let fetch_state = auth_events.fetch_state_fn(); + + // Cannot accept event if user not in room. + check_state_dependent_auth_rules(&RoomVersionRules::V6, &incoming_event, &fetch_state) + .await + .unwrap_err(); +} + +#[tokio::test] +async fn room_third_party_invite_not_enough_power() { + let _guard = init_subscriber(); + + let incoming_event = room_third_party_invite(charlie()); + + let mut init_events = INITIAL_EVENTS(); + *init_events.get_mut(&event_id("IPOWER")).unwrap() = to_pdu_event( + "IPOWER", + alice(), + TimelineEventType::RoomPowerLevels, + Some(""), + to_raw_json_value(&json!({ + "users": { alice(): 100 }, + "invite": 50, + })) + .unwrap(), + &["CREATE", "IMA"], + &["IMA"], + ); + + let auth_events = TestStateMap::new(&init_events); + let fetch_state = auth_events.fetch_state_fn(); + + // Cannot accept `m.room.third_party_invite` if not enough power. + check_state_dependent_auth_rules(&RoomVersionRules::V6, &incoming_event, &fetch_state) + .await + .unwrap_err(); +} + +#[tokio::test] +async fn room_third_party_invite_with_enough_power() { + let _guard = init_subscriber(); + + let incoming_event = room_third_party_invite(charlie()); + + let init_events = INITIAL_EVENTS(); + let auth_events = TestStateMap::new(&init_events); + let fetch_state = auth_events.fetch_state_fn(); + + // Accept `m.room.third_party_invite` if enough power. + check_state_dependent_auth_rules(&RoomVersionRules::V6, &incoming_event, &fetch_state) + .await + .unwrap(); +} + +#[tokio::test] +async fn event_type_not_enough_power() { + let _guard = init_subscriber(); + + let incoming_event = to_pdu_event( + "HELLO", + charlie(), + TimelineEventType::RoomMessage, + None, + to_raw_json_value(&RoomMessageEventContent::text_plain("Hi!")).unwrap(), + &["CREATE", "IMA", "IPOWER"], + &["IPOWER"], + ); + + let mut init_events = INITIAL_EVENTS(); + *init_events.get_mut(&event_id("IPOWER")).unwrap() = to_pdu_event( + "IPOWER", + alice(), + TimelineEventType::RoomPowerLevels, + Some(""), + to_raw_json_value(&json!({ + "users": { alice(): 100 }, + "events": { + "m.room.message": "50", + }, + })) + .unwrap(), + &["CREATE", "IMA"], + &["IMA"], + ); + + let auth_events = TestStateMap::new(&init_events); + let fetch_state = auth_events.fetch_state_fn(); + + // Cannot send event if not enough power for the event's type. + check_state_dependent_auth_rules(&RoomVersionRules::V6, &incoming_event, &fetch_state) + .await + .unwrap_err(); +} + +#[tokio::test] +async fn user_id_state_key_not_sender() { + let _guard = init_subscriber(); + + let incoming_event = to_pdu_event( + "HELLO", + alice(), + "dev.ruma.fake_state_event".into(), + Some(ella().as_str()), + to_raw_json_value(&json!({})).unwrap(), + &["IMA", "IPOWER", "CREATE"], + &["IPOWER"], + ); + + let init_events = INITIAL_EVENTS(); + let auth_events = TestStateMap::new(&init_events); + let fetch_state = auth_events.fetch_state_fn(); + + // Cannot send state event with a user ID as a state key that doesn't match the + // sender. + check_state_dependent_auth_rules(&RoomVersionRules::V6, &incoming_event, &fetch_state) + .await + .unwrap_err(); +} + +#[tokio::test] +async fn user_id_state_key_is_sender() { + let _guard = init_subscriber(); + + let incoming_event = to_pdu_event( + "HELLO", + alice(), + "dev.ruma.fake_state_event".into(), + Some(alice().as_str()), + to_raw_json_value(&json!({})).unwrap(), + &["IMA", "IPOWER", "CREATE"], + &["IPOWER"], + ); + + let init_events = INITIAL_EVENTS(); + let auth_events = TestStateMap::new(&init_events); + let fetch_state = auth_events.fetch_state_fn(); + + // Can send state event with a user ID as a state key that matches the sender. + check_state_dependent_auth_rules(&RoomVersionRules::V6, &incoming_event, &fetch_state) + .await + .unwrap(); +} + +#[tokio::test] +async fn auth_event_in_different_room() { + let _guard = init_subscriber(); + + let incoming_event = to_pdu_event( + "HELLO", + alice(), + TimelineEventType::RoomMessage, + None, + to_raw_json_value(&RoomMessageEventContent::text_plain("Hi!")).unwrap(), + &["CREATE", "IMA", "IPOWER"], + &["IPOWER"], + ); + + let mut init_events = INITIAL_EVENTS(); + let power_level = PduEvent { + event_id: event_id("IPOWER"), + room_id: owned_room_id!("!wrongroom:foo"), + sender: alice().to_owned(), + origin: None, + origin_server_ts: uint!(3), + state_key: Some(StateKey::new()), + kind: TimelineEventType::RoomPowerLevels, + content: to_raw_json_value(&json!({ "users": { alice(): 100 } })).unwrap(), + redacts: None, + unsigned: None, + auth_events: vec![event_id("CREATE"), event_id("IMA")], + prev_events: vec![event_id("IMA")], + depth: uint!(0), + hashes: EventHash::default(), + signatures: None, + rejected: false, + }; + init_events + .insert(power_level.event_id.clone(), power_level) + .unwrap(); + + // Cannot accept with auth event in different room. + check_state_independent_auth_rules( + &RoomVersionRules::V6, + &incoming_event, + &async |event_id| { + init_events + .get(&event_id) + .cloned() + .ok_or_else(not_found) + }, + ) + .await + .unwrap_err(); +} + +#[tokio::test] +async fn duplicate_auth_event_type() { + let _guard = init_subscriber(); + + let incoming_event = to_pdu_event( + "HELLO", + alice(), + TimelineEventType::RoomMessage, + None, + to_raw_json_value(&RoomMessageEventContent::text_plain("Hi!")).unwrap(), + &["CREATE", "IMA", "IMA2", "IPOWER"], + &["IPOWER"], + ); + + let mut init_events = INITIAL_EVENTS(); + init_events.insert( + event_id("IMA2"), + to_pdu_event( + "IMA2", + alice(), + TimelineEventType::RoomMember, + Some(alice().as_str()), + member_content_join(), + &["CREATE", "IMA"], + &["IMA"], + ), + ); + + // Cannot accept with two auth events with same (type, state_key) pair. + check_state_independent_auth_rules( + &RoomVersionRules::V6, + &incoming_event, + &async |event_id| { + init_events + .get(&event_id) + .cloned() + .ok_or_else(not_found) + }, + ) + .await + .unwrap_err(); +} + +#[tokio::test] +async fn unexpected_auth_event_type() { + let _guard = init_subscriber(); + + let incoming_event = to_pdu_event( + "HELLO", + alice(), + TimelineEventType::RoomMessage, + None, + to_raw_json_value(&RoomMessageEventContent::text_plain("Hi!")).unwrap(), + &["CREATE", "IMA", "IPOWER", "IMC"], + &["IMC"], + ); + + let mut init_events = INITIAL_EVENTS(); + init_events.insert( + event_id("IMC"), + to_pdu_event( + "IMC", + charlie(), + TimelineEventType::RoomMember, + Some(charlie().as_str()), + member_content_join(), + &["CREATE", "IMA", "IPOWER"], + &["IPOWER"], + ), + ); + + // Cannot accept with auth event with unexpected (type, state_key) pair. + check_state_independent_auth_rules( + &RoomVersionRules::V6, + &incoming_event, + &async |event_id| { + init_events + .get(&event_id) + .cloned() + .ok_or_else(not_found) + }, + ) + .await + .unwrap_err(); +} + +#[tokio::test] +async fn rejected_auth_event() { + let _guard = init_subscriber(); + + let incoming_event = to_pdu_event( + "HELLO", + alice(), + TimelineEventType::RoomMessage, + None, + to_raw_json_value(&RoomMessageEventContent::text_plain("Hi!")).unwrap(), + &["CREATE", "IMA", "IPOWER"], + &["IPOWER"], + ); + + let mut init_events = INITIAL_EVENTS(); + let power_level = PduEvent { + event_id: event_id("IPOWER"), + room_id: room_id().to_owned(), + sender: alice().to_owned(), + origin: None, + origin_server_ts: uint!(3), + state_key: Some(StateKey::new()), + kind: TimelineEventType::RoomPowerLevels, + content: to_raw_json_value(&json!({ "users": { alice(): 100 } })).unwrap(), + redacts: None, + unsigned: None, + auth_events: vec![event_id("CREATE"), event_id("IMA")], + prev_events: vec![event_id("IMA")], + depth: uint!(0), + hashes: EventHash::default(), + signatures: None, + rejected: true, + }; + init_events + .insert(power_level.event_id.clone(), power_level) + .unwrap(); + + // Cannot accept with auth event that was rejected. + check_state_independent_auth_rules( + &RoomVersionRules::V6, + &incoming_event, + &async |event_id| { + init_events + .get(&event_id) + .cloned() + .ok_or_else(not_found) + }, + ) + .await + .unwrap_err(); +} + +#[test] +fn room_create_with_allowed_or_rejected_room_id() { + // v11, room_id is required. + let v11_content = json!({ + "room_version": "11", + }); + + let event_with_room_id = to_init_pdu_event( + "CREATE", + alice(), + TimelineEventType::RoomCreate, + Some(""), + to_raw_json_value(&v11_content).unwrap(), + ); + check_room_create(&RoomCreateEvent::new(event_with_room_id), &AuthorizationRules::V11) + .unwrap(); + + let event_no_room_id = + room_create_hydra_pdu_event("CREATE", alice(), to_raw_json_value(&v11_content).unwrap()); + check_room_create(&RoomCreateEvent::new(event_no_room_id), &AuthorizationRules::V11) + .unwrap_err(); + + // `org.matrix.hydra.11`, room_id is rejected. + let hydra_content = json!({ + "room_version": "12", + }); + + let event_with_room_id = to_init_pdu_event( + "CREATE", + alice(), + TimelineEventType::RoomCreate, + Some(""), + to_raw_json_value(&hydra_content).unwrap(), + ); + check_room_create(&RoomCreateEvent::new(event_with_room_id), &AuthorizationRules::V12) + .unwrap_err(); + + let event_no_room_id = room_create_hydra_pdu_event( + "CREATE", + alice(), + to_raw_json_value(&hydra_content).unwrap(), + ); + check_room_create(&RoomCreateEvent::new(event_no_room_id), &AuthorizationRules::V12).unwrap(); +} + +#[tokio::test] +async fn event_without_room_id() { + let _guard = init_subscriber(); + + let incoming_event = PduEvent { + event_id: owned_event_id!("$HELLO"), + room_id: owned_room_id!("!IGNORED"), + sender: alice().to_owned(), + origin: None, + origin_server_ts: uint!(3), + state_key: None, + kind: TimelineEventType::RoomMessage, + content: to_raw_json_value(&RoomMessageEventContent::text_plain("Hi!")).unwrap(), + redacts: None, + unsigned: None, + auth_events: vec![ + owned_event_id!("$CREATE"), + owned_event_id!("$IMA"), + owned_event_id!("$IPOWER"), + ], + prev_events: vec![owned_event_id!("$IPOWER")], + depth: uint!(0), + hashes: EventHash::default(), + signatures: None, + rejected: false, + }; + + let init_events = INITIAL_HYDRA_EVENTS(); + + // Cannot accept event without room ID. + check_state_independent_auth_rules( + &RoomVersionRules::V11, + &incoming_event, + &async |event_id| { + init_events + .get(&event_id) + .cloned() + .ok_or_else(not_found) + }, + ) + .await + .unwrap_err(); +} + +#[tokio::test] +async fn allow_missing_room_create_auth_events() { + let _guard = init_subscriber(); + + let incoming_event = to_hydra_pdu_event( + "HELLO", + alice(), + TimelineEventType::RoomMessage, + None, + to_raw_json_value(&RoomMessageEventContent::text_plain("Hi!")).unwrap(), + &["IMA", "IPOWER"], + &["IPOWER"], + ); + + let init_events = INITIAL_HYDRA_EVENTS(); + + // Accept event if no `m.room.create` in auth events. + check_state_independent_auth_rules( + &RoomVersionRules::V12, + &incoming_event, + &async |event_id| { + init_events + .get(&event_id) + .cloned() + .ok_or_else(not_found) + }, + ) + .await + .unwrap(); +} + +#[tokio::test] +async fn reject_room_create_in_auth_events() { + let _guard = init_subscriber(); + + let incoming_event = to_hydra_pdu_event( + "HELLO", + alice(), + TimelineEventType::RoomMessage, + None, + to_raw_json_value(&RoomMessageEventContent::text_plain("Hi!")).unwrap(), + &["CREATE", "IMA", "IPOWER"], + &["IPOWER"], + ); + + let init_events = INITIAL_HYDRA_EVENTS(); + + // Reject event if `m.room.create` in auth events. + check_state_independent_auth_rules( + &RoomVersionRules::V12, + &incoming_event, + &async |event_id| { + init_events + .get(&event_id) + .cloned() + .ok_or_else(not_found) + }, + ) + .await + .unwrap_err(); +} + +#[tokio::test] +async fn missing_room_create_in_fetch_event() { + let _guard = init_subscriber(); + + let incoming_event = to_hydra_pdu_event( + "HELLO", + alice(), + TimelineEventType::RoomMessage, + None, + to_raw_json_value(&RoomMessageEventContent::text_plain("Hi!")).unwrap(), + &["IMA", "IPOWER"], + &["IPOWER"], + ); + + let mut init_events = INITIAL_HYDRA_EVENTS(); + init_events + .remove(&owned_event_id!("$CREATE")) + .unwrap(); + + // Reject event if `m.room.create` can't be found. + check_state_independent_auth_rules( + &RoomVersionRules::V12, + &incoming_event, + &async |event_id| { + init_events + .get(&event_id) + .cloned() + .ok_or_else(not_found) + }, + ) + .await + .unwrap_err(); +} + +#[tokio::test] +async fn rejected_room_create_in_fetch_event() { + let _guard = init_subscriber(); + + let incoming_event = to_hydra_pdu_event( + "HELLO", + alice(), + TimelineEventType::RoomMessage, + None, + to_raw_json_value(&RoomMessageEventContent::text_plain("Hi!")).unwrap(), + &["IMA", "IPOWER"], + &["IPOWER"], + ); + + let mut init_events = INITIAL_HYDRA_EVENTS(); + let create_event_id = owned_event_id!("$CREATE"); + let mut create_event = init_events.remove(&create_event_id).unwrap(); + create_event.rejected = true; + init_events.insert(create_event_id, create_event); + + // Reject event if `m.room.create` was rejected. + check_state_independent_auth_rules( + &RoomVersionRules::V12, + &incoming_event, + &async |event_id| { + init_events + .get(&event_id) + .cloned() + .ok_or_else(not_found) + }, + ) + .await + .unwrap_err(); +} diff --git a/src/core/matrix/state_res/event_auth/tests/room_power_levels.rs b/src/core/matrix/state_res/event_auth/tests/room_power_levels.rs new file mode 100644 index 00000000..712d1213 --- /dev/null +++ b/src/core/matrix/state_res/event_auth/tests/room_power_levels.rs @@ -0,0 +1,1274 @@ +use std::iter; + +use ruma::{ + events::{TimelineEventType, room::power_levels::UserPowerLevel}, + int, + room_version_rules::AuthorizationRules, +}; +use serde_json::{ + Value as JsonValue, json, + value::{Map as JsonMap, to_raw_value as to_raw_json_value}, +}; + +use super::{ + super::{ + check_room_power_levels, + events::RoomPowerLevelsEvent, + test_utils::{alice, bob, init_subscriber, to_pdu_event, zara}, + }, + PduEvent, +}; +use crate::{extract_variant, info}; + +/// The default `m.room.power_levels` event when creating a public room. +pub(super) fn default_room_power_levels() -> RoomPowerLevelsEvent { + RoomPowerLevelsEvent::new(to_pdu_event( + "IPOWER", + alice(), + TimelineEventType::RoomPowerLevels, + Some(""), + to_raw_json_value(&json!({ "users": { alice(): 100 } })).unwrap(), + &["CREATE", "IMA"], + &["IMA"], + )) +} + +#[test] +fn not_int_or_string_int_in_content() { + let _guard = init_subscriber(); + + let original_content = json!({ + "users": { alice(): 100 }, + }); + let original_content_object = extract_variant!(original_content, JsonValue::Object).unwrap(); + + let current_room_power_levels_event = Some(default_room_power_levels()); + + let int_fields = &[ + "users_default", + "events_default", + "state_default", + "ban", + "redact", + "kick", + "invite", + ]; + + // Tuples of (is_string, is_int) booleans. + let combinations = &[(true, false), (true, true), (false, true)]; + + for field in int_fields { + for (is_string, is_int) in combinations { + info!(?field, ?is_string, ?is_int, "checking field"); + + let value = match (is_string, is_int) { + | (true, false) => "foo".into(), + | (true, true) => "50".into(), + | (false, true) => 50.into(), + | _ => unreachable!(), + }; + + let mut content_object = original_content_object.clone(); + content_object.insert((*field).to_owned(), value); + + let incoming_event = to_pdu_event( + "IPOWER2", + alice(), + TimelineEventType::RoomPowerLevels, + Some(""), + to_raw_json_value(&content_object).unwrap(), + &["CREATE", "IMA", "IPOWER"], + &["IPOWER"], + ); + + // String that is not a number is not accepted. + let v6_result = check_room_power_levels( + &RoomPowerLevelsEvent::new(incoming_event.clone()), + current_room_power_levels_event.as_ref(), + &AuthorizationRules::V6, + int!(100), + iter::empty(), + ); + + if *is_int { + v6_result.unwrap(); + } else { + v6_result.unwrap_err(); + } + + // String is not accepted. + let v10_result = check_room_power_levels( + &RoomPowerLevelsEvent::new(incoming_event), + current_room_power_levels_event.as_ref(), + &AuthorizationRules::V10, + int!(100), + iter::empty(), + ); + + if *is_string { + v10_result.unwrap_err(); + } else { + v10_result.unwrap(); + } + } + } +} + +#[test] +fn not_int_or_string_int_in_events() { + let _guard = init_subscriber(); + + let original_content = json!({ + "users": { alice(): 100 }, + }); + let original_content_object = extract_variant!(original_content, JsonValue::Object).unwrap(); + + let current_room_power_levels_event = Some(default_room_power_levels()); + + // Tuples of (is_string, is_int) booleans. + let combinations = &[(true, false), (true, true), (false, true)]; + + for (is_string, is_int) in combinations { + info!(?is_string, ?is_int, "checking field"); + + let value = match (is_string, is_int) { + | (true, false) => "foo".into(), + | (true, true) => "50".into(), + | (false, true) => 50.into(), + | _ => unreachable!(), + }; + let mut events_object = JsonMap::new(); + events_object.insert("bar".to_owned(), value); + + let mut content_object = original_content_object.clone(); + content_object.insert("events".to_owned(), events_object.into()); + + let incoming_event = to_pdu_event( + "IPOWER2", + alice(), + TimelineEventType::RoomPowerLevels, + Some(""), + to_raw_json_value(&content_object).unwrap(), + &["CREATE", "IMA", "IPOWER"], + &["IPOWER"], + ); + + // String that is not a number is not accepted. + let v6_result = check_room_power_levels( + &RoomPowerLevelsEvent::new(incoming_event.clone()), + current_room_power_levels_event.as_ref(), + &AuthorizationRules::V6, + int!(100), + iter::empty(), + ); + + if *is_int { + v6_result.unwrap(); + } else { + v6_result.unwrap_err(); + } + + // String is not accepted. + let v10_result = check_room_power_levels( + &RoomPowerLevelsEvent::new(incoming_event), + current_room_power_levels_event.as_ref(), + &AuthorizationRules::V10, + int!(100), + iter::empty(), + ); + + if *is_string { + v10_result.unwrap_err(); + } else { + v10_result.unwrap(); + } + } +} + +#[test] +fn not_int_or_string_int_in_notifications() { + let _guard = init_subscriber(); + + let original_content = json!({ + "users": { alice(): 100 }, + }); + let original_content_object = extract_variant!(original_content, JsonValue::Object).unwrap(); + + let current_room_power_levels_event = Some(default_room_power_levels()); + + // Tuples of (is_string, is_int) booleans. + let combinations = &[(true, false), (true, true), (false, true)]; + + for (is_string, is_int) in combinations { + info!(?is_string, ?is_int, "checking field"); + + let value = match (is_string, is_int) { + | (true, false) => "foo".into(), + | (true, true) => "50".into(), + | (false, true) => 50.into(), + | _ => unreachable!(), + }; + let mut notifications_object = JsonMap::new(); + notifications_object.insert("room".to_owned(), value); + + let mut content_object = original_content_object.clone(); + content_object.insert("notifications".to_owned(), notifications_object.into()); + + let incoming_event = to_pdu_event( + "IPOWER2", + alice(), + TimelineEventType::RoomPowerLevels, + Some(""), + to_raw_json_value(&content_object).unwrap(), + &["CREATE", "IMA", "IPOWER"], + &["IPOWER"], + ); + + // String that is not a number is not accepted. + let v6_result = check_room_power_levels( + &RoomPowerLevelsEvent::new(incoming_event.clone()), + current_room_power_levels_event.as_ref(), + &AuthorizationRules::V6, + int!(100), + iter::empty(), + ); + + if *is_int { + v6_result.unwrap(); + } else { + v6_result.unwrap_err(); + } + + // String is not accepted. + let v10_result = check_room_power_levels( + &RoomPowerLevelsEvent::new(incoming_event), + current_room_power_levels_event.as_ref(), + &AuthorizationRules::V10, + int!(100), + iter::empty(), + ); + + if *is_string { + v10_result.unwrap_err(); + } else { + v10_result.unwrap(); + } + } +} + +#[test] +fn not_user_id_in_users() { + let _guard = init_subscriber(); + + let content = json!({ + "users": { + alice(): 100, + "spambot": -1, + }, + }); + let incoming_event = to_pdu_event( + "IPOWER2", + alice(), + TimelineEventType::RoomPowerLevels, + Some(""), + to_raw_json_value(&content).unwrap(), + &["CREATE", "IMA", "IPOWER"], + &["IPOWER"], + ); + + let current_room_power_levels_event = Some(default_room_power_levels()); + + // Key that is not a user ID is not accepted. + check_room_power_levels( + &RoomPowerLevelsEvent::new(incoming_event), + current_room_power_levels_event.as_ref(), + &AuthorizationRules::V6, + int!(100), + iter::empty(), + ) + .unwrap_err(); +} + +#[test] +fn not_int_or_string_int_in_users() { + let _guard = init_subscriber(); + + let original_content = json!({ + "users": { + alice(): 100, + }, + }); + let original_content_object = extract_variant!(original_content, JsonValue::Object).unwrap(); + + let current_room_power_levels_event = Some(default_room_power_levels()); + + // Tuples of (is_string, is_int) booleans. + let combinations = &[(true, false), (true, true), (false, true)]; + + for (is_string, is_int) in combinations { + info!(?is_string, ?is_int, "checking field"); + + let value = match (is_string, is_int) { + | (true, false) => "foo".into(), + | (true, true) => "50".into(), + | (false, true) => 50.into(), + | _ => unreachable!(), + }; + + let mut content_object = original_content_object.clone(); + let users_object = content_object + .get_mut("users") + .unwrap() + .as_object_mut() + .unwrap(); + + users_object.insert("@bar:baz".to_owned(), value); + + let incoming_event = to_pdu_event( + "IPOWER2", + alice(), + TimelineEventType::RoomPowerLevels, + Some(""), + to_raw_json_value(&content_object).unwrap(), + &["CREATE", "IMA", "IPOWER"], + &["IPOWER"], + ); + + // String that is not a number is not accepted. + let v6_result = check_room_power_levels( + &RoomPowerLevelsEvent::new(incoming_event.clone()), + current_room_power_levels_event.as_ref(), + &AuthorizationRules::V6, + int!(100), + iter::empty(), + ); + + if *is_int { + v6_result.unwrap(); + } else { + v6_result.unwrap_err(); + } + + // String is not accepted. + let v10_result = check_room_power_levels( + &RoomPowerLevelsEvent::new(incoming_event), + current_room_power_levels_event.as_ref(), + &AuthorizationRules::V10, + int!(100), + iter::empty(), + ); + + if *is_string { + v10_result.unwrap_err(); + } else { + v10_result.unwrap(); + } + } +} + +#[test] +fn first_power_levels_event() { + let _guard = init_subscriber(); + + let content = json!({ + "users": { + alice(): 100, + }, + }); + let incoming_event = to_pdu_event( + "IPOWER", + alice(), + TimelineEventType::RoomPowerLevels, + Some(""), + to_raw_json_value(&content).unwrap(), + &["CREATE", "IMA"], + &["IMA"], + ); + + let current_room_power_levels_event = None::>; + + // First power levels event is accepted. + check_room_power_levels( + &RoomPowerLevelsEvent::new(incoming_event), + current_room_power_levels_event.as_ref(), + &AuthorizationRules::V6, + int!(100), + iter::empty(), + ) + .unwrap(); +} + +#[test] +fn change_content_level_with_current_higher_power_level() { + let _guard = init_subscriber(); + + let original_content = json!({ + "users": { + alice(): 100, + bob(): 40, + }, + }); + let original_content_object = extract_variant!(original_content, JsonValue::Object).unwrap(); + + let int_fields = &[ + "users_default", + "events_default", + "state_default", + "ban", + "redact", + "kick", + "invite", + ]; + + for field in int_fields { + info!(?field, "checking field"); + + let current_value = 60; + let incoming_value = 40; + + let mut current_content_object = original_content_object.clone(); + current_content_object.insert((*field).to_owned(), current_value.into()); + + let current_room_power_levels_event = to_pdu_event( + "IPOWER", + alice(), + TimelineEventType::RoomPowerLevels, + Some(""), + to_raw_json_value(¤t_content_object).unwrap(), + &["CREATE", "IMA"], + &["IMA"], + ); + + let mut incoming_content_object = original_content_object.clone(); + incoming_content_object.insert((*field).to_owned(), incoming_value.into()); + let incoming_event = to_pdu_event( + "IPOWER2", + bob(), + TimelineEventType::RoomPowerLevels, + Some(""), + to_raw_json_value(&incoming_content_object).unwrap(), + &["CREATE", "IMA", "IPOWER"], + &["IPOWER"], + ); + + // Cannot change from a power level that is higher than the user. + check_room_power_levels( + &RoomPowerLevelsEvent::new(incoming_event), + Some(&RoomPowerLevelsEvent::new(current_room_power_levels_event)), + &AuthorizationRules::V6, + int!(40), + iter::empty(), + ) + .unwrap_err(); + } +} + +#[test] +fn change_content_level_with_new_higher_power_level() { + let _guard = init_subscriber(); + + let original_content = json!({ + "users": { + alice(): 100, + bob(): 40, + }, + }); + let original_content_object = extract_variant!(original_content, JsonValue::Object).unwrap(); + + let int_fields = &[ + "users_default", + "events_default", + "state_default", + "ban", + "redact", + "kick", + "invite", + ]; + + for field in int_fields { + info!(?field, "checking field"); + + let current_value = 40; + let incoming_value = 60; + + let mut current_content_object = original_content_object.clone(); + current_content_object.insert((*field).to_owned(), current_value.into()); + + let current_room_power_levels_event = to_pdu_event( + "IPOWER", + alice(), + TimelineEventType::RoomPowerLevels, + Some(""), + to_raw_json_value(¤t_content_object).unwrap(), + &["CREATE", "IMA"], + &["IMA"], + ); + + let mut incoming_content_object = original_content_object.clone(); + incoming_content_object.insert((*field).to_owned(), incoming_value.into()); + let incoming_event = to_pdu_event( + "IPOWER2", + bob(), + TimelineEventType::RoomPowerLevels, + Some(""), + to_raw_json_value(&incoming_content_object).unwrap(), + &["CREATE", "IMA", "IPOWER"], + &["IPOWER"], + ); + + // Cannot change to a power level that is higher than the user. + check_room_power_levels( + &RoomPowerLevelsEvent::new(incoming_event), + Some(&RoomPowerLevelsEvent::new(current_room_power_levels_event)), + &AuthorizationRules::V6, + int!(40), + iter::empty(), + ) + .unwrap_err(); + } +} + +#[test] +fn change_content_level_with_same_power_level() { + let _guard = init_subscriber(); + + let original_content = json!({ + "users": { + alice(): 100, + bob(): 40, + }, + }); + let original_content_object = extract_variant!(original_content, JsonValue::Object).unwrap(); + + let int_fields = &[ + "users_default", + "events_default", + "state_default", + "ban", + "redact", + "kick", + "invite", + ]; + + for field in int_fields { + info!(?field, "checking field"); + + let current_value = 30; + let incoming_value = 40; + + let mut current_content_object = original_content_object.clone(); + current_content_object.insert((*field).to_owned(), current_value.into()); + + let current_room_power_levels_event = to_pdu_event( + "IPOWER", + alice(), + TimelineEventType::RoomPowerLevels, + Some(""), + to_raw_json_value(¤t_content_object).unwrap(), + &["CREATE", "IMA"], + &["IMA"], + ); + + let mut incoming_content_object = original_content_object.clone(); + incoming_content_object.insert((*field).to_owned(), incoming_value.into()); + let incoming_event = to_pdu_event( + "IPOWER2", + bob(), + TimelineEventType::RoomPowerLevels, + Some(""), + to_raw_json_value(&incoming_content_object).unwrap(), + &["CREATE", "IMA", "IPOWER"], + &["IPOWER"], + ); + + // Can change a power level that is the same or lower than the user. + check_room_power_levels( + &RoomPowerLevelsEvent::new(incoming_event), + Some(&RoomPowerLevelsEvent::new(current_room_power_levels_event)), + &AuthorizationRules::V6, + int!(40), + iter::empty(), + ) + .unwrap(); + } +} + +#[test] +fn change_events_level_with_current_higher_power_level() { + let _guard = init_subscriber(); + + let current_content = json!({ + "users": { + alice(): 100, + bob(): 40, + }, + "events": { + "foo": 60, + }, + }); + let current_room_power_levels_event = to_pdu_event( + "IPOWER", + alice(), + TimelineEventType::RoomPowerLevels, + Some(""), + to_raw_json_value(¤t_content).unwrap(), + &["CREATE", "IMA"], + &["IMA"], + ); + + let incoming_content = json!({ + "users": { + alice(): 100, + bob(): 40, + }, + "events": { + "foo": 40, + }, + }); + let incoming_event = to_pdu_event( + "IPOWER2", + bob(), + TimelineEventType::RoomPowerLevels, + Some(""), + to_raw_json_value(&incoming_content).unwrap(), + &["CREATE", "IMA", "IPOWER"], + &["IPOWER"], + ); + + // Cannot change from a power level that is higher than the user. + check_room_power_levels( + &RoomPowerLevelsEvent::new(incoming_event), + Some(&RoomPowerLevelsEvent::new(current_room_power_levels_event)), + &AuthorizationRules::V6, + int!(40), + iter::empty(), + ) + .unwrap_err(); +} + +#[test] +fn change_events_level_with_new_higher_power_level() { + let _guard = init_subscriber(); + + let current_content = json!({ + "users": { + alice(): 100, + bob(): 40, + }, + "events": { + "foo": 30, + }, + }); + let current_room_power_levels_event = to_pdu_event( + "IPOWER", + alice(), + TimelineEventType::RoomPowerLevels, + Some(""), + to_raw_json_value(¤t_content).unwrap(), + &["CREATE", "IMA"], + &["IMA"], + ); + + let incoming_content = json!({ + "users": { + alice(): 100, + bob(): 40, + }, + "events": { + "foo": 60, + }, + }); + let incoming_event = to_pdu_event( + "IPOWER2", + bob(), + TimelineEventType::RoomPowerLevels, + Some(""), + to_raw_json_value(&incoming_content).unwrap(), + &["CREATE", "IMA", "IPOWER"], + &["IPOWER"], + ); + + // Cannot change to a power level that is higher than the user. + check_room_power_levels( + &RoomPowerLevelsEvent::new(incoming_event), + Some(&RoomPowerLevelsEvent::new(current_room_power_levels_event)), + &AuthorizationRules::V6, + int!(40), + iter::empty(), + ) + .unwrap_err(); +} + +#[test] +fn change_events_level_with_same_power_level() { + let _guard = init_subscriber(); + + let current_content = json!({ + "users": { + alice(): 100, + bob(): 40, + }, + "events": { + "foo": 40, + }, + }); + let current_room_power_levels_event = to_pdu_event( + "IPOWER", + alice(), + TimelineEventType::RoomPowerLevels, + Some(""), + to_raw_json_value(¤t_content).unwrap(), + &["CREATE", "IMA"], + &["IMA"], + ); + + let incoming_content = json!({ + "users": { + alice(): 100, + bob(): 40, + }, + "events": { + "foo": 10, + }, + }); + let incoming_event = to_pdu_event( + "IPOWER2", + bob(), + TimelineEventType::RoomPowerLevels, + Some(""), + to_raw_json_value(&incoming_content).unwrap(), + &["CREATE", "IMA", "IPOWER"], + &["IPOWER"], + ); + + // Can change a power level that is the same or lower than the user. + check_room_power_levels( + &RoomPowerLevelsEvent::new(incoming_event), + Some(&RoomPowerLevelsEvent::new(current_room_power_levels_event)), + &AuthorizationRules::V6, + int!(40), + iter::empty(), + ) + .unwrap(); +} + +#[test] +fn change_notifications_level_with_current_higher_power_level() { + let _guard = init_subscriber(); + + let current_content = json!({ + "users": { + alice(): 100, + bob(): 40, + }, + "notifications": { + "room": 60, + }, + }); + let current_room_power_levels_event = to_pdu_event( + "IPOWER", + alice(), + TimelineEventType::RoomPowerLevels, + Some(""), + to_raw_json_value(¤t_content).unwrap(), + &["CREATE", "IMA"], + &["IMA"], + ); + + let incoming_content = json!({ + "users": { + alice(): 100, + bob(): 40, + }, + "notifications": { + "room": 40, + }, + }); + let incoming_event = to_pdu_event( + "IPOWER2", + bob(), + TimelineEventType::RoomPowerLevels, + Some(""), + to_raw_json_value(&incoming_content).unwrap(), + &["CREATE", "IMA", "IPOWER"], + &["IPOWER"], + ); + + // Notifications are not checked before v6. + check_room_power_levels( + &RoomPowerLevelsEvent::new(incoming_event.clone()), + Some(&RoomPowerLevelsEvent::new(current_room_power_levels_event.clone())), + &AuthorizationRules::V3, + int!(40), + iter::empty(), + ) + .unwrap(); + + // Cannot change from a power level that is higher than the user. + check_room_power_levels( + &RoomPowerLevelsEvent::new(incoming_event), + Some(&RoomPowerLevelsEvent::new(current_room_power_levels_event)), + &AuthorizationRules::V6, + int!(40), + iter::empty(), + ) + .unwrap_err(); +} + +#[test] +fn change_notifications_level_with_new_higher_power_level() { + let _guard = init_subscriber(); + + let current_content = json!({ + "users": { + alice(): 100, + bob(): 40, + }, + "notifications": { + "room": 30, + }, + }); + let current_room_power_levels_event = to_pdu_event( + "IPOWER", + alice(), + TimelineEventType::RoomPowerLevels, + Some(""), + to_raw_json_value(¤t_content).unwrap(), + &["CREATE", "IMA"], + &["IMA"], + ); + + let incoming_content = json!({ + "users": { + alice(): 100, + bob(): 40, + }, + "notifications": { + "room": 60, + }, + }); + let incoming_event = to_pdu_event( + "IPOWER2", + bob(), + TimelineEventType::RoomPowerLevels, + Some(""), + to_raw_json_value(&incoming_content).unwrap(), + &["CREATE", "IMA", "IPOWER"], + &["IPOWER"], + ); + + // Notifications are not checked before v6. + check_room_power_levels( + &RoomPowerLevelsEvent::new(incoming_event.clone()), + Some(&RoomPowerLevelsEvent::new(current_room_power_levels_event.clone())), + &AuthorizationRules::V3, + int!(40), + iter::empty(), + ) + .unwrap(); + + // Cannot change to a power level that is higher than the user. + check_room_power_levels( + &RoomPowerLevelsEvent::new(incoming_event), + Some(&RoomPowerLevelsEvent::new(current_room_power_levels_event)), + &AuthorizationRules::V6, + int!(40), + iter::empty(), + ) + .unwrap_err(); +} + +#[test] +fn change_notifications_level_with_same_power_level() { + let _guard = init_subscriber(); + + let current_content = json!({ + "users": { + alice(): 100, + bob(): 40, + }, + "notifications": { + "room": 30, + }, + }); + let current_room_power_levels_event = to_pdu_event( + "IPOWER", + alice(), + TimelineEventType::RoomPowerLevels, + Some(""), + to_raw_json_value(¤t_content).unwrap(), + &["CREATE", "IMA"], + &["IMA"], + ); + + let incoming_content = json!({ + "users": { + alice(): 100, + bob(): 40, + }, + "notifications": { + "room": 31, + }, + }); + let incoming_event = to_pdu_event( + "IPOWER2", + bob(), + TimelineEventType::RoomPowerLevels, + Some(""), + to_raw_json_value(&incoming_content).unwrap(), + &["CREATE", "IMA", "IPOWER"], + &["IPOWER"], + ); + + // Notifications are not checked before v6. + check_room_power_levels( + &RoomPowerLevelsEvent::new(incoming_event.clone()), + Some(&RoomPowerLevelsEvent::new(current_room_power_levels_event.clone())), + &AuthorizationRules::V3, + int!(40), + iter::empty(), + ) + .unwrap(); + + // Can change a power level that is the same or lower than the user. + check_room_power_levels( + &RoomPowerLevelsEvent::new(incoming_event), + Some(&RoomPowerLevelsEvent::new(current_room_power_levels_event)), + &AuthorizationRules::V6, + int!(40), + iter::empty(), + ) + .unwrap(); +} + +#[test] +fn change_other_user_level_with_current_higher_power_level() { + let _guard = init_subscriber(); + + let current_content = json!({ + "users": { + alice(): 100, + bob(): 40, + zara(): 70, + }, + }); + let current_room_power_levels_event = to_pdu_event( + "IPOWER", + alice(), + TimelineEventType::RoomPowerLevels, + Some(""), + to_raw_json_value(¤t_content).unwrap(), + &["CREATE", "IMA"], + &["IMA"], + ); + + let incoming_content = json!({ + "users": { + alice(): 100, + bob(): 40, + }, + }); + let incoming_event = to_pdu_event( + "IPOWER2", + bob(), + TimelineEventType::RoomPowerLevels, + Some(""), + to_raw_json_value(&incoming_content).unwrap(), + &["CREATE", "IMA", "IPOWER"], + &["IPOWER"], + ); + + // Cannot change from a power level that is higher than the user. + check_room_power_levels( + &RoomPowerLevelsEvent::new(incoming_event), + Some(&RoomPowerLevelsEvent::new(current_room_power_levels_event)), + &AuthorizationRules::V6, + int!(40), + iter::empty(), + ) + .unwrap_err(); +} + +#[test] +fn change_other_user_level_with_new_higher_power_level() { + let _guard = init_subscriber(); + + let current_content = json!({ + "users": { + alice(): 100, + bob(): 40, + zara(): 10, + }, + }); + let current_room_power_levels_event = to_pdu_event( + "IPOWER", + alice(), + TimelineEventType::RoomPowerLevels, + Some(""), + to_raw_json_value(¤t_content).unwrap(), + &["CREATE", "IMA"], + &["IMA"], + ); + + let incoming_content = json!({ + "users": { + alice(): 100, + bob(): 40, + zara(): 45, + }, + }); + let incoming_event = to_pdu_event( + "IPOWER2", + bob(), + TimelineEventType::RoomPowerLevels, + Some(""), + to_raw_json_value(&incoming_content).unwrap(), + &["CREATE", "IMA", "IPOWER"], + &["IPOWER"], + ); + + // Cannot change to a power level that is higher than the user. + check_room_power_levels( + &RoomPowerLevelsEvent::new(incoming_event), + Some(&RoomPowerLevelsEvent::new(current_room_power_levels_event)), + &AuthorizationRules::V6, + int!(40), + iter::empty(), + ) + .unwrap_err(); +} + +#[test] +fn change_other_user_level_with_same_power_level() { + let _guard = init_subscriber(); + + let current_content = json!({ + "users": { + alice(): 100, + bob(): 40, + zara(): 20, + }, + }); + let current_room_power_levels_event = to_pdu_event( + "IPOWER", + alice(), + TimelineEventType::RoomPowerLevels, + Some(""), + to_raw_json_value(¤t_content).unwrap(), + &["CREATE", "IMA"], + &["IMA"], + ); + + let incoming_content = json!({ + "users": { + alice(): 100, + bob(): 40, + zara(): 40, + }, + }); + let incoming_event = to_pdu_event( + "IPOWER2", + bob(), + TimelineEventType::RoomPowerLevels, + Some(""), + to_raw_json_value(&incoming_content).unwrap(), + &["CREATE", "IMA", "IPOWER"], + &["IPOWER"], + ); + + // Can change a power level that is the same or lower than the user. + check_room_power_levels( + &RoomPowerLevelsEvent::new(incoming_event), + Some(&RoomPowerLevelsEvent::new(current_room_power_levels_event)), + &AuthorizationRules::V6, + int!(40), + iter::empty(), + ) + .unwrap(); +} + +#[test] +fn change_own_user_level_to_new_higher_power_level() { + let _guard = init_subscriber(); + + let current_content = json!({ + "users": { + alice(): 100, + bob(): 40, + }, + }); + let current_room_power_levels_event = to_pdu_event( + "IPOWER", + alice(), + TimelineEventType::RoomPowerLevels, + Some(""), + to_raw_json_value(¤t_content).unwrap(), + &["CREATE", "IMA"], + &["IMA"], + ); + + let incoming_content = json!({ + "users": { + alice(): 100, + bob(): 100, + }, + }); + let incoming_event = to_pdu_event( + "IPOWER2", + bob(), + TimelineEventType::RoomPowerLevels, + Some(""), + to_raw_json_value(&incoming_content).unwrap(), + &["CREATE", "IMA", "IPOWER"], + &["IPOWER"], + ); + + // Cannot change to a power level that is higher than the user. + check_room_power_levels( + &RoomPowerLevelsEvent::new(incoming_event), + Some(&RoomPowerLevelsEvent::new(current_room_power_levels_event)), + &AuthorizationRules::V6, + int!(40), + iter::empty(), + ) + .unwrap_err(); +} + +#[test] +fn change_own_user_level_to_lower_power_level() { + let _guard = init_subscriber(); + + let current_content = json!({ + "users": { + alice(): 100, + bob(): 40, + }, + }); + let current_room_power_levels_event = to_pdu_event( + "IPOWER", + alice(), + TimelineEventType::RoomPowerLevels, + Some(""), + to_raw_json_value(¤t_content).unwrap(), + &["CREATE", "IMA"], + &["IMA"], + ); + + let incoming_content = json!({ + "users": { + alice(): 100, + bob(): 20, + }, + }); + let incoming_event = to_pdu_event( + "IPOWER2", + bob(), + TimelineEventType::RoomPowerLevels, + Some(""), + to_raw_json_value(&incoming_content).unwrap(), + &["CREATE", "IMA", "IPOWER"], + &["IPOWER"], + ); + + // Can change own power level to a lower level than the user. + check_room_power_levels( + &RoomPowerLevelsEvent::new(incoming_event), + Some(&RoomPowerLevelsEvent::new(current_room_power_levels_event)), + &AuthorizationRules::V6, + int!(40), + iter::empty(), + ) + .unwrap(); +} + +#[test] +fn creator_has_infinite_power() { + let _guard = init_subscriber(); + + let current_content = json!({ + "users": { + bob(): i64::from(ruma::Int::MAX), + }, + }); + let current_room_power_levels_event = to_pdu_event( + "IPOWER", + alice(), + TimelineEventType::RoomPowerLevels, + Some(""), + to_raw_json_value(¤t_content).unwrap(), + &["CREATE", "IMA"], + &["IMA"], + ); + + let incoming_content = json!({ + "users": { + bob(): 0, + }, + }); + let incoming_event = to_pdu_event( + "IPOWER2", + alice(), + TimelineEventType::RoomPowerLevels, + Some(""), + to_raw_json_value(&incoming_content).unwrap(), + &["CREATE", "IMA", "IPOWER"], + &["IPOWER"], + ); + + // Room creator has infinite power level, and hence can change the power level + // of any other user. + check_room_power_levels( + &RoomPowerLevelsEvent::new(incoming_event), + Some(&RoomPowerLevelsEvent::new(current_room_power_levels_event)), + &AuthorizationRules::V12, + UserPowerLevel::Infinite, + iter::empty(), + ) + .unwrap(); +} + +#[test] +fn dont_allow_creator_in_users_field() { + let _guard = init_subscriber(); + + let current_content = json!({ + "users": { + bob(): 40, + }, + }); + let current_room_power_levels_event = to_pdu_event( + "IPOWER", + alice(), + TimelineEventType::RoomPowerLevels, + Some(""), + to_raw_json_value(¤t_content).unwrap(), + &["CREATE", "IMA"], + &["IMA"], + ); + + let incoming_content = json!({ + "users": { + alice(): 10, + bob(): 40, + }, + }); + let incoming_event = to_pdu_event( + "IPOWER2", + alice(), + TimelineEventType::RoomPowerLevels, + Some(""), + to_raw_json_value(&incoming_content).unwrap(), + &["CREATE", "IMA", "IPOWER"], + &["IPOWER"], + ); + + // Room creator cannot be in the `users` field of the power levels event + check_room_power_levels( + &RoomPowerLevelsEvent::new(incoming_event), + Some(&RoomPowerLevelsEvent::new(current_room_power_levels_event)), + &AuthorizationRules::V12, + UserPowerLevel::Infinite, + iter::once(alice().to_owned()), + ) + .unwrap_err(); +} diff --git a/src/core/matrix/state_res/event_format.rs b/src/core/matrix/state_res/event_format.rs new file mode 100644 index 00000000..6e1cba3f --- /dev/null +++ b/src/core/matrix/state_res/event_format.rs @@ -0,0 +1,461 @@ +use ruma::{ + CanonicalJsonObject, CanonicalJsonValue, ID_MAX_BYTES, RoomId, int, + room_version_rules::EventFormatRules, +}; +use serde_json::to_string as to_json_string; + +use crate::{ + Err, Result, err, + matrix::pdu::{MAX_AUTH_EVENTS, MAX_PDU_BYTES, MAX_PREV_EVENTS}, +}; + +/// Check that the given canonicalized PDU respects the event format of the room +/// version and the [size limits] from the Matrix specification. +/// +/// This is part of the [checks performed on receipt of a PDU]. +/// +/// This checks the following and enforces their size limits: +/// +/// * Full PDU +/// * `sender` +/// * `room_id` +/// * `type` +/// * `event_id` +/// * `state_key` +/// * `prev_events` +/// * `auth_events` +/// * `depth` +/// +/// Returns an `Err(_)` if the JSON is malformed or if the PDU doesn't pass the +/// checks. +/// +/// [size limits]: https://spec.matrix.org/latest/client-server-api/#size-limits +/// [checks performed on receipt of a PDU]: https://spec.matrix.org/latest/server-server-api/#checks-performed-on-receipt-of-a-pdu +pub fn check_pdu_format(pdu: &CanonicalJsonObject, rules: &EventFormatRules) -> Result { + // Check the PDU size, it must occur on the full PDU with signatures. + let json = to_json_string(&pdu) + .map_err(|e| err!(Request(BadJson("Failed to serialize canonical JSON: {e}"))))?; + + if json.len() > MAX_PDU_BYTES { + return Err!(Request(InvalidParam( + "PDU is larger than maximum of {MAX_PDU_BYTES} bytes" + ))); + } + + // Check the presence, type and length of the `type` field. + let event_type = extract_required_string_field(pdu, "type")?; + + // Check the presence, type and length of the `sender` field. + extract_required_string_field(pdu, "sender")?; + + // Check the presence, type and length of the `room_id` field. + let room_id = (event_type != "m.room.create" || rules.require_room_create_room_id) + .then(|| extract_required_string_field(pdu, "room_id")) + .transpose()?; + + // Check the presence, type and length of the `event_id` field. + if rules.require_event_id { + extract_required_string_field(pdu, "event_id")?; + } + + // Check the type and length of the `state_key` field. + extract_optional_string_field(pdu, "state_key")?; + + // Check the presence, type and length of the `prev_events` field. + extract_required_array_field(pdu, "prev_events", MAX_PREV_EVENTS)?; + + // Check the presence, type and length of the `auth_events` field. + let auth_events = extract_required_array_field(pdu, "auth_events", MAX_AUTH_EVENTS)?; + + if !rules.allow_room_create_in_auth_events { + // The only case where the room ID should be missing is for m.room.create which + // shouldn't have any auth_events. + if let Some(room_id) = room_id { + let room_create_event_reference_hash = <&RoomId>::try_from(room_id.as_str()) + .map_err(|e| err!("invalid `room_id` field in PDU: {e}"))? + .strip_sigil(); + + for event_id in auth_events { + let CanonicalJsonValue::String(event_id) = event_id else { + return Err!(Request(InvalidParam( + "unexpected format of array item in `auth_events` field in PDU: \ + expected string, got {event_id:?}" + ))); + }; + + let reference_hash = + event_id + .strip_prefix('$') + .ok_or(err!(Request(InvalidParam( + "unexpected format of array item in `auth_events` field in PDU: \ + string not beginning with the `$` sigil" + ))))?; + + if reference_hash == room_create_event_reference_hash { + return Err!(Request(InvalidParam( + "invalid `auth_events` field in PDU: cannot contain the `m.room.create` \ + event ID" + ))); + } + } + } + } + + // Check the presence, type and value of the `depth` field. + match pdu.get("depth") { + | Some(CanonicalJsonValue::Integer(value)) => + if *value < int!(0) { + return Err!(Request(InvalidParam( + "invalid `depth` field in PDU: cannot be a negative integer" + ))); + }, + | Some(value) => { + return Err!(Request(InvalidParam( + "unexpected format of `depth` field in PDU: expected integer, got {value:?}" + ))); + }, + | None => return Err!(Request(InvalidParam("missing `depth` field in PDU"))), + } + + Ok(()) +} + +/// Extract the optional string field with the given name from the given +/// canonical JSON object. +/// +/// Returns `Ok(Some(value))` if the field is present and a valid string, +/// `Ok(None)` if the field is missing and `Err(_)` if the field is not a string +/// or its length is bigger than [`ID_MAX_BYTES`]. +fn extract_optional_string_field<'a>( + object: &'a CanonicalJsonObject, + field: &'a str, +) -> Result> { + match object.get(field) { + | Some(CanonicalJsonValue::String(value)) => + if value.len() > ID_MAX_BYTES { + Err!(Request(InvalidParam( + "invalid `{field}` field in PDU: string length is larger than maximum of \ + {ID_MAX_BYTES} bytes" + ))) + } else { + Ok(Some(value)) + }, + + | Some(value) => Err!(Request(InvalidParam( + "unexpected format of `{field}` field in PDU: expected string, got {value:?}" + ))), + + | None => Ok(None), + } +} + +/// Extract the required string field with the given name from the given +/// canonical JSON object. +/// +/// Returns `Ok(value)` if the field is present and a valid string and `Err(_)` +/// if the field is missing, not a string or its length is bigger than +/// [`ID_MAX_BYTES`]. +fn extract_required_string_field<'a>( + object: &'a CanonicalJsonObject, + field: &'a str, +) -> Result<&'a String> { + extract_optional_string_field(object, field)? + .ok_or_else(|| err!(Request(InvalidParam("missing `{field}` field in PDU")))) +} + +/// Extract the required array field with the given name from the given +/// canonical JSON object. +/// +/// Returns `Ok(value)` if the field is present and a valid array or `Err(_)` if +/// the field is missing, not an array or its length is bigger than the given +/// value. +fn extract_required_array_field<'a>( + object: &'a CanonicalJsonObject, + field: &'a str, + max_len: usize, +) -> Result<&'a [CanonicalJsonValue]> { + match object.get(field) { + | Some(CanonicalJsonValue::Array(value)) => + if value.len() > max_len { + Err!(Request(InvalidParam( + "invalid `{field}` field in PDU: array length is larger than maximum of \ + {max_len}" + ))) + } else { + Ok(value) + }, + + | Some(value) => Err!(Request(InvalidParam( + "unexpected format of `{field}` field in PDU: expected array, got {value:?}" + ))), + + | None => Err!(Request(InvalidParam("missing `{field}` field in PDU"))), + } +} + +#[cfg(test)] +mod tests { + use std::iter::repeat_n; + + use ruma::{ + CanonicalJsonObject, CanonicalJsonValue, int, room_version_rules::EventFormatRules, + }; + use serde_json::{from_value as from_json_value, json}; + + use super::check_pdu_format; + + /// Construct a PDU valid for the event format of room v1. + fn pdu_v1() -> CanonicalJsonObject { + let pdu = json!({ + "auth_events": [ + [ + "$af232176:example.org", + { "sha256": "abase64encodedsha256hashshouldbe43byteslong" }, + ], + ], + "content": { + "key": "value", + }, + "depth": 12, + "event_id": "$a4ecee13e2accdadf56c1025:example.com", + "hashes": { + "sha256": "thishashcoversallfieldsincasethisisredacted" + }, + "origin_server_ts": 1_838_188_000, + "prev_events": [ + [ + "$af232176:example.org", + { "sha256": "abase64encodedsha256hashshouldbe43byteslong" } + ], + ], + "room_id": "!UcYsUzyxTGDxLBEvLy:example.org", + "sender": "@alice:example.com", + "signatures": { + "example.com": { + "ed25519:key_version": "these86bytesofbase64signaturecoveressentialfieldsincludinghashessocancheckredactedpdus", + }, + }, + "type": "m.room.message", + "unsigned": { + "age": 4612, + }, + }); + + from_json_value(pdu).unwrap() + } + + /// Construct a PDU valid for the event format of room v3. + fn pdu_v3() -> CanonicalJsonObject { + let pdu = json!({ + "auth_events": [ + "$base64encodedeventid", + "$adifferenteventid", + ], + "content": { + "key": "value", + }, + "depth": 12, + "hashes": { + "sha256": "thishashcoversallfieldsincasethisisredacted", + }, + "origin_server_ts": 1_838_188_000, + "prev_events": [ + "$base64encodedeventid", + "$adifferenteventid", + ], + "redacts": "$some/old+event", + "room_id": "!UcYsUzyxTGDxLBEvLy:example.org", + "sender": "@alice:example.com", + "signatures": { + "example.com": { + "ed25519:key_version": "these86bytesofbase64signaturecoveressentialfieldsincludinghashessocancheckredactedpdus", + }, + }, + "type": "m.room.message", + "unsigned": { + "age": 4612, + } + }); + + from_json_value(pdu).unwrap() + } + + /// Construct an `m.room.create` PDU valid for the event format of + /// `org.matrix.hydra.11`. + fn room_create_hydra() -> CanonicalJsonObject { + let pdu = json!({ + "auth_events": [], + "content": { + "room_version": "org.matrix.hydra.11", + }, + "depth": 1, + "hashes": { + "sha256": "thishashcoversallfieldsincasethisisredacted", + }, + "origin_server_ts": 1_838_188_000, + "prev_events": [], + "sender": "@alice:example.com", + "signatures": { + "example.com": { + "ed25519:key_version": "these86bytesofbase64signaturecoveressentialfieldsincludinghashessocancheckredactedpdus", + }, + }, + "type": "m.room.create", + "unsigned": { + "age": 4612, + } + }); + + from_json_value(pdu).unwrap() + } + + /// Construct a PDU valid for the event format of `org.matrix.hydra.11`. + fn pdu_hydra() -> CanonicalJsonObject { + let pdu = json!({ + "auth_events": [ + "$base64encodedeventid", + "$adifferenteventid", + ], + "content": { + "key": "value", + }, + "depth": 12, + "hashes": { + "sha256": "thishashcoversallfieldsincasethisisredacted", + }, + "origin_server_ts": 1_838_188_000, + "prev_events": [ + "$base64encodedeventid", + ], + "room_id": "!roomcreatereferencehash", + "sender": "@alice:example.com", + "signatures": { + "example.com": { + "ed25519:key_version": "these86bytesofbase64signaturecoveressentialfieldsincludinghashessocancheckredactedpdus", + }, + }, + "type": "m.room.message", + "unsigned": { + "age": 4612, + } + }); + + from_json_value(pdu).unwrap() + } + + #[test] + fn check_pdu_format_valid_v1() { + check_pdu_format(&pdu_v1(), &EventFormatRules::V1).unwrap(); + } + + #[test] + fn check_pdu_format_valid_v3() { + check_pdu_format(&pdu_v3(), &EventFormatRules::V3).unwrap(); + } + + #[test] + fn check_pdu_format_pdu_too_big() { + // Add a lot of data in the content to reach MAX_PDU_SIZE. + let mut pdu = pdu_v3(); + let content = pdu + .get_mut("content") + .unwrap() + .as_object_mut() + .unwrap(); + + let long_string = repeat_n('a', 66_000).collect::(); + content.insert("big_data".to_owned(), long_string.into()); + check_pdu_format(&pdu, &EventFormatRules::V3).unwrap_err(); + } + + #[test] + fn check_pdu_format_fields_missing() { + for field in + &["event_id", "sender", "room_id", "type", "prev_events", "auth_events", "depth"] + { + let mut pdu = pdu_v1(); + pdu.remove(*field).unwrap(); + check_pdu_format(&pdu, &EventFormatRules::V1).unwrap_err(); + } + } + + #[test] + fn check_pdu_format_strings_too_big() { + for field in &["event_id", "sender", "room_id", "type", "state_key"] { + let mut pdu = pdu_v1(); + let value = repeat_n('a', 300).collect::(); + pdu.insert((*field).to_owned(), value.into()); + check_pdu_format(&pdu, &EventFormatRules::V1).unwrap_err(); + } + } + + #[test] + fn check_pdu_format_strings_wrong_format() { + for field in &["event_id", "sender", "room_id", "type", "state_key"] { + let mut pdu = pdu_v1(); + pdu.insert((*field).to_owned(), true.into()); + check_pdu_format(&pdu, &EventFormatRules::V1).unwrap_err(); + } + } + + #[test] + fn check_pdu_format_arrays_too_big() { + for field in &["prev_events", "auth_events"] { + let mut pdu = pdu_v3(); + let value: Vec<_> = + repeat_n(CanonicalJsonValue::from("$eventid".to_owned()), 30).collect(); + + pdu.insert((*field).to_owned(), value.into()); + check_pdu_format(&pdu, &EventFormatRules::V3).unwrap_err(); + } + } + + #[test] + fn check_pdu_format_arrays_wrong_format() { + for field in &["prev_events", "auth_events"] { + let mut pdu = pdu_v3(); + pdu.insert((*field).to_owned(), true.into()); + check_pdu_format(&pdu, &EventFormatRules::V3).unwrap_err(); + } + } + + #[test] + fn check_pdu_format_negative_depth() { + let mut pdu = pdu_v3(); + pdu.insert("depth".to_owned(), int!(-1).into()) + .unwrap(); + + check_pdu_format(&pdu, &EventFormatRules::V3).unwrap_err(); + } + + #[test] + fn check_pdu_format_depth_wrong_format() { + let mut pdu = pdu_v3(); + pdu.insert("depth".to_owned(), true.into()); + check_pdu_format(&pdu, &EventFormatRules::V3).unwrap_err(); + } + + #[test] + fn check_pdu_format_valid_room_create_hydra() { + let pdu = room_create_hydra(); + check_pdu_format(&pdu, &EventFormatRules::V12).unwrap(); + } + + #[test] + fn check_pdu_format_valid_hydra() { + let pdu = pdu_hydra(); + check_pdu_format(&pdu, &EventFormatRules::V12).unwrap(); + } + + #[test] + fn check_pdu_format_hydra_with_room_create() { + let mut pdu = pdu_hydra(); + pdu.get_mut("auth_events") + .unwrap() + .as_array_mut() + .unwrap() + .push("$roomcreatereferencehash".to_owned().into()); + + check_pdu_format(&pdu, &EventFormatRules::V12).unwrap_err(); + } +} diff --git a/src/core/matrix/state_res/events.rs b/src/core/matrix/state_res/events.rs new file mode 100644 index 00000000..355af30d --- /dev/null +++ b/src/core/matrix/state_res/events.rs @@ -0,0 +1,50 @@ +//! Helper traits and types to work with events (aka PDUs). + +pub mod create; +pub mod join_rules; +pub mod member; +pub mod power_levels; +pub mod third_party_invite; + +pub use self::{ + create::RoomCreateEvent, + join_rules::{JoinRule, RoomJoinRulesEvent}, + member::{RoomMemberEvent, RoomMemberEventContent}, + power_levels::{RoomPowerLevelsEvent, RoomPowerLevelsIntField}, + third_party_invite::RoomThirdPartyInviteEvent, +}; + +/// Whether the given event is a power event. +/// +/// Definition in the spec: +/// +/// > A power event is a state event with type `m.room.power_levels` or +/// > `m.room.join_rules`, or a +/// > state event with type `m.room.member` where the `membership` is `leave` or +/// > `ban` and the +/// > `sender` does not match the `state_key`. The idea behind this is that +/// > power events are events +/// > that might remove someone’s ability to do something in the room. +pub(super) fn is_power_event(event: &Pdu) -> bool +where + Pdu: crate::matrix::Event, +{ + use ruma::events::{TimelineEventType, room::member::MembershipState}; + + match event.event_type() { + | TimelineEventType::RoomPowerLevels + | TimelineEventType::RoomJoinRules + | TimelineEventType::RoomCreate => event.state_key() == Some(""), + | TimelineEventType::RoomMember => { + let content = RoomMemberEventContent::new(event.content()); + if content.membership().is_ok_and(|membership| { + matches!(membership, MembershipState::Leave | MembershipState::Ban) + }) { + return Some(event.sender().as_str()) != event.state_key(); + } + + false + }, + | _ => false, + } +} diff --git a/src/core/matrix/state_res/events/create.rs b/src/core/matrix/state_res/events/create.rs new file mode 100644 index 00000000..6791ac97 --- /dev/null +++ b/src/core/matrix/state_res/events/create.rs @@ -0,0 +1,152 @@ +//! Types to deserialize `m.room.create` events. + +use std::{borrow::Cow, iter, ops::Deref}; + +use ruma::{ + OwnedUserId, RoomVersionId, UserId, room_version_rules::AuthorizationRules, + serde::from_raw_json_value, +}; +use serde::{Deserialize, de::IgnoredAny}; + +use crate::{Error, Result, err, matrix::Event}; + +/// A helper type for an [`Event`] of type `m.room.create`. +/// +/// This is a type that deserializes each field lazily, when requested. +#[derive(Debug)] +pub struct RoomCreateEvent(E); + +impl RoomCreateEvent { + /// Construct a new `RoomCreateEvent` around the given event. + #[inline] + pub fn new(event: E) -> Self { Self(event) } + + /// The version of the room. + #[allow(dead_code)] + pub fn room_version(&self) -> Result { + #[derive(Deserialize)] + struct RoomCreateContentRoomVersion { + #[allow(dead_code)] + room_version: Option, + } + + let content: RoomCreateContentRoomVersion = + from_raw_json_value(self.content()).map_err(|err: Error| { + err!("invalid `room_version` field in `m.room.create` event: {err}") + })?; + + Ok(content.room_version.unwrap_or(RoomVersionId::V1)) + } + + /// Whether the room is federated. + pub fn federate(&self) -> Result { + #[derive(Deserialize)] + struct RoomCreateContentFederate { + #[serde(rename = "m.federate")] + federate: Option, + } + + let content: RoomCreateContentFederate = + from_raw_json_value(self.content()).map_err(|err: Error| { + err!("invalid `m.federate` field in `m.room.create` event: {err}") + })?; + + Ok(content.federate.unwrap_or(true)) + } + + /// The creator of the room. + /// + /// If the `use_room_create_sender` field of `AuthorizationRules` is set, + /// the creator is the sender of this `m.room.create` event, otherwise it + /// is deserialized from the `creator` field of this event's content. + pub fn creator(&self, rules: &AuthorizationRules) -> Result> { + #[derive(Deserialize)] + struct RoomCreateContentCreator { + creator: OwnedUserId, + } + + if rules.use_room_create_sender { + Ok(Cow::Borrowed(self.sender())) + } else { + let content: RoomCreateContentCreator = + from_raw_json_value(self.content()).map_err(|err: Error| { + err!("missing or invalid `creator` field in `m.room.create` event: {err}") + })?; + + Ok(Cow::Owned(content.creator)) + } + } + + /// The creators of the room. + /// + /// If the `use_room_create_sender` field of `AuthorizationRules` is set, + /// the creator is the sender of this `m.room.create` event, otherwise it + /// is deserialized from the `creator` field of this event's content. + /// Additionally if the `explicitly_privilege_room_creators` + /// field of `AuthorizationRules` is set, any additional user IDs in + /// `additional_creators`, if present, will also be considered creators. + pub fn creators<'a>( + &'a self, + rules: &'a AuthorizationRules, + ) -> Result + Clone + use<'a, E>> { + let initial = self.creator(rules)?.into_owned(); + let additional = self.additional_creators(rules)?; + + Ok(iter::once(initial).chain(additional)) + } + + /// The additional creators of the room (if any). + /// + /// If the `explicitly_privilege_room_creators` + /// field of `AuthorizationRules` is set, any additional user IDs in + /// `additional_creators`, if present, will also be considered creators. + /// + /// This function ignores the primary room creator, and should only be used + /// in `check_room_member_join`. Otherwise, you should use `creators` + /// instead. + pub(super) fn additional_creators( + &self, + rules: &AuthorizationRules, + ) -> Result + Clone> { + #[derive(Deserialize)] + struct RoomCreateContentAdditionalCreators { + #[serde(default)] + additional_creators: Vec, + } + + Ok(if rules.additional_room_creators { + let mut content: RoomCreateContentAdditionalCreators = + from_raw_json_value(self.content()).map_err(|err: serde_json::Error| { + err!("invalid `additional_creators` field in `m.room.create` event: {err}") + })?; + + content.additional_creators.sort(); + content.additional_creators.dedup(); + content.additional_creators.into_iter() + } else { + Vec::new().into_iter() + }) + } + + /// Whether the event has a `creator` field. + pub fn has_creator(&self) -> Result { + #[derive(Deserialize)] + struct RoomCreateContentCreator { + creator: Option, + } + + let content: RoomCreateContentCreator = + from_raw_json_value(self.content()).map_err(|err: Error| { + err!("invalid `creator` field in `m.room.create` event: {err}") + })?; + + Ok(content.creator.is_some()) + } +} + +impl Deref for RoomCreateEvent { + type Target = E; + + #[inline] + fn deref(&self) -> &Self::Target { &self.0 } +} diff --git a/src/core/matrix/state_res/events/join_rules.rs b/src/core/matrix/state_res/events/join_rules.rs new file mode 100644 index 00000000..22575818 --- /dev/null +++ b/src/core/matrix/state_res/events/join_rules.rs @@ -0,0 +1,74 @@ +//! Types to deserialize `m.room.join_rules` events. + +use std::ops::Deref; + +use ruma::serde::{PartialEqAsRefStr, StringEnum, from_raw_json_value}; +use serde::Deserialize; + +use crate::{Error, Result, err, matrix::Event}; + +/// A helper type for an [`Event`] of type `m.room.join_rules`. +/// +/// This is a type that deserializes each field lazily, when requested. +#[derive(Debug, Clone)] +pub struct RoomJoinRulesEvent(E); + +impl RoomJoinRulesEvent { + /// Construct a new `RoomJoinRulesEvent` around the given event. + #[inline] + pub fn new(event: E) -> Self { Self(event) } + + /// The join rule of the room. + pub fn join_rule(&self) -> Result { + #[derive(Deserialize)] + struct RoomJoinRulesContentJoinRule { + join_rule: JoinRule, + } + + let content: RoomJoinRulesContentJoinRule = + from_raw_json_value(self.content()).map_err(|err: Error| { + err!("missing or invalid `join_rule` field in `m.room.join_rules` event: {err}") + })?; + + Ok(content.join_rule) + } +} + +impl Deref for RoomJoinRulesEvent { + type Target = E; + + #[inline] + fn deref(&self) -> &Self::Target { &self.0 } +} + +/// The possible values for the join rule of a room. +#[derive(Clone, StringEnum, PartialEqAsRefStr)] +#[ruma_enum(rename_all = "snake_case")] +#[non_exhaustive] +pub enum JoinRule { + /// `public` + Public, + + /// `invite` + Invite, + + /// `knock` + Knock, + + /// `restricted` + Restricted, + + /// `KnockRestricted` + KnockRestricted, + + #[doc(hidden)] + _Custom(PrivOwnedStr), +} + +impl Eq for JoinRule {} + +// Wrapper around `Box` that cannot be used in a meaningful way outside of +// this crate. Used for string enums because their `_Custom` variant can't be +// truly private (only `#[doc(hidden)]`). +#[derive(Debug, Clone)] +pub struct PrivOwnedStr(Box); diff --git a/src/core/matrix/state_res/events/member.rs b/src/core/matrix/state_res/events/member.rs new file mode 100644 index 00000000..abaf66a4 --- /dev/null +++ b/src/core/matrix/state_res/events/member.rs @@ -0,0 +1,207 @@ +//! Types to deserialize `m.room.member` events. + +use std::ops::Deref; + +use ruma::{ + CanonicalJsonObject, OwnedUserId, events::room::member::MembershipState, + serde::from_raw_json_value, signatures::canonical_json, +}; +use serde::Deserialize; +use serde_json::value::RawValue as RawJsonValue; + +use crate::{Err, Error, Result, debug_error, err, matrix::Event}; + +/// A helper type for an [`Event`] of type `m.room.member`. +/// +/// This is a type that deserializes each field lazily, as requested. +#[derive(Debug, Clone)] +pub struct RoomMemberEvent(E); + +impl RoomMemberEvent { + /// Construct a new `RoomMemberEvent` around the given event. + #[inline] + pub fn new(event: E) -> Self { Self(event) } + + /// The membership of the user. + #[inline] + pub fn membership(&self) -> Result { + RoomMemberEventContent(self.content()).membership() + } + + /// If this is a `join` event, the ID of a user on the homeserver that + /// authorized it. + #[inline] + pub fn join_authorised_via_users_server(&self) -> Result> { + RoomMemberEventContent(self.content()).join_authorised_via_users_server() + } + + /// If this is an `invite` event, details about the third-party invite that + /// resulted in this event. + #[inline] + pub fn third_party_invite(&self) -> Result> { + RoomMemberEventContent(self.content()).third_party_invite() + } +} + +impl Deref for RoomMemberEvent { + type Target = E; + + #[inline] + fn deref(&self) -> &Self::Target { &self.0 } +} + +/// Helper trait for `Option>`. +pub(crate) trait RoomMemberEventResultExt { + /// The membership of the user. + /// + /// Defaults to `leave` if there is no `m.room.member` event. + fn membership(&self) -> Result; +} + +impl RoomMemberEventResultExt for Result> { + fn membership(&self) -> Result { + match self { + | Ok(event) => event.membership(), + | Err(e) if e.is_not_found() => Ok(MembershipState::Leave), + | Err(e) if cfg!(test) => panic!("membership(): unexpected: {e}"), + | Err(e) => { + debug_error!("membership(): unexpected: {e}"); + Ok(MembershipState::Leave) + }, + } + } +} + +/// A helper type for the raw JSON content of an event of type `m.room.member`. +pub struct RoomMemberEventContent<'a>(&'a RawJsonValue); + +impl<'a> RoomMemberEventContent<'a> { + /// Construct a new `RoomMemberEventContent` around the given raw JSON + /// content. + #[inline] + #[must_use] + pub fn new(content: &'a RawJsonValue) -> Self { Self(content) } +} + +impl RoomMemberEventContent<'_> { + /// The membership of the user. + pub fn membership(&self) -> Result { + #[derive(Deserialize)] + struct RoomMemberContentMembership { + membership: MembershipState, + } + + let content: RoomMemberContentMembership = + from_raw_json_value(self.0).map_err(|err: Error| { + err!(Request(InvalidParam( + "missing or invalid `membership` field in `m.room.member` event: {err}" + ))) + })?; + + Ok(content.membership) + } + + /// If this is a `join` event, the ID of a user on the homeserver that + /// authorized it. + pub fn join_authorised_via_users_server(&self) -> Result> { + #[derive(Deserialize)] + struct RoomMemberContentJoinAuthorizedViaUsersServer { + join_authorised_via_users_server: Option, + } + + let content: RoomMemberContentJoinAuthorizedViaUsersServer = from_raw_json_value(self.0) + .map_err(|err: Error| { + err!(Request(InvalidParam( + "invalid `join_authorised_via_users_server` field in `m.room.member` event: \ + {err}" + ))) + })?; + + Ok(content.join_authorised_via_users_server) + } + + /// If this is an `invite` event, details about the third-party invite that + /// resulted in this event. + pub fn third_party_invite(&self) -> Result> { + #[derive(Deserialize)] + struct RoomMemberContentThirdPartyInvite { + third_party_invite: Option, + } + + let content: RoomMemberContentThirdPartyInvite = + from_raw_json_value(self.0).map_err(|err: Error| { + err!(Request(InvalidParam( + "invalid `third_party_invite` field in `m.room.member` event: {err}" + ))) + })?; + + Ok(content.third_party_invite) + } +} + +/// Details about a third-party invite. +#[derive(Deserialize)] +pub struct ThirdPartyInvite { + /// Signed details about the third-party invite. + signed: CanonicalJsonObject, +} + +impl ThirdPartyInvite { + /// The unique identifier for the third-party invite. + pub fn token(&self) -> Result<&str> { + let Some(token_value) = self.signed.get("token") else { + return Err!(Request(InvalidParam( + "missing `token` field in `third_party_invite.signed` of `m.room.member` event" + ))); + }; + + token_value.as_str().ok_or_else(|| { + err!(Request(InvalidParam( + "unexpected format of `token` field in `third_party_invite.signed` of \ + `m.room.member` event: expected string, got {token_value:?}" + ))) + }) + } + + /// The Matrix ID of the user that was invited. + pub fn mxid(&self) -> Result<&str> { + let Some(mxid_value) = self.signed.get("mxid") else { + return Err!(Request(InvalidParam( + "missing `mxid` field in `third_party_invite.signed` of `m.room.member` event" + ))); + }; + + mxid_value.as_str().ok_or_else(|| { + err!(Request(InvalidParam( + "unexpected format of `mxid` field in `third_party_invite.signed` of \ + `m.room.member` event: expected string, got {mxid_value:?}" + ))) + }) + } + + /// The signatures of the event. + pub fn signatures(&self) -> Result<&CanonicalJsonObject> { + let Some(signatures_value) = self.signed.get("signatures") else { + return Err!(Request(InvalidParam( + "missing `signatures` field in `third_party_invite.signed` of `m.room.member` \ + event" + ))); + }; + + signatures_value.as_object().ok_or_else(|| { + err!(Request(InvalidParam( + "unexpected format of `signatures` field in `third_party_invite.signed` of \ + `m.room.member` event: expected object, got {signatures_value:?}" + ))) + }) + } + + /// The `signed` object as canonical JSON string to verify the signatures. + pub fn signed_canonical_json(&self) -> Result { + canonical_json(&self.signed).map_err(|error| { + err!(Request(InvalidParam( + "invalid `third_party_invite.signed` field in `m.room.member` event: {error}" + ))) + }) + } +} diff --git a/src/core/matrix/state_res/events/power_levels.rs b/src/core/matrix/state_res/events/power_levels.rs new file mode 100644 index 00000000..b2ebf0a9 --- /dev/null +++ b/src/core/matrix/state_res/events/power_levels.rs @@ -0,0 +1,401 @@ +//! Types to deserialize `m.room.power_levels` events. + +use std::ops::Deref; + +use ruma::{ + Int, OwnedUserId, UserId, + events::{TimelineEventType, room::power_levels::UserPowerLevel}, + int, + room_version_rules::AuthorizationRules, + serde::{ + DebugAsRefStr, DisplayAsRefStr, JsonObject, OrdAsRefStr, PartialEqAsRefStr, + PartialOrdAsRefStr, deserialize_v1_powerlevel, from_raw_json_value, + vec_deserialize_int_powerlevel_values, vec_deserialize_v1_powerlevel_values, + }, +}; +use serde::de::DeserializeOwned; +use serde_json::{Error, from_value as from_json_value}; + +use crate::{Result, err, is_equal_to, matrix::Event, ref_at}; + +/// The default value of the creator's power level. +const DEFAULT_CREATOR_POWER_LEVEL: i32 = 100; + +/// A helper type for an [`Event`] of type `m.room.power_levels`. +#[derive(Clone, Debug)] +pub struct RoomPowerLevelsEvent(E); + +impl RoomPowerLevelsEvent { + /// Construct a new `RoomPowerLevelsEvent` around the given event. + #[inline] + pub fn new(event: E) -> Self { Self(event) } + + /// The deserialized content of the event. + fn deserialized_content(&self) -> Result { + from_raw_json_value(self.content()).map_err(|error: Error| { + err!(Request(InvalidParam("malformed `m.room.power_levels` content: {error}"))) + }) + } + + /// Get the value of a field that should contain an integer, if any. + /// + /// The deserialization of this field is cached in memory. + pub(crate) fn get_as_int( + &self, + field: RoomPowerLevelsIntField, + rules: &AuthorizationRules, + ) -> Result> { + let content = self.deserialized_content()?; + + let Some(value) = content.get(field.as_str()) else { + return Ok(None); + }; + + let res = if rules.integer_power_levels { + from_json_value(value.clone()) + } else { + deserialize_v1_powerlevel(value) + }; + + let power_level = res.map(Some).map_err(|error| { + err!(Request(InvalidParam( + "unexpected format of `{field}` field in `content` of `m.room.power_levels` \ + event: {error}" + ))) + })?; + + Ok(power_level) + } + + /// Get the value of a field that should contain an integer, or its default + /// value if it is absent. + #[inline] + pub(crate) fn get_as_int_or_default( + &self, + field: RoomPowerLevelsIntField, + rules: &AuthorizationRules, + ) -> Result { + Ok(self + .get_as_int(field, rules)? + .unwrap_or_else(|| field.default_value())) + } + + /// Get the value of a field that should contain a map of any value to + /// integer, if any. + fn get_as_int_map( + &self, + field: &str, + rules: &AuthorizationRules, + ) -> Result>> { + let content = self.deserialized_content()?; + + let Some(value) = content.get(field) else { + return Ok(None); + }; + + let res = if rules.integer_power_levels { + vec_deserialize_int_powerlevel_values(value) + } else { + vec_deserialize_v1_powerlevel_values(value) + }; + + res.map(Some).map_err(|error| { + err!(Request(InvalidParam( + "unexpected format of `{field}` field in `content` of `m.room.power_levels` \ + event: {error}" + ))) + }) + } + + /// Get the power levels required to send events, if any. + #[inline] + pub(crate) fn events( + &self, + rules: &AuthorizationRules, + ) -> Result>> { + self.get_as_int_map("events", rules) + } + + /// Get the power levels required to trigger notifications, if any. + #[inline] + pub(crate) fn notifications( + &self, + rules: &AuthorizationRules, + ) -> Result>> { + self.get_as_int_map("notifications", rules) + } + + /// Get the power levels of the users, if any. + /// + /// The deserialization of this field is cached in memory. + #[inline] + pub(crate) fn users( + &self, + rules: &AuthorizationRules, + ) -> Result>> { + self.get_as_int_map("users", rules) + } + + /// Get the power level of the user with the given ID. + /// + /// Calling this method several times should be cheap because the necessary + /// deserialization results are cached. + pub(crate) fn user_power_level( + &self, + user_id: &UserId, + rules: &AuthorizationRules, + ) -> Result { + let power_level = if let Some(power_level) = self + .users(rules)? + .as_ref() + .and_then(|users| get_value(users, user_id)) + { + Ok(*power_level) + } else { + self.get_as_int_or_default(RoomPowerLevelsIntField::UsersDefault, rules) + }; + + power_level.map(Into::into) + } + + /// Get the power level required to send an event of the given type. + pub(crate) fn event_power_level( + &self, + event_type: &TimelineEventType, + state_key: Option<&str>, + rules: &AuthorizationRules, + ) -> Result { + let events = self.events(rules)?; + + if let Some(power_level) = events + .as_ref() + .and_then(|events| get_value(events, event_type)) + { + return Ok(*power_level); + } + + let default_field = if state_key.is_some() { + RoomPowerLevelsIntField::StateDefault + } else { + RoomPowerLevelsIntField::EventsDefault + }; + + self.get_as_int_or_default(default_field, rules) + } + + /// Get a map of all the fields with an integer value in the `content` of an + /// `m.room.power_levels` event. + pub(crate) fn int_fields_map( + &self, + rules: &AuthorizationRules, + ) -> Result> { + RoomPowerLevelsIntField::ALL + .iter() + .copied() + .filter_map(|field| match self.get_as_int(field, rules) { + | Ok(value) => value.map(|value| Ok((field, value))), + | Err(error) => Some(Err(error)), + }) + .collect() + } +} + +impl Deref for RoomPowerLevelsEvent { + type Target = E; + + #[inline] + fn deref(&self) -> &Self::Target { &self.0 } +} + +/// Helper trait for `Option>`. +pub(crate) trait RoomPowerLevelsEventOptionExt { + /// Get the power level of the user with the given ID. + fn user_power_level( + &self, + user_id: &UserId, + creators: impl Iterator, + rules: &AuthorizationRules, + ) -> Result; + + /// Get the value of a field that should contain an integer, or its default + /// value if it is absent. + fn get_as_int_or_default( + &self, + field: RoomPowerLevelsIntField, + rules: &AuthorizationRules, + ) -> Result; + + /// Get the power level required to send an event of the given type. + fn event_power_level( + &self, + event_type: &TimelineEventType, + state_key: Option<&str>, + rules: &AuthorizationRules, + ) -> Result; +} + +impl RoomPowerLevelsEventOptionExt for Option> +where + E: Event, +{ + fn user_power_level( + &self, + user_id: &UserId, + mut creators: impl Iterator, + rules: &AuthorizationRules, + ) -> Result { + if rules.explicitly_privilege_room_creators && creators.any(is_equal_to!(user_id)) { + Ok(UserPowerLevel::Infinite) + } else if let Some(room_power_levels_event) = self { + room_power_levels_event.user_power_level(user_id, rules) + } else { + let power_level = if creators.any(is_equal_to!(user_id)) { + DEFAULT_CREATOR_POWER_LEVEL.into() + } else { + RoomPowerLevelsIntField::UsersDefault.default_value() + }; + + Ok(power_level.into()) + } + } + + fn get_as_int_or_default( + &self, + field: RoomPowerLevelsIntField, + rules: &AuthorizationRules, + ) -> Result { + if let Some(room_power_levels_event) = self { + room_power_levels_event.get_as_int_or_default(field, rules) + } else { + Ok(field.default_value()) + } + } + + fn event_power_level( + &self, + event_type: &TimelineEventType, + state_key: Option<&str>, + rules: &AuthorizationRules, + ) -> Result { + if let Some(room_power_levels_event) = self { + room_power_levels_event.event_power_level(event_type, state_key, rules) + } else { + let default_field = if state_key.is_some() { + RoomPowerLevelsIntField::StateDefault + } else { + RoomPowerLevelsIntField::EventsDefault + }; + + Ok(default_field.default_value()) + } + } +} + +#[inline] +pub(crate) fn get_value<'a, K, V, B>(vec: &'a [(K, V)], key: &'a B) -> Option<&'a V> +where + &'a K: PartialEq<&'a B>, + B: ?Sized, +{ + position(vec, key) + .and_then(|i| vec.get(i)) + .map(ref_at!(1)) +} + +#[inline] +pub(crate) fn contains_key<'a, K, V, B>(vec: &'a [(K, V)], key: &'a B) -> bool +where + &'a K: PartialEq<&'a B>, + B: ?Sized, +{ + position(vec, key).is_some() +} + +fn position<'a, K, V, B>(vec: &'a [(K, V)], key: &'a B) -> Option +where + &'a K: PartialEq<&'a B>, + B: ?Sized, +{ + vec.iter() + .map(ref_at!(0)) + .position(is_equal_to!(key)) +} + +/// Fields in the `content` of an `m.room.power_levels` event with an integer +/// value. +#[derive( + DebugAsRefStr, + Clone, + Copy, + DisplayAsRefStr, + PartialEqAsRefStr, + Eq, + PartialOrdAsRefStr, + OrdAsRefStr, +)] +#[non_exhaustive] +pub enum RoomPowerLevelsIntField { + /// `users_default` + UsersDefault, + + /// `events_default` + EventsDefault, + + /// `state_default` + StateDefault, + + /// `ban` + Ban, + + /// `redact` + Redact, + + /// `kick` + Kick, + + /// `invite` + Invite, +} + +impl RoomPowerLevelsIntField { + /// A slice containing all the variants. + pub const ALL: &[Self] = &[ + Self::UsersDefault, + Self::EventsDefault, + Self::StateDefault, + Self::Ban, + Self::Redact, + Self::Kick, + Self::Invite, + ]; + + /// The string representation of this field. + #[inline] + #[must_use] + pub fn as_str(&self) -> &str { self.as_ref() } + + /// The default value for this field if it is absent. + #[inline] + #[must_use] + pub fn default_value(self) -> Int { + match self { + | Self::UsersDefault | Self::EventsDefault | Self::Invite => int!(0), + | Self::StateDefault | Self::Kick | Self::Ban | Self::Redact => int!(50), + } + } +} + +impl AsRef for RoomPowerLevelsIntField { + #[inline] + fn as_ref(&self) -> &'static str { + match self { + | Self::UsersDefault => "users_default", + | Self::EventsDefault => "events_default", + | Self::StateDefault => "state_default", + | Self::Ban => "ban", + | Self::Redact => "redact", + | Self::Kick => "kick", + | Self::Invite => "invite", + } + } +} diff --git a/src/core/matrix/state_res/events/third_party_invite.rs b/src/core/matrix/state_res/events/third_party_invite.rs new file mode 100644 index 00000000..6b6149c6 --- /dev/null +++ b/src/core/matrix/state_res/events/third_party_invite.rs @@ -0,0 +1,61 @@ +//! Types to deserialize `m.room.third_party_invite` events. + +use std::{collections::BTreeSet, ops::Deref}; + +use ruma::{serde::from_raw_json_value, third_party_invite::IdentityServerBase64PublicKey}; +use serde::Deserialize; + +use crate::{Error, Result, err, matrix::Event}; + +/// A helper type for an [`Event`] of type `m.room.third_party_invite`. +/// +/// This is a type that deserializes each field lazily, when requested. +#[derive(Debug, Clone)] +pub struct RoomThirdPartyInviteEvent(E); + +impl RoomThirdPartyInviteEvent { + /// Construct a new `RoomThirdPartyInviteEvent` around the given event. + pub fn new(event: E) -> Self { Self(event) } + + /// The public keys of the identity server that might be used to sign the + /// third-party invite. + pub(crate) fn public_keys(&self) -> Result> { + #[derive(Deserialize)] + struct RoomThirdPartyInviteContentPublicKeys { + public_key: Option, + #[serde(default)] + public_keys: Vec, + } + + #[derive(Deserialize)] + struct PublicKey { + public_key: IdentityServerBase64PublicKey, + } + + let content: RoomThirdPartyInviteContentPublicKeys = from_raw_json_value(self.content()) + .map_err(|err: Error| { + err!( + "invalid `public_key` or `public_keys` in `m.room.third_party_invite` \ + event: {err}" + ) + })?; + + let public_keys = content + .public_keys + .into_iter() + .map(|k| k.public_key); + + Ok(content + .public_key + .into_iter() + .chain(public_keys) + .collect()) + } +} + +impl Deref for RoomThirdPartyInviteEvent { + type Target = E; + + #[inline] + fn deref(&self) -> &Self::Target { &self.0 } +} diff --git a/src/core/matrix/state_res/fetch_state.rs b/src/core/matrix/state_res/fetch_state.rs new file mode 100644 index 00000000..8849ba86 --- /dev/null +++ b/src/core/matrix/state_res/fetch_state.rs @@ -0,0 +1,74 @@ +use ruma::{ + UserId, + events::{StateEventType, room::member::MembershipState}, +}; + +use super::events::{ + JoinRule, RoomCreateEvent, RoomJoinRulesEvent, RoomMemberEvent, RoomPowerLevelsEvent, + RoomThirdPartyInviteEvent, member::RoomMemberEventResultExt, +}; +use crate::{ + Result, err, + matrix::{Event, StateKey}, +}; + +pub(super) trait FetchStateExt { + async fn room_create_event(&self) -> Result>; + + async fn user_membership(&self, user_id: &UserId) -> Result; + + async fn room_power_levels_event(&self) -> Option>; + + async fn join_rule(&self) -> Result; + + async fn room_third_party_invite_event( + &self, + token: &str, + ) -> Option>; +} + +impl FetchStateExt for &Fetch +where + Fetch: Fn(StateEventType, StateKey) -> Fut + Sync, + Fut: Future>, + Pdu: Event, +{ + async fn room_create_event(&self) -> Result> { + self(StateEventType::RoomCreate, "".into()) + .await + .map(RoomCreateEvent::new) + .map_err(|e| err!("no `m.room.create` event in current state: {e}")) + } + + async fn user_membership(&self, user_id: &UserId) -> Result { + self(StateEventType::RoomMember, user_id.as_str().into()) + .await + .map(RoomMemberEvent::new) + .membership() + } + + async fn room_power_levels_event(&self) -> Option> { + self(StateEventType::RoomPowerLevels, "".into()) + .await + .map(RoomPowerLevelsEvent::new) + .ok() + } + + async fn join_rule(&self) -> Result { + self(StateEventType::RoomJoinRules, "".into()) + .await + .map(RoomJoinRulesEvent::new) + .map_err(|e| err!("no `m.room.join_rules` event in current state: {e}"))? + .join_rule() + } + + async fn room_third_party_invite_event( + &self, + token: &str, + ) -> Option> { + self(StateEventType::RoomThirdPartyInvite, token.into()) + .await + .ok() + .map(RoomThirdPartyInviteEvent::new) + } +} diff --git a/src/core/matrix/state_res/mod.rs b/src/core/matrix/state_res/mod.rs index ce299667..d516246d 100644 --- a/src/core/matrix/state_res/mod.rs +++ b/src/core/matrix/state_res/mod.rs @@ -1,1700 +1,77 @@ -#![cfg_attr(test, allow(warnings))] - -pub(crate) mod error; -pub mod event_auth; -mod power_levels; -mod room_version; - -#[cfg(test)] -mod test_utils; +//! State resolution and checks on incoming PDUs according to the [Matrix](https://matrix.org/) specification. +//! +//! When creating or receiving a PDU (or event), a server must check whether it +//! is valid and how it affects the room state. The purpose of this crate is to +//! provide functions that solve that. +//! +//! # Checks performed on receipt of a PDU +//! +//! This crate used with [ruma-signatures] should allow to perform all the +//! [necessary checks on receipt of a PDU]. +//! +//! To respect the Matrix specification, the following functions should be +//! called for a PDU: +//! +//! 1. [`check_pdu_format()`] - The event should be dropped on error. +//! 2. [`ruma_signatures::verify_event()`] - The event should be dropped on +//! error. The PDU should be redacted before checking the authorization rules +//! if the result is `Verified::Signatures`. +//! 3. [`check_state_independent_auth_rules()`] - The event should be rejected +//! on error. +//! 4. [`check_state_dependent_auth_rules()`] - This function must be called 3 +//! times: +//! 1. With the `auth_events` for the state, the event should be rejected on +//! error. +//! 2. With the state before the event, the event should be rejected on +//! error. +//! 3. With the current state of the room, the event should be "soft failed" +//! on error. +//! +//! # Room State Resolution +//! +//! Because of the distributed nature of Matrix, homeservers might not receive +//! all events in the same order, which might cause the room state to diverge +//! temporarily between homeservers. The purpose of [state resolution] is to +//! make sure that all homeservers arrive at the same room state given the same +//! events. +//! +//! The [`resolve()`] function allows to do that. It takes an iterator of state +//! maps and applies the proper state resolution algorithm for the current room +//! version to output the map of events in the current room state. +//! +//! # Event helper types +//! +//! The types from [ruma-events] use strict deserialization rules according to +//! their definition in the specification, which means that they also validate +//! fields that are not checked when receiving a PDU. Since it is not +//! appropriate for servers to reject an event that passes those checks, this +//! crate provides helper types in the [`events`] module, built around the +//! [`Event`] trait, to deserialize lazily a handful of fields from the most +//! common state events. Since these are the same types used for checking the +//! authorization rules, they are guaranteed to not perform extra validation on +//! unvalidated fields. +//! +//! The types from ruma-events are still appropriate to be used to create a new +//! event, or to validate an event received from a client. +//! +//! [ruma-signatures]: https://crates.io/crates/ruma-signatures +//! [necessary checks on receipt of a PDU]: https://spec.matrix.org/latest/server-server-api/#checks-performed-on-receipt-of-a-pdu +//! [ruma-events]: https://crates.io/crates/ruma-events #[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, RoomVersionId, - events::{ - StateEventType, TimelineEventType, - room::member::{MembershipState, RoomMemberEventContent}, - }, - int, -}; -use serde_json::from_str as from_json_str; - -pub(crate) use self::error::Error; -use self::power_levels::PowerLevelsContentFields; -pub use self::{ - event_auth::{auth_check, auth_types_for_event}, - room_version::RoomVersion, -}; -use crate::{ - debug, debug_error, - matrix::{Event, StateKey}, - trace, - utils::stream::{BroadbandExt, IterStream, ReadyExt, TryBroadbandExt, 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))] -pub async fn resolve<'a, Pdu, Sets, SetIter, Hasher, Fetch, FetchFut, Exists, ExistsFut>( - room_version: &RoomVersionId, - 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, -{ - debug!("State resolution starting"); - - // Split non-conflicting and conflicting state - let (clean, conflicting) = separate(state_sets.into_iter()); - - debug!(count = clean.len(), "non-conflicting events"); - trace!(map = ?clean, "non-conflicting events"); - - if conflicting.is_empty() { - debug!("no conflicting state found"); - return Ok(clean); - } - - debug!(count = conflicting.len(), "conflicting events"); - trace!(map = ?conflicting, "conflicting events"); - - let conflicting_values = conflicting.into_values().flatten().stream(); - - // `all_conflicted` contains unique items - // synapse says `full_set = {eid for eid in full_conflicted_set if eid in - // event_map}` - let all_conflicted: HashSet<_> = get_auth_chain_diff(auth_chain_sets) - .chain(conflicting_values) - .broad_filter_map(async |id| event_exists(id.clone()).await.then_some(id)) - .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, &event_fetch).await?; - - debug!(count = sorted_control_levels.len(), "power events"); - trace!(list = ?sorted_control_levels, "sorted power events"); - - let room_version = RoomVersion::new(room_version)?; - // Sequentially auth check each control event. - let resolved_control = iterative_auth_check( - &room_version, - sorted_control_levels - .iter() - .stream() - .map(AsRef::as_ref), - clean.clone(), - &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(); - - // 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); - - debug!(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"); - - 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?; - - // Add unconflicted state to the resolved state - // We priorities the unconflicting state - resolved_state.extend(clean); - - debug!("state resolution finished"); - - 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) -} - -/// 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, - 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, 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>, - 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, -{ - #[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` or return a default value -/// of zero. -/// -/// Do NOT use this any where but topological sort, we find the power level for -/// the eventId at the eventId's generation (we walk backwards to `EventId`s -/// most recent previous power level event). -async fn get_power_level_for_sender( - event_id: &EventId, - fetch_event: &F, -) -> serde_json::Result -where - F: Fn(OwnedEventId) -> Fut + Sync, - Fut: Future> + Send, - E: Event + Send, -{ - debug!("fetch event ({event_id}) senders power level"); - - let event = fetch_event(event_id.to_owned()).await; - - let auth_events = event.as_ref().map(Event::auth_events); - - let pl = auth_events - .into_iter() - .flatten() - .stream() - .broadn_filter_map(5, |aid| fetch_event(aid.to_owned())) - .ready_find(|aev| is_type_and_key(aev, &TimelineEventType::RoomPowerLevels, "")) - .await; - - let content: PowerLevelsContentFields = match pl { - | None => return Ok(int!(0)), - | Some(ev) => from_json_str(ev.content().get())?, - }; - - if let Some(ev) = event { - if let Some(&user_level) = content.get_user_power(ev.sender()) { - debug!("found {} at power_level {user_level}", ev.sender()); - return Ok(user_level); - } - } - - Ok(content.users_default) -} - -/// 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. -async fn iterative_auth_check<'a, E, F, Fut, S>( - room_version: &RoomVersion, - 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) - .broad_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?; - - let auth_event_ids: HashSet = events_to_check - .iter() - .flat_map(|event: &E| event.auth_events().map(ToOwned::to_owned)) - .collect(); - - 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; - - let auth_events = &auth_events; - let mut resolved_state = unconflicted_state; - for event in events_to_check { - 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(), - )?; - - let mut auth_state = StateMap::new(); - 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 - 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; - - debug!("event to check {:?}", event.event_id()); - - // The key for this is (eventType + a state_key of the signed token not sender) - // so search for it - let current_third_party = auth_state.iter().find_map(|(_, pdu)| { - (*pdu.event_type() == TimelineEventType::RoomThirdPartyInvite).then_some(pdu) - }); - - let fetch_state = |ty: &StateEventType, key: &str| { - future::ready( - auth_state - .get(&ty.with_state_key(key)) - .map(ToOwned::to_owned), - ) - }; - - let auth_result = - auth_check(room_version, &event, current_third_party, fetch_state).await; - - match auth_result { - | Ok(true) => { - // add event to resolved state map - resolved_state.insert( - event.event_type().with_state_key(state_key), - event.event_id().to_owned(), - ); - }, - | 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); - }, - } - } - - 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.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) - } -} - +mod event_auth; +mod event_format; +pub mod events; +mod fetch_state; +mod resolve; #[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, uint, - }; - use serde_json::{json, value::to_raw_value as to_raw_json_value}; - - use super::{ - StateMap, is_power_event, - room_version::RoomVersion, - 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, - }; - - 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, &fetcher) - .await - .unwrap(); - - let resolved_power = super::iterative_auth_check( - &RoomVersion::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::thread_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::>() - ); - } - - #[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; - } - } - - #[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(&RoomVersionId::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(&RoomVersionId::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], - ],); - } -} +mod test_utils; + +use self::{event_auth::check_state_dependent_auth_rules, fetch_state::FetchStateExt}; +pub use self::{ + event_auth::{AuthTypes, auth_check, auth_types_for_event}, + event_format::check_pdu_format, + resolve::{AuthSet, ConflictMap, StateMap, resolve, topological_sort}, +}; +use crate::matrix::TypeStateKey; 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 4f8158f6..00000000 --- a/src/core/matrix/state_res/power_levels.rs +++ /dev/null @@ -1,265 +0,0 @@ -use std::collections::BTreeMap; - -use ruma::{ - Int, OwnedUserId, UserId, - events::{TimelineEventType, room::power_levels::RoomPowerLevelsEventContent}, - power_levels::{NotificationPowerLevels, default_power_level}, - serde::{ - deserialize_v1_powerlevel, vec_deserialize_int_powerlevel_values, - vec_deserialize_v1_powerlevel_values, - }, -}; -use serde::Deserialize; -use serde_json::{Error, from_str as from_json_str}; - -use super::{Result, RoomVersion}; -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 From for RoomPowerLevelsEventContent { - fn from(int_pl: IntRoomPowerLevelsEventContent) -> Self { - let IntRoomPowerLevelsEventContent { - ban, - events, - events_default, - invite, - kick, - redact, - state_default, - users, - users_default, - notifications, - } = int_pl; - - let mut pl = Self::new(); - 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: &RoomVersion, -) -> Option { - if room_version.integer_power_levels { - deserialize_integer_power_levels(content) - } else { - deserialize_legacy_power_levels(content) - } -} - -fn deserialize_integer_power_levels(content: &str) -> Option { - match from_json_str::(content) { - | Ok(content) => Some(content.into()), - | 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 } - } -} - -#[inline] -pub(crate) fn deserialize_power_levels_content_fields( - content: &str, - room_version: &RoomVersion, -) -> Result { - if room_version.integer_power_levels { - deserialize_integer_power_levels_content_fields(content) - } else { - deserialize_legacy_power_levels_content_fields(content) - } -} - -fn deserialize_integer_power_levels_content_fields( - content: &str, -) -> Result { - from_json_str::(content).map(Into::into) -} - -fn deserialize_legacy_power_levels_content_fields( - content: &str, -) -> Result { - from_json_str(content) -} - -#[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: &RoomVersion, -) -> Result { - if room_version.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: &RoomVersion, -) -> Result { - if room_version.integer_power_levels { - from_json_str::(content).map(Into::into) - } else { - from_json_str(content) - } -} diff --git a/src/core/matrix/state_res/resolve.rs b/src/core/matrix/state_res/resolve.rs new file mode 100644 index 00000000..5810ba67 --- /dev/null +++ b/src/core/matrix/state_res/resolve.rs @@ -0,0 +1,213 @@ +#[cfg(test)] +mod tests; + +mod auth_difference; +mod conflicted_subgraph; +mod iterative_auth_check; +mod mainline_sort; +mod power_sort; +mod split_conflicted; +mod topological_sort; + +use std::collections::{BTreeMap, BTreeSet}; + +use futures::{FutureExt, Stream, StreamExt, TryFutureExt, future::OptionFuture}; +use ruma::{OwnedEventId, events::StateEventType, room_version_rules::RoomVersionRules}; + +pub use self::topological_sort::topological_sort; +use self::{ + auth_difference::auth_difference, conflicted_subgraph::conflicted_subgraph_dfs, + iterative_auth_check::iterative_auth_check, mainline_sort::mainline_sort, + power_sort::power_sort, split_conflicted::split_conflicted_state, +}; +#[cfg(test)] +use super::test_utils; +use crate::{ + Result, debug, + matrix::{Event, TypeStateKey}, + utils::stream::{BroadbandExt, IterStream}, +}; + +/// ConflictMap of OwnedEventId specifically. +pub type ConflictMap = StateMap; + +/// A mapping of event type and state_key to some value `T`, usually an +/// `EventId`. +pub type StateMap = BTreeMap; + +/// Full recursive set of `auth_events` for each event in a StateMap. +pub type AuthSet = BTreeSet; + +/// List of conflicting event_ids +type ConflictVec = Vec; + +/// Apply the [state resolution] algorithm introduced in room version 2 to +/// resolve the state of a room. +/// +/// ## Arguments +/// +/// * `rules` - The rules to apply for the version of the current room. +/// +/// * `state_maps` - The incoming states to resolve. Each `StateMap` represents +/// a possible fork in the state of a room. +/// +/// * `auth_chains` - The list of full recursive sets of `auth_events` for each +/// event in the `state_maps`. +/// +/// * `fetch_event` - Function to fetch an event in the room given its event ID. +/// +/// ## Invariants +/// +/// The caller of `resolve` must ensure that all the events are from the same +/// room. +/// +/// ## Returns +/// +/// The resolved room state. +/// +/// [state resolution]: https://spec.matrix.org/latest/rooms/v2/#state-resolution +#[tracing::instrument(level = "debug", skip_all)] +pub async fn resolve<'a, States, AuthSets, FetchExists, ExistsFut, FetchEvent, EventFut, Pdu>( + rules: &RoomVersionRules, + state_maps: States, + auth_sets: AuthSets, + fetch: &FetchEvent, + exists: &FetchExists, + backport_css: bool, +) -> Result> +where + States: Stream> + Send, + AuthSets: Stream> + Send, + FetchExists: Fn(OwnedEventId) -> ExistsFut + Sync, + ExistsFut: Future + Send, + FetchEvent: Fn(OwnedEventId) -> EventFut + Sync, + EventFut: Future> + Send, + Pdu: Event + Clone, +{ + // Split the unconflicted state map and the conflicted state set. + let (unconflicted_state, conflicted_states) = split_conflicted_state(state_maps).await; + + debug!(?unconflicted_state, unconflicted = unconflicted_state.len(), "unresolved state"); + debug!(?conflicted_states, conflicted = conflicted_states.len(), "unresolved states"); + + if conflicted_states.is_empty() { + return Ok(unconflicted_state.into_iter().collect()); + } + + let consider_conflicted_subgraph = rules + .state_res + .v2_rules() + .is_some_and(|rules| rules.consider_conflicted_state_subgraph) + || backport_css; + + // Since `org.matrix.hydra.11`, fetch the conflicted state subgraph. + let conflicted_subgraph: OptionFuture<_> = consider_conflicted_subgraph + .then(|| conflicted_states.clone().into_values().flatten()) + .map(async |ids| conflicted_subgraph_dfs(ids.stream(), fetch)) + .into(); + + let conflicted_subgraph = conflicted_subgraph + .await + .into_iter() + .stream() + .flatten(); + + // 0. The full conflicted set is the union of the conflicted state set and the + // auth difference. Don't honor events that don't exist. + let full_conflicted_set: AuthSet<_> = auth_difference(auth_sets) + .chain(conflicted_states.into_values().flatten().stream()) + .broad_filter_map(async |id| exists(id.clone()).await.then_some(id)) + .chain(conflicted_subgraph) + .collect::>() + .inspect(|set| debug!(count = set.len(), "full conflicted set")) + .inspect(|set| debug!(?set, "full conflicted set")) + .await; + + // 1. Select the set X of all power events that appear in the full conflicted + // set. For each such power event P, enlarge X by adding the events in the + // auth chain of P which also belong to the full conflicted set. Sort X into + // a list using the reverse topological power ordering. + let sorted_power_events: Vec<_> = power_sort(rules, &full_conflicted_set, fetch) + .inspect_ok(|list| debug!(count = list.len(), "sorted power events")) + .inspect_ok(|list| debug!(?list, "sorted power events")) + .await?; + + let sorted_power_events_set: AuthSet<_> = sorted_power_events.iter().collect(); + + let sorted_power_events = sorted_power_events + .iter() + .stream() + .map(AsRef::as_ref); + + let start_with_incoming_state = rules + .state_res + .v2_rules() + .is_none_or(|r| !r.begin_iterative_auth_checks_with_empty_state_map); + + let initial_state = start_with_incoming_state + .then(|| unconflicted_state.clone()) + .unwrap_or_default(); + + // 2. Apply the iterative auth checks algorithm, starting from the unconflicted + // state map, to the list of events from the previous step to get a partially + // resolved state. + let partially_resolved_state = + iterative_auth_check(rules, sorted_power_events, initial_state, fetch) + .inspect_ok(|map| debug!(count = map.len(), "partially resolved power state")) + .inspect_ok(|map| debug!(?map, "partially resolved power state")) + .await?; + + // This "epochs" power level event + let power_ty_sk = (StateEventType::RoomPowerLevels, "".into()); + let power_event = partially_resolved_state.get(&power_ty_sk); + debug!(event_id = ?power_event, "epoch power event"); + + let remaining_events: Vec<_> = full_conflicted_set + .into_iter() + .filter(|id| !sorted_power_events_set.contains(id)) + .collect(); + + debug!(count = remaining_events.len(), "remaining events"); + debug!(list = ?remaining_events, "remaining events"); + + let have_remaining_events = !remaining_events.is_empty(); + let remaining_events = remaining_events + .iter() + .stream() + .map(AsRef::as_ref); + + // 3. Take all remaining events that weren’t picked in step 1 and order them by + // the mainline ordering based on the power level in the partially resolved + // state obtained in step 2. + let sorted_remaining_events: OptionFuture<_> = have_remaining_events + .then(move || mainline_sort(power_event.cloned(), remaining_events, fetch)) + .into(); + + let sorted_remaining_events = sorted_remaining_events + .await + .unwrap_or(Ok(Vec::new()))?; + + debug!(count = sorted_remaining_events.len(), "sorted remaining events"); + debug!(list = ?sorted_remaining_events, "sorted remaining events"); + + let sorted_remaining_events = sorted_remaining_events + .iter() + .stream() + .map(AsRef::as_ref); + + // 4. Apply the iterative auth checks algorithm on the partial resolved state + // and the list of events from the previous step. + let mut resolved_state = + iterative_auth_check(rules, sorted_remaining_events, partially_resolved_state, fetch) + .await?; + + // 5. Update the result by replacing any event with the event with the same key + // from the unconflicted state map, if such an event exists, to get the final + // resolved state. + resolved_state.extend(unconflicted_state); + + debug!(resolved_state = resolved_state.len(), "resolved state"); + debug!(?resolved_state, "resolved state"); + + Ok(resolved_state) +} diff --git a/src/core/matrix/state_res/resolve/auth_difference.rs b/src/core/matrix/state_res/resolve/auth_difference.rs new file mode 100644 index 00000000..fd6fde1e --- /dev/null +++ b/src/core/matrix/state_res/resolve/auth_difference.rs @@ -0,0 +1,40 @@ +use std::borrow::Borrow; + +use futures::{FutureExt, Stream}; +use ruma::EventId; + +use super::AuthSet; +use crate::utils::stream::{IterStream, ReadyExt}; + +/// Get the auth difference for the given auth chains. +/// +/// Definition in the specification: +/// +/// The auth difference is calculated by first calculating the full auth chain +/// for each state _Si_, that is the union of the auth chains for each event in +/// _Si_, and then taking every event that doesn’t appear in every auth chain. +/// If _Ci_ is the full auth chain of _Si_, then the auth difference is ∪_Ci_ − +/// ∩_Ci_. +/// +/// ## Arguments +/// +/// * `auth_chains` - The list of full recursive sets of `auth_events`. Inputs +/// must be sorted. +/// +/// ## Returns +/// +/// Outputs the event IDs that are not present in all the auth chains. +pub(super) fn auth_difference<'a, AuthSets, Id>(auth_sets: AuthSets) -> impl Stream +where + AuthSets: Stream>, + Id: Borrow + Clone + Eq + Ord + Send + 'a, +{ + auth_sets + .ready_fold_default(|ret: AuthSet, set| { + ret.symmetric_difference(&set) + .cloned() + .collect::>() + }) + .map(|set: AuthSet| set.into_iter().stream()) + .flatten_stream() +} diff --git a/src/core/matrix/state_res/resolve/conflicted_subgraph.rs b/src/core/matrix/state_res/resolve/conflicted_subgraph.rs new file mode 100644 index 00000000..f873bc02 --- /dev/null +++ b/src/core/matrix/state_res/resolve/conflicted_subgraph.rs @@ -0,0 +1,124 @@ +use std::{ + collections::HashSet as Set, + mem::take, + sync::{Arc, Mutex}, +}; + +use futures::{Future, FutureExt, Stream, StreamExt}; +use ruma::OwnedEventId; + +use crate::{ + Result, + matrix::Event, + utils::stream::{IterStream, automatic_width}, +}; + +#[derive(Default)] +struct Global { + subgraph: Mutex>, + seen: Mutex>, +} + +#[derive(Default)] +struct Local { + path: Vec, + stack: Vec>, +} + +pub(super) fn conflicted_subgraph_dfs( + conflicted_event_ids: ConflictedEventIds, + fetch: &Fetch, +) -> impl Stream + Send +where + ConflictedEventIds: Stream + Send, + Fetch: Fn(OwnedEventId) -> Fut + Sync, + Fut: Future> + Send, + Pdu: Event, +{ + conflicted_event_ids + .collect::>() + .map(|ids| (Arc::new(Global::default()), ids)) + .then(async |(state, conflicted_event_ids)| { + conflicted_event_ids + .iter() + .stream() + .map(ToOwned::to_owned) + .map(|event_id| (state.clone(), event_id)) + .for_each_concurrent(automatic_width(), async |(state, event_id)| { + subgraph_descent(state, event_id, &conflicted_event_ids, fetch) + .await + .expect("only mutex errors expected"); + }) + .await; + + let mut state = state.subgraph.lock().expect("locked"); + take(&mut *state) + }) + .map(Set::into_iter) + .map(IterStream::stream) + .flatten_stream() +} + +async fn subgraph_descent( + state: Arc, + conflicted_event_id: OwnedEventId, + conflicted_event_ids: &Set, + fetch: &Fetch, +) -> Result> +where + Fetch: Fn(OwnedEventId) -> Fut + Sync, + Fut: Future> + Send, + Pdu: Event, +{ + let Global { subgraph, seen } = &*state; + + let mut local = Local { + path: vec![conflicted_event_id.clone()], + stack: vec![vec![conflicted_event_id]], + }; + + while let Some(event_id) = pop(&mut local) { + if subgraph.lock()?.contains(&event_id) { + if local.path.len() > 1 { + subgraph + .lock()? + .extend(local.path.iter().cloned()); + } + + local.path.pop(); + continue; + } + + if !seen.lock()?.insert(event_id.clone()) { + continue; + } + + if local.path.len() > 1 && conflicted_event_ids.contains(&event_id) { + subgraph + .lock()? + .extend(local.path.iter().cloned()); + } + + if let Ok(event) = fetch(event_id).await { + local + .stack + .push(event.auth_events_into().into_iter().collect()); + } + } + + Ok(state) +} + +fn pop(local: &mut Local) -> Option { + let Local { path, stack } = local; + + while stack.last().is_some_and(Vec::is_empty) { + stack.pop(); + path.pop(); + } + + stack + .last_mut() + .and_then(Vec::pop) + .inspect(|event_id| path.push(event_id.clone())) +} diff --git a/src/core/matrix/state_res/resolve/iterative_auth_check.rs b/src/core/matrix/state_res/resolve/iterative_auth_check.rs new file mode 100644 index 00000000..792d2e43 --- /dev/null +++ b/src/core/matrix/state_res/resolve/iterative_auth_check.rs @@ -0,0 +1,203 @@ +use futures::{Stream, StreamExt, TryFutureExt, TryStreamExt}; +use ruma::{ + EventId, OwnedEventId, + events::{StateEventType, TimelineEventType}, + room_version_rules::RoomVersionRules, +}; + +use super::{ + super::{auth_types_for_event, check_state_dependent_auth_rules}, + StateMap, +}; +use crate::{ + Error, Result, debug_warn, err, error, + matrix::{Event, EventTypeExt, StateKey}, + trace, + utils::stream::{IterStream, ReadyExt, TryReadyExt, TryWidebandExt}, +}; + +/// Perform the iterative auth checks to the given list of events. +/// +/// Definition in the specification: +/// +/// The iterative auth checks algorithm takes as input an initial room state and +/// a sorted list of state events, and constructs a new room state by iterating +/// through the event list and applying the state event to the room state if the +/// state event is allowed by the authorization rules. If the state event is not +/// allowed by the authorization rules, then the event is ignored. If a +/// (event_type, state_key) key that is required for checking the authorization +/// rules is not present in the state, then the appropriate state event from the +/// event’s auth_events is used if the auth event is not rejected. +/// +/// ## Arguments +/// +/// * `rules` - The authorization rules for the current room version. +/// * `events` - The sorted state events to apply to the `partial_state`. +/// * `state` - The current state that was partially resolved for the room. +/// * `fetch_event` - Function to fetch an event in the room given its event ID. +/// +/// ## Returns +/// +/// Returns the partially resolved state, or an `Err(_)` if one of the state +/// events in the room has an unexpected format. +#[tracing::instrument( + name = "iterative_auth", + level = "debug", + skip_all, + fields( + states = ?state.len(), + ) +)] +pub(super) async fn iterative_auth_check<'b, SortedPowerEvents, Fetch, Fut, Pdu>( + rules: &RoomVersionRules, + events: SortedPowerEvents, + state: StateMap, + fetch: &Fetch, +) -> Result> +where + SortedPowerEvents: Stream + Send, + Fetch: Fn(OwnedEventId) -> Fut + Sync, + Fut: Future> + Send, + Pdu: Event, +{ + events + .map(Ok) + .wide_and_then(async |event_id| { + let event = fetch(event_id.to_owned()).await?; + let state_key: StateKey = event + .state_key() + .ok_or_else(|| err!(Request(InvalidParam("Missing state_key"))))? + .into(); + + Ok((event_id, state_key, event)) + }) + .try_fold(state, |state, (event_id, state_key, event)| { + auth_check(rules, state, event_id, state_key, event, fetch) + }) + .await +} + +#[tracing::instrument( + name = "check", + level = "debug", + skip_all, + fields( + %event_id, + %state_key, + ) +)] +async fn auth_check( + rules: &RoomVersionRules, + mut state: StateMap, + event_id: &EventId, + state_key: StateKey, + event: Pdu, + fetch: &Fetch, +) -> Result> +where + Fetch: Fn(OwnedEventId) -> Fut + Sync, + Fut: Future> + Send, + Pdu: Event, +{ + let Ok(auth_types) = auth_types_for_event( + event.event_type(), + event.sender(), + Some(&state_key), + event.content(), + &rules.authorization, + true, + ) + .inspect_err(|e| error!("failed to get auth types for event: {e}")) else { + return Ok(state); + }; + + let auth_types_events = auth_types + .stream() + .ready_filter_map(|key| { + state + .get(&key) + .map(move |auth_event_id| (auth_event_id, key)) + }) + .filter_map(async |(id, key)| { + fetch(id.clone()) + .inspect_err(|e| debug_warn!(%id, "missing auth event: {e}")) + .inspect_err(|e| debug_assert!(!cfg!(test), "missing auth {id:?}: {e:?}")) + .map_ok(move |auth_event| (key, auth_event)) + .await + .ok() + }) + .ready_filter_map(|(key, auth_event)| { + auth_event + .rejected() + .eq(&false) + .then_some((key, auth_event)) + }) + .map(Ok); + + // If the `m.room.create` event is not in the auth events, we need to add it, + // because it's always part of the state and required in the auth rules. + let also_need_create_event = *event.event_type() != TimelineEventType::RoomCreate + && rules + .authorization + .room_create_event_id_as_room_id; + + let also_create_id: Option = also_need_create_event + .then(|| event.room_id().as_event_id().ok()) + .flatten(); + + let auth_events = event + .auth_events() + .chain(also_create_id.as_deref().into_iter()) + .stream() + .filter_map(async |id| { + fetch(id.to_owned()) + .inspect_err(|e| debug_warn!(%id, "missing auth event: {e}")) + .inspect_err(|e| debug_assert!(!cfg!(test), "missing auth {id:?}: {e:?}")) + .await + .ok() + }) + .map(Result::::Ok) + .ready_try_filter_map(|auth_event| { + let state_key = auth_event + .state_key() + .ok_or_else(|| err!(Request(InvalidParam("Missing state_key"))))?; + + let key_val = auth_event + .rejected() + .eq(&false) + .then_some((auth_event.event_type().with_state_key(state_key), auth_event)); + + Ok(key_val) + }); + + let auth_events: Vec<_> = auth_events + .chain(auth_types_events) + .try_collect() + .map_ok(|mut vec: Vec<_>| { + vec.sort_by(|a, b| a.0.cmp(&b.0)); + vec.reverse(); + vec.dedup_by(|a, b| a.0.eq(&b.0)); + vec + }) + .await?; + + let fetch_state = async |ty: StateEventType, key: StateKey| -> Result { + trace!(?ty, ?key, auth_events = auth_events.len(), "fetch state"); + auth_events + .binary_search_by(|a| ty.cmp(&a.0.0).then(key.cmp(&a.0.1))) + .map(|i| auth_events[i].1.clone()) + .map_err(|_| err!(Request(NotFound("Missing auth_event {ty:?},{key:?}")))) + }; + + // Add authentic event to the partially resolved state. + if check_state_dependent_auth_rules(rules, &event, &fetch_state) + .await + .inspect_err(|e| debug_warn!("event failed auth check: {e}")) + .is_ok() + { + let key = event.event_type().with_state_key(state_key); + state.insert(key, event_id.to_owned()); + } + + Ok(state) +} diff --git a/src/core/matrix/state_res/resolve/mainline_sort.rs b/src/core/matrix/state_res/resolve/mainline_sort.rs new file mode 100644 index 00000000..c1412def --- /dev/null +++ b/src/core/matrix/state_res/resolve/mainline_sort.rs @@ -0,0 +1,171 @@ +use std::collections::HashMap; + +use futures::{Stream, StreamExt, TryStreamExt, pin_mut}; +use ruma::{EventId, OwnedEventId, events::TimelineEventType}; + +use crate::{ + Result, + matrix::Event, + trace, + utils::stream::{IterStream, TryReadyExt, WidebandExt}, +}; + +/// Perform mainline ordering of the given events. +/// +/// Definition in the spec: +/// Given mainline positions calculated from P, the mainline ordering based on P +/// of a set of events is the ordering, from smallest to largest, using the +/// following comparison relation on events: for events x and y, x < y if +/// +/// 1. the mainline position of x is greater than the mainline position of y +/// (i.e. the auth chain of x is based on an earlier event in the mainline +/// than y); or +/// 2. the mainline positions of the events are the same, but x’s +/// origin_server_ts is less than y’s origin_server_ts; or +/// 3. the mainline positions of the events are the same and the events have the +/// same origin_server_ts, but x’s event_id is less than y’s event_id. +/// +/// ## Arguments +/// +/// * `events` - The list of event IDs to sort. +/// * `power_level` - The power level event in the current state. +/// * `fetch_event` - Function to fetch an event in the room given its event ID. +/// +/// ## Returns +/// +/// Returns the sorted list of event IDs, or an `Err(_)` if one the event in the +/// room has an unexpected format. +#[tracing::instrument( + level = "debug", + skip_all, + fields( + ?power_level, + ) +)] +pub(super) async fn mainline_sort<'a, RemainingEvents, Fetch, Fut, Pdu>( + mut power_level: Option, + events: RemainingEvents, + fetch: &Fetch, +) -> Result> +where + RemainingEvents: Stream + Send, + Fetch: Fn(OwnedEventId) -> Fut + Sync, + Fut: Future> + Send, + Pdu: Event, +{ + // Populate the mainline of the power level. + let mut mainline = vec![]; + while let Some(power_level_event_id) = power_level { + let power_level_event = fetch(power_level_event_id).await?; + + mainline.push(power_level_event.event_id().to_owned()); + power_level = get_power_levels_auth_event(&power_level_event, fetch) + .await? + .map(|event| event.event_id().to_owned()); + } + + let mainline_map: HashMap<_, _> = mainline + .iter() + .rev() + .enumerate() + .map(|(idx, event_id)| (event_id.clone(), idx)) + .collect(); + + let order_map: HashMap<_, _> = events + .wide_filter_map(async |event_id| { + let event = fetch(event_id.to_owned()).await.ok()?; + let position = mainline_position(&event, &mainline_map, fetch) + .await + .ok()?; + + let event_id = event.event_id().to_owned(); + let origin_server_ts = event.origin_server_ts(); + Some((event_id, (position, origin_server_ts))) + }) + .collect() + .await; + + let mut sorted_event_ids: Vec<_> = order_map.keys().cloned().collect(); + + sorted_event_ids.sort_by(|a, b| { + let (a_pos, a_ots) = &order_map[a]; + let (b_pos, b_ots) = &order_map[b]; + a_pos + .cmp(b_pos) + .then(a_ots.cmp(b_ots)) + .then(a.cmp(b)) + }); + + Ok(sorted_event_ids) +} + +/// Get the mainline position of the given event from the given mainline map. +/// +/// ## Arguments +/// +/// * `event` - The event to compute the mainline position of. +/// * `mainline_map` - The mainline map of the m.room.power_levels event. +/// * `fetch` - Function to fetch an event in the room given its event ID. +/// +/// ## Returns +/// +/// Returns the mainline position of the event, or an `Err(_)` if one of the +/// events in the auth chain of the event was not found. +#[tracing::instrument( + level = "trace", + skip_all, + fields( + event = ?event.event_id(), + mainline = mainline_map.len(), + ) +)] +async fn mainline_position( + event: &Pdu, + mainline_map: &HashMap, + fetch: &Fetch, +) -> Result +where + Fetch: Fn(OwnedEventId) -> Fut + Sync, + Fut: Future>, + Pdu: Event, +{ + let mut current_event = Some(event.clone()); + while let Some(event) = current_event { + trace!(event_id = ?event.event_id(), "mainline"); + + // If the current event is in the mainline map, return its position. + if let Some(position) = mainline_map.get(event.event_id()) { + return Ok(*position); + } + + // Look for the power levels event in the auth events. + current_event = get_power_levels_auth_event(&event, fetch).await?; + } + + // Did not find a power level event so we default to zero. + Ok(0) +} + +#[allow(clippy::redundant_closure)] +#[tracing::instrument(level = "trace", skip_all)] +async fn get_power_levels_auth_event( + event: &Pdu, + fetch: &Fetch, +) -> Result> +where + Fetch: Fn(OwnedEventId) -> Fut + Sync, + Fut: Future>, + Pdu: Event, +{ + let power_level_event = event + .auth_events() + .try_stream() + .map_ok(ToOwned::to_owned) + .and_then(|auth_event_id| fetch(auth_event_id)) + .ready_try_skip_while(|auth_event| { + Ok(!auth_event.is_type_and_state_key(&TimelineEventType::RoomPowerLevels, "")) + }); + + pin_mut!(power_level_event); + power_level_event.try_next().await +} diff --git a/src/core/matrix/state_res/resolve/power_sort.rs b/src/core/matrix/state_res/resolve/power_sort.rs new file mode 100644 index 00000000..be471a9b --- /dev/null +++ b/src/core/matrix/state_res/resolve/power_sort.rs @@ -0,0 +1,273 @@ +use std::{ + borrow::Borrow, + collections::{HashMap, HashSet}, +}; + +use futures::{StreamExt, TryFutureExt, TryStreamExt}; +use ruma::{ + EventId, OwnedEventId, + events::{TimelineEventType, room::power_levels::UserPowerLevel}, + room_version_rules::RoomVersionRules, +}; + +use super::{ + super::events::{ + RoomCreateEvent, RoomPowerLevelsEvent, RoomPowerLevelsIntField, is_power_event, + power_levels::RoomPowerLevelsEventOptionExt, + }, + AuthSet, topological_sort, +}; +use crate::{ + Result, err, + matrix::Event, + utils::stream::{BroadbandExt, IterStream, TryBroadbandExt}, +}; + +/// Enlarge the given list of conflicted power events by adding the events in +/// their auth chain that are in the full conflicted set, and sort it using +/// reverse topological power ordering. +/// +/// ## Arguments +/// +/// * `conflicted_power_events` - The list of power events in the full +/// conflicted set. +/// +/// * `full_conflicted_set` - The full conflicted set. +/// +/// * `rules` - The authorization rules for the current room version. +/// +/// * `fetch` - Function to fetch an event in the room given its event ID. +/// +/// ## Returns +/// +/// Returns the ordered list of event IDs from earliest to latest. +#[tracing::instrument( + level = "debug", + skip_all, + fields( + full_conflicted = full_conflicted_set.len(), + ) +)] +pub(super) async fn power_sort( + rules: &RoomVersionRules, + full_conflicted_set: &AuthSet, + fetch: &Fetch, +) -> Result> +where + Fetch: Fn(OwnedEventId) -> Fut + Sync, + Fut: Future> + Send, + Pdu: Event, +{ + // A representation of the DAG, a map of event ID to its list of auth events + // that are in the full conflicted set. Fill the graph. + let graph = full_conflicted_set + .iter() + .stream() + .broad_filter_map(async |id| is_power_event_id(id, fetch).await.then_some(id)) + .fold(HashMap::new(), |graph, event_id| { + add_event_auth_chain(graph, full_conflicted_set, event_id, fetch) + }) + .await; + + // The map of event ID to the power level of the sender of the event. + // Get the power level of the sender of each event in the graph. + let event_to_power_level: HashMap<_, _> = graph + .keys() + .try_stream() + .map_ok(AsRef::as_ref) + .broad_and_then(|event_id| { + power_level_for_sender(event_id, rules, fetch) + .map_ok(move |sender_power| (event_id, sender_power)) + .map_err(|e| err!(Request(NotFound("Missing PL for sender: {e}")))) + }) + .try_collect() + .await?; + + let query = async |event_id: OwnedEventId| { + let power_level = *event_to_power_level + .get(&event_id.borrow()) + .ok_or_else(|| err!(Request(NotFound("Missing PL event: {event_id}"))))?; + + let event = fetch(event_id).await?; + Ok((power_level, event.origin_server_ts())) + }; + + topological_sort(&graph, &query).await +} + +/// Add the event with the given event ID and all the events in its auth chain +/// that are in the full conflicted set to the graph. +#[tracing::instrument( + level = "trace", + skip_all, + fields( + ?event_id, + graph = graph.len(), + ) +)] +async fn add_event_auth_chain( + mut graph: HashMap>, + full_conflicted_set: &AuthSet, + event_id: &EventId, + fetch: &Fetch, +) -> HashMap> +where + Fetch: Fn(OwnedEventId) -> Fut + Sync, + Fut: Future> + Send, + Pdu: Event, +{ + let mut state = vec![event_id.to_owned()]; + + // Iterate through the auth chain of the event. + while let Some(event_id) = state.pop() { + // Iterate through the auth events of this event. + let event = fetch(event_id.clone()).await.ok(); + + // Add the current event to the graph. + graph.entry(event_id).or_default(); + + let auth_events = event + .as_ref() + .map(Event::auth_events) + .into_iter() + .flatten(); + + for auth_event_id in auth_events { + // If the auth event ID is in the full conflicted set… + if !full_conflicted_set.contains(auth_event_id) { + continue; + } + + // If the auth event ID is not in the graph, we need to check its auth events + // later. + if !graph.contains_key(auth_event_id) { + state.push(auth_event_id.to_owned()); + } + + let event_id = event + .as_ref() + .expect("event is Some if there are auth_events") + .event_id(); + + // Add the auth event ID to the list of incoming edges. + graph + .get_mut(event_id) + .expect("event_id must be added to graph") + .insert(auth_event_id.to_owned()); + } + } + + graph +} + +/// Find the power level for the sender of the event of the given event ID or +/// return a default value of zero. +/// +/// We find the most recent `m.room.power_levels` by walking backwards in the +/// auth chain of the event. +/// +/// Do NOT use this anywhere but topological sort. +/// +/// ## Arguments +/// +/// * `event_id` - The event ID of the event to get the power level of the +/// sender of. +/// +/// * `rules` - The authorization rules for the current room version. +/// +/// * `fetch` - Function to fetch an event in the room given its event ID. +/// +/// ## Returns +/// +/// Returns the power level of the sender of the event or an `Err(_)` if one of +/// the auth events if malformed. +#[tracing::instrument( + level = "trace", + skip_all, + fields( + ?event_id, + ) +)] +async fn power_level_for_sender( + event_id: &EventId, + rules: &RoomVersionRules, + fetch: &Fetch, +) -> Result +where + Fetch: Fn(OwnedEventId) -> Fut + Sync, + Fut: Future> + Send, + Pdu: Event, +{ + let mut room_create_event = None; + let mut room_power_levels_event = None; + let event = fetch(event_id.to_owned()).await; + if let Ok(event) = &event { + if rules + .authorization + .room_create_event_id_as_room_id + { + let create_id = event.room_id().as_event_id()?; + let fetched = fetch(create_id).await?; + room_create_event = Some(RoomCreateEvent::new(fetched)); + } + } + + for auth_event_id in event + .as_ref() + .map(Event::auth_events) + .into_iter() + .flatten() + { + if let Ok(auth_event) = fetch(auth_event_id.to_owned()).await { + if auth_event.is_type_and_state_key(&TimelineEventType::RoomPowerLevels, "") { + room_power_levels_event = Some(RoomPowerLevelsEvent::new(auth_event)); + } else if !rules + .authorization + .room_create_event_id_as_room_id + && auth_event.is_type_and_state_key(&TimelineEventType::RoomCreate, "") + { + room_create_event = Some(RoomCreateEvent::new(auth_event)); + } + + if room_power_levels_event.is_some() && room_create_event.is_some() { + break; + } + } + } + + let auth_rules = &rules.authorization; + let creators = room_create_event + .as_ref() + .and_then(|event| event.creators(auth_rules).ok()); + + if let Some((event, creators)) = event.ok().zip(creators) { + room_power_levels_event.user_power_level(event.sender(), creators, auth_rules) + } else { + room_power_levels_event + .get_as_int_or_default(RoomPowerLevelsIntField::UsersDefault, auth_rules) + .map(Into::into) + } +} + +/// Whether the given event ID belongs to a power event. +/// +/// See the docs of `is_power_event()` for the definition of a power event. +#[tracing::instrument( + name = "is_power_event", + level = "trace", + skip_all, + fields( + ?event_id, + ) +)] +async fn is_power_event_id(event_id: &EventId, fetch: &Fetch) -> bool +where + Fetch: Fn(OwnedEventId) -> Fut + Sync, + Fut: Future> + Send, + Pdu: Event, +{ + match fetch(event_id.to_owned()).await { + | Ok(state) => is_power_event(&state), + | _ => false, + } +} diff --git a/src/core/matrix/state_res/resolve/split_conflicted.rs b/src/core/matrix/state_res/resolve/split_conflicted.rs new file mode 100644 index 00000000..ee950f65 --- /dev/null +++ b/src/core/matrix/state_res/resolve/split_conflicted.rs @@ -0,0 +1,70 @@ +use std::{collections::HashMap, hash::Hash}; + +use futures::{Stream, StreamExt}; + +use super::StateMap; +use crate::validated; + +/// Split the unconflicted state map and the conflicted state set. +/// +/// Definition in the specification: +/// +/// If a given key _K_ is present in every _Si_ with the same value _V_ in each +/// state map, then the pair (_K_, _V_) belongs to the unconflicted state map. +/// Otherwise, _V_ belongs to the conflicted state set. +/// +/// It means that, for a given (event type, state key) tuple, if all state maps +/// have the same event ID, it lands in the unconflicted state map, otherwise +/// the event IDs land in the conflicted state set. +/// +/// ## Arguments +/// +/// * `state_maps` - The incoming states to resolve. Each `StateMap` represents +/// a possible fork in the state of a room. +/// +/// ## Returns +/// +/// Returns an `(unconflicted_state, conflicted_states)` tuple. +pub(super) async fn split_conflicted_state<'a, Maps, Id>( + state_maps: Maps, +) -> (StateMap, StateMap>) +where + Maps: Stream>, + Id: Clone + Eq + Hash + Ord + Send + Sync + 'a, +{ + let state_maps: Vec<_> = state_maps.collect().await; + + let mut state_set_count = 0_usize; + let mut occurrences = HashMap::<_, HashMap<_, usize>>::new(); + let state_maps = state_maps.iter().inspect(|_state| { + state_set_count = validated!(state_set_count + 1); + }); + + for (k, v) in state_maps.into_iter().flat_map(|s| s.iter()) { + let acc = occurrences + .entry(k.clone()) + .or_default() + .entry(v.clone()) + .or_default(); + + *acc = acc.saturating_add(1); + } + + let mut unconflicted_state_map = StateMap::new(); + let mut conflicted_state_set = StateMap::>::new(); + + for (k, v) in occurrences { + for (id, occurrence_count) in v { + if occurrence_count == state_set_count { + unconflicted_state_map.insert((k.0.clone(), k.1.clone()), id.clone()); + } else { + conflicted_state_set + .entry((k.0.clone(), k.1.clone())) + .or_default() + .push(id.clone()); + } + } + } + + (unconflicted_state_map, conflicted_state_set) +} diff --git a/src/core/matrix/state_res/resolve/tests.rs b/src/core/matrix/state_res/resolve/tests.rs new file mode 100644 index 00000000..cac26a6e --- /dev/null +++ b/src/core/matrix/state_res/resolve/tests.rs @@ -0,0 +1,840 @@ +use std::collections::HashMap; + +use futures::StreamExt; +use maplit::{hashmap, hashset}; +use rand::seq::SliceRandom; +use ruma::{ + MilliSecondsSinceUnixEpoch, OwnedEventId, + 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::{ + AuthSet, StateMap, + test_utils::{ + INITIAL_EVENTS, TestStore, alice, bob, charlie, do_check, ella, event_id, + member_content_ban, member_content_join, not_found, room_id, to_init_pdu_event, + to_pdu_event, zara, + }, +}; +use crate::{ + debug, + matrix::{Event, EventTypeExt, PduEvent}, + utils::stream::IterStream, +}; + +async fn test_event_sort() { + _ = tracing::subscriber::set_default( + tracing_subscriber::fmt() + .with_test_writer() + .finish(), + ); + + let rules = RoomVersionRules::V6; + let events = INITIAL_EVENTS(); + + let auth_chain: AuthSet = AuthSet::new(); + + let sorted_power_events = super::power_sort(&rules, &auth_chain, &async |id| { + events.get(&id).cloned().ok_or_else(not_found) + }) + .await + .unwrap(); + + let sorted_power_events = sorted_power_events + .iter() + .stream() + .map(AsRef::as_ref); + + let resolved_power = + super::iterative_auth_check(&rules, sorted_power_events, StateMap::new(), &async |id| { + events.get(&id).cloned().ok_or_else(not_found) + }) + .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::thread_rng()); + + let power_level = resolved_power + .get(&(StateEventType::RoomPowerLevels, "".into())) + .cloned(); + + let events_to_sort = events_to_sort.iter().stream().map(AsRef::as_ref); + + let sorted_event_ids = super::mainline_sort(power_level, events_to_sort, &async |id| { + events.get(&id).cloned().ok_or_else(not_found) + }) + .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(ToString::to_string) + .collect::>() + ); +} + +#[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; + } +} + +#[tokio::test] +async fn ban_vs_power_level() { + _ = 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() { + _ = 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() { + _ = 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() { + _ = 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() { + _ = 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() { + _ = tracing::subscriber::set_default( + tracing_subscriber::fmt() + .with_test_writer() + .finish(), + ); + + let events = vec![ + 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() { + _ = 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 state_sets = [state_at_bob, state_at_charlie]; + let auth_chains = state_sets + .iter() + .map(|map| { + store + .auth_event_ids(room_id(), map.values().cloned().collect()) + .unwrap() + }) + .collect::>(); + + let rules = RoomVersionRules::V1; + let resolved = match super::resolve( + &rules, + state_sets.into_iter().stream(), + auth_chains.into_iter().stream(), + &async |id| ev_map.get(&id).cloned().ok_or_else(not_found), + &async |id| ev_map.contains_key(&id), + false, + ) + .await + { + | Ok(state) => state, + | Err(e) => panic!("{e}"), + }; + + assert_eq!(expected, resolved); +} + +#[tokio::test] +async fn test_reverse_topological_power_sort() { + _ = 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::topological_sort(&graph, &async |_id| { + Ok((int!(0).into(), 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() { + _ = 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() { + _ = 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[&event_id("CREATE")], + &inner[&event_id("IJR")], + &inner[&event_id("IMA")], + &inner[&event_id("IMB")], + &inner[&event_id("IMC")], + &inner[&event_id("MB")], + &inner[&event_id("PA")], + ] + .iter() + .map(|ev| { + ( + ev.event_type() + .with_state_key(ev.state_key().unwrap()), + ev.event_id.clone(), + ) + }) + .collect::>(); + + let state_set_b = [ + &inner[&event_id("CREATE")], + &inner[&event_id("IJR")], + &inner[&event_id("IMA")], + &inner[&event_id("IMB")], + &inner[&event_id("IMC")], + &inner[&event_id("IME")], + &inner[&event_id("PA")], + ] + .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_chains = state_sets + .iter() + .map(|map| { + store + .auth_event_ids(room_id(), map.values().cloned().collect()) + .unwrap() + }) + .collect::>(); + + let resolved = match super::resolve( + &RoomVersionRules::V6, + state_sets.into_iter().stream(), + auth_chains.into_iter().stream(), + &async |id| ev_map.get(&id).cloned().ok_or_else(not_found), + &async |id| ev_map.contains_key(&id), + false, + ) + .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 => $key:expr => $id:expr),* $(,)?) => {{ + #[allow(unused_mut)] + let mut x = StateMap::new(); + $( + x.insert(($kind, $key.into()), $id); + )* + x + }}; +} + +#[tokio::test] +async fn split_conflicted_state_set_conflicted_unique_state_keys() { + let (unconflicted, conflicted) = super::split_conflicted_state( + [ + state_set![StateEventType::RoomMember => "@a:hs1" => 0], + state_set![StateEventType::RoomMember => "@b:hs1" => 1], + state_set![StateEventType::RoomMember => "@c:hs1" => 2], + ] + .into_iter() + .stream(), + ) + .await; + + let (unconflicted, conflicted): (StateMap<_>, StateMap<_>) = + (unconflicted.into_iter().collect(), conflicted.into_iter().collect()); + + 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], + ],); +} + +#[tokio::test] +async fn split_conflicted_state_set_conflicted_same_state_key() { + let (unconflicted, conflicted) = super::split_conflicted_state( + [ + state_set![StateEventType::RoomMember => "@a:hs1" => 0], + state_set![StateEventType::RoomMember => "@a:hs1" => 1], + state_set![StateEventType::RoomMember => "@a:hs1" => 2], + ] + .into_iter() + .stream(), + ) + .await; + + let (unconflicted, mut conflicted): (StateMap<_>, StateMap<_>) = + (unconflicted.into_iter().collect(), conflicted.into_iter().collect()); + + // 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], + ],); +} + +#[tokio::test] +async fn split_conflicted_state_set_unconflicted() { + let (unconflicted, conflicted) = super::split_conflicted_state( + [ + state_set![StateEventType::RoomMember => "@a:hs1" => 0], + state_set![StateEventType::RoomMember => "@a:hs1" => 0], + state_set![StateEventType::RoomMember => "@a:hs1" => 0], + ] + .into_iter() + .stream(), + ) + .await; + + let (unconflicted, conflicted): (StateMap<_>, StateMap<_>) = + (unconflicted.into_iter().collect(), conflicted.into_iter().collect()); + + assert_eq!(unconflicted, state_set![ + StateEventType::RoomMember => "@a:hs1" => 0, + ],); + assert_eq!(conflicted, StateMap::new()); +} + +#[tokio::test] +async fn split_conflicted_state_set_mixed() { + let (unconflicted, conflicted) = super::split_conflicted_state( + [ + 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, + ], + ] + .into_iter() + .stream(), + ) + .await; + + let (unconflicted, conflicted): (StateMap<_>, StateMap<_>) = + (unconflicted.into_iter().collect(), conflicted.into_iter().collect()); + + 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/resolve/topological_sort.rs b/src/core/matrix/state_res/resolve/topological_sort.rs new file mode 100644 index 00000000..6a285904 --- /dev/null +++ b/src/core/matrix/state_res/resolve/topological_sort.rs @@ -0,0 +1,163 @@ +use std::{ + cmp::{Ordering, Reverse}, + collections::{BinaryHeap, HashMap, HashSet}, +}; + +use futures::TryStreamExt; +use ruma::{ + EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, events::room::power_levels::UserPowerLevel, +}; + +use crate::{Result, utils::stream::IterStream}; + +#[derive(PartialEq, Eq)] +struct TieBreaker<'a> { + power_level: UserPowerLevel, + origin_server_ts: MilliSecondsSinceUnixEpoch, + event_id: &'a EventId, +} + +type PduInfo = (UserPowerLevel, MilliSecondsSinceUnixEpoch); + +// NOTE: the power level comparison is "backwards" intentionally. +impl Ord for TieBreaker<'_> { + fn cmp(&self, other: &Self) -> Ordering { + 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<'_> { + fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } +} + +/// Sorts the given event graph using reverse topological power ordering. +/// +/// Definition in the specification: +/// +/// The reverse topological power ordering of a set of events is the +/// lexicographically smallest topological ordering based on the DAG formed by +/// auth events. The reverse topological power ordering is ordered from earliest +/// event to latest. For comparing two topological orderings to determine which +/// is the lexicographically smallest, the following comparison relation on +/// events is used: for events x and y, x < y if +/// +/// 1. x’s sender has greater power level than y’s sender, when looking at their +/// respective auth_events; or +/// 2. the senders have the same power level, but x’s origin_server_ts is less +/// than y’s origin_server_ts; or +/// 3. the senders have the same power level and the events have the same +/// origin_server_ts, but x’s event_id is less than y’s event_id. +/// +/// The reverse topological power ordering can be found by sorting the events +/// using Kahn’s algorithm for topological sorting, and at each step selecting, +/// among all the candidate vertices, the smallest vertex using the above +/// comparison relation. +/// +/// ## Arguments +/// +/// * `graph` - The graph to sort. A map of event ID to its auth events that are +/// in the full conflicted set. +/// +/// * `query` - Function to obtain a (power level, origin_server_ts) of an event +/// for breaking ties. +/// +/// ## Returns +/// +/// Returns the ordered list of event IDs from earliest to latest. +#[tracing::instrument( + level = "debug", + skip_all, + fields( + graph = graph.len(), + ) +)] +pub async fn topological_sort( + graph: &HashMap>, + query: &Query, +) -> Result> +where + Query: Fn(OwnedEventId) -> Fut + Sync, + Fut: Future> + Send, +{ + // We consider that the DAG is directed from most recent events to oldest + // events, so an event is an incoming edge to its auth events. zero_outdegs: + // Vec of events that have an outdegree of zero (no outgoing edges), i.e. the + // oldest events. incoming_edges_map: Map of event to the list of events that + // reference it in its auth events. + let init = (Vec::new(), HashMap::>::new()); + + // Populate the list of events with an outdegree of zero, and the map of + // incoming edges. + let (zero_outdeg, incoming_edges) = graph + .iter() + .try_stream() + .try_fold( + init, + async |(mut zero_outdeg, mut incoming_edges), (event_id, outgoing_edges)| { + if outgoing_edges.is_empty() { + let (power_level, origin_server_ts) = query(event_id.clone()).await?; + + // `Reverse` because `BinaryHeap` sorts largest -> smallest and we need + // smallest -> largest. + zero_outdeg.push(Reverse(TieBreaker { + power_level, + origin_server_ts, + event_id, + })); + } + + incoming_edges.entry(event_id.into()).or_default(); + + for auth_event_id in outgoing_edges { + incoming_edges + .entry(auth_event_id.into()) + .or_default() + .insert(event_id.into()); + } + + Ok((zero_outdeg, incoming_edges)) + }, + ) + .await?; + + // Map of event to the list of events in its auth events. + let mut outgoing_edges_map = graph.clone(); + + // Use a BinaryHeap to keep the events with an outdegree of zero sorted. + let mut heap = BinaryHeap::from(zero_outdeg); + let mut sorted = vec![]; + + // Apply Kahn's algorithm. + // https://en.wikipedia.org/wiki/Topological_sorting#Kahn's_algorithm + while let Some(Reverse(item)) = heap.pop() { + for parent_id in incoming_edges + .get(item.event_id) + .expect("event_id in heap should also be in incoming_edges") + { + let outgoing_edges = outgoing_edges_map + .get_mut(parent_id) + .expect("outgoing_edges should contain all event_ids"); + + outgoing_edges.remove(item.event_id); + if !outgoing_edges.is_empty() { + continue; + } + + // Push on the heap once all the outgoing edges have been removed. + let (power_level, origin_server_ts) = query(parent_id.clone()).await?; + heap.push(Reverse(TieBreaker { + power_level, + origin_server_ts, + event_id: parent_id.as_ref(), + })); + } + + sorted.push(item.event_id.into()); + } + + Ok(sorted) +} diff --git a/src/core/matrix/state_res/room_version.rs b/src/core/matrix/state_res/room_version.rs deleted file mode 100644 index 8dfd6cde..00000000 --- a/src/core/matrix/state_res/room_version.rs +++ /dev/null @@ -1,150 +0,0 @@ -use ruma::RoomVersionId; - -use super::{Error, Result}; - -#[derive(Debug)] -#[allow(clippy::exhaustive_enums)] -pub enum RoomDisposition { - /// A room version that has a stable specification. - Stable, - /// A room version that is not yet fully specified. - Unstable, -} - -#[derive(Debug)] -#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] -pub enum EventFormatVersion { - /// $id:server event id format - V1, - /// MSC1659-style $hash event id format: introduced for room v3 - V2, - /// MSC1884-style $hash format: introduced for room v4 - V3, -} - -#[derive(Debug)] -#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] -pub enum StateResolutionVersion { - /// State resolution for rooms at version 1. - V1, - /// State resolution for room at version 2 or later. - V2, -} - -#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] -#[allow(clippy::struct_excessive_bools)] -pub struct RoomVersion { - /// The stability of this room. - pub disposition: RoomDisposition, - /// The format of the EventId. - pub event_format: EventFormatVersion, - /// Which state resolution algorithm is used. - pub state_res: StateResolutionVersion, - // FIXME: not sure what this one means? - pub enforce_key_validity: bool, - - /// `m.room.aliases` had special auth rules and redaction rules - /// before room version 6. - /// - /// before MSC2261/MSC2432, - pub special_case_aliases_auth: bool, - /// Strictly enforce canonical json, do not allow: - /// * Integers outside the range of [-2 ^ 53 + 1, 2 ^ 53 - 1] - /// * Floats - /// * NaN, Infinity, -Infinity - pub strict_canonicaljson: bool, - /// Verify notifications key while checking m.room.power_levels. - /// - /// bool: MSC2209: Check 'notifications' - pub limit_notifications_power_levels: bool, - /// Extra rules when verifying redaction events. - pub extra_redaction_checks: bool, - /// Allow knocking in event authentication. - /// - /// See [room v7 specification](https://spec.matrix.org/latest/rooms/v7/) for more information. - pub allow_knocking: bool, - /// Adds support for the restricted join rule. - /// - /// See: [MSC3289](https://github.com/matrix-org/matrix-spec-proposals/pull/3289) for more information. - pub restricted_join_rules: bool, - /// Adds support for the knock_restricted join rule. - /// - /// See: [MSC3787](https://github.com/matrix-org/matrix-spec-proposals/pull/3787) for more information. - pub knock_restricted_join_rule: bool, - /// Enforces integer power levels. - /// - /// See: [MSC3667](https://github.com/matrix-org/matrix-spec-proposals/pull/3667) for more information. - pub integer_power_levels: bool, - /// Determine the room creator using the `m.room.create` event's `sender`, - /// instead of the event content's `creator` field. - /// - /// See: [MSC2175](https://github.com/matrix-org/matrix-spec-proposals/pull/2175) for more information. - pub use_room_create_sender: bool, -} - -impl RoomVersion { - pub const V1: Self = Self { - disposition: RoomDisposition::Stable, - event_format: EventFormatVersion::V1, - state_res: StateResolutionVersion::V1, - enforce_key_validity: false, - special_case_aliases_auth: true, - strict_canonicaljson: false, - limit_notifications_power_levels: false, - extra_redaction_checks: true, - allow_knocking: false, - restricted_join_rules: false, - knock_restricted_join_rule: false, - integer_power_levels: false, - use_room_create_sender: false, - }; - pub const V10: Self = Self { - knock_restricted_join_rule: true, - integer_power_levels: true, - ..Self::V9 - }; - pub const V11: Self = Self { - use_room_create_sender: true, - ..Self::V10 - }; - pub const V2: Self = Self { - state_res: StateResolutionVersion::V2, - ..Self::V1 - }; - pub const V3: Self = Self { - event_format: EventFormatVersion::V2, - extra_redaction_checks: false, - ..Self::V2 - }; - pub const V4: Self = Self { - event_format: EventFormatVersion::V3, - ..Self::V3 - }; - pub const V5: Self = Self { enforce_key_validity: true, ..Self::V4 }; - pub const V6: Self = Self { - special_case_aliases_auth: false, - strict_canonicaljson: true, - limit_notifications_power_levels: true, - ..Self::V5 - }; - pub const V7: Self = Self { allow_knocking: true, ..Self::V6 }; - pub const V8: Self = Self { restricted_join_rules: true, ..Self::V7 }; - pub const V9: Self = Self::V8; - - pub fn new(version: &RoomVersionId) -> Result { - Ok(match version { - | RoomVersionId::V1 => Self::V1, - | RoomVersionId::V2 => Self::V2, - | RoomVersionId::V3 => Self::V3, - | RoomVersionId::V4 => Self::V4, - | RoomVersionId::V5 => Self::V5, - | RoomVersionId::V6 => Self::V6, - | RoomVersionId::V7 => Self::V7, - | RoomVersionId::V8 => Self::V8, - | RoomVersionId::V9 => Self::V9, - | RoomVersionId::V10 => Self::V10, - | RoomVersionId::V11 => Self::V11, - | ver => return Err(Error::Unsupported(format!("found version `{ver}`"))), - }) - } -} diff --git a/src/core/matrix/state_res/test_utils.rs b/src/core/matrix/state_res/test_utils.rs index 91f28455..bbe592cc 100644 --- a/src/core/matrix/state_res/test_utils.rs +++ b/src/core/matrix/state_res/test_utils.rs @@ -1,37 +1,53 @@ use std::{ borrow::Borrow, - collections::{BTreeMap, HashMap, HashSet}, - sync::atomic::{AtomicU64, Ordering::SeqCst}, + collections::{HashMap, HashSet}, + pin::Pin, + slice, + sync::{ + Arc, + atomic::{AtomicU64, Ordering::SeqCst}, + }, }; -use futures::future::ready; use ruma::{ - EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, RoomId, RoomVersionId, ServerSignatures, - UserId, event_id, + EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, RoomId, UserId, event_id, events::{ - TimelineEventType, + StateEventType, TimelineEventType, room::{ join_rules::{JoinRule, RoomJoinRulesEventContent}, member::{MembershipState, RoomMemberEventContent}, }, }, - int, room_id, uint, user_id, + int, room_id, + room_version_rules::{AuthorizationRules, 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 super::{AuthSet, StateMap, auth_types_for_event, events::RoomCreateEvent}; use crate::{ - Result, info, - matrix::{Event, EventTypeExt, Pdu, StateMap, pdu::EventHash}, + Error, Result, err, info, + matrix::{Event, EventHash, EventTypeExt, PduEvent, StateKey}, + utils::stream::IterStream, }; static SERVER_TIMESTAMP: AtomicU64 = AtomicU64::new(0); -pub(crate) async fn do_check( - events: &[Pdu], +pub(super) fn not_found() -> Error { err!(Request(NotFound("Test event not found"))) } + +pub(super) fn event_not_found(event_id: &EventId) -> Error { + err!(Request(NotFound("Test event not found: {event_id:?}"))) +} + +pub(super) fn state_not_found(ty: &StateEventType, sk: &str) -> Error { + err!(Request(NotFound("Test state not found: ({ty:?},{sk:?})"))) +} + +pub(super) async fn do_check( + events: &[PduEvent], edges: Vec>, expected_state_ids: Vec, ) { @@ -79,35 +95,32 @@ pub(crate) async fn do_check( } } - // event_id -> Pdu - let mut event_map: HashMap = HashMap::new(); + // event_id -> PduEvent + 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)))) + for node in super::topological_sort(&graph, &async |_id| { + Ok((int!(0).into(), MilliSecondsSinceUnixEpoch(uint!(0)))) }) .await .unwrap() { - let fake_event = fake_event_map.get(&node).unwrap(); + let fake_event = &fake_event_map[&node]; let event_id = fake_event.event_id().to_owned(); - let prev_events = graph.get(&node).unwrap(); + let prev_events = &graph[&node]; let state_before: StateMap = if prev_events.is_empty() { - HashMap::new() + StateMap::new() } else if prev_events.len() == 1 { - state_at_event - .get(prev_events.iter().next().unwrap()) - .unwrap() - .clone() + state_at_event[prev_events.iter().next().unwrap()].clone() } else { let state_sets = prev_events .iter() - .filter_map(|k| state_at_event.get(k)) + .filter_map(|k| state_at_event.get(k).cloned()) .collect::>(); info!( @@ -121,21 +134,27 @@ pub(crate) async fn do_check( .collect::>() ); - let auth_chain_sets: Vec<_> = state_sets + let auth_chain_sets = state_sets .iter() .map(|map| { store .auth_event_ids(room_id(), map.values().cloned().collect()) .unwrap() }) - .collect(); + .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(&RoomVersionId::V6, state_sets, &auth_chain_sets, &fetch, &exists) - .await; + let state_sets = state_sets.into_iter().stream(); + + let rules = RoomVersionRules::V6; + let resolved = super::resolve( + &rules, + state_sets, + auth_chain_sets.into_iter().stream(), + &async |id| event_map.get(&id).cloned().ok_or_else(not_found), + &async |id| event_map.contains_key(&id), + false, + ) + .await; match resolved { | Ok(state) => state, @@ -147,13 +166,15 @@ pub(crate) async fn do_check( 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()); + state_after.insert(ty.with_state_key(key), event_id.clone()); let auth_types = auth_types_for_event( fake_event.event_type(), fake_event.sender(), fake_event.state_key(), fake_event.content(), + &AuthorizationRules::V6, + false, ) .unwrap(); @@ -183,7 +204,7 @@ pub(crate) async fn do_check( 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()); + event_map.insert(event_id.clone(), store.0[ev_id].clone()); } let mut expected_state = StateMap::new(); @@ -228,24 +249,23 @@ pub(crate) async fn do_check( } #[allow(clippy::exhaustive_structs)] -pub(crate) struct TestStore(pub(crate) HashMap); +pub(super) struct TestStore(pub(super) HashMap); -impl TestStore { - pub(crate) fn get_event(&self, _: &RoomId, event_id: &EventId) -> Result { +impl TestStore { + pub(super) 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) + .ok_or_else(|| event_not_found(event_id)) } /// Returns a Vec of the related auth events to the given `event`. - pub(crate) fn auth_event_ids( + pub(super) fn auth_event_ids( &self, room_id: &RoomId, event_ids: Vec, - ) -> Result> { - let mut result = HashSet::new(); + ) -> Result> { + let mut result = AuthSet::new(); let mut stack = event_ids; // DFS for auth event chain @@ -267,8 +287,8 @@ impl TestStore { // A StateStore implementation for testing #[allow(clippy::type_complexity)] -impl TestStore { - pub(crate) fn set_up( +impl TestStore { + pub(super) fn set_up( &mut self, ) -> (StateMap, StateMap, StateMap) { let create_event = to_pdu_event::<&EventId>( @@ -289,8 +309,8 @@ impl TestStore { TimelineEventType::RoomMember, Some(alice().as_str()), member_content_join(), - &[cre.clone()], - &[cre.clone()], + slice::from_ref(&cre), + slice::from_ref(&cre), ); self.0 .insert(alice_mem.event_id().to_owned(), alice_mem.clone()); @@ -370,7 +390,7 @@ impl TestStore { } } -pub(crate) fn event_id(id: &str) -> OwnedEventId { +pub(super) fn event_id(id: &str) -> OwnedEventId { if id.contains('$') { return id.try_into().unwrap(); } @@ -378,33 +398,35 @@ pub(crate) fn event_id(id: &str) -> OwnedEventId { format!("${id}:foo").try_into().unwrap() } -pub(crate) fn alice() -> &'static UserId { user_id!("@alice:foo") } +pub(super) fn alice() -> &'static UserId { user_id!("@alice:foo") } -pub(crate) fn bob() -> &'static UserId { user_id!("@bob:foo") } +pub(super) fn bob() -> &'static UserId { user_id!("@bob:foo") } -pub(crate) fn charlie() -> &'static UserId { user_id!("@charlie:foo") } +pub(super) fn charlie() -> &'static UserId { user_id!("@charlie:foo") } -pub(crate) fn ella() -> &'static UserId { user_id!("@ella:foo") } +pub(super) fn ella() -> &'static UserId { user_id!("@ella:foo") } -pub(crate) fn zara() -> &'static UserId { user_id!("@zara:foo") } +pub(super) fn zara() -> &'static UserId { user_id!("@zara:foo") } -pub(crate) fn room_id() -> &'static RoomId { room_id!("!test:foo") } +pub(super) fn room_id() -> &'static RoomId { room_id!("!test:foo") } -pub(crate) fn member_content_ban() -> Box { +pub(crate) fn hydra_room_id() -> &'static RoomId { room_id!("!CREATE") } + +pub(super) fn member_content_ban() -> Box { to_raw_json_value(&RoomMemberEventContent::new(MembershipState::Ban)).unwrap() } -pub(crate) fn member_content_join() -> Box { +pub(super) fn member_content_join() -> Box { to_raw_json_value(&RoomMemberEventContent::new(MembershipState::Join)).unwrap() } -pub(crate) fn to_init_pdu_event( +pub(super) fn to_init_pdu_event( id: &str, sender: &UserId, ev_type: TimelineEventType, state_key: Option<&str>, content: Box, -) -> Pdu { +) -> PduEvent { let ts = SERVER_TIMESTAMP.fetch_add(1, SeqCst); let id = if id.contains('$') { id.to_owned() @@ -412,15 +434,16 @@ pub(crate) fn to_init_pdu_event( format!("${id}:foo") }; - Pdu { + let state_key = state_key.map(ToOwned::to_owned); + PduEvent { event_id: id.try_into().unwrap(), room_id: room_id().to_owned(), sender: sender.to_owned(), + origin: None, origin_server_ts: ts.try_into().unwrap(), state_key: state_key.map(Into::into), kind: ev_type, content, - origin: None, redacts: None, unsigned: None, auth_events: vec![], @@ -428,10 +451,11 @@ pub(crate) fn to_init_pdu_event( depth: uint!(0), hashes: EventHash::default(), signatures: None, + rejected: false, } } -pub(crate) fn to_pdu_event( +pub(super) fn to_pdu_event( id: &str, sender: &UserId, ev_type: TimelineEventType, @@ -439,7 +463,7 @@ pub(crate) fn to_pdu_event( content: Box, auth_events: &[S], prev_events: &[S], -) -> Pdu +) -> PduEvent where S: AsRef, { @@ -460,15 +484,16 @@ where .map(event_id) .collect::>(); - Pdu { + let state_key = state_key.map(ToOwned::to_owned); + PduEvent { event_id: id.try_into().unwrap(), room_id: room_id().to_owned(), sender: sender.to_owned(), + origin: None, origin_server_ts: ts.try_into().unwrap(), state_key: state_key.map(Into::into), kind: ev_type, content, - origin: None, redacts: None, unsigned: None, auth_events, @@ -476,12 +501,153 @@ where depth: uint!(0), hashes: EventHash::default(), signatures: None, + rejected: false, + } +} + +/// Same as `to_pdu_event()`, but uses the default m.room.create event ID to +/// generate the room ID. +pub(super) fn to_hydra_pdu_event( + id: &str, + sender: &UserId, + ev_type: TimelineEventType, + state_key: Option<&str>, + content: Box, + auth_events: &[S], + prev_events: &[S], +) -> PduEvent +where + S: AsRef, +{ + fn event_id(id: &str) -> OwnedEventId { + if id.contains('$') { + id.try_into().unwrap() + } else { + format!("${id}").try_into().unwrap() + } + } + + let ts = SERVER_TIMESTAMP.fetch_add(1, SeqCst); + 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::>(); + + let state_key = state_key.map(ToOwned::to_owned); + PduEvent { + event_id: event_id(id), + room_id: hydra_room_id().to_owned(), + sender: sender.to_owned(), + origin: None, + origin_server_ts: ts.try_into().unwrap(), + state_key: state_key.map(Into::into), + kind: ev_type, + content, + redacts: None, + unsigned: None, + auth_events, + prev_events, + depth: uint!(0), + hashes: EventHash::default(), + signatures: None, + rejected: false, + } +} + +pub(super) fn room_redaction_pdu_event( + id: &str, + sender: &UserId, + redacts: OwnedEventId, + content: Box, + auth_events: &[S], + prev_events: &[S], +) -> PduEvent +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::>(); + + PduEvent { + event_id: id.try_into().unwrap(), + room_id: room_id().to_owned(), + sender: sender.to_owned(), + origin: None, + origin_server_ts: ts.try_into().unwrap(), + state_key: None, + kind: TimelineEventType::RoomRedaction, + content, + redacts: Some(redacts), + unsigned: None, + auth_events, + prev_events, + depth: uint!(0), + hashes: EventHash::default(), + signatures: None, + rejected: false, + } +} + +pub(super) fn room_create_hydra_pdu_event( + id: &str, + sender: &UserId, + content: Box, +) -> PduEvent { + let ts = SERVER_TIMESTAMP.fetch_add(1, SeqCst); + let eid = if id.contains('$') { + id.to_owned() + } else { + format!("${id}") + }; + let rid = if id.contains('!') { + id.to_owned() + } else { + format!("!{id}") + }; + + PduEvent { + event_id: eid.try_into().unwrap(), + room_id: rid.try_into().unwrap(), + sender: sender.to_owned(), + origin: None, + origin_server_ts: ts.try_into().unwrap(), + state_key: Some(StateKey::new()), + kind: TimelineEventType::RoomCreate, + content, + redacts: None, + unsigned: None, + auth_events: vec![], + prev_events: vec![], + depth: uint!(0), + hashes: EventHash::default(), + signatures: None, + rejected: false, } } // all graphs start with these input events #[allow(non_snake_case)] -pub(crate) fn INITIAL_EVENTS() -> HashMap { +pub(super) fn INITIAL_EVENTS() -> HashMap { vec![ to_pdu_event::<&EventId>( "CREATE", @@ -561,9 +727,88 @@ pub(crate) fn INITIAL_EVENTS() -> HashMap { .collect() } +/// Batch of initial events to use for incoming events from room version +/// `org.matrix.hydra.11` onwards. +#[allow(non_snake_case)] +pub(super) fn INITIAL_HYDRA_EVENTS() -> HashMap { + vec![ + room_create_hydra_pdu_event( + "CREATE", + alice(), + to_raw_json_value(&json!({ "room_version": "org.matrix.hydra.11" })).unwrap(), + ), + to_hydra_pdu_event( + "IMA", + alice(), + TimelineEventType::RoomMember, + Some(alice().as_str()), + member_content_join(), + &["CREATE"], + &["CREATE"], + ), + to_hydra_pdu_event( + "IPOWER", + alice(), + TimelineEventType::RoomPowerLevels, + Some(""), + to_raw_json_value(&json!({})).unwrap(), + &["CREATE", "IMA"], + &["IMA"], + ), + to_hydra_pdu_event( + "IJR", + alice(), + TimelineEventType::RoomJoinRules, + Some(""), + to_raw_json_value(&RoomJoinRulesEventContent::new(JoinRule::Public)).unwrap(), + &["CREATE", "IMA", "IPOWER"], + &["IPOWER"], + ), + to_hydra_pdu_event( + "IMB", + bob(), + TimelineEventType::RoomMember, + Some(bob().as_str()), + member_content_join(), + &["CREATE", "IJR", "IPOWER"], + &["IJR"], + ), + to_hydra_pdu_event( + "IMC", + charlie(), + TimelineEventType::RoomMember, + Some(charlie().as_str()), + member_content_join(), + &["CREATE", "IJR", "IPOWER"], + &["IMB"], + ), + to_hydra_pdu_event::<&EventId>( + "START", + charlie(), + TimelineEventType::RoomMessage, + Some("dummy"), + to_raw_json_value(&json!({})).unwrap(), + &[], + &[], + ), + to_hydra_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 { +pub(super) fn INITIAL_EVENTS_CREATE_ROOM() -> HashMap { vec![to_pdu_event::<&EventId>( "CREATE", alice(), @@ -579,9 +824,99 @@ pub(crate) fn INITIAL_EVENTS_CREATE_ROOM() -> HashMap { } #[allow(non_snake_case)] -pub(crate) fn INITIAL_EDGES() -> Vec { +pub(super) fn INITIAL_EDGES() -> Vec { vec!["START", "IMC", "IMB", "IJR", "IPOWER", "IMA", "CREATE"] .into_iter() .map(event_id) .collect::>() } + +pub(super) fn init_subscriber() -> tracing::dispatcher::DefaultGuard { + tracing::subscriber::set_default( + tracing_subscriber::fmt() + .with_test_writer() + .finish(), + ) +} + +/// Wrapper around a state map. +pub(super) struct TestStateMap(HashMap>); + +impl TestStateMap { + /// Construct a `TestStateMap` from the given event map. + pub(super) fn new(events: &HashMap) -> Arc { + let mut state_map: HashMap> = HashMap::new(); + + for event in events.values() { + let event_type = StateEventType::from(event.event_type().to_string()); + + state_map + .entry(event_type) + .or_default() + .insert(event.state_key().unwrap().to_owned(), event.clone()); + } + + Arc::new(Self(state_map)) + } + + /// Get the event with the given event type and state key. + pub(super) fn get( + self: &Arc, + event_type: &StateEventType, + state_key: &str, + ) -> Result { + self.0 + .get(event_type) + .ok_or_else(|| state_not_found(event_type, state_key))? + .get(state_key) + .cloned() + .ok_or_else(|| state_not_found(event_type, state_key)) + } + + /// A function to get a state event from this map. + pub(super) fn fetch_state_fn( + self: &Arc, + ) -> impl Fn(StateEventType, StateKey) -> Pin> + Send>> + { + move |event_type: StateEventType, state_key: StateKey| { + let s = self.clone(); + Box::pin(async move { s.get(&event_type, state_key.as_str()) }) + } + } + + /// The `m.room.create` event contained in this map. + /// + /// Panics if there is no `m.room.create` event in this map. + pub(super) fn room_create_event(self: &Arc) -> RoomCreateEvent { + RoomCreateEvent::new(self.get(&StateEventType::RoomCreate, "").unwrap()) + } +} + +/// Create an `m.room.third_party_invite` event with the given sender. +pub(super) fn room_third_party_invite(sender: &UserId) -> PduEvent { + let content = json!({ + "display_name": "o...@g...", + "key_validity_url": "https://identity.local/_matrix/identity/v2/pubkey/isvalid", + "public_key": "Gb9ECWmEzf6FQbrBZ9w7lshQhqowtrbLDFw4rXAxZuE", + "public_keys": [ + { + "key_validity_url": "https://identity.local/_matrix/identity/v2/pubkey/isvalid", + "public_key": "Gb9ECWmEzf6FQbrBZ9w7lshQhqowtrbLDFw4rXAxZuE" + }, + { + "key_validity_url": "https://identity.local/_matrix/identity/v2/pubkey/ephemeral/isvalid", + "public_key": "Kxdvv7lo0O6JVI7yimFgmYPfpLGnctcpYjuypP5zx/c" + } + ] + }); + + to_pdu_event( + "THIRDPARTY", + sender, + TimelineEventType::RoomThirdPartyInvite, + Some("somerandomtoken"), + to_raw_json_value(&content).unwrap(), + &["CREATE", "IJR", "IPOWER"], + &["IPOWER"], + ) +} diff --git a/src/core/tests/it/fixtures/MSC4297-problem-A/pdus-hydra.json b/src/core/tests/it/fixtures/MSC4297-problem-A/pdus-hydra.json new file mode 100644 index 00000000..7841670b --- /dev/null +++ b/src/core/tests/it/fixtures/MSC4297-problem-A/pdus-hydra.json @@ -0,0 +1,214 @@ +[ + { + "test-comments": [ + "NOTE: Unlike the v11 pdus, alice is never in `m.room.power_levels`.", + "This is due to MSC4289 forbidding room creators from being in the", + "`users` field of `m.room.power_levels`." + ], + "event_id": "$00-m-room-create", + "room_id": "!00-m-room-create", + "sender": "@alice:example.com", + "type": "m.room.create", + "content": { + "room_version": "12" + }, + "state_key": "", + "origin_server_ts": 0, + "depth": 0, + "signatures": {}, + "hashes": {"sha256": ""}, + "prev_events": [], + "auth_events": [] + }, + { + "event_id": "$00-m-room-member-join-alice", + "room_id": "!00-m-room-create", + "sender": "@alice:example.com", + "type": "m.room.member", + "content": { + "displayname": "alice", + "membership": "join" + }, + "state_key": "@alice:example.com", + "origin_server_ts": 1, + "depth": 1, + "signatures": {}, + "hashes": {"sha256": ""}, + "prev_events": [ + "$00-m-room-create" + ], + "auth_events": [] + }, + { + "event_id": "$00-m-room-power_levels", + "room_id": "!00-m-room-create", + "sender": "@alice:example.com", + "type": "m.room.power_levels", + "content": {}, + "state_key": "", + "origin_server_ts": 2, + "depth": 2, + "signatures": {}, + "hashes": {"sha256": ""}, + "prev_events": [ + "$00-m-room-member-join-alice" + ], + "auth_events": [ + "$00-m-room-member-join-alice" + ] + }, + { + "event_id": "$00-m-room-join_rules", + "room_id": "!00-m-room-create", + "sender": "@alice:example.com", + "type": "m.room.join_rules", + "content": { + "join_rule": "public" + }, + "state_key": "", + "origin_server_ts": 3, + "depth": 3, + "signatures": {}, + "hashes": {"sha256": ""}, + "prev_events": [ + "$00-m-room-power_levels" + ], + "auth_events": [ + "$00-m-room-member-join-alice", + "$00-m-room-power_levels" + ] + }, + { + "event_id": "$00-m-room-member-join-bob", + "room_id": "!00-m-room-create", + "sender": "@bob:example.com", + "type": "m.room.member", + "content": { + "displayname": "bob", + "membership": "join" + }, + "state_key": "@bob:example.com", + "origin_server_ts": 4, + "depth": 4, + "signatures": {}, + "hashes": {"sha256": ""}, + "prev_events": [ + "$00-m-room-join_rules" + ], + "auth_events": [ + "$00-m-room-power_levels", + "$00-m-room-join_rules" + ] + }, + { + "event_id": "$00-m-room-member-join-charlie", + "room_id": "!00-m-room-create", + "sender": "@charlie:example.com", + "type": "m.room.member", + "content": { + "displayname": "charlie", + "membership": "join" + }, + "state_key": "@charlie:example.com", + "origin_server_ts": 5, + "depth": 5, + "signatures": {}, + "hashes": {"sha256": ""}, + "prev_events": [ + "$00-m-room-member-join-bob" + ], + "auth_events": [ + "$00-m-room-power_levels", + "$00-m-room-join_rules" + ] + }, + { + "event_id": "$01-m-room-join_rules", + "room_id": "!00-m-room-create", + "sender": "@alice:example.com", + "type": "m.room.join_rules", + "content": { + "join_rule": "invite" + }, + "state_key": "", + "origin_server_ts": 6, + "depth": 6, + "signatures": {}, + "hashes": {"sha256": ""}, + "prev_events": [ + "$00-m-room-member-join-charlie" + ], + "auth_events": [ + "$00-m-room-power_levels", + "$00-m-room-member-join-alice" + ] + }, + { + "event_id": "$01-m-room-member-leave-alice", + "room_id": "!00-m-room-create", + "sender": "@alice:example.com", + "type": "m.room.member", + "content": { + "displayname": "alice", + "membership": "leave" + }, + "state_key": "@alice:example.com", + "origin_server_ts": 7, + "depth": 7, + "signatures": {}, + "hashes": {"sha256": ""}, + "prev_events": [ + "$01-m-room-join_rules" + ], + "auth_events": [ + "$00-m-room-power_levels", + "$00-m-room-member-join-alice" + ] + }, + { + "event_id": "$01-m-room-member-change-display-name-bob", + "room_id": "!00-m-room-create", + "sender": "@bob:example.com", + "type": "m.room.member", + "content": { + "displayname": "bob++", + "membership": "join" + }, + "state_key": "@bob:example.com", + "origin_server_ts": 8, + "depth": 8, + "signatures": {}, + "hashes": {"sha256": ""}, + "prev_events": [ + "$01-m-room-member-leave-alice" + ], + "auth_events": [ + "$00-m-room-power_levels", + "$00-m-room-member-join-bob", + "$01-m-room-join_rules" + ] + }, + { + "event_id": "$01-m-room-member-change-display-name-charlie", + "room_id": "!00-m-room-create", + "sender": "@charlie:example.com", + "type": "m.room.member", + "content": { + "displayname": "charlie++", + "membership": "join" + }, + "state_key": "@charlie:example.com", + "origin_server_ts": 9, + "depth": 9, + "signatures": {}, + "hashes": {"sha256": ""}, + "prev_events": [ + "$01-m-room-member-leave-alice" + ], + "auth_events": [ + "$00-m-room-power_levels", + "$00-m-room-member-join-charlie", + "$01-m-room-join_rules" + ] + } +] diff --git a/src/core/tests/it/fixtures/MSC4297-problem-A/pdus-v11.json b/src/core/tests/it/fixtures/MSC4297-problem-A/pdus-v11.json new file mode 100644 index 00000000..027c0f50 --- /dev/null +++ b/src/core/tests/it/fixtures/MSC4297-problem-A/pdus-v11.json @@ -0,0 +1,223 @@ +[ + { + "event_id": "$00-m-room-create", + "room_id": "!room:example.com", + "sender": "@alice:example.com", + "type": "m.room.create", + "content": { + "room_version": "11" + }, + "state_key": "", + "origin_server_ts": 0, + "depth": 0, + "signatures": {}, + "hashes": {"sha256": ""}, + "prev_events": [], + "auth_events": [] + }, + { + "event_id": "$00-m-room-member-join-alice", + "room_id": "!room:example.com", + "sender": "@alice:example.com", + "type": "m.room.member", + "content": { + "displayname": "alice", + "membership": "join" + }, + "state_key": "@alice:example.com", + "origin_server_ts": 1, + "depth": 1, + "signatures": {}, + "hashes": {"sha256": ""}, + "prev_events": [ + "$00-m-room-create" + ], + "auth_events": [ + "$00-m-room-create" + ] + }, + { + "event_id": "$00-m-room-power_levels", + "room_id": "!room:example.com", + "sender": "@alice:example.com", + "type": "m.room.power_levels", + "content": { + "users": { + "@alice:example.com": 100 + } + }, + "state_key": "", + "origin_server_ts": 2, + "depth": 2, + "signatures": {}, + "hashes": {"sha256": ""}, + "prev_events": [ + "$00-m-room-member-join-alice" + ], + "auth_events": [ + "$00-m-room-create", + "$00-m-room-member-join-alice" + ] + }, + { + "event_id": "$00-m-room-join_rules", + "room_id": "!room:example.com", + "sender": "@alice:example.com", + "type": "m.room.join_rules", + "content": { + "join_rule": "public" + }, + "state_key": "", + "origin_server_ts": 3, + "depth": 3, + "signatures": {}, + "hashes": {"sha256": ""}, + "prev_events": [ + "$00-m-room-power_levels" + ], + "auth_events": [ + "$00-m-room-create", + "$00-m-room-member-join-alice", + "$00-m-room-power_levels" + ] + }, + { + "event_id": "$00-m-room-member-join-bob", + "room_id": "!room:example.com", + "sender": "@bob:example.com", + "type": "m.room.member", + "content": { + "displayname": "bob", + "membership": "join" + }, + "state_key": "@bob:example.com", + "origin_server_ts": 4, + "depth": 4, + "signatures": {}, + "hashes": {"sha256": ""}, + "prev_events": [ + "$00-m-room-join_rules" + ], + "auth_events": [ + "$00-m-room-create", + "$00-m-room-power_levels", + "$00-m-room-join_rules" + ] + }, + { + "event_id": "$00-m-room-member-join-charlie", + "room_id": "!room:example.com", + "sender": "@charlie:example.com", + "type": "m.room.member", + "content": { + "displayname": "charlie", + "membership": "join" + }, + "state_key": "@charlie:example.com", + "origin_server_ts": 5, + "depth": 5, + "signatures": {}, + "hashes": {"sha256": ""}, + "prev_events": [ + "$00-m-room-member-join-bob" + ], + "auth_events": [ + "$00-m-room-create", + "$00-m-room-power_levels", + "$00-m-room-join_rules" + ] + }, + { + "event_id": "$01-m-room-join_rules", + "room_id": "!room:example.com", + "sender": "@alice:example.com", + "type": "m.room.join_rules", + "content": { + "join_rule": "invite" + }, + "state_key": "", + "origin_server_ts": 6, + "depth": 6, + "signatures": {}, + "hashes": {"sha256": ""}, + "prev_events": [ + "$00-m-room-member-join-charlie" + ], + "auth_events": [ + "$00-m-room-create", + "$00-m-room-power_levels", + "$00-m-room-member-join-alice" + ] + }, + { + "event_id": "$01-m-room-member-leave-alice", + "room_id": "!room:example.com", + "sender": "@alice:example.com", + "type": "m.room.member", + "content": { + "displayname": "alice", + "membership": "leave" + }, + "state_key": "@alice:example.com", + "origin_server_ts": 7, + "depth": 7, + "signatures": {}, + "hashes": {"sha256": ""}, + "prev_events": [ + "$01-m-room-join_rules" + ], + "auth_events": [ + "$00-m-room-create", + "$00-m-room-power_levels", + "$00-m-room-member-join-alice" + ] + }, + { + "event_id": "$01-m-room-member-change-display-name-bob", + "room_id": "!room:example.com", + "sender": "@bob:example.com", + "type": "m.room.member", + "content": { + "displayname": "bob++", + "membership": "join" + }, + "state_key": "@bob:example.com", + "origin_server_ts": 8, + "depth": 8, + "signatures": {}, + "hashes": {"sha256": ""}, + "prev_events": [ + "$01-m-room-member-leave-alice" + ], + "auth_events": [ + "$00-m-room-create", + "$00-m-room-power_levels", + "$00-m-room-member-join-bob", + "$01-m-room-join_rules" + ] + }, + { + "event_id": "$01-m-room-member-change-display-name-charlie", + "room_id": "!room:example.com", + "sender": "@charlie:example.com", + "type": "m.room.member", + "content": { + "displayname": "charlie++", + "membership": "join" + }, + "state_key": "@charlie:example.com", + "origin_server_ts": 9, + "depth": 9, + "signatures": {}, + "hashes": {"sha256": ""}, + "prev_events": [ + "$01-m-room-member-leave-alice" + ], + "auth_events": [ + "$00-m-room-create", + "$00-m-room-power_levels", + "$00-m-room-member-join-charlie", + "$01-m-room-join_rules" + ] + } +] diff --git a/src/core/tests/it/fixtures/MSC4297-problem-A/state-bob.json b/src/core/tests/it/fixtures/MSC4297-problem-A/state-bob.json new file mode 100644 index 00000000..495ffb31 --- /dev/null +++ b/src/core/tests/it/fixtures/MSC4297-problem-A/state-bob.json @@ -0,0 +1,8 @@ +[ + "$00-m-room-create", + "$00-m-room-power_levels", + "$00-m-room-member-join-charlie", + "$01-m-room-join_rules", + "$01-m-room-member-leave-alice", + "$01-m-room-member-change-display-name-bob" +] diff --git a/src/core/tests/it/fixtures/MSC4297-problem-A/state-charlie.json b/src/core/tests/it/fixtures/MSC4297-problem-A/state-charlie.json new file mode 100644 index 00000000..fc11716c --- /dev/null +++ b/src/core/tests/it/fixtures/MSC4297-problem-A/state-charlie.json @@ -0,0 +1,8 @@ +[ + "$00-m-room-create", + "$00-m-room-power_levels", + "$00-m-room-join_rules", + "$00-m-room-member-join-bob", + "$01-m-room-member-leave-alice", + "$01-m-room-member-change-display-name-charlie" +] diff --git a/src/core/tests/it/fixtures/MSC4297-problem-B/pdus-hydra.json b/src/core/tests/it/fixtures/MSC4297-problem-B/pdus-hydra.json new file mode 100644 index 00000000..4bc59a9e --- /dev/null +++ b/src/core/tests/it/fixtures/MSC4297-problem-B/pdus-hydra.json @@ -0,0 +1,239 @@ +[ + { + "test-comments": [ + "NOTE: Unlike the v11 pdus, alice is never in `m.room.power_levels`.", + "This is due to MSC4289 forbidding room creators from being in the", + "`users` field of `m.room.power_levels`." + ], + "event_id": "$00-m-room-create", + "room_id": "!00-m-room-create", + "sender": "@alice:example.com", + "type": "m.room.create", + "content": { + "room_version": "12" + }, + "state_key": "", + "origin_server_ts": 0, + "depth": 0, + "signatures": {}, + "hashes": {"sha256": ""}, + "prev_events": [], + "auth_events": [] + }, + { + "event_id": "$00-m-room-member-join-alice", + "room_id": "!00-m-room-create", + "sender": "@alice:example.com", + "type": "m.room.member", + "content": { + "displayname": "alice", + "membership": "join" + }, + "state_key": "@alice:example.com", + "origin_server_ts": 1, + "depth": 1, + "signatures": {}, + "hashes": {"sha256": ""}, + "prev_events": [ + "$00-m-room-create" + ], + "auth_events": [] + }, + { + "event_id": "$00-m-room-power_levels", + "room_id": "!00-m-room-create", + "sender": "@alice:example.com", + "type": "m.room.power_levels", + "content": {}, + "state_key": "", + "origin_server_ts": 2, + "depth": 2, + "signatures": {}, + "hashes": {"sha256": ""}, + "prev_events": [ + "$00-m-room-member-join-alice" + ], + "auth_events": [ + "$00-m-room-member-join-alice" + ] + }, + { + "event_id": "$00-m-room-join_rules", + "room_id": "!00-m-room-create", + "sender": "@alice:example.com", + "type": "m.room.join_rules", + "content": { + "join_rule": "public" + }, + "state_key": "", + "origin_server_ts": 3, + "depth": 3, + "signatures": {}, + "hashes": {"sha256": ""}, + "prev_events": [ + "$00-m-room-power_levels" + ], + "auth_events": [ + "$00-m-room-member-join-alice", + "$00-m-room-power_levels" + ] + }, + { + "event_id": "$00-m-room-member-join-bob", + "room_id": "!00-m-room-create", + "sender": "@bob:example.com", + "type": "m.room.member", + "content": { + "displayname": "bob", + "membership": "join" + }, + "state_key": "@bob:example.com", + "origin_server_ts": 4, + "depth": 4, + "signatures": {}, + "hashes": {"sha256": ""}, + "prev_events": [ + "$00-m-room-join_rules" + ], + "auth_events": [ + "$00-m-room-power_levels", + "$00-m-room-join_rules" + ] + }, + { + "event_id": "$00-m-room-member-join-charlie", + "room_id": "!00-m-room-create", + "sender": "@charlie:example.com", + "type": "m.room.member", + "content": { + "displayname": "charlie", + "membership": "join" + }, + "state_key": "@charlie:example.com", + "origin_server_ts": 5, + "depth": 5, + "signatures": {}, + "hashes": {"sha256": ""}, + "prev_events": [ + "$00-m-room-member-join-bob" + ], + "auth_events": [ + "$00-m-room-power_levels", + "$00-m-room-join_rules" + ] + }, + { + "event_id": "$01-m-room-power_levels", + "room_id": "!00-m-room-create", + "sender": "@alice:example.com", + "type": "m.room.power_levels", + "content": { + "users": { + "@bob:example.com": 50 + } + }, + "state_key": "", + "origin_server_ts": 6, + "depth": 6, + "signatures": {}, + "hashes": {"sha256": ""}, + "prev_events": [ + "$00-m-room-member-join-charlie" + ], + "auth_events": [ + "$00-m-room-power_levels", + "$00-m-room-member-join-alice" + ] + }, + { + "event_id": "$02-m-room-power_levels", + "room_id": "!00-m-room-create", + "sender": "@bob:example.com", + "type": "m.room.power_levels", + "content": { + "users": { + "@bob:example.com": 50, + "@charlie:example.com": 50 + } + }, + "state_key": "", + "origin_server_ts": 7, + "depth": 7, + "signatures": {}, + "hashes": {"sha256": ""}, + "prev_events": [ + "$01-m-room-power_levels" + ], + "auth_events": [ + "$01-m-room-power_levels", + "$00-m-room-member-join-bob" + ] + }, + { + "event_id": "$00-m-room-member-join-zara", + "room_id": "!00-m-room-create", + "sender": "@zara:example.com", + "type": "m.room.member", + "content": { + "displayname": "zara", + "membership": "join" + }, + "state_key": "@zara:example.com", + "origin_server_ts": 8, + "depth": 8, + "signatures": {}, + "hashes": {"sha256": ""}, + "prev_events": [ + "$02-m-room-power_levels" + ], + "auth_events": [ + "$02-m-room-power_levels", + "$00-m-room-join_rules" + ] + }, + { + "event_id": "$00-m-room-member-join-eve", + "room_id": "!00-m-room-create", + "sender": "@eve:example.com", + "type": "m.room.member", + "content": { + "displayname": "eve", + "membership": "join" + }, + "state_key": "@eve:example.com", + "origin_server_ts": 9, + "depth": 9, + "signatures": {}, + "hashes": {"sha256": ""}, + "prev_events": [ + "$02-m-room-power_levels" + ], + "auth_events": [ + "$02-m-room-power_levels", + "$00-m-room-join_rules" + ] + }, + { + "event_id": "$01-m-room-member-change-display-name-eve", + "room_id": "!00-m-room-create", + "sender": "@eve:example.com", + "type": "m.room.member", + "content": { + "displayname": "eve++", + "membership": "join" + }, + "state_key": "@eve:example.com", + "origin_server_ts": 9, + "depth": 9, + "signatures": {}, + "hashes": {"sha256": ""}, + "prev_events": [ + "$00-m-room-member-join-eve" + ], + "auth_events": [ + "$02-m-room-power_levels", + "$00-m-room-member-join-eve", + "$00-m-room-join_rules" + ] + } +] diff --git a/src/core/tests/it/fixtures/MSC4297-problem-B/pdus-v11.json b/src/core/tests/it/fixtures/MSC4297-problem-B/pdus-v11.json new file mode 100644 index 00000000..64526a5e --- /dev/null +++ b/src/core/tests/it/fixtures/MSC4297-problem-B/pdus-v11.json @@ -0,0 +1,251 @@ +[ + { + "event_id": "$00-m-room-create", + "room_id": "!room:example.com", + "sender": "@alice:example.com", + "type": "m.room.create", + "content": { + "room_version": "11" + }, + "state_key": "", + "origin_server_ts": 0, + "depth": 0, + "signatures": {}, + "hashes": {"sha256": ""}, + "prev_events": [], + "auth_events": [] + }, + { + "event_id": "$00-m-room-member-join-alice", + "room_id": "!room:example.com", + "sender": "@alice:example.com", + "type": "m.room.member", + "content": { + "displayname": "alice", + "membership": "join" + }, + "state_key": "@alice:example.com", + "origin_server_ts": 1, + "depth": 1, + "signatures": {}, + "hashes": {"sha256": ""}, + "prev_events": [ + "$00-m-room-create" + ], + "auth_events": [ + "$00-m-room-create" + ] + }, + { + "event_id": "$00-m-room-power_levels", + "room_id": "!room:example.com", + "sender": "@alice:example.com", + "type": "m.room.power_levels", + "content": { + "users": { + "@alice:example.com": 100 + } + }, + "state_key": "", + "origin_server_ts": 2, + "depth": 2, + "signatures": {}, + "hashes": {"sha256": ""}, + "prev_events": [ + "$00-m-room-member-join-alice" + ], + "auth_events": [ + "$00-m-room-create", + "$00-m-room-member-join-alice" + ] + }, + { + "event_id": "$00-m-room-join_rules", + "room_id": "!room:example.com", + "sender": "@alice:example.com", + "type": "m.room.join_rules", + "content": { + "join_rule": "public" + }, + "state_key": "", + "origin_server_ts": 3, + "depth": 3, + "signatures": {}, + "hashes": {"sha256": ""}, + "prev_events": [ + "$00-m-room-power_levels" + ], + "auth_events": [ + "$00-m-room-create", + "$00-m-room-member-join-alice", + "$00-m-room-power_levels" + ] + }, + { + "event_id": "$00-m-room-member-join-bob", + "room_id": "!room:example.com", + "sender": "@bob:example.com", + "type": "m.room.member", + "content": { + "displayname": "bob", + "membership": "join" + }, + "state_key": "@bob:example.com", + "origin_server_ts": 4, + "depth": 4, + "signatures": {}, + "hashes": {"sha256": ""}, + "prev_events": [ + "$00-m-room-join_rules" + ], + "auth_events": [ + "$00-m-room-create", + "$00-m-room-power_levels", + "$00-m-room-join_rules" + ] + }, + { + "event_id": "$00-m-room-member-join-charlie", + "room_id": "!room:example.com", + "sender": "@charlie:example.com", + "type": "m.room.member", + "content": { + "displayname": "charlie", + "membership": "join" + }, + "state_key": "@charlie:example.com", + "origin_server_ts": 5, + "depth": 5, + "signatures": {}, + "hashes": {"sha256": ""}, + "prev_events": [ + "$00-m-room-member-join-bob" + ], + "auth_events": [ + "$00-m-room-create", + "$00-m-room-power_levels", + "$00-m-room-join_rules" + ] + }, + { + "event_id": "$01-m-room-power_levels", + "room_id": "!room:example.com", + "sender": "@alice:example.com", + "type": "m.room.power_levels", + "content": { + "users": { + "@alice:example.com": 100, + "@bob:example.com": 50 + } + }, + "state_key": "", + "origin_server_ts": 6, + "depth": 6, + "signatures": {}, + "hashes": {"sha256": ""}, + "prev_events": [ + "$00-m-room-member-join-charlie" + ], + "auth_events": [ + "$00-m-room-create", + "$00-m-room-power_levels", + "$00-m-room-member-join-alice" + ] + }, + { + "event_id": "$02-m-room-power_levels", + "room_id": "!room:example.com", + "sender": "@bob:example.com", + "type": "m.room.power_levels", + "content": { + "users": { + "@alice:example.com": 100, + "@bob:example.com": 50, + "@charlie:example.com": 50 + } + }, + "state_key": "", + "origin_server_ts": 7, + "depth": 7, + "signatures": {}, + "hashes": {"sha256": ""}, + "prev_events": [ + "$01-m-room-power_levels" + ], + "auth_events": [ + "$00-m-room-create", + "$01-m-room-power_levels", + "$00-m-room-member-join-bob" + ] + }, + { + "event_id": "$00-m-room-member-join-zara", + "room_id": "!room:example.com", + "sender": "@zara:example.com", + "type": "m.room.member", + "content": { + "displayname": "zara", + "membership": "join" + }, + "state_key": "@zara:example.com", + "origin_server_ts": 8, + "depth": 8, + "signatures": {}, + "hashes": {"sha256": ""}, + "prev_events": [ + "$02-m-room-power_levels" + ], + "auth_events": [ + "$00-m-room-create", + "$02-m-room-power_levels", + "$00-m-room-join_rules" + ] + }, + { + "event_id": "$00-m-room-member-join-eve", + "room_id": "!room:example.com", + "sender": "@eve:example.com", + "type": "m.room.member", + "content": { + "displayname": "eve", + "membership": "join" + }, + "state_key": "@eve:example.com", + "origin_server_ts": 9, + "depth": 9, + "signatures": {}, + "hashes": {"sha256": ""}, + "prev_events": [ + "$02-m-room-power_levels" + ], + "auth_events": [ + "$00-m-room-create", + "$02-m-room-power_levels", + "$00-m-room-join_rules" + ] + }, + { + "event_id": "$01-m-room-member-change-display-name-eve", + "room_id": "!room:example.com", + "sender": "@eve:example.com", + "type": "m.room.member", + "content": { + "displayname": "eve++", + "membership": "join" + }, + "state_key": "@eve:example.com", + "origin_server_ts": 9, + "depth": 9, + "signatures": {}, + "hashes": {"sha256": ""}, + "prev_events": [ + "$00-m-room-member-join-eve" + ], + "auth_events": [ + "$00-m-room-create", + "$02-m-room-power_levels", + "$00-m-room-member-join-eve", + "$00-m-room-join_rules" + ] + } +] diff --git a/src/core/tests/it/fixtures/MSC4297-problem-B/state-eve.json b/src/core/tests/it/fixtures/MSC4297-problem-B/state-eve.json new file mode 100644 index 00000000..1f63f937 --- /dev/null +++ b/src/core/tests/it/fixtures/MSC4297-problem-B/state-eve.json @@ -0,0 +1,9 @@ +[ + "$00-m-room-create", + "$00-m-room-member-join-alice", + "$00-m-room-power_levels", + "$00-m-room-join_rules", + "$00-m-room-member-join-bob", + "$00-m-room-member-join-charlie", + "$01-m-room-member-change-display-name-eve" +] diff --git a/src/core/tests/it/fixtures/MSC4297-problem-B/state-zara.json b/src/core/tests/it/fixtures/MSC4297-problem-B/state-zara.json new file mode 100644 index 00000000..51fb9f51 --- /dev/null +++ b/src/core/tests/it/fixtures/MSC4297-problem-B/state-zara.json @@ -0,0 +1,9 @@ +[ + "$00-m-room-create", + "$00-m-room-member-join-alice", + "$00-m-room-join_rules", + "$00-m-room-member-join-bob", + "$00-m-room-member-join-charlie", + "$02-m-room-power_levels", + "$00-m-room-member-join-zara" +] diff --git a/src/core/tests/it/fixtures/bootstrap-private-chat.json b/src/core/tests/it/fixtures/bootstrap-private-chat.json new file mode 100644 index 00000000..170b1dd5 --- /dev/null +++ b/src/core/tests/it/fixtures/bootstrap-private-chat.json @@ -0,0 +1,129 @@ +[ + { + "event_id": "$00-m-room-create", + "room_id": "!room:example.com", + "sender": "@alice:example.com", + "type": "m.room.create", + "content": { + "creator": "@alice:example.com", + "room_version": "10" + }, + "state_key": "", + "origin_server_ts": 0, + "depth": 0, + "hashes": {"sha256": "aaa"}, + "signatures": {}, + "prev_events": [], + "auth_events": [] + }, + { + "event_id": "$00-m-room-member-join-alice", + "room_id": "!room:example.com", + "sender": "@alice:example.com", + "type": "m.room.member", + "content": { + "displayname": "alice", + "membership": "join" + }, + "state_key": "@alice:example.com", + "origin_server_ts": 1, + "depth": 1, + "hashes": {"sha256": "aaa"}, + "signatures": {}, + "prev_events": [ + "$00-m-room-create" + ], + "auth_events": [ + "$00-m-room-create" + ] + }, + { + "event_id": "$00-m-room-power_levels", + "room_id": "!room:example.com", + "sender": "@alice:example.com", + "type": "m.room.power_levels", + "content": { + "users": { + "@alice:example.com": 100 + } + }, + "state_key": "", + "origin_server_ts": 2, + "depth": 2, + "hashes": {"sha256": "aaa"}, + "signatures": {}, + "prev_events": [ + "$00-m-room-member-join-alice" + ], + "auth_events": [ + "$00-m-room-create", + "$00-m-room-member-join-alice" + ] + }, + { + "event_id": "$00-m-room-join_rules", + "room_id": "!room:example.com", + "sender": "@alice:example.com", + "type": "m.room.join_rules", + "content": { + "join_rule": "invite" + }, + "state_key": "", + "origin_server_ts": 3, + "depth": 3, + "hashes": {"sha256": "aaa"}, + "signatures": {}, + "prev_events": [ + "$00-m-room-power_levels" + ], + "auth_events": [ + "$00-m-room-create", + "$00-m-room-member-join-alice", + "$00-m-room-power_levels" + ] + }, + { + "event_id": "$00-m-room-history_visibility", + "room_id": "!room:example.com", + "sender": "@alice:example.com", + "type": "m.room.history_visibility", + "content": { + "history_visibility": "shared" + }, + "state_key": "", + "origin_server_ts": 4, + "depth": 4, + "hashes": {"sha256": "aaa"}, + "signatures": {}, + "prev_events": [ + "$00-m-room-join_rules" + ], + "auth_events": [ + "$00-m-room-create", + "$00-m-room-member-join-alice", + "$00-m-room-power_levels" + ] + }, + { + "event_id": "$00-m-room-guest_access", + "room_id": "!room:example.com", + "sender": "@alice:example.com", + "type": "m.room.guest_access", + "content": { + "guest_access": "can_join" + }, + "state_key": "", + "origin_server_ts": 5, + "depth": 5, + "hashes": {"sha256": "aaa"}, + "signatures": {}, + "prev_events": [ + "$00-m-room-history_visibility" + ], + "auth_events": [ + "$00-m-room-create", + "$00-m-room-member-join-alice", + "$00-m-room-power_levels" + ] + } +] diff --git a/src/core/tests/it/fixtures/origin-server-ts-tiebreak.json b/src/core/tests/it/fixtures/origin-server-ts-tiebreak.json new file mode 100644 index 00000000..87e4cdfc --- /dev/null +++ b/src/core/tests/it/fixtures/origin-server-ts-tiebreak.json @@ -0,0 +1,64 @@ +[ + { + "test-comments": [ + "NOTE: It is very important that the `event_id` of this PDU is ", + "lexicographically larger than the `event_id` of the following PDU, to ", + "ensure that the tiebreaking is done by the `origin_server_ts` field ", + "and not by the `event_id` field." + ], + "event_id": "$02-m-room-join_rules", + "room_id": "!room:example.com", + "sender": "@alice:example.com", + "type": "m.room.join_rules", + "content": { + "join_rule": "restricted", + "allow": [ + { + "room_id": "!other:example.com", + "type": "m.room_membership" + } + ] + }, + "state_key": "", + "origin_server_ts": 6, + "depth": 6, + "hashes": {"sha256": "aaa"}, + "signatures": {}, + "prev_events": [ + "$00-m-room-guest_access" + ], + "auth_events": [ + "$00-m-room-create", + "$00-m-room-member-join-alice", + "$00-m-room-power_levels" + ] + }, + { + "test-comments": [ + "NOTE: It is very important that the `event_id` of this PDU is ", + "lexicographically smaller than the `event_id` of the previous PDU, to ", + "ensure that the tiebreaking is done by the `origin_server_ts` field ", + "and not by the `event_id` field." + ], + "event_id": "$01-m-room-join_rules", + "room_id": "!room:example.com", + "sender": "@alice:example.com", + "type": "m.room.join_rules", + "content": { + "join_rule": "public" + }, + "state_key": "", + "origin_server_ts": 7, + "depth": 7, + "hashes": {"sha256": "aaa"}, + "signatures": {}, + "prev_events": [ + "$00-m-room-guest_access" + ], + "auth_events": [ + "$00-m-room-create", + "$00-m-room-member-join-alice", + "$00-m-room-power_levels" + ] + } +] diff --git a/src/core/tests/it/fixtures/state-res-v2.1-includes-conflicted-state-subgraph.json b/src/core/tests/it/fixtures/state-res-v2.1-includes-conflicted-state-subgraph.json new file mode 100644 index 00000000..e2136814 --- /dev/null +++ b/src/core/tests/it/fixtures/state-res-v2.1-includes-conflicted-state-subgraph.json @@ -0,0 +1,232 @@ +[ + { + "event_id": "$00-m-room-create", + "room_id": "!room:example.com", + "sender": "@alice:example.com", + "type": "m.room.create", + "content": { + "creator": "@alice:example.com", + "room_version": "10" + }, + "state_key": "", + "origin_server_ts": 0, + "depth": 0, + "hashes": {"sha256": "aaa"}, + "signatures": {}, + "prev_events": [], + "auth_events": [] + }, + { + "event_id": "$00-m-room-member-join-alice", + "room_id": "!room:example.com", + "sender": "@alice:example.com", + "type": "m.room.member", + "content": { + "displayname": "alice", + "membership": "join" + }, + "state_key": "@alice:example.com", + "origin_server_ts": 1, + "depth": 1, + "hashes": {"sha256": "aaa"}, + "signatures": {}, + "prev_events": ["$00-m-room-create"], + "auth_events": [ + "$00-m-room-create" + ] + }, + { + "event_id": "$00-m-room-power_levels", + "room_id": "!room:example.com", + "sender": "@alice:example.com", + "type": "m.room.power_levels", + "content": { + "users": { + "@alice:example.com": 100 + } + }, + "state_key": "", + "origin_server_ts": 2, + "depth": 2, + "hashes": {"sha256": "aaa"}, + "signatures": {}, + "prev_events": ["$00-m-room-member-join-alice"], + "auth_events": [ + "$00-m-room-create", + "$00-m-room-member-join-alice" + ] + }, + { + "event_id": "$00-m-room-join_rules", + "room_id": "!room:example.com", + "sender": "@alice:example.com", + "type": "m.room.join_rules", + "content": { + "join_rule": "public" + }, + "state_key": "", + "origin_server_ts": 3, + "depth": 3, + "hashes": {"sha256": "aaa"}, + "signatures": {}, + "prev_events": ["$00-m-room-power_levels"], + "auth_events": [ + "$00-m-room-create", + "$00-m-room-member-join-alice", + "$00-m-room-power_levels" + ] + }, + { + "event_id": "$00-m-room-member-join-bob", + "room_id": "!room:example.com", + "sender": "@bob:example.com", + "type": "m.room.member", + "content": { + "displayname": "bob", + "membership": "join" + }, + "state_key": "@bob:example.com", + "origin_server_ts": 4, + "depth": 4, + "hashes": {"sha256": "aaa"}, + "signatures": {}, + "prev_events": ["$00-m-room-join_rules"], + "auth_events": [ + "$00-m-room-create", + "$00-m-room-power_levels", + "$00-m-room-join_rules" + ] + }, + { + "event_id": "$00-m-room-member-join-charlie", + "room_id": "!room:example.com", + "sender": "@charlie:example.com", + "type": "m.room.member", + "content": { + "displayname": "charlie", + "membership": "join" + }, + "state_key": "@charlie:example.com", + "origin_server_ts": 5, + "depth": 5, + "hashes": {"sha256": "aaa"}, + "signatures": {}, + "prev_events": ["$00-m-room-member-join-bob"], + "auth_events": [ + "$00-m-room-create", + "$00-m-room-power_levels", + "$00-m-room-join_rules" + ] + }, + { + "event_id": "$01-m-room-power_levels", + "room_id": "!room:example.com", + "sender": "@alice:example.com", + "type": "m.room.power_levels", + "content": { + "users": { + "@alice:example.com": 100, + "@bob:example.com": 50 + } + }, + "state_key": "", + "origin_server_ts": 6, + "depth": 6, + "hashes": {"sha256": "aaa"}, + "signatures": {}, + "prev_events": ["$00-m-room-member-join-charlie"], + "auth_events": [ + "$00-m-room-create", + "$00-m-room-power_levels", + "$00-m-room-member-join-alice" + ] + }, + { + "event_id": "$02-m-room-power_levels", + "room_id": "!room:example.com", + "sender": "@bob:example.com", + "type": "m.room.power_levels", + "content": { + "users": { + "@alice:example.com": 100, + "@bob:example.com": 50, + "@charlie:example.com": 50 + } + }, + "state_key": "", + "origin_server_ts": 7, + "depth": 7, + "hashes": {"sha256": "aaa"}, + "signatures": {}, + "prev_events": ["$01-m-room-power_levels"], + "auth_events": [ + "$00-m-room-create", + "$01-m-room-power_levels", + "$00-m-room-member-join-bob" + ] + }, + { + "event_id": "$00-m-room-member-join-zara", + "room_id": "!room:example.com", + "sender": "@zara:example.com", + "type": "m.room.member", + "content": { + "displayname": "zara", + "membership": "join" + }, + "state_key": "@zara:example.com", + "origin_server_ts": 8, + "depth": 8, + "hashes": {"sha256": "aaa"}, + "signatures": {}, + "prev_events": ["$02-m-room-power_levels"], + "auth_events": [ + "$00-m-room-create", + "$02-m-room-power_levels", + "$00-m-room-join_rules" + ] + }, + { + "event_id": "$00-m-room-member-join-eve", + "room_id": "!room:example.com", + "sender": "@eve:example.com", + "type": "m.room.member", + "content": { + "displayname": "eve", + "membership": "join" + }, + "state_key": "@eve:example.com", + "origin_server_ts": 9, + "depth": 9, + "hashes": {"sha256": "aaa"}, + "signatures": {}, + "prev_events": ["$02-m-room-power_levels"], + "auth_events": [ + "$00-m-room-create", + "$02-m-room-power_levels", + "$00-m-room-join_rules" + ] + }, + { + "event_id": "$01-m-room-member-change-display-name-eve", + "room_id": "!room:example.com", + "sender": "@eve:example.com", + "type": "m.room.member", + "content": { + "displayname": "eve++", + "membership": "join" + }, + "state_key": "@eve:example.com", + "origin_server_ts": 9, + "depth": 9, + "hashes": {"sha256": "aaa"}, + "signatures": {}, + "prev_events": ["$00-m-room-member-join-eve"], + "auth_events": [ + "$00-m-room-create", + "$02-m-room-power_levels", + "$00-m-room-member-join-eve", + "$00-m-room-join_rules" + ] + } +] diff --git a/src/core/tests/it/fixtures/state-res-v2.1-starts-from-empty-set.json b/src/core/tests/it/fixtures/state-res-v2.1-starts-from-empty-set.json new file mode 100644 index 00000000..101f0461 --- /dev/null +++ b/src/core/tests/it/fixtures/state-res-v2.1-starts-from-empty-set.json @@ -0,0 +1,206 @@ +[ + { + "event_id": "$00-m-room-create", + "room_id": "!room:example.com", + "sender": "@alice:example.com", + "type": "m.room.create", + "content": { + "creator": "@alice:example.com", + "room_version": "10" + }, + "state_key": "", + "origin_server_ts": 0, + "depth": 0, + "hashes": {"sha256": "aaa"}, + "signatures": {}, + "prev_events": [], + "auth_events": [] + }, + { + "event_id": "$00-m-room-member-join-alice", + "room_id": "!room:example.com", + "sender": "@alice:example.com", + "type": "m.room.member", + "content": { + "displayname": "alice", + "membership": "join" + }, + "state_key": "@alice:example.com", + "origin_server_ts": 1, + "depth": 1, + "hashes": {"sha256": "aaa"}, + "signatures": {}, + "prev_events": ["$00-m-room-create"], + "auth_events": [ + "$00-m-room-create" + ] + }, + { + "event_id": "$00-m-room-power_levels", + "room_id": "!room:example.com", + "sender": "@alice:example.com", + "type": "m.room.power_levels", + "content": { + "users": { + "@alice:example.com": 100 + } + }, + "state_key": "", + "origin_server_ts": 2, + "depth": 2, + "hashes": {"sha256": "aaa"}, + "signatures": {}, + "prev_events": ["$00-m-room-member-join-alice"], + "auth_events": [ + "$00-m-room-create", + "$00-m-room-member-join-alice" + ] + }, + { + "event_id": "$00-m-room-join_rules", + "room_id": "!room:example.com", + "sender": "@alice:example.com", + "type": "m.room.join_rules", + "content": { + "join_rule": "public" + }, + "state_key": "", + "origin_server_ts": 3, + "depth": 3, + "hashes": {"sha256": "aaa"}, + "signatures": {}, + "prev_events": ["$00-m-room-power_levels"], + "auth_events": [ + "$00-m-room-create", + "$00-m-room-member-join-alice", + "$00-m-room-power_levels" + ] + }, + { + "event_id": "$00-m-room-member-join-bob", + "room_id": "!room:example.com", + "sender": "@bob:example.com", + "type": "m.room.member", + "content": { + "displayname": "bob", + "membership": "join" + }, + "state_key": "@bob:example.com", + "origin_server_ts": 4, + "depth": 4, + "hashes": {"sha256": "aaa"}, + "signatures": {}, + "prev_events": ["$00-m-room-join_rules"], + "auth_events": [ + "$00-m-room-create", + "$00-m-room-power_levels", + "$00-m-room-join_rules" + ] + }, + { + "event_id": "$00-m-room-member-join-charlie", + "room_id": "!room:example.com", + "sender": "@charlie:example.com", + "type": "m.room.member", + "content": { + "displayname": "charlie", + "membership": "join" + }, + "state_key": "@charlie:example.com", + "origin_server_ts": 5, + "depth": 5, + "hashes": {"sha256": "aaa"}, + "signatures": {}, + "prev_events": ["$00-m-room-member-join-bob"], + "auth_events": [ + "$00-m-room-create", + "$00-m-room-power_levels", + "$00-m-room-join_rules" + ] + }, + { + "event_id": "$01-m-room-join_rules", + "room_id": "!room:example.com", + "sender": "@alice:example.com", + "type": "m.room.join_rules", + "content": { + "join_rule": "invite" + }, + "state_key": "", + "origin_server_ts": 6, + "depth": 6, + "hashes": {"sha256": "aaa"}, + "signatures": {}, + "prev_events": ["$00-m-room-member-join-charlie"], + "auth_events": [ + "$00-m-room-create", + "$00-m-room-power_levels", + "$00-m-room-member-join-alice" + ] + }, + { + "event_id": "$01-m-room-member-leave-alice", + "room_id": "!room:example.com", + "sender": "@alice:example.com", + "type": "m.room.member", + "content": { + "displayname": "alice", + "membership": "leave" + }, + "state_key": "@alice:example.com", + "origin_server_ts": 7, + "depth": 7, + "hashes": {"sha256": "aaa"}, + "signatures": {}, + "prev_events": ["$01-m-room-join_rules"], + "auth_events": [ + "$00-m-room-create", + "$00-m-room-power_levels", + "$00-m-room-member-join-alice" + ] + }, + { + "event_id": "$01-m-room-member-change-display-name-bob", + "room_id": "!room:example.com", + "sender": "@bob:example.com", + "type": "m.room.member", + "content": { + "displayname": "bob++", + "membership": "join" + }, + "state_key": "@bob:example.com", + "origin_server_ts": 8, + "depth": 8, + "hashes": {"sha256": "aaa"}, + "signatures": {}, + "prev_events": ["$01-m-room-member-leave-alice"], + "auth_events": [ + "$00-m-room-create", + "$00-m-room-power_levels", + "$00-m-room-member-join-bob", + "$01-m-room-join_rules" + ] + }, + { + "event_id": "$01-m-room-member-change-display-name-charlie", + "room_id": "!room:example.com", + "sender": "@charlie:example.com", + "type": "m.room.member", + "content": { + "displayname": "charlie++", + "membership": "join" + }, + "state_key": "@charlie:example.com", + "origin_server_ts": 9, + "depth": 9, + "hashes": {"sha256": "aaa"}, + "signatures": {}, + "prev_events": ["$01-m-room-member-leave-alice"], + "auth_events": [ + "$00-m-room-create", + "$00-m-room-power_levels", + "$00-m-room-member-join-charlie", + "$01-m-room-join_rules" + ] + } +] diff --git a/src/core/tests/it/main.rs b/src/core/tests/it/main.rs new file mode 100644 index 00000000..aa0a2957 --- /dev/null +++ b/src/core/tests/it/main.rs @@ -0,0 +1,3 @@ +//! Integration tests entrypoint. + +mod resolve; diff --git a/src/core/tests/it/resolve.rs b/src/core/tests/it/resolve.rs new file mode 100644 index 00000000..78633fed --- /dev/null +++ b/src/core/tests/it/resolve.rs @@ -0,0 +1,700 @@ +//! State resolution integration tests. +#![cfg(test)] + +use std::{ + cmp::Ordering, + collections::{BTreeSet, HashMap}, + error::Error, + fs, + path::Path, +}; + +use ruma::{ + OwnedEventId, RoomVersionId, + events::{StateEventType, TimelineEventType}, + room_version_rules::{AuthorizationRules, RoomVersionRules, StateResolutionV2Rules}, +}; +use serde::{Deserialize, Serialize}; +use serde_json::{ + Error as JsonError, Value as JsonValue, from_str as from_json_str, + to_string_pretty as to_json_string_pretty, to_value as to_json_value, +}; +use similar::{Algorithm, udiff::unified_diff}; +use tracing_subscriber::EnvFilter; +use tuwunel_core::{ + Result, err, + matrix::{ + Event, Pdu, StateKey, StateMap, + state_res::{AuthSet, resolve}, + }, + utils::stream::IterStream, +}; + +/// Create a new snapshot test. +/// +/// # Arguments +/// +/// * The test function's name. +/// * A list of JSON files relative to `tests/it/fixtures` to load PDUs to +/// resolve from. +macro_rules! snapshot_test { + ($name:ident, $paths:expr $(,)?) => { + #[tokio::test] + async fn $name() { + let crate::resolve::Snapshots { + resolved_state, + } = crate::resolve::test_resolve(&$paths).await; + + insta::with_settings!({ + description => "Resolved state", + omit_expression => true, + snapshot_suffix => "resolved_state", + }, { + insta::assert_json_snapshot!(&resolved_state); + }); + } + }; +} + +/// Create a new snapshot test, attempting to resolve multiple contrived states. +/// +/// # Arguments +/// +/// * The test function's name. +/// * A list of JSON files relative to `tests/it/fixtures` to load PDUs to +/// resolve from. +/// * A list of JSON files relative to `tests/it/fixtures` to load event IDs +/// forming contrived states to resolve. +macro_rules! snapshot_test_contrived_states { + ($name:ident, $pdus_path:expr, $state_set_paths:expr $(,)?) => { + #[tokio::test] + async fn $name() { + let crate::resolve::Snapshots { + resolved_state, + } = crate::resolve::test_contrived_states(&$pdus_path, &$state_set_paths).await; + + insta::with_settings!({ + description => "Resolved state", + omit_expression => true, + snapshot_suffix => "resolved_state", + }, { + insta::assert_json_snapshot!(&resolved_state); + }); + } + }; +} + +// This module must be defined lexically after the `snapshot_test` macro. +mod snapshot_tests; + +/// Extract `.content.room_version` from a PDU. +#[derive(Deserialize)] +struct ExtractRoomVersion { + room_version: RoomVersionId, +} + +/// Type describing a resolved state event. +#[derive(Serialize)] +struct ResolvedStateEvent { + kind: StateEventType, + state_key: StateKey, + event_id: OwnedEventId, + + // Ignored in `PartialEq` and `Ord` because we don't want to consider it while sorting. + content: JsonValue, +} + +impl PartialEq for ResolvedStateEvent { + fn eq(&self, other: &Self) -> bool { + self.kind == other.kind + && self.state_key == other.state_key + && self.event_id == other.event_id + } +} + +impl Eq for ResolvedStateEvent {} + +impl Ord for ResolvedStateEvent { + fn cmp(&self, other: &Self) -> Ordering { + Ordering::Equal + .then(self.kind.cmp(&other.kind)) + .then(self.state_key.cmp(&other.state_key)) + .then(self.event_id.cmp(&other.event_id)) + } +} + +impl PartialOrd for ResolvedStateEvent { + fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } +} + +/// Information to be captured in snapshot assertions +struct Snapshots { + /// The resolved state of the room. + resolved_state: BTreeSet, +} + +fn snapshot_test_prelude( + paths: &[&str], +) -> (Vec>, RoomVersionRules, AuthorizationRules, StateResolutionV2Rules) { + // Run `cargo test -- --show-output` to view traces, set `RUST_LOG` to control + // filtering. + let subscriber = tracing_subscriber::fmt() + .with_env_filter(EnvFilter::from_default_env()) + .with_test_writer() + .with_span_events(tracing_subscriber::fmt::format::FmtSpan::ACTIVE) + .finish(); + + tracing::subscriber::set_global_default(subscriber).ok(); + + let fixtures_path = Path::new("tests/it/fixtures"); + + let pdu_batches = paths + .iter() + .map(|x| { + from_json_str( + &fs::read_to_string(fixtures_path.join(x)) + .expect("should be able to read JSON file of PDUs"), + ) + .expect("should be able to deserialize JSON file of PDUs") + }) + .collect::>>(); + + let room_version_id = { + let first_pdu = pdu_batches + .first() + .expect("there should be at least one file of PDUs") + .first() + .expect("there should be at least one PDU in the first file"); + + assert_eq!( + first_pdu.kind, + TimelineEventType::RoomCreate, + "the first PDU in the first file should be an m.room.create event", + ); + + from_json_str::(first_pdu.content.get()) + .expect("the m.room.create PDU's content should be valid") + .room_version + }; + let rules = room_version_id + .rules() + .expect("room version should be supported"); + let auth_rules = rules.clone().authorization; + let state_res_rules = rules + .state_res + .v2_rules() + .copied() + .expect("resolve only supports state resolution version 2"); + + (pdu_batches, rules, auth_rules, state_res_rules) +} + +/// Reshape the data a bit to make the diff and snapshots easier to compare. +fn reshape( + pdus_by_id: &HashMap, + x: StateMap, +) -> Result, JsonError> { + x.into_iter() + .map(|((kind, state_key), event_id)| { + Ok(ResolvedStateEvent { + kind, + state_key, + content: to_json_value(pdus_by_id[&event_id].content())?, + event_id, + }) + }) + .collect() +} + +/// Test a list of JSON files containing a list of PDUs and return the results. +/// +/// State resolution is run both atomically for all PDUs and in batches of PDUs +/// by file. +async fn test_resolve(paths: &[&str]) -> Snapshots { + let (pdu_batches, rules, auth_rules, state_res_rules) = snapshot_test_prelude(paths); + + // Resolve PDUs iteratively, using the ordering of `prev_events`. + let iteratively_resolved_state = resolve_iteratively( + &rules, + &auth_rules, + &state_res_rules, + pdu_batches.iter().flat_map(|x| x.iter()), + ) + .await + .expect("iterative state resolution should succeed"); + + // Resolve PDUs in batches by file + let mut pdus_by_id = HashMap::new(); + let mut batched_resolved_state = None; + for pdus in &pdu_batches { + batched_resolved_state = Some( + resolve_batch( + &rules, + &auth_rules, + &state_res_rules, + pdus, + &mut pdus_by_id, + &mut batched_resolved_state, + ) + .await + .expect("batched state resolution step should succeed"), + ); + } + let batched_resolved_state = + batched_resolved_state.expect("batched state resolution should have run at least once"); + + // Resolve all PDUs in a single step + let atomic_resolved_state = resolve_batch( + &rules, + &auth_rules, + &state_res_rules, + pdu_batches.iter().flat_map(|x| x.iter()), + &mut HashMap::new(), + &mut None, + ) + .await + .expect("atomic state resolution should succeed"); + + let iteratively_resolved_state = reshape(&pdus_by_id, iteratively_resolved_state) + .expect("should be able to reshape iteratively resolved state"); + let batched_resolved_state = reshape(&pdus_by_id, batched_resolved_state) + .expect("should be able to reshape batched resolved state"); + let atomic_resolved_state = reshape(&pdus_by_id, atomic_resolved_state) + .expect("should be able to reshape atomic resolved state"); + + let assert_states_match = |first_resolved_state: &BTreeSet, + second_resolved_state: &BTreeSet, + first_name: &str, + second_name: &str| { + if first_resolved_state != second_resolved_state { + let diff = unified_diff( + Algorithm::default(), + &to_json_string_pretty(first_resolved_state) + .expect("should be able to serialize first resolved state"), + &to_json_string_pretty(second_resolved_state) + .expect("should be able to serialize second resolved state"), + 3, + Some((first_name, second_name)), + ); + + panic!( + "{first_name} and {second_name} results should match; but they differ:\n{diff}" + ); + } + }; + + assert_states_match( + &iteratively_resolved_state, + &batched_resolved_state, + "iterative", + "batched", + ); + assert_states_match(&batched_resolved_state, &atomic_resolved_state, "batched", "atomic"); + + Snapshots { + resolved_state: iteratively_resolved_state, + } +} + +/// Test a list of JSON files containing a list of PDUs and a list of JSON files +/// containing the event IDs that form a contrived state and return the results. +#[tracing::instrument(parent = None, name = "test", skip_all)] +async fn test_contrived_states(pdus_paths: &[&str], state_sets_paths: &[&str]) -> Snapshots { + let (pdu_batches, rules, _auth_rules, _state_res_rules) = snapshot_test_prelude(pdus_paths); + + let pdus = pdu_batches + .into_iter() + .flat_map(IntoIterator::into_iter) + .collect::>(); + + let pdus_by_id: HashMap = pdus + .clone() + .into_iter() + .map(|pdu| (pdu.event_id().to_owned(), pdu.clone())) + .collect(); + + let fixtures_path = Path::new("tests/it/fixtures"); + + let state_sets = state_sets_paths + .iter() + .map(|x| { + from_json_str::>( + &fs::read_to_string(fixtures_path.join(x)) + .expect("should be able to read JSON file of PDUs"), + ) + .expect("should be able to deserialize JSON file of PDUs") + .into_iter() + .map(|event_id| { + pdus_by_id + .get(&event_id) + .map(|pdu| { + ( + ( + pdu.event_type().to_string().into(), + pdu.state_key + .clone() + .expect("All PDUs must be state events"), + ), + event_id, + ) + }) + .expect("Event IDs in JSON file must be in PDUs JSON") + }) + .collect() + }) + .collect::>>(); + + let mut auth_chain_sets = Vec::new(); + for state_map in &state_sets { + let mut auth_chain = AuthSet::new(); + + for event_id in state_map.values() { + let pdu = pdus_by_id + .get(event_id) + .expect("We already confirmed all state set event ids have pdus"); + + auth_chain.extend( + auth_events_dfs(&pdus_by_id, pdu).expect("Auth events DFS should not fail"), + ); + } + + auth_chain_sets.push(auth_chain); + } + + let exists = async |x| pdus_by_id.contains_key(&x); + let fetch = async |x| { + pdus_by_id + .get(&x) + .cloned() + .ok_or_else(|| err!(Request(NotFound("event not found")))) + }; + + let resolved_state = resolve( + &rules, + state_sets.into_iter().stream(), + auth_chain_sets.into_iter().stream(), + &fetch, + &exists, + false, + ) + .await + .expect("atomic state resolution should succeed"); + + Snapshots { + resolved_state: reshape(&pdus_by_id, resolved_state) + .expect("should be able to reshape atomic resolved state"), + } +} + +/// Perform state resolution on a batch of PDUs. +/// +/// This function can be used to resolve the state of a room in a single call if +/// all PDUs are provided at once, or across multiple calls if given PDUs in +/// batches in a loop. The latter form simulates the case commonly experienced +/// by homeservers during normal operation. +/// +/// # Arguments +/// +/// * `rules`: The rules of the room version. +/// * `pdus`: An iterator of [`Pdu`]s to resolve, either alone or against the +/// `prev_state`. +/// * `pdus_by_id`: A map of [`OwnedEventId`]s to the [`Pdu`] with that ID. +/// * Should be empty for the first call. +/// * Should not be mutated outside of this function. +/// * `prev_state`: The state returned by a previous call to this function, if +/// any. +/// * Should be `None` for the first call. +/// * Should not be mutated outside of this function. +async fn resolve_batch<'a, I, II>( + rules: &'a RoomVersionRules, + _auth_rules: &'a AuthorizationRules, + _state_res_rules: &'a StateResolutionV2Rules, + pdus: II, + pdus_by_id: &'a mut HashMap, + prev_state: &'a mut Option>, +) -> Result, Box> +where + I: Iterator + Send + 'a, + II: IntoIterator + Clone + Send + 'a, + Pdu: Send + Sync + 'a, + &'a Pdu: Send + 'a, +{ + let mut state_sets = prev_state + .take() + .map(|x| vec![x]) + .unwrap_or_default(); + + for pdu in pdus.clone() { + // Insert each state event into its own StateMap because we don't know any valid + // groupings. + let mut state_map = StateMap::new(); + + state_map.insert( + ( + pdu.event_type().to_string().into(), + pdu.state_key() + .ok_or("all PDUs should be state events")? + .into(), + ), + pdu.event_id().to_owned(), + ); + + state_sets.push(state_map); + } + + pdus_by_id.extend( + pdus.clone() + .into_iter() + .map(|pdu| (pdu.event_id().to_owned(), pdu.to_owned())), + ); + + let mut auth_chain_sets = Vec::new(); + for pdu in pdus { + auth_chain_sets.push(auth_events_dfs(&*pdus_by_id, pdu)?); + } + + let fetch = async |x| { + pdus_by_id + .get(&x) + .cloned() + .ok_or_else(|| err!(Request(NotFound("event not found")))) + }; + + let exists = async |x| pdus_by_id.contains_key(&x); + + resolve( + rules, + state_sets.into_iter().stream(), + auth_chain_sets.into_iter().stream(), + &fetch, + &exists, + false, + ) + .await + .map_err(Into::into) +} + +/// Perform state resolution on a batch of PDUs iteratively, one-by-one. +/// +/// This function walks the `prev_events` of each PDU forward, resolving each +/// pdu against the state(s) of it's `prev_events`, to emulate what would happen +/// in a regular room a server is participating in. +/// +/// # Arguments +/// +/// * `auth_rules`: The authorization rules of the room version. +/// * `state_res_rules`: The state resolution rules of the room version. +/// * `pdus`: An iterator of [`Pdu`]s to resolve, with the following +/// assumptions: +/// * `prev_events` of each PDU points to another provided state event. +/// +/// # Returns +/// +/// The state resolved by resolving all the leaves (PDUs which don't have any +/// other PDUs pointing to it via `prev_events`). +async fn resolve_iteratively<'a, I, II>( + rules: &'a RoomVersionRules, + _auth_rules: &'a AuthorizationRules, + _state_res_rules: &'a StateResolutionV2Rules, + pdus: II, +) -> Result, Box> +where + I: Iterator, + II: IntoIterator + Clone, +{ + let mut forward_prev_events_graph: HashMap> = HashMap::new(); + let mut stack = Vec::new(); + + for pdu in pdus.clone() { + let mut has_prev_events = false; + for prev_event in pdu.prev_events() { + forward_prev_events_graph + .entry(prev_event.into()) + .or_default() + .push(pdu.event_id().into()); + + has_prev_events = true; + } + + if pdu.event_type() == &TimelineEventType::RoomCreate && !has_prev_events { + stack.push(pdu.event_id().to_owned()); + } + } + + let pdus_by_id: HashMap = pdus + .clone() + .into_iter() + .map(|pdu| (pdu.event_id().to_owned(), pdu.to_owned())) + .collect(); + + let exists = async |x| pdus_by_id.contains_key(&x); + let fetch = async |x| { + pdus_by_id + .get(&x) + .cloned() + .ok_or_else(|| err!(Request(NotFound("event not found")))) + }; + + let mut state_at_events: HashMap> = HashMap::new(); + let mut leaves = Vec::new(); + + 'outer: while let Some(event_id) = stack.pop() { + let mut states_before_event = Vec::new(); + let mut auth_chain_sets = Vec::new(); + + let current_pdu = pdus_by_id + .get(&event_id) + .expect("every pdu should be available"); + + for prev_event in current_pdu.prev_events() { + let Some(state_at_event) = state_at_events.get(prev_event) else { + // State for a prev event is not known, we will come back to this event on a + // later iteration. + continue 'outer; + }; + + for pdu in state_at_event.values().map(|event_id| { + pdus_by_id + .get(event_id) + .expect("every pdu should be available") + }) { + auth_chain_sets.push(auth_events_dfs(&pdus_by_id, pdu)?); + } + + states_before_event.push(state_at_event.clone()); + } + + if states_before_event.is_empty() { + // initial event, nothing to resolve + state_at_events.insert( + event_id.clone(), + StateMap::from_iter([( + ( + current_pdu.event_type().to_string().into(), + current_pdu + .state_key() + .expect("all pdus are state events") + .into(), + ), + event_id.clone(), + )]), + ); + } else { + let state_before_event = resolve( + rules, + states_before_event.clone().into_iter().stream(), + auth_chain_sets.clone().into_iter().stream(), + &fetch, + &exists, + false, + ) + .await?; + + let mut proposed_state_at_event = state_before_event.clone(); + proposed_state_at_event.insert( + ( + current_pdu.event_type().to_string().into(), + current_pdu + .state_key() + .expect("all pdus are state events") + .into(), + ), + event_id.clone(), + ); + + auth_chain_sets.push(auth_events_dfs(&pdus_by_id, current_pdu)?); + + let state_at_event = resolve( + rules, + [state_before_event, proposed_state_at_event] + .into_iter() + .stream(), + auth_chain_sets.into_iter().stream(), + &fetch, + &exists, + false, + ) + .await?; + + state_at_events.insert(event_id.clone(), state_at_event); + } + + if let Some(prev_events) = forward_prev_events_graph.get(&event_id) { + stack.extend(prev_events.iter().cloned()); + } else { + // pdu is a leaf: no `prev_events` point to it. + leaves.push(event_id); + } + } + + assert!( + state_at_events.len() == pdus_by_id.len(), + "Not all events have a state calculated! This is likely due to an event having a \ + `prev_events` which points to a non-existent PDU." + ); + + let mut leaf_states = Vec::new(); + let mut auth_chain_sets = Vec::new(); + + for leaf in leaves { + let state_at_event = state_at_events + .get(&leaf) + .expect("states at all events are known"); + + for pdu in state_at_event.values().map(|event_id| { + pdus_by_id + .get(event_id) + .expect("every pdu should be available") + }) { + auth_chain_sets.push(auth_events_dfs(&pdus_by_id, pdu)?); + } + + leaf_states.push(state_at_event.clone()); + } + + resolve( + rules, + leaf_states.into_iter().stream(), + auth_chain_sets.into_iter().stream(), + &fetch, + &exists, + false, + ) + .await + .map_err(Into::into) +} + +/// Depth-first search for the `auth_events` of the given PDU. +/// +/// # Errors +/// +/// Fails if `pdus` does not contain a PDU that appears in the recursive +/// `auth_events` of `pdu`. +fn auth_events_dfs( + pdus_by_id: &HashMap, + pdu: &Pdu, +) -> Result, Box> { + let mut out = AuthSet::new(); + let mut stack = pdu + .auth_events() + .map(ToOwned::to_owned) + .collect::>(); + + while let Some(event_id) = stack.pop() { + if out.contains(&event_id) { + continue; + } + + out.insert(event_id.clone()); + + stack.extend( + pdus_by_id + .get(&event_id) + .ok_or_else(|| format!("missing required PDU: {event_id}"))? + .auth_events() + .map(ToOwned::to_owned), + ); + } + + Ok(out) +} diff --git a/src/core/tests/it/resolve/snapshot_tests.rs b/src/core/tests/it/resolve/snapshot_tests.rs new file mode 100644 index 00000000..43f25499 --- /dev/null +++ b/src/core/tests/it/resolve/snapshot_tests.rs @@ -0,0 +1,56 @@ +//! Snapshot tests. + +// Test the minimal set of events required to create a room with the +// "private_chat" preset. +snapshot_test!(minimal_private_chat, ["bootstrap-private-chat.json"]); + +// Start with a private room, then transition its join rules to restricted, then +// to public. The events in the second file are tied topologically, so they must +// have the tiebreaking algorithm applied. The ordering should be decided by +// the `origin_server_ts` fields of these events, not the `event_id` fields. The +// power levels of these events are equivalent, so they don't really matter. +snapshot_test!(origin_server_ts_tiebreak, [ + "bootstrap-private-chat.json", + "origin-server-ts-tiebreak.json" +],); + +// Test that state res v2.0 is implemented starting from the unconflicted set, +// and NOT the empty set, leading to there being no join rules state. +// +// This example comes directly from the "Problem A" section of MSC4297. +snapshot_test_contrived_states!( + msc4297_problem_a_state_res_v2_0, + ["MSC4297-problem-A/pdus-v11.json"], + ["MSC4297-problem-A/state-bob.json", "MSC4297-problem-A/state-charlie.json"] +); + +// Test that state res v2.1 is implemented starting from the empty set, and NOT +// the unconflicted set. +// +// This example comes directly from the "Problem A" section of MSC4297. +snapshot_test_contrived_states!( + msc4297_problem_a_state_res_v2_1, + ["MSC4297-problem-A/pdus-hydra.json"], + ["MSC4297-problem-A/state-bob.json", "MSC4297-problem-A/state-charlie.json"] +); + +// Test that state res v2.0 does NOT consider the conflicted state subgraph as +// part of the full conflicted state set, leading to the state resetting to the +// first power levels event. +// +// This example comes directly from the "Problem B" section of MSC4297. +snapshot_test_contrived_states!( + msc4297_problem_b_state_res_v2_0, + ["MSC4297-problem-B/pdus-v11.json"], + ["MSC4297-problem-B/state-eve.json", "MSC4297-problem-B/state-zara.json"] +); + +// Test that state res v2.1 considers the conflicted state subgraph as part of +// the full conflicted state set. +// +// This example comes directly from the "Problem B" section of MSC4297. +snapshot_test_contrived_states!( + msc4297_problem_b_state_res_v2_1, + ["MSC4297-problem-B/pdus-hydra.json"], + ["MSC4297-problem-B/state-eve.json", "MSC4297-problem-B/state-zara.json"] +); diff --git a/src/core/tests/it/resolve/snapshots/it__resolve__snapshot_tests__minimal_private_chat@resolved_state.snap b/src/core/tests/it/resolve/snapshots/it__resolve__snapshot_tests__minimal_private_chat@resolved_state.snap new file mode 100644 index 00000000..c3d7b22c --- /dev/null +++ b/src/core/tests/it/resolve/snapshots/it__resolve__snapshot_tests__minimal_private_chat@resolved_state.snap @@ -0,0 +1,58 @@ +--- +source: crates/ruma-state-res/tests/it/resolve/snapshot_tests.rs +description: Resolved state +--- +[ + { + "kind": "m.room.create", + "state_key": "", + "event_id": "$00-m-room-create", + "content": { + "creator": "@alice:example.com", + "room_version": "10" + } + }, + { + "kind": "m.room.guest_access", + "state_key": "", + "event_id": "$00-m-room-guest_access", + "content": { + "guest_access": "can_join" + } + }, + { + "kind": "m.room.history_visibility", + "state_key": "", + "event_id": "$00-m-room-history_visibility", + "content": { + "history_visibility": "shared" + } + }, + { + "kind": "m.room.join_rules", + "state_key": "", + "event_id": "$00-m-room-join_rules", + "content": { + "join_rule": "invite" + } + }, + { + "kind": "m.room.member", + "state_key": "@alice:example.com", + "event_id": "$00-m-room-member-join-alice", + "content": { + "displayname": "alice", + "membership": "join" + } + }, + { + "kind": "m.room.power_levels", + "state_key": "", + "event_id": "$00-m-room-power_levels", + "content": { + "users": { + "@alice:example.com": 100 + } + } + } +] diff --git a/src/core/tests/it/resolve/snapshots/it__resolve__snapshot_tests__msc4297_problem_a_state_res_v2_0@resolved_state.snap b/src/core/tests/it/resolve/snapshots/it__resolve__snapshot_tests__msc4297_problem_a_state_res_v2_0@resolved_state.snap new file mode 100644 index 00000000..625d226f --- /dev/null +++ b/src/core/tests/it/resolve/snapshots/it__resolve__snapshot_tests__msc4297_problem_a_state_res_v2_0@resolved_state.snap @@ -0,0 +1,51 @@ +--- +source: crates/ruma-state-res/tests/it/resolve/snapshot_tests.rs +description: Resolved state +--- +[ + { + "kind": "m.room.create", + "state_key": "", + "event_id": "$00-m-room-create", + "content": { + "room_version": "11" + } + }, + { + "kind": "m.room.member", + "state_key": "@alice:example.com", + "event_id": "$01-m-room-member-leave-alice", + "content": { + "displayname": "alice", + "membership": "leave" + } + }, + { + "kind": "m.room.member", + "state_key": "@bob:example.com", + "event_id": "$01-m-room-member-change-display-name-bob", + "content": { + "displayname": "bob++", + "membership": "join" + } + }, + { + "kind": "m.room.member", + "state_key": "@charlie:example.com", + "event_id": "$01-m-room-member-change-display-name-charlie", + "content": { + "displayname": "charlie++", + "membership": "join" + } + }, + { + "kind": "m.room.power_levels", + "state_key": "", + "event_id": "$00-m-room-power_levels", + "content": { + "users": { + "@alice:example.com": 100 + } + } + } +] diff --git a/src/core/tests/it/resolve/snapshots/it__resolve__snapshot_tests__msc4297_problem_a_state_res_v2_1@resolved_state.snap b/src/core/tests/it/resolve/snapshots/it__resolve__snapshot_tests__msc4297_problem_a_state_res_v2_1@resolved_state.snap new file mode 100644 index 00000000..64625621 --- /dev/null +++ b/src/core/tests/it/resolve/snapshots/it__resolve__snapshot_tests__msc4297_problem_a_state_res_v2_1@resolved_state.snap @@ -0,0 +1,55 @@ +--- +source: crates/ruma-state-res/tests/it/resolve/snapshot_tests.rs +description: Resolved state +--- +[ + { + "kind": "m.room.create", + "state_key": "", + "event_id": "$00-m-room-create", + "content": { + "room_version": "12" + } + }, + { + "kind": "m.room.join_rules", + "state_key": "", + "event_id": "$01-m-room-join_rules", + "content": { + "join_rule": "invite" + } + }, + { + "kind": "m.room.member", + "state_key": "@alice:example.com", + "event_id": "$01-m-room-member-leave-alice", + "content": { + "displayname": "alice", + "membership": "leave" + } + }, + { + "kind": "m.room.member", + "state_key": "@bob:example.com", + "event_id": "$01-m-room-member-change-display-name-bob", + "content": { + "displayname": "bob++", + "membership": "join" + } + }, + { + "kind": "m.room.member", + "state_key": "@charlie:example.com", + "event_id": "$01-m-room-member-change-display-name-charlie", + "content": { + "displayname": "charlie++", + "membership": "join" + } + }, + { + "kind": "m.room.power_levels", + "state_key": "", + "event_id": "$00-m-room-power_levels", + "content": {} + } +] diff --git a/src/core/tests/it/resolve/snapshots/it__resolve__snapshot_tests__msc4297_problem_b_state_res_v2_0@resolved_state.snap b/src/core/tests/it/resolve/snapshots/it__resolve__snapshot_tests__msc4297_problem_b_state_res_v2_0@resolved_state.snap new file mode 100644 index 00000000..c913930c --- /dev/null +++ b/src/core/tests/it/resolve/snapshots/it__resolve__snapshot_tests__msc4297_problem_b_state_res_v2_0@resolved_state.snap @@ -0,0 +1,77 @@ +--- +source: crates/ruma-state-res/tests/it/resolve/snapshot_tests.rs +description: Resolved state +--- +[ + { + "kind": "m.room.create", + "state_key": "", + "event_id": "$00-m-room-create", + "content": { + "room_version": "11" + } + }, + { + "kind": "m.room.join_rules", + "state_key": "", + "event_id": "$00-m-room-join_rules", + "content": { + "join_rule": "public" + } + }, + { + "kind": "m.room.member", + "state_key": "@alice:example.com", + "event_id": "$00-m-room-member-join-alice", + "content": { + "displayname": "alice", + "membership": "join" + } + }, + { + "kind": "m.room.member", + "state_key": "@bob:example.com", + "event_id": "$00-m-room-member-join-bob", + "content": { + "displayname": "bob", + "membership": "join" + } + }, + { + "kind": "m.room.member", + "state_key": "@charlie:example.com", + "event_id": "$00-m-room-member-join-charlie", + "content": { + "displayname": "charlie", + "membership": "join" + } + }, + { + "kind": "m.room.member", + "state_key": "@eve:example.com", + "event_id": "$01-m-room-member-change-display-name-eve", + "content": { + "displayname": "eve++", + "membership": "join" + } + }, + { + "kind": "m.room.member", + "state_key": "@zara:example.com", + "event_id": "$00-m-room-member-join-zara", + "content": { + "displayname": "zara", + "membership": "join" + } + }, + { + "kind": "m.room.power_levels", + "state_key": "", + "event_id": "$00-m-room-power_levels", + "content": { + "users": { + "@alice:example.com": 100 + } + } + } +] diff --git a/src/core/tests/it/resolve/snapshots/it__resolve__snapshot_tests__msc4297_problem_b_state_res_v2_1@resolved_state.snap b/src/core/tests/it/resolve/snapshots/it__resolve__snapshot_tests__msc4297_problem_b_state_res_v2_1@resolved_state.snap new file mode 100644 index 00000000..d24be521 --- /dev/null +++ b/src/core/tests/it/resolve/snapshots/it__resolve__snapshot_tests__msc4297_problem_b_state_res_v2_1@resolved_state.snap @@ -0,0 +1,78 @@ +--- +source: crates/ruma-state-res/tests/it/resolve/snapshot_tests.rs +description: Resolved state +--- +[ + { + "kind": "m.room.create", + "state_key": "", + "event_id": "$00-m-room-create", + "content": { + "room_version": "12" + } + }, + { + "kind": "m.room.join_rules", + "state_key": "", + "event_id": "$00-m-room-join_rules", + "content": { + "join_rule": "public" + } + }, + { + "kind": "m.room.member", + "state_key": "@alice:example.com", + "event_id": "$00-m-room-member-join-alice", + "content": { + "displayname": "alice", + "membership": "join" + } + }, + { + "kind": "m.room.member", + "state_key": "@bob:example.com", + "event_id": "$00-m-room-member-join-bob", + "content": { + "displayname": "bob", + "membership": "join" + } + }, + { + "kind": "m.room.member", + "state_key": "@charlie:example.com", + "event_id": "$00-m-room-member-join-charlie", + "content": { + "displayname": "charlie", + "membership": "join" + } + }, + { + "kind": "m.room.member", + "state_key": "@eve:example.com", + "event_id": "$01-m-room-member-change-display-name-eve", + "content": { + "displayname": "eve++", + "membership": "join" + } + }, + { + "kind": "m.room.member", + "state_key": "@zara:example.com", + "event_id": "$00-m-room-member-join-zara", + "content": { + "displayname": "zara", + "membership": "join" + } + }, + { + "kind": "m.room.power_levels", + "state_key": "", + "event_id": "$02-m-room-power_levels", + "content": { + "users": { + "@bob:example.com": 50, + "@charlie:example.com": 50 + } + } + } +] diff --git a/src/core/tests/it/resolve/snapshots/it__resolve__snapshot_tests__origin_server_ts_tiebreak@resolved_state.snap b/src/core/tests/it/resolve/snapshots/it__resolve__snapshot_tests__origin_server_ts_tiebreak@resolved_state.snap new file mode 100644 index 00000000..e59178c3 --- /dev/null +++ b/src/core/tests/it/resolve/snapshots/it__resolve__snapshot_tests__origin_server_ts_tiebreak@resolved_state.snap @@ -0,0 +1,58 @@ +--- +source: crates/ruma-state-res/tests/it/resolve/snapshot_tests.rs +description: Resolved state +--- +[ + { + "kind": "m.room.create", + "state_key": "", + "event_id": "$00-m-room-create", + "content": { + "creator": "@alice:example.com", + "room_version": "10" + } + }, + { + "kind": "m.room.guest_access", + "state_key": "", + "event_id": "$00-m-room-guest_access", + "content": { + "guest_access": "can_join" + } + }, + { + "kind": "m.room.history_visibility", + "state_key": "", + "event_id": "$00-m-room-history_visibility", + "content": { + "history_visibility": "shared" + } + }, + { + "kind": "m.room.join_rules", + "state_key": "", + "event_id": "$01-m-room-join_rules", + "content": { + "join_rule": "public" + } + }, + { + "kind": "m.room.member", + "state_key": "@alice:example.com", + "event_id": "$00-m-room-member-join-alice", + "content": { + "displayname": "alice", + "membership": "join" + } + }, + { + "kind": "m.room.power_levels", + "state_key": "", + "event_id": "$00-m-room-power_levels", + "content": { + "users": { + "@alice:example.com": 100 + } + } + } +] diff --git a/src/service/admin/create.rs b/src/service/admin/create.rs index 1c7c3f07..fe301b68 100644 --- a/src/service/admin/create.rs +++ b/src/service/admin/create.rs @@ -13,7 +13,7 @@ use ruma::{ name::RoomNameEventContent, power_levels::RoomPowerLevelsEventContent, preview_url::RoomPreviewUrlsEventContent, - topic::RoomTopicEventContent, + topic::{RoomTopicEventContent, TopicContentBlock}, }, }; use tuwunel_core::{Result, pdu::PduBuilder}; @@ -41,8 +41,8 @@ pub async fn create_server_user(services: &Services) -> Result { /// Users in this room are considered admins by tuwunel, and the room can be /// used to issue admin commands by talking to the server user inside it. pub async fn create_admin_room(services: &Services) -> Result { - let room_id = RoomId::new(services.globals.server_name()); - let room_version = &services.config.default_room_version; + let room_id = RoomId::new_v1(services.globals.server_name()); + let room_version = RoomVersionId::V11; let _short_id = services .rooms @@ -183,6 +183,7 @@ pub async fn create_admin_room(services: &Services) -> Result { .timeline .build_and_append_pdu( PduBuilder::state(String::new(), &RoomTopicEventContent { + topic_block: TopicContentBlock::default(), topic: format!("Manage {} | Run commands prefixed with `!admin` | Run `!admin -h` for help | Documentation: https://github.com/matrix-construct/tuwunel/", services.config.server_name), }), server_user, diff --git a/src/service/federation/execute.rs b/src/service/federation/execute.rs index 005a0920..6d3e658f 100644 --- a/src/service/federation/execute.rs +++ b/src/service/federation/execute.rs @@ -8,7 +8,8 @@ use ruma::{ CanonicalJsonObject, CanonicalJsonValue, ServerName, ServerSigningKeyId, api::{ EndpointError, IncomingResponse, MatrixVersion, OutgoingRequest, SendAccessToken, - client::error::Error as RumaError, federation::authentication::XMatrix, + SupportedVersions, client::error::Error as RumaError, + federation::authentication::XMatrix, }, serde::Base64, }; @@ -79,6 +80,7 @@ where .resolver .get_actual_dest(dest) .await?; + let request = into_http_request::(&actual, request)?; let request = self.prepare(dest, request)?; self.perform::(dest, &actual, request, client) @@ -298,9 +300,13 @@ where { const VERSIONS: [MatrixVersion; 1] = [MatrixVersion::V1_11]; const SATIR: SendAccessToken<'_> = SendAccessToken::IfRequired(EMPTY); + let supported = SupportedVersions { + versions: VERSIONS.into(), + features: Default::default(), + }; let http_request = request - .try_into_http_request::>(actual.string().as_str(), SATIR, &VERSIONS) + .try_into_http_request::>(actual.string().as_str(), SATIR, &supported) .map_err(|e| err!(BadServerResponse("Invalid destination: {e:?}")))?; Ok(http_request) diff --git a/src/service/migrations.rs b/src/service/migrations.rs index f69980c6..1f9ec904 100644 --- a/src/service/migrations.rs +++ b/src/service/migrations.rs @@ -534,10 +534,10 @@ async fn fix_referencedevents_missing_sep(services: &Services) -> Result { } async fn fix_readreceiptid_readreceipt_duplicates(services: &Services) -> Result { - use ruma::identifiers_validation::MAX_BYTES; + use ruma::identifiers_validation::ID_MAX_BYTES; use tuwunel_core::arrayvec::ArrayString; - type ArrayId = ArrayString; + type ArrayId = ArrayString; type Key<'a> = (&'a RoomId, u64, &'a UserId); warn!("Fixing undeleted entries in readreceiptid_readreceipt..."); diff --git a/src/service/pusher/mod.rs b/src/service/pusher/mod.rs index d7c33370..04e8d96f 100644 --- a/src/service/pusher/mod.rs +++ b/src/service/pusher/mod.rs @@ -6,17 +6,14 @@ use ipaddress::IPAddress; use ruma::{ DeviceId, OwnedDeviceId, RoomId, UInt, UserId, api::{ - IncomingResponse, MatrixVersion, OutgoingRequest, SendAccessToken, + IncomingResponse, MatrixVersion, OutgoingRequest, SendAccessToken, SupportedVersions, client::push::{Pusher, PusherKind, set_pusher}, push_gateway::send_event_notification::{ self, v1::{Device, Notification, NotificationCounts, NotificationPriority}, }, }, - events::{ - AnySyncTimelineEvent, StateEventType, TimelineEventType, - room::power_levels::RoomPowerLevelsEventContent, - }, + events::{AnySyncTimelineEvent, TimelineEventType, room::power_levels::RoomPowerLevels}, push::{ Action, PushConditionPowerLevelsCtx, PushConditionRoomCtx, PushFormat, Ruleset, Tweak, }, @@ -202,12 +199,16 @@ impl Service { T: OutgoingRequest + Debug + Send, { const VERSIONS: [MatrixVersion; 1] = [MatrixVersion::V1_0]; + let supported = SupportedVersions { + versions: VERSIONS.into(), + features: Default::default(), + }; let dest = dest.replace(self.services.globals.notification_push_path(), ""); trace!("Push gateway destination: {dest}"); let http_request = request - .try_into_http_request::(&dest, SendAccessToken::IfRequired(""), &VERSIONS) + .try_into_http_request::(&dest, SendAccessToken::IfRequired(""), &supported) .map_err(|e| { err!(BadServerResponse(warn!( "Failed to find destination {dest} for push gateway: {e}" @@ -301,13 +302,11 @@ impl Service { let mut notify = None; let mut tweaks = Vec::new(); - let power_levels: RoomPowerLevelsEventContent = self + let power_levels: RoomPowerLevels = self .services .state_accessor - .room_state_get(event.room_id(), &StateEventType::RoomPowerLevels, "") - .await - .and_then(|event| event.get_content()) - .unwrap_or_default(); + .get_power_levels(event.room_id()) + .await?; let serialized = event.to_format(); for action in self @@ -346,7 +345,7 @@ impl Service { &self, user: &UserId, ruleset: &'a Ruleset, - power_levels: &RoomPowerLevelsEventContent, + power_levels: &RoomPowerLevels, pdu: &Raw, room_id: &RoomId, ) -> &'a [Action] { @@ -354,6 +353,7 @@ impl Service { users: power_levels.users.clone(), users_default: power_levels.users_default, notifications: power_levels.notifications.clone(), + rules: power_levels.rules.clone(), }; let room_joined_count = self @@ -380,7 +380,7 @@ impl Service { power_levels: Some(power_levels), }; - ruleset.get_actions(pdu, &ctx) + ruleset.get_actions(pdu, &ctx).await } #[tracing::instrument(skip(self, unread, pusher, tweaks, event))] diff --git a/src/service/rooms/alias/mod.rs b/src/service/rooms/alias/mod.rs index 0270acb4..5b478603 100644 --- a/src/service/rooms/alias/mod.rs +++ b/src/service/rooms/alias/mod.rs @@ -2,13 +2,10 @@ mod remote; use std::sync::Arc; -use futures::{Stream, StreamExt, TryFutureExt}; +use futures::{Stream, StreamExt}; use ruma::{ OwnedRoomId, OwnedServerName, OwnedUserId, RoomAliasId, RoomId, RoomOrAliasId, UserId, - events::{ - StateEventType, - room::power_levels::{RoomPowerLevels, RoomPowerLevelsEventContent}, - }, + events::StateEventType, }; use tuwunel_core::{ Err, Result, Server, err, @@ -225,12 +222,7 @@ impl Service { if let Ok(power_levels) = self .services .state_accessor - .room_state_get_content::( - &room_id, - &StateEventType::RoomPowerLevels, - "", - ) - .map_ok(RoomPowerLevels::from) + .get_power_levels(&room_id) .await { return Ok( diff --git a/src/service/rooms/auth_chain/mod.rs b/src/service/rooms/auth_chain/mod.rs index 45b06cde..4cc9a167 100644 --- a/src/service/rooms/auth_chain/mod.rs +++ b/src/service/rooms/auth_chain/mod.rs @@ -8,9 +8,11 @@ use std::{ }; use futures::{FutureExt, Stream, StreamExt, TryFutureExt, TryStreamExt, pin_mut}; -use ruma::{EventId, OwnedEventId, RoomId}; +use ruma::{EventId, OwnedEventId, RoomId, room_version_rules::RoomVersionRules}; use tuwunel_core::{ - Err, Result, at, debug, debug_error, implement, trace, + Err, Result, at, debug, debug_error, implement, + matrix::Event, + trace, utils::{ IterStream, stream::{ReadyExt, TryBroadbandExt}, @@ -28,6 +30,7 @@ pub struct Service { struct Services { short: Dep, + state: Dep, timeline: Dep, } @@ -38,6 +41,7 @@ impl crate::Service for Service { Ok(Arc::new(Self { services: Services { short: args.depend::("rooms::short"), + state: args.depend::("rooms::state"), timeline: args.depend::("rooms::timeline"), }, db: Data::new(&args), @@ -80,6 +84,12 @@ where const BUCKET: Bucket<'_> = BTreeSet::new(); let started = Instant::now(); + let room_rules = self + .services + .state + .get_room_version_rules(room_id) + .await?; + let starting_ids = self .services .short @@ -103,7 +113,7 @@ where let full_auth_chain: Vec = buckets .into_iter() .try_stream() - .broad_and_then(|chunk| self.get_auth_chain_outer(room_id, started, chunk)) + .broad_and_then(|chunk| self.get_auth_chain_outer(room_id, started, chunk, &room_rules)) .try_collect() .map_ok(|auth_chain: Vec<_>| auth_chain.into_iter().flatten().collect()) .map_ok(|mut full_auth_chain: Vec<_>| { @@ -129,6 +139,7 @@ async fn get_auth_chain_outer( room_id: &RoomId, started: Instant, chunk: Bucket<'_>, + room_rules: &RoomVersionRules, ) -> Result> { let chunk_key: Vec = chunk.iter().map(at!(0)).collect(); @@ -155,7 +166,7 @@ async fn get_auth_chain_outer( } let auth_chain = self - .get_auth_chain_inner(room_id, event_id) + .get_auth_chain_inner(room_id, event_id, room_rules) .await?; self.cache_auth_chain_vec(vec![shortid], auth_chain.as_slice()); @@ -187,14 +198,33 @@ async fn get_auth_chain_outer( } #[implement(Service)] -#[tracing::instrument(name = "inner", level = "trace", skip(self, room_id))] +#[tracing::instrument( + name = "inner", + level = "trace", + skip(self, room_id, room_rules) +)] async fn get_auth_chain_inner( &self, room_id: &RoomId, event_id: &EventId, + room_rules: &RoomVersionRules, ) -> Result> { - let mut todo: VecDeque<_> = [event_id.to_owned()].into(); let mut found = HashSet::new(); + let mut todo: VecDeque<_> = [event_id.to_owned()].into(); + + if room_rules + .authorization + .room_create_event_id_as_room_id + { + let create_id = room_id.as_event_id()?; + let sauthevent = self + .services + .short + .get_or_create_shorteventid(&create_id) + .await; + + found.insert(sauthevent); + } while let Some(event_id) = todo.pop_front() { trace!(?event_id, "processing auth event"); @@ -213,7 +243,7 @@ async fn get_auth_chain_inner( )))); } - for auth_event in &pdu.auth_events { + for auth_event in pdu.auth_events() { let sauthevent = self .services .short @@ -223,7 +253,7 @@ async fn get_auth_chain_inner( if found.insert(sauthevent) { trace!(?event_id, ?auth_event, "adding auth event to processing queue"); - todo.push_back(auth_event.clone()); + todo.push_back(auth_event.to_owned()); } } }, diff --git a/src/service/rooms/event_handler/fetch_auth.rs b/src/service/rooms/event_handler/fetch_auth.rs index 4930d71c..9bff020c 100644 --- a/src/service/rooms/event_handler/fetch_auth.rs +++ b/src/service/rooms/event_handler/fetch_auth.rs @@ -140,10 +140,7 @@ async fn fetch_auth_chain( let Ok(res) = self .services .sending - .send_federation_request(origin, get_event::v1::Request { - event_id: next_id.clone(), - include_unredacted_content: None, - }) + .send_federation_request(origin, get_event::v1::Request { event_id: next_id.clone() }) .await .inspect_err(|e| debug_error!("Failed to fetch event {next_id}: {e}")) else { diff --git a/src/service/rooms/event_handler/fetch_prev.rs b/src/service/rooms/event_handler/fetch_prev.rs index 6b5426cb..a1f9d88b 100644 --- a/src/service/rooms/event_handler/fetch_prev.rs +++ b/src/service/rooms/event_handler/fetch_prev.rs @@ -119,10 +119,10 @@ where // This return value is the key used for sorting events, // events are then sorted by power level, time, // and lexically by event_id. - Ok((int!(0), MilliSecondsSinceUnixEpoch(origin_server_ts))) + Ok((int!(0).into(), MilliSecondsSinceUnixEpoch(origin_server_ts))) }; - let sorted = state_res::lexicographical_topological_sort(&graph, &event_fetch) + let sorted = state_res::topological_sort(&graph, &event_fetch) .await .map_err(|e| err!(Database(error!("Error sorting prev events: {e}"))))?; diff --git a/src/service/rooms/event_handler/handle_incoming_pdu.rs b/src/service/rooms/event_handler/handle_incoming_pdu.rs index e9ed07df..ba20f89c 100644 --- a/src/service/rooms/event_handler/handle_incoming_pdu.rs +++ b/src/service/rooms/event_handler/handle_incoming_pdu.rs @@ -4,11 +4,14 @@ use futures::{ }; use ruma::{CanonicalJsonObject, EventId, RoomId, ServerName, UserId, events::StateEventType}; use tuwunel_core::{ - Err, Result, debug, debug::INFO_SPAN_LEVEL, err, implement, matrix::Event, - utils::stream::IterStream, warn, + Err, Result, debug, + debug::INFO_SPAN_LEVEL, + err, implement, + matrix::{Event, room_version}, + utils::stream::IterStream, + warn, }; -use super::get_room_version_id; use crate::rooms::timeline::RawPduId; /// When receiving an event one needs to: @@ -47,7 +50,7 @@ use crate::rooms::timeline::RawPduId; ret(Debug), )] pub async fn handle_incoming_pdu<'a>( - &self, + &'a self, origin: &'a ServerName, room_id: &'a RoomId, event_id: &'a EventId, @@ -107,11 +110,10 @@ pub async fn handle_incoming_pdu<'a>( return Err!(Request(Forbidden("Federation of this room is disabled by this server."))); } - let room_version = get_room_version_id(create_event)?; + let room_version = room_version::from_create_event(create_event)?; let (incoming_pdu, val) = self .handle_outlier_pdu(origin, room_id, event_id, pdu, &room_version, false) - .boxed() .await?; // 8. if not timeline event: stop diff --git a/src/service/rooms/event_handler/handle_outlier_pdu.rs b/src/service/rooms/event_handler/handle_outlier_pdu.rs index 7c8310fc..e4832298 100644 --- a/src/service/rooms/event_handler/handle_outlier_pdu.rs +++ b/src/service/rooms/event_handler/handle_outlier_pdu.rs @@ -1,17 +1,16 @@ use std::collections::{HashMap, hash_map}; -use futures::future::ready; use ruma::{ CanonicalJsonObject, CanonicalJsonValue, EventId, RoomId, RoomVersionId, ServerName, - events::StateEventType, + events::{StateEventType, TimelineEventType}, }; use tuwunel_core::{ Err, Result, debug, debug_info, err, implement, - matrix::{Event, PduEvent}, + matrix::{Event, PduEvent, event::TypeExt, room_version}, state_res, trace, warn, }; -use super::{check_room_id, to_room_version}; +use super::check_room_id; #[implement(super::Service)] pub(super) async fn handle_outlier_pdu( @@ -40,7 +39,13 @@ pub(super) async fn handle_outlier_pdu( | Ok(ruma::signatures::Verified::Signatures) => { // Redact debug_info!("Calculated hash does not match (redaction): {event_id}"); - let Ok(obj) = ruma::canonical_json::redact(pdu_json, room_version, None) else { + let Some(rules) = room_version.rules() else { + return Err!(Request(UnsupportedRoomVersion( + "Cannot redact event for unknown room version {room_version:?}." + ))); + }; + + let Ok(obj) = ruma::canonical_json::redact(pdu_json, &rules.redaction, None) else { return Err!(Request(InvalidParam("Redaction failed"))); }; @@ -62,8 +67,7 @@ pub(super) async fn handle_outlier_pdu( // Now that we have checked the signature and hashes we can add the eventID and // convert to our PduEvent type - pdu_json - .insert("event_id".to_owned(), CanonicalJsonValue::String(event_id.as_str().to_owned())); + pdu_json.insert("event_id".to_owned(), CanonicalJsonValue::String(event_id.to_string())); let event = serde_json::from_value::(serde_json::to_value(&pdu_json)?) .map_err(|e| err!(Request(BadJson(debug_warn!("Event is not a valid PDU: {e}")))))?; @@ -83,16 +87,28 @@ pub(super) async fn handle_outlier_pdu( // 6. Reject "due to auth events" if the event doesn't pass auth based on the // auth events debug!("Checking based on auth events"); + + let room_rules = room_version::rules(room_version)?; + let is_create = *event.kind() == TimelineEventType::RoomCreate; + let is_hydra = room_rules + .authorization + .room_create_event_id_as_room_id; + + let hydra_create_id = (is_hydra && !is_create).then_some(event.room_id().as_event_id()?); + let auth_event_ids = event + .auth_events() + .map(ToOwned::to_owned) + .chain(hydra_create_id.into_iter()); + // Build map of auth events - let mut auth_events = HashMap::with_capacity(event.auth_events().count()); - for id in event.auth_events() { - let Ok(auth_event) = self.services.timeline.get_pdu(id).await else { + let mut auth_events = HashMap::with_capacity(event.auth_events().count().saturating_add(1)); + for id in auth_event_ids { + let Ok(auth_event) = self.services.timeline.get_pdu(&id).await else { warn!("Could not find auth event {id}"); continue; }; check_room_id(room_id, &auth_event)?; - match auth_events.entry(( auth_event.kind.to_string().into(), auth_event @@ -119,24 +135,20 @@ pub(super) async fn handle_outlier_pdu( return Err!(Request(InvalidParam("Incoming event refers to wrong create event."))); } - let state_fetch = |ty: &StateEventType, sk: &str| { - let key = (ty.to_owned(), sk.into()); - ready(auth_events.get(&key).map(ToOwned::to_owned)) - }; - - let auth_check = state_res::event_auth::auth_check( - &to_room_version(room_version), + state_res::auth_check( + &room_rules, &event, - None, // TODO: third party invite - state_fetch, + &async |event_id| self.event_fetch(&event_id).await, + &async |event_type, state_key| { + auth_events + .get(&event_type.with_state_key(state_key.as_str())) + .map(ToOwned::to_owned) + .ok_or_else(|| err!(Request(NotFound("state not found")))) + }, ) .await .map_err(|e| err!(Request(Forbidden("Auth check failed: {e:?}"))))?; - if !auth_check { - return Err!(Request(Forbidden("Auth check failed"))); - } - trace!("Validation successful."); // 7. Persist the event as an outlier. diff --git a/src/service/rooms/event_handler/mod.rs b/src/service/rooms/event_handler/mod.rs index da9e36c3..70ce67f4 100644 --- a/src/service/rooms/event_handler/mod.rs +++ b/src/service/rooms/event_handler/mod.rs @@ -19,12 +19,9 @@ use std::{ }; use async_trait::async_trait; -use ruma::{ - EventId, OwnedEventId, OwnedRoomId, RoomId, RoomVersionId, - events::room::create::RoomCreateEventContent, -}; +use ruma::{EventId, OwnedRoomId, RoomId}; use tuwunel_core::{ - Err, Result, RoomVersion, Server, implement, + Err, Result, Server, implement, matrix::{Event, PduEvent}, utils::{MutexMap, continue_exponential_backoff}, }; @@ -126,17 +123,13 @@ fn is_backed_off(&self, event_id: &EventId, range: Range) -> bool { } #[implement(Service)] -async fn event_exists(&self, event_id: OwnedEventId) -> bool { - self.services.timeline.pdu_exists(&event_id).await +async fn event_exists(&self, event_id: &EventId) -> bool { + self.services.timeline.pdu_exists(event_id).await } #[implement(Service)] -async fn event_fetch(&self, event_id: OwnedEventId) -> Option { - self.services - .timeline - .get_pdu(&event_id) - .await - .ok() +async fn event_fetch(&self, event_id: &EventId) -> Result { + self.services.timeline.get_pdu(event_id).await } fn check_room_id(room_id: &RoomId, pdu: &Pdu) -> Result { @@ -151,15 +144,3 @@ fn check_room_id(room_id: &RoomId, pdu: &Pdu) -> Result { Ok(()) } - -fn get_room_version_id(create_event: &Pdu) -> Result { - let content: RoomCreateEventContent = create_event.get_content()?; - let room_version = content.room_version; - - Ok(room_version) -} - -#[inline] -fn to_room_version(room_version_id: &RoomVersionId) -> RoomVersion { - RoomVersion::new(room_version_id).expect("room version is supported") -} diff --git a/src/service/rooms/event_handler/resolve_state.rs b/src/service/rooms/event_handler/resolve_state.rs index 09e078aa..19e28c6d 100644 --- a/src/service/rooms/event_handler/resolve_state.rs +++ b/src/service/rooms/event_handler/resolve_state.rs @@ -1,14 +1,11 @@ -use std::{ - borrow::Borrow, - collections::{HashMap, HashSet}, - sync::Arc, -}; +use std::{borrow::Borrow, collections::HashMap, sync::Arc}; -use futures::{FutureExt, StreamExt, TryFutureExt, TryStreamExt, future::try_join}; +use futures::{FutureExt, Stream, StreamExt, TryFutureExt, TryStreamExt}; use ruma::{OwnedEventId, RoomId, RoomVersionId}; use tuwunel_core::{ - Error, Result, err, implement, - state_res::{self, StateMap}, + Result, err, implement, + matrix::room_version, + state_res::{self, AuthSet, StateMap}, trace, utils::stream::{IterStream, ReadyExt, TryWidebandExt, WidebandExt}, }; @@ -47,9 +44,9 @@ pub async fn resolve_state( self.services .auth_chain .event_ids_iter(room_id, state.values().map(Borrow::borrow)) - .try_collect() + .try_collect::>() }) - .try_collect::>>(); + .ready_filter_map(Result::ok); let fork_states = fork_states .iter() @@ -62,17 +59,12 @@ pub async fn resolve_state( .multi_get_statekey_from_short(shortstatekeys) .zip(event_ids) .ready_filter_map(|(ty_sk, id)| Some((ty_sk.ok()?, id))) - .collect() - }) - .map(Ok::<_, Error>) - .try_collect::>>(); - - let (fork_states, auth_chain_sets) = try_join(fork_states, auth_chain_sets).await?; + .collect::>() + }); trace!("Resolving state"); let state = self - .state_resolution(room_version_id, fork_states.iter(), &auth_chain_sets) - .boxed() + .state_resolution(room_version_id, fork_states, auth_chain_sets) .await?; trace!("State resolution done."); @@ -104,19 +96,24 @@ pub async fn resolve_state( } #[implement(super::Service)] -#[tracing::instrument(name = "ruma", level = "debug", skip_all)] -pub async fn state_resolution<'a, StateSets>( - &'a self, - room_version: &'a RoomVersionId, +pub(super) async fn state_resolution( + &self, + room_version: &RoomVersionId, state_sets: StateSets, - auth_chain_sets: &'a [HashSet], + auth_chains: AuthSets, ) -> Result> where - StateSets: Iterator> + Clone + Send, + StateSets: Stream> + Send, + AuthSets: Stream> + Send, { - 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, state_sets, auth_chain_sets, &event_fetch, &event_exists) - .map_err(|e| err!(error!("State resolution failed: {e:?}"))) - .await + state_res::resolve( + &room_version::rules(room_version)?, + state_sets, + auth_chains, + &async |event_id: OwnedEventId| self.event_fetch(&event_id).await, + &async |event_id: OwnedEventId| self.event_exists(&event_id).await, + self.services.server.config.hydra_backports, + ) + .map_err(|e| err!(error!("State resolution failed: {e:?}"))) + .await } diff --git a/src/service/rooms/event_handler/state_at_incoming.rs b/src/service/rooms/event_handler/state_at_incoming.rs index b9368fa3..0ed2d946 100644 --- a/src/service/rooms/event_handler/state_at_incoming.rs +++ b/src/service/rooms/event_handler/state_at_incoming.rs @@ -1,15 +1,17 @@ use std::{ - borrow::Borrow, - collections::{HashMap, HashSet}, - iter::Iterator, + collections::HashMap, + iter::{Iterator, once}, }; -use futures::{FutureExt, StreamExt, TryFutureExt, TryStreamExt, future::try_join}; +use futures::{ + FutureExt, StreamExt, TryFutureExt, TryStreamExt, + future::{OptionFuture, try_join}, +}; use ruma::{OwnedEventId, RoomId, RoomVersionId}; use tuwunel_core::{ - Result, err, implement, - matrix::{Event, StateMap}, - trace, + Result, apply, err, implement, + matrix::{Event, StateMap, state_res::AuthSet}, + ref_at, trace, utils::stream::{BroadbandExt, IterStream, ReadyExt, TryBroadbandExt, TryWidebandExt}, }; @@ -60,14 +62,16 @@ where let (prev_event, mut state) = try_join(prev_event, state).await?; - if let Some(state_key) = prev_event.state_key { + if let Some(state_key) = prev_event.state_key() { + let prev_event_type = prev_event.event_type().to_cow_str().into(); + let shortstatekey = self .services .short - .get_or_create_shortstatekey(&prev_event.kind.into(), &state_key) + .get_or_create_shortstatekey(&prev_event_type, state_key) .await; - state.insert(shortstatekey, prev_event.event_id); + state.insert(shortstatekey, prev_event.event_id().into()); // Now it's the state after the pdu } @@ -113,26 +117,28 @@ where }; trace!("Calculating fork states..."); - let (fork_states, auth_chain_sets): (Vec>, Vec>) = - extremity_sstatehashes - .into_iter() - .try_stream() - .wide_and_then(|(sstatehash, prev_event)| { - self.state_at_incoming_fork(room_id, sstatehash, prev_event) - }) - .try_collect() - .map_ok(Vec::into_iter) - .map_ok(Iterator::unzip) - .await?; + let (fork_states, auth_chain_sets) = extremity_sstatehashes + .into_iter() + .try_stream() + .wide_and_then(|(sstatehash, prev_event)| { + self.state_at_incoming_fork(room_id, sstatehash, prev_event) + }) + .try_collect() + .map_ok(Vec::into_iter) + .map_ok(Iterator::unzip) + .map_ok(apply!(2, Vec::into_iter)) + .map_ok(apply!(2, IterStream::stream)) + .await?; + trace!("Resolving state"); let Ok(new_state) = self - .state_resolution(room_version_id, fork_states.iter(), &auth_chain_sets) - .boxed() + .state_resolution(room_version_id, fork_states, auth_chain_sets) .await else { return Ok(None); }; + trace!("State resolution done."); new_state .into_iter() .stream() @@ -150,41 +156,55 @@ where } #[implement(super::Service)] +#[tracing::instrument( + name = "fork", + level = "debug", + skip_all, + fields( + ?sstatehash, + prev_event = ?prev_event.event_id(), + ) +)] async fn state_at_incoming_fork( &self, room_id: &RoomId, sstatehash: ShortStateHash, prev_event: Pdu, -) -> Result<(StateMap, HashSet)> +) -> Result<(StateMap, AuthSet)> where Pdu: Event, { - let mut leaf_state: HashMap<_, _> = self + let leaf: OptionFuture<_> = prev_event + .state_key() + .map(async |state_key| { + self.services + .short + .get_or_create_shortstatekey(&prev_event.kind().to_cow_str().into(), state_key) + .map(|shortstatekey| once((shortstatekey, prev_event.event_id().to_owned()))) + .await + }) + .into(); + + let leaf_state_after_event: Vec<_> = self .services .state_accessor .state_full_ids(sstatehash) + .chain(leaf.await.into_iter().flatten().stream()) .collect() .await; - if let Some(state_key) = prev_event.state_key() { - let shortstatekey = self - .services - .short - .get_or_create_shortstatekey(&prev_event.kind().to_string().into(), state_key) - .await; - - let event_id = prev_event.event_id(); - leaf_state.insert(shortstatekey, event_id.to_owned()); - // Now it's the state after the pdu - } + let starting_events = leaf_state_after_event + .iter() + .map(ref_at!(1)) + .map(AsRef::as_ref); let auth_chain = self .services .auth_chain - .event_ids_iter(room_id, leaf_state.values().map(Borrow::borrow)) + .event_ids_iter(room_id, starting_events) .try_collect(); - let fork_state = leaf_state + let fork_state = leaf_state_after_event .iter() .stream() .broad_then(|(k, id)| { diff --git a/src/service/rooms/event_handler/upgrade_outlier_pdu.rs b/src/service/rooms/event_handler/upgrade_outlier_pdu.rs index 7db6f963..b7ba31fb 100644 --- a/src/service/rooms/event_handler/upgrade_outlier_pdu.rs +++ b/src/service/rooms/event_handler/upgrade_outlier_pdu.rs @@ -1,18 +1,18 @@ use std::{borrow::Borrow, iter::once, sync::Arc, time::Instant}; -use futures::{FutureExt, StreamExt, future::ready}; +use futures::{FutureExt, StreamExt}; use ruma::{ - CanonicalJsonObject, EventId, RoomId, RoomVersionId, ServerName, events::StateEventType, + CanonicalJsonObject, EventId, OwnedEventId, RoomId, RoomVersionId, ServerName, + events::StateEventType, }; use tuwunel_core::{ Err, Result, debug, debug_info, err, implement, is_equal_to, - matrix::{Event, EventTypeExt, PduEvent, StateKey, state_res}, + matrix::{Event, EventTypeExt, PduEvent, StateKey, room_version, state_res}, trace, utils::stream::{BroadbandExt, ReadyExt}, warn, }; -use super::to_room_version; use crate::rooms::{ state_compressor::{CompressedState, HashSetCompressStateEvent}, timeline::RawPduId, @@ -50,6 +50,7 @@ pub(super) async fn upgrade_outlier_to_timeline_pdu( debug!("Upgrading to timeline pdu"); let timer = Instant::now(); + let room_rules = room_version::rules(room_version)?; // 10. Fetch missing state and auth chain events by calling /state_ids at // backwards extremities doing all the checks in this list starting at 1. @@ -77,35 +78,26 @@ pub(super) async fn upgrade_outlier_to_timeline_pdu( debug!("Performing auth check"); // 11. Check the auth of the event passes based on the state of the event - let state_fetch_state = &state_at_incoming_event; - let state_fetch = |k: StateEventType, s: StateKey| async move { + let state_fetch = async |k: StateEventType, s: StateKey| { let shortstatekey = self .services .short - .get_shortstatekey(&k, &s) - .await - .ok()?; + .get_shortstatekey(&k, s.as_str()) + .await?; - let event_id = state_fetch_state.get(&shortstatekey)?; - self.services - .timeline - .get_pdu(event_id) - .await - .ok() + let event_id = state_at_incoming_event + .get(&shortstatekey) + .ok_or_else(|| { + err!(Request(NotFound( + "shortstatekey {shortstatekey:?} not found for ({k:?},{s:?})" + ))) + })?; + + self.services.timeline.get_pdu(event_id).await }; - let auth_check = state_res::event_auth::auth_check( - &to_room_version(room_version), - &incoming_pdu, - None, // TODO: third party invite - |ty, sk| state_fetch(ty.clone(), sk.into()), - ) - .await - .map_err(|e| err!(Request(Forbidden("Auth check failed: {e:?}"))))?; - - if !auth_check { - return Err!(Request(Forbidden("Event has failed auth check with state at the event."))); - } + let event_fetch = async |event_id: OwnedEventId| self.event_fetch(&event_id).await; + state_res::auth_check(&room_rules, &incoming_pdu, &event_fetch, &state_fetch).await?; debug!("Gathering auth events"); let auth_events = self @@ -117,29 +109,25 @@ pub(super) async fn upgrade_outlier_to_timeline_pdu( incoming_pdu.sender(), incoming_pdu.state_key(), incoming_pdu.content(), + &room_rules.authorization, + true, ) .await?; - let state_fetch = |k: &StateEventType, s: &str| { - let key = k.with_state_key(s); - ready(auth_events.get(&key).map(ToOwned::to_owned)) + let state_fetch = async |k: StateEventType, s: StateKey| { + auth_events + .get(&k.with_state_key(s.as_str())) + .map(ToOwned::to_owned) + .ok_or_else(|| err!(Request(NotFound("state event not found")))) }; - let auth_check = state_res::event_auth::auth_check( - &to_room_version(room_version), - &incoming_pdu, - None, // third-party invite - state_fetch, - ) - .await - .map_err(|e| err!(Request(Forbidden("Auth check failed: {e:?}"))))?; + state_res::auth_check(&room_rules, &incoming_pdu, &event_fetch, &state_fetch).await?; // Soft fail check before doing state res debug!("Performing soft-fail check"); - let soft_fail = match (auth_check, incoming_pdu.redacts_id(room_version)) { - | (false, _) => true, - | (true, None) => false, - | (true, Some(redact_id)) => + let soft_fail = match incoming_pdu.redacts_id(room_version) { + | None => false, + | Some(redact_id) => !self .services .state_accessor @@ -224,11 +212,13 @@ pub(super) async fn upgrade_outlier_to_timeline_pdu( .services .state_compressor .save_state(room_id, new_room_state) + .boxed() .await?; self.services .state .force_state(room_id, shortstatehash, added, removed, &state_lock) + .boxed() .await?; } diff --git a/src/service/rooms/spaces/mod.rs b/src/service/rooms/spaces/mod.rs index 84329c5d..420c1114 100644 --- a/src/service/rooms/spaces/mod.rs +++ b/src/service/rooms/spaces/mod.rs @@ -11,17 +11,14 @@ use ruma::{ OwnedEventId, OwnedRoomId, OwnedServerName, RoomId, ServerName, UserId, api::{ client::space::SpaceHierarchyRoomsChunk, - federation::{ - self, - space::{SpaceHierarchyChildSummary, SpaceHierarchyParentSummary}, - }, + federation::{self, space::SpaceHierarchyParentSummary}, }, events::{ StateEventType, space::child::{HierarchySpaceChildEvent, SpaceChildEventContent}, }, + room::{JoinRuleSummary, RoomSummary}, serde::Raw, - space::SpaceRoomJoinRule, }; use tokio::sync::{Mutex, MutexGuard}; use tuwunel_core::{ @@ -130,17 +127,12 @@ pub async fn get_summary_and_children_local( | None => (), // cache miss | Some(None) => return Ok(None), | Some(Some(cached)) => { - let allowed_rooms = cached - .summary - .allowed_room_ids - .iter() - .map(AsRef::as_ref); - + let join_rule = &cached.summary.summary.join_rule; let is_accessible_child = self.is_accessible_child( current_room, - &cached.summary.join_rule, + join_rule, identifier, - allowed_rooms, + join_rule.allowed_room_ids(), ); let accessibility = if is_accessible_child.await { @@ -236,10 +228,11 @@ async fn get_summary_and_children_federation( .await; let identifier = Identifier::UserId(user_id); - let allowed_room_ids = summary.allowed_room_ids.iter().map(AsRef::as_ref); + let join_rule = &summary.summary.join_rule; + let allowed_room_ids = join_rule.allowed_room_ids(); let is_accessible_child = self - .is_accessible_child(current_room, &summary.join_rule, &identifier, allowed_room_ids) + .is_accessible_child(current_room, join_rule, &identifier, allowed_room_ids) .await; let accessibility = if is_accessible_child { @@ -334,7 +327,7 @@ async fn get_room_summary( room_id, &join_rule.clone().into(), identifier, - join_rule.allowed_rooms(), + join_rule.allowed_room_ids(), ) .await; @@ -421,23 +414,21 @@ async fn get_room_summary( ); let summary = SpaceHierarchyParentSummary { - canonical_alias, - name, - topic, - world_readable, - guest_can_join, - avatar_url, - room_type, children_state, - encryption, - room_version, - room_id: room_id.to_owned(), - num_joined_members: num_joined_members.try_into().unwrap_or_default(), - allowed_room_ids: join_rule - .allowed_rooms() - .map(Into::into) - .collect(), - join_rule: join_rule.clone().into(), + summary: RoomSummary { + canonical_alias, + name, + topic, + world_readable, + guest_can_join, + avatar_url, + room_type, + encryption, + room_version, + room_id: room_id.to_owned(), + num_joined_members: num_joined_members.try_into().unwrap_or_default(), + join_rule: join_rule.clone().into(), + }, }; Ok(summary) @@ -448,7 +439,7 @@ async fn get_room_summary( async fn is_accessible_child<'a, I>( &self, current_room: &RoomId, - join_rule: &SpaceRoomJoinRule, + join_rule: &JoinRuleSummary, identifier: &Identifier<'_>, allowed_rooms: I, ) -> bool @@ -486,10 +477,10 @@ where } match *join_rule { - | SpaceRoomJoinRule::Public - | SpaceRoomJoinRule::Knock - | SpaceRoomJoinRule::KnockRestricted => true, - | SpaceRoomJoinRule::Restricted => + | JoinRuleSummary::Public + | JoinRuleSummary::Knock + | JoinRuleSummary::KnockRestricted(_) => true, + | JoinRuleSummary::Restricted(_) => allowed_rooms .stream() .any(async |room| match identifier { @@ -516,9 +507,9 @@ where pub fn get_parent_children_via( parent: &SpaceHierarchyParentSummary, suggested_only: bool, -) -> impl DoubleEndedIterator + use<>)> -+ Send -+ '_ { +) -> impl DoubleEndedIterator< + Item = (OwnedRoomId, impl Iterator + Send + use<>), +> + '_ { parent .children_state .iter() @@ -535,9 +526,9 @@ async fn cache_insert( &self, mut cache: MutexGuard<'_, Cache>, current_room: &RoomId, - child: SpaceHierarchyChildSummary, + child: RoomSummary, ) { - let SpaceHierarchyChildSummary { + let RoomSummary { canonical_alias, name, num_joined_members, @@ -548,30 +539,30 @@ async fn cache_insert( avatar_url, join_rule, room_type, - allowed_room_ids, encryption, room_version, } = child; let summary = SpaceHierarchyParentSummary { - canonical_alias, - name, - num_joined_members, - topic, - world_readable, - guest_can_join, - avatar_url, - join_rule, - room_type, - allowed_room_ids, - room_id: room_id.clone(), + summary: RoomSummary { + canonical_alias, + name, + num_joined_members, + topic, + world_readable, + guest_can_join, + avatar_url, + join_rule, + room_type, + room_id: room_id.clone(), + encryption, + room_version, + }, children_state: self .get_space_child_events(&room_id) .map(Event::into_format) .collect() .await, - encryption, - room_version, }; cache.insert(current_room.to_owned(), Some(CachedSpaceHierarchySummary { summary })); @@ -581,39 +572,9 @@ async fn cache_insert( // ruma-client-api types impl From for SpaceHierarchyRoomsChunk { fn from(value: CachedSpaceHierarchySummary) -> Self { - let SpaceHierarchyParentSummary { - canonical_alias, - name, - num_joined_members, - room_id, - topic, - world_readable, - guest_can_join, - avatar_url, - join_rule, - room_type, - children_state, - allowed_room_ids, - encryption, - room_version, - } = value.summary; + let SpaceHierarchyParentSummary { children_state, summary } = value.summary; - Self { - canonical_alias, - name, - num_joined_members, - room_id, - topic, - world_readable, - guest_can_join, - avatar_url, - join_rule, - room_type, - children_state, - encryption, - room_version, - allowed_room_ids, - } + Self { children_state, summary } } } @@ -621,37 +582,7 @@ impl From for SpaceHierarchyRoomsChunk { /// ruma-client-api types #[must_use] pub fn summary_to_chunk(summary: SpaceHierarchyParentSummary) -> SpaceHierarchyRoomsChunk { - let SpaceHierarchyParentSummary { - canonical_alias, - name, - num_joined_members, - room_id, - topic, - world_readable, - guest_can_join, - avatar_url, - join_rule, - room_type, - children_state, - allowed_room_ids, - encryption, - room_version, - } = summary; + let SpaceHierarchyParentSummary { children_state, summary } = summary; - SpaceHierarchyRoomsChunk { - canonical_alias, - name, - num_joined_members, - room_id, - topic, - world_readable, - guest_can_join, - avatar_url, - join_rule, - room_type, - children_state, - encryption, - room_version, - allowed_room_ids, - } + SpaceHierarchyRoomsChunk { children_state, summary } } diff --git a/src/service/rooms/spaces/tests.rs b/src/service/rooms/spaces/tests.rs index d0395fdd..3ba7398f 100644 --- a/src/service/rooms/spaces/tests.rs +++ b/src/service/rooms/spaces/tests.rs @@ -2,21 +2,23 @@ use std::str::FromStr; use ruma::{ UInt, - api::federation::space::{SpaceHierarchyParentSummary, SpaceHierarchyParentSummaryInit}, + api::federation::space::SpaceHierarchyParentSummary, owned_room_id, owned_server_name, - space::SpaceRoomJoinRule, + room::{JoinRuleSummary, RoomSummary}, }; use crate::rooms::spaces::{PaginationToken, get_parent_children_via}; #[test] fn get_summary_children() { - let summary: SpaceHierarchyParentSummary = SpaceHierarchyParentSummaryInit { - num_joined_members: UInt::from(1_u32), - room_id: owned_room_id!("!root:example.org"), - world_readable: true, - guest_can_join: true, - join_rule: SpaceRoomJoinRule::Public, + let summary: SpaceHierarchyParentSummary = SpaceHierarchyParentSummary { + summary: RoomSummary::new( + owned_room_id!("!root:example.org"), + JoinRuleSummary::Public, + true, + UInt::from(1_u32), + true, + ), children_state: vec![ serde_json::from_str( r#"{ @@ -63,9 +65,7 @@ fn get_summary_children() { ) .unwrap(), ], - allowed_room_ids: vec![], - } - .into(); + }; assert_eq!( get_parent_children_via(&summary, false) diff --git a/src/service/rooms/state/mod.rs b/src/service/rooms/state/mod.rs index cc85fd10..2da8af3d 100644 --- a/src/service/rooms/state/mod.rs +++ b/src/service/rooms/state/mod.rs @@ -1,21 +1,21 @@ use std::{collections::HashMap, fmt::Write, iter::once, sync::Arc}; use async_trait::async_trait; -use futures::{ - FutureExt, Stream, StreamExt, TryFutureExt, TryStreamExt, future::join_all, pin_mut, -}; +use futures::{FutureExt, Stream, StreamExt, TryStreamExt, future::join_all, pin_mut}; use ruma::{ EventId, OwnedEventId, OwnedRoomId, RoomId, RoomVersionId, UserId, events::{ AnyStrippedStateEvent, StateEventType, TimelineEventType, - room::{create::RoomCreateEventContent, member::RoomMemberEventContent}, + room::member::RoomMemberEventContent, }, + room_version_rules::AuthorizationRules, serde::Raw, }; use tuwunel_core::{ Event, PduEvent, Result, err, - result::FlatOk, - state_res::{self, StateMap}, + matrix::{RoomVersionRules, StateKey, TypeStateKey, room_version}, + result::{AndThenRef, FlatOk}, + state_res::{StateMap, auth_types_for_event}, utils::{ IterStream, MutexMap, MutexMapGuard, ReadyExt, calculate_hash, stream::{BroadbandExt, TryIgnore}, @@ -27,7 +27,7 @@ use tuwunel_database::{Deserialized, Ignore, Interfix, Map}; use crate::{ Dep, globals, rooms, rooms::{ - short::{ShortEventId, ShortStateHash}, + short::{ShortEventId, ShortStateHash, ShortStateKey}, state_compressor::{CompressedState, parse_compressed_state_event}, }, }; @@ -355,11 +355,91 @@ impl Service { } } - #[tracing::instrument(skip_all, level = "trace")] - pub async fn summary_stripped(&self, event: &Pdu) -> Vec> + /// Set the state hash to a new version, but does not update state_cache. + #[tracing::instrument(skip(self, _mutex_lock), level = "debug")] + pub fn set_room_state( + &self, + room_id: &RoomId, + shortstatehash: u64, + // Take mutex guard to make sure users get the room state mutex + _mutex_lock: &RoomMutexGuard, + ) { + const BUFSIZE: usize = size_of::(); + + self.db + .roomid_shortstatehash + .raw_aput::(room_id, shortstatehash); + } + + /// This fetches auth events from the current state. + #[allow(clippy::too_many_arguments)] + #[tracing::instrument(skip(self, content), level = "debug")] + pub async fn get_auth_events( + &self, + room_id: &RoomId, + kind: &TimelineEventType, + sender: &UserId, + state_key: Option<&str>, + content: &serde_json::value::RawValue, + auth_rules: &AuthorizationRules, + include_create: bool, + ) -> Result> where - Pdu: Event, + StateEventType: Send + Sync, + StateKey: Send + Sync, { + let Ok(shortstatehash) = self.get_room_shortstatehash(room_id).await else { + return Ok(StateMap::new()); + }; + + let sauthevents: HashMap = + auth_types_for_event(kind, sender, state_key, content, auth_rules, include_create)? + .into_iter() + .stream() + .broad_filter_map(async |(event_type, state_key): TypeStateKey| { + self.services + .short + .get_shortstatekey(&event_type, &state_key) + .await + .map(move |sstatekey| (sstatekey, (event_type, state_key))) + .ok() + }) + .collect() + .await; + + self.services + .state_accessor + .state_full_shortids(shortstatehash) + .ready_filter_map(Result::ok) + .ready_filter_map(|(shortstatekey, shorteventid)| { + sauthevents + .get(&shortstatekey) + .map(move |(ty, sk)| ((ty, sk), shorteventid)) + }) + .unzip() + .map(|(state_keys, event_ids): (Vec<_>, Vec<_>)| { + self.services + .short + .multi_get_eventid_from_short(event_ids.into_iter().stream()) + .zip(state_keys.into_iter().stream()) + }) + .flatten_stream() + .ready_filter_map(|(event_id, (ty, sk))| Some(((ty, sk), event_id.ok()?))) + .broad_filter_map(async |((ty, sk), event_id): ((&_, &_), OwnedEventId)| { + let pdu = self.services.timeline.get_pdu(&event_id).await; + + Some(((ty.clone(), sk.clone()), pdu.ok()?)) + }) + .collect() + .map(Ok) + .await + } + + #[tracing::instrument(skip_all, level = "debug")] + pub async fn summary_stripped( + &self, + event: &Pdu, + ) -> Vec> { let cells = [ (&StateEventType::RoomCreate, ""), (&StateEventType::RoomJoinRules, ""), @@ -386,20 +466,12 @@ impl Service { .collect() } - /// Set the state hash to a new version, but does not update state_cache. - #[tracing::instrument(skip(self, _mutex_lock), level = "debug")] - pub fn set_room_state( - &self, - room_id: &RoomId, - shortstatehash: u64, - // Take mutex guard to make sure users get the room state mutex - _mutex_lock: &RoomMutexGuard, - ) { - const BUFSIZE: usize = size_of::(); - - self.db - .roomid_shortstatehash - .raw_aput::(room_id, shortstatehash); + /// Returns the room's version rules + #[inline] + pub async fn get_room_version_rules(&self, room_id: &RoomId) -> Result { + self.get_room_version(room_id) + .await + .and_then_ref(room_version::rules) } /// Returns the room's version. @@ -413,7 +485,9 @@ impl Service { .state_accessor .room_state_get_content(room_id, &StateEventType::RoomCreate, "") .await - .map(|content: RoomCreateEventContent| content.room_version) + .as_ref() + .map(room_version::from_create_content) + .cloned() .map_err(|e| err!(Request(NotFound("No create event found: {e:?}")))) } @@ -469,64 +543,4 @@ impl Service { self.db.roomid_pduleaves.put_raw(key, event_id); } } - - /// This fetches auth events from the current state. - #[tracing::instrument(skip(self, content), level = "trace")] - pub async fn get_auth_events( - &self, - room_id: &RoomId, - kind: &TimelineEventType, - sender: &UserId, - state_key: Option<&str>, - content: &serde_json::value::RawValue, - ) -> Result> { - let Ok(shortstatehash) = self.get_room_shortstatehash(room_id).await else { - return Ok(HashMap::new()); - }; - - let auth_types = state_res::auth_types_for_event(kind, sender, state_key, content)?; - - let sauthevents: HashMap<_, _> = auth_types - .iter() - .stream() - .broad_filter_map(|(event_type, state_key)| { - self.services - .short - .get_shortstatekey(event_type, state_key) - .map_ok(move |ssk| (ssk, (event_type, state_key))) - .map(Result::ok) - }) - .collect() - .await; - - let (state_keys, event_ids): (Vec<_>, Vec<_>) = self - .services - .state_accessor - .state_full_shortids(shortstatehash) - .ready_filter_map(Result::ok) - .ready_filter_map(|(shortstatekey, shorteventid)| { - sauthevents - .get(&shortstatekey) - .map(|(ty, sk)| ((ty, sk), shorteventid)) - }) - .unzip() - .await; - - self.services - .short - .multi_get_eventid_from_short(event_ids.into_iter().stream()) - .zip(state_keys.into_iter().stream()) - .ready_filter_map(|(event_id, (ty, sk))| Some(((ty, sk), event_id.ok()?))) - .broad_filter_map(|((ty, sk), event_id): (_, OwnedEventId)| async move { - self.services - .timeline - .get_pdu(&event_id) - .await - .map(move |pdu| (((*ty).clone(), (*sk).clone()), pdu)) - .ok() - }) - .collect() - .map(Ok) - .await - } } diff --git a/src/service/rooms/state_accessor/mod.rs b/src/service/rooms/state_accessor/mod.rs index 9143ad75..8d4d5fc7 100644 --- a/src/service/rooms/state_accessor/mod.rs +++ b/src/service/rooms/state_accessor/mod.rs @@ -6,6 +6,7 @@ mod user_can; use std::sync::Arc; use async_trait::async_trait; +use futures::{FutureExt, TryFutureExt, future::try_join}; use ruma::{ EventEncryptionAlgorithm, JsOption, OwnedRoomAliasId, RoomId, UserId, events::{ @@ -20,12 +21,16 @@ use ruma::{ join_rules::{JoinRule, RoomJoinRulesEventContent}, member::RoomMemberEventContent, name::RoomNameEventContent, + power_levels::{RoomPowerLevels, RoomPowerLevelsEventContent}, topic::RoomTopicEventContent, }, }, room::RoomType, }; -use tuwunel_core::{Result, err}; +use tuwunel_core::{ + Result, err, + matrix::{Event, room_version, state_res::events::RoomCreateEvent}, +}; use tuwunel_database::Map; use crate::{Dep, rooms}; @@ -69,6 +74,31 @@ impl crate::Service for Service { } impl Service { + /// Gets the effective power levels of a room, regardless of if there is an + /// `m.rooms.power_levels` state. + pub async fn get_power_levels(&self, room_id: &RoomId) -> Result { + let create = self.get_create(room_id); + let power_levels = self + .room_state_get_content(room_id, &StateEventType::RoomPowerLevels, "") + .map_ok(|c: RoomPowerLevelsEventContent| c) + .map(Result::ok) + .map(Ok); + + let (create, power_levels) = try_join(create, power_levels).await?; + + let room_version = create.room_version()?; + let rules = room_version::rules(&room_version)?; + let creators = create.creators(&rules.authorization)?; + + Ok(RoomPowerLevels::new(power_levels.into(), &rules.authorization, creators)) + } + + pub async fn get_create(&self, room_id: &RoomId) -> Result> { + self.room_state_get(room_id, &StateEventType::RoomCreate, "") + .await + .map(RoomCreateEvent::new) + } + pub async fn get_name(&self, room_id: &RoomId) -> Result { self.room_state_get_content(room_id, &StateEventType::RoomName, "") .await diff --git a/src/service/rooms/state_accessor/state.rs b/src/service/rooms/state_accessor/state.rs index c83b687a..f6c099f0 100644 --- a/src/service/rooms/state_accessor/state.rs +++ b/src/service/rooms/state_accessor/state.rs @@ -321,7 +321,9 @@ pub fn state_full( shortstatehash: ShortStateHash, ) -> impl Stream + Send + '_ { self.state_full_pdus(shortstatehash) - .ready_filter_map(|pdu| Some(((pdu.kind().clone().into(), pdu.state_key()?.into()), pdu))) + .ready_filter_map(|pdu| { + Some(((pdu.kind().to_cow_str().into(), pdu.state_key()?.into()), pdu)) + }) } #[implement(super::Service)] diff --git a/src/service/rooms/state_accessor/user_can.rs b/src/service/rooms/state_accessor/user_can.rs index 75fbdd77..b4ab009a 100644 --- a/src/service/rooms/state_accessor/user_can.rs +++ b/src/service/rooms/state_accessor/user_can.rs @@ -5,7 +5,6 @@ use ruma::{ room::{ history_visibility::{HistoryVisibility, RoomHistoryVisibilityEventContent}, member::{MembershipState, RoomMemberEventContent}, - power_levels::{RoomPowerLevels, RoomPowerLevelsEventContent}, }, }, }; @@ -44,28 +43,18 @@ pub async fn user_can_redact( ))); } - match self - .room_state_get_content::( - room_id, - &StateEventType::RoomPowerLevels, - "", - ) - .await - { - | Ok(pl_event_content) => { - let pl_event: RoomPowerLevels = pl_event_content.into(); - Ok(pl_event.user_can_redact_event_of_other(sender) - || pl_event.user_can_redact_own_event(sender) - && match redacting_event { - | Ok(redacting_event) => - if federation { - redacting_event.sender().server_name() == sender.server_name() - } else { - redacting_event.sender() == sender - }, - | _ => false, - }) - }, + match self.get_power_levels(room_id).await { + | Ok(power_levels) => Ok(power_levels.user_can_redact_event_of_other(sender) + || power_levels.user_can_redact_own_event(sender) + && match redacting_event { + | Ok(redacting_event) => + if federation { + redacting_event.sender().server_name() == sender.server_name() + } else { + redacting_event.sender() == sender + }, + | _ => false, + }), | _ => { // Falling back on m.room.create to judge power level match self diff --git a/src/service/rooms/state_cache/mod.rs b/src/service/rooms/state_cache/mod.rs index 5de4d47f..bb3e6636 100644 --- a/src/service/rooms/state_cache/mod.rs +++ b/src/service/rooms/state_cache/mod.rs @@ -405,7 +405,7 @@ pub fn rooms_invited<'a>( .stream_prefix(&prefix) .ignore_err() .map(|((_, room_id), state): KeyVal<'_>| (room_id.to_owned(), state)) - .map(|(room_id, state)| Ok((room_id, state.deserialize_as()?))) + .map(|(room_id, state)| Ok((room_id, state.deserialize_as_unchecked()?))) .ignore_err() } @@ -425,7 +425,7 @@ pub fn rooms_knocked<'a>( .stream_prefix(&prefix) .ignore_err() .map(|((_, room_id), state): KeyVal<'_>| (room_id.to_owned(), state)) - .map(|(room_id, state)| Ok((room_id, state.deserialize_as()?))) + .map(|(room_id, state)| Ok((room_id, state.deserialize_as_unchecked()?))) .ignore_err() } @@ -442,7 +442,9 @@ pub async fn invite_state( .qry(&key) .await .deserialized() - .and_then(|val: Raw>| val.deserialize_as().map_err(Into::into)) + .and_then(|val: Raw>| { + val.deserialize_as_unchecked().map_err(Into::into) + }) } #[implement(Service)] @@ -458,7 +460,9 @@ pub async fn knock_state( .qry(&key) .await .deserialized() - .and_then(|val: Raw>| val.deserialize_as().map_err(Into::into)) + .and_then(|val: Raw>| { + val.deserialize_as_unchecked().map_err(Into::into) + }) } #[implement(Service)] @@ -474,7 +478,9 @@ pub async fn left_state( .qry(&key) .await .deserialized() - .and_then(|val: Raw>| val.deserialize_as().map_err(Into::into)) + .and_then(|val: Raw>| { + val.deserialize_as_unchecked().map_err(Into::into) + }) } /// Returns an iterator over all rooms a user left. @@ -493,7 +499,7 @@ pub fn rooms_left<'a>( .stream_prefix(&prefix) .ignore_err() .map(|((_, room_id), state): KeyVal<'_>| (room_id.to_owned(), state)) - .map(|(room_id, state)| Ok((room_id, state.deserialize_as()?))) + .map(|(room_id, state)| Ok((room_id, state.deserialize_as_unchecked()?))) .ignore_err() } diff --git a/src/service/rooms/timeline/append.rs b/src/service/rooms/timeline/append.rs index 8b7c711e..f0f3a8bf 100644 --- a/src/service/rooms/timeline/append.rs +++ b/src/service/rooms/timeline/append.rs @@ -7,12 +7,11 @@ use futures::StreamExt; use ruma::{ CanonicalJsonObject, CanonicalJsonValue, EventId, OwnedUserId, RoomId, RoomVersionId, UserId, events::{ - GlobalAccountDataEventType, StateEventType, TimelineEventType, + GlobalAccountDataEventType, TimelineEventType, push_rules::PushRulesEvent, room::{ encrypted::Relation, member::{MembershipState, RoomMemberEventContent}, - power_levels::RoomPowerLevelsEventContent, redaction::RoomRedactionEventContent, }, }, @@ -186,14 +185,6 @@ where drop(insert_lock); - // See if the event matches any known pushers via power level - let power_levels: RoomPowerLevelsEventContent = self - .services - .state_accessor - .room_state_get_content(pdu.room_id(), &StateEventType::RoomPowerLevels, "") - .await - .unwrap_or_default(); - // Don't notify the sender of their own events, and dont send from ignored users let mut push_target: HashSet<_> = self .services @@ -245,6 +236,12 @@ where let mut highlight = false; let mut notify = false; + let power_levels = self + .services + .state_accessor + .get_power_levels(pdu.room_id()) + .await?; + for action in self .services .pusher diff --git a/src/service/rooms/timeline/build.rs b/src/service/rooms/timeline/build.rs index ff097793..37fa1043 100644 --- a/src/service/rooms/timeline/build.rs +++ b/src/service/rooms/timeline/build.rs @@ -35,6 +35,15 @@ pub async fn build_and_append_pdu( .create_hash_and_sign_event(pdu_builder, sender, room_id, state_lock) .await?; + //TODO: Use proper room version here + if *pdu.kind() == TimelineEventType::RoomCreate && pdu.room_id().server_name().is_none() { + let _short_id = self + .services + .short + .get_or_create_shortroomid(pdu.room_id()) + .await; + } + if self .services .admin diff --git a/src/service/rooms/timeline/create.rs b/src/service/rooms/timeline/create.rs index d1ce68f7..b54603ad 100644 --- a/src/service/rooms/timeline/create.rs +++ b/src/service/rooms/timeline/create.rs @@ -1,21 +1,26 @@ use std::cmp; -use futures::{StreamExt, TryStreamExt, future, future::ready}; +use futures::{StreamExt, TryStreamExt}; use ruma::{ - CanonicalJsonObject, CanonicalJsonValue, OwnedEventId, RoomId, RoomVersionId, UserId, - canonical_json::to_canonical_value, + CanonicalJsonObject, CanonicalJsonValue, MilliSecondsSinceUnixEpoch, OwnedEventId, + OwnedRoomId, RoomId, RoomVersionId, UserId, events::{StateEventType, TimelineEventType, room::create::RoomCreateEventContent}, + room_version_rules::RoomIdFormatVersion, uint, }; use serde_json::value::to_raw_value; use tuwunel_core::{ Err, Error, Result, err, implement, matrix::{ - event::{Event, gen_event_id}, + event::{Event, StateKey, TypeExt, gen_event_id}, pdu::{EventHash, PduBuilder, PduEvent}, - state_res::{self, RoomVersion}, + room_version, + state_res::{self}, + }, + utils::{ + IterStream, ReadyExt, TryReadyExt, millis_since_unix_epoch, stream::TryIgnore, + to_canonical_object, }, - utils::{self, IterStream, ReadyExt, stream::TryIgnore}, }; use super::RoomMutexGuard; @@ -48,7 +53,7 @@ pub async fn create_hash_and_sign_event( .await; // If there was no create event yet, assume we are creating a room - let room_version_id = self + let (room_version, version_rules) = self .services .state .get_room_version(room_id) @@ -63,14 +68,23 @@ pub async fn create_hash_and_sign_event( room_id.to_owned(), )) } + }) + .and_then(|room_version| { + Ok((room_version.clone(), room_version::rules(&room_version)?)) })?; - let room_version = RoomVersion::new(&room_version_id).expect("room version is supported"); - let auth_events = self .services .state - .get_auth_events(room_id, &event_type, sender, state_key.as_deref(), &content) + .get_auth_events( + room_id, + &event_type, + sender, + state_key.as_deref(), + &content, + &version_rules.authorization, + true, + ) .await?; // Our depth is the maximum depth of prev_events + 1 @@ -79,7 +93,7 @@ pub async fn create_hash_and_sign_event( .stream() .map(Ok) .and_then(|event_id| self.get_pdu(event_id)) - .and_then(|pdu| future::ok(pdu.depth)) + .ready_and_then(|pdu| Ok(pdu.depth)) .ignore_err() .ready_fold(uint!(0), cmp::max) .await @@ -100,26 +114,25 @@ pub async fn create_hash_and_sign_event( } } - let unsigned = if unsigned.is_empty() { - None - } else { - Some(to_raw_value(&unsigned)?) - }; + let unsigned = unsigned + .is_empty() + .eq(&false) + .then_some(to_raw_value(&unsigned)?); - let origin_server_ts = timestamp.map_or_else( - || { - utils::millis_since_unix_epoch() + let origin_server_ts = timestamp + .as_ref() + .map(MilliSecondsSinceUnixEpoch::get) + .unwrap_or_else(|| { + millis_since_unix_epoch() .try_into() - .expect("timestamp to UInt") - }, - |ts| ts.get(), - ); + .expect("u64 to UInt") + }); let mut pdu = PduEvent { - event_id: ruma::event_id!("$thiswillbefilledinlater").into(), + event_id: ruma::event_id!("$thiswillbereplaced").into(), room_id: room_id.to_owned(), sender: sender.to_owned(), - origin: None, + origin: Some(self.services.globals.server_name().to_owned()), origin_server_ts, kind: event_type, content, @@ -132,55 +145,57 @@ pub async fn create_hash_and_sign_event( prev_events, auth_events: auth_events .values() + .filter(|pdu| { + version_rules + .event_format + .allow_room_create_in_auth_events + || *pdu.kind() != TimelineEventType::RoomCreate + }) .map(|pdu| pdu.event_id.clone()) .collect(), }; - let auth_fetch = |k: &StateEventType, s: &str| { - let key = (k.clone(), s.into()); - ready(auth_events.get(&key).map(ToOwned::to_owned)) + let auth_fetch = async |k: StateEventType, s: StateKey| { + auth_events + .get(&k.with_state_key(s.as_str())) + .map(ToOwned::to_owned) + .ok_or_else(|| err!(Request(NotFound("Missing auth events")))) }; - let auth_check = state_res::auth_check( - &room_version, + state_res::auth_check( + &version_rules, &pdu, - None, // TODO: third_party_invite - auth_fetch, + &async |event_id: OwnedEventId| self.get_pdu(&event_id).await, + &auth_fetch, ) - .await - .map_err(|e| err!(Request(Forbidden(warn!("Auth check failed: {e:?}")))))?; - - if !auth_check { - return Err!(Request(Forbidden("Event is not authorized."))); - } + .await?; // Hash and sign - let mut pdu_json = utils::to_canonical_object(&pdu).map_err(|e| { + let mut pdu_json = to_canonical_object(&pdu).map_err(|e| { err!(Request(BadJson(warn!("Failed to convert PDU to canonical JSON: {e}")))) })?; // room v3 and above removed the "event_id" field from remote PDU format - match room_version_id { - | RoomVersionId::V1 | RoomVersionId::V2 => {}, - | _ => { - pdu_json.remove("event_id"); - }, + if !matches!(room_version, RoomVersionId::V1 | RoomVersionId::V2) { + pdu_json.remove("event_id"); } - // Add origin because synapse likes that (and it's required in the spec) - pdu_json.insert( - "origin".to_owned(), - to_canonical_value(self.services.globals.server_name()) - .expect("server name is a valid CanonicalJsonValue"), - ); + // room v12 and above removed the placeholder "room_id" field from m.room.create + if matches!(version_rules.room_id_format, RoomIdFormatVersion::V2) + && pdu.kind == TimelineEventType::RoomCreate + { + pdu_json.remove("room_id"); + } if let Err(e) = self .services .server_keys - .hash_and_sign_event(&mut pdu_json, &room_version_id) + .hash_and_sign_event(&mut pdu_json, &room_version) { + use ruma::signatures::Error::PduSize; + return match e { - | Error::Signatures(ruma::signatures::Error::PduSize) => { + | Error::Signatures(PduSize) => { Err!(Request(TooLarge("Message/PDU is too long (exceeds 65535 bytes)"))) }, | _ => Err!(Request(Unknown(warn!("Signing event failed: {e}")))), @@ -188,10 +203,17 @@ pub async fn create_hash_and_sign_event( } // Generate event id - pdu.event_id = gen_event_id(&pdu_json, &room_version_id)?; - + pdu.event_id = gen_event_id(&pdu_json, &room_version)?; pdu_json.insert("event_id".into(), CanonicalJsonValue::String(pdu.event_id.clone().into())); + // Room id is event id for V12+ + if matches!(version_rules.room_id_format, RoomIdFormatVersion::V2) + && pdu.kind == TimelineEventType::RoomCreate + { + pdu.room_id = OwnedRoomId::from_parts('!', pdu.event_id.localpart(), None)?; + pdu_json.insert("room_id".into(), CanonicalJsonValue::String(pdu.room_id.clone().into())); + } + // Generate short event id let _shorteventid = self .services diff --git a/src/service/sending/appservice.rs b/src/service/sending/appservice.rs index 9d868473..58d3986a 100644 --- a/src/service/sending/appservice.rs +++ b/src/service/sending/appservice.rs @@ -3,7 +3,8 @@ use std::{fmt::Debug, mem}; use bytes::BytesMut; use reqwest::Client; use ruma::api::{ - IncomingResponse, MatrixVersion, OutgoingRequest, SendAccessToken, appservice::Registration, + IncomingResponse, MatrixVersion, OutgoingRequest, SendAccessToken, SupportedVersions, + appservice::Registration, }; use tuwunel_core::{Err, Result, debug_error, err, trace, utils, warn}; @@ -20,6 +21,10 @@ where T: OutgoingRequest + Debug + Send, { const VERSIONS: [MatrixVersion; 1] = [MatrixVersion::V1_7]; + let supported = SupportedVersions { + versions: VERSIONS.into(), + features: Default::default(), + }; let Some(dest) = registration.url else { return Ok(None); @@ -36,7 +41,7 @@ where .try_into_http_request::( &dest, SendAccessToken::IfRequired(hs_token), - &VERSIONS, + &supported, ) .map_err(|e| { err!(BadServerResponse( @@ -76,6 +81,7 @@ where let mut http_response_builder = http::Response::builder() .status(status) .version(response.version()); + mem::swap( response.headers_mut(), http_response_builder diff --git a/src/service/sending/sender.rs b/src/service/sending/sender.rs index 162b070e..917de252 100644 --- a/src/service/sending/sender.rs +++ b/src/service/sending/sender.rs @@ -16,8 +16,8 @@ use futures::{ stream::FuturesUnordered, }; use ruma::{ - CanonicalJsonObject, MilliSecondsSinceUnixEpoch, OwnedRoomId, OwnedServerName, OwnedUserId, - RoomId, RoomVersionId, ServerName, UInt, + CanonicalJsonObject, CanonicalJsonValue, MilliSecondsSinceUnixEpoch, OwnedRoomId, + OwnedServerName, OwnedUserId, RoomId, ServerName, UInt, api::{ appservice::event::push_events::v1::EphemeralData, federation::transactions::{ @@ -39,7 +39,8 @@ use ruma::{ }; use serde_json::value::{RawValue as RawJsonValue, to_raw_value}; use tuwunel_core::{ - Error, Event, Result, debug, err, error, + Error, Event, Result, debug, err, error, implement, + matrix::room_version, result::LogErr, trace, utils::{ @@ -720,7 +721,7 @@ impl Service { .filter(|event| matches!(event, SendingEvent::Pdu(_))) .count(), ); - let mut edu_jsons: Vec = Vec::with_capacity( + let mut edu_jsons: Vec> = Vec::with_capacity( events .iter() .filter(|event| matches!(event, SendingEvent::Edu(_))) @@ -740,7 +741,9 @@ impl Service { }, | SendingEvent::Edu(edu) => if appservice.receive_ephemeral { - if let Ok(edu) = serde_json::from_slice(edu) { + if let Ok(edu) = + serde_json::from_slice(edu).and_then(|edu| Raw::new(&edu)) + { edu_jsons.push(edu); } }, @@ -749,7 +752,7 @@ impl Service { } let txn_hash = calculate_hash(events.iter().filter_map(|e| match e { - | SendingEvent::Edu(b) => Some(&**b), + | SendingEvent::Edu(b) => Some(b.as_ref()), | SendingEvent::Pdu(b) => Some(b.as_ref()), | SendingEvent::Flush => None, })); @@ -763,8 +766,8 @@ impl Service { client, appservice, ruma::api::appservice::event::push_events::v1::Request { - events: pdu_jsons, txn_id: txn_id.into(), + events: pdu_jsons, ephemeral: edu_jsons, to_device: Vec::new(), // TODO }, @@ -933,47 +936,59 @@ impl Service { | Ok(_) => Ok(Destination::Federation(server)), } } +} - /// This does not return a full `Pdu` it is only to satisfy ruma's types. - pub async fn convert_to_outgoing_federation_event( - &self, - mut pdu_json: CanonicalJsonObject, - ) -> Box { - if let Some(unsigned) = pdu_json - .get_mut("unsigned") - .and_then(|val| val.as_object_mut()) - { - unsigned.remove("transaction_id"); - } +/// This does not return a full `Pdu` it is only to satisfy ruma's types. +#[implement(Service)] +pub async fn convert_to_outgoing_federation_event( + &self, + mut pdu_json: CanonicalJsonObject, +) -> Box { + self.strip_outgoing_federation_event(&mut pdu_json) + .await; - // room v3 and above removed the "event_id" field from remote PDU format - if let Some(room_id) = pdu_json - .get("room_id") - .and_then(|val| RoomId::parse(val.as_str()?).ok()) - { - match self - .services - .state - .get_room_version(room_id) - .await - { - | Ok(room_version_id) => match room_version_id { - | RoomVersionId::V1 | RoomVersionId::V2 => {}, - | _ => _ = pdu_json.remove("event_id"), - }, - | Err(_) => _ = pdu_json.remove("event_id"), - } - } else { - pdu_json.remove("event_id"); - } + to_raw_value(&pdu_json).expect("CanonicalJson is valid serde_json::Value") +} - // TODO: another option would be to convert it to a canonical string to validate - // size and return a Result> - // serde_json::from_str::>( - // ruma::serde::to_canonical_json_string(pdu_json).expect("CanonicalJson is - // valid serde_json::Value"), ) - // .expect("Raw::from_value always works") +#[implement(Service)] +async fn strip_outgoing_federation_event(&self, pdu_json: &mut CanonicalJsonObject) { + if let Some(unsigned) = pdu_json + .get_mut("unsigned") + .and_then(|val| val.as_object_mut()) + { + unsigned.remove("transaction_id"); + } - to_raw_value(&pdu_json).expect("CanonicalJson is valid serde_json::Value") + let Some(room_id) = pdu_json + .get("room_id") + .and_then(CanonicalJsonValue::as_str) + .map(RoomId::parse) + .transpose() + .ok() + .flatten() + else { + return; + }; + + let Ok(room_rules) = self + .services + .state + .get_room_version(room_id) + .await + .and_then(|ref ver| room_version::rules(ver)) + else { + pdu_json.remove("event_id"); + return; + }; + + if !room_rules.event_format.require_event_id { + pdu_json.remove("event_id"); + } + + if !room_rules + .event_format + .require_room_create_room_id + { + pdu_json.remove("room_id"); } } diff --git a/src/service/server_keys/get.rs b/src/service/server_keys/get.rs index dfd1dd9e..71cfb0d7 100644 --- a/src/service/server_keys/get.rs +++ b/src/service/server_keys/get.rs @@ -1,8 +1,8 @@ use std::borrow::Borrow; use ruma::{ - CanonicalJsonObject, RoomVersionId, ServerName, ServerSigningKeyId, - api::federation::discovery::VerifyKey, + CanonicalJsonObject, ServerName, ServerSigningKeyId, api::federation::discovery::VerifyKey, + room_version_rules::RoomVersionRules, }; use tuwunel_core::{Err, Result, implement}; @@ -12,11 +12,11 @@ use super::{PubKeyMap, PubKeys, extract_key}; pub async fn get_event_keys( &self, object: &CanonicalJsonObject, - version: &RoomVersionId, + version: &RoomVersionRules, ) -> Result { use ruma::signatures::required_keys; - let required = match required_keys(object, version) { + let required = match required_keys(object, &version.signatures) { | Ok(required) => required, | Err(e) => { return Err!(BadServerResponse("Failed to determine keys required to verify: {e}")); diff --git a/src/service/server_keys/mod.rs b/src/service/server_keys/mod.rs index 186940e4..af36713f 100644 --- a/src/service/server_keys/mod.rs +++ b/src/service/server_keys/mod.rs @@ -9,9 +9,10 @@ use std::{collections::BTreeMap, sync::Arc, time::Duration}; use futures::StreamExt; use ruma::{ - CanonicalJsonObject, MilliSecondsSinceUnixEpoch, OwnedServerSigningKeyId, RoomVersionId, - ServerName, ServerSigningKeyId, + CanonicalJsonObject, MilliSecondsSinceUnixEpoch, OwnedServerSigningKeyId, ServerName, + ServerSigningKeyId, api::federation::discovery::{ServerSigningKeys, VerifyKey}, + room_version_rules::RoomVersionRules, serde::Raw, signatures::{Ed25519KeyPair, PublicKeyMap, PublicKeySet}, }; @@ -109,6 +110,7 @@ async fn add_signing_keys(&self, new_keys: ServerSigningKeys) { keys.verify_keys.extend(new_keys.verify_keys); keys.old_verify_keys .extend(new_keys.old_verify_keys); + self.db .server_signingkeys .raw_put(origin, Json(&keys)); @@ -118,11 +120,11 @@ async fn add_signing_keys(&self, new_keys: ServerSigningKeys) { pub async fn required_keys_exist( &self, object: &CanonicalJsonObject, - version: &RoomVersionId, + rules: &RoomVersionRules, ) -> bool { use ruma::signatures::required_keys; - let Ok(required_keys) = required_keys(object, version) else { + let Ok(required_keys) = required_keys(object, &rules.signatures) else { return false; }; @@ -191,6 +193,7 @@ pub async fn signing_keys_for(&self, origin: &ServerName) -> Result MilliSecondsSinceUnixEpoch { let timepoint = timepoint_from_now(self.minimum_valid).expect("SystemTime should not overflow"); + MilliSecondsSinceUnixEpoch::from_system_time(timepoint).expect("UInt should not overflow") } diff --git a/src/service/server_keys/sign.rs b/src/service/server_keys/sign.rs index 6b17b557..58d08d86 100644 --- a/src/service/server_keys/sign.rs +++ b/src/service/server_keys/sign.rs @@ -1,5 +1,5 @@ use ruma::{CanonicalJsonObject, RoomVersionId}; -use tuwunel_core::{Result, implement}; +use tuwunel_core::{Result, err, implement}; #[implement(super::Service)] pub fn sign_json(&self, object: &mut CanonicalJsonObject) -> Result { @@ -17,6 +17,18 @@ pub fn hash_and_sign_event( ) -> Result { use ruma::signatures::hash_and_sign_event; - let server_name = self.services.globals.server_name().as_str(); - hash_and_sign_event(server_name, self.keypair(), object, room_version).map_err(Into::into) + let server_name = &self.services.server.name; + let room_version_rules = room_version.rules().ok_or_else(|| { + err!(Request(UnsupportedRoomVersion( + "Cannot hash and sign event for unknown room version {room_version:?}." + ))) + })?; + + hash_and_sign_event( + server_name.as_str(), + self.keypair(), + object, + &room_version_rules.redaction, + ) + .map_err(Into::into) } diff --git a/src/service/server_keys/verify.rs b/src/service/server_keys/verify.rs index c1a5e06c..f83e811c 100644 --- a/src/service/server_keys/verify.rs +++ b/src/service/server_keys/verify.rs @@ -2,7 +2,7 @@ use ruma::{ CanonicalJsonObject, CanonicalJsonValue, OwnedEventId, RoomVersionId, signatures::Verified, }; use serde_json::value::RawValue as RawJsonValue; -use tuwunel_core::{Err, Result, implement, matrix::event::gen_event_id_canonical_json}; +use tuwunel_core::{Err, Result, err, implement, matrix::event::gen_event_id_canonical_json}; #[implement(super::Service)] pub async fn validate_and_add_event_id( @@ -32,8 +32,14 @@ pub async fn validate_and_add_event_id_no_fetch( room_version: &RoomVersionId, ) -> Result<(OwnedEventId, CanonicalJsonObject)> { let (event_id, mut value) = gen_event_id_canonical_json(pdu, room_version)?; + let room_version_rules = room_version.rules().ok_or_else(|| { + err!(Request(UnsupportedRoomVersion( + "Cannot verify event for unknown room version {room_version:?}." + ))) + })?; + if !self - .required_keys_exist(&value, room_version) + .required_keys_exist(&value, &room_version_rules) .await { return Err!(BadServerResponse(debug_warn!( @@ -62,8 +68,17 @@ pub async fn verify_event( room_version: Option<&RoomVersionId>, ) -> Result { let room_version = room_version.unwrap_or(&RoomVersionId::V11); - let keys = self.get_event_keys(event, room_version).await?; - ruma::signatures::verify_event(&keys, event, room_version).map_err(Into::into) + let room_version_rules = room_version.rules().ok_or_else(|| { + err!(Request(UnsupportedRoomVersion( + "Cannot verify event for unknown room version {room_version:?}." + ))) + })?; + + let event_keys = self + .get_event_keys(event, &room_version_rules) + .await?; + + ruma::signatures::verify_event(&event_keys, event, &room_version_rules).map_err(Into::into) } #[implement(super::Service)] @@ -73,6 +88,15 @@ pub async fn verify_json( room_version: Option<&RoomVersionId>, ) -> Result { let room_version = room_version.unwrap_or(&RoomVersionId::V11); - let keys = self.get_event_keys(event, room_version).await?; - ruma::signatures::verify_json(&keys, event.clone()).map_err(Into::into) + let room_version_rules = room_version.rules().ok_or_else(|| { + err!(Request(UnsupportedRoomVersion( + "Cannot verify json for unknown room version {room_version:?}." + ))) + })?; + + let event_keys = self + .get_event_keys(event, &room_version_rules) + .await?; + + ruma::signatures::verify_json(&event_keys, event).map_err(Into::into) } diff --git a/src/service/sync/mod.rs b/src/service/sync/mod.rs index 400d68c3..fa5d0129 100644 --- a/src/service/sync/mod.rs +++ b/src/service/sync/mod.rs @@ -119,7 +119,8 @@ impl Service { &mut list.room_details.required_state, &cached_list.room_details.required_state, ); - some_or_sticky(&mut list.include_heroes, cached_list.include_heroes); + + //some_or_sticky(&mut list.include_heroes, cached_list.include_heroes); match (&mut list.filters, cached_list.filters.clone()) { | (Some(filters), Some(cached_filters)) => { diff --git a/tests/test_results/complement/test_results.jsonl b/tests/test_results/complement/test_results.jsonl index a556c802..476bac17 100644 --- a/tests/test_results/complement/test_results.jsonl +++ b/tests/test_results/complement/test_results.jsonl @@ -245,12 +245,12 @@ {"Action":"pass","Test":"TestKnocking/Attempting_to_join_a_room_with_join_rule_'knock'_without_an_invite_should_fail#01"} {"Action":"pass","Test":"TestKnocking/Change_the_join_rule_of_a_room_from_'invite'_to_'knock'"} {"Action":"pass","Test":"TestKnocking/Change_the_join_rule_of_a_room_from_'invite'_to_'knock'#01"} -{"Action":"pass","Test":"TestKnocking/Knocking_on_a_room_with_a_join_rule_other_than_'knock'_should_fail"} -{"Action":"pass","Test":"TestKnocking/Knocking_on_a_room_with_a_join_rule_other_than_'knock'_should_fail#01"} +{"Action":"fail","Test":"TestKnocking/Knocking_on_a_room_with_a_join_rule_other_than_'knock'_should_fail"} +{"Action":"fail","Test":"TestKnocking/Knocking_on_a_room_with_a_join_rule_other_than_'knock'_should_fail#01"} {"Action":"fail","Test":"TestKnocking/Knocking_on_a_room_with_join_rule_'knock'_should_succeed"} {"Action":"fail","Test":"TestKnocking/Knocking_on_a_room_with_join_rule_'knock'_should_succeed#01"} -{"Action":"pass","Test":"TestKnocking/Users_in_the_room_see_a_user's_membership_update_when_they_knock"} -{"Action":"pass","Test":"TestKnocking/Users_in_the_room_see_a_user's_membership_update_when_they_knock#01"} +{"Action":"fail","Test":"TestKnocking/Users_in_the_room_see_a_user's_membership_update_when_they_knock"} +{"Action":"fail","Test":"TestKnocking/Users_in_the_room_see_a_user's_membership_update_when_they_knock#01"} {"Action":"fail","Test":"TestKnockingInMSC3787Room"} {"Action":"fail","Test":"TestKnockingInMSC3787Room/A_user_can_knock_on_a_room_without_a_reason"} {"Action":"fail","Test":"TestKnockingInMSC3787Room/A_user_can_knock_on_a_room_without_a_reason#01"} diff --git a/tuwunel-example.toml b/tuwunel-example.toml index 4661355f..bba9a70b 100644 --- a/tuwunel-example.toml +++ b/tuwunel-example.toml @@ -533,20 +533,33 @@ #allow_room_creation = true # Set to false to disable users from joining or creating room versions -# that aren't officially supported by tuwunel. +# that aren't officially supported by tuwunel. Unstable room versions may +# have flawed specifications or our implementation may be non-conforming. +# Correct operation may not be guaranteed, but incorrect operation may be +# tolerable and unnoticed. # -# tuwunel officially supports room versions 6 - 11. -# -# tuwunel has slightly experimental (though works fine in practice) -# support for versions 3 - 5. +# tuwunel officially supports room versions 6+. tuwunel has slightly +# experimental (though works fine in practice) support for versions 3 - 5. # #allow_unstable_room_versions = true +# Set to true to enable experimental room versions. +# +# Unlike unstable room versions these versions are either under +# development, protype spec-changes, or somehow present a serious risk to +# the server's operation or database corruption. This is for developer use +# only. +# +#allow_experimental_room_versions = false + # Default room version tuwunel will create rooms with. # -# Per spec, room version 11 is the default. +# The default is prescribed by the spec, but may be selected by developer +# recommendation. To prevent stale documentation we no longer list it +# here. It is only advised to override this if you know what you are +# doing, and by doing so, updates with new versions are precluded. # -#default_room_version = 11 +#default_room_version = # This item is undocumented. Please contribute documentation for it. # @@ -1615,6 +1628,30 @@ # #config_reload_signal = true +# Backport state-reset security fixes to all room versions. +# +# This option applies the State Resolution 2.1 mitigation developed during +# project Hydra for room version 12 to all prior State Resolution 2.0 room +# versions (all room versions supported by this server). These mitigations +# increase resilience to state-resets without any new definition of +# correctness; therefor it is safe to set this to true for existing rooms. +# +# Furthermore, state-reset attacks are not consistent as they result in +# rooms without any single consensus, therefor it is unnecessary to set +# this to false to match other servers which set this to false or simply +# lack support; even if replicating the post-reset state suffered by other +# servers is somehow desired. +# +# This option exists for developer and debug use, and as a failsafe in +# lieu of hardcoding it. +# +# This currently defaults to false as a matter of development until +# real-world testing can shake out any implementation issues rather than +# jeopardize existing rooms, but otherwise will default to true at the +# next point release or patch. +# +#hydra_backports = false + #[global.tls] # Path to a valid TLS certificate file.