diff --git a/src/event_auth.rs b/src/event_auth.rs index 0f5f2cb3..a904fb4d 100644 --- a/src/event_auth.rs +++ b/src/event_auth.rs @@ -1,19 +1,23 @@ -use std::convert::TryFrom; +use std::{collections::BTreeMap, convert::TryFrom}; use maplit::btreeset; use ruma::{ events::{ - room::{self, join_rules::JoinRule, member::MembershipState}, + room::{ + self, + join_rules::JoinRule, + member::{self, MembershipState}, + power_levels::{self, PowerLevelsEventContent}, + }, EventType, }, identifiers::{RoomVersionId, UserId}, }; -use serde_json::json; use crate::{ room_version::RoomVersion, state_event::{Requester, StateEvent}, - Result, StateMap, + Error, Result, StateMap, }; /// Represents the 3 event redaction outcomes. @@ -223,7 +227,7 @@ pub fn auth_check( return Ok(false); } - if !is_membership_change_allowed(event.to_requester(), prev_event, &auth_events)? { + if !valid_membership_change(event.to_requester(), prev_event, &auth_events)? { return Ok(false); } @@ -298,182 +302,164 @@ pub fn can_federate(auth_events: &StateMap) -> bool { } /// Does the user who sent this member event have required power levels to do so. -pub fn is_membership_change_allowed( +/// +/// * `user` - Information about the membership event and user making the request. +/// * `prev_event` - The event that occurred immediately before the `user` event or None. +/// * `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. +pub fn valid_membership_change( user: Requester<'_>, prev_event: Option<&StateEvent>, auth_events: &StateMap, ) -> Result { + let state_key = if let Some(s) = user.state_key.as_ref() { + s + } else { + return Err(Error::TempString("State event requires state_key".into())); + }; + let content = serde_json::from_str::(&user.content.to_string())?; - let membership = content.membership; + let target_membership = content.membership; - // If the only previous event is an m.room.create and the state_key is the creator, allow - if user.prev_event_ids.len() == 1 && membership == MembershipState::Join { - if let Some(create) = prev_event { - if let Ok(create_ev) = create.deserialize_content::() - { - if user.state_key == Some(create_ev.creator.to_string()) - && create.prev_event_ids().is_empty() - { - tracing::debug!("m.room.member event allowed via m.room.create"); - return Ok(true); - } - } - } - } - - let target_user_id = UserId::try_from(user.state_key.as_deref().unwrap()).unwrap(); + let target_user_id = + UserId::try_from(state_key.as_str()).map_err(|e| Error::TempString(format!("{}", e)))?; let key = (EventType::RoomMember, Some(user.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 sender = auth_events.get(&key); + let sender_membership = + sender.map_or(Ok::<_, Error>(member::MembershipState::Leave), |pdu| { + Ok(pdu + .deserialize_content::()? + .membership) + })?; let key = (EventType::RoomMember, Some(target_user_id.to_string())); - let target = auth_events.get(&key); + let current = auth_events.get(&key); + let current_membership = + current.map_or(Ok::<_, Error>(member::MembershipState::Leave), |pdu| { + Ok(pdu + .deserialize_content::()? + .membership) + })?; - 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::RoomPowerLevels, Some("".into())); + let power_levels = auth_events.get(&key).map_or_else( + || { + Ok::<_, Error>(power_levels::PowerLevelsEventContent { + ban: 50.into(), + events: BTreeMap::new(), + events_default: 0.into(), + invite: 50.into(), + kick: 50.into(), + redact: 50.into(), + state_default: 0.into(), + users: BTreeMap::new(), + users_default: 0.into(), + notifications: ruma::events::room::power_levels::NotificationPowerLevels { + room: 50.into(), + }, + }) + }, + |power_levels| { + power_levels + .deserialize_content::() + .map_err(Into::into) + }, + )?; + + let sender_power = power_levels.users.get(&user.sender).map_or_else( + || { + if sender_membership != member::MembershipState::Join { + None + } else { + Some(&power_levels.users_default) + } + }, + // If it's okay, wrap with Some(_) + Some, + ); + let target_power = power_levels.users.get(&target_user_id).map_or_else( + || { + if target_membership != member::MembershipState::Join { + None + } else { + Some(&power_levels.users_default) + } + }, + // If it's okay, wrap with Some(_) + Some, + ); let key = (EventType::RoomJoinRules, Some("".to_string())); let join_rules_event = auth_events.get(&key); - - let mut join_rule = JoinRule::Invite; + let mut join_rules = JoinRule::Invite; if let Some(jr) = join_rules_event { - join_rule = jr + join_rules = jr .deserialize_content::()? .join_rule; } - let senders_level = get_user_power_level(user.sender, auth_events); - let target_level = get_user_power_level(&target_user_id, auth_events); - - // synapse has a not "what to do for default here 50" - let ban_level = get_named_level(auth_events, "ban", 50); - - // TODO clean this up - tracing::debug!( - "_is_membership_change_allowed: {}", - serde_json::to_string_pretty(&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": user.sender, - })) - .unwrap(), - ); - - // we already check if the join event was the room creator - if membership == MembershipState::Invite && content.third_party_invite.is_some() { - // TODO this is unimpled - if !verify_third_party_invite(&user, auth_events) { - tracing::warn!("not invited to this room",); - return Ok(false); + if let Some(prev) = prev_event { + if prev.kind() == EventType::RoomCreate && prev.prev_event_ids().is_empty() { + return Ok(true); } - if target_banned { - tracing::warn!("banned from this room",); - return Ok(false); - } - tracing::info!("invite succeded"); - return Ok(true); - } else if membership == MembershipState::Join { - // If the sender does not match state_key, reject. - if user.sender != &target_user_id { - tracing::warn!("cannot force another user to join"); - return Ok(false); // cannot force another user to join - } else if target_banned { - tracing::warn!("cannot join when banned"); - return Ok(false); // cannot joined when banned - } else if join_rule == JoinRule::Invite { - if !caller_in_room && !caller_invited { - tracing::warn!("user has not been invited to this room"); - return Ok(false); // you are not invited to this room - } - } else if join_rule == JoinRule::Public { - tracing::info!("join rule public") - // pass - } else { - tracing::warn!("the join rule is Private or yet to be spec'ed by Matrix"); - // synapse has 2 TODO's may_join list and private rooms - - // the join_rule is Private or Knock which means it is not yet spec'ed - return Ok(false); - } - } else if membership == MembershipState::Invite { - // if senders current membership is not join reject - if !caller_in_room { - tracing::warn!("invite sender not in room they are inviting user to"); - return Ok(false); - } - - // If the targets current membership is ban or join - if target_banned { - tracing::warn!("target has been banned"); - return Ok(false); - } else if target_in_room { - tracing::warn!("already in room"); - return Ok(false); // already in room - } else { - let invite_level = get_named_level(auth_events, "invite", 0); - // If the sender's power level is greater than or equal to the invite level, allow. - if senders_level < invite_level { - tracing::warn!("invite sender does not have power to invite"); - return Ok(false); - } - } - } else if membership == MembershipState::Leave { - if user.sender == &target_user_id && !(caller_in_room || caller_invited) {} - // if senders current membership is not join reject - if !caller_in_room { - tracing::warn!("sender not in room they are leaving"); - return Ok(false); - } - - // If the target user's current membership state is ban, and the sender's power level is less than the ban level, reject - if target_banned && senders_level < ban_level { - tracing::warn!("not enough power to unban"); - return Ok(false); // you cannot unban this user - - // 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 - } else if senders_level <= get_named_level(auth_events, "kick", 50) - || target_level < senders_level - { - tracing::warn!("not enough power to kick user"); - return Ok(false); // you do not have the power to kick user - } - } else if membership == MembershipState::Ban { - // if senders current membership is not join reject - if !caller_in_room { - tracing::warn!("ban sender not in room they are banning user from"); - return Ok(false); - } - - tracing::debug!( - "{} < {} || {} <= {}", - senders_level, - ban_level, - senders_level, - target_level - ); - - if senders_level < ban_level || senders_level <= target_level { - tracing::warn!("not enough power to ban"); - return Ok(false); - } - } else { - tracing::warn!("unknown membership status"); - // Unknown membership status - return Ok(false); } - Ok(true) + Ok(if target_membership == MembershipState::Join { + if user.sender != &target_user_id { + false + } else if let MembershipState::Ban = current_membership { + false + } else { + join_rules == JoinRule::Invite + && (current_membership == MembershipState::Join + || current_membership == MembershipState::Invite) + || join_rules == JoinRule::Public + } + } else if target_membership == MembershipState::Invite { + if let Some(_tp_id) = content.third_party_invite { + if current_membership == MembershipState::Ban { + false + } else { + // TODO this is not filled out + verify_third_party_invite(&user, auth_events) + } + } else if sender_membership != MembershipState::Join + || current_membership == MembershipState::Join + || current_membership == MembershipState::Ban + { + false + } else { + sender_power + .filter(|&p| p >= &power_levels.invite) + .is_some() + } + } else if target_membership == MembershipState::Leave { + if user.sender == &target_user_id { + current_membership == MembershipState::Join + || current_membership == MembershipState::Invite + } else if sender_membership != MembershipState::Join + || current_membership == MembershipState::Ban + && sender_power.filter(|&p| p < &power_levels.ban).is_some() + { + false + } else { + sender_power.filter(|&p| p >= &power_levels.kick).is_some() + && target_power < sender_power + } + } else if target_membership == MembershipState::Ban { + if sender_membership != MembershipState::Join { + false + } else { + sender_power.filter(|&p| p >= &power_levels.ban).is_some() + && target_power < sender_power + } + } else { + false + }) } /// Is the event's sender in the room that they sent the event to. diff --git a/tests/event_auth.rs b/tests/event_auth.rs index 7ffa58e7..6ae41778 100644 --- a/tests/event_auth.rs +++ b/tests/event_auth.rs @@ -16,7 +16,7 @@ use serde_json::{json, Value as JsonValue}; use state_res::{ event_auth::{ // auth_check, auth_types_for_event, can_federate, check_power_levels, check_redaction, - is_membership_change_allowed, + valid_membership_change, }, Requester, StateEvent, StateMap, StateStore, }; @@ -261,7 +261,7 @@ fn test_ban_pass() { sender: &alice(), }; - assert!(is_membership_change_allowed(requester, prev, &auth_events).unwrap()) + assert!(valid_membership_change(requester, prev, &auth_events).unwrap()) } #[test] @@ -285,5 +285,5 @@ fn test_ban_fail() { sender: &charlie(), }; - assert!(!is_membership_change_allowed(requester, prev, &auth_events).unwrap()) + assert!(!valid_membership_change(requester, prev, &auth_events).unwrap()) }