diff options
| author | tv <tv@krebsco.de> | 2026-11-09 12:25:09 +0000 |
|---|---|---|
| committer | tv <tv@krebsco.de> | 2026-11-09 13:32:05 +0100 |
| commit | d12ccc782d8e6eab46eb6e0976b747dca1013b36 (patch) | |
| tree | de9d5ae9135a182dbb19a6067d240054483561d4 /src/remake | |
| parent | b0e711f9f7e0d2d9f6b39dd987004d2f9b1dcda7 (diff) | |
access: ending a single session
Logging out of the dashboard (`DELETE /auth/session`) only cleared the cookie: the
session's tokens stayed taken until they expired, by every node. Ending them took
`POST /auth/logout`, which ends all of a user's sessions.
Now `DELETE /auth/session` ends the session of the token it comes with (the cookie, or
a bearer token): where the swarm has a registry, it is recorded as
`_continuum/sessions/<user>/<session>` (`registry::session_path`), its value until when
the session would have lasted, and every node refuses the session's tokens once the
change reaches it (`token::Refused::Ended`): the page's own, its narrow tokens, and one
handed to another member's dashboard, all refreshed from it and keeping its ID. The
rules take the entry from the user itself or one who manages users, as a logout. A
remake's snapshot leaves out entries past that time, a user's map where none is left:
their tokens are expired anyway.
Without a registry the document can't record it: the node ending it refuses the
session's tokens itself (`AppState::ended_sessions`, until it restarts), others take
them until they expire, as a logout there is refused.
Diffstat (limited to 'src/remake')
| -rw-r--r-- | src/remake/snapshot.rs | 72 |
1 files changed, 72 insertions, 0 deletions
diff --git a/src/remake/snapshot.rs b/src/remake/snapshot.rs index e82a2ab..2bb5fbb 100644 --- a/src/remake/snapshot.rs +++ b/src/remake/snapshot.rs @@ -289,6 +289,31 @@ pub(crate) fn epoch_metadata_path(generation: u64) -> Vec<String> { meta::path(&[EPOCHS, generation.to_string().as_str()]) } +/// The ended sessions `doc` records (`_continuum/sessions`), by user, each session with +/// until when it would have lasted (one that doesn't read: at once, so it goes). +fn ended_sessions( + doc: &impl ReadDoc, +) -> Result<BTreeMap<String, BTreeMap<String, u64>>, StoreError> { + let mut users = BTreeMap::new(); + for user in store::keys_at(doc, &meta::path(&[meta::SESSIONS]))?.unwrap_or_default() { + let path = meta::path(&[meta::SESSIONS, &user]); + let sessions = store::keys_at(doc, &path)? + .unwrap_or_default() + .into_iter() + .map(|session| { + let until = store::read_plain(doc, &meta::path(&[meta::SESSIONS, &user, &session])) + .ok() + .flatten() + .and_then(|until| until.as_u64()) + .unwrap_or(0); + (session, until) + }) + .collect(); + users.insert(user, sessions); + } + Ok(users) +} + /// Of `earlier` epochs' metadata (by generation), the generations a snapshot of /// `generation`, proposed at `proposed_at`, leaves out: those no rejoining node replays /// through any more. A node on epoch P replays while the replay window of epoch P+1 @@ -377,6 +402,23 @@ pub(crate) fn build( for g in left_out(&earlier, generation, params.proposed_at) { store::delete_path(&mut target, &epoch_metadata_path(g), false)?; } + // Ended sessions past when they would have lasted: their tokens are refused as + // expired anyway. A user's whole map where none is left. + for (user, ended) in ended_sessions(&base)? { + let gone: Vec<&String> = ended + .iter() + .filter(|(_, until)| **until <= params.proposed_at.as_u64()) + .map(|(session, _)| session) + .collect(); + if gone.len() == ended.len() { + store::delete_path(&mut target, &meta::path(&[meta::SESSIONS, &user]), false)?; + } else { + for session in gone { + let path = meta::path(&[meta::SESSIONS, &user, session]); + store::delete_path(&mut target, &path, false)?; + } + } + } // What the kind does to it (a strict remake keeps only what its manifest names). kind.protocol(&base, params.content, &mut target)?; @@ -691,6 +733,36 @@ mod tests { assert_eq!(epoch["baseHeads"].as_array().unwrap().len(), heads.len()); } + /// Ended sessions go once they would have ended anyway; a user's map with them. + #[test] + fn ended_sessions_left_out_once_past() { + let mut a = doc(b"actor-a"); + change(&mut a, |d| { + for (user, session, until) in + [("alice", "s1", 5), ("alice", "s2", 50), ("bob", "s3", 5)] + { + store::write_path( + d, + &meta::path(&[meta::SESSIONS, user, session]), + &json!(until), + ) + .unwrap(); + } + }); + let policy = json!({}); + let heads = a.get_heads(); + let mut at = params(&heads, &policy); + at.proposed_at = UnixMillis(10); + let built = build(&a, &at).unwrap(); + let keys = |path: &[&str]| { + store::keys_at(&built.doc, &meta::path(path)) + .unwrap() + .unwrap_or_default() + }; + assert_eq!(keys(&[meta::SESSIONS]), vec!["alice"]); + assert_eq!(keys(&[meta::SESSIONS, "alice"]), vec!["s2"]); + } + /// Earlier epochs' metadata goes once no rejoining node replays through it, oldest /// first, but for a strict remake's; what doesn't read stays. #[test] |
