mirror of
https://forgejo.ellis.link/continuwuation/continuwuity.git
synced 2026-05-26 20:49:55 +00:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8a561fcd3a | |||
| 25c305f473 | |||
| c900350164 | |||
| c565e6ffbc | |||
| 442f887c98 | |||
| 03220845e5 | |||
| f8c1e9bcde |
@@ -0,0 +1 @@
|
|||||||
|
Certain potentially dangerous admin commands are now restricted to only be usable in the admin room and server console.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Implemented a configuration defined admin list independent of the admin room. (@Terryiscool160).
|
||||||
@@ -1590,6 +1590,18 @@
|
|||||||
#
|
#
|
||||||
#admin_room_tag = "m.server_notice"
|
#admin_room_tag = "m.server_notice"
|
||||||
|
|
||||||
|
# A list of Matrix IDs that are qualified as server admins.
|
||||||
|
#
|
||||||
|
# Any Matrix IDs within this list are regarded as an admin
|
||||||
|
# regardless of whether they are in the admin room or not
|
||||||
|
#
|
||||||
|
#admins_list = []
|
||||||
|
|
||||||
|
# Defines whether those within the admin room are added to the
|
||||||
|
# admins_list.
|
||||||
|
#
|
||||||
|
#admins_from_room = true
|
||||||
|
|
||||||
# Sentry.io crash/panic reporting, performance monitoring/metrics, etc.
|
# Sentry.io crash/panic reporting, performance monitoring/metrics, etc.
|
||||||
# This is NOT enabled by default.
|
# This is NOT enabled by default.
|
||||||
#
|
#
|
||||||
|
|||||||
+15
-3
@@ -53,14 +53,26 @@ pub(super) async fn process(command: AdminCommand, context: &Context<'_>) -> Res
|
|||||||
use AdminCommand::*;
|
use AdminCommand::*;
|
||||||
|
|
||||||
match command {
|
match command {
|
||||||
| Appservices(command) => appservice::process(command, context).await,
|
| Appservices(command) => {
|
||||||
|
// appservice commands are all restricted
|
||||||
|
context.bail_restricted()?;
|
||||||
|
appservice::process(command, context).await
|
||||||
|
},
|
||||||
| Media(command) => media::process(command, context).await,
|
| Media(command) => media::process(command, context).await,
|
||||||
| Users(command) => user::process(command, context).await,
|
| Users(command) => {
|
||||||
|
// user commands are all restricted
|
||||||
|
context.bail_restricted()?;
|
||||||
|
user::process(command, context).await
|
||||||
|
},
|
||||||
| Rooms(command) => room::process(command, context).await,
|
| Rooms(command) => room::process(command, context).await,
|
||||||
| Federation(command) => federation::process(command, context).await,
|
| Federation(command) => federation::process(command, context).await,
|
||||||
| Server(command) => server::process(command, context).await,
|
| Server(command) => server::process(command, context).await,
|
||||||
| Debug(command) => debug::process(command, context).await,
|
| Debug(command) => debug::process(command, context).await,
|
||||||
| Query(command) => query::process(command, context).await,
|
| Query(command) => {
|
||||||
|
// query commands are all restricted
|
||||||
|
context.bail_restricted()?;
|
||||||
|
query::process(command, context).await
|
||||||
|
},
|
||||||
| Check(command) => check::process(command, context).await,
|
| Check(command) => check::process(command, context).await,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+21
-1
@@ -1,6 +1,6 @@
|
|||||||
use std::{fmt, time::SystemTime};
|
use std::{fmt, time::SystemTime};
|
||||||
|
|
||||||
use conduwuit::Result;
|
use conduwuit::{Err, Result};
|
||||||
use conduwuit_service::Services;
|
use conduwuit_service::Services;
|
||||||
use futures::{
|
use futures::{
|
||||||
Future, FutureExt, TryFutureExt,
|
Future, FutureExt, TryFutureExt,
|
||||||
@@ -8,6 +8,7 @@ use futures::{
|
|||||||
lock::Mutex,
|
lock::Mutex,
|
||||||
};
|
};
|
||||||
use ruma::{EventId, UserId};
|
use ruma::{EventId, UserId};
|
||||||
|
use service::admin::InvocationSource;
|
||||||
|
|
||||||
pub(crate) struct Context<'a> {
|
pub(crate) struct Context<'a> {
|
||||||
pub(crate) services: &'a Services,
|
pub(crate) services: &'a Services,
|
||||||
@@ -16,6 +17,7 @@ pub(crate) struct Context<'a> {
|
|||||||
pub(crate) reply_id: Option<&'a EventId>,
|
pub(crate) reply_id: Option<&'a EventId>,
|
||||||
pub(crate) sender: Option<&'a UserId>,
|
pub(crate) sender: Option<&'a UserId>,
|
||||||
pub(crate) output: Mutex<BufWriter<Vec<u8>>>,
|
pub(crate) output: Mutex<BufWriter<Vec<u8>>>,
|
||||||
|
pub(crate) source: InvocationSource,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Context<'_> {
|
impl Context<'_> {
|
||||||
@@ -43,4 +45,22 @@ impl Context<'_> {
|
|||||||
self.sender
|
self.sender
|
||||||
.unwrap_or_else(|| self.services.globals.server_user.as_ref())
|
.unwrap_or_else(|| self.services.globals.server_user.as_ref())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns an Err if the [`Self::source`] of this context does not allow
|
||||||
|
/// restricted commands to be executed.
|
||||||
|
///
|
||||||
|
/// This is intended to be placed at the start of restricted commands'
|
||||||
|
/// implementations, like so:
|
||||||
|
///
|
||||||
|
/// ```ignore
|
||||||
|
/// self.bail_restricted()?;
|
||||||
|
/// // actual command impl
|
||||||
|
/// ```
|
||||||
|
pub(crate) fn bail_restricted(&self) -> Result {
|
||||||
|
if self.source.allows_restricted() {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err!("This command can only be used in the admin room.")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -291,6 +291,8 @@ pub(super) async fn get_remote_pdu(
|
|||||||
|
|
||||||
#[admin_command]
|
#[admin_command]
|
||||||
pub(super) async fn get_room_state(&self, room: OwnedRoomOrAliasId) -> Result {
|
pub(super) async fn get_room_state(&self, room: OwnedRoomOrAliasId) -> Result {
|
||||||
|
self.bail_restricted()?;
|
||||||
|
|
||||||
let room_id = self.services.rooms.alias.resolve(&room).await?;
|
let room_id = self.services.rooms.alias.resolve(&room).await?;
|
||||||
let room_state: Vec<Raw<AnyStateEvent>> = self
|
let room_state: Vec<Raw<AnyStateEvent>> = self
|
||||||
.services
|
.services
|
||||||
@@ -417,27 +419,6 @@ pub(super) async fn change_log_level(&self, filter: Option<String>, reset: bool)
|
|||||||
Err!("No log level was specified.")
|
Err!("No log level was specified.")
|
||||||
}
|
}
|
||||||
|
|
||||||
#[admin_command]
|
|
||||||
pub(super) async fn sign_json(&self) -> Result {
|
|
||||||
if self.body.len() < 2
|
|
||||||
|| !self.body[0].trim().starts_with("```")
|
|
||||||
|| self.body.last().unwrap_or(&"").trim() != "```"
|
|
||||||
{
|
|
||||||
return Err!("Expected code block in command body. Add --help for details.");
|
|
||||||
}
|
|
||||||
|
|
||||||
let string = self.body[1..self.body.len().checked_sub(1).unwrap()].join("\n");
|
|
||||||
match serde_json::from_str(&string) {
|
|
||||||
| Err(e) => return Err!("Invalid json: {e}"),
|
|
||||||
| Ok(mut value) => {
|
|
||||||
self.services.server_keys.sign_json(&mut value)?;
|
|
||||||
let json_text = serde_json::to_string_pretty(&value)?;
|
|
||||||
write!(self, "{json_text}")
|
|
||||||
},
|
|
||||||
}
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
#[admin_command]
|
#[admin_command]
|
||||||
pub(super) async fn verify_json(&self) -> Result {
|
pub(super) async fn verify_json(&self) -> Result {
|
||||||
if self.body.len() < 2
|
if self.body.len() < 2
|
||||||
@@ -477,6 +458,8 @@ pub(super) async fn verify_pdu(&self, event_id: OwnedEventId) -> Result {
|
|||||||
#[admin_command]
|
#[admin_command]
|
||||||
#[tracing::instrument(skip(self))]
|
#[tracing::instrument(skip(self))]
|
||||||
pub(super) async fn first_pdu_in_room(&self, room_id: OwnedRoomId) -> Result {
|
pub(super) async fn first_pdu_in_room(&self, room_id: OwnedRoomId) -> Result {
|
||||||
|
self.bail_restricted()?;
|
||||||
|
|
||||||
if !self
|
if !self
|
||||||
.services
|
.services
|
||||||
.rooms
|
.rooms
|
||||||
@@ -502,6 +485,8 @@ pub(super) async fn first_pdu_in_room(&self, room_id: OwnedRoomId) -> Result {
|
|||||||
#[admin_command]
|
#[admin_command]
|
||||||
#[tracing::instrument(skip(self))]
|
#[tracing::instrument(skip(self))]
|
||||||
pub(super) async fn latest_pdu_in_room(&self, room_id: OwnedRoomId) -> Result {
|
pub(super) async fn latest_pdu_in_room(&self, room_id: OwnedRoomId) -> Result {
|
||||||
|
self.bail_restricted()?;
|
||||||
|
|
||||||
if !self
|
if !self
|
||||||
.services
|
.services
|
||||||
.rooms
|
.rooms
|
||||||
@@ -532,6 +517,8 @@ pub(super) async fn force_set_room_state_from_server(
|
|||||||
server_name: OwnedServerName,
|
server_name: OwnedServerName,
|
||||||
at_event: Option<OwnedEventId>,
|
at_event: Option<OwnedEventId>,
|
||||||
) -> Result {
|
) -> Result {
|
||||||
|
self.bail_restricted()?;
|
||||||
|
|
||||||
if !self
|
if !self
|
||||||
.services
|
.services
|
||||||
.rooms
|
.rooms
|
||||||
|
|||||||
@@ -47,9 +47,9 @@ pub enum DebugCommand {
|
|||||||
shorteventid: ShortEventId,
|
shorteventid: ShortEventId,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// - Attempts to retrieve a PDU from a remote server. Inserts it into our
|
/// - Attempts to retrieve a PDU from a remote server. **Does not** insert
|
||||||
/// database/timeline if found and we do not have this PDU already
|
/// it into the database
|
||||||
/// (following normal event auth rules, handles it as an incoming PDU).
|
/// or persist it anywhere.
|
||||||
GetRemotePdu {
|
GetRemotePdu {
|
||||||
/// An event ID (a $ followed by the base64 reference hash)
|
/// An event ID (a $ followed by the base64 reference hash)
|
||||||
event_id: OwnedEventId,
|
event_id: OwnedEventId,
|
||||||
@@ -125,12 +125,6 @@ pub enum DebugCommand {
|
|||||||
reset: bool,
|
reset: bool,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// - Sign JSON blob
|
|
||||||
///
|
|
||||||
/// This command needs a JSON blob provided in a Markdown code block below
|
|
||||||
/// the command.
|
|
||||||
SignJson,
|
|
||||||
|
|
||||||
/// - Verify JSON signatures
|
/// - Verify JSON signatures
|
||||||
///
|
///
|
||||||
/// This command needs a JSON blob provided in a Markdown code block below
|
/// This command needs a JSON blob provided in a Markdown code block below
|
||||||
|
|||||||
@@ -8,12 +8,14 @@ use crate::{admin_command, get_room_info};
|
|||||||
|
|
||||||
#[admin_command]
|
#[admin_command]
|
||||||
pub(super) async fn disable_room(&self, room_id: OwnedRoomId) -> Result {
|
pub(super) async fn disable_room(&self, room_id: OwnedRoomId) -> Result {
|
||||||
|
self.bail_restricted()?;
|
||||||
self.services.rooms.metadata.disable_room(&room_id, true);
|
self.services.rooms.metadata.disable_room(&room_id, true);
|
||||||
self.write_str("Room disabled.").await
|
self.write_str("Room disabled.").await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[admin_command]
|
#[admin_command]
|
||||||
pub(super) async fn enable_room(&self, room_id: OwnedRoomId) -> Result {
|
pub(super) async fn enable_room(&self, room_id: OwnedRoomId) -> Result {
|
||||||
|
self.bail_restricted()?;
|
||||||
self.services.rooms.metadata.disable_room(&room_id, false);
|
self.services.rooms.metadata.disable_room(&room_id, false);
|
||||||
self.write_str("Room enabled.").await
|
self.write_str("Room enabled.").await
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ pub(super) async fn delete(
|
|||||||
mxc: Option<OwnedMxcUri>,
|
mxc: Option<OwnedMxcUri>,
|
||||||
event_id: Option<OwnedEventId>,
|
event_id: Option<OwnedEventId>,
|
||||||
) -> Result {
|
) -> Result {
|
||||||
|
self.bail_restricted()?;
|
||||||
|
|
||||||
if event_id.is_some() && mxc.is_some() {
|
if event_id.is_some() && mxc.is_some() {
|
||||||
return Err!("Please specify either an MXC or an event ID, not both.",);
|
return Err!("Please specify either an MXC or an event ID, not both.",);
|
||||||
}
|
}
|
||||||
@@ -176,6 +178,8 @@ pub(super) async fn delete(
|
|||||||
|
|
||||||
#[admin_command]
|
#[admin_command]
|
||||||
pub(super) async fn delete_list(&self) -> Result {
|
pub(super) async fn delete_list(&self) -> Result {
|
||||||
|
self.bail_restricted()?;
|
||||||
|
|
||||||
if self.body.len() < 2
|
if self.body.len() < 2
|
||||||
|| !self.body[0].trim().starts_with("```")
|
|| !self.body[0].trim().starts_with("```")
|
||||||
|| self.body.last().unwrap_or(&"").trim() != "```"
|
|| self.body.last().unwrap_or(&"").trim() != "```"
|
||||||
@@ -231,6 +235,8 @@ pub(super) async fn delete_past_remote_media(
|
|||||||
after: bool,
|
after: bool,
|
||||||
yes_i_want_to_delete_local_media: bool,
|
yes_i_want_to_delete_local_media: bool,
|
||||||
) -> Result {
|
) -> Result {
|
||||||
|
self.bail_restricted()?;
|
||||||
|
|
||||||
if before && after {
|
if before && after {
|
||||||
return Err!("Please only pick one argument, --before or --after.",);
|
return Err!("Please only pick one argument, --before or --after.",);
|
||||||
}
|
}
|
||||||
@@ -273,6 +279,8 @@ pub(super) async fn delete_all_from_server(
|
|||||||
server_name: OwnedServerName,
|
server_name: OwnedServerName,
|
||||||
yes_i_want_to_delete_local_media: bool,
|
yes_i_want_to_delete_local_media: bool,
|
||||||
) -> Result {
|
) -> Result {
|
||||||
|
self.bail_restricted()?;
|
||||||
|
|
||||||
if server_name == self.services.globals.server_name() && !yes_i_want_to_delete_local_media {
|
if server_name == self.services.globals.server_name() && !yes_i_want_to_delete_local_media {
|
||||||
return Err!("This command only works for remote media by default.",);
|
return Err!("This command only works for remote media by default.",);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ async fn process_command(services: Arc<Services>, input: &CommandInput) -> Proce
|
|||||||
reply_id: input.reply_id.as_deref(),
|
reply_id: input.reply_id.as_deref(),
|
||||||
sender: input.sender.as_deref(),
|
sender: input.sender.as_deref(),
|
||||||
output: BufWriter::new(Vec::new()).into(),
|
output: BufWriter::new(Vec::new()).into(),
|
||||||
|
source: input.source,
|
||||||
};
|
};
|
||||||
|
|
||||||
let (result, mut logs) = process(&context, command, &args).await;
|
let (result, mut logs) = process(&context, command, &args).await;
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ pub(super) async fn uptime(&self) -> Result {
|
|||||||
|
|
||||||
#[admin_command]
|
#[admin_command]
|
||||||
pub(super) async fn show_config(&self) -> Result {
|
pub(super) async fn show_config(&self) -> Result {
|
||||||
|
self.bail_restricted()?;
|
||||||
|
|
||||||
self.write_str(&format!("{}", *self.services.server.config))
|
self.write_str(&format!("{}", *self.services.server.config))
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
@@ -118,6 +120,8 @@ pub(super) async fn list_backups(&self) -> Result {
|
|||||||
|
|
||||||
#[admin_command]
|
#[admin_command]
|
||||||
pub(super) async fn backup_database(&self) -> Result {
|
pub(super) async fn backup_database(&self) -> Result {
|
||||||
|
self.bail_restricted()?;
|
||||||
|
|
||||||
let db = Arc::clone(&self.services.db);
|
let db = Arc::clone(&self.services.db);
|
||||||
let result = self
|
let result = self
|
||||||
.services
|
.services
|
||||||
@@ -144,6 +148,8 @@ pub(super) async fn admin_notice(&self, message: Vec<String>) -> Result {
|
|||||||
|
|
||||||
#[admin_command]
|
#[admin_command]
|
||||||
pub(super) async fn reload_mods(&self) -> Result {
|
pub(super) async fn reload_mods(&self) -> Result {
|
||||||
|
self.bail_restricted()?;
|
||||||
|
|
||||||
self.services.server.reload()?;
|
self.services.server.reload()?;
|
||||||
|
|
||||||
self.write_str("Reloading server...").await
|
self.write_str("Reloading server...").await
|
||||||
@@ -168,6 +174,8 @@ pub(super) async fn restart(&self, force: bool) -> Result {
|
|||||||
|
|
||||||
#[admin_command]
|
#[admin_command]
|
||||||
pub(super) async fn shutdown(&self) -> Result {
|
pub(super) async fn shutdown(&self) -> Result {
|
||||||
|
self.bail_restricted()?;
|
||||||
|
|
||||||
warn!("shutdown command");
|
warn!("shutdown command");
|
||||||
self.services.server.shutdown()?;
|
self.services.server.shutdown()?;
|
||||||
|
|
||||||
|
|||||||
@@ -461,8 +461,10 @@ pub(super) async fn force_join_list_of_local_users(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let Ok(admin_room) = self.services.admin.get_admin_room().await else {
|
let server_admins = self.services.admin.get_admins().await;
|
||||||
return Err!("There is not an admin room to check for server admins.",);
|
|
||||||
|
if server_admins.is_empty() {
|
||||||
|
return Err!("There are no admins set for this server.");
|
||||||
};
|
};
|
||||||
|
|
||||||
let (room_id, servers) = self
|
let (room_id, servers) = self
|
||||||
@@ -482,15 +484,6 @@ pub(super) async fn force_join_list_of_local_users(
|
|||||||
return Err!("We are not joined in this room.");
|
return Err!("We are not joined in this room.");
|
||||||
}
|
}
|
||||||
|
|
||||||
let server_admins: Vec<_> = self
|
|
||||||
.services
|
|
||||||
.rooms
|
|
||||||
.state_cache
|
|
||||||
.active_local_users_in_room(&admin_room)
|
|
||||||
.map(ToOwned::to_owned)
|
|
||||||
.collect()
|
|
||||||
.await;
|
|
||||||
|
|
||||||
if !self
|
if !self
|
||||||
.services
|
.services
|
||||||
.rooms
|
.rooms
|
||||||
@@ -583,8 +576,10 @@ pub(super) async fn force_join_all_local_users(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let Ok(admin_room) = self.services.admin.get_admin_room().await else {
|
let server_admins = self.services.admin.get_admins().await;
|
||||||
return Err!("There is not an admin room to check for server admins.",);
|
|
||||||
|
if server_admins.is_empty() {
|
||||||
|
return Err!("There are no admins set for this server.");
|
||||||
};
|
};
|
||||||
|
|
||||||
let (room_id, servers) = self
|
let (room_id, servers) = self
|
||||||
@@ -604,15 +599,6 @@ pub(super) async fn force_join_all_local_users(
|
|||||||
return Err!("We are not joined in this room.");
|
return Err!("We are not joined in this room.");
|
||||||
}
|
}
|
||||||
|
|
||||||
let server_admins: Vec<_> = self
|
|
||||||
.services
|
|
||||||
.rooms
|
|
||||||
.state_cache
|
|
||||||
.active_local_users_in_room(&admin_room)
|
|
||||||
.map(ToOwned::to_owned)
|
|
||||||
.collect()
|
|
||||||
.await;
|
|
||||||
|
|
||||||
if !self
|
if !self
|
||||||
.services
|
.services
|
||||||
.rooms
|
.rooms
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
use axum::{Json, extract::State, response::IntoResponse};
|
use axum::{Json, extract::State, response::IntoResponse};
|
||||||
use conduwuit::{Error, Result};
|
use conduwuit::{Error, Result};
|
||||||
use futures::StreamExt;
|
|
||||||
use ruma::api::client::{
|
use ruma::api::client::{
|
||||||
discovery::{
|
discovery::{
|
||||||
discover_homeserver::{self, HomeserverInfo, SlidingSyncProxyInfo},
|
discover_homeserver::{self, HomeserverInfo, SlidingSyncProxyInfo},
|
||||||
@@ -71,21 +70,18 @@ pub(crate) async fn well_known_support(
|
|||||||
|
|
||||||
// Try to add admin users as contacts if no contacts are configured
|
// Try to add admin users as contacts if no contacts are configured
|
||||||
if contacts.is_empty() {
|
if contacts.is_empty() {
|
||||||
if let Ok(admin_room) = services.admin.get_admin_room().await {
|
let admin_users = services.admin.get_admins().await;
|
||||||
let admin_users = services.rooms.state_cache.room_members(&admin_room);
|
|
||||||
let mut stream = admin_users;
|
|
||||||
|
|
||||||
while let Some(user_id) = stream.next().await {
|
for user_id in admin_users.iter() {
|
||||||
// Skip server user
|
if *user_id == services.globals.server_user {
|
||||||
if *user_id == services.globals.server_user {
|
continue;
|
||||||
continue;
|
|
||||||
}
|
|
||||||
contacts.push(Contact {
|
|
||||||
role: role_value.clone(),
|
|
||||||
email_address: None,
|
|
||||||
matrix_id: Some(user_id.to_owned()),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
contacts.push(Contact {
|
||||||
|
role: role_value.clone(),
|
||||||
|
email_address: None,
|
||||||
|
matrix_id: Some(user_id.to_owned()),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1819,6 +1819,22 @@ pub struct Config {
|
|||||||
#[serde(default = "default_admin_room_tag")]
|
#[serde(default = "default_admin_room_tag")]
|
||||||
pub admin_room_tag: String,
|
pub admin_room_tag: String,
|
||||||
|
|
||||||
|
/// A list of Matrix IDs that are qualified as server admins.
|
||||||
|
///
|
||||||
|
/// Any Matrix IDs within this list are regarded as an admin
|
||||||
|
/// regardless of whether they are in the admin room or not
|
||||||
|
///
|
||||||
|
/// default: []
|
||||||
|
#[serde(default)]
|
||||||
|
pub admins_list: Vec<OwnedUserId>,
|
||||||
|
|
||||||
|
/// Defines whether those within the admin room are added to the
|
||||||
|
/// admins_list.
|
||||||
|
///
|
||||||
|
/// default: true
|
||||||
|
#[serde(default = "true_fn")]
|
||||||
|
pub admins_from_room: bool,
|
||||||
|
|
||||||
/// Sentry.io crash/panic reporting, performance monitoring/metrics, etc.
|
/// Sentry.io crash/panic reporting, performance monitoring/metrics, etc.
|
||||||
/// This is NOT enabled by default.
|
/// This is NOT enabled by default.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
|||||||
@@ -66,7 +66,10 @@ pub(crate) fn build(services: &Arc<Services>) -> Result<(Router, Guard)> {
|
|||||||
.layer(RequestBodyTimeoutLayer::new(Duration::from_secs(
|
.layer(RequestBodyTimeoutLayer::new(Duration::from_secs(
|
||||||
server.config.client_receive_timeout,
|
server.config.client_receive_timeout,
|
||||||
)))
|
)))
|
||||||
.layer(TimeoutLayer::with_status_code(StatusCode::REQUEST_TIMEOUT, Duration::from_secs(server.config.client_request_timeout)))
|
.layer(TimeoutLayer::with_status_code(
|
||||||
|
StatusCode::REQUEST_TIMEOUT,
|
||||||
|
Duration::from_secs(server.config.client_request_timeout),
|
||||||
|
))
|
||||||
.layer(SetResponseHeaderLayer::if_not_present(
|
.layer(SetResponseHeaderLayer::if_not_present(
|
||||||
HeaderName::from_static("origin-agent-cluster"), // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Origin-Agent-Cluster
|
HeaderName::from_static("origin-agent-cluster"), // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Origin-Agent-Cluster
|
||||||
HeaderValue::from_static("?1"),
|
HeaderValue::from_static("?1"),
|
||||||
|
|||||||
@@ -9,7 +9,10 @@ use rustyline_async::{Readline, ReadlineError, ReadlineEvent};
|
|||||||
use termimad::MadSkin;
|
use termimad::MadSkin;
|
||||||
use tokio::task::JoinHandle;
|
use tokio::task::JoinHandle;
|
||||||
|
|
||||||
use crate::{Dep, admin};
|
use crate::{
|
||||||
|
Dep,
|
||||||
|
admin::{self, InvocationSource},
|
||||||
|
};
|
||||||
|
|
||||||
pub struct Console {
|
pub struct Console {
|
||||||
server: Arc<Server>,
|
server: Arc<Server>,
|
||||||
@@ -160,7 +163,11 @@ impl Console {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn process(self: Arc<Self>, line: String) {
|
async fn process(self: Arc<Self>, line: String) {
|
||||||
match self.admin.command_in_place(line, None).await {
|
match self
|
||||||
|
.admin
|
||||||
|
.command_in_place(line, None, InvocationSource::Console)
|
||||||
|
.await
|
||||||
|
{
|
||||||
| Ok(Some(ref content)) => self.output(content),
|
| Ok(Some(ref content)) => self.output(content),
|
||||||
| Err(ref content) => self.output_err(content),
|
| Err(ref content) => self.output_err(content),
|
||||||
| _ => unreachable!(),
|
| _ => unreachable!(),
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ use conduwuit::{Err, Result, debug, debug_info, error, implement, info};
|
|||||||
use ruma::events::room::message::RoomMessageEventContent;
|
use ruma::events::room::message::RoomMessageEventContent;
|
||||||
use tokio::time::{Duration, sleep};
|
use tokio::time::{Duration, sleep};
|
||||||
|
|
||||||
|
use crate::admin::InvocationSource;
|
||||||
|
|
||||||
pub(super) const SIGNAL: &str = "SIGUSR2";
|
pub(super) const SIGNAL: &str = "SIGUSR2";
|
||||||
|
|
||||||
/// Possibly spawn the terminal console at startup if configured.
|
/// Possibly spawn the terminal console at startup if configured.
|
||||||
@@ -88,7 +90,10 @@ pub(super) async fn signal_execute(&self) -> Result {
|
|||||||
async fn execute_command(&self, i: usize, command: String) -> Result {
|
async fn execute_command(&self, i: usize, command: String) -> Result {
|
||||||
debug!("Execute command #{i}: executing {command:?}");
|
debug!("Execute command #{i}: executing {command:?}");
|
||||||
|
|
||||||
match self.command_in_place(command, None).await {
|
match self
|
||||||
|
.command_in_place(command, None, InvocationSource::Console)
|
||||||
|
.await
|
||||||
|
{
|
||||||
| Ok(Some(output)) => Self::execute_command_output(i, &output),
|
| Ok(Some(output)) => Self::execute_command_output(i, &output),
|
||||||
| Err(output) => Self::execute_command_error(i, &output),
|
| Err(output) => Self::execute_command_error(i, &output),
|
||||||
| Ok(None) => {
|
| Ok(None) => {
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
use conduwuit::{Err, Result, debug_info, debug_warn, error, implement, matrix::pdu::PduBuilder};
|
use conduwuit::{
|
||||||
|
Err, Result, debug_info, debug_warn, error, implement, matrix::pdu::PduBuilder, warn,
|
||||||
|
};
|
||||||
use ruma::{
|
use ruma::{
|
||||||
RoomId, UserId,
|
RoomId, UserId,
|
||||||
events::{
|
events::{
|
||||||
@@ -176,6 +178,19 @@ async fn set_room_tag(&self, room_id: &RoomId, user_id: &UserId, tag: &str) -> R
|
|||||||
pub async fn revoke_admin(&self, user_id: &UserId) -> Result {
|
pub async fn revoke_admin(&self, user_id: &UserId) -> Result {
|
||||||
use MembershipState::{Invite, Join, Knock, Leave};
|
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 {
|
let Ok(room_id) = self.get_admin_room().await else {
|
||||||
return Err!(error!("No admin room available or created."));
|
return Err!(error!("No admin room available or created."));
|
||||||
};
|
};
|
||||||
|
|||||||
+122
-61
@@ -14,10 +14,10 @@ use conduwuit_core::{
|
|||||||
Error, Event, Result, Server, debug, err, error, error::default_log, pdu::PduBuilder,
|
Error, Event, Result, Server, debug, err, error, error::default_log, pdu::PduBuilder,
|
||||||
};
|
};
|
||||||
pub use create::create_admin_room;
|
pub use create::create_admin_room;
|
||||||
use futures::{Future, FutureExt, TryFutureExt};
|
use futures::{Future, FutureExt, StreamExt, TryFutureExt};
|
||||||
use loole::{Receiver, Sender};
|
use loole::{Receiver, Sender};
|
||||||
use ruma::{
|
use ruma::{
|
||||||
Mxc, OwnedEventId, OwnedMxcUri, OwnedRoomId, RoomId, UInt, UserId,
|
Mxc, OwnedEventId, OwnedMxcUri, OwnedRoomId, OwnedUserId, RoomId, UInt, UserId,
|
||||||
events::{
|
events::{
|
||||||
Mentions,
|
Mentions,
|
||||||
room::{
|
room::{
|
||||||
@@ -54,15 +54,37 @@ struct Services {
|
|||||||
media: Dep<crate::media::Service>,
|
media: Dep<crate::media::Service>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Inputs to a command are a multi-line string, optional reply_id, and optional
|
/// Inputs to a command are a multi-line string, invocation source, optional
|
||||||
/// sender.
|
/// reply_id, and optional sender.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct CommandInput {
|
pub struct CommandInput {
|
||||||
pub command: String,
|
pub command: String,
|
||||||
pub reply_id: Option<OwnedEventId>,
|
pub reply_id: Option<OwnedEventId>,
|
||||||
|
pub source: InvocationSource,
|
||||||
pub sender: Option<Box<UserId>>,
|
pub sender: Option<Box<UserId>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Where a command is being invoked from.
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub enum InvocationSource {
|
||||||
|
/// The server's private admin room
|
||||||
|
AdminRoom,
|
||||||
|
/// An escaped `\!admin` command in a public room
|
||||||
|
EscapedCommand,
|
||||||
|
/// The server's admin console
|
||||||
|
Console,
|
||||||
|
/// Some other trusted internal source
|
||||||
|
Internal,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InvocationSource {
|
||||||
|
/// Returns whether this invocation source allows "restricted"
|
||||||
|
/// commands, i.e. ones that could be potentially dangerous if executed by
|
||||||
|
/// an attacker or in a public room.
|
||||||
|
#[must_use]
|
||||||
|
pub fn allows_restricted(&self) -> bool { !matches!(self, Self::EscapedCommand) }
|
||||||
|
}
|
||||||
|
|
||||||
/// Prototype of the tab-completer. The input is buffered text when tab
|
/// Prototype of the tab-completer. The input is buffered text when tab
|
||||||
/// asserted; the output will fully replace the input buffer.
|
/// asserted; the output will fully replace the input buffer.
|
||||||
pub type Completer = fn(&str) -> String;
|
pub type Completer = fn(&str) -> String;
|
||||||
@@ -276,10 +298,15 @@ impl Service {
|
|||||||
/// Posts a command to the command processor queue and returns. Processing
|
/// Posts a command to the command processor queue and returns. Processing
|
||||||
/// will take place on the service worker's task asynchronously. Errors if
|
/// will take place on the service worker's task asynchronously. Errors if
|
||||||
/// the queue is full.
|
/// the queue is full.
|
||||||
pub fn command(&self, command: String, reply_id: Option<OwnedEventId>) -> Result<()> {
|
pub fn command(
|
||||||
|
&self,
|
||||||
|
command: String,
|
||||||
|
reply_id: Option<OwnedEventId>,
|
||||||
|
source: InvocationSource,
|
||||||
|
) -> Result<()> {
|
||||||
self.channel
|
self.channel
|
||||||
.0
|
.0
|
||||||
.send(CommandInput { command, reply_id, sender: None })
|
.send(CommandInput { command, reply_id, source, sender: None })
|
||||||
.map_err(|e| err!("Failed to enqueue admin command: {e:?}"))
|
.map_err(|e| err!("Failed to enqueue admin command: {e:?}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -290,11 +317,17 @@ impl Service {
|
|||||||
&self,
|
&self,
|
||||||
command: String,
|
command: String,
|
||||||
reply_id: Option<OwnedEventId>,
|
reply_id: Option<OwnedEventId>,
|
||||||
|
source: InvocationSource,
|
||||||
sender: Box<UserId>,
|
sender: Box<UserId>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
self.channel
|
self.channel
|
||||||
.0
|
.0
|
||||||
.send(CommandInput { command, reply_id, sender: Some(sender) })
|
.send(CommandInput {
|
||||||
|
command,
|
||||||
|
reply_id,
|
||||||
|
source,
|
||||||
|
sender: Some(sender),
|
||||||
|
})
|
||||||
.map_err(|e| err!("Failed to enqueue admin command: {e:?}"))
|
.map_err(|e| err!("Failed to enqueue admin command: {e:?}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -304,8 +337,9 @@ impl Service {
|
|||||||
&self,
|
&self,
|
||||||
command: String,
|
command: String,
|
||||||
reply_id: Option<OwnedEventId>,
|
reply_id: Option<OwnedEventId>,
|
||||||
|
source: InvocationSource,
|
||||||
) -> ProcessorResult {
|
) -> ProcessorResult {
|
||||||
self.process_command(CommandInput { command, reply_id, sender: None })
|
self.process_command(CommandInput { command, reply_id, source, sender: None })
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -349,16 +383,43 @@ impl Service {
|
|||||||
handle(services, command).await
|
handle(services, command).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the list of admins for this server. First loads
|
||||||
|
/// the admin_list from the configuration, then adds users from
|
||||||
|
/// the admin room if applicable.
|
||||||
|
pub async fn get_admins(&self) -> Vec<OwnedUserId> {
|
||||||
|
let mut generated_admin_list: Vec<OwnedUserId> =
|
||||||
|
self.services.server.config.admins_list.clone();
|
||||||
|
|
||||||
|
if self.services.server.config.admins_from_room {
|
||||||
|
if let Ok(admin_room) = self.get_admin_room().await {
|
||||||
|
let admin_users = self.services.state_cache.room_members(&admin_room);
|
||||||
|
let mut stream = admin_users;
|
||||||
|
|
||||||
|
while let Some(user_id) = stream.next().await {
|
||||||
|
generated_admin_list.push(user_id.to_owned());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
generated_admin_list
|
||||||
|
}
|
||||||
|
|
||||||
/// Checks whether a given user is an admin of this server
|
/// Checks whether a given user is an admin of this server
|
||||||
pub async fn user_is_admin(&self, user_id: &UserId) -> bool {
|
pub async fn user_is_admin(&self, user_id: &UserId) -> bool {
|
||||||
let Ok(admin_room) = self.get_admin_room().await else {
|
if self.services.server.config.admins_list.contains(user_id) {
|
||||||
return false;
|
return true;
|
||||||
};
|
}
|
||||||
|
|
||||||
self.services
|
if self.services.server.config.admins_from_room {
|
||||||
.state_cache
|
if let Ok(admin_room) = self.get_admin_room().await {
|
||||||
.is_joined(user_id, &admin_room)
|
self.services
|
||||||
.await
|
.state_cache
|
||||||
|
.is_joined(user_id, &admin_room)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Gets the room ID of the admin room
|
/// Gets the room ID of the admin room
|
||||||
@@ -459,59 +520,59 @@ impl Service {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn is_admin_command<E>(&self, event: &E, body: &str) -> bool
|
pub async fn is_admin_command<E>(&self, event: &E, body: &str) -> Option<InvocationSource>
|
||||||
where
|
where
|
||||||
E: Event + Send + Sync,
|
E: Event + Send + Sync,
|
||||||
{
|
{
|
||||||
// Server-side command-escape with public echo
|
// If the user isn't an admin they definitely can't run admin commands
|
||||||
let is_escape = body.starts_with('\\');
|
|
||||||
let is_public_escape = is_escape && body.trim_start_matches('\\').starts_with("!admin");
|
|
||||||
|
|
||||||
// Admin command with public echo (in admin room)
|
|
||||||
let server_user = &self.services.globals.server_user;
|
|
||||||
let is_public_prefix =
|
|
||||||
body.starts_with("!admin") || body.starts_with(server_user.as_str());
|
|
||||||
|
|
||||||
// Expected backward branch
|
|
||||||
if !is_public_escape && !is_public_prefix {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
let user_is_local = self.services.globals.user_is_local(event.sender());
|
|
||||||
|
|
||||||
// only allow public escaped commands by local admins
|
|
||||||
if is_public_escape && !user_is_local {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if server-side command-escape is disabled by configuration
|
|
||||||
if is_public_escape && !self.services.server.config.admin_escape_commands {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prevent unescaped !admin from being used outside of the admin room
|
|
||||||
if event.room_id().is_some()
|
|
||||||
&& is_public_prefix
|
|
||||||
&& !self.is_admin_room(event.room_id().unwrap()).await
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only senders who are admin can proceed
|
|
||||||
if !self.user_is_admin(event.sender()).await {
|
if !self.user_is_admin(event.sender()).await {
|
||||||
return false;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
// This will evaluate to false if the emergency password is set up so that
|
if let Some(room_id) = event.room_id()
|
||||||
// the administrator can execute commands as the server user
|
&& self.is_admin_room(room_id).await
|
||||||
let emergency_password_set = self.services.server.config.emergency_password.is_some();
|
{
|
||||||
let from_server = event.sender() == server_user && !emergency_password_set;
|
// This is a message in the admin room
|
||||||
if from_server && self.is_admin_room(event.room_id().unwrap()).await {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Authentic admin command
|
// Ignore messages which aren't admin commands
|
||||||
true
|
let server_user = &self.services.globals.server_user;
|
||||||
|
if !(body.starts_with("!admin") || body.starts_with(server_user.as_str())) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ignore messages from the server user _unless_ the emergency password is set
|
||||||
|
let emergency_password_set = self.services.server.config.emergency_password.is_some();
|
||||||
|
if event.sender() == server_user && !emergency_password_set {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Looks good
|
||||||
|
Some(InvocationSource::AdminRoom)
|
||||||
|
} else {
|
||||||
|
// This is a message outside the admin room
|
||||||
|
|
||||||
|
// Is it an escaped admin command? i.e. `\!admin --help`
|
||||||
|
let is_public_escape =
|
||||||
|
body.starts_with('\\') && body.trim_start_matches('\\').starts_with("!admin");
|
||||||
|
|
||||||
|
// Ignore the message if it's not
|
||||||
|
if !is_public_escape {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only admin users belonging to this server can use escaped commands
|
||||||
|
if !self.services.globals.user_is_local(event.sender()) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if escaped commands are disabled in the config
|
||||||
|
if !self.services.server.config.admin_escape_commands {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Looks good
|
||||||
|
Some(InvocationSource::EscapedCommand)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
|
|||||||
@@ -335,10 +335,11 @@ where
|
|||||||
if let Some(body) = content.body {
|
if let Some(body) = content.body {
|
||||||
self.services.search.index_pdu(shortroomid, &pdu_id, &body);
|
self.services.search.index_pdu(shortroomid, &pdu_id, &body);
|
||||||
|
|
||||||
if self.services.admin.is_admin_command(pdu, &body).await {
|
if let Some(source) = self.services.admin.is_admin_command(pdu, &body).await {
|
||||||
self.services.admin.command_with_sender(
|
self.services.admin.command_with_sender(
|
||||||
body,
|
body,
|
||||||
Some((pdu.event_id()).into()),
|
Some((pdu.event_id()).into()),
|
||||||
|
source,
|
||||||
pdu.sender.clone().into(),
|
pdu.sender.clone().into(),
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user