diff --git a/crates/ruma-state-res/src/event_auth.rs b/crates/ruma-state-res/src/event_auth.rs index 7dc40e15..11bbe17b 100644 --- a/crates/ruma-state-res/src/event_auth.rs +++ b/crates/ruma-state-res/src/event_auth.rs @@ -19,6 +19,24 @@ use tracing::{debug, error, info, warn}; use crate::{room_version::RoomVersion, Error, Event, PowerLevelsContentFields, Result}; +// FIXME: field extracting could be bundled for `content` +#[derive(Deserialize)] +struct GetMembership { + membership: MembershipState, +} + +#[derive(Deserialize)] +struct RoomMemberContentFields { + membership: Option>, + #[cfg(feature = "unstable-pre-spec")] + join_authorised_via_users_server: Option>>, +} + +#[derive(Deserialize)] +struct PowerLevelsContentInvite { + invite: Int, +} + /// For the given event `kind` what are the relevant auth events that are needed to authenticate /// this `content`. /// @@ -95,11 +113,6 @@ pub fn auth_check( current_third_party_invite: Option, fetch_state: impl Fn(&EventType, &str) -> Option, ) -> Result { - #[derive(Deserialize)] - struct RoomMemberContentFields { - membership: Option>, - } - info!( "auth_check beginning for {} ({})", incoming_event.event_id(), @@ -221,7 +234,7 @@ pub fn auth_check( }; let content: RoomMemberContentFields = from_json_str(incoming_event.content().get())?; - if content.membership.and_then(|m| m.deserialize().ok()).is_none() { + 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); } @@ -229,6 +242,17 @@ pub fn auth_check( let target_user = <&UserId>::try_from(state_key).map_err(|e| Error::InvalidPdu(format!("{}", e)))?; + #[cfg(feature = "unstable-pre-spec")] + let join_authed_user = + content.join_authorised_via_users_server.as_ref().and_then(|u| u.deserialize().ok()); + #[cfg(feature = "unstable-pre-spec")] + let join_authed_user_membership = if let Some(auth_user) = &join_authed_user { + fetch_state(&EventType::RoomMember, auth_user.as_str()) + .and_then(|mem| from_json_str::(mem.content().get()).ok()) + .map(|mem| mem.membership) + } else { + None + }; if !valid_membership_change( room_version, target_user, @@ -240,6 +264,10 @@ pub fn auth_check( current_third_party_invite, power_levels_event.as_ref(), fetch_state(&EventType::RoomJoinRules, "").as_ref(), + #[cfg(feature = "unstable-pre-spec")] + join_authed_user.as_deref(), + #[cfg(feature = "unstable-pre-spec")] + join_authed_user_membership, )? { return Ok(false); } @@ -290,11 +318,6 @@ pub fn auth_check( // Allow if and only if sender's current power level is greater than // or equal to the invite level if *incoming_event.event_type() == EventType::RoomThirdPartyInvite { - #[derive(Deserialize)] - struct PowerLevelsContentInvite { - invite: Int, - } - let invite_level = match &power_levels_event { Some(power_levels) => { from_json_str::(power_levels.content().get())?.invite @@ -376,7 +399,7 @@ pub fn auth_check( /// State. #[allow(clippy::too_many_arguments)] fn valid_membership_change( - _room_version: &RoomVersion, + room_version: &RoomVersion, target_user: &UserId, target_user_membership_event: Option, sender: &UserId, @@ -386,13 +409,9 @@ fn valid_membership_change( current_third_party_invite: Option, power_levels_event: Option, join_rules_event: Option, + #[cfg(feature = "unstable-pre-spec")] authed_user_id: Option<&UserId>, + #[cfg(feature = "unstable-pre-spec")] auth_user_membership: Option, ) -> Result { - // FIXME: field extracting could be bundled for `content` - #[derive(Deserialize)] - struct GetMembership { - membership: MembershipState, - } - #[derive(Deserialize)] struct GetThirdPartyInvite { third_party_invite: Option>, @@ -443,6 +462,54 @@ fn valid_membership_change( let target_user_membership_event_id = target_user_membership_event.as_ref().map(|e| e.event_id()); + #[cfg(not(feature = "unstable-pre-spec"))] + let restricted = false; + + #[cfg(not(feature = "unstable-pre-spec"))] + let allow_based_on_membership = false; + + #[cfg(not(feature = "unstable-pre-spec"))] + let restricted_join_rules_auth = false; + // FIXME: `JoinRule::Restricted(_)` can contain conditions that allow a user to join if + // they are met. So far the spec talks about roomId based auth inheritance, the problem with + // this is that ruma-state-res can only request events from one room at a time :( + #[cfg(feature = "unstable-pre-spec")] + let restricted = matches!(join_rules, JoinRule::Restricted(_)); + + #[cfg(feature = "unstable-pre-spec")] + let allow_based_on_membership = + matches!(target_user_current_membership, MembershipState::Invite | MembershipState::Join) + || authed_user_id.is_none(); + + #[cfg(feature = "unstable-pre-spec")] + let restricted_join_rules_auth = if let Some(authed_user_id) = authed_user_id { + // Is the authorised user allowed to invite users into this rooom + let (auth_user_pl, invite_level) = if let Some(pl) = &power_levels_event { + let invite = match from_json_str::(pl.content().get()) { + Ok(power_levels) => power_levels.invite, + _ => int!(50), + }; + + if let Ok(content) = from_json_str::(pl.content().get()) { + let user_pl = if let Some(level) = content.users.get(authed_user_id) { + *level + } else { + content.users_default + }; + + (user_pl, invite) + } else { + (int!(0), invite) + } + } else { + (int!(0), int!(0)) + }; + (auth_user_membership == Some(MembershipState::Join)) && (auth_user_pl >= invite_level) + } else { + // If the `join_authorised_via_users_server` was empty we treat the target user as invited + true + }; + Ok(match target_membership { MembershipState::Join => { if sender != target_user { @@ -455,7 +522,15 @@ fn valid_membership_change( let allow = join_rules == JoinRule::Invite && (target_user_current_membership == MembershipState::Join || target_user_current_membership == MembershipState::Invite) - || join_rules == JoinRule::Public; + || join_rules == JoinRule::Public + || room_version.restricted_join_rules + // 0. room version of 8 ^ and join rule of restricted + && restricted + // 1. The user's previous membership was invite or join + && allow_based_on_membership + // 2. The join event has a valid signature from a homeserver whose + // users have the power to issue invites or the field was `None`. + && restricted_join_rules_auth; if !allow { warn!( @@ -557,17 +632,21 @@ fn valid_membership_change( } } #[cfg(feature = "unstable-pre-spec")] - MembershipState::Knock if _room_version.allow_knocking => { + MembershipState::Knock if room_version.allow_knocking => { // 1. If the `join_rule` is anything other than `knock`, reject. if join_rules != JoinRule::Knock { - warn!("Join rule is not set to knock"); + warn!("Join rule is not set to knock, knocking is not allowed"); false } else { // 2. If `sender` does not match `state_key`, reject. // 3. If the `sender`'s current membership is not `ban`, `invite`, or `join`, allow. // 4. Otherwise, reject. if sender != target_user { - warn!("Can't make other user join"); + warn!( + ?sender, + ?target_user, + "Can't make another user join, sender did not match target" + ); false } else if matches!( sender_membership, @@ -866,8 +945,21 @@ mod tests { }, Event, RoomVersion, StateMap, }; + #[cfg(feature = "unstable-pre-spec")] + use { + crate::test_utils::{bob, ella, event_id, room_id}, + ruma_events::room::{ + join_rules::{ + AllowRule, JoinRule, Restricted, RoomJoinRulesEventContent, RoomMembership, + }, + member::{MembershipState, RoomMemberEventContent}, + }, + serde_json::value::to_raw_value as to_raw_json_value, + }; + use ruma_events::EventType; + // #[cfg(not(feature = "unstable-pre-spec"))] #[test] fn test_ban_pass() { let _ = @@ -909,10 +1001,15 @@ mod tests { None::, fetch_state(EventType::RoomPowerLevels, "".to_owned()), fetch_state(EventType::RoomJoinRules, "".to_owned()), + #[cfg(feature = "unstable-pre-spec")] + None, + #[cfg(feature = "unstable-pre-spec")] + None ) .unwrap()); } + // #[cfg(not(feature = "unstable-pre-spec"))] #[test] fn test_ban_fail() { let _ = @@ -954,6 +1051,160 @@ mod tests { None::, fetch_state(EventType::RoomPowerLevels, "".to_owned()), fetch_state(EventType::RoomJoinRules, "".to_owned()), + #[cfg(feature = "unstable-pre-spec")] + None, + #[cfg(feature = "unstable-pre-spec")] + None + ) + .unwrap()); + } + + #[cfg(feature = "unstable-pre-spec")] + #[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(), + EventType::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::Invite); + member.join_authorized_via_users_server = Some(alice()); + events.insert( + event_id("new"), + to_pdu_event( + "new", + bob(), + EventType::RoomMember, + Some(ella().as_str()), + to_raw_json_value(&member).unwrap(), + &["CREATE", "IJR", "IPOWER"], + &["IMC"], + ), + ); + + let prev_event = + events.values().find(|ev| ev.event_id.as_str().contains("IMC")).map(Arc::clone); + + let auth_events = events + .values() + .map(|ev| { + ((ev.event_type().to_owned(), ev.state_key().unwrap().to_owned()), Arc::clone(ev)) + }) + .collect::>(); + + let requester = to_pdu_event( + "HELLO", + ella(), + EventType::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(EventType::RoomMember, target_user.to_string()), + &sender, + fetch_state(EventType::RoomMember, sender.to_string()), + requester.content(), + prev_event.clone(), + None::, + fetch_state(EventType::RoomPowerLevels, "".to_owned()), + fetch_state(EventType::RoomJoinRules, "".to_owned()), + Some(&alice()), + Some(MembershipState::Join), + ) + .unwrap()); + + assert!(!valid_membership_change( + &RoomVersion::V9, + &target_user, + fetch_state(EventType::RoomMember, target_user.to_string()), + &sender, + fetch_state(EventType::RoomMember, sender.to_string()), + requester.content(), + prev_event, + None::, + fetch_state(EventType::RoomPowerLevels, "".to_owned()), + fetch_state(EventType::RoomJoinRules, "".to_owned()), + Some(&ella()), + Some(MembershipState::Join), + ) + .unwrap()); + } + + #[cfg(feature = "unstable-pre-spec")] + #[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(), + EventType::RoomJoinRules, + Some(""), + to_raw_json_value(&RoomJoinRulesEventContent::new(JoinRule::Knock)).unwrap(), + &["CREATE", "IMA", "IPOWER"], + &["IPOWER"], + ); + + let prev_event = + events.values().find(|ev| ev.event_id.as_str().contains("IMC")).map(Arc::clone); + + let auth_events = events + .values() + .map(|ev| { + ((ev.event_type().to_owned(), ev.state_key().unwrap().to_owned()), Arc::clone(ev)) + }) + .collect::>(); + + let requester = to_pdu_event( + "HELLO", + ella(), + EventType::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(EventType::RoomMember, target_user.to_string()), + &sender, + fetch_state(EventType::RoomMember, sender.to_string()), + requester.content(), + prev_event, + None::, + fetch_state(EventType::RoomPowerLevels, "".to_owned()), + fetch_state(EventType::RoomJoinRules, "".to_owned()), + None, + None, ) .unwrap()); }