From f712c0cefb28681cd4fbd27b59761bcb24b19989 Mon Sep 17 00:00:00 2001 From: Matthias Ahouansou Date: Wed, 12 Jun 2024 01:42:39 -0400 Subject: [PATCH] fix: restrict who can remove aliases Previously, anyone could remove any local alias, meaning that someone could re-route a popular alias elsewhere Now, only the creator of the alias, users who can set canonical aliases for the room, server admins and the server user can delete aliases added some additional changes/fixes to adapt to our codebase Co-authored-by: strawberry Signed-off-by: strawberry --- src/admin/room/room_alias_commands.rs | 24 +++++++-- src/api/client/alias.rs | 29 ++++------- src/api/client/room.rs | 7 ++- src/database/kvdatabase.rs | 2 + src/service/admin.rs | 19 +++++-- src/service/rooms/alias/data.rs | 34 +++++++++++-- src/service/rooms/alias/mod.rs | 72 +++++++++++++++++++++++++-- 7 files changed, 151 insertions(+), 36 deletions(-) diff --git a/src/admin/room/room_alias_commands.rs b/src/admin/room/room_alias_commands.rs index 33c165b82..7312b0f6f 100644 --- a/src/admin/room/room_alias_commands.rs +++ b/src/admin/room/room_alias_commands.rs @@ -1,11 +1,14 @@ use std::fmt::Write; -use ruma::{events::room::message::RoomMessageEventContent, RoomAliasId}; +use ruma::{events::room::message::RoomMessageEventContent, RoomAliasId, UserId}; use super::RoomAliasCommand; use crate::{escape_html, services, Result}; pub(crate) async fn process(command: RoomAliasCommand, _body: Vec<&str>) -> Result { + let server_user = UserId::parse_with_server_name(String::from("conduit"), services().globals.server_name()) + .expect("server's username is valid"); + match command { RoomAliasCommand::Set { ref room_alias_localpart, @@ -28,7 +31,11 @@ pub(crate) async fn process(command: RoomAliasCommand, _body: Vec<&str>) -> Resu room_id, .. } => match (force, services().rooms.alias.resolve_local_alias(&room_alias)) { - (true, Ok(Some(id))) => match services().rooms.alias.set_alias(&room_alias, &room_id) { + (true, Ok(Some(id))) => match services() + .rooms + .alias + .set_alias(&room_alias, &room_id, &server_user) + { Ok(()) => Ok(RoomMessageEventContent::text_plain(format!( "Successfully overwrote alias (formerly {id})" ))), @@ -37,7 +44,11 @@ pub(crate) async fn process(command: RoomAliasCommand, _body: Vec<&str>) -> Resu (false, Ok(Some(id))) => Ok(RoomMessageEventContent::text_plain(format!( "Refusing to overwrite in use alias for {id}, use -f or --force to overwrite" ))), - (_, Ok(None)) => match services().rooms.alias.set_alias(&room_alias, &room_id) { + (_, Ok(None)) => match services() + .rooms + .alias + .set_alias(&room_alias, &room_id, &server_user) + { Ok(()) => Ok(RoomMessageEventContent::text_plain("Successfully set alias")), Err(err) => Ok(RoomMessageEventContent::text_plain(format!("Failed to remove alias: {err}"))), }, @@ -46,7 +57,12 @@ pub(crate) async fn process(command: RoomAliasCommand, _body: Vec<&str>) -> Resu RoomAliasCommand::Remove { .. } => match services().rooms.alias.resolve_local_alias(&room_alias) { - Ok(Some(id)) => match services().rooms.alias.remove_alias(&room_alias) { + Ok(Some(id)) => match services() + .rooms + .alias + .remove_alias(&room_alias, &server_user) + .await + { Ok(()) => Ok(RoomMessageEventContent::text_plain(format!("Removed alias from {id}"))), Err(err) => Ok(RoomMessageEventContent::text_plain(format!("Failed to remove alias: {err}"))), }, diff --git a/src/api/client/alias.rs b/src/api/client/alias.rs index e70105f3f..5752ea9b6 100644 --- a/src/api/client/alias.rs +++ b/src/api/client/alias.rs @@ -22,6 +22,8 @@ use crate::{ /// /// Creates a new room alias on this server. pub(crate) async fn create_alias_route(body: Ruma) -> Result { + let sender_user = body.sender_user.as_ref().expect("user is authenticated"); + alias_checks(&body.room_alias, &body.appservice_info).await?; // this isn't apart of alias_checks or delete alias route because we should @@ -43,17 +45,10 @@ pub(crate) async fn create_alias_route(body: Ruma) -> return Err(Error::Conflict("Alias already exists.")); } - if services() + services() .rooms .alias - .set_alias(&body.room_alias, &body.room_id) - .is_err() - { - return Err(Error::BadRequest( - ErrorKind::InvalidParam, - "Invalid room alias. Alias must be in the form of '#localpart:server_name'", - )); - }; + .set_alias(&body.room_alias, &body.room_id, sender_user)?; Ok(create_alias::v3::Response::new()) } @@ -62,10 +57,12 @@ pub(crate) async fn create_alias_route(body: Ruma) -> /// /// Deletes a room alias from this server. /// -/// - TODO: additional access control checks /// - TODO: Update canonical alias event pub(crate) async fn delete_alias_route(body: Ruma) -> Result { + let sender_user = body.sender_user.as_ref().expect("user is authenticated"); + alias_checks(&body.room_alias, &body.appservice_info).await?; + if services() .rooms .alias @@ -75,17 +72,11 @@ pub(crate) async fn delete_alias_route(body: Ruma) -> return Err(Error::BadRequest(ErrorKind::NotFound, "Alias does not exist.")); } - if services() + services() .rooms .alias - .remove_alias(&body.room_alias) - .is_err() - { - return Err(Error::BadRequest( - ErrorKind::InvalidParam, - "Invalid room alias. Alias must be in the form of '#localpart:server_name'", - )); - }; + .remove_alias(&body.room_alias, sender_user) + .await?; // TODO: update alt_aliases? diff --git a/src/api/client/room.rs b/src/api/client/room.rs index 0cea6ef7d..14071d395 100644 --- a/src/api/client/room.rs +++ b/src/api/client/room.rs @@ -467,7 +467,10 @@ pub(crate) async fn create_room_route(body: Ruma) -> R // Homeserver specific stuff if let Some(alias) = alias { - services().rooms.alias.set_alias(&alias, &room_id)?; + services() + .rooms + .alias + .set_alias(&alias, &room_id, sender_user)?; } if body.visibility == room::Visibility::Public { @@ -787,7 +790,7 @@ pub(crate) async fn upgrade_room_route(body: Ruma) -> services() .rooms .alias - .set_alias(&alias, &replacement_room)?; + .set_alias(&alias, &replacement_room, sender_user)?; } // Get the old room power levels diff --git a/src/database/kvdatabase.rs b/src/database/kvdatabase.rs index ce3bf30f9..ef34fec4d 100644 --- a/src/database/kvdatabase.rs +++ b/src/database/kvdatabase.rs @@ -57,6 +57,7 @@ pub struct KeyValueDatabase { pub eventid_pduid: Arc, pub roomid_pduleaves: Arc, pub alias_roomid: Arc, + pub alias_userid: Arc, // UserId = AliasId (User who created the alias) pub aliasid_alias: Arc, // AliasId = RoomId + Count pub publicroomids: Arc, @@ -193,6 +194,7 @@ impl KeyValueDatabase { roomid_pduleaves: builder.open_tree("roomid_pduleaves")?, alias_roomid: builder.open_tree("alias_roomid")?, + alias_userid: builder.open_tree("alias_userid")?, aliasid_alias: builder.open_tree("aliasid_alias")?, publicroomids: builder.open_tree("publicroomids")?, diff --git a/src/service/admin.rs b/src/service/admin.rs index 63d6d5c64..37a07a582 100644 --- a/src/service/admin.rs +++ b/src/service/admin.rs @@ -72,8 +72,9 @@ impl Service { let Ok(Some(admin_room)) = Self::get_admin_room().await else { return Ok(()); }; - let server_name = services().globals.server_name(); - let server_user = UserId::parse(format!("@conduit:{server_name}")).expect("server's username is valid"); + + let server_user = UserId::parse_with_server_name(String::from("conduit"), services().globals.server_name()) + .expect("server's username is valid"); loop { debug_assert!(!receiver.is_closed(), "channel closed"); @@ -386,7 +387,10 @@ impl Service { ) .await?; - services().rooms.alias.set_alias(&alias, &room_id)?; + services() + .rooms + .alias + .set_alias(&alias, &room_id, &server_user)?; // 7. (ad-hoc) Disable room previews for everyone by default services() @@ -533,4 +537,13 @@ impl Service { Ok(()) } + + /// Checks whether a given user is an admin of this server + pub async fn user_is_admin(&self, user_id: &UserId) -> Result { + let Ok(Some(admin_room)) = Self::get_admin_room().await else { + return Ok(false); + }; + + services().rooms.state_cache.is_joined(user_id, &admin_room) + } } diff --git a/src/service/rooms/alias/data.rs b/src/service/rooms/alias/data.rs index 536f8228d..e0f4335eb 100644 --- a/src/service/rooms/alias/data.rs +++ b/src/service/rooms/alias/data.rs @@ -1,10 +1,10 @@ -use ruma::{api::client::error::ErrorKind, OwnedRoomAliasId, OwnedRoomId, RoomAliasId, RoomId}; +use ruma::{api::client::error::ErrorKind, OwnedRoomAliasId, OwnedRoomId, OwnedUserId, RoomAliasId, RoomId, UserId}; use crate::{services, utils, Error, KeyValueDatabase, Result}; pub trait Data: Send + Sync { /// Creates or updates the alias to the given room id. - fn set_alias(&self, alias: &RoomAliasId, room_id: &RoomId) -> Result<()>; + fn set_alias(&self, alias: &RoomAliasId, room_id: &RoomId, user_id: &UserId) -> Result<()>; /// Forgets about an alias. Returns an error if the alias did not exist. fn remove_alias(&self, alias: &RoomAliasId) -> Result<()>; @@ -12,6 +12,9 @@ pub trait Data: Send + Sync { /// Looks up the roomid for the given alias. fn resolve_local_alias(&self, alias: &RoomAliasId) -> Result>; + /// Finds the user who assigned the given alias to a room + fn who_created_alias(&self, alias: &RoomAliasId) -> Result>; + /// Returns all local aliases that point to the given room fn local_aliases_for_room<'a>( &'a self, room_id: &RoomId, @@ -22,13 +25,19 @@ pub trait Data: Send + Sync { } impl Data for KeyValueDatabase { - fn set_alias(&self, alias: &RoomAliasId, room_id: &RoomId) -> Result<()> { + fn set_alias(&self, alias: &RoomAliasId, room_id: &RoomId, user_id: &UserId) -> Result<()> { + // Comes first as we don't want a stuck alias + self.alias_userid + .insert(alias.alias().as_bytes(), user_id.as_bytes())?; + self.alias_roomid .insert(alias.alias().as_bytes(), room_id.as_bytes())?; + let mut aliasid = room_id.as_bytes().to_vec(); aliasid.push(0xFF); aliasid.extend_from_slice(&services().globals.next_count()?.to_be_bytes()); self.aliasid_alias.insert(&aliasid, alias.as_bytes())?; + Ok(()) } @@ -40,10 +49,14 @@ impl Data for KeyValueDatabase { for (key, _) in self.aliasid_alias.scan_prefix(prefix) { self.aliasid_alias.remove(&key)?; } + self.alias_roomid.remove(alias.alias().as_bytes())?; + + self.alias_userid.remove(alias.alias().as_bytes())?; } else { - return Err(Error::BadRequest(ErrorKind::NotFound, "Alias does not exist.")); + return Err(Error::BadRequest(ErrorKind::NotFound, "Alias does not exist or is invalid.")); } + Ok(()) } @@ -60,6 +73,19 @@ impl Data for KeyValueDatabase { .transpose() } + fn who_created_alias(&self, alias: &RoomAliasId) -> Result> { + self.alias_userid + .get(alias.alias().as_bytes())? + .map(|bytes| { + UserId::parse( + utils::string_from_bytes(&bytes) + .map_err(|_| Error::bad_database("User ID in alias_userid is invalid unicode."))?, + ) + .map_err(|_| Error::bad_database("User ID in alias_roomid is invalid.")) + }) + .transpose() + } + fn local_aliases_for_room<'a>( &'a self, room_id: &RoomId, ) -> Box> + 'a> { diff --git a/src/service/rooms/alias/mod.rs b/src/service/rooms/alias/mod.rs index f8d10b452..311456fe3 100644 --- a/src/service/rooms/alias/mod.rs +++ b/src/service/rooms/alias/mod.rs @@ -3,9 +3,16 @@ mod data; use std::sync::Arc; use data::Data; -use ruma::{OwnedRoomAliasId, OwnedRoomId, RoomAliasId, RoomId}; +use ruma::{ + api::client::error::ErrorKind, + events::{ + room::power_levels::{RoomPowerLevels, RoomPowerLevelsEventContent}, + StateEventType, + }, + OwnedRoomAliasId, OwnedRoomId, RoomAliasId, RoomId, UserId, +}; -use crate::Result; +use crate::{services, Error, Result}; pub struct Service { pub db: Arc, @@ -13,10 +20,21 @@ pub struct Service { impl Service { #[tracing::instrument(skip(self))] - pub fn set_alias(&self, alias: &RoomAliasId, room_id: &RoomId) -> Result<()> { self.db.set_alias(alias, room_id) } + pub fn set_alias(&self, alias: &RoomAliasId, room_id: &RoomId, user_id: &UserId) -> Result<()> { + self.db.set_alias(alias, room_id, user_id) + } #[tracing::instrument(skip(self))] - pub fn remove_alias(&self, alias: &RoomAliasId) -> Result<()> { self.db.remove_alias(alias) } + pub async fn remove_alias(&self, alias: &RoomAliasId, user_id: &UserId) -> Result<()> { + if self.user_can_remove_alias(alias, user_id).await? { + self.db.remove_alias(alias) + } else { + Err(Error::BadRequest( + ErrorKind::forbidden(), + "User is not permitted to remove this alias.", + )) + } + } #[tracing::instrument(skip(self))] pub fn resolve_local_alias(&self, alias: &RoomAliasId) -> Result> { @@ -34,4 +52,50 @@ impl Service { pub fn all_local_aliases<'a>(&'a self) -> Box> + 'a> { self.db.all_local_aliases() } + + async fn user_can_remove_alias(&self, alias: &RoomAliasId, user_id: &UserId) -> Result { + let Some(room_id) = self.resolve_local_alias(alias)? else { + return Err(Error::BadRequest(ErrorKind::NotFound, "Alias not found.")); + }; + + let server_user = + UserId::parse_with_server_name(String::from("conduit"), services().globals.server_name()).unwrap(); + + // The creator of an alias can remove it + if self + .db + .who_created_alias(alias)? + .is_some_and(|user| user == user_id) + // Server admins can remove any local alias + || services().admin.user_is_admin(user_id).await? + // Always allow the server service account to remove the alias, since there may not be an admin room + || server_user == user_id + { + Ok(true) + // Checking whether the user is able to change canonical aliases of the + // room + } else if let Some(event) = + services() + .rooms + .state_accessor + .room_state_get(&room_id, &StateEventType::RoomPowerLevels, "")? + { + serde_json::from_str(event.content.get()) + .map_err(|_| Error::bad_database("Invalid event content for m.room.power_levels")) + .map(|content: RoomPowerLevelsEventContent| { + RoomPowerLevels::from(content).user_can_send_state(user_id, StateEventType::RoomCanonicalAlias) + }) + // If there is no power levels event, only the room creator can change + // canonical aliases + } else if let Some(event) = + services() + .rooms + .state_accessor + .room_state_get(&room_id, &StateEventType::RoomCreate, "")? + { + Ok(event.sender == user_id) + } else { + Err(Error::bad_database("Room has no m.room.create event")) + } + } }