diff --git a/src/config.rs b/src/config.rs index c6bc0a4..bd351cf 100644 --- a/src/config.rs +++ b/src/config.rs @@ -70,7 +70,7 @@ pub struct Config { pub tls_cert: Option, // PEM certificate chain pub tls_key: Option, // PEM private key pub motd: Vec, - pub opers: Vec<(String, String)>, // (name, password) + pub opers: Vec<(String, String, u32)>, // (name, password, operlevel) pub cloak_key: Option, // secret key for host cloaking (+x); None = off pub sid: String, // this server's 3-char server id (S2S) pub serverdesc: String, // this server's description @@ -197,7 +197,8 @@ impl Config { "oper" => { let mut it = v.split_whitespace(); if let (Some(n), Some(p)) = (it.next(), it.next()) { - c.opers.push((n.to_string(), p.to_string())); + let level = it.next().and_then(|l| l.parse().ok()).unwrap_or(0); + c.opers.push((n.to_string(), p.to_string(), level)); } } // +G censor word: `badword = [replace]` (no replace ⇒ block) diff --git a/src/coremods/core_extra.rs b/src/coremods/core_extra.rs index f159460..9dd359b 100644 --- a/src/coremods/core_extra.rs +++ b/src/coremods/core_extra.rs @@ -238,7 +238,7 @@ impl Command for Stats { ); } 'o' => { - let opers: Vec = s.opers.iter().map(|(n, _)| n.clone()).collect(); + let opers: Vec = s.opers.iter().map(|(n, _, _)| n.clone()).collect(); for n in opers { s.numeric(uid, RPL_STATSOLINE, &format!("O * * {n} :oper")); } diff --git a/src/coremods/core_oper.rs b/src/coremods/core_oper.rs index bad2c25..195f848 100644 --- a/src/coremods/core_oper.rs +++ b/src/coremods/core_oper.rs @@ -122,11 +122,14 @@ impl Command for Oper { } fn handle(&self, s: &mut Server, uid: Uid, params: &[String]) -> CmdResult { let (name, pass) = (¶ms[0], ¶ms[1]); - if s.opers + let level = s + .opers .iter() - .any(|(n, p)| n == name && crate::modules::password_hash::verify(p, pass)) - { + .find(|(n, p, _)| n == name && crate::modules::password_hash::verify(p, pass)) + .map(|(_, _, lvl)| *lvl); + if let Some(level) = level { s.oper_up(uid); + crate::modules::operlevels::set(s, uid, level); // operlevels: KILL protection CmdResult::Ok } else { s.numeric(uid, ERR_PASSWDMISMATCH, ":Password incorrect"); @@ -165,6 +168,11 @@ impl Command for Kill { ); return CmdResult::Fail; }; + // operlevels: a lower-level oper can't KILL a higher-level oper + if let Some(reason) = crate::modules::operlevels::deny_kill(s, uid, tuid) { + s.numeric(uid, ERR_NOPRIVILEGES, &format!(":{reason}")); + return CmdResult::Fail; + } let killer = s .users .get(&uid) diff --git a/src/modules/mod.rs b/src/modules/mod.rs index b3247bc..fcf5bf6 100644 --- a/src/modules/mod.rs +++ b/src/modules/mod.rs @@ -48,6 +48,7 @@ pub mod metadata; pub mod multiline; pub mod network_icon; pub mod ojoin; +pub mod operlevels; pub mod operprefix; pub mod password_hash; pub mod profilelink; diff --git a/src/modules/operlevels.rs b/src/modules/operlevels.rs new file mode 100644 index 0000000..526cd83 --- /dev/null +++ b/src/modules/operlevels.rs @@ -0,0 +1,46 @@ +//! operlevels — each `oper` block may carry a numeric level (`oper = +//! `, default 0). A lower-level oper cannot KILL a higher-level oper. The +//! level is stored on the user at OPER; with all levels at the default it's inert, +//! so no config flag is needed. + +use crate::server::Server; +use crate::Uid; + +/// The oper level stored on a user's `ext`. +struct OperLevel(u32); + +/// Record `uid`'s oper level (called from the OPER handler after oper-up). +pub fn set(s: &mut Server, uid: Uid, level: u32) { + if let Some(u) = s.users.get_mut(&uid) { + *u.ext.get_or_insert_with(|| OperLevel(0)) = OperLevel(level); + } +} + +/// A user's oper level (0 if unset / not an oper). +pub fn level(s: &Server, uid: Uid) -> u32 { + s.users + .get(&uid) + .and_then(|u| u.ext.get::()) + .map(|l| l.0) + .unwrap_or(0) +} + +/// The reason `actor` may not KILL `target` under operlevels, if any: only when the +/// target is an oper of a strictly higher level than the actor. +pub fn deny_kill(s: &Server, actor: Uid, target: Uid) -> Option { + if !s.is_oper(target) { + return None; // non-opers aren't protected + } + if level(s, actor) < level(s, target) { + let tnick = s + .users + .get(&target) + .map(|u| u.nick.clone()) + .unwrap_or_default(); + Some(format!( + "Permission Denied- {tnick} outranks you (higher oper level)" + )) + } else { + None + } +} diff --git a/src/modules/rpc/stats.rs b/src/modules/rpc/stats.rs index 2ea4555..85b1504 100644 --- a/src/modules/rpc/stats.rs +++ b/src/modules/rpc/stats.rs @@ -25,7 +25,7 @@ pub fn handle(s: &mut Server, method: &str, _params: &str) -> Result = s .opers .iter() - .map(|(name, _pass)| { + .map(|(name, _pass, _lvl)| { obj(&[ ("name", qstr(name)), ("type", qstr("")), diff --git a/src/server.rs b/src/server.rs index c771e83..a0d0b93 100644 --- a/src/server.rs +++ b/src/server.rs @@ -118,7 +118,7 @@ pub struct Server { pub nick_index: HashMap, // lower nick -> uid pub channels: HashMap, // lower name -> channel pub events: VecDeque, - pub opers: Vec<(String, String)>, // (name, password) from config + pub opers: Vec<(String, String, u32)>, // (name, password, operlevel) from config pub cloak_key: Option, // host-cloaking key (see modules::cloak) pub line_ctags: String, // client-only tags of the line being handled // --- server-to-server (see crate::link) ---