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` |
|
| `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` |
|
| `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/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/flood` | exemption from the message- and join-flood limits |
|
||||||
| `users/ignore-commonchans` | message a `+c` user without sharing a common channel |
|
| `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
|
## Snomasks
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -766,8 +766,10 @@ impl Server {
|
||||||
if crate::modules::denychans::intercept(self, uid, name, is_oper) {
|
if crate::modules::denychans::intercept(self, uid, name, is_oper) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// restrictchans — only opers may create new channels (unless whitelisted)
|
// restrictchans — channels/restricted-create may create new channels (unless whitelisted)
|
||||||
if crate::modules::restrictchans::intercept(self, uid, name, is_oper) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
// channames — forbidden characters in new channel names
|
// channames — forbidden characters in new channel names
|
||||||
|
|
|
||||||
|
|
@ -438,8 +438,11 @@ impl Command for Whois {
|
||||||
&format!("{nick} {idle} {signon} :seconds idle, signon time"),
|
&format!("{nick} {idle} {signon} :seconds idle, signon time"),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// +W showwhois — tell the target that someone looked them up
|
// +W showwhois — tell the target that someone looked them up (users/secret-whois is silent)
|
||||||
if showwhois && !is_self {
|
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 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()]);
|
let m = s.trf("{0} did a /WHOIS on you", &[by.as_str()]);
|
||||||
s.send(
|
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) {
|
if ssl_only && !s.users.get(&uid).map(|u| u.secure).unwrap_or(false) {
|
||||||
return true;
|
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();
|
let sender_nick = s.users.get(&uid).map(|u| u.nick.clone()).unwrap_or_default();
|
||||||
if !s.is_accepted(tuid, &sender_nick) {
|
if !s.is_accepted(tuid, &sender_nick) {
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -566,7 +568,11 @@ pub(crate) fn deliver(s: &mut Server, uid: Uid, params: &[String], notice: bool)
|
||||||
.get(&tuid)
|
.get(&tuid)
|
||||||
.map(|u| u.flags.callerid)
|
.map(|u| u.flags.callerid)
|
||||||
.unwrap_or(false);
|
.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 (tnick, sident, shost) = {
|
||||||
let t = s.users.get(&tuid);
|
let t = s.users.get(&tuid);
|
||||||
let u = s.users.get(&uid);
|
let u = s.users.get(&uid);
|
||||||
|
|
|
||||||
|
|
@ -511,8 +511,8 @@ impl Command for Nick {
|
||||||
);
|
);
|
||||||
return CmdResult::Fail;
|
return CmdResult::Fail;
|
||||||
}
|
}
|
||||||
// +N — can't change nick while on a no-nick-change channel (opers bypass)
|
// +N — can't change nick while on a no-nick-change channel (channels/ignore-nonicks bypasses)
|
||||||
if !s.is_oper(uid) {
|
if !crate::modules::opertypes::has_priv(s, uid, crate::modules::opertypes::privs::CHANNELS_IGNORE_NONICKS) {
|
||||||
let blocked = s
|
let blocked = s
|
||||||
.users
|
.users
|
||||||
.get(&uid)
|
.get(&uid)
|
||||||
|
|
|
||||||
|
|
@ -50,8 +50,8 @@ impl Module for BlockAmsg {
|
||||||
if params.len() < 2 {
|
if params.len() < 2 {
|
||||||
return ModResult::Passthru;
|
return ModResult::Passthru;
|
||||||
}
|
}
|
||||||
// opers bypass the check entirely
|
// servers/ignore-blockamsg opers bypass the check entirely
|
||||||
if srv.is_oper(uid) {
|
if crate::modules::opertypes::has_priv(srv, uid, crate::modules::opertypes::privs::SERVERS_IGNORE_BLOCKAMSG) {
|
||||||
return ModResult::Passthru;
|
return ModResult::Passthru;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,8 +29,8 @@ impl Module for Disable {
|
||||||
cmd: &str,
|
cmd: &str,
|
||||||
_params: &[String],
|
_params: &[String],
|
||||||
) -> ModResult {
|
) -> ModResult {
|
||||||
// opers are never restricted
|
// servers/use-disabled-commands opers bypass the disabled list
|
||||||
if srv.is_oper(uid) {
|
if crate::modules::opertypes::has_priv(srv, uid, crate::modules::opertypes::privs::SERVERS_USE_DISABLED_COMMANDS) {
|
||||||
return ModResult::Passthru;
|
return ModResult::Passthru;
|
||||||
}
|
}
|
||||||
// (re)build the set only when the config generation changes, not per command
|
// (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";
|
pub const USERS_IGNORE_COMMONCHANS: &str = "users/ignore-commonchans";
|
||||||
/// join through +k/+b/+i/+l/+z/+R/+J, CBAN and the max-channels cap
|
/// join through +k/+b/+i/+l/+z/+R/+J, CBAN and the max-channels cap
|
||||||
pub const CHANNELS_OVERRIDE: &str = "channels/override";
|
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
|
/// 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();
|
let mut classes: HashMap<String, ClassDef> = HashMap::default();
|
||||||
classes.insert("announce".into(), cdef(&["WALLOPS", "GLOBOPS"], &[], "ag"));
|
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("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("host".into(), cdef(&["CHGHOST", "CHGIDENT", "CHGNAME", "SETHOST", "SETIDENT", "SETIDLE", "SWHOIS"], &[], ""));
|
||||||
classes.insert("services".into(), cdef(&["SVSNICK", "SVSJOIN", "SVSPART", "SVSMODE", "SVSLOGIN", "SVSLOGOUT"], &[], ""));
|
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)
|
// 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();
|
let mut types: HashMap<String, TypeDef> = HashMap::default();
|
||||||
// The WHOIS title line is bold + colour 4 (red) by default; override per type
|
// 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"));
|
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]
|
#[test]
|
||||||
fn gated_covers_the_dangerous_commands_only() {
|
fn gated_covers_the_dangerous_commands_only() {
|
||||||
assert!(gated("kill") && gated("DIE") && gated("svsnick") && gated("CONNECT"));
|
assert!(gated("kill") && gated("DIE") && gated("svsnick") && gated("CONNECT"));
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,10 @@ use crate::server::Server;
|
||||||
use crate::Uid;
|
use crate::Uid;
|
||||||
|
|
||||||
/// Called from `Server::join`. Returns true when creating `name` should be blocked
|
/// Called from `Server::join`. Returns true when creating `name` should be blocked
|
||||||
/// (the caller returns without joining). Opers and joins to *existing* channels pass.
|
/// (the caller returns without joining). Holders of `channels/restricted-create` and
|
||||||
pub fn intercept(s: &mut Server, uid: Uid, name: &str, is_oper: bool) -> bool {
|
/// joins to *existing* channels pass.
|
||||||
if is_oper || !s.conf_bool("restrictchans", false) {
|
pub fn intercept(s: &mut Server, uid: Uid, name: &str, may_create: bool) -> bool {
|
||||||
|
if may_create || !s.conf_bool("restrictchans", false) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// joining a channel that already exists is always fine
|
// joining a channel that already exists is always fine
|
||||||
|
|
|
||||||
|
|
@ -29,8 +29,8 @@ impl Module for RestrictMsg {
|
||||||
if target.starts_with('#') {
|
if target.starts_with('#') {
|
||||||
return ModResult::Passthru;
|
return ModResult::Passthru;
|
||||||
}
|
}
|
||||||
// sender opers may message anyone
|
// users/ignore-restrictmsg senders may message anyone
|
||||||
if srv.is_oper(uid) {
|
if crate::modules::opertypes::has_priv(srv, uid, crate::modules::opertypes::privs::USERS_IGNORE_RESTRICTMSG) {
|
||||||
return ModResult::Passthru;
|
return ModResult::Passthru;
|
||||||
}
|
}
|
||||||
let Some(tuid) = srv.find_nick(target) else {
|
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?
|
/// Is `uid` exempt from the LIST hold?
|
||||||
fn is_exempt(s: &Server, uid: Uid) -> bool {
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
if s.conf_bool("securelist_exemptregistered", true) && s.is_logged_in(uid) {
|
if s.conf_bool("securelist_exemptregistered", true) && s.is_logged_in(uid) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue