tuwunel_service/admin/
mod.rs1mod attach;
2pub mod console;
3pub mod context;
4pub mod create;
5mod execute;
6mod grant;
7mod notices;
8mod processor;
9mod register;
10mod respond;
11
12use std::{
13 collections::BTreeMap,
14 sync::{Arc, Mutex as StdMutex, RwLock as StdRwLock},
15 time::Instant,
16};
17
18use async_trait::async_trait;
19pub use context::Context;
20pub use create::create_admin_room;
21use futures::TryFutureExt;
22use ruma::{OwnedEventId, OwnedRoomAliasId, OwnedRoomId, RoomId, RoomOrAliasId, UserId};
23use tokio::sync::mpsc;
24use tuwunel_core::{Err, Event, Result, debug, err, error::default_log, warn};
25
26pub struct Service {
27 services: Arc<crate::services::OnceServices>,
28 channel: StdRwLock<Option<mpsc::Sender<CommandInput>>>,
29 pub command: StdRwLock<Option<Arc<dyn Command>>>,
30 pub admin_alias: OwnedRoomAliasId,
31 register_nonces: StdMutex<BTreeMap<String, Instant>>,
32 #[cfg(feature = "console")]
33 pub console: Arc<console::Console>,
34}
35
36#[derive(Clone, Debug, Default)]
38pub struct CommandInput {
39 pub command: String,
40 pub reply_id: Option<OwnedEventId>,
41}
42
43#[async_trait]
45pub trait Command: Send + Sync + 'static {
46 fn clap(&self) -> clap::Command;
49
50 async fn dispatch(&self, matches: clap::ArgMatches, context: &Context<'_>) -> Result;
52}
53
54pub type ProcessorResult = Result<Option<CommandOutput>, CommandOutput>;
59
60pub enum CommandOutput {
63 Markdown(String),
64 Plain(String),
65}
66
67impl CommandOutput {
68 #[inline]
69 #[must_use]
70 pub fn as_str(&self) -> &str {
71 match self {
72 | Self::Markdown(text) | Self::Plain(text) => text,
73 }
74 }
75}
76
77const COMMAND_QUEUE_LIMIT: usize = 512;
79
80#[async_trait]
81impl crate::Service for Service {
82 fn build(args: &crate::Args<'_>) -> Result<Arc<Self>> {
83 Ok(Arc::new(Self {
84 services: args.services.clone(),
85 channel: StdRwLock::new(None),
86 command: StdRwLock::new(None),
87 admin_alias: OwnedRoomAliasId::try_from(format!("#admins:{}", args.server.name))
88 .expect("#admins:server_name is valid alias name"),
89 register_nonces: StdMutex::new(BTreeMap::new()),
90 #[cfg(feature = "console")]
91 console: console::Console::new(args),
92 }))
93 }
94
95 async fn worker(self: Arc<Self>) -> Result {
96 let mut signals = self.services.server.signal.subscribe();
97 let (sender, mut receiver) = mpsc::channel(COMMAND_QUEUE_LIMIT);
98 _ = self
99 .channel
100 .write()
101 .expect("locked for writing")
102 .insert(sender);
103
104 self.console_auto_start().await;
105
106 loop {
107 tokio::select! {
108 command = receiver.recv() => match command {
109 Some(command) => self.handle_command(command).await,
110 None => break,
111 },
112 sig = signals.recv() => if let Ok(sig) = sig {
113 self.handle_signal(sig).await;
114 },
115 }
116 }
117
118 self.interrupt().await;
120 self.console_auto_stop().await;
121
122 Ok(())
123 }
124
125 async fn interrupt(&self) {
126 #[cfg(feature = "console")]
127 self.console.interrupt();
128
129 _ = self
130 .channel
131 .write()
132 .expect("locked for writing")
133 .take();
134 }
135
136 fn name(&self) -> &str { crate::service::make_name(std::module_path!()) }
137}
138
139impl Service {
140 pub async fn command(&self, command: String, reply_id: Option<OwnedEventId>) -> Result {
144 let Some(sender) = self
145 .channel
146 .read()
147 .expect("locked for reading")
148 .clone()
149 else {
150 return Err!("Admin command queue unavailable.");
151 };
152
153 sender
154 .send(CommandInput { command, reply_id })
155 .await
156 .map_err(|e| err!("Failed to enqueue admin command: {e:?}"))
157 }
158
159 pub async fn command_in_place(
162 &self,
163 command: String,
164 reply_id: Option<OwnedEventId>,
165 ) -> ProcessorResult {
166 self.process_command(&CommandInput { command, reply_id })
167 .await
168 }
169
170 pub fn complete_command(&self, command: &str) -> Option<String> {
173 self.command
174 .read()
175 .expect("locked for reading")
176 .as_ref()
177 .map(|root| processor::complete(root.clap(), command))
178 }
179
180 async fn handle_signal(&self, sig: &'static str) {
181 if sig == execute::SIGNAL {
182 self.signal_execute().await.ok();
183 }
184
185 #[cfg(feature = "console")]
186 self.console.handle_signal(sig);
187 }
188
189 async fn handle_command(&self, command: CommandInput) {
190 match self.process_command(&command).await {
191 | Ok(None) => debug!("Command successful with no response"),
192 | Err(output) | Ok(Some(output)) => self
193 .handle_response(output, command.reply_id.as_deref())
194 .await
195 .unwrap_or_else(default_log),
196 }
197 }
198
199 async fn process_command(&self, command: &CommandInput) -> ProcessorResult {
200 let root = self
201 .command
202 .read()
203 .expect("locked for reading")
204 .clone()
205 .expect("Admin module is not loaded");
206
207 processor::handle_command(root, Arc::clone(self.services.get()), command).await
208 }
209
210 pub async fn user_is_admin(&self, user_id: &UserId) -> bool {
212 if user_id == self.services.globals.server_user {
213 return true;
214 }
215
216 let Ok(admin_room) = self.get_admin_room().await else {
217 return false;
218 };
219
220 self.services
221 .state_cache
222 .is_joined(user_id, &admin_room)
223 .await
224 }
225
226 pub async fn get_admin_room(&self) -> Result<OwnedRoomId> {
231 let room_id = self
232 .services
233 .alias
234 .resolve_local_alias(&self.admin_alias)
235 .await?;
236
237 self.services
238 .state_cache
239 .is_joined(&self.services.globals.server_user, &room_id)
240 .await
241 .then_some(room_id)
242 .ok_or_else(|| err!(Request(NotFound("Admin user not joined to admin room"))))
243 }
244
245 pub async fn get_report_room(&self) -> Result<OwnedRoomId> {
248 let Some(report_room) = self.services.server.config.report_room.as_ref() else {
249 return self.get_admin_room().await;
250 };
251
252 match self.resolve_report_room(report_room).await {
253 | Ok(room_id) => Ok(room_id),
254 | Err(e) => {
255 warn!(%report_room, error = %e, "Falling back to the admin room for reports");
256 self.get_admin_room().await
257 },
258 }
259 }
260
261 async fn resolve_report_room(&self, report_room: &RoomOrAliasId) -> Result<OwnedRoomId> {
262 let room_id = self
263 .services
264 .alias
265 .maybe_resolve(report_room)
266 .await?;
267
268 self.services
269 .state_cache
270 .is_joined(&self.services.globals.server_user, &room_id)
271 .await
272 .then_some(room_id)
273 .ok_or_else(|| err!("server user is not joined to the configured report room"))
274 }
275
276 pub async fn is_admin_command<Pdu>(&self, event: &Pdu, body: &str) -> bool
277 where
278 Pdu: Event,
279 {
280 let is_escape = body.starts_with('\\');
282 let is_public_escape = is_escape
283 && body
284 .trim_start_matches('\\')
285 .starts_with("!admin");
286
287 let server_user = &self.services.globals.server_user;
289 let is_public_prefix =
290 body.starts_with("!admin") || body.starts_with(server_user.as_str());
291
292 if !is_public_escape && !is_public_prefix {
294 return false;
295 }
296
297 let user_is_local = self
298 .services
299 .globals
300 .user_is_local(event.sender());
301
302 if is_public_escape && !user_is_local {
304 return false;
305 }
306
307 if is_public_escape && !self.services.server.config.admin_escape_commands {
309 return false;
310 }
311
312 if is_public_prefix && !self.is_admin_room(event.room_id()).await {
314 return false;
315 }
316
317 if !self.user_is_admin(event.sender()).await {
319 return false;
320 }
321
322 let emergency_password_set = self
325 .services
326 .server
327 .config
328 .emergency_password
329 .is_some();
330 let from_server = event.sender() == server_user && !emergency_password_set;
331 if from_server && self.is_admin_room(event.room_id()).await {
332 return false;
333 }
334
335 true
337 }
338
339 #[must_use]
340 pub async fn is_admin_room(&self, room_id_: &RoomId) -> bool {
341 self.get_admin_room()
342 .map_ok(|room_id| room_id == room_id_)
343 .await
344 .unwrap_or(false)
345 }
346}