diff --git a/src/core/matrix/state_res/error.rs b/src/core/matrix/state_res/error.rs index c048516a..f54e6b71 100644 --- a/src/core/matrix/state_res/error.rs +++ b/src/core/matrix/state_res/error.rs @@ -1,3 +1,4 @@ +use ruma::OwnedEventId; use serde_json::Error as JsonError; use thiserror::Error; @@ -17,10 +18,19 @@ pub enum Error { #[error("Event not found: {0}")] NotFound(String), + /// A required event this event depended on could not be fetched, + /// either as it was missing, or because it was invalid + #[error("Failed to fetch required {0} event: {1}")] + DependencyFailed(OwnedEventId, String), + /// Invalid fields in the given PDU. #[error("Invalid PDU: {0}")] InvalidPdu(String), + /// This event failed an authorization condition. + #[error("Auth check failed: {0}")] + AuthConditionFailed(String), + /// This event contained multiple auth events of the same type and state /// key. #[error("Duplicate auth events: {0}")] diff --git a/src/core/matrix/state_res/event_auth/context.rs b/src/core/matrix/state_res/event_auth/context.rs new file mode 100644 index 00000000..f4cf0731 --- /dev/null +++ b/src/core/matrix/state_res/event_auth/context.rs @@ -0,0 +1,108 @@ +//! Context for event authorisation checks + +use ruma::{ + Int, OwnedUserId, UserId, + events::{ + StateEventType, + room::{create::RoomCreateEventContent, power_levels::RoomPowerLevelsEventContent}, + }, +}; + +use crate::{Event, EventTypeExt, Pdu, RoomVersion, matrix::StateKey, state_res::Error}; + +pub enum UserPower { + /// Creator indicates this user should be granted a power level above all. + Creator, + /// Standard indicates power levels should be used to determine rank. + Standard, +} + +impl PartialEq for UserPower { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + | (UserPower::Creator, UserPower::Creator) => true, + | (UserPower::Standard, UserPower::Standard) => true, + | _ => false, + } + } +} + +/// Get the creators of the room. +/// If this room only supports one creator, a vec of one will be returned. +/// If multiple creators are supported, all will be returned, with the +/// m.room.create sender first. +pub async fn calculate_creators( + room_version: &RoomVersion, + fetch_state: FS, +) -> Result, Error> +where + FS: AsyncFn((StateEventType, StateKey)) -> Result, Error>, +{ + let create_event = fetch_state(StateEventType::RoomCreate.with_state_key("")) + .await? + .ok_or_else(|| Error::InvalidPdu("Room create event not found".to_owned()))?; + let content = create_event + .get_content::() + .map_err(|e| { + Error::InvalidPdu(format!("Room create event has invalid content: {}", e)) + })?; + + if room_version.explicitly_privilege_room_creators { + let mut creators = vec![create_event.sender().to_owned()]; + if let Some(additional) = content.additional_creators { + for user_id in additional { + if !creators.contains(&user_id) { + creators.push(user_id); + } + } + } + Ok(creators) + } else if room_version.use_room_create_sender { + Ok(vec![create_event.sender().to_owned()]) + } else { + // Have to check the event content + if let Some(creator) = content.creator { + Ok(vec![creator]) + } else { + Err(Error::InvalidPdu("Room create event missing creator field".to_owned())) + } + } +} + +/// Rank fetches the creatorship and power level of the target user +pub async fn get_rank( + room_version: &RoomVersion, + fetch_state: FS, + user_id: &UserId, +) -> Result<(UserPower, Int, Option), Error> +where + FS: AsyncFn((StateEventType, StateKey)) -> Result, Error>, +{ + let creators = calculate_creators(room_version, &fetch_state).await?; + if creators.contains(&user_id.to_owned()) && room_version.explicitly_privilege_room_creators { + return Ok((UserPower::Creator, Int::MAX, None)); + } + + let power_levels = fetch_state(StateEventType::RoomPowerLevels.with_state_key("")).await?; + if let Some(power_levels) = power_levels { + let power_levels = power_levels + .get_content::() + .map_err(|e| { + Error::InvalidPdu(format!("m.room.power_levels event has invalid content: {}", e)) + })?; + Ok(( + UserPower::Standard, + *power_levels + .users + .get(user_id) + .unwrap_or(&power_levels.users_default), + Some(power_levels), + )) + } else { + // No power levels event, use defaults + if creators[0] == user_id { + return Ok((UserPower::Creator, Int::MAX, None)); + } + Ok((UserPower::Standard, Int::from(0), None)) + } +} diff --git a/src/core/matrix/state_res/event_auth/member_event.rs b/src/core/matrix/state_res/event_auth/member_event.rs index c5650218..de56fe4a 100644 --- a/src/core/matrix/state_res/event_auth/member_event.rs +++ b/src/core/matrix/state_res/event_auth/member_event.rs @@ -1,3 +1,219 @@ //! Auth checks relevant to the `m.room.member` event specifically. //! //! See: https://spec.matrix.org/v1.16/rooms/v12/#authorization-rules + +use ruma::{ + EventId, OwnedUserId, RoomId, UserId, + events::{ + StateEventType, + room::join_rules::{JoinRule, RoomJoinRulesEventContent}, + }, +}; + +use crate::{ + Event, EventTypeExt, Pdu, RoomVersion, + matrix::StateKey, + state_res::{ + Error, + event_auth::context::{UserPower, get_rank}, + }, +}; + +#[derive(serde::Deserialize)] +struct PartialMembershipObject { + membership: Option, + join_authorized_via_users_server: Option, + third_party_invite: Option, +} + +async fn check_join_event( + room_version: RoomVersion, + event: &Pdu, + membership: &PartialMembershipObject, + target: &UserId, + fetch_event: FE, + fetch_state: FS, +) -> Result<(), Error> +where + FE: AsyncFn(&EventId) -> Result, Error>, + FS: AsyncFn((StateEventType, StateKey)) -> Result, Error>, +{ + // 3.1: If the only previous event is an m.room.create and the state_key is the + // sender of the m.room.create, allow. + if event.prev_events.len() == 1 { + let only_prev = fetch_event(&event.prev_events[0]).await?; + if let Some(prev_event) = only_prev { + let k = prev_event.event_type().with_state_key(""); + if k.0 == StateEventType::RoomCreate && k.1.as_str() == event.sender().as_str() { + return Ok(()); + } + } else { + return Err(Error::DependencyFailed( + event.prev_events[0].to_owned(), + "Previous event not found when checking join event".to_owned(), + )); + } + } + + // 3.2: If the sender does not match state_key, reject. + if event.sender() != target { + return Err(Error::AuthConditionFailed( + "m.room.member join event sender does not match state_key".to_owned(), + )); + } + + let prev_membership = if let Some(ev) = + fetch_state(StateEventType::RoomMember.with_state_key(target.as_str())).await? + { + Some(ev.get_content::().map_err(|e| { + Error::InvalidPdu(format!("Previous m.room.member event has invalid content: {}", e)) + })?) + } else { + None + }; + let join_rule_content = + if let Some(jr) = fetch_state(StateEventType::RoomJoinRules.with_state_key("")).await? { + jr.get_content::().map_err(|e| { + Error::InvalidPdu(format!("m.room.join_rules event has invalid content: {}", e)) + })? + } else { + // Default to invite if no join rules event is present. + RoomJoinRulesEventContent { join_rule: JoinRule::Private } + }; + + // 3.3: If the sender is banned, reject. + let prev_member = if let Some(prev_content) = &prev_membership { + if let Some(membership) = &prev_content.membership { + if membership == "ban" { + return Err(Error::AuthConditionFailed( + "m.room.member join event sender is banned".to_owned(), + )); + } + membership + } else { + "leave" + } + } else { + "leave" + }; + + // 3.4: If the join_rule is invite or knock then allow if membership + // state is invite or join. + // 3.5: If the join_rule is restricted or knock_restricted: + // 3.5.1: If membership state is join or invite, allow. + match join_rule_content.join_rule { + | JoinRule::Invite | JoinRule::Knock => { + if prev_member == "invite" || prev_member == "join" { + return Ok(()); + } + Err(Error::AuthConditionFailed( + "m.room.member join event not invited under invite/knock join rule".to_owned(), + )) + }, + | JoinRule::Restricted(_) | JoinRule::KnockRestricted(_) => { + // 3.5.2: If the join_authorised_via_users_server key in content is not a user + // with sufficient permission to invite other users or is not a joined + // member of the room, reject. + if prev_member == "invite" || prev_member == "join" { + return Ok(()); + } + let join_authed_by = membership.join_authorized_via_users_server.as_ref(); + if let Some(user_id) = join_authed_by { + let rank = get_rank(&room_version, &fetch_state, user_id).await?; + if rank.0 == UserPower::Standard { + // This user is not a creator, check that they have + // sufficient power level + if rank.1 < rank.2.unwrap().invite { + return Err(Error::InvalidPdu( + "m.room.member join event join_authorised_via_users_server does not \ + have sufficient power level to invite" + .to_owned(), + )); + } + } + // Check that the user is a joined member of the room + if let Some(state_event) = + fetch_state(StateEventType::RoomMember.with_state_key(user_id.as_str())) + .await? + { + let state_content = state_event + .get_content::() + .map_err(|e| { + Error::InvalidPdu(format!( + "m.room.member event has invalid content: {}", + e + )) + })?; + if let Some(state_membership) = &state_content.membership { + if state_membership == "join" { + return Ok(()); + } + } + } + } else { + return Err(Error::AuthConditionFailed( + "m.room.member join event missing join_authorised_via_users_server" + .to_owned(), + )); + } + + // 3.5.3: Otherwise, allow + return Ok(()); + }, + | JoinRule::Public => return Ok(()), + | _ => Err(Error::AuthConditionFailed(format!( + "unknown join rule: {:?}", + join_rule_content.join_rule + )))?, + } +} + +pub async fn check_member_event( + room_version: RoomVersion, + room_id: &RoomId, + event: &Pdu, + fetch_event: FE, + fetch_state: FS, +) -> Result<(), Error> +where + FE: AsyncFn(&EventId) -> Result, Error>, + FS: AsyncFn((StateEventType, StateKey)) -> Result, Error>, +{ + // 1. If there is no state_key property, or no membership property in content, + // reject. + if event.state_key.is_none() { + return Err(Error::InvalidPdu("m.room.member event missing state_key".to_owned())); + } + + let target = UserId::parse(event.state_key().unwrap()) + .map_err(|_| Error::InvalidPdu("m.room.member event has invalid state_key".to_owned()))? + .to_owned(); + let content = event + .get_content::() + .map_err(|e| { + Error::InvalidPdu(format!("m.room.member event has invalid content: {}", e)) + })?; + + if content.membership.is_none() { + return Err(Error::InvalidPdu( + "m.room.member event missing membership in content".to_owned(), + )); + } + let membership = content.membership.as_ref().unwrap(); + + // 2: If content has a join_authorised_via_users_server key + // + // 2.1: If the event is not validly signed by the homeserver of the user ID + // denoted by the key, reject. + if let Some(_join_auth) = &content.join_authorized_via_users_server { + // We need to check the signature here, but don't have the means to do so yet. + todo!("Implement join_authorised_via_users_server check"); + } + + // 3: If membership is join: + if membership == "join" { + check_join_event(room_version, event, &content, &target, fetch_event, fetch_state) + .await?; + } + Ok(()) +} diff --git a/src/core/matrix/state_res/event_auth/mod.rs b/src/core/matrix/state_res/event_auth/mod.rs index 953aa9c1..daf224d0 100644 --- a/src/core/matrix/state_res/event_auth/mod.rs +++ b/src/core/matrix/state_res/event_auth/mod.rs @@ -1,3 +1,5 @@ pub mod auth_events; +mod context; pub mod create_event; pub mod iterative_auth_checks; +pub mod member_event;