auth: deferred login is authoritative in the account map, not just ircd metadata
All checks were successful
CI / check (push) Successful in 3m46s

This commit is contained in:
Jean Chevronnet 2026-07-17 03:30:47 +00:00
parent 75c6537532
commit e112b8737c
No known key found for this signature in database
2 changed files with 27 additions and 2 deletions

View file

@ -275,7 +275,7 @@ impl Engine {
/// ctx helpers the inline path did, so its login side-effects (login, notice,
/// AJOIN, vhost, memo notice) stay identical.
pub fn complete_authenticate(&mut self, ok: bool, then: AuthThen) -> Vec<NetAction> {
match then {
let actions = match then {
AuthThen::Identify { uid, agent, name, account } => {
self.db.note_auth(&name, ok);
let mut ctx = ServiceCtx::default();
@ -320,6 +320,14 @@ impl Engine {
self.sasl_deny("SASL PLAIN", &agent, &client, Some(&account), "bad password")
}
}
}
};
// This runs in the link layer, off the handle() path, so the login's
// accountname metadata never reaches track_accounts in handle(). Apply it
// here so echo's own account map is authoritative and doesn't depend on the
// ircd reflecting the metadata back — which is a no-op when re-authenticating
// an already-logged-in user after a services relink, leaving the user unable
// to use their access despite a successful login.
self.track_accounts(&actions);
actions
}
}

View file

@ -673,6 +673,23 @@
assert_eq!(e.network.account_of("000AAAAAB"), None, "empty account is a logout");
}
// A login that finishes off the handle() path (the link layer's deferred
// password/keycard verify) must still make echo's OWN account map authoritative.
// Otherwise re-identifying an already-logged-in user after a services relink —
// a no-op accountname on the ircd, so nothing is reflected back — leaves them
// unable to use their access despite a successful login.
#[test]
fn deferred_login_makes_account_authoritative() {
let mut e = engine_with("deferlogin", "foo", "sesame");
e.handle(NetEvent::UserConnect { uid: "000AAAAAB".into(), nick: "foo".into(), host: "h".into(), ip: "0.0.0.0".into() });
assert_eq!(e.network.account_of("000AAAAAB"), None);
// Exactly what the link layer does after the off-thread verify — not via handle().
let then = echo_api::AuthThen::Identify { uid: "000AAAAAB".into(), agent: "42SAAAAAA".into(), name: "foo".into(), account: "foo".into() };
let out = e.complete_authenticate(true, then);
assert!(out.iter().any(|a| matches!(a, NetAction::Metadata { key, value, .. } if key == "accountname" && value == "foo")), "emits accountname metadata for the ircd");
assert_eq!(e.network.account_of("000AAAAAB"), Some("foo"), "and is authoritative in echo's own map");
}
// A suspension that arrives by gossip must end local sessions on that account,
// just as a local SUSPEND does — the account and its channels stay put.
#[test]