aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--docs/access.md4
-rw-r--r--src/api/store.rs14
-rw-r--r--src/document/inspect.rs67
3 files changed, 78 insertions, 7 deletions
diff --git a/docs/access.md b/docs/access.md
index d1f69e1..b853b5a 100644
--- a/docs/access.md
+++ b/docs/access.md
@@ -59,7 +59,9 @@ Every request is checked against the registry, as the node serving it holds it:
change to it counts on a node as soon as it has reached it.
* **The store** (`/store`, `/stream`): writing a path (`PUT`, `POST`, `DELETE`) takes
- `write` on it; `?prune=true` deletes maps left empty only up to the highest one the
+ `write` on it, or, inside a list or text, on the list, as every node judges the
+ change (a list's items have no paths of their own, Changes judged on every node,
+ below); `?prune=true` deletes maps left empty only up to the highest one the
user may write. Reading takes `read`: of a place the user may read only partly, it
sees the keys on the way to what it may read, as a directory one may pass through
but not list (a capability on `servers/eu` shows `servers` with `eu` alone in it),
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();