forbid: type the FORBID kind as an enum at the store boundary, string only in storage
All checks were successful
CI / check (push) Successful in 3m56s

This commit is contained in:
Jean Chevronnet 2026-07-17 13:26:16 +00:00
parent 1fb3615b7e
commit 2c8ba4460d
No known key found for this signature in database
10 changed files with 91 additions and 49 deletions

View file

@ -40,7 +40,7 @@ pub(crate) use event::{apply, Scope};
// echo-api SDK crate; re-exported so the engine keeps naming them locally and
// modules importing `crate::engine::db::{ChanError, ...}` are unaffected.
pub use echo_api::{
AccountView, AjoinView, AkillView, BotView, Caps, ForbidView, GroupView, HelpView, IgnoreView, MemoView, NewsView, Privs, ReportView, SuspensionView, ChanAccessView, ChanAkickView, ChanError, ChanSetting, ChannelView, CertError, CodeKind, Kicker, RegError, Store, TriggerView, VhostView,
AccountView, AjoinView, AkillView, BotView, Caps, ForbidKind, ForbidView, GroupView, HelpView, IgnoreView, MemoView, NewsView, Privs, ReportView, SuspensionView, ChanAccessView, ChanAkickView, ChanError, ChanSetting, ChannelView, CertError, CodeKind, Kicker, RegError, Store, TriggerView, VhostView,
};
#[derive(Debug, Clone, Serialize, Deserialize)]

View file

