groupserv: gate channel access on the c flag, document !group
All checks were successful
CI / check (push) Successful in 3m43s

This commit is contained in:
Jean Chevronnet 2026-07-17 01:00:27 +00:00
parent b87a0c56a2
commit 96654b5e5a
No known key found for this signature in database
6 changed files with 23 additions and 14 deletions

View file

@ -478,9 +478,15 @@ impl Db {
}
/// Whether an account is in a group (its founder or a member).
pub fn is_group_member(&self, name: &str, account: &str) -> bool {
// Whether `account` inherits a group's channel access (when the group is on a
// channel's access list): the group's founder, or a member holding the 'c'
// (channel-access) flag. Plain membership is not enough — the flag gates it.
fn group_grants_channel_access(&self, name: &str, account: &str) -> bool {
let k = key(name);
self.net.groups.iter().find(|g| key(&g.name) == k).is_some_and(|g| g.founder.eq_ignore_ascii_case(account) || g.members.iter().any(|m| m.account.eq_ignore_ascii_case(account)))
self.net.groups.iter().find(|g| key(&g.name) == k).is_some_and(|g| {
g.founder.eq_ignore_ascii_case(account)
|| g.members.iter().any(|m| m.account.eq_ignore_ascii_case(account) && m.flags.contains('c'))
})
}
/// An account's effective channel capabilities, combining its direct access
@ -494,7 +500,7 @@ impl Db {
let mut caps = Caps::default();
for a in &c.access {
let applies = match a.account.strip_prefix('!') {
Some(g) => self.is_group_member(&format!("!{g}"), account),
Some(g) => self.group_grants_channel_access(&format!("!{g}"), account),
None => a.account.eq_ignore_ascii_case(account),
};
if applies {

View file

@ -751,14 +751,14 @@
// Sanity: every reference is in place.
assert_eq!(db.channel("#foo").unwrap().join_mode("alice"), Some("+o"), "alice starts as an op");
assert_eq!(db.channel_successor("#bar").as_deref(), Some("alice"));
assert!(db.is_group_member("!bobgrp", "alice"));
assert!(db.group("!bobgrp").unwrap().members.iter().any(|m| m.account.eq_ignore_ascii_case("alice")));
assert!(db.group("!alicegrp").is_some());
// Dropping alice erases all of them.
assert!(db.drop_account("alice").unwrap());
assert_eq!(db.channel("#foo").unwrap().join_mode("alice"), None, "access grant purged");
assert_eq!(db.channel_successor("#bar"), None, "successorship purged");
assert!(!db.is_group_member("!bobgrp", "alice"), "group membership purged");
assert!(!db.group("!bobgrp").unwrap().members.iter().any(|m| m.account.eq_ignore_ascii_case("alice")), "group membership purged");
assert!(db.group("!alicegrp").is_none(), "founderless group dropped");
// Re-registering the name must not inherit the old op access.