diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/api/store.rs | 14 | ||||
| -rw-r--r-- | src/document/inspect.rs | 67 |
2 files changed, 75 insertions, 6 deletions
diff --git a/src/api/store.rs b/src/api/store.rs index 16dd966..e43d129 100644 --- a/src/api/store.rs +++ b/src/api/store.rs @@ -472,7 +472,7 @@ pub(crate) async fn put_store_entry( let path = if own_entry { crate::access::registry::user_path(&user.0) } else { - parse_write_path(&state, &user, &access, &key, "PUT")? + parse_write_path(&state, &user, &access, &key, "PUT").await? }; let sign = Sign::Write { path: path.clone(), @@ -662,8 +662,9 @@ fn parse_key_path(key: &str) -> Result<Vec<String>, StatusCode> { /// what every node acts on, so a write there acts on the whole swarm, and each is /// logged. Epoch metadata (and `_continuum` as a whole, which contains it) is never /// writable: the protocol reads it at the snapshot change, so an edit only misleads -/// whoever reads it later; nor is the swarm ID. -fn parse_write_path( +/// whoever reads it later; nor is the swarm ID. A path inside a list or text is +/// judged where the rules judge the write (`inspect::placed`): at the list. +async fn parse_write_path( state: &AppState, user: &AuthenticatedUser, access: &Access, @@ -671,12 +672,13 @@ fn parse_write_path( method: &str, ) -> Result<Vec<String>, StatusCode> { let path = parse_key_path(key)?; + let placed = crate::document::inspect::placed(&state.doc.read().await.doc, &path); let protocol = meta::is_protocol(&path); // `_continuum` itself, epoch metadata and the swarm ID: nobody. let fixed = protocol && (path.len() < 2 || path[1] == meta::EPOCHS || path[1] == meta::SWARM); let allowed = match access { Access::Everything => !protocol || state.protocol_writers.contains(&user.0), - Access::Caps(caps) => caps.allows(crate::access::Right::Write, &path), + Access::Caps(caps) => caps.allows(crate::access::Right::Write, &placed), }; if !protocol && allowed { return Ok(path); @@ -936,7 +938,7 @@ pub(crate) async fn post_store_entry( if !state.is_ready().await { return Err(StatusCode::SERVICE_UNAVAILABLE.into()); } - let path = parse_write_path(&state, &user, &access, &key, "POST")?; + let path = parse_write_path(&state, &user, &access, &key, "POST").await?; let sign = Sign::Write { path: path.clone(), asked: query.signed, @@ -1050,7 +1052,7 @@ pub(crate) async fn delete_store_entry( Query(query): Query<DeleteQuery>, RawQuery(raw_query): RawQuery, ) -> Result<Response, ApiError> { - let path = parse_write_path(&state, &user, &access, &key, "DELETE")?; + let path = parse_write_path(&state, &user, &access, &key, "DELETE").await?; let sign = Sign::Write { path: path.clone(), asked: query.signed, 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(); |
