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/access | |
| 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/access')
| -rw-r--r-- | src/access/registry.rs | 16 | ||||
| -rw-r--r-- | src/access/rules.rs | 4 | ||||
| -rw-r--r-- | src/access/token.rs | 21 |
3 files changed, 39 insertions, 2 deletions
diff --git a/src/access/registry.rs b/src/access/registry.rs index 01e6ba9..e806d0c 100644 --- a/src/access/registry.rs +++ b/src/access/registry.rs @@ -146,6 +146,22 @@ pub(crate) fn logouts_path(name: &str) -> Vec<String> { meta::path(&[meta::LOGOUTS, name]) } +/// Where user `name`'s session `session` is recorded as ended (logged out of): its +/// tokens aren't taken any more (`token::verify`). The value: until when the session +/// would have lasted (Unix ms), after which it is left out of snapshots. Written by the +/// user itself (`DELETE /auth/session`) or by one who may manage users. +pub(crate) fn session_path(name: &str, session: &str) -> Vec<String> { + meta::path(&[meta::SESSIONS, name, session]) +} + +/// Whether user `name`'s session `session` was ended (`session_path`). +pub(crate) fn session_ended(doc: &impl ReadDoc, name: &str, session: &str) -> bool { + store::read_path(doc, &session_path(name, session)) + .ok() + .flatten() + .is_some() +} + /// The users who may manage users: enabled, with `write` on `_continuum/users`. pub(crate) fn managers(doc: &impl ReadDoc) -> Vec<String> { names(doc) diff --git a/src/access/rules.rs b/src/access/rules.rs index d120961..5c839af 100644 --- a/src/access/rules.rs +++ b/src/access/rules.rs @@ -269,7 +269,9 @@ pub(crate) fn judge_reading( (Some(meta::LOCAL), _) => false, // Logging out: a user revokes its own tokens (`POST /auth/logout`); one who // manages users, anyone's (`DELETE /admin/users/{name}/tokens`). - (Some(meta::LOGOUTS), Some(owner)) => { + // Ending a session: a user its own (`DELETE /auth/session`); one who manages + // users, anyone's. + (Some(meta::LOGOUTS | meta::SESSIONS), Some(owner)) => { change.user == Some(owner) || user_may(path) || user_may(&meta::path(&[meta::USERS])) diff --git a/src/access/token.rs b/src/access/token.rs index d33a3fe..e7aff42 100644 --- a/src/access/token.rs +++ b/src/access/token.rs @@ -89,13 +89,16 @@ pub(crate) enum Refused { Disabled, #[error("revoked: issued before its user's tokens were")] Revoked, + #[error("its session was ended (logged out of)")] + Ended, } /// The claims of `token` if this node takes it: well formed, for `swarm`, signed by a /// node that is a member in `doc` and not banned, not expired at `now` (Unix ms), nor issued later /// than a clock off by `SKEW_MS` would; and in a swarm with a registry, for a /// registered user that is enabled, issued after its tokens were last revoked -/// (`registry::User::tokens_not_before`). +/// (`registry::User::tokens_not_before`), of a session not ended +/// (`registry::session_ended`). pub(crate) fn verify( doc: &impl ReadDoc, token: &str, @@ -150,6 +153,9 @@ pub(crate) fn verify( if claims.iat < user.tokens_not_before { return Err(Refused::Revoked); } + if registry::session_ended(doc, &claims.user, &claims.session) { + return Err(Refused::Ended); + } } Ok(claims) } @@ -290,6 +296,19 @@ mod tests { store::write_path(tx, ®istry::logouts_path("alice"), &json!(1_001)).unwrap() }); assert_eq!(verify(&doc, &token, "s", 1_500), Err(Refused::Revoked)); + // One session ended: its tokens refused, another's taken. + let session = claims_of(&token).session; + change(&mut doc, |tx| { + store::write_path(tx, ®istry::logouts_path("alice"), &json!(0)).unwrap(); + store::write_path( + tx, + ®istry::session_path("alice", &session), + &json!(10_000), + ) + .unwrap() + }); + assert_eq!(verify(&doc, &token, "s", 1_500), Err(Refused::Ended)); + assert!(verify(&doc, &issue(&key, &claims(&key)), "s", 1_500).is_ok()); alice["disabled"] = json!(true); change(&mut doc, |tx| { store::write_path(tx, ®istry::logouts_path("alice"), &json!(0)).unwrap(); |
