Cap and freeze-gate GroupServ registration

This commit is contained in:
Jean Chevronnet 2026-07-19 17:45:04 +00:00
parent 0c85c5b569
commit fa9eaece45
No known key found for this signature in database
4 changed files with 22 additions and 0 deletions

View file

@ -2079,6 +2079,7 @@ pub trait Store {
fn group(&self, name: &str) -> Option<GroupView>; fn group(&self, name: &str) -> Option<GroupView>;
fn groups(&self) -> Vec<String>; fn groups(&self) -> Vec<String>;
fn groups_of(&self, account: &str) -> Vec<String>; fn groups_of(&self, account: &str) -> Vec<String>;
fn groups_founded(&self, account: &str) -> usize;
// A user's effective channel capabilities (direct access unioned with groups). // A user's effective channel capabilities (direct access unioned with groups).
fn channel_caps(&self, channel: &str, account: &str) -> Caps; fn channel_caps(&self, channel: &str, account: &str) -> Caps;
// Runtime operator grants (OperServ OPER), merged with config opers. `expires` // Runtime operator grants (OperServ OPER), merged with config opers. `expires`

View file

@ -11,6 +11,12 @@ pub fn handle(me: &str, from: &Sender, name: Option<&str>, ctx: &mut ServiceCtx,
ctx.notice(me, from.uid, "A group name starts with \x02!\x02, e.g. \x02!staff\x02."); ctx.notice(me, from.uid, "A group name starts with \x02!\x02, e.g. \x02!staff\x02.");
return; return;
} }
// Honour the registration freeze staff raise during a spam wave, like
// NickServ/ChanServ REGISTER — otherwise the group namespace stays open.
if db.registrations_frozen() {
ctx.notice(me, from.uid, "Group registration is temporarily disabled. Please try again later.");
return;
}
// Guard the group namespace like NickServ/ChanServ REGISTER: a look-alike group // Guard the group namespace like NickServ/ChanServ REGISTER: a look-alike group
// name (!аdmin) could impersonate a real one that opers grant channel access to. // name (!аdmin) could impersonate a real one that opers grant channel access to.
if db.confusable_check_enabled() { if db.confusable_check_enabled() {
@ -19,6 +25,13 @@ pub fn handle(me: &str, from: &Sender, name: Option<&str>, ctx: &mut ServiceCtx,
return; return;
} }
} }
// Cap groups per founder so the append-only log can't be grown without bound
// (mirrors NickServ's grouped-nick and AJOIN caps).
const MAX_GROUPS: usize = 25;
if db.groups_founded(acc) >= MAX_GROUPS {
ctx.notice(me, from.uid, format!("You've reached the maximum of {MAX_GROUPS} registered groups."));
return;
}
let acc = acc.to_string(); let acc = acc.to_string();
match db.group_register(name, &acc) { match db.group_register(name, &acc) {
Ok(()) => ctx.notice(me, from.uid, format!("Group \x02{name}\x02 registered — you're the founder.")), Ok(()) => ctx.notice(me, from.uid, format!("Group \x02{name}\x02 registered — you're the founder.")),

View file

@ -657,6 +657,11 @@ impl Db {
names names
} }
/// How many groups `account` is the founder of (bounds group registration).
pub fn groups_founded(&self, account: &str) -> usize {
self.net.groups.iter().filter(|g| g.founder.eq_ignore_ascii_case(account)).count()
}
/// The groups an account belongs to (founder or member). /// The groups an account belongs to (founder or member).
pub fn groups_of(&self, account: &str) -> Vec<String> { pub fn groups_of(&self, account: &str) -> Vec<String> {
self.net self.net

View file

@ -401,6 +401,9 @@ impl Store for Db {
fn groups_of(&self, account: &str) -> Vec<String> { fn groups_of(&self, account: &str) -> Vec<String> {
Db::groups_of(self, account) Db::groups_of(self, account)
} }
fn groups_founded(&self, account: &str) -> usize {
Db::groups_founded(self, account)
}
fn channel_caps(&self, channel: &str, account: &str) -> Caps { fn channel_caps(&self, channel: &str, account: &str) -> Caps {
Db::channel_caps(self, channel, account) Db::channel_caps(self, channel, account)
} }