From 954fe5e51e34b7c580a88cb3b8d91b76f7a6128f Mon Sep 17 00:00:00 2001 From: Devin R Date: Sun, 19 Jul 2020 08:42:45 -0400 Subject: [PATCH] Done transcribing all logic is filled in no more unimplemented! --- Cargo.toml | 2 + src/event_auth.rs | 567 ++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 238 +++++++++++++++++-- src/room_version.rs | 147 ++++++++++++ src/state_event.rs | 133 +++++++++-- src/state_store.rs | 14 +- tests/init.rs | 8 +- 7 files changed, 1064 insertions(+), 45 deletions(-) create mode 100644 src/event_auth.rs create mode 100644 src/room_version.rs diff --git a/Cargo.toml b/Cargo.toml index e72bb7e4..bb725f44 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,8 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +itertools = "0.9.0" +js_int = "0.1.8" petgraph = "0.5.1" serde = { version = "1.0.114", features = ["derive"] } serde_json = "1.0.56" diff --git a/src/event_auth.rs b/src/event_auth.rs new file mode 100644 index 00000000..4b0ebe11 --- /dev/null +++ b/src/event_auth.rs @@ -0,0 +1,567 @@ +use std::convert::TryFrom; + +use ruma::{ + events::{ + room::{self, join_rules::JoinRule, member::MembershipState}, + EventType, + }, + identifiers::{RoomVersionId, UserId}, +}; + +use crate::{room_version::RoomVersion, state_event::StateEvent, StateMap}; + +/// Represents the 3 event redaction outcomes. +pub enum RedactAllowed { + /// The event is the users so redaction can take place. + OwnEvent, + /// The user can easily redact the event. + CanRedact, + /// The user does not have enough power to redact this event. + No, +} + +pub(crate) fn auth_types_for_event(event: &StateEvent) -> Vec<(EventType, String)> { + if event.kind() == EventType::RoomCreate { + return vec![]; + } + + let mut auth_types = vec![ + (EventType::RoomPowerLevels, "".to_string()), + (EventType::RoomMember, event.sender().to_string()), + (EventType::RoomCreate, "".to_string()), + ]; + + if event.kind() == EventType::RoomMember { + if let Ok(content) = event.deserialize_content::() { + if [MembershipState::Join, MembershipState::Invite].contains(&content.membership) { + auth_types.push((EventType::RoomJoinRules, "".into())) + } + + // TODO is None the same as "" for state_key, probably NOT + auth_types.push((EventType::RoomMember, event.state_key().unwrap_or_default())); + + if content.membership == MembershipState::Invite { + if let Some(t_id) = content.third_party_invite { + auth_types.push((EventType::RoomThirdPartyInvite, t_id.signed.token)) + } + } + } + } + auth_types +} + +pub(crate) fn auth_check( + room_version: &RoomVersionId, + event: &StateEvent, + auth_events: StateMap, +) -> Option { + tracing::debug!("auth_check begingin"); + for auth_event in auth_events.values() { + if auth_event.room_id() != event.room_id() { + return Some(false); + } + } + + // TODO sig_check is false when called by `iterative_auth_check` + + // Implementation of https://matrix.org/docs/spec/rooms/v1#authorization-rules + // + // 1. If type is m.room.create: + if event.kind() == EventType::RoomCreate { + // domain of room_id must match domain of sender. + if event.room_id().map(|id| id.server_name()) != Some(event.sender().server_name()) { + return Some(false); + } + + // if content.room_version is present and is not a valid version + // TODO check this out (what event has this as content?) + if serde_json::from_value::( + event + .content() + .get("room_version") + .cloned() + .unwrap_or(serde_json::json!({})), + ) + .is_err() + { + return Some(false); + } + + tracing::debug!("m.room.create event was allowed"); + return Some(true); + } + + // 3. If event does not have m.room.create in auth_events reject. + if auth_events + .get(&(EventType::RoomCreate, "".into())) + .is_none() + { + return Some(false); + } + + // check for m.federate + if event.room_id().map(|id| id.server_name()) != Some(event.sender().server_name()) { + if !can_federate(&auth_events) { + return Some(false); + } + } + + // 4. if type is m.room.aliases + if event.kind() == EventType::RoomAliases { + // TODO && room_version "special case aliases auth" ?? + if event.state_key().is_none() { + return Some(false); // must have state_key + } + if event.state_key().unwrap().is_empty() { + return Some(false); // and be non-empty state_key (point to a user_id) + } + + if event.state_key() != Some(event.sender().to_string()) { + return Some(false); + } + + tracing::debug!("m.room.aliases event was allowed"); + return Some(true); + } + + if event.kind() == EventType::RoomMember { + if is_membership_change_allowed(event, &auth_events)? { + tracing::debug!("m.room.member event was allowed"); + return Some(true); + } + } + + if !check_event_sender_in_room(event, &auth_events)? { + return Some(false); + } + + // Special case to allow m.room.third_party_invite events where ever + // a user is allowed to issue invites + if event.kind() == EventType::RoomThirdPartyInvite { + // TODO impl this + unimplemented!("third party invite") + } + + if !can_send_event(event, &auth_events)? { + return Some(false); + } + + if event.kind() == EventType::RoomPowerLevels { + if !check_power_levels(room_version, event, &auth_events)? { + return Some(false); + } + } + + if event.kind() == EventType::RoomRedaction { + if let RedactAllowed::No = check_redaction(room_version, event, &auth_events)? { + return Some(false); + } + } + + tracing::debug!("allowing event passed all checks"); + Some(true) +} + +// synapse has an `event: &StateEvent` param but it's never used +/// Can this room federate based on its m.room.create event. +fn can_federate(auth_events: &StateMap) -> bool { + let creation_event = auth_events.get(&(EventType::RoomCreate, "".into())); + if let Some(ev) = creation_event { + if let Some(fed) = ev.content().get("m.federate") { + fed.to_string() == "true" + } else { + false + } + } else { + false + } +} + +/// Dose the user who sent this member event have required power levels to do so. +fn is_membership_change_allowed( + event: &StateEvent, + auth_events: &StateMap, +) -> Option { + let content = event + .deserialize_content::() + .ok()?; + let membership = content.membership; + + // check if this is the room creator joining + if event.prev_event_ids().len() == 1 && membership == MembershipState::Join { + if let Some(create) = auth_events.get(&(EventType::RoomCreate, "".into())) { + if let Ok(create_ev) = create.deserialize_content::() + { + if event.state_key() == Some(create_ev.creator.to_string()) { + tracing::debug!("m.room.member event allowed via m.room.create"); + return Some(true); + } + } + } + } + + let target_user_id = UserId::try_from(event.state_key()?).ok()?; + // if the server_names are different and federation is NOT allowed + if event.room_id()?.server_name() != target_user_id.server_name() { + if !can_federate(auth_events) { + return Some(false); + } + } + + // TODO according to + // https://github.com/matrix-org/synapse/blob/f2af3e4fc550e7e93be1b0f425c3e9c484b96293/synapse/events/__init__.py#L240 + // sender is the `user_id`? + let key = (EventType::RoomMember, event.sender().to_string()); + let caller = auth_events.get(&key); + + let caller_in_room = caller.is_some() && check_membership(caller, MembershipState::Join); + let caller_invited = caller.is_some() && check_membership(caller, MembershipState::Invite); + + let key = (EventType::RoomMember, target_user_id.to_string()); + let target = auth_events.get(&key); + + let target_in_room = target.is_some() && check_membership(target, MembershipState::Join); + let target_banned = target.is_some() && check_membership(target, MembershipState::Ban); + + let key = (EventType::RoomJoinRules, "".to_string()); + let join_rules_event = auth_events.get(&key); + let mut join_rule = JoinRule::Invite; + if let Some(jr) = join_rules_event { + join_rule = jr + .deserialize_content::() + .ok()? // TODO these are errors? and should be treated as a DB failure? + .join_rule; + } + + let user_level = get_user_power_level(event.sender(), auth_events); + let target_level = get_user_power_level(event.sender(), auth_events); + + // synapse has a not "what to do for default here ##" + let ban_level = get_named_level(auth_events, "ban", 50); + + // TODO clean this up + tracing::debug!( + "_is_membership_change_allowed: {}", + serde_json::json!({ + "caller_in_room": caller_in_room, + "caller_invited": caller_invited, + "target_banned": target_banned, + "target_in_room": target_in_room, + "membership": membership, + "join_rule": join_rule, + "target_user_id": target_user_id, + "event.user_id": event.sender(), + }), + ); + + if membership == MembershipState::Invite && content.third_party_invite.is_some() { + // TODO impl this + unimplemented!("third party invite") + } + + if membership != MembershipState::Join { + if caller_invited + && membership == MembershipState::Leave + && &target_user_id == event.sender() + { + return Some(true); + } + } + + if membership == MembershipState::Invite { + if target_banned { + return Some(false); + } else if target_in_room { + return Some(false); + } else { + let invite_level = get_named_level(auth_events, "invite", 0); + if user_level < invite_level { + return Some(false); + } + } + } else if membership == MembershipState::Join { + if event.sender() != &target_user_id { + return Some(false); // cannot force another user to join + } else if target_banned { + return Some(false); // cannot joined when banned + } else if join_rule == JoinRule::Public { + // pass + } else if join_rule == JoinRule::Invite { + if !caller_in_room && !caller_invited { + return Some(false); // you are not invited to this room + } + } else { + // synapse has 2 TODO's may_join list and private rooms + return Some(false); + } + } else if membership == MembershipState::Leave { + if target_banned && user_level < ban_level { + return Some(false); // you cannot unban this user + } else if &target_user_id != event.sender() { + let kick_level = get_named_level(auth_events, "kick", 50); + + if user_level < kick_level || user_level <= target_level { + return Some(false); // you do not have the power to kick user + } + } + } else if membership == MembershipState::Ban { + if user_level < ban_level || user_level <= target_level { + return Some(false); + } + } else { + // Unknown membership status + return Some(false); + } + + Some(false) +} + +/// Is the event's sender in the room that they sent the event to. +fn check_event_sender_in_room( + event: &StateEvent, + auth_events: &StateMap, +) -> Option { + let mem = auth_events.get(&(EventType::RoomMember, event.sender().to_string()))?; + // TODO this is check_membership + Some( + mem.deserialize_content::() + .ok()? + .membership + == MembershipState::Join, + ) +} + +/// Is the user allowed to send a specific event. +fn can_send_event(event: &StateEvent, auth_events: &StateMap) -> Option { + let ple = auth_events.get(&(EventType::RoomPowerLevels, "".into())); + + let send_level = get_send_level(event.kind(), event.state_key(), ple); + let user_level = get_user_power_level(event.sender(), auth_events); + + if user_level < send_level { + return Some(false); + } + + if let Some(sk) = event.state_key() { + if sk.starts_with("@") { + if sk != event.sender().to_string() { + return Some(false); // permission required to post in this room + } + } + } + Some(true) +} + +/// Confirm that the event sender has the required power levels. +fn check_power_levels( + room_version: &RoomVersionId, + power_event: &StateEvent, + auth_events: &StateMap, +) -> Option { + use itertools::Itertools; + + let key = (power_event.kind(), power_event.state_key()?); + let current_state = auth_events.get(&key)?; + + let user_content = power_event + .deserialize_content::() + .ok()?; + let current_content = current_state + .deserialize_content::() + .ok()?; + + // validation of users is done in Ruma, synapse for loops validating user_ids and integers here + + let user_level = get_user_power_level(power_event.sender(), auth_events); + + let mut user_levels_to_check = vec![]; + let old_list = ¤t_content.users; + let user_list = &user_content.users; + for user in old_list.keys().chain(user_list.keys()).dedup() { + let user: &UserId = user; + user_levels_to_check.push(user); + } + + let mut event_levels_to_check = vec![]; + let old_list = ¤t_content.events; + let new_list = &user_content.events; + for ev_id in old_list.keys().chain(new_list.keys()).dedup() { + let ev_id: &EventType = ev_id; + event_levels_to_check.push(ev_id); + } + + // TODO validate MSC2209 depending on room version check "notifications". + // synapse does this very differently with the loops (see comments below) + // but since we have a validated JSON event we can check the levels directly + // I hope... + if RoomVersion::new(room_version).limit_notifications_power_levels { + let old_level: i64 = current_content.notifications.room.into(); + let new_level: i64 = user_content.notifications.room.into(); + + 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 { + return Some(false); // cannot add ops greater than own + } + } + + 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() { + if old_level == new_level { + continue; + } + } + if user != power_event.sender() { + if old_level.map(|int| (*int).into()) == Some(user_level) { + return Some(false); // cannot remove ops level == to own + } + } + + let old_level_too_big = old_level.map(|int| (*int).into()) > Some(user_level); + let new_level_too_big = new_level.map(|int| (*int).into()) > Some(user_level); + if old_level_too_big || new_level_too_big { + return Some(false); // cannot add ops greater than own + } + } + + // EventType loop + for ev_type in event_levels_to_check { + let old_level = old_state.events.get(ev_type); + let new_level = new_state.events.get(ev_type); + if old_level.is_some() && new_level.is_some() { + if old_level == new_level { + continue; + } + } + + let old_level_too_big = old_level.map(|int| (*int).into()) > Some(user_level); + let new_level_too_big = new_level.map(|int| (*int).into()) > Some(user_level); + if old_level_too_big || new_level_too_big { + return Some(false); // cannot add ops greater than own + } + } + + Some(true) +} + +/// Does the event redacting come from a user with enough power to redact the given event. +fn check_redaction( + room_version: &RoomVersionId, + redaction_event: &StateEvent, + auth_events: &StateMap, +) -> Option { + let user_level = get_user_power_level(redaction_event.sender(), auth_events); + let redact_level = get_named_level(auth_events, "redact", 50); + + if user_level >= redact_level { + return Some(RedactAllowed::CanRedact); + } + + if room_version.is_version_1() { + if redaction_event.event_id() == redaction_event.redacts() { + return Some(RedactAllowed::OwnEvent); + } + } else { + // TODO synapse has this line also + // event.internal_metadata.recheck_redaction = True + return Some(RedactAllowed::OwnEvent); + } + Some(RedactAllowed::No) +} + +/// Check that the member event matches `state`. +/// +/// This function returns false instead of failing when deserialization fails. +fn check_membership(member_event: Option<&StateEvent>, state: MembershipState) -> bool { + if let Some(event) = member_event { + if let Ok(content) = + serde_json::from_value::(event.content()) + { + content.membership == state + } else { + false + } + } else { + false + } +} + +fn get_named_level(auth_events: &StateMap, name: &str, default: i64) -> i64 { + let power_level_event = auth_events.get(&(EventType::RoomPowerLevels, "".into())); + if let Some(pl) = power_level_event { + // TODO do this the right way and deserialize + if let Some(level) = pl.content().get(name) { + level.to_string().parse().unwrap_or(default) + } else { + 0 + } + } else { + default + } +} + +fn get_user_power_level(user_id: &UserId, auth_events: &StateMap) -> i64 { + if let Some(pl) = auth_events.get(&(EventType::RoomPowerLevels, "".into())) { + if let Ok(content) = pl.deserialize_content::() + { + if let Some(level) = content.users.get(user_id) { + (*level).into() + } else { + 0 + } + } else { + 0 // TODO if this fails DB error? + } + } else { + // if no power level event found the creator gets 100 everyone else gets 0 + let key = (EventType::RoomCreate, "".into()); + if let Some(create) = auth_events.get(&key) { + if let Ok(c) = create.deserialize_content::() { + if &c.creator == user_id { + 100 + } else { + 0 + } + } else { + 0 + } + } else { + 0 + } + } +} + +fn get_send_level( + e_type: EventType, + state_key: Option, + power_lvl: Option<&StateEvent>, +) -> i64 { + if let Some(ple) = power_lvl { + if state_key.is_some() { + if let Ok(content) = + serde_json::from_value::(ple.content()) + { + if let Some(_specific_ev) = content.events.get(&e_type) { + // this is done differently in synapse the `specific_ev` is set and if `users_default` is + // found it is used + } + content.users_default.into() + } else { + return 50; + } + } else { + return 0; + } + } else { + return 0; + } +} diff --git a/src/lib.rs b/src/lib.rs index 49e31546..13008103 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,15 +1,16 @@ -use std::{collections::BTreeMap, time::SystemTime}; +use std::{ + collections::{BTreeMap, BinaryHeap}, + time::SystemTime, +}; -use petgraph::Graph; use ruma::{ - events::{ - room::{self}, - AnyStateEvent, AnyStrippedStateEvent, AnySyncStateEvent, EventType, - }, + events::EventType, identifiers::{EventId, RoomId, RoomVersionId}, }; use serde::{Deserialize, Serialize}; +mod event_auth; +mod room_version; mod state_event; mod state_store; @@ -78,10 +79,11 @@ impl StateResolution { // add the auth_diff to conflicting now we have a full set of conflicting events auth_diff.extend(conflicting.values().cloned().flatten()); - let all_conflicted = auth_diff; + let mut all_conflicted = auth_diff; tracing::debug!("full conflicted set is {} events", all_conflicted.len()); + // gather missing events for the event_map let events = store .get_events( &all_conflicted @@ -92,6 +94,12 @@ impl StateResolution { .collect::>(), ) .unwrap(); + // update event_map to include the fetched events + event_map.extend( + events + .into_iter() + .flat_map(|ev| Some((ev.event_id()?.clone(), ev))), + ); for event in event_map.values() { if event.room_id() != Some(room_id) { @@ -106,9 +114,11 @@ impl StateResolution { )); } } - // TODO throw error if event is not for this room - // TODO make sure each conflicting event is in?? event_map `{eid for eid in full_conflicted_set if eid in event_map}` + // TODO make sure each conflicting event is in?? event_map `{eid for eid in full_conflicted_set if eid in event_map}` + all_conflicted.retain(|id| event_map.contains_key(id)); + + // get only the power events with a state_key: "" or ban/kick event (sender != state_key) let power_events = all_conflicted .iter() .filter(|id| is_power_event(id, store)) @@ -116,7 +126,7 @@ impl StateResolution { .collect::>(); // sort the power events based on power_level/clock/event_id and outgoing/incoming edges - let mut sorted_power_levels = self.revers_topological_power_sort( + let mut sorted_power_levels = self.reverse_topological_power_sort( room_id, &power_events, &mut event_map, @@ -132,13 +142,14 @@ impl StateResolution { &clean, &mut event_map, store, - ); + )?; // At this point the power_events have been resolved we now have to // sort the remaining events using the mainline of the resolved power level. sorted_power_levels.dedup(); let deduped_power_ev = sorted_power_levels; + // we have resolved the power events so remove them, I'm sure theres other reasons to do so let events_to_resolve = all_conflicted .iter() .filter(|id| deduped_power_ev.contains(id)) @@ -157,7 +168,7 @@ impl StateResolution { &resolved, &mut event_map, store, - ); + )?; // add unconflicted state to the resolved state resolved_state.extend(clean); @@ -197,23 +208,153 @@ impl StateResolution { fn get_auth_chain_diff( &mut self, state_sets: &[StateMap], - event_map: &EventMap, + _event_map: &EventMap, store: &dyn StateStore, ) -> Result, String> { + use itertools::Itertools; + tracing::debug!("calculating auth chain difference"); - panic!() + store.auth_chain_diff( + &state_sets + .iter() + .flat_map(|map| map.values()) + .dedup() + .collect::>(), + ) } - fn revers_topological_power_sort( + fn reverse_topological_power_sort( &mut self, room_id: &RoomId, power_events: &[EventId], event_map: &EventMap, store: &dyn StateStore, - conflicted_set: &[EventId], + auth_diff: &[EventId], ) -> Vec { tracing::debug!("reverse topological sort of power events"); - panic!() + + let mut graph = BTreeMap::new(); + for (idx, event_id) in power_events.iter().enumerate() { + self.add_event_and_auth_chain_to_graph(room_id, &mut graph, event_id, store, auth_diff); + + // We yield occasionally when we're working with large data sets to + // ensure that we don't block the reactor loop for too long. + if idx % _YIELD_AFTER_ITERATIONS == 0 { + // yield clock.sleep(0) + } + } + + // this is used in the `key_fn` passed to the lexico_topo_sort fn + let mut event_to_pl = BTreeMap::new(); + for (idx, event_id) in graph.keys().enumerate() { + let pl = self.get_power_level_for_sender(room_id, &event_id, event_map, store); + + event_to_pl.insert(event_id.clone(), pl); + + // We yield occasionally when we're working with large data sets to + // ensure that we don't block the reactor loop for too long. + if idx % _YIELD_AFTER_ITERATIONS == 0 { + // yield clock.sleep(0) + } + } + + self.lexicographical_topological_sort(&mut graph, |event_id| { + let ev = event_map.get(event_id).cloned().unwrap(); + let pl = event_to_pl.get(event_id).unwrap(); + + (*pl, ev.origin_server_ts().clone(), ev.event_id().cloned()) + }) + } + + fn lexicographical_topological_sort( + &mut self, + graph: &BTreeMap>, + key_fn: F, + ) -> Vec + where + F: Fn(&EventId) -> (i64, SystemTime, Option), + { + // 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 + let outdegree_map = graph; + let mut reverse_graph = BTreeMap::new(); + + // Vec of nodes that have zero out degree. + let mut zero_outdegree = vec![]; + + for (node, edges) in graph.iter() { + if edges.is_empty() { + zero_outdegree.push((key_fn(node), node)); + } + + reverse_graph.insert(node, vec![]); + for edge in edges { + reverse_graph.entry(edge).or_insert(vec![]).push(node); + } + } + + let mut heap = BinaryHeap::from(zero_outdegree); + + while let Some((_, node)) = heap.pop() { + for parent in reverse_graph.get(node).unwrap() { + let out = outdegree_map.get(parent).unwrap(); + if out.iter().filter(|id| *id == node).count() == 0 { + heap.push((key_fn(parent), parent)); + } + } + } + + heap.into_iter().map(|(_, id)| id).cloned().collect() + } + + fn get_power_level_for_sender( + &self, + room_id: &RoomId, + event_id: &EventId, + event_map: &EventMap, + store: &dyn StateStore, + ) -> i64 { + let mut pl = None; + for aid in store.auth_event_ids(room_id, event_id).unwrap() { + if let Ok(aev) = store.get_event(&aid) { + if aev.is_type_and_key(EventType::RoomPowerLevels, "") { + pl = Some(aev); + break; + } + } + } + + if pl.is_none() { + for aid in store.auth_event_ids(room_id, event_id).unwrap() { + if let Ok(aev) = store.get_event(&aid) { + if aev.is_type_and_key(EventType::RoomCreate, "") { + if let Ok(content) = aev + .deserialize_content::() + { + if &content.creator == aev.sender() { + return 100; + } + break; + } + } + } + } + return 0; + } + + if let Some(content) = pl + .map(|pl| { + pl.deserialize_content::( + ) + .ok() + }) + .flatten() + { + content.users_default.into() + } else { + 0 + } } fn iterative_auth_check( @@ -224,9 +365,41 @@ impl StateResolution { unconflicted_state: &StateMap, event_map: &EventMap, store: &dyn StateStore, - ) -> StateMap { + ) -> Result, String> { tracing::debug!("starting iter auth check"); - panic!() + let resolved_state = unconflicted_state.clone(); + for (idx, event_id) in power_events.iter().enumerate() { + let event = store.get_event(event_id).unwrap(); + + let mut auth_events = BTreeMap::new(); + for aid in store.auth_event_ids(room_id, event_id).unwrap() { + if let Ok(ev) = store.get_event(&aid) { + // TODO is None the same as "" for state_key, pretty sure it is NOT + auth_events.insert((ev.kind(), ev.state_key().unwrap_or_default()), ev); + } else { + tracing::warn!("auth event id for {} is missing {}", aid, event_id); + } + } + + for key in event_auth::auth_types_for_event(&event) { + if let Some(ev_id) = resolved_state.get(&key) { + // TODO synapse gets the event from the store then checks its not None + // then pulls the same `ev_id` event from the event_map?? + if let Ok(event) = store.get_event(ev_id) { + auth_events.insert(key.clone(), event); + } + } + } + + if !event_auth::auth_check(room_version, &event, auth_events).ok_or("".to_string())? {} + + // We yield occasionally when we're working with large data sets to + // ensure that we don't block the reactor loop for too long. + if idx % _YIELD_AFTER_ITERATIONS == 0 { + // yield clock.sleep(0) + } + } + Ok(resolved_state) } /// Returns the sorted `to_sort` list of `EventId`s based on a mainline sort using @@ -337,6 +510,33 @@ impl StateResolution { // Did not find a power level event so we default to zero 0 } + + fn add_event_and_auth_chain_to_graph( + &self, + room_id: &RoomId, + graph: &mut BTreeMap>, + event_id: &EventId, + store: &dyn StateStore, + auth_diff: &[EventId], + ) { + let mut state = vec![event_id.clone()]; + while !state.is_empty() { + // we just checked if it was empty so unwrap is fine + let eid = state.pop().unwrap(); + graph.insert(eid.clone(), vec![]); + + for aid in store.auth_event_ids(room_id, &eid).unwrap() { + if auth_diff.contains(&aid) { + if !graph.contains_key(&aid) { + state.push(aid.clone()); + } + + // we just inserted this at the start of the while loop + graph.get_mut(&eid).unwrap().push(aid); + } + } + } + } } pub fn is_power_event(event_id: &EventId, store: &dyn StateStore) -> bool { diff --git a/src/room_version.rs b/src/room_version.rs new file mode 100644 index 00000000..4c527713 --- /dev/null +++ b/src/room_version.rs @@ -0,0 +1,147 @@ +use ruma::identifiers::RoomVersionId; + +pub enum RoomDisposition { + /// A room version that has a stable specification. + Stable, + /// A room version that is not yet fully specified. + #[allow(dead_code)] + Unstable, +} + +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, +} + +pub enum StateResolutionVersion { + /// State resolution for rooms at version 1. + V1, + /// State resolution for room at version 2 or later. + V2, +} + +pub struct RoomVersion { + /// The version this room is set to. + pub version: RoomVersionId, + /// 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, + /// not sure + pub enforce_key_validity: bool, + + // bool: before MSC2261/MSC2432, m.room.aliases had special auth rules and redaction rules + pub special_case_aliases_auth: bool, + // Strictly enforce canonicaljson, do not allow: + // * Integers outside the range of [-2 ^ 53 + 1, 2 ^ 53 - 1] + // * Floats + // * NaN, Infinity, -Infinity + pub strict_canonicaljson: bool, + // bool: MSC2209: Check 'notifications' key while verifying + // m.room.power_levels auth rules. + pub limit_notifications_power_levels: bool, +} + +impl RoomVersion { + pub fn new(version: &RoomVersionId) -> Self { + if version.is_version_1() { + Self::version_1() + } else if version.is_version_2() { + Self::version_2() + } else if version.is_version_3() { + Self::version_3() + } else if version.is_version_4() { + Self::version_4() + } else if version.is_version_5() { + Self::version_5() + } else if version.is_version_6() { + Self::version_6() + } else { + panic!("this crate needs to be updated with ruma") + } + } + + fn version_1() -> Self { + Self { + version: RoomVersionId::version_1(), + 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, + } + } + + fn version_2() -> Self { + Self { + version: RoomVersionId::version_2(), + disposition: RoomDisposition::Stable, + event_format: EventFormatVersion::V1, + state_res: StateResolutionVersion::V2, + enforce_key_validity: false, + special_case_aliases_auth: true, + strict_canonicaljson: false, + limit_notifications_power_levels: false, + } + } + + fn version_3() -> Self { + Self { + version: RoomVersionId::version_3(), + disposition: RoomDisposition::Stable, + event_format: EventFormatVersion::V2, + state_res: StateResolutionVersion::V2, + enforce_key_validity: false, + special_case_aliases_auth: true, + strict_canonicaljson: false, + limit_notifications_power_levels: false, + } + } + + fn version_4() -> Self { + Self { + version: RoomVersionId::version_4(), + disposition: RoomDisposition::Stable, + event_format: EventFormatVersion::V3, + state_res: StateResolutionVersion::V2, + enforce_key_validity: false, + special_case_aliases_auth: true, + strict_canonicaljson: false, + limit_notifications_power_levels: false, + } + } + + fn version_5() -> Self { + Self { + version: RoomVersionId::version_5(), + disposition: RoomDisposition::Stable, + event_format: EventFormatVersion::V3, + state_res: StateResolutionVersion::V2, + enforce_key_validity: true, + special_case_aliases_auth: true, + strict_canonicaljson: false, + limit_notifications_power_levels: false, + } + } + + fn version_6() -> Self { + Self { + version: RoomVersionId::version_6(), + disposition: RoomDisposition::Stable, + event_format: EventFormatVersion::V3, + state_res: StateResolutionVersion::V2, + enforce_key_validity: true, + special_case_aliases_auth: false, + strict_canonicaljson: true, + limit_notifications_power_levels: true, + } + } +} diff --git a/src/state_event.rs b/src/state_event.rs index 3fe07b4b..d8df36ce 100644 --- a/src/state_event.rs +++ b/src/state_event.rs @@ -1,15 +1,15 @@ use ruma::{ events::{ from_raw_json_value, - pdu::{Pdu, PduStub, RoomV1Pdu, RoomV1PduStub, RoomV3Pdu, RoomV3PduStub}, + pdu::{Pdu, PduStub}, room::member::{MemberEventContent, MembershipState}, - AnyStateEvent, AnyStrippedStateEvent, AnySyncStateEvent, EventDeHelper, EventType, + EventDeHelper, EventType, }, - identifiers::{EventId, RoomId}, + identifiers::{EventId, RoomId, UserId}, }; use serde::{de, Serialize}; use serde_json::value::RawValue as RawJsonValue; -use std::{convert::TryFrom, time::SystemTime}; +use std::time::SystemTime; #[derive(Clone, Debug, Serialize)] #[serde(untagged)] @@ -72,6 +72,20 @@ impl StateEvent { }, } } + pub fn deserialize_content( + &self, + ) -> Result { + match self { + Self::Full(ev) => match ev { + Pdu::RoomV1Pdu(ev) => serde_json::from_value(ev.content.clone()), + Pdu::RoomV3Pdu(ev) => serde_json::from_value(ev.content.clone()), + }, + Self::Sync(ev) => match ev { + PduStub::RoomV1PduStub(ev) => serde_json::from_value(ev.content.clone()), + PduStub::RoomV3PduStub(ev) => serde_json::from_value(ev.content.clone()), + }, + } + } pub fn origin_server_ts(&self) -> &SystemTime { match self { Self::Full(ev) => match ev { @@ -88,9 +102,35 @@ impl StateEvent { match self { Self::Full(ev) => match ev { Pdu::RoomV1Pdu(ev) => Some(&ev.event_id), - Pdu::RoomV3Pdu(ev) => None, + Pdu::RoomV3Pdu(_) => None, + }, + Self::Sync(_) => None, + } + } + + pub fn sender(&self) -> &UserId { + match self { + Self::Full(ev) => match ev { + Pdu::RoomV1Pdu(ev) => &ev.sender, + Pdu::RoomV3Pdu(ev) => &ev.sender, + }, + Self::Sync(ev) => match ev { + PduStub::RoomV1PduStub(ev) => &ev.sender, + PduStub::RoomV3PduStub(ev) => &ev.sender, + }, + } + } + + pub fn redacts(&self) -> Option<&EventId> { + match self { + Self::Full(ev) => match ev { + Pdu::RoomV1Pdu(ev) => ev.redacts.as_ref(), + Pdu::RoomV3Pdu(ev) => ev.redacts.as_ref(), + }, + Self::Sync(ev) => match ev { + PduStub::RoomV1PduStub(ev) => ev.redacts.as_ref(), + PduStub::RoomV3PduStub(ev) => ev.redacts.as_ref(), }, - Self::Sync(ev) => None, } } @@ -100,12 +140,81 @@ impl StateEvent { Pdu::RoomV1Pdu(ev) => Some(&ev.room_id), Pdu::RoomV3Pdu(ev) => Some(&ev.room_id), }, - Self::Sync(ev) => None, + Self::Sync(_) => None, + } + } + pub fn kind(&self) -> EventType { + match self { + Self::Full(ev) => match ev { + Pdu::RoomV1Pdu(ev) => ev.kind.clone(), + Pdu::RoomV3Pdu(ev) => ev.kind.clone(), + }, + Self::Sync(ev) => match ev { + PduStub::RoomV1PduStub(ev) => ev.kind.clone(), + PduStub::RoomV3PduStub(ev) => ev.kind.clone(), + }, + } + } + pub fn state_key(&self) -> Option { + match self { + Self::Full(ev) => match ev { + Pdu::RoomV1Pdu(ev) => ev.state_key.clone(), + Pdu::RoomV3Pdu(ev) => ev.state_key.clone(), + }, + Self::Sync(ev) => match ev { + PduStub::RoomV1PduStub(ev) => ev.state_key.clone(), + PduStub::RoomV3PduStub(ev) => ev.state_key.clone(), + }, + } + } + + pub fn prev_event_ids(&self) -> Vec { + match self { + Self::Full(ev) => match ev { + Pdu::RoomV1Pdu(ev) => ev.prev_events.iter().map(|(id, _)| id).cloned().collect(), + Pdu::RoomV3Pdu(ev) => ev.prev_events.clone(), + }, + Self::Sync(ev) => match ev { + PduStub::RoomV1PduStub(ev) => { + ev.prev_events.iter().map(|(id, _)| id).cloned().collect() + } + PduStub::RoomV3PduStub(ev) => ev.prev_events.clone(), + }, + } + } + + pub fn content(&self) -> serde_json::Value { + match self { + Self::Full(ev) => match ev { + Pdu::RoomV1Pdu(ev) => ev.content.clone(), + Pdu::RoomV3Pdu(ev) => ev.content.clone(), + }, + Self::Sync(ev) => match ev { + PduStub::RoomV1PduStub(ev) => ev.content.clone(), + PduStub::RoomV3PduStub(ev) => ev.content.clone(), + }, } } pub fn is_type_and_key(&self, ev_type: EventType, state_key: &str) -> bool { - true + match self { + Self::Full(ev) => match ev { + Pdu::RoomV1Pdu(ev) => { + ev.kind == ev_type && ev.state_key.as_deref() == Some(state_key) + } + Pdu::RoomV3Pdu(ev) => { + ev.kind == ev_type && ev.state_key.as_deref() == Some(state_key) + } + }, + Self::Sync(ev) => match ev { + PduStub::RoomV1PduStub(ev) => { + ev.kind == ev_type && ev.state_key.as_deref() == Some(state_key) + } + PduStub::RoomV3PduStub(ev) => { + ev.kind == ev_type && ev.state_key.as_deref() == Some(state_key) + } + }, + } } } @@ -116,14 +225,10 @@ impl<'de> de::Deserialize<'de> for StateEvent { { let json = Box::::deserialize(deserializer)?; let EventDeHelper { - state_key, - event_id, - room_id, - unsigned, - .. + room_id, unsigned, .. } = from_raw_json_value(&json)?; - // Determine whether the event is a full, sync, or stripped + // Determine whether the event is a full or sync // based on the fields present. if room_id.is_some() { Ok(match unsigned { diff --git a/src/state_store.rs b/src/state_store.rs index d5c7c862..37c357e0 100644 --- a/src/state_store.rs +++ b/src/state_store.rs @@ -1,13 +1,4 @@ -use std::{collections::BTreeMap, time::SystemTime}; - -use petgraph::Graph; -use ruma::{ - events::{ - room::{self}, - AnyStateEvent, AnyStrippedStateEvent, AnySyncStateEvent, EventType, - }, - identifiers::{EventId, RoomId, RoomVersionId}, -}; +use ruma::identifiers::{EventId, RoomId, RoomVersionId}; use crate::StateEvent; @@ -21,6 +12,9 @@ pub trait StateStore { /// Returns a Vec of the related auth events to the given `event`. fn auth_event_ids(&self, room_id: &RoomId, event_id: &EventId) -> Result, String>; + /// Returns a Vec representing the difference in auth chains of the given `events`. + fn auth_chain_diff(&self, event_id: &[&EventId]) -> Result, String>; + /// Returns a tuple of requested state events from `event_id` and the auth chain events that /// relate to the. fn get_remote_state_for_room( diff --git a/tests/init.rs b/tests/init.rs index a3c4936b..f0aa8839 100644 --- a/tests/init.rs +++ b/tests/init.rs @@ -155,6 +155,10 @@ impl StateStore for TestStore { ]) } + fn auth_chain_diff(&self, event_id: &[&EventId]) -> Result, String> { + Ok(vec![]) + } + fn get_remote_state_for_room( &self, room_id: &RoomId, @@ -176,11 +180,11 @@ fn it_works() { let room_version = RoomVersionId::version_6(); let initial_state = btreemap! { - (EventType::RoomCreate, "".into()) => EventId::try_from("").unwrap(), + (EventType::RoomCreate, "".into()) => EventId::try_from("$aaa:example.org").unwrap(), }; let state_to_resolve = btreemap! { - (EventType::RoomCreate, "".into()) => EventId::try_from("").unwrap(), + (EventType::RoomCreate, "".into()) => EventId::try_from("$bbb:example.org").unwrap(), }; let mut resolver = StateResolution::default();