aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/access/registry.rs25
-rw-r--r--src/access/rules.rs9
-rw-r--r--src/access/token.rs9
-rw-r--r--src/api/login.rs8
-rw-r--r--src/document/meta.rs2
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, &registry::user_path("alice"), &alice).unwrap()
+ store::write_path(tx, &registry::user_path("alice"), &alice).unwrap();
+ store::write_path(tx, &registry::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, &registry::user_path("alice"), &alice).unwrap()
+ store::write_path(tx, &registry::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, &registry::logouts_path("alice"), &json!(0)).unwrap();
store::write_path(tx, &registry::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> {