aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--docs/access.md2
-rw-r--r--src/access/rules.rs84
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() {