diff options
| -rw-r--r-- | docs/access.md | 5 | ||||
| -rw-r--r-- | src/access/rules.rs | 50 |
2 files changed, 49 insertions, 6 deletions
diff --git a/docs/access.md b/docs/access.md index d46627a..3bc824b 100644 --- a/docs/access.md +++ b/docs/access.md @@ -220,12 +220,13 @@ written before the policy (at its dependencies) is judged by none: history from stays as it is, wherever it comes from later (a joining node catching up, say). * A member's entry (`_continuum/members/<id>`) and its votes are written by that member - only; removing an entry, also by a user who may write there. + only; removing an entry, also by a user who may write there, or who forgets the + member in the same change (`DELETE /peers/{pk}`, with `_continuum/forgotten`). * Epochs and the swarm ID (`_continuum/epochs`, `_continuum/swarm`) by an epoch's first change only (the genesis change, a remake's snapshot). * A remake's decision by the coordinator its proposal names; the rest of a record by any member. -* A user's tokens revoked by that user too (logging out). +* A user's tokens revoked by that user too (logging out), and by one who manages users. * A member's forget record cancelled by that member too (forgotten while alive). * Everything else, protocol data or not, by a user the registry lets write there. * A map made on the way to what a change writes, where there was none (`servers`, for a diff --git a/src/access/rules.rs b/src/access/rules.rs index 78557b0..0de901a 100644 --- a/src/access/rules.rs +++ b/src/access/rules.rs @@ -154,15 +154,31 @@ pub(crate) fn judge(doc: &Document, epoch: Epoch, change: &Change<'_>) -> Result let part = path.get(1).map(String::as_str); let owner = path.get(2).map(String::as_str); let allowed = match (part, owner) { - // A member's entry, its votes: its own. Removing an entry, also a user's. - (Some(meta::MEMBERS), Some(owner)) => owner == node || user_may(path), + // A member's entry, its votes: its own. Removing an entry, also a user's: one + // who may write it, or forgets the member in the same change + // (`DELETE /peers/{pk}`, which needs `_continuum/forgotten` alone). + (Some(meta::MEMBERS), Some(owner)) => { + let forgetting = meta::path(&[meta::FORGOTTEN, owner]); + owner == node + || user_may(path) + || (change + .paths + .iter() + .any(|other| other.starts_with(&forgetting)) + && user_may(&forgetting)) + } (Some(meta::VOTES), Some(owner)) => owner == node, // What holds them, made by whichever member first writes in it. (Some(meta::MEMBERS | meta::VOTES | meta::REMAKES), None) => true, // Written by a root only. (Some(meta::EPOCHS | meta::SWARM), _) => false, - // Logging out: a user revokes its own tokens (`POST /auth/logout`). - (Some(meta::LOGOUTS), Some(owner)) => change.user == Some(owner) || user_may(path), + // 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)) => { + change.user == Some(owner) + || user_may(path) + || user_may(&meta::path(&[meta::USERS])) + } // A node forgotten while alive cancels its own record (`swarm::peer::forget`). (Some(meta::FORGOTTEN), Some(owner)) => owner == node || user_may(path), (Some(meta::REMAKES), Some(id)) => match path.get(3).map(String::as_str) { @@ -463,6 +479,32 @@ mod tests { let tokens = "_continuum/logouts/bob"; assert!(judged(&doc, "a", Some("bob"), &[tokens]).is_ok()); assert!(refused(judged(&doc, "a", Some("alice"), &[tokens]))); + // Revoked by one who manages users. + let mut managed = swarm(true); + write( + &mut managed, + "_continuum/users/carol", + registry::entry("a", 2, &[(path("_continuum/users"), &[Right::Write][..])]), + ); + assert!(judged(&managed, "a", Some("carol"), &[tokens]).is_ok()); + // A member forgotten, its entry removed with it, by one who may forget only. + write( + &mut managed, + "_continuum/users/dave", + registry::entry( + "a", + 3, + &[(path("_continuum/forgotten"), &[Right::Write][..])], + ), + ); + let forgetting = ["_continuum/forgotten/b", "_continuum/members/b"]; + assert!(judged(&managed, "a", Some("dave"), &forgetting).is_ok()); + assert!(refused(judged( + &managed, + "a", + Some("dave"), + &["_continuum/members/b"] + ))); // A forget record: cancelled by the node it names, not by another. let forgotten = "_continuum/forgotten/a"; assert!(judged(&doc, "a", None, &[forgotten]).is_ok()); |
