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 | |
| 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')
| -rw-r--r-- | src/access/registry.rs | 25 | ||||
| -rw-r--r-- | src/access/rules.rs | 9 | ||||
| -rw-r--r-- | src/access/token.rs | 9 | ||||
| -rw-r--r-- | src/api/login.rs | 8 | ||||
| -rw-r--r-- | src/document/meta.rs | 2 |
5 files changed, 30 insertions, 23 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)); diff --git a/src/api/login.rs b/src/api/login.rs index b84a412..72a4d9d 100644 --- a/src/api/login.rs +++ b/src/api/login.rs @@ -8,7 +8,7 @@ //! * `POST /auth/refresh`, with a token: a new one for the same login, signed by this //! node, optionally narrower still (`limit`, within the token's). //! * `POST /auth/logout`: every token of the user, of every login, no longer taken -//! (`tokensNotBefore` in its registry entry). +//! (`_continuum/logouts/<name>`, `registry::logouts_path`). //! * `GET /auth/whoami`: who the request's user is, how it proved it, what it may do. //! //! Every request may bring a token instead of a password: `Authorization: Bearer ct1…` @@ -258,7 +258,8 @@ pub(crate) async fn revoke_tokens(state: &AppState, name: &str, by: &str) -> Res if !state.is_ready().await { return Err(StatusCode::SERVICE_UNAVAILABLE.into()); } - let mut entry = registry::user_path(name); + let entry = registry::user_path(name); + let logouts = registry::logouts_path(name); // Issued in this very millisecond: revoked too. let after = state.clock.now_millis().as_u64() + 1; state @@ -275,8 +276,7 @@ pub(crate) async fn revoke_tokens(state: &AppState, name: &str, by: &str) -> Res if crate::document::store::read_path(&*doc, &entry)?.is_none() { return Err(ApiError(StatusCode::NOT_FOUND, None)); } - entry.push("tokensNotBefore".to_owned()); - crate::document::store::write_path(doc, &entry, &json!(after))?; + crate::document::store::write_path(doc, &logouts, &json!(after))?; Ok(()) }) .await?; diff --git a/src/document/meta.rs b/src/document/meta.rs index df7741a..691cf40 100644 --- a/src/document/meta.rs +++ b/src/document/meta.rs @@ -20,6 +20,7 @@ //! * `access/policy`: whether every node judges received changes (`access::policy`). //! * `access/userSigned`: the paths whose values their users sign (`access::envelope`). //! * `banned/<pk>`: banned nodes (`access::bans`). +//! * `logouts/<name>`: when a user's tokens were last revoked (`access::registry`). /// The key every protocol path starts with. pub(crate) const ROOT: &str = "_continuum"; @@ -36,6 +37,7 @@ pub(crate) const DISSEMINATION: &str = "dissemination"; pub(crate) const USERS: &str = "users"; pub(crate) const ACCESS: &str = "access"; pub(crate) const BANNED: &str = "banned"; +pub(crate) const LOGOUTS: &str = "logouts"; /// The store path `_continuum/<parts…>`. pub(crate) fn path(parts: &[&str]) -> Vec<String> { |