@ -37,41 +37,48 @@ impl Db {
/// Add a registration ban of `kind` ("NICK"/"CHAN"/"EMAIL") for `mask`.
/// Returns whether it was new.
pub fn forbid_add(&mut self, kind: &str, mask: &str, setter: &str, reason: &str) -> Result<bool, RegError> {
let same = |f: &Forbid| f.kind == kind && f.mask.eq_ignore_ascii_case(mask);
pub fn forbid_add(&mut self, kind: ForbidKind, mask: &str, setter: &str, reason: &str) -> Result<bool, RegError> {
// The store persists/gossips the wire token; the API is typed.
let wire = kind.wire();
let same = |f: &Forbid| f.kind == wire && f.mask.eq_ignore_ascii_case(mask);
let fresh = !self.net.forbids.iter().any(same);
self.log
.append(Event::ForbidAdded { kind: kind.to_string(), mask: mask.to_string(), setter: setter.to_string(), reason: reason.to_string(), ts: now() })
.append(Event::ForbidAdded { kind: wire.to_string(), mask: mask.to_string(), setter: setter.to_string(), reason: reason.to_string(), ts: now() })
.map_err(|_| RegError::Internal)?;
self.net.forbids.retain(|f| !same(f));
self.net.forbids.push(Forbid { kind: kind.to_string(), mask: mask.to_string(), setter: setter.to_string(), reason: reason.to_string(), ts: now() });
self.net.forbids.push(Forbid { kind: wire.to_string(), mask: mask.to_string(), setter: setter.to_string(), reason: reason.to_string(), ts: now() });
Ok(fresh)
}
/// Remove a registration ban of `kind` for `mask`. Returns whether one existed.
pub fn forbid_del(&mut self, kind: &str, mask: &str) -> Result<bool, RegError> {
let same = |f: &Forbid| f.kind == kind && f.mask.eq_ignore_ascii_case(mask);
pub fn forbid_del(&mut self, kind: ForbidKind, mask: &str) -> Result<bool, RegError> {
let wire = kind.wire();
let same = |f: &Forbid| f.kind == wire && f.mask.eq_ignore_ascii_case(mask);
if !self.net.forbids.iter().any(same) {
return Ok(false);
}
self.log.append(Event::ForbidRemoved { kind: kind.to_string(), mask: mask.to_string() }).map_err(|_| RegError::Internal)?;
self.log.append(Event::ForbidRemoved { kind: wire.to_string(), mask: mask.to_string() }).map_err(|_| RegError::Internal)?;
self.net.forbids.retain(|f| !same(f));
Ok(true)
}
/// All registration bans, oldest first.
/// All registration bans, oldest first. A stored kind that no longer parses
/// (corrupt/old log) is skipped rather than shown.
pub fn forbids(&self) -> Vec<ForbidView> {
self.net.forbids
.iter()
.map(|f| ForbidView { kind: f.kind.clone(), mask: f.mask.clone(), setter: f.setter.clone(), reason: f.reason.clone(), ts: f.ts })
.filter_map(|f| {
ForbidKind::from_name(&f.kind).map(|kind| ForbidView { kind, mask: f.mask.clone(), setter: f.setter.clone(), reason: f.reason.clone(), ts: f.ts })
})
.collect()
}
/// The reason `name` is forbidden for `kind` registration (glob), if it is.
pub fn is_forbidden(&self, kind: &str, name: &str) -> Option<String> {
pub fn is_forbidden(&self, kind: ForbidKind, name: &str) -> Option<String> {
let wire = kind.wire();
self.net.forbids
.iter()
.find(|f| f.kind == kind && glob_match(&f.mask, name))
.find(|f| f.kind == wire && glob_match(&f.mask, name))
.map(|f| f.reason.clone())
}

View file

@ -218,16 +218,16 @@ impl Store for Db {
fn akills(&self) -> Vec<AkillView> {
Db::akills(self)
}
fn forbid_add(&mut self, kind: &str, mask: &str, setter: &str, reason: &str) -> Result<bool, RegError> {
fn forbid_add(&mut self, kind: ForbidKind, mask: &str, setter: &str, reason: &str) -> Result<bool, RegError> {
Db::forbid_add(self, kind, mask, setter, reason)
}
fn forbid_del(&mut self, kind: &str, mask: &str) -> Result<bool, RegError> {
fn forbid_del(&mut self, kind: ForbidKind, mask: &str) -> Result<bool, RegError> {
Db::forbid_del(self, kind, mask)
}
fn forbids(&self) -> Vec<ForbidView> {
Db::forbids(self)
}
fn is_forbidden(&self, kind: &str, name: &str) -> Option<String> {
fn is_forbidden(&self, kind: ForbidKind, name: &str) -> Option<String> {
Db::is_forbidden(self, kind, name)
}
fn ignore_add(&mut self, mask: &str, reason: &str, expires: Option<u64>) {

View file

@ -610,7 +610,7 @@
db.group_register("!other", "bob").unwrap();
db.group_set_flags("!other", "alice", "").unwrap();
db.akill_add("G", "*@evil", "oper", "spam", None).unwrap();
db.forbid_add("NICK", "bad*", "oper", "squat").unwrap();
db.forbid_add(ForbidKind::Nick, "bad*", "oper", "squat").unwrap();
db.vhost_offer_add("cool.vhost").unwrap();
db.vhost_forbid_add("(?i)admin").unwrap();
db.set_vhost_template(Some("$account.users.example".into())).unwrap();
@ -642,8 +642,8 @@
db.unsuspend_account("carol").unwrap();
db.akill_add("Q", "spam*", "oper", "nick", None).unwrap();
db.akill_del("Q", "spam*").unwrap();
db.forbid_add("CHAN", "#evil*", "oper", "bad").unwrap();
db.forbid_del("CHAN", "#evil*").unwrap();
db.forbid_add(ForbidKind::Chan, "#evil*", "oper", "bad").unwrap();
db.forbid_del(ForbidKind::Chan, "#evil*").unwrap();
let nid = db.news_add("logon", "temp notice", "oper");
db.news_del(nid);
db.certfp_add("bob", "1122334455667788990011223344556677889900").unwrap();
@ -704,7 +704,7 @@
a.set_account_kill("alice", false).unwrap();
a.suspend_account("bob", "op", "bad", None).unwrap();
a.akill_add("G", "*@evil", "op", "spam", None).unwrap();
a.forbid_add("NICK", "bad*", "op", "squat").unwrap();
a.forbid_add(ForbidKind::Nick, "bad*", "op", "squat").unwrap();
a.group_register("!g", "alice").unwrap();
a.group_set_flags("!g", "bob", "").unwrap();

View file

@ -214,7 +214,7 @@ impl Engine {
if self.db.exists(account) {
return Some(reg_reply(reply, RegOutcome::Exists, account));
}
if self.db.is_forbidden("NICK", account).is_some() {
if self.db.is_forbidden(echo_api::ForbidKind::Nick, account).is_some() {
return Some(reg_reply(reply, RegOutcome::Forbidden, account));
}
if !self.reg_limiter.allow() {
@ -230,7 +230,7 @@ impl Engine {
};
// A forbidden email pattern (OperServ FORBID EMAIL) blocks registration.
if let Some(addr) = &email {
if self.db.is_forbidden("EMAIL", addr).is_some() {
if self.db.is_forbidden(echo_api::ForbidKind::Email, addr).is_some() {
return reg_reply(&reply, RegOutcome::ForbiddenEmail, account);
}
}

View file

@ -1740,7 +1740,7 @@
let reply = crate::proto::RegReply::NickServ { agent: "42SAAAAAA".into(), uid: "000AAAAAC".into(), nick: "evilbob".into() };
assert!(e.pre_register_check("evilbob", &reply).is_some(), "forbidden nick refused");
assert!(e.pre_register_check("cleanname", &reply).is_none(), "clean nick allowed");
assert!(e.db.is_forbidden("CHAN", "#warez").is_some(), "channel is forbidden");
assert!(e.db.is_forbidden(echo_api::ForbidKind::Chan, "#warez").is_some(), "channel is forbidden");
// EMAIL forbids block registration and SET EMAIL.
assert!(notice(&os(&mut e, "FORBID ADD EMAIL *@spam.tld disposable"), "Forbade"), "email forbid added");