db: purge all references to a dropped account
All checks were successful
CI / check (push) Successful in 3m45s

Dropping (or expiring) an account left its channel access, channel
successorship, and group membership/foundership behind, keyed by name — so
re-registering the same nick silently inherited the old privileges. The
AccountDropped fold and the live drop_account path now share one helper
that erases every reference (founder channels are still handled by
handle_account_gone's successor transfer). Closes a privilege-inheritance
hole; especially relevant to inactivity expiry.
This commit is contained in:
Jean Chevronnet 2026-07-16 03:51:01 +00:00
parent dc23a44f57
commit fa55bc3e3a
No known key found for this signature in database
3 changed files with 74 additions and 1 deletions

View file

@ -461,3 +461,46 @@
assert_eq!(db.channel("#c").unwrap().join_mode("bob"), Some("+v"), "survives reopen");
assert_eq!(db.channel("#c").unwrap().join_mode("alice"), None, "removal survives reopen");
}
// Dropping an account erases every reference to it — channel access and
// successorship, group membership and foundership — so a later re-registration
// of the same name can't inherit its standing. Holds live and after replay.
#[test]
fn dropping_an_account_purges_all_its_references() {
let p = tmp("droppurge");
let mut db = Db::open(&p, "local");
db.scram_iterations = 4096;
db.register("bob", "pw", None).unwrap();
db.register("carol", "pw", None).unwrap();
db.register("alice", "pw", None).unwrap();
db.register_channel("#foo", "bob").unwrap();
db.access_add("#foo", "alice", "op").unwrap();
db.register_channel("#bar", "carol").unwrap();
db.set_successor("#bar", Some("alice")).unwrap();
db.group_register("!alicegrp", "alice").unwrap();
db.group_register("!bobgrp", "bob").unwrap();
db.group_set_flags("!bobgrp", "alice", "").unwrap();
// 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("!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("!alicegrp").is_none(), "founderless group dropped");
// Re-registering the name must not inherit the old op access.
db.register("alice", "pw2", None).unwrap();
assert_eq!(db.channel("#foo").unwrap().join_mode("alice"), None, "re-registration inherits nothing");
// And the purge survives a reload (the fold matches live state).
drop(db);
let db = Db::open(&p, "local");
assert_eq!(db.channel("#foo").unwrap().join_mode("alice"), None, "purge holds after replay");
assert_eq!(db.channel_successor("#bar"), None, "successor purge holds after replay");
}