diff options
| author | tv <tv@krebsco.de> | 2026-11-07 22:35:21 +0100 |
|---|---|---|
| committer | tv <tv@krebsco.de> | 2026-11-07 22:35:21 +0100 |
| commit | a3e936154e07ba3b95157d26ffeae9364f3d5cef (patch) | |
| tree | 771a46e192e9cee20833cc61c9e7e6dad239baba /src/access | |
| parent | 02e06975d02bd8f02d811b4dd1d45ecad2374861 (diff) | |
access: logouts apart from users' entries
_continuum/logouts/<name> holds when a user's tokens were last revoked,
written by the user itself or by one who may manage users, so users'
entries can be signed by their managers alone.
Auth plan M7.
Diffstat (limited to 'src/access')
| -rw-r--r-- | src/access/registry.rs | 25 | ||||
| -rw-r--r-- | src/access/rules.rs | 9 | ||||
| -rw-r--r-- | src/access/token.rs | 9 |
3 files changed, 24 insertions, 19 deletions
diff --git a/src/access/registry.rs b/src/access/registry.rs index eb7919c..caa263d 100644 --- a/src/access/registry.rs +++ b/src/access/registry.rs @@ -67,7 +67,8 @@ pub(crate) struct User { pub(crate) caps: BTreeMap<String, Capability>, pub(crate) disabled: bool, /// Tokens issued before (Unix ms) are no longer taken (`token::verify`): how all - /// of a user's tokens are revoked at once. + /// of a user's tokens are revoked at once, kept apart from its entry + /// (`logouts_path`), as the user writes it itself. pub(crate) tokens_not_before: u64, /// Its keys (`keys/<id>`, `authorized_keys` lines, `access::sshsig`): what it signs /// values with (`access::envelope`) and logs in with. One that doesn't read is left @@ -100,11 +101,7 @@ impl User { disabled: value .get("disabled") .is_some_and(|disabled| disabled != &JsonValue::Bool(false)), - // A damaged one revokes every token. - tokens_not_before: match value.get("tokensNotBefore") { - None => 0, - Some(at) => at.as_u64().unwrap_or(u64::MAX), - }, + tokens_not_before: 0, keys: value .get("keys") .and_then(JsonValue::as_object) @@ -132,7 +129,21 @@ impl User { /// The entry of user `name`, if it has one. pub(crate) fn user(doc: &impl ReadDoc, name: &str) -> Option<User> { let value = store::read_path(doc, &user_path(name)).ok()??; - value.is_object().then(|| User::from_json(&value)) + let mut user = value.is_object().then(|| User::from_json(&value))?; + // A damaged one revokes every token. + user.tokens_not_before = match store::read_path(doc, &logouts_path(name)) { + Ok(None) => 0, + Ok(Some(at)) => at.as_u64().unwrap_or(u64::MAX), + Err(_) => u64::MAX, + }; + Some(user) +} + +/// Where user `name`'s tokens were last revoked (Unix ms: those issued before aren't +/// taken any more), written by the user itself (`POST /auth/logout`) or by one who may +/// manage users. +pub(crate) fn logouts_path(name: &str) -> Vec<String> { + meta::path(&[meta::LOGOUTS, name]) } /// The users who may manage users: enabled, with `write` on `_continuum/users`. diff --git a/src/access/rules.rs b/src/access/rules.rs index c061893..4df0ba8 100644 --- a/src/access/rules.rs +++ b/src/access/rules.rs @@ -125,12 +125,7 @@ pub(crate) fn judge(doc: &Document, epoch: Epoch, change: &Change<'_>) -> Result // Written by a root only. (Some(meta::EPOCHS | meta::SWARM), _) => false, // Logging out: a user revokes its own tokens (`POST /auth/logout`). - (Some(meta::USERS), Some(owner)) - if path.get(3).map(String::as_str) == Some("tokensNotBefore") - && change.user == Some(owner) => - { - true - } + (Some(meta::LOGOUTS), Some(owner)) => change.user == Some(owner) || user_may(path), (Some(meta::REMAKES), Some(id)) => match path.get(3).map(String::as_str) { // Its coordinator's; or a record carried into a new epoch whole, by the // node that coordinated it (`remake::switch`): its proposal written with it. @@ -312,7 +307,7 @@ mod tests { &["_continuum/neighbours"] ))); // Logging out: its own tokens only. - let tokens = "_continuum/users/bob/tokensNotBefore"; + let tokens = "_continuum/logouts/bob"; assert!(judged(&doc, "a", Some("bob"), &[tokens]).is_ok()); assert!(refused(judged(&doc, "a", Some("alice"), &[tokens]))); } diff --git a/src/access/token.rs b/src/access/token.rs index bfbce62..3af8672 100644 --- a/src/access/token.rs +++ b/src/access/token.rs @@ -265,19 +265,18 @@ mod tests { }); assert_eq!(verify(&doc, &token, "s", 1_500), Err(Refused::UnknownUser)); let mut alice = registry::entry("x", 0, &[(Vec::new(), &[Right::Read][..])]); - alice["tokensNotBefore"] = json!(1_000); change(&mut doc, |tx| { - store::write_path(tx, ®istry::user_path("alice"), &alice).unwrap() + store::write_path(tx, ®istry::user_path("alice"), &alice).unwrap(); + store::write_path(tx, ®istry::logouts_path("alice"), &json!(1_000)).unwrap() }); assert!(verify(&doc, &token, "s", 1_500).is_ok()); - alice["tokensNotBefore"] = json!(1_001); change(&mut doc, |tx| { - store::write_path(tx, ®istry::user_path("alice"), &alice).unwrap() + store::write_path(tx, ®istry::logouts_path("alice"), &json!(1_001)).unwrap() }); assert_eq!(verify(&doc, &token, "s", 1_500), Err(Refused::Revoked)); - alice["tokensNotBefore"] = json!(0); alice["disabled"] = json!(true); change(&mut doc, |tx| { + store::write_path(tx, ®istry::logouts_path("alice"), &json!(0)).unwrap(); store::write_path(tx, ®istry::user_path("alice"), &alice).unwrap() }); assert_eq!(verify(&doc, &token, "s", 1_500), Err(Refused::Disabled)); |
