diff options
| author | tv <tv@krebsco.de> | 2026-11-08 09:39:08 +0000 |
|---|---|---|
| committer | tv <tv@krebsco.de> | 2026-11-08 14:28:47 +0100 |
| commit | e959799c95b4d6e33f6b95d4c38a5386cc1dcf02 (patch) | |
| tree | f1bf729b42b8615651c44ea8776bc63a9d83fbbf | |
| parent | f2fbee33e219fdfe7b67878dc8bfa7eeb526fa65 (diff) | |
access: maps made on the way judged by what they hold
A write makes the maps on its way where there are none
(store::write_path); the rules judged each made map as a write at its
own path, so a user whose capability lies below a map not made yet
(write on servers/eu, a first PUT at servers/eu/0) had its first write
taken by its node and refused by every other. A map made where there
was none, in a change writing within it, is now judged by what is
written within it.
Review finding 3 (parent maps created on the way).
| -rw-r--r-- | docs/access.md | 2 | ||||
| -rw-r--r-- | src/access/rules.rs | 84 |
2 files changed, 86 insertions, 0 deletions
diff --git a/docs/access.md b/docs/access.md index f1ed1c3..d46627a 100644 --- a/docs/access.md +++ b/docs/access.md @@ -228,6 +228,8 @@ stays as it is, wherever it comes from later (a joining node catching up, say). * A user's tokens revoked by that user too (logging out). * 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 + first write at `servers/eu/0`), as what it writes within it. * A node that isn't a member may only register itself; a banned node nothing (Banning a node, below). So a node registers before anything else it writes, through the API or its own: a change refused stays out with every change built on it. diff --git a/src/access/rules.rs b/src/access/rules.rs index 185996a..78557b0 100644 --- a/src/access/rules.rs +++ b/src/access/rules.rs @@ -18,6 +18,8 @@ //! user too (logging out); a member's forget record by that member too (cancelling it, //! alive); the rest by a user who may write there. //! * User data: by a user who may write there. +//! * A map made on the way to what a change writes, where there was none: as what is +//! written within it. //! * Under a strict policy (`access::policy`), the values their users sign //! (`access::envelope`) only as they signed them (`signed_values`). //! * Where the swarm declares values sealed (`access::sealed`), only those sealed as @@ -123,7 +125,26 @@ pub(crate) fn judge(doc: &Document, epoch: Epoch, change: &Change<'_>) -> Result (true, Some(user)) => registry::caps_of(&at, user).allows(Right::Write, path), (true, None) => false, }; + // Maps made on the way to what the change writes, where there were none + // (`store::write_path` makes them): judged by what is written within them. + let on_the_way: BTreeSet<&Path> = change + .paths + .iter() + .filter(|path| { + let mut here = change.ops.iter().filter(|op| op.path == **path).peekable(); + here.peek().is_some() + && here.all(|op| op.action == Action::MakeMap) + && change + .paths + .iter() + .any(|other| other.len() > path.len() && other.starts_with(path)) + && store::read_path(&at, path).ok().flatten().is_none() + }) + .collect(); for path in change.paths { + if on_the_way.contains(path) { + continue; + } if !meta::is_protocol(path) { if !user_may(path) { return refuse(format!("{} may not write {}", who(change), shown(path))); @@ -741,6 +762,69 @@ mod tests { assert!(judged(&doc, "stranger", Some("mallory"), &["notes/x"]).is_ok()); } + /// A map made on the way to what is written, where there was none: judged by what is + /// written within it; one made where there is one, or holding nothing written, as + /// any write there. + #[test] + fn maps_made_on_the_way() { + let mut doc = swarm(true); + write( + &mut doc, + "_continuum/users/bob", + registry::entry("a", 2, &[(path("servers/eu"), &[Right::Write][..])]), + ); + let judged_ops = |doc: &Document, ops: Vec<Op>| { + let deps = doc.get_heads(); + let touched: BTreeSet<Path> = ops.iter().map(|op| op.path.clone()).collect(); + judge( + doc, + Epoch::GENESIS, + &Change { + hash: ChangeHash([9; 32]), + deps: &deps, + seen: &deps, + committer: Some("a"), + user: Some("bob"), + paths: &touched, + ops: &ops, + }, + ) + }; + let op = |at: &str, action: Action| Op { + path: path(at), + action, + }; + let first = vec![ + op("servers", Action::MakeMap), + op("servers/eu", Action::MakeMap), + op("servers/eu/0", Action::Other), + ]; + assert!(judged_ops(&doc, first).is_ok()); + // Nothing written within: a map bob may not make. + assert!(refused(judged_ops( + &doc, + vec![op("servers", Action::MakeMap)] + ))); + // Within it, where bob may not write. + assert!(refused(judged_ops( + &doc, + vec![ + op("servers", Action::MakeMap), + op("servers/us", Action::Other) + ] + ))); + // Where there is one already: replacing it. + write(&mut doc, "servers", json!({"us": 1})); + assert!(refused(judged_ops( + &doc, + vec![ + op("servers", Action::MakeMap), + op("servers/eu", Action::MakeMap), + op("servers/eu/0", Action::Other) + ] + ))); + } + /// Roots: the first change of a swarm's first epoch, a later epoch's snapshot. #[test] fn roots() { |
