style: Refactor remaining parts of admin service

This commit is contained in:
timedout
2026-06-27 18:36:44 +01:00
parent 0bb97a53c5
commit fed9a599d9
3 changed files with 344 additions and 316 deletions
+100 -107
View File
@@ -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()))
}
+233 -199
View File
@@ -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::<RoomPowerLevelsEventContent>(
&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::<RoomPowerLevelsEventContent>(
&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::<RoomPowerLevelsEventContent>(
&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(|_| ())
}
}
+11 -10
View File
@@ -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<Arc<Self>> {
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<reqwest::ClientBuilder> {
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))
}