From 5bf2116786442b7f36c499f82cb01a3d2fd31266 Mon Sep 17 00:00:00 2001 From: reverse Date: Fri, 14 Aug 2026 17:42:34 +0000 Subject: [PATCH] =?UTF-8?q?modes:=20close=20channel-mode=20privilege=20gap?= =?UTF-8?q?s=20=E2=80=94=20+P=20(permanent)=20and=20+O=20(oper-only=20join?= =?UTF-8?q?)=20are=20now=20IRC-oper-only,=20and=20adding=20a=20+w=20autoop?= =?UTF-8?q?=20entry=20requires=20the=20rank=20the=20granted=20prefix=20nee?= =?UTF-8?q?ds=20(a=20half-op=20could=20otherwise=20auto-op=20itself)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/mode.rs | 69 +++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 67 insertions(+), 2 deletions(-) diff --git a/src/mode.rs b/src/mode.rs index 84985da..c07eabc 100644 --- a/src/mode.rs +++ b/src/mode.rs @@ -289,7 +289,7 @@ static PRIVATE: Flag = Flag { ch: 'p', set: set_private, }; -static OPERONLY: Flag = Flag { +static OPERONLY: OperFlagChan = OperFlagChan { ch: 'O', set: set_operonly, }; @@ -364,7 +364,7 @@ static ALLOWINVITE: Flag = Flag { ch: 'A', set: set_allowinvite, }; -static PERMANENT: Flag = Flag { +static PERMANENT: OperFlagChan = OperFlagChan { ch: 'P', set: set_permanent, }; @@ -400,6 +400,45 @@ impl ChanMode for Flag { } } +/// A channel flag only an IRC operator may **set** — for modes that reach past a +/// single channel (a network resource, or a staff-only policy) where chan-op rank +/// isn't enough. Anyone with the usual rank may clear it. Server / services authority +/// (`mode_sudo`, used by SAMODE and the RPC/S2S appliers) bypasses the oper check. +struct OperFlagChan { + ch: char, + set: fn(&mut ChanModes, bool), +} +impl ChanMode for OperFlagChan { + fn letter(&self) -> char { + self.ch + } + fn wants_param(&self, _adding: bool) -> bool { + false + } + fn apply( + &self, + s: &mut Server, + chan: &str, + key: &str, + uid: Uid, + adding: bool, + _param: Option<&str>, + ) -> Applied { + if adding && !s.mode_sudo && !s.is_oper(uid) { + s.numeric( + uid, + ERR_NOPRIVILEGES, + &format!("{chan} :Only IRC operators may set channel mode +{}", self.ch), + ); + return Applied::No; + } + if let Some(c) = s.channels.get_mut(key) { + (self.set)(&mut c.modes, adding); + } + Applied::Yes(None) + } +} + // --- +k channel key --------------------------------------------------------- struct Key; @@ -648,6 +687,32 @@ impl ChanMode for ListMode { s.numeric(uid, end_num, &format!("{chan} :End of channel {noun}")); return Applied::No; }; + // autoop (+w) embeds a status prefix to grant on join, applied under server + // authority — so adding an entry that grants a prefix the setter couldn't grant + // by hand would let e.g. a half-op auto-op itself. Gate it by the rank the + // prefix needs (SAMODE / services authority bypasses via mode_sudo). + if adding && matches!(self.kind, ListKind::AutoOp) && !s.mode_sudo { + let pfx = mask.split_once(':').and_then(|(p, _)| p.chars().next()); + let needed = match pfx { + Some('q') => RANK_OWNER, + Some('a') => RANK_ADMIN, + Some('o') => RANK_OP, + Some('h') => RANK_HALFOP, + Some('v') => RANK_VOICE, + _ => 0, + }; + if s.rank(uid, key) < needed { + s.numeric( + uid, + ERR_CHANOPRIVSNEEDED, + &format!( + "{chan} :You lack the channel rank to auto-grant +{}", + pfx.unwrap_or('?') + ), + ); + return Applied::No; + } + } let mask = if self.kind.normalizes() { normalize_ban_mask(mask) } else {