diff --git a/docs/operators.md b/docs/operators.md index 82a1403..b2f064c 100644 --- a/docs/operators.md +++ b/docs/operators.md @@ -86,8 +86,22 @@ command. Assign them via `privs=` on a class or type (`*` = all, `-x` removes on | `channels/auspex` | secret/private (`+s`/`+p`) channels and their members in `/LIST`, `/WHO`, `/WHOIS`, `/NAMES` | | `servers/auspex` | U-lined/services servers otherwise hidden by `hideservices` in `/MAP` & `/LINKS` | | `channels/override` | join through `+k`/`+b`/`+i`/`+l`/`+z`/`+R`/`+J`, a `CBAN`, and the max-channels cap | +| `channels/restricted-create` | create a new channel while `restrictchans` is on | +| `channels/ignore-nonicks` | change nick while on a `+N` (no-nick-change) channel | | `users/flood` | exemption from the message- and join-flood limits | | `users/ignore-commonchans` | message a `+c` user without sharing a common channel | +| `users/ignore-callerid` | message a `+g` (caller-ID) user without being on their accept list | +| `users/ignore-restrictmsg` | private-message anyone while `restrictmsg` is on | +| `users/secret-whois` | `/WHOIS` a `+W` (showwhois) user without notifying them | +| `servers/use-disabled-commands` | use a command turned off by `disabled_commands` | +| `servers/ignore-securelist` | bypass the `securelist` LIST hold for fresh connections | +| `servers/ignore-blockamsg` | send `/AMSG`-style multi-channel messages the `blockamsg` module blocks | + +The built-in `override` class carries the channel/message/anti-spam bypasses +(`channels/restricted-create`, `channels/ignore-nonicks`, `users/ignore-restrictmsg`, +`servers/ignore-securelist`, `servers/ignore-blockamsg`), `auspex` carries the +see-through-privacy set (the three `*/auspex` plus `users/secret-whois` and +`users/ignore-callerid`), and `servers/use-disabled-commands` sits on the `server` class. ## Snomasks diff --git a/src/channels.rs b/src/channels.rs index 660f18f..ef78656 100644 --- a/src/channels.rs +++ b/src/channels.rs @@ -766,8 +766,10 @@ impl Server { if crate::modules::denychans::intercept(self, uid, name, is_oper) { return; } - // restrictchans — only opers may create new channels (unless whitelisted) - if crate::modules::restrictchans::intercept(self, uid, name, is_oper) { + // restrictchans — channels/restricted-create may create new channels (unless whitelisted) + let may_create = + crate::modules::opertypes::has_priv(self, uid, crate::modules::opertypes::privs::CHANNELS_RESTRICTED_CREATE); + if crate::modules::restrictchans::intercept(self, uid, name, may_create) { return; } // channames — forbidden characters in new channel names diff --git a/src/coremods/core_info.rs b/src/coremods/core_info.rs index d25b165..22fbfa5 100644 --- a/src/coremods/core_info.rs +++ b/src/coremods/core_info.rs @@ -438,8 +438,11 @@ impl Command for Whois { &format!("{nick} {idle} {signon} :seconds idle, signon time"), ); } - // +W showwhois — tell the target that someone looked them up - if showwhois && !is_self { + // +W showwhois — tell the target that someone looked them up (users/secret-whois is silent) + if showwhois + && !is_self + && !crate::modules::opertypes::has_priv(s, uid, crate::modules::opertypes::privs::USERS_SECRET_WHOIS) + { let by = s.users.get(&uid).map(|u| u.prefix()).unwrap_or_default(); let m = s.trf("{0} did a /WHOIS on you", &[by.as_str()]); s.send( diff --git a/src/coremods/core_message.rs b/src/coremods/core_message.rs index fd3402a..c62754f 100644 --- a/src/coremods/core_message.rs +++ b/src/coremods/core_message.rs @@ -153,7 +153,9 @@ fn dm_blocked(s: &Server, uid: Uid, tuid: Uid) -> bool { if ssl_only && !s.users.get(&uid).map(|u| u.secure).unwrap_or(false) { return true; } - if callerid { + if callerid + && !crate::modules::opertypes::has_priv(s, uid, crate::modules::opertypes::privs::USERS_IGNORE_CALLERID) + { let sender_nick = s.users.get(&uid).map(|u| u.nick.clone()).unwrap_or_default(); if !s.is_accepted(tuid, &sender_nick) { return true; @@ -566,7 +568,11 @@ pub(crate) fn deliver(s: &mut Server, uid: Uid, params: &[String], notice: bool) .get(&tuid) .map(|u| u.flags.callerid) .unwrap_or(false); - if target_g && uid != tuid && !s.is_accepted(tuid, &sender_nick) { + if target_g + && uid != tuid + && !s.is_accepted(tuid, &sender_nick) + && !crate::modules::opertypes::has_priv(s, uid, crate::modules::opertypes::privs::USERS_IGNORE_CALLERID) + { let (tnick, sident, shost) = { let t = s.users.get(&tuid); let u = s.users.get(&uid); diff --git a/src/coremods/core_user.rs b/src/coremods/core_user.rs index 7c994f1..9d7471e 100644 --- a/src/coremods/core_user.rs +++ b/src/coremods/core_user.rs @@ -511,8 +511,8 @@ impl Command for Nick { ); return CmdResult::Fail; } - // +N — can't change nick while on a no-nick-change channel (opers bypass) - if !s.is_oper(uid) { + // +N — can't change nick while on a no-nick-change channel (channels/ignore-nonicks bypasses) + if !crate::modules::opertypes::has_priv(s, uid, crate::modules::opertypes::privs::CHANNELS_IGNORE_NONICKS) { let blocked = s .users .get(&uid) diff --git a/src/modules/blockamsg.rs b/src/modules/blockamsg.rs index 11f80b1..4d7e6bb 100644 --- a/src/modules/blockamsg.rs +++ b/src/modules/blockamsg.rs @@ -50,8 +50,8 @@ impl Module for BlockAmsg { if params.len() < 2 { return ModResult::Passthru; } - // opers bypass the check entirely - if srv.is_oper(uid) { + // servers/ignore-blockamsg opers bypass the check entirely + if crate::modules::opertypes::has_priv(srv, uid, crate::modules::opertypes::privs::SERVERS_IGNORE_BLOCKAMSG) { return ModResult::Passthru; } diff --git a/src/modules/disable.rs b/src/modules/disable.rs index 73f568e..c439e53 100644 --- a/src/modules/disable.rs +++ b/src/modules/disable.rs @@ -29,8 +29,8 @@ impl Module for Disable { cmd: &str, _params: &[String], ) -> ModResult { - // opers are never restricted - if srv.is_oper(uid) { + // servers/use-disabled-commands opers bypass the disabled list + if crate::modules::opertypes::has_priv(srv, uid, crate::modules::opertypes::privs::SERVERS_USE_DISABLED_COMMANDS) { return ModResult::Passthru; } // (re)build the set only when the config generation changes, not per command diff --git a/src/modules/opertypes.rs b/src/modules/opertypes.rs index 74ae9ae..0b5c413 100644 --- a/src/modules/opertypes.rs +++ b/src/modules/opertypes.rs @@ -36,6 +36,22 @@ pub mod privs { pub const USERS_IGNORE_COMMONCHANS: &str = "users/ignore-commonchans"; /// join through +k/+b/+i/+l/+z/+R/+J, CBAN and the max-channels cap pub const CHANNELS_OVERRIDE: &str = "channels/override"; + /// create a new channel while `restrictchans` is on + pub const CHANNELS_RESTRICTED_CREATE: &str = "channels/restricted-create"; + /// change nick while on a +N (no-nick-change) channel + pub const CHANNELS_IGNORE_NONICKS: &str = "channels/ignore-nonicks"; + /// message a +g (caller-id) user without being on their ACCEPT list + pub const USERS_IGNORE_CALLERID: &str = "users/ignore-callerid"; + /// `/WHOIS` a +W (showwhois) user without notifying them + pub const USERS_SECRET_WHOIS: &str = "users/secret-whois"; + /// private-message anyone while `restrictmsg` is on + pub const USERS_IGNORE_RESTRICTMSG: &str = "users/ignore-restrictmsg"; + /// use a command turned off by `disabled_commands` + pub const SERVERS_USE_DISABLED_COMMANDS: &str = "servers/use-disabled-commands"; + /// bypass the `securelist` LIST hold for fresh connections + pub const SERVERS_IGNORE_SECURELIST: &str = "servers/ignore-securelist"; + /// send `/AMSG`-style multi-channel messages the `blockamsg` module blocks + pub const SERVERS_IGNORE_BLOCKAMSG: &str = "servers/ignore-blockamsg"; } /// Per-user resolved grant, stored on `User.ext` at oper-up. Present ⇒ a typed @@ -346,12 +362,12 @@ fn builtin() -> (HashMap, HashMap) { let mut classes: HashMap = HashMap::default(); classes.insert("announce".into(), cdef(&["WALLOPS", "GLOBOPS"], &[], "ag")); classes.insert("ban".into(), cdef(&["KILL", "KLINE", "GLINE", "ZLINE", "QLINE", "ELINE", "RLINE", "SHUN", "CBAN", "CHECK", "NICKLOCK", "NICKUNLOCK"], &[], "kx")); - classes.insert("override".into(), cdef(&["SAJOIN", "SAPART", "SANICK", "SAKICK", "SAMODE", "SATOPIC", "SAQUIT", "CLEARCHAN"], &["channels/override", "users/flood"], "v")); + classes.insert("override".into(), cdef(&["SAJOIN", "SAPART", "SANICK", "SAKICK", "SAMODE", "SATOPIC", "SAQUIT", "CLEARCHAN"], &["channels/override", "users/flood", "channels/restricted-create", "channels/ignore-nonicks", "users/ignore-restrictmsg", "servers/ignore-securelist", "servers/ignore-blockamsg"], "v")); classes.insert("host".into(), cdef(&["CHGHOST", "CHGIDENT", "CHGNAME", "SETHOST", "SETIDENT", "SETIDLE", "SWHOIS"], &[], "")); classes.insert("services".into(), cdef(&["SVSNICK", "SVSJOIN", "SVSPART", "SVSMODE", "SVSLOGIN", "SVSLOGOUT"], &[], "")); - classes.insert("server".into(), cdef(&["CONNECT", "SQUIT", "DIE", "RESTART"], &[], "lr")); + classes.insert("server".into(), cdef(&["CONNECT", "SQUIT", "DIE", "RESTART"], &["servers/use-disabled-commands"], "lr")); // auspex: see through user/channel privacy (real host+IP, geo, secret channels) - classes.insert("auspex".into(), cdef(&[], &["users/auspex", "channels/auspex", "servers/auspex"], "")); + classes.insert("auspex".into(), cdef(&[], &["users/auspex", "channels/auspex", "servers/auspex", "users/secret-whois", "users/ignore-callerid"], "")); let mut types: HashMap = HashMap::default(); // The WHOIS title line is bold + colour 4 (red) by default; override per type @@ -661,6 +677,19 @@ mod tests { assert!(!r2.all_commands && r2.commands.contains("KILL") && r2.deny_commands.contains("GLINE")); } + #[test] + fn builtin_classes_grant_the_new_privileges() { + let (classes, _) = builtin(); + let has = |c: &str, p: &str| classes.get(c).unwrap().privs.contains(&p.to_string()); + assert!(has("override", "channels/restricted-create") && has("override", "channels/ignore-nonicks")); + assert!(has("override", "users/ignore-restrictmsg")); + assert!(has("override", "servers/ignore-securelist") && has("override", "servers/ignore-blockamsg")); + assert!(has("auspex", "users/secret-whois") && has("auspex", "users/ignore-callerid")); + assert!(has("server", "servers/use-disabled-commands")); + // netadmin holds every class ⇒ every one of the new privileges resolves in + assert!(resolved("netadmin").all_privs); + } + #[test] fn gated_covers_the_dangerous_commands_only() { assert!(gated("kill") && gated("DIE") && gated("svsnick") && gated("CONNECT")); diff --git a/src/modules/restrictchans.rs b/src/modules/restrictchans.rs index 84922db..25bc19a 100644 --- a/src/modules/restrictchans.rs +++ b/src/modules/restrictchans.rs @@ -9,9 +9,10 @@ use crate::server::Server; use crate::Uid; /// Called from `Server::join`. Returns true when creating `name` should be blocked -/// (the caller returns without joining). Opers and joins to *existing* channels pass. -pub fn intercept(s: &mut Server, uid: Uid, name: &str, is_oper: bool) -> bool { - if is_oper || !s.conf_bool("restrictchans", false) { +/// (the caller returns without joining). Holders of `channels/restricted-create` and +/// joins to *existing* channels pass. +pub fn intercept(s: &mut Server, uid: Uid, name: &str, may_create: bool) -> bool { + if may_create || !s.conf_bool("restrictchans", false) { return false; } // joining a channel that already exists is always fine diff --git a/src/modules/restrictmsg.rs b/src/modules/restrictmsg.rs index 15c63fb..6f23b4a 100644 --- a/src/modules/restrictmsg.rs +++ b/src/modules/restrictmsg.rs @@ -29,8 +29,8 @@ impl Module for RestrictMsg { if target.starts_with('#') { return ModResult::Passthru; } - // sender opers may message anyone - if srv.is_oper(uid) { + // users/ignore-restrictmsg senders may message anyone + if crate::modules::opertypes::has_priv(srv, uid, crate::modules::opertypes::privs::USERS_IGNORE_RESTRICTMSG) { return ModResult::Passthru; } let Some(tuid) = srv.find_nick(target) else { diff --git a/src/modules/securelist.rs b/src/modules/securelist.rs index f9f8f1f..6324bce 100644 --- a/src/modules/securelist.rs +++ b/src/modules/securelist.rs @@ -39,7 +39,7 @@ fn rand_name(len: usize) -> String { /// Is `uid` exempt from the LIST hold? fn is_exempt(s: &Server, uid: Uid) -> bool { - if s.is_oper(uid) { + if crate::modules::opertypes::has_priv(s, uid, crate::modules::opertypes::privs::SERVERS_IGNORE_SECURELIST) { return true; } if s.conf_bool("securelist_exemptregistered", true) && s.is_logged_in(uid) {