db: purge all references to a dropped account
All checks were successful
CI / check (push) Successful in 3m45s
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:
parent
dc23a44f57
commit
fa55bc3e3a
3 changed files with 74 additions and 1 deletions
|
|
@ -500,6 +500,10 @@ impl Db {
|
||||||
}
|
}
|
||||||
self.log.append(Event::AccountDropped { account: account.to_string() }).map_err(|_| RegError::Internal)?;
|
self.log.append(Event::AccountDropped { account: account.to_string() }).map_err(|_| RegError::Internal)?;
|
||||||
self.accounts.remove(&k);
|
self.accounts.remove(&k);
|
||||||
|
// Keep live state in step with what the AccountDropped fold does on replay:
|
||||||
|
// erase every reference so a re-registration can't inherit the account's
|
||||||
|
// channel access, successorship, or group standing.
|
||||||
|
super::event::purge_account_refs(account, &mut self.channels, &mut self.grouped, &mut self.net.groups);
|
||||||
Ok(true)
|
Ok(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -249,6 +249,32 @@ fn owns_over(held: &Account, claim: &Account) -> bool {
|
||||||
|
|
||||||
// Fold one event into the store. Shared by log replay (`open`) and gossip
|
// Fold one event into the store. Shared by log replay (`open`) and gossip
|
||||||
// ingest, so both routes reconstruct identical state.
|
// ingest, so both routes reconstruct identical state.
|
||||||
|
// Erase every remaining reference to a gone account — its nick aliases, channel
|
||||||
|
// access and successorship, and group membership/foundership — so that a later
|
||||||
|
// re-registration of the same name can't silently inherit its standing. Channels
|
||||||
|
// *founded* by the account are handled separately (transfer to a successor or
|
||||||
|
// drop) by the engine's `handle_account_gone`; groups have no successor concept,
|
||||||
|
// so a founderless group is dropped here. Shared by the local `drop_account`
|
||||||
|
// path and the replay/gossip `apply` path so both converge on the same state.
|
||||||
|
pub(crate) fn purge_account_refs(
|
||||||
|
account: &str,
|
||||||
|
channels: &mut HashMap<String, ChannelInfo>,
|
||||||
|
grouped: &mut HashMap<String, String>,
|
||||||
|
groups: &mut Vec<Group>,
|
||||||
|
) {
|
||||||
|
grouped.retain(|_, a| !a.eq_ignore_ascii_case(account));
|
||||||
|
for c in channels.values_mut() {
|
||||||
|
c.access.retain(|a| !a.account.eq_ignore_ascii_case(account));
|
||||||
|
if c.successor.as_deref().is_some_and(|s| s.eq_ignore_ascii_case(account)) {
|
||||||
|
c.successor = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for g in groups.iter_mut() {
|
||||||
|
g.members.retain(|m| !m.account.eq_ignore_ascii_case(account));
|
||||||
|
}
|
||||||
|
groups.retain(|g| !g.founder.eq_ignore_ascii_case(account));
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn apply(accounts: &mut HashMap<String, Account>, channels: &mut HashMap<String, ChannelInfo>, grouped: &mut HashMap<String, String>, bots: &mut HashMap<String, Bot>, host_cfg: &mut HostConfig, net: &mut NetData, event: Event) {
|
pub(crate) fn apply(accounts: &mut HashMap<String, Account>, channels: &mut HashMap<String, ChannelInfo>, grouped: &mut HashMap<String, String>, bots: &mut HashMap<String, Bot>, host_cfg: &mut HostConfig, net: &mut NetData, event: Event) {
|
||||||
match event {
|
match event {
|
||||||
Event::AccountRegistered(a) => {
|
Event::AccountRegistered(a) => {
|
||||||
|
|
@ -306,7 +332,7 @@ pub(crate) fn apply(accounts: &mut HashMap<String, Account>, channels: &mut Hash
|
||||||
}
|
}
|
||||||
Event::AccountDropped { account } => {
|
Event::AccountDropped { account } => {
|
||||||
accounts.remove(&key(&account));
|
accounts.remove(&key(&account));
|
||||||
grouped.retain(|_, a| !a.eq_ignore_ascii_case(&account)); // its aliases go too
|
purge_account_refs(&account, channels, grouped, &mut net.groups);
|
||||||
}
|
}
|
||||||
Event::AccountVerified { account } => {
|
Event::AccountVerified { account } => {
|
||||||
if let Some(a) = accounts.get_mut(&key(&account)) {
|
if let Some(a) = accounts.get_mut(&key(&account)) {
|
||||||
|
|
|
||||||
|
|
@ -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("bob"), Some("+v"), "survives reopen");
|
||||||
assert_eq!(db.channel("#c").unwrap().join_mode("alice"), None, "removal 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");
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue