From fed9a599d97d0f9c2bf9723b24126e65fd82eebe Mon Sep 17 00:00:00 2001 From: timedout Date: Fri, 26 Jun 2026 06:09:43 +0100 Subject: [PATCH] style: Refactor remaining parts of admin service --- src/service/admin/execute.rs | 207 ++++++++--------- src/service/admin/grant.rs | 432 +++++++++++++++++++---------------- src/service/client/mod.rs | 21 +- 3 files changed, 344 insertions(+), 316 deletions(-) diff --git a/src/service/admin/execute.rs b/src/service/admin/execute.rs index 0df7b9882..9aeb80ce3 100644 --- a/src/service/admin/execute.rs +++ b/src/service/admin/execute.rs @@ -1,4 +1,4 @@ -use conduwuit::{Err, Result, debug, debug_info, error, implement, info}; +use conduwuit::{Err, Result, debug, debug_info, error, info}; use ruma::events::room::message::RoomMessageEventContent; use tokio::time::{Duration, sleep}; @@ -6,127 +6,120 @@ pub(super) const SIGNAL: &str = "SIGUSR2"; -/// Possibly spawn the terminal console at startup if configured. -#[implement(super::Service)] -pub(super) async fn console_auto_start(&self) { - #[cfg(feature = "console")] - if self.services.server.config.admin_console_automatic { - // Allow more of the startup sequence to execute before spawning - tokio::task::yield_now().await; - self.console.start().await; +impl super::Service { + /// Possibly spawn the terminal console at startup if configured. + pub(super) async fn console_auto_start(&self) { + #[cfg(feature = "console")] + if self.services.server.config.admin_console_automatic { + // Allow more of the startup sequence to execute before spawning + tokio::task::yield_now().await; + self.console.start().await; + } } -} -/// Shutdown the console when the admin worker terminates. -#[implement(super::Service)] -pub(super) async fn console_auto_stop(&self) { - #[cfg(feature = "console")] - self.console.close().await; -} + /// Shutdown the console when the admin worker terminates. + pub(super) async fn console_auto_stop(&self) { + #[cfg(feature = "console")] + self.console.close().await; + } -/// Execute admin commands after startup -#[implement(super::Service)] -pub async fn startup_execute(&self) -> Result { - // List of commands to execute - let commands = &self.services.server.config.admin_execute; + /// Execute admin commands after startup + pub async fn startup_execute(&self) -> Result { + // List of commands to execute + let commands = &self.services.server.config.admin_execute; - // Determine if we're running in smoketest-mode which will change some behaviors - let smoketest = self.services.server.config.test.contains("smoke"); + // Determine if we're running in smoketest-mode which will change some behaviors + let smoketest = self.services.server.config.test.contains("smoke"); - // When true, errors are ignored and startup continues. - let errors = !smoketest && self.services.server.config.admin_execute_errors_ignore; + // When true, errors are ignored and startup continues. + let errors = !smoketest && self.services.server.config.admin_execute_errors_ignore; - //TODO: remove this after run-states are broadcast - sleep(Duration::from_millis(500)).await; + //TODO: remove this after run-states are broadcast + sleep(Duration::from_millis(500)).await; - for (i, command) in commands.iter().enumerate() { - if let Err(e) = self.execute_command(i, command.clone()).await { - if !errors { - return Err(e); + for (i, command) in commands.iter().enumerate() { + if let Err(e) = self.execute_command(i, command.clone()).await { + if !errors { + return Err(e); + } } + + tokio::task::yield_now().await; } - tokio::task::yield_now().await; - } - - // The smoketest functionality is placed here for now and simply initiates - // shutdown after all commands have executed. - if smoketest { - debug_info!("Smoketest mode. All commands complete. Shutting down now..."); - self.services - .server - .shutdown() - .inspect_err(error::inspect_log) - .expect("Error shutting down from smoketest"); - } - - Ok(()) -} - -/// Execute admin commands after signal -#[implement(super::Service)] -pub(super) async fn signal_execute(&self) -> Result { - // List of commands to execute - let commands = self.services.server.config.admin_signal_execute.clone(); - - // When true, errors are ignored and execution continues. - let ignore_errors = self.services.server.config.admin_execute_errors_ignore; - - for (i, command) in commands.iter().enumerate() { - if let Err(e) = self.execute_command(i, command.clone()).await { - if !ignore_errors { - return Err(e); - } + // The smoketest functionality is placed here for now and simply initiates + // shutdown after all commands have executed. + if smoketest { + debug_info!("Smoketest mode. All commands complete. Shutting down now..."); + self.services + .server + .shutdown() + .inspect_err(error::inspect_log) + .expect("Error shutting down from smoketest"); } - tokio::task::yield_now().await; + Ok(()) } - Ok(()) -} + /// Execute admin commands after signal + pub(super) async fn signal_execute(&self) -> Result { + // List of commands to execute + let commands = self.services.server.config.admin_signal_execute.clone(); -/// Execute one admin command after startup or signal -#[implement(super::Service)] -async fn execute_command(&self, i: usize, command: String) -> Result { - debug!("Execute command #{i}: executing {command:?}"); + // When true, errors are ignored and execution continues. + let ignore_errors = self.services.server.config.admin_execute_errors_ignore; - match self - .command_in_place(command, None, InvocationSource::Console) - .await - { - | Ok(Some(output)) => Self::execute_command_output(i, &output), - | Err(output) => Self::execute_command_error(i, &output), - | Ok(None) => { - info!("Execute command #{i} completed (no output)."); - Ok(()) - }, + for (i, command) in commands.iter().enumerate() { + if let Err(e) = self.execute_command(i, command.clone()).await { + if !ignore_errors { + return Err(e); + } + } + + tokio::task::yield_now().await; + } + + Ok(()) + } + + /// Execute one admin command after startup or signal + async fn execute_command(&self, i: usize, command: String) -> Result { + debug!("Execute command #{i}: executing {command:?}"); + + match self + .command_in_place(command, None, InvocationSource::Console) + .await + { + | Ok(Some(output)) => Self::execute_command_output(i, &output), + | Err(output) => Self::execute_command_error(i, &output), + | Ok(None) => { + info!("Execute command #{i} completed (no output)."); + Ok(()) + }, + } + } + + #[cfg(feature = "console")] + fn execute_command_output(i: usize, content: &RoomMessageEventContent) -> Result { + debug_info!("Execute command #{i} completed:"); + super::console::print(content.body()); + Ok(()) + } + + #[cfg(feature = "console")] + fn execute_command_error(i: usize, content: &RoomMessageEventContent) -> Result { + super::console::print_err(content.body()); + Err!(debug_error!("Execute command #{i} failed.")) + } + + #[cfg(not(feature = "console"))] + fn execute_command_output(i: usize, content: &RoomMessageEventContent) -> Result { + info!("Execute command #{i} completed:\n{:#}", content.body()); + Ok(()) + } + + #[cfg(not(feature = "console"))] + fn execute_command_error(i: usize, content: &RoomMessageEventContent) -> Result { + Err!(error!("Execute command #{i} failed:\n{:#}", content.body())) } } - -#[cfg(feature = "console")] -#[implement(super::Service)] -fn execute_command_output(i: usize, content: &RoomMessageEventContent) -> Result { - debug_info!("Execute command #{i} completed:"); - super::console::print(content.body()); - Ok(()) -} - -#[cfg(feature = "console")] -#[implement(super::Service)] -fn execute_command_error(i: usize, content: &RoomMessageEventContent) -> Result { - super::console::print_err(content.body()); - Err!(debug_error!("Execute command #{i} failed.")) -} - -#[cfg(not(feature = "console"))] -#[implement(super::Service)] -fn execute_command_output(i: usize, content: &RoomMessageEventContent) -> Result { - info!("Execute command #{i} completed:\n{:#}", content.body()); - Ok(()) -} - -#[cfg(not(feature = "console"))] -#[implement(super::Service)] -fn execute_command_error(i: usize, content: &RoomMessageEventContent) -> Result { - Err!(error!("Execute command #{i} failed:\n{:#}", content.body())) -} diff --git a/src/service/admin/grant.rs b/src/service/admin/grant.rs index 624c6eade..938a15019 100644 --- a/src/service/admin/grant.rs +++ b/src/service/admin/grant.rs @@ -1,10 +1,8 @@ use std::collections::BTreeMap; -use conduwuit::{ - Err, Result, debug_info, debug_warn, error, implement, matrix::pdu::PartialPdu, warn, -}; +use conduwuit::{Err, Result, debug_info, debug_warn, error, matrix::pdu::PartialPdu, warn}; use ruma::{ - RoomId, UserId, + Int, RoomId, UserId, events::{ RoomAccountDataEventType, StateEventType, room::{ @@ -13,205 +11,241 @@ }, tag::{TagEvent, TagEventContent, TagInfo}, }, + int, }; -/// Invite the user to the conduwuit admin room. -/// -/// This is equivalent to granting server admin privileges. -#[implement(super::Service)] -pub async fn make_user_admin(&self, user_id: &UserId) -> Result { - let Ok(room_id) = self.get_admin_room().await else { - debug_warn!( - "make_user_admin was called without an admin room being available or created" - ); - return Ok(()); - }; - - let state_lock = self.services.state.mutex.lock(room_id.as_str()).await; - - if self.services.state_cache.is_joined(user_id, &room_id).await { - return Err!(debug_warn!("User is already joined in the admin room")); - } - if self - .services - .state_cache - .is_invited(user_id, &room_id) - .await - { - return Err!(debug_warn!("User is already pending an invitation to the admin room")); - } - - // Use the server user to grant the new admin's power level - let server_user = self.services.globals.server_user.as_ref(); - - // if this is our local user, just forcefully join them in the room. otherwise, - // invite the remote user. - if self.services.globals.user_is_local(user_id) { - debug_info!("Inviting local user {user_id} to admin room {room_id}"); - self.services - .timeline - .build_and_append_pdu( - PartialPdu::state( - String::from(user_id), - &RoomMemberEventContent::new(MembershipState::Invite), - ), - server_user, - Some(&room_id), - &state_lock, - ) - .await?; - - debug_info!("Force joining local user {user_id} to admin room {room_id}"); - self.services - .timeline - .build_and_append_pdu( - PartialPdu::state( - String::from(user_id), - &RoomMemberEventContent::new(MembershipState::Join), - ), - user_id, - Some(&room_id), - &state_lock, - ) - .await?; - } else { - debug_info!("Inviting remote user {user_id} to admin room {room_id}"); - self.services - .timeline - .build_and_append_pdu( - PartialPdu::state( - user_id.to_string(), - &RoomMemberEventContent::new(MembershipState::Invite), - ), - server_user, - Some(&room_id), - &state_lock, - ) - .await?; - } - - // Set power levels - let mut room_power_levels = self - .services - .state_accessor - .room_state_get_content::( - &room_id, - &StateEventType::RoomPowerLevels, - "", - ) - .await - .expect("admin room should have power levels"); - - room_power_levels - .users - .insert(server_user.into(), 69420.into()); - room_power_levels.users.insert(user_id.into(), 100.into()); - - self.services - .timeline - .build_and_append_pdu( - PartialPdu::state(String::new(), &room_power_levels), - server_user, - Some(&room_id), - &state_lock, - ) - .await?; - - // Set room tag - let room_tag = self.services.server.config.admin_room_tag.as_str(); - if !room_tag.is_empty() { - if let Err(e) = self.set_room_tag(&room_id, user_id, room_tag).await { - error!(%room_id, %user_id, %room_tag, "Failed to set tag for admin grant: {e}"); - } - } - - Ok(()) -} - -#[implement(super::Service)] -async fn set_room_tag(&self, room_id: &RoomId, user_id: &UserId, tag: &str) -> Result { - let mut event = self - .services - .account_data - .get_room(room_id, user_id, RoomAccountDataEventType::Tag) - .await - .unwrap_or_else(|_| TagEvent::new(TagEventContent::new(BTreeMap::new()))); - - event - .content - .tags - .insert(tag.to_owned().into(), TagInfo::new()); - - self.services - .account_data - .update( - Some(room_id), - user_id, - RoomAccountDataEventType::Tag, - &serde_json::to_value(event)?, - ) - .await -} - -/// Demote an admin, removing its rights. -#[implement(super::Service)] -pub async fn revoke_admin(&self, user_id: &UserId) -> Result { - use MembershipState::{Invite, Join, Knock, Leave}; - - if self - .services - .server - .config - .admins_list - .contains(&user_id.to_owned()) - { - warn!( - "Revoking the admin status of {user_id} will not work correctly as they are within \ - the admins_list config." - ); - } - - let Ok(room_id) = self.get_admin_room().await else { - return Err!(error!("No admin room available or created.")); - }; - - let state_lock = self.services.state.mutex.lock(room_id.as_str()).await; - - let mut member_content = match self - .services - .state_accessor - .get_member(&room_id, user_id) - .await - { - | Err(e) if e.is_not_found() => return Err!("{user_id} was never an admin."), - - | Err(e) => return Err!(error!(?e, "Failure occurred while attempting revoke.")), - - | Ok(event) if !matches!(event.membership, Invite | Knock | Join) => { - return Err!("Cannot revoke {user_id} in membership state {:?}.", event.membership); - }, - - | Ok(event) => { - assert!( - matches!(event.membership, Invite | Knock | Join), - "Incorrect membership state to remove user." +impl super::Service { + /// Invite the user to the conduwuit admin room. + /// + /// This is equivalent to granting server admin privileges. + pub async fn make_user_admin(&self, user_id: &UserId) -> Result { + let Ok(room_id) = self.get_admin_room().await else { + debug_warn!( + "make_user_admin was called without an admin room being available or created" ); + return Ok(()); + }; - event - }, - }; + let state_lock = self.services.state.mutex.lock(room_id.as_str()).await; - member_content.membership = Leave; - member_content.reason = Some("Admin Revoked".to_owned()); + if self.services.state_cache.is_joined(user_id, &room_id).await { + return Err!(debug_warn!("User is already joined in the admin room")); + } + if self + .services + .state_cache + .is_invited(user_id, &room_id) + .await + { + return Err!(debug_warn!("User is already pending an invitation to the admin room")); + } - self.services - .timeline - .build_and_append_pdu( - PartialPdu::state(user_id.to_string(), &member_content), - self.services.globals.server_user.as_ref(), - Some(&room_id), - &state_lock, - ) - .await - .map(|_| ()) + // Use the server user to grant the new admin's power level + let server_user = self.services.globals.server_user.as_ref(); + + // if this is our local user, just forcefully join them in the room. otherwise, + // invite the remote user. + if self.services.globals.user_is_local(user_id) { + debug_info!("Inviting local user {user_id} to admin room {room_id}"); + self.services + .timeline + .build_and_append_pdu( + PartialPdu::state( + String::from(user_id), + &RoomMemberEventContent::new(MembershipState::Invite), + ), + server_user, + Some(&room_id), + &state_lock, + ) + .await?; + + debug_info!("Force joining local user {user_id} to admin room {room_id}"); + self.services + .timeline + .build_and_append_pdu( + PartialPdu::state( + String::from(user_id), + &RoomMemberEventContent::new(MembershipState::Join), + ), + user_id, + Some(&room_id), + &state_lock, + ) + .await?; + } else { + debug_info!("Inviting remote user {user_id} to admin room {room_id}"); + self.services + .timeline + .build_and_append_pdu( + PartialPdu::state( + user_id.to_string(), + &RoomMemberEventContent::new(MembershipState::Invite), + ), + server_user, + Some(&room_id), + &state_lock, + ) + .await?; + } + + // Set power levels + let mut room_power_levels = self + .services + .state_accessor + .room_state_get_content::( + &room_id, + &StateEventType::RoomPowerLevels, + "", + ) + .await + .expect("admin room should have power levels"); + + let server_user_power_level = room_power_levels + .users + .get(server_user) + .copied() + .unwrap_or(Int::MAX); + room_power_levels + .users + .insert(user_id.into(), server_user_power_level.saturating_sub(int!(1))); + + self.services + .timeline + .build_and_append_pdu( + PartialPdu::state(String::new(), &room_power_levels), + server_user, + Some(&room_id), + &state_lock, + ) + .await?; + + // Set room tag + let room_tag = self.services.server.config.admin_room_tag.as_str(); + if !room_tag.is_empty() { + if let Err(e) = self.set_room_tag(&room_id, user_id, room_tag).await { + error!(%room_id, %user_id, %room_tag, "Failed to set tag for admin grant: {e}"); + } + } + + Ok(()) + } + + async fn set_room_tag(&self, room_id: &RoomId, user_id: &UserId, tag: &str) -> Result { + let mut event = self + .services + .account_data + .get_room(room_id, user_id, RoomAccountDataEventType::Tag) + .await + .unwrap_or_else(|_| TagEvent::new(TagEventContent::new(BTreeMap::new()))); + + event + .content + .tags + .insert(tag.to_owned().into(), TagInfo::new()); + + self.services + .account_data + .update( + Some(room_id), + user_id, + RoomAccountDataEventType::Tag, + &serde_json::to_value(event)?, + ) + .await + } + + /// Demote an admin, removing its rights. + pub async fn revoke_admin(&self, user_id: &UserId) -> Result { + use MembershipState::{Invite, Join, Knock, Leave}; + + if self + .services + .server + .config + .admins_list + .contains(&user_id.to_owned()) + { + warn!( + "Revoking the admin status of {user_id} will not work correctly as they are \ + within the admins_list config." + ); + } + + let Ok(room_id) = self.get_admin_room().await else { + return Err!(error!("No admin room available or created.")); + }; + + let state_lock = self.services.state.mutex.lock(room_id.as_str()).await; + + let mut room_power_levels = self + .services + .state_accessor + .room_state_get_content::( + &room_id, + &StateEventType::RoomPowerLevels, + "", + ) + .await + .expect("admin room should have power levels"); + if room_power_levels.users.remove(user_id).is_some() { + // drop the target's power level + self.services + .timeline + .build_and_append_pdu( + PartialPdu::state(String::new(), &room_power_levels), + self.services.globals.server_user.as_ref(), + Some(&room_id), + &state_lock, + ) + .await + .inspect_err(|e| { + error!(?e, "Failed to update power levels while revoking admin."); + }) + .ok(); + // Don't treat this as fatal. removing them from the room is more + // important. + } + + let mut member_content = match self + .services + .state_accessor + .get_member(&room_id, user_id) + .await + { + | Err(e) if e.is_not_found() => return Err!("{user_id} was never an admin."), + + | Err(e) => return Err!(error!(?e, "Failure occurred while attempting revoke.")), + + | Ok(event) if !matches!(event.membership, Invite | Knock | Join) => { + return Err!( + "Cannot revoke {user_id} in membership state {:?}.", + event.membership + ); + }, + + | Ok(event) => { + assert!( + matches!(event.membership, Invite | Knock | Join), + "Incorrect membership state to remove user." + ); + + event + }, + }; + + member_content.membership = Leave; + member_content.reason = Some("Admin Revoked".to_owned()); + + self.services + .timeline + .build_and_append_pdu( + PartialPdu::state(user_id.to_string(), &member_content), + self.services.globals.server_user.as_ref(), + Some(&room_id), + &state_lock, + ) + .await + .map(|_| ()) + } } diff --git a/src/service/client/mod.rs b/src/service/client/mod.rs index 4792bd7ad..8492714da 100644 --- a/src/service/client/mod.rs +++ b/src/service/client/mod.rs @@ -1,6 +1,6 @@ use std::{sync::Arc, time::Duration}; -use conduwuit::{Config, Result, err, implement, trace}; +use conduwuit::{Config, Result, err, trace}; use either::Either; use ipaddress::IPAddress; use reqwest::redirect; @@ -141,6 +141,16 @@ fn build(args: crate::Args<'_>) -> Result> { fn name(&self) -> &str { service::make_name(std::module_path!()) } } +impl Service { + #[inline] + #[must_use] + pub fn valid_cidr_range(&self, ip: &IPAddress) -> bool { + self.cidr_range_denylist + .iter() + .all(|cidr| !cidr.includes(ip)) + } +} + fn base(config: &Config) -> Result { let mut builder = reqwest::Client::builder() .hickory_dns(true) @@ -227,12 +237,3 @@ fn builder_interface( Ok(builder) } } - -#[inline] -#[must_use] -#[implement(Service)] -pub fn valid_cidr_range(&self, ip: &IPAddress) -> bool { - self.cidr_range_denylist - .iter() - .all(|cidr| !cidr.includes(ip)) -}