hidelist: restrict list-mode viewing by rank; allow members to view lists by default (parity)

This commit is contained in:
Jean Chevronnet 2026-08-10 08:24:37 +00:00
parent 9d84a66437
commit ab4d87488b
5 changed files with 73 additions and 7 deletions

View file

@ -120,6 +120,11 @@ amu_target = both
# --- chanlog (m_chanlog): mirror the oper server-notice stream into a channel so # --- chanlog (m_chanlog): mirror the oper server-notice stream into a channel so
# staff can watch it in a normal window. Set the channel (create/keep it opped): # staff can watch it in a normal window. Set the channel (create/keep it opped):
# chanlog = #snotices # chanlog = #snotices
# --- hidelist (m_hidelist): list modes (+b/+e/+I/…) are viewable by members by
# default; this restricts a given list to a minimum rank. Repeatable,
# `hidelist = <modechar> <rank>` (rank: owner|admin|op|halfop|voice). Opers see
# everything. e.g. only ops may view the ban list:
# hidelist = b op
# --- autoop (m_autoop): no config needed — it's the channel list mode +w. Grant a # --- autoop (m_autoop): no config needed — it's the channel list mode +w. Grant a
# status prefix to matching users on join, `+w <prefix>:<hostmask>`, e.g. # status prefix to matching users on join, `+w <prefix>:<hostmask>`, e.g.
# /MODE #chan +w o:*!*@trusted.host (auto-op) # /MODE #chan +w o:*!*@trusted.host (auto-op)

View file

@ -88,9 +88,19 @@ pub fn apply_mode(s: &mut Server, uid: Uid, params: &[String]) -> CmdResult {
s.numeric(uid, RPL_CREATIONTIME, &format!("{target} {created}")); s.numeric(uid, RPL_CREATIONTIME, &format!("{target} {created}"));
return CmdResult::Ok; return CmdResult::Ok;
} }
// setting modes needs at least half-op; each handler then enforces its // dispatch each mode letter to its handler
// own finer rule (prefixes need enough rank, +z needs all-secure, …) let modestring = params[1].clone();
if s.rank(uid, &key) < RANK_HALFOP { let args = &params[2..];
// a *pure list query* (only list-mode letters, no arguments, e.g. `MODE #c +b`)
// is just viewing — allow it for anyone (the hidelist module may still restrict
// it). Anything that changes a mode needs at least half-op; each handler then
// enforces its own finer rule (prefixes need enough rank, +z needs all-secure…).
let pure_list_query = args.is_empty()
&& modestring
.chars()
.filter(|c| *c != '+' && *c != '-')
.all(|c| chan_mode(c).is_some_and(|h| h.is_list()));
if !pure_list_query && s.rank(uid, &key) < RANK_HALFOP {
s.numeric( s.numeric(
uid, uid,
ERR_CHANOPRIVSNEEDED, ERR_CHANOPRIVSNEEDED,
@ -98,10 +108,6 @@ pub fn apply_mode(s: &mut Server, uid: Uid, params: &[String]) -> CmdResult {
); );
return CmdResult::Fail; return CmdResult::Fail;
} }
// dispatch each mode letter to its handler
let modestring = params[1].clone();
let args = &params[2..];
let mut argi = 0usize; let mut argi = 0usize;
let mut sign = '+'; let mut sign = '+';
let mut applied = String::new(); let mut applied = String::new();

View file

@ -32,6 +32,11 @@ pub trait ChanMode: Sync {
fn letter(&self) -> char; fn letter(&self) -> char;
/// Whether to consume an argument for this sign (taken only if one remains). /// Whether to consume an argument for this sign (taken only if one remains).
fn wants_param(&self, adding: bool) -> bool; fn wants_param(&self, adding: bool) -> bool;
/// A list mode (+b/+e/+I/+g/+X/+w): a query with no argument is just viewing,
/// so it needn't require channel-operator rank (unlike setting an entry).
fn is_list(&self) -> bool {
false
}
/// Apply `+`/`-` to channel `key` (display name `chan`) on behalf of `uid`. /// Apply `+`/`-` to channel `key` (display name `chan`) on behalf of `uid`.
fn apply( fn apply(
&self, &self,
@ -571,6 +576,9 @@ impl ChanMode for ListMode {
fn letter(&self) -> char { fn letter(&self) -> char {
self.ch self.ch
} }
fn is_list(&self) -> bool {
true
}
fn wants_param(&self, _adding: bool) -> bool { fn wants_param(&self, _adding: bool) -> bool {
true // a mask to add/remove; absent ⇒ list query true // a mask to add/remove; absent ⇒ list query
} }
@ -586,6 +594,15 @@ impl ChanMode for ListMode {
let (entry_num, end_num, noun) = self.kind.numerics(); let (entry_num, end_num, noun) = self.kind.numerics();
// no mask ⇒ list query // no mask ⇒ list query
let Some(mask) = param else { let Some(mask) = param else {
// hidelist: low-rank members may be barred from viewing this list
if crate::modules::hidelist::denied(s, uid, key, self.ch) {
s.numeric(
uid,
ERR_CHANOPRIVSNEEDED,
&format!("{chan} :You do not have access to view the {noun}"),
);
return Applied::No;
}
let rows: Vec<(String, String, u64)> = s let rows: Vec<(String, String, u64)> = s
.channels .channels
.get(key) .get(key)

37
src/modules/hidelist.rs Normal file
View file

@ -0,0 +1,37 @@
//! hidelist — hide a channel list mode's entries (e.g. the +b ban list) from users
//! below a configured rank, so ordinary members can't enumerate who's banned.
//! Config, repeatable: `hidelist = <modechar> <rank>` where rank is one of
//! owner|admin|op|halfop|voice. Opers always see. Reference: InspIRCd's
//! `m_hidelist`. Original native Rust.
use crate::channels::{RANK_ADMIN, RANK_HALFOP, RANK_OP, RANK_OWNER, RANK_VOICE};
use crate::server::Server;
use crate::Uid;
fn rank_value(name: &str) -> u8 {
match name.to_ascii_lowercase().as_str() {
"owner" | "founder" | "q" => RANK_OWNER,
"admin" | "protect" | "a" => RANK_ADMIN,
"op" | "o" => RANK_OP,
"halfop" | "h" => RANK_HALFOP,
"voice" | "v" => RANK_VOICE,
_ => RANK_OP, // unknown ⇒ require op, the safe default
}
}
/// True if `uid` may NOT view the `modechar` list in channel `key`: the config sets
/// a per-mode minimum rank via `hidelist = <modechar> <rank>`. Opers always see.
pub fn denied(s: &Server, uid: Uid, key: &str, modechar: char) -> bool {
if s.is_oper(uid) {
return false;
}
for line in s.conf_all("hidelist") {
let mut it = line.split_whitespace();
if let (Some(mc), Some(rank)) = (it.next(), it.next()) {
if mc.chars().next() == Some(modechar) {
return s.rank(uid, key) < rank_value(rank);
}
}
}
false
}

View file

@ -30,6 +30,7 @@ pub mod filter;
pub mod flood; pub mod flood;
pub mod geoip; pub mod geoip;
pub mod hashident; pub mod hashident;
pub mod hidelist;
pub mod hidewhois; pub mod hidewhois;
pub mod irccloudtags; pub mod irccloudtags;
pub mod jsonlog; pub mod jsonlog;