use std::convert::TryFrom; use maplit::btreeset; use ruma::{ events::{ room::{self, join_rules::JoinRule, member::MembershipState}, EventType, }, identifiers::{RoomVersionId, UserId}, }; use serde_json::json; use crate::{ room_version::RoomVersion, state_event::{Requester, 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, } /// For the given event `kind` what are the relevant auth events /// that are needed to authenticate this `content`. pub fn auth_types_for_event( kind: EventType, sender: &UserId, state_key: Option, content: serde_json::Value, ) -> Vec<(EventType, Option)> { if kind == EventType::RoomCreate { return vec![]; } let mut auth_types = vec![ (EventType::RoomPowerLevels, Some("".to_string())), (EventType::RoomMember, Some(sender.to_string())), (EventType::RoomCreate, Some("".to_string())), ]; if kind == EventType::RoomMember { if let Ok(content) = serde_json::from_value::(content) { if [MembershipState::Join, MembershipState::Invite].contains(&content.membership) { let key = (EventType::RoomJoinRules, Some("".into())); if !auth_types.contains(&key) { auth_types.push(key) } } // TODO what when we don't find a state_key let key = (EventType::RoomMember, state_key); if !auth_types.contains(&key) { auth_types.push(key) } if content.membership == MembershipState::Invite { if let Some(t_id) = content.third_party_invite { let key = (EventType::RoomThirdPartyInvite, Some(t_id.signed.token)); if !auth_types.contains(&key) { auth_types.push(key) } } } } } auth_types } /// Authenticate the incoming `event`. The steps of authentication are: /// * check that the event is being authenticated for the correct room /// * check that the events signatures are valid /// * then there are checks for specific event types pub fn auth_check( room_version: &RoomVersionId, event: &StateEvent, auth_events: StateMap, do_sig_check: bool, ) -> Option { tracing::info!("auth_check beginning for {}", event.event_id().as_str()); // don't let power from other rooms be used for auth_event in auth_events.values() { if auth_event.room_id() != event.room_id() { tracing::warn!("found auth event that did not match event's room_id"); return Some(false); } } if do_sig_check { let sender_domain = event.sender().server_name(); let is_invite_via_3pid = if event.kind() == EventType::RoomMember { event .deserialize_content::() .map(|c| c.membership == MembershipState::Invite && c.third_party_invite.is_some()) .unwrap_or_default() } else { false }; // check the event has been signed by the domain of the sender if event.signatures().get(sender_domain).is_none() && !is_invite_via_3pid { tracing::warn!("event not signed by sender's server"); return Some(false); } if event.room_version() == RoomVersionId::Version1 && event .signatures() .get(event.event_id().server_name().unwrap()) .is_none() { tracing::warn!("event not signed by event_id's server"); return Some(false); } } // 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 65536 respectively) // Implementation of https://matrix.org/docs/spec/rooms/v1#authorization-rules // // 1. If type is m.room.create: if event.kind() == EventType::RoomCreate { tracing::info!("start m.room.create check"); // domain of room_id must match domain of sender. if event.room_id().map(|id| id.server_name()) != Some(event.sender().server_name()) { tracing::warn!("creation events server does not match sender"); return Some(false); // creation events room id does not match senders } // if content.room_version is present and is not a valid version if serde_json::from_value::( event .content() .get("room_version") .cloned() // synapse defaults to version 1 .unwrap_or_else(|| serde_json::json!("1")), ) .is_err() { tracing::warn!("invalid room version found in m.room.create event"); return Some(false); } tracing::info!("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, Some("".into()))) .is_none() { tracing::warn!("no m.room.create event in auth chain"); return Some(false); } // check for m.federate if event.room_id().map(|id| id.server_name()) != Some(event.sender().server_name()) { tracing::info!("checking federation"); if !can_federate(&auth_events) { tracing::warn!("federation not allowed"); return Some(false); } } // 4. if type is m.room.aliases if event.kind() == EventType::RoomAliases { tracing::info!("starting m.room.aliases check"); // TODO && room_version "special case aliases auth" ?? if event.state_key().is_none() { tracing::warn!("no state_key field found for event"); return Some(false); // must have state_key } if event.state_key().unwrap().is_empty() { tracing::warn!("state_key must be non-empty"); return Some(false); // and be non-empty state_key (point to a user_id) } if event.state_key() != Some(event.sender().to_string()) { tracing::warn!("no state_key field found for event"); return Some(false); } tracing::info!("m.room.aliases event was allowed"); return Some(true); } if event.kind() == EventType::RoomMember { tracing::info!("starting m.room.member check"); if !is_membership_change_allowed(event.to_requester(), &auth_events)? { return Some(false); } tracing::info!("m.room.member event was allowed"); return Some(true); } if let Some(in_room) = check_event_sender_in_room(event, &auth_events) { if !in_room { tracing::warn!("sender not in room"); return Some(false); } } else { tracing::warn!("sender not in room"); 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)? { tracing::warn!("user cannot send event"); return Some(false); } if event.kind() == EventType::RoomPowerLevels { tracing::info!("starting m.room.power_levels check"); if let Some(required_pwr_lvl) = check_power_levels(room_version, event, &auth_events) { if !required_pwr_lvl { tracing::warn!("power level was not allowed"); return Some(false); } } else { tracing::warn!("power level was not allowed"); return Some(false); } tracing::info!("power levels event allowed"); } if event.kind() == EventType::RoomRedaction { if let RedactAllowed::No = check_redaction(room_version, event, &auth_events)? { return Some(false); } } tracing::info!("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. pub fn can_federate(auth_events: &StateMap) -> bool { let creation_event = auth_events.get(&(EventType::RoomCreate, Some("".into()))); if let Some(ev) = creation_event { if let Some(fed) = ev.content().get("m.federate") { fed == "true" } else { false } } else { false } } /// Dose the user who sent this member event have required power levels to do so. pub fn is_membership_change_allowed( user: Requester<'_>, auth_events: &StateMap, ) -> Option { let content = // TODO return error serde_json::from_str::(&user.content.to_string()).ok()?; let membership = content.membership; // check if this is the room creator joining if user.prev_event_ids.len() == 1 && membership == MembershipState::Join { if let Some(create) = auth_events.get(&(EventType::RoomCreate, Some("".into()))) { if let Ok(create_ev) = create.deserialize_content::() { if user.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(user.state_key.as_deref().unwrap()) .ok() .unwrap(); // if the server_names are different and federation is NOT allowed if user.room_id.server_name() != target_user_id.server_name() && !can_federate(auth_events) { tracing::warn!("server cannot federate"); return Some(false); } 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 key = (EventType::RoomMember, Some(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, Some("".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(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(), ); 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 Some(false); } if target_banned { tracing::warn!("banned from this room",); return Some(false); } tracing::info!("invite succeded"); return Some(true); } if membership != MembershipState::Join { if caller_invited && membership == MembershipState::Leave && &target_user_id == user.sender { tracing::warn!("join event succeded"); return Some(true); } if !caller_in_room { tracing::warn!("user is not in this room {}", user.room_id.as_str(),); return Some(false); // caller is not joined } } if membership == MembershipState::Invite { if target_banned { tracing::warn!("target has been banned"); return Some(false); } else if target_in_room { tracing::warn!("already in room"); return Some(false); // already in room } 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 user.sender != &target_user_id { tracing::warn!("cannot force another user to join"); return Some(false); // cannot force another user to join } else if target_banned { tracing::warn!("cannot join when banned"); return Some(false); // cannot joined when banned } else if join_rule == JoinRule::Public { tracing::info!("join rule public") // pass } else if join_rule == JoinRule::Invite { if !caller_in_room && !caller_invited { tracing::warn!("user has not been invited to this room"); return Some(false); // you are not invited to this room } } 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 Some(false); } } else if membership == MembershipState::Leave { if target_banned && user_level < ban_level { tracing::warn!("not enough power to unban"); return Some(false); // you cannot unban this user } else if &target_user_id != user.sender { let kick_level = get_named_level(auth_events, "kick", 50); if user_level < kick_level || user_level <= target_level { tracing::warn!("not enough power to kick user"); return Some(false); // you do not have the power to kick user } } } else if membership == MembershipState::Ban { tracing::debug!( "{} < {} || {} <= {}", user_level, ban_level, user_level, target_level ); if user_level < ban_level || user_level <= target_level { tracing::warn!("not enough power to ban"); return Some(false); } } else { tracing::warn!("unknown membership status"); // Unknown membership status return Some(false); } Some(true) } /// Is the event's sender in the room that they sent the event to. /// /// A return value of None is not a failure pub fn check_event_sender_in_room( event: &StateEvent, auth_events: &StateMap, ) -> Option { let mem = auth_events.get(&(EventType::RoomMember, Some(event.sender().to_string())))?; // TODO this is check_membership a helper fn in synapse but it does this Some( mem.deserialize_content::() .ok()? .membership == MembershipState::Join, ) } /// Is the user allowed to send a specific event based on the rooms power levels. pub fn can_send_event(event: &StateEvent, auth_events: &StateMap) -> Option { let ple = auth_events.get(&(EventType::RoomPowerLevels, Some("".into()))); let send_level = get_send_level(event.kind(), event.state_key(), ple); let user_level = get_user_power_level(event.sender(), auth_events); tracing::debug!( "{} snd {} usr {}", event.event_id().to_string(), send_level, user_level ); if user_level < send_level { return Some(false); } if let Some(sk) = event.state_key() { if sk.starts_with('@') && sk != event.sender().as_str() { return Some(false); // permission required to post in this room } } Some(true) } /// Confirm that the event sender has the required power levels. pub 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 = if let Some(current_state) = auth_events.get(&key) { current_state } else { // TODO synapse returns here, shouldn't this be an error ?? return Some(true); }; let user_content = power_event .deserialize_content::() .unwrap(); let current_content = current_state .deserialize_content::() .unwrap(); // validation of users is done in Ruma, synapse for loops validating user_ids and integers here tracing::info!("validation of power event finished"); let user_level = get_user_power_level(power_event.sender(), auth_events); let mut user_levels_to_check = btreeset![]; 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.insert(user); } tracing::debug!("users to check {:?}", user_levels_to_check); let mut event_levels_to_check = btreeset![]; 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.insert(ev_id); } tracing::debug!("events to check {:?}", event_levels_to_check); // 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 { tracing::warn!("m.room.power_level cannot add ops > than own"); 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() && old_level == new_level { continue; } if user != power_event.sender() && old_level.map(|int| (*int).into()) == Some(user_level) { tracing::warn!("m.room.power_level cannot remove ops == to own"); 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 { tracing::warn!("m.room.power_level failed to add ops > than own"); return Some(false); // cannot add ops greater than own } } // EventType loop for ev_type in event_levels_to_check { let old_level = old_state.events.get(ev_type); let new_level = new_state.events.get(ev_type); if old_level.is_some() && new_level.is_some() && old_level == new_level { continue; } 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 { tracing::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 { tracing::warn!("cannot add ops > than own"); return Some(false); } } } Some(true) } fn get_deserialize_levels( old: &serde_json::Value, new: &serde_json::Value, name: &str, ) -> Option<(i64, i64)> { Some(( serde_json::from_value(old.get(name)?.clone()).ok()?, serde_json::from_value(new.get(name)?.clone()).ok()?, )) } /// Does the event redacting come from a user with enough power to redact the given event. pub 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 let RoomVersionId::Version1 = room_version { // are the redacter and redactee in the same domain if Some(redaction_event.event_id().server_name()) == redaction_event.redacts().map(|id| id.server_name()) { 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. pub 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().clone()) { content.membership == state } else { false } } else { false } } /// Helper function to fetch a field, `name`, from a "m.room.power_level" event's content. /// or return `default` if no power level event is found or zero if no field matches `name`. pub fn get_named_level(auth_events: &StateMap, name: &str, default: i64) -> i64 { let power_level_event = auth_events.get(&(EventType::RoomPowerLevels, Some("".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 } } /// Helper function to fetch a users default power level from a "m.room.power_level" event's `users` /// object. pub fn get_user_power_level(user_id: &UserId, auth_events: &StateMap) -> i64 { if let Some(pl) = auth_events.get(&(EventType::RoomPowerLevels, Some("".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, Some("".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 } } } /// 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. pub fn get_send_level( e_type: EventType, state_key: Option, power_lvl: Option<&StateEvent>, ) -> i64 { tracing::debug!("{:?} {:?}", e_type, state_key); if let Some(ple) = power_lvl { if let Ok(content) = serde_json::from_value::( ple.content().clone(), ) { let mut lvl: i64 = content .events .get(&e_type) .cloned() .unwrap_or_else(|| js_int::int!(50)) .into(); let state_def: i64 = content.state_default.into(); let event_def: i64 = content.events_default.into(); if (state_key.is_some() && state_def > lvl) || event_def > lvl { lvl = event_def; } lvl } else { 50 } } else { 0 } } /// TODO this is unimplemented pub fn verify_third_party_invite( _event: &Requester<'_>, _auth_events: &StateMap, ) -> bool { unimplemented!("impl third party invites") }