From a2056647b6351797880741ea7d845657877587ed Mon Sep 17 00:00:00 2001 From: tv Date: Sun, 8 Nov 2026 19:01:55 +0000 Subject: store: a write inside a list checked where the rules judge it Review 2, N9: the API checked a write's capability on the path asked (servers/list/3/name), the rules on the list it is in (servers/list): a user whose capability lay inside a list had every such write pass the API and refused by the node's own judgement (403, after rolling the document back). inspect::placed places a write's path in the document by the rule the inspector places operations by (Place::child: cut after the first list or text on the way), and the store's writes check capabilities there. One rule for both; a test checks placed against the paths the inspector finds in the change such a write makes. --- src/document/inspect.rs | 67 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) (limited to 'src/document/inspect.rs') diff --git a/src/document/inspect.rs b/src/document/inspect.rs index d395008..c42de82 100644 --- a/src/document/inspect.rs +++ b/src/document/inspect.rs @@ -86,6 +86,33 @@ impl Place { } } +/// Where a write at `path` in `doc` is, as rules see it (`Inspector`): placed by the +/// same rule (`Place::child`), so cut after the first list or text on the way, which +/// what is inside one touches. Where `doc` holds nothing (or a scalar) on the way, the +/// rest is as asked: the write makes maps there. For the API's checks, so they and the +/// rules judge the same path. +pub(crate) fn placed(doc: &Document, path: &[String]) -> Path { + let mut place = Place::root(); + let mut object = automerge::ROOT; + for (index, key) in path.iter().enumerate() { + if place.in_list { + break; + } + place = place.child(&Key::Map(key.as_str().into())); + match doc.get(&object, key.as_str()) { + Ok(Some((automerge::Value::Object(kind), id))) => { + place.in_list = matches!(kind, automerge::ObjType::List | automerge::ObjType::Text); + object = id; + } + _ => { + place.path.extend(path[index + 1..].iter().cloned()); + break; + } + } + } + place.path +} + /// Finds the paths changes touch in `doc`, with the places of the objects it looked up /// kept for the next changes. pub(crate) struct Inspector<'a> { @@ -296,6 +323,46 @@ mod tests { } /// Inside a list or text, a path stops at it: items move. + /// A write's place in the document, as the rules see the change it makes: cut after + /// a list or text; as asked where nothing is yet. + #[test] + fn writes_placed_as_rules_see_them() { + let mut doc = Document::new(); + change(&mut doc, |doc| { + store::write_path( + doc, + &path(&["servers", "list"]), + &serde_json::json!([{"name": "a"}]), + ) + .unwrap(); + store::write_path(doc, &path(&["config", "mode"]), &serde_json::json!("fast")).unwrap(); + }); + let placed = |at: &[&str]| super::placed(&doc, &path(at)); + assert_eq!( + placed(&["servers", "list", "0", "name"]), + path(&["servers", "list"]) + ); + assert_eq!(placed(&["servers", "list"]), path(&["servers", "list"])); + assert_eq!(placed(&["config", "mode"]), path(&["config", "mode"])); + assert_eq!( + placed(&["config", "new", "deep"]), + path(&["config", "new", "deep"]) + ); + assert_eq!(placed(&["nothing", "here"]), path(&["nothing", "here"])); + // As the change of such a write touches it. + let before = doc.get_heads(); + change(&mut doc, |doc| { + store::write_path( + doc, + &path(&["servers", "list", "0", "name"]), + &serde_json::json!("b"), + ) + .unwrap(); + }); + let touched = Inspector::new(&doc.fork_at(&before).unwrap()).paths(&decoded(&doc, &before)); + assert_eq!(touched, vec![[path(&["servers", "list"])].into()]); + } + #[test] fn lists_and_text_stop_paths() { let mut doc = Document::new(); -- cgit v1.3.1