opers: wire eight more privileges over their raw is_oper gates
Convert the remaining oper bypass/visibility gates to named privileges: channels/restricted-create, channels/ignore-nonicks, users/ignore-callerid, users/secret-whois, users/ignore-restrictmsg, servers/use-disabled-commands, servers/ignore-securelist, servers/ignore-blockamsg. The override class gains the channel/message/anti-spam bypasses, auspex gains secret-whois + ignore-callerid, server gains use-disabled-commands; netadmin keeps all via privs=*. Docs updated; verified restricted-create + ignore-nonicks live.
This commit is contained in:
parent
bbe2682478
commit
5dfedb5091
11 changed files with 76 additions and 21 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<String, ClassDef>, HashMap<String, TypeDef>) {
|
|||
let mut classes: HashMap<String, ClassDef> = 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<String, TypeDef> = 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"));
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue