diff --git a/src/api/client/room/create.rs b/src/api/client/room/create.rs index 5a9d87cc9..6d7fd269b 100644 --- a/src/api/client/room/create.rs +++ b/src/api/client/room/create.rs @@ -2,7 +2,7 @@ use std::collections::{BTreeMap, BTreeSet}; use axum::extract::State; use conduwuit::{ - Err, Result, debug, debug_info, err, info, + Err, Error, Result, debug, debug_info, err, info, matrix::{StateKey, pdu::PartialPdu}, trace, warn, }; @@ -11,10 +11,13 @@ use futures::FutureExt; use ruma::{ CanonicalJsonObject, CanonicalJsonValue, Int, MilliSecondsSinceUnixEpoch, OwnedRoomAliasId, OwnedUserId, RoomAliasId, RoomId, RoomVersionId, UserId, - api::client::room::{self, create_room}, + api::{ + client::room::{self, create_room}, + error::ErrorKind::Forbidden, + }, assign, events::{ - TimelineEventType, + AnyStateEventContent, StateEventType, TimelineEventType, room::{ canonical_alias::RoomCanonicalAliasEventContent, create::RoomCreateEventContent, @@ -512,30 +515,42 @@ pub(crate) async fn create_room_route( // 7. Events listed in initial_state for event in &body.initial_state { - let mut partial_pdu = event - .deserialize_as_unchecked::() - .map_err(|e| { - err!(Request(InvalidParam(warn!("Invalid initial state event: {e:?}")))) + let event_type: StateEventType = event.get_field("type")?.ok_or_else(|| { + err!(Request(InvalidParam(warn!( + r#"Invalid initial state event: missing "type" field "# + )))) + })?; + + let content: Raw = + event.get_field("content")?.ok_or_else(|| { + err!(Request(InvalidParam(warn!( + r#"Invalid initial state event: missing "content" field "# + )))) })?; - debug_info!("Room creation initial state event: {event:?}"); + let state_key: String = event.get_field("state_key")?.unwrap_or_default(); - // Implicit state key defaults to "" - partial_pdu.state_key.get_or_insert_with(StateKey::new); - - // Silently skip encryption events if they are not allowed - if partial_pdu.event_type == TimelineEventType::RoomEncryption - && !services.config.allow_encryption - { - continue; - } - - services + match services .rooms .timeline - .build_and_append_pdu(partial_pdu, sender_user, Some(&room_id), &state_lock) + .send_state_event_for_key( + sender_user, + &room_id, + &state_lock, + &event_type, + &content, + &state_key, + None, + ) .boxed() - .await?; + .await + { + | Err(Error::Request(Forbidden, ..)) => { + // Silently skip forbidden events + continue; + }, + | r => r, + }?; } // 8. Events implied by name and topic diff --git a/src/api/client/state.rs b/src/api/client/state.rs index 7aa1d935a..d2ab10e0e 100644 --- a/src/api/client/state.rs +++ b/src/api/client/state.rs @@ -8,22 +8,11 @@ use conduwuit::{ use conduwuit_service::Services; use futures::{FutureExt, TryStreamExt}; use ruma::{ - MilliSecondsSinceUnixEpoch, OwnedEventId, RoomId, UserId, + RoomId, api::client::state::{ get_state_event_for_key::{self, v3::StateEventFormat}, get_state_events, send_state_event, }, - events::{ - AnyStateEventContent, StateEventType, - room::{ - canonical_alias::RoomCanonicalAliasEventContent, - history_visibility::{HistoryVisibility, RoomHistoryVisibilityEventContent}, - join_rules::{JoinRule, RoomJoinRulesEventContent}, - member::{MembershipState, RoomMemberEventContent}, - server_acl::RoomServerAclEventContent, - }, - }, - serde::Raw, }; use serde_json::{json, value::to_raw_value}; @@ -47,21 +36,31 @@ pub(crate) async fn send_state_event_for_key_route( return Err!(Request(UserSuspended("You cannot perform this action while suspended."))); } - let event_id = send_state_event_for_key_helper( - &services, - sender_user, - &body.room_id, - &body.event_type, - &body.body.body, - &body.state_key, - if body.identity.is_appservice() { - body.timestamp - } else { - None - }, - ) - .boxed() - .await?; + let state_lock = services + .rooms + .state + .mutex + .lock::(&body.room_id) + .await; + + let event_id = services + .rooms + .timeline + .send_state_event_for_key( + sender_user, + &body.room_id, + &state_lock, + &body.event_type, + &body.body.body, + &body.state_key, + if body.identity.is_appservice() { + body.timestamp + } else { + None + }, + ) + .boxed() + .await?; Ok(send_state_event::v3::Response::new(event_id)) } @@ -185,257 +184,3 @@ pub(crate) async fn get_state_events_for_empty_key_route( .await .map(RumaResponse) } - -async fn send_state_event_for_key_helper( - services: &Services, - sender: &UserId, - room_id: &RoomId, - event_type: &StateEventType, - json: &Raw, - state_key: &str, - timestamp: Option, -) -> Result { - let json: &mut Raw = &mut json.clone(); - allowed_to_send_state_event(services, room_id, event_type, state_key, json).await?; - let state_lock = services.rooms.state.mutex.lock(room_id).await; - let event_id = services - .rooms - .timeline - .build_and_append_pdu( - PartialPdu { - event_type: event_type.to_string().into(), - content: serde_json::from_str(json.json().get())?, - state_key: Some(state_key.into()), - timestamp, - ..Default::default() - }, - sender, - Some(room_id), - &state_lock, - ) - .await?; - - Ok(event_id) -} - -async fn allowed_to_send_state_event( - services: &Services, - room_id: &RoomId, - event_type: &StateEventType, - state_key: &str, - json: &mut Raw, -) -> Result { - match event_type { - | StateEventType::RoomCreate => { - return Err!(Request(BadJson(debug_warn!( - %room_id, - "You cannot update m.room.create after a room has been created." - )))); - }, - | StateEventType::RoomServerAcl => { - // prevents common ACL paw-guns as ACL management is difficult and prone to - // irreversible mistakes - match json.deserialize_as_unchecked::() { - | Ok(acl_content) => { - let allow_has_wildcard = acl_content.allow.iter().any(|entry| entry == "*"); - let deny_has_wildcard = acl_content.deny.iter().any(|entry| entry == "*"); - let allow_has_server = acl_content - .allow - .iter() - .any(|entry| entry == services.globals.server_name().as_str()); - - if acl_content.allow.is_empty() { - return Err!(Request(BadJson(debug_warn!( - %room_id, - "Sending an ACL event with an empty allow key will permanently \ - brick the room for non-conduwuit's as this equates to no servers \ - being allowed to participate in this room." - )))); - } - - if allow_has_wildcard && deny_has_wildcard { - return Err!(Request(BadJson(debug_warn!( - %room_id, - "Sending an ACL event with a deny and allow key value of \"*\" will \ - permanently brick the room for non-conduwuit's as this equates to \ - no servers being allowed to participate in this room." - )))); - } - - if deny_has_wildcard - && !acl_content.is_allowed(services.globals.server_name()) - && !allow_has_server - { - return Err!(Request(BadJson(debug_warn!( - %room_id, - "Sending an ACL event with a deny key value of \"*\" and without \ - your own server name in the allow key will result in you being \ - unable to participate in this room." - )))); - } - - if !allow_has_wildcard - && !acl_content.is_allowed(services.globals.server_name()) - && !allow_has_server - { - return Err!(Request(BadJson(debug_warn!( - %room_id, - "Sending an ACL event for an allow key without \"*\" and without \ - your own server name in the allow key will result in you being \ - unable to participate in this room." - )))); - } - }, - | Err(e) => { - return Err!(Request(BadJson(debug_warn!( - "Room server ACL event is invalid: {e}" - )))); - }, - } - }, - | StateEventType::RoomEncryption => - // Forbid m.room.encryption if encryption is disabled - if !services.config.allow_encryption { - return Err!(Request(Forbidden("Encryption is disabled on this homeserver."))); - }, - | StateEventType::RoomJoinRules => { - // admin room is a sensitive room, it should not ever be made public - if let Ok(admin_room_id) = services.admin.get_admin_room().await { - if admin_room_id == room_id { - match json.deserialize_as_unchecked::() { - | Ok(join_rule) => - if join_rule.join_rule == JoinRule::Public { - return Err!(Request(Forbidden( - "Admin room is a sensitive room, it cannot be made public" - ))); - }, - | Err(e) => { - return Err!(Request(BadJson(debug_warn!( - "Room join rules event is invalid: {e}" - )))); - }, - } - } - } - }, - | StateEventType::RoomHistoryVisibility => { - // admin room is a sensitive room, it should not ever be made world readable - if let Ok(admin_room_id) = services.admin.get_admin_room().await { - match json.deserialize_as_unchecked::() { - | Ok(visibility_content) => { - if admin_room_id == room_id - && visibility_content.history_visibility - == HistoryVisibility::WorldReadable - { - return Err!(Request(Forbidden( - "Admin room is a sensitive room, it cannot be made world \ - readable (public room history)." - ))); - } - }, - | Err(e) => { - return Err!(Request(BadJson(debug_warn!( - "Room history visibility event is invalid: {e}" - )))); - }, - } - } - }, - | StateEventType::RoomCanonicalAlias => { - match json.deserialize_as_unchecked::() { - | Ok(canonical_alias_content) => { - let mut aliases = canonical_alias_content.alt_aliases.clone(); - - if let Some(alias) = canonical_alias_content.alias { - aliases.push(alias); - } - - for alias in aliases { - let (alias_room_id, _) = services - .rooms - .alias - .resolve_alias(&alias) - .await - .map_err(|e| { - err!(Request(Unknown("Failed resolving alias \"{alias}\": {e}"))) - })?; - - if alias_room_id != room_id { - return Err!(Request(BadAlias( - "Room alias {alias} does not belong to room {room_id}" - ))); - } - } - }, - | Err(e) => { - return Err!(Request(InvalidParam(debug_warn!( - "Room canonical alias event is invalid: {e}" - )))); - }, - } - }, - | StateEventType::RoomMember => - match json.deserialize_as_unchecked::() { - | Ok(mut membership_content) => { - let Ok(state_key) = UserId::parse(state_key) else { - return Err!(Request(BadJson( - "Membership event has invalid or non-existent state key" - ))); - }; - - if let Some(authorising_user) = - membership_content.join_authorized_via_users_server - { - // join_authorized_via_users_server must be thrown away, if user is - // already a member of the room. - if services - .rooms - .state_cache - .is_joined(&state_key, room_id) - .await - { - membership_content.join_authorized_via_users_server = None; - *json = Raw::::from_json_string( - serde_json::to_string(&membership_content)?, - )?; - return Ok(()); - } - - if membership_content.membership != MembershipState::Join { - return Err!(Request(BadJson( - "join_authorised_via_users_server is only for member joins" - ))); - } - - if !services.globals.user_is_local(&authorising_user) { - return Err!(Request(InvalidParam( - "Authorising user {authorising_user} does not belong to this \ - homeserver" - ))); - } - - if !services - .rooms - .state_cache - .is_joined(&authorising_user, room_id) - .await - { - return Err!(Request(InvalidParam( - "Authorising user {authorising_user} is not in the room, they \ - cannot authorise the join." - ))); - } - } - }, - | Err(e) => { - return Err!(Request(BadJson( - "Membership content must have a valid JSON body with at least a valid \ - membership state: {e}" - ))); - }, - }, - | _ => (), - } - - Ok(()) -} diff --git a/src/api/client/state/tests.rs b/src/api/client/state/tests.rs index 41b1cd833..51946fbb5 100644 --- a/src/api/client/state/tests.rs +++ b/src/api/client/state/tests.rs @@ -1,3 +1,8 @@ +use ruma::{ + events::{AnyStateEventContent, room::member::RoomMemberEventContent}, + serde::Raw, +}; + use super::*; #[test] diff --git a/src/service/rooms/timeline/helpers.rs b/src/service/rooms/timeline/helpers.rs new file mode 100644 index 000000000..c025dc69a --- /dev/null +++ b/src/service/rooms/timeline/helpers.rs @@ -0,0 +1,316 @@ +//! Helpers for submitting events with the right checks performed + +use conduwuit::{Err, Result, err, matrix::pdu::PartialPdu}; +use ruma::{ + MilliSecondsSinceUnixEpoch, OwnedEventId, RoomId, UserId, + events::{ + AnyStateEventContent, StateEventType, + room::{ + canonical_alias::RoomCanonicalAliasEventContent, + history_visibility::{HistoryVisibility, RoomHistoryVisibilityEventContent}, + join_rules::{JoinRule, RoomJoinRulesEventContent}, + member::{MembershipState, RoomMemberEventContent}, + server_acl::RoomServerAclEventContent, + }, + }, + serde::Raw, +}; + +use crate::rooms::state::RoomMutexGuard; + +impl super::Service { + #[allow(clippy::too_many_arguments)] + pub async fn send_state_event_for_key( + &self, + sender: &UserId, + room_id: &RoomId, + state_lock: &RoomMutexGuard, + event_type: &StateEventType, + content: &Raw, + state_key: &str, + timestamp: Option, + ) -> Result { + let mut content: Raw = content.clone(); + self.assert_allowed_to_send_state_event(room_id, event_type, state_key, &mut content) + .await?; + + let content = serde_json::from_str(content.json().get()) + .map_err(|e| err!(Request(BadJson("Invalid JSON body: {e}"))))?; + + let event_id = self + .build_and_append_pdu( + PartialPdu { + event_type: event_type.to_string().into(), + content, + state_key: Some(state_key.into()), + timestamp, + ..Default::default() + }, + sender, + Some(room_id), + state_lock, + ) + .await?; + + Ok(event_id) + } + + async fn assert_allowed_to_send_state_event( + &self, + room_id: &RoomId, + event_type: &StateEventType, + state_key: &str, + json: &mut Raw, + ) -> Result { + match event_type { + | StateEventType::RoomCreate => { + return Err!(Request(BadJson(debug_warn!( + %room_id, + "You cannot update m.room.create after a room has been created." + )))); + }, + | StateEventType::RoomServerAcl => + self.assert_allowed_to_send_room_server_acl_event(room_id, json) + .await?, + | StateEventType::RoomEncryption => + // Forbid m.room.encryption if encryption is disabled + if !self.services.config.allow_encryption { + return Err!(Request(Forbidden( + "Encryption is disabled on this homeserver." + ))); + }, + | StateEventType::RoomJoinRules => + self.assert_allowed_to_send_room_join_rules_event(room_id, json) + .await?, + | StateEventType::RoomHistoryVisibility => + self.assert_allowed_to_send_room_history_visibility_event(room_id, json) + .await?, + | StateEventType::RoomCanonicalAlias => + self.assert_allowed_to_send_room_canonical_alias_event(room_id, json) + .await?, + | StateEventType::RoomMember => + self.assert_allowed_to_send_room_member_event(room_id, state_key, json) + .await?, + | _ => (), + } + + Ok(()) + } + + async fn assert_allowed_to_send_room_server_acl_event( + &self, + room_id: &RoomId, + json: &Raw, + ) -> Result { + // prevents common ACL paw-guns as ACL management is difficult and prone to + // irreversible mistakes + + let acl_content = json + .deserialize_as_unchecked::() + .map_err(|e| { + err!(Request(BadJson(debug_warn!("Room server ACL event is invalid: {e}")))) + })?; + + let allow_has_wildcard = acl_content.allow.iter().any(|entry| entry == "*"); + let deny_has_wildcard = acl_content.deny.iter().any(|entry| entry == "*"); + let allow_has_server = acl_content + .allow + .iter() + .any(|entry| entry == self.services.globals.server_name().as_str()); + + if acl_content.allow.is_empty() { + return Err!(Request(BadJson(debug_warn!( + %room_id, + "Sending an ACL event with an empty allow key will permanently \ + brick the room for non-conduwuit's as this equates to no servers \ + being allowed to participate in this room." + )))); + } + + if allow_has_wildcard && deny_has_wildcard { + return Err!(Request(BadJson(debug_warn!( + %room_id, + "Sending an ACL event with a deny and allow key value of \"*\" will \ + permanently brick the room for non-conduwuit's as this equates to \ + no servers being allowed to participate in this room." + )))); + } + + if deny_has_wildcard + && !acl_content.is_allowed(self.services.globals.server_name()) + && !allow_has_server + { + return Err!(Request(BadJson(debug_warn!( + %room_id, + "Sending an ACL event with a deny key value of \"*\" and without \ + your own server name in the allow key will result in you being \ + unable to participate in this room." + )))); + } + + if !allow_has_wildcard + && !acl_content.is_allowed(self.services.globals.server_name()) + && !allow_has_server + { + return Err!(Request(BadJson(debug_warn!( + %room_id, + "Sending an ACL event for an allow key without \"*\" and without \ + your own server name in the allow key will result in you being \ + unable to participate in this room." + )))); + } + Ok(()) + } + + async fn assert_allowed_to_send_room_join_rules_event( + &self, + room_id: &RoomId, + json: &Raw, + ) -> Result { + // admin room is a sensitive room, it should not ever be made public + if let Ok(admin_room_id) = self.services.admin.get_admin_room().await + && admin_room_id == room_id + { + let join_rule = json + .deserialize_as_unchecked::() + .map_err(|e| { + err!(Request(BadJson(debug_warn!("Room join rules event is invalid: {e}")))) + })?; + + if join_rule.join_rule == JoinRule::Public { + return Err!(Request(Forbidden( + "Admin room is a sensitive room, it cannot be made public" + ))); + } + } + + Ok(()) + } + + async fn assert_allowed_to_send_room_history_visibility_event( + &self, + room_id: &RoomId, + json: &Raw, + ) -> Result { + // admin room is a sensitive room, it should not ever be made world readable + + if let Ok(admin_room_id) = self.services.admin.get_admin_room().await + && admin_room_id == room_id + { + let visibility_content = json + .deserialize_as_unchecked::() + .map_err(|e| { + err!(Request(BadJson(debug_warn!( + "Room history visibility event is invalid: {e}" + )))) + })?; + + if visibility_content.history_visibility == HistoryVisibility::WorldReadable { + return Err!(Request(Forbidden( + "Admin room is a sensitive room, it cannot be made public" + ))); + } + } + + Ok(()) + } + + async fn assert_allowed_to_send_room_canonical_alias_event( + &self, + room_id: &RoomId, + json: &Raw, + ) -> Result { + let canonical_alias_content = json + .deserialize_as_unchecked::() + .map_err(|e| { + err!(Request(BadJson(debug_warn!("Room canonical alias event is invalid: {e}")))) + })?; + + let mut aliases = canonical_alias_content.alt_aliases.clone(); + + if let Some(alias) = canonical_alias_content.alias { + aliases.push(alias); + } + + for alias in aliases { + let (alias_room_id, _) = self + .services + .alias + .resolve_alias(&alias) + .await + .map_err(|e| err!(Request(Unknown("Failed resolving alias \"{alias}\": {e}"))))?; + + if alias_room_id != room_id { + return Err!(Request(BadAlias( + "Room alias {alias} does not belong to room {room_id}" + ))); + } + } + + Ok(()) + } + + async fn assert_allowed_to_send_room_member_event( + &self, + room_id: &RoomId, + state_key: &str, + json: &mut Raw, + ) -> Result { + let mut membership_content = json + .deserialize_as_unchecked::() + .map_err(|e| { + err!(Request(BadJson(debug_warn!("Room member event is invalid: {e}")))) + })?; + + let Ok(state_key) = UserId::parse(state_key) else { + return Err!(Request(BadJson( + "Membership event has invalid or non-existent state key" + ))); + }; + + let Some(authorising_user) = membership_content.join_authorized_via_users_server else { + return Ok(()); + }; + + // join_authorized_via_users_server must be thrown away, if user is + // already a member of the room. + if self + .services + .state_cache + .is_joined(&state_key, room_id) + .await + { + membership_content.join_authorized_via_users_server = None; + *json = Raw::::from_json_string(serde_json::to_string( + &membership_content, + )?)?; + return Ok(()); + } + + if membership_content.membership != MembershipState::Join { + return Err!(Request(BadJson( + "join_authorised_via_users_server is only for member joins" + ))); + } + + if !self.services.globals.user_is_local(&authorising_user) { + return Err!(Request(InvalidParam( + "Authorising user {authorising_user} does not belong to this homeserver" + ))); + } + + if !self + .services + .state_cache + .is_joined(&authorising_user, room_id) + .await + { + return Err!(Request(InvalidParam( + "Authorising user {authorising_user} is not in the room, they cannot authorise \ + the join." + ))); + } + + Ok(()) + } +} diff --git a/src/service/rooms/timeline/mod.rs b/src/service/rooms/timeline/mod.rs index a3d0fbd7c..8d7ce6d63 100644 --- a/src/service/rooms/timeline/mod.rs +++ b/src/service/rooms/timeline/mod.rs @@ -3,6 +3,7 @@ mod backfill; mod build; mod create; mod data; +mod helpers; mod redact; use std::{fmt::Write, sync::Arc}; @@ -28,8 +29,8 @@ use serde::Deserialize; use self::data::Data; pub use self::{create::pdu_fits, data::PdusIterItem}; use crate::{ - Dep, account_data, admin, appservice, globals, pusher, rooms, sending, server_keys, sync, - users, + Dep, account_data, admin, appservice, config, globals, pusher, rooms, sending, server_keys, + sync, users, }; // Update Relationships @@ -67,6 +68,7 @@ struct Services { admin: Dep, alias: Dep, event_handler: Dep, + config: Dep, globals: Dep, pdu_metadata: Dep, pusher: Dep, @@ -98,6 +100,7 @@ impl crate::Service for Service { appservice: args.depend::("appservice"), admin: args.depend::("admin"), alias: args.depend::("rooms::alias"), + config: args.depend::("config"), event_handler: args .depend::("rooms::event_handler"), globals: args.depend::("globals"),