OperServ: add FORBID (nick/channel/email registration bans)
All checks were successful
CI / check (push) Successful in 3m56s
All checks were successful
CI / check (push) Successful in 3m56s
FORBID ADD/DEL/LIST bans a NICK, CHAN, or EMAIL glob from being registered — a network-wide policy that gossips like an AKILL and every node enforces. Nick registration is blocked in pre_register_check (both NickServ REGISTER and the account-registration relay), and channel registration in ChanServ REGISTER. New Forbid store + events, mirroring the akill subsystem.
This commit is contained in:
parent
ac50af92fc
commit
be4860c9a8
12 changed files with 220 additions and 1 deletions
|
|
@ -118,6 +118,10 @@ pub enum Event {
|
|||
kind: String,
|
||||
mask: String,
|
||||
},
|
||||
// Registration bans (OperServ FORBID). Global network policy. `kind` is
|
||||
// "NICK", "CHAN", or "EMAIL".
|
||||
ForbidAdded { kind: String, mask: String, setter: String, reason: String, ts: u64 },
|
||||
ForbidRemoved { kind: String, mask: String },
|
||||
}
|
||||
|
||||
// Whether an event replicates across the federation. Account identity is Global
|
||||
|
|
@ -160,6 +164,8 @@ impl Event {
|
|||
| Event::AccountOperNoteSet { .. }
|
||||
| Event::AkillAdded { .. }
|
||||
| Event::AkillRemoved { .. }
|
||||
| Event::ForbidAdded { .. }
|
||||
| Event::ForbidRemoved { .. }
|
||||
| Event::NewsAdded { .. }
|
||||
| Event::NewsDeleted { .. }
|
||||
| Event::ReportFiled { .. }
|
||||
|
|
@ -495,6 +501,13 @@ pub(crate) fn apply(accounts: &mut HashMap<String, Account>, channels: &mut Hash
|
|||
Event::AkillRemoved { kind, mask } => {
|
||||
net.akills.retain(|a| !(a.kind == kind && a.mask.eq_ignore_ascii_case(&mask)));
|
||||
}
|
||||
Event::ForbidAdded { kind, mask, setter, reason, ts } => {
|
||||
net.forbids.retain(|f| !(f.kind == kind && f.mask.eq_ignore_ascii_case(&mask)));
|
||||
net.forbids.push(Forbid { kind, mask, setter, reason, ts });
|
||||
}
|
||||
Event::ForbidRemoved { kind, mask } => {
|
||||
net.forbids.retain(|f| !(f.kind == kind && f.mask.eq_ignore_ascii_case(&mask)));
|
||||
}
|
||||
Event::AccountExpiryWarned { account } => {
|
||||
if let Some(a) = accounts.get_mut(&key(&account)) {
|
||||
a.expiry_warned = true;
|
||||
|
|
|
|||
|
|
@ -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, 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, 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)]
|
||||
|
|
@ -166,6 +166,18 @@ pub struct Akill {
|
|||
pub expires: Option<u64>,
|
||||
}
|
||||
|
||||
// A registration ban: an account name, channel, or email pattern that may not be
|
||||
// registered. `kind` is "NICK", "CHAN", or "EMAIL". Network-wide policy like an
|
||||
// AKILL, so it gossips and every node enforces it.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Forbid {
|
||||
pub kind: String,
|
||||
pub mask: String,
|
||||
pub setter: String,
|
||||
pub reason: String,
|
||||
pub ts: u64,
|
||||
}
|
||||
|
||||
// The default ban kind for records written before bans carried one: a G-line.
|
||||
fn gline_kind() -> String {
|
||||
"G".to_string()
|
||||
|
|
@ -249,6 +261,7 @@ pub struct Report {
|
|||
#[derive(Debug, Clone, Default)]
|
||||
pub struct NetData {
|
||||
pub akills: Vec<Akill>,
|
||||
pub forbids: Vec<Forbid>,
|
||||
pub news: Vec<News>,
|
||||
pub news_seq: u64,
|
||||
// Abuse reports (ReportServ), oldest first.
|
||||
|
|
@ -1039,6 +1052,9 @@ impl Db {
|
|||
}
|
||||
// Compaction is a good moment to forget akills that have already expired.
|
||||
let now = now();
|
||||
for f in &self.net.forbids {
|
||||
snapshot.push(Event::ForbidAdded { kind: f.kind.clone(), mask: f.mask.clone(), setter: f.setter.clone(), reason: f.reason.clone(), ts: f.ts });
|
||||
}
|
||||
for a in self.net.akills.iter().filter(|a| a.expires.is_none_or(|e| e > now)) {
|
||||
snapshot.push(Event::AkillAdded { kind: a.kind.clone(), mask: a.mask.clone(), setter: a.setter.clone(), reason: a.reason.clone(), ts: a.ts, expires: a.expires });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,6 +35,46 @@ impl Db {
|
|||
.collect()
|
||||
}
|
||||
|
||||
/// 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);
|
||||
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() })
|
||||
.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() });
|
||||
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);
|
||||
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.net.forbids.retain(|f| !same(f));
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
/// All registration bans, oldest first.
|
||||
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 })
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// The reason `name` is forbidden for `kind` registration (glob), if it is.
|
||||
pub fn is_forbidden(&self, kind: &str, name: &str) -> Option<String> {
|
||||
self.net.forbids
|
||||
.iter()
|
||||
.find(|f| f.kind == kind && glob_match(&f.mask, name))
|
||||
.map(|f| f.reason.clone())
|
||||
}
|
||||
|
||||
/// Add (or replace) a session-limit exception for an IP-mask.
|
||||
pub fn session_except_add(&mut self, mask: &str, limit: u32, reason: &str) {
|
||||
let _ = self.log.append(Event::SessionExceptionAdded { mask: mask.to_string(), limit, reason: reason.to_string() });
|
||||
|
|
|
|||
|
|
@ -186,6 +186,18 @@ 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> {
|
||||
Db::forbid_add(self, kind, mask, setter, reason)
|
||||
}
|
||||
fn forbid_del(&mut self, kind: &str, 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> {
|
||||
Db::is_forbidden(self, kind, name)
|
||||
}
|
||||
fn ignore_add(&mut self, mask: &str, reason: &str, expires: Option<u64>) {
|
||||
Db::ignore_add(self, mask, reason, expires)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue