aboutsummaryrefslogtreecommitdiffstats
path: root/src/access
diff options
context:
space:
mode:
authortv <tv@krebsco.de>2026-11-08 09:39:08 +0000
committertv <tv@krebsco.de>2026-11-08 14:28:47 +0100
commite959799c95b4d6e33f6b95d4c38a5386cc1dcf02 (patch)
treef1bf729b42b8615651c44ea8776bc63a9d83fbbf /src/access
parentf2fbee33e219fdfe7b67878dc8bfa7eeb526fa65 (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).
Diffstat (limited to 'src/access')
-rw-r--r--src/access/rules.rs84
1 files changed, 84 insertions, 0 deletions
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() {