aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authortv <tv@krebsco.de>2026-11-08 09:33:41 +0000
committertv <tv@krebsco.de>2026-11-08 10:36:32 +0100
commit47c3c7842f6c31af18cfaa2d84ada5dc83b9831b (patch)
tree280530bae675ddb51fcc5a93e657f1016d6bdda1
parentf713fe668c29ecd60df547d989b0ace40cfe4da0 (diff)
access: judged by the policy written under
The rules read the policy at the change's dependencies only for strict mode, so history from before the policy was judged by today's rules when a late node caught up, or when REFUSALS had it judged again. A change written while the document had no policy, or one this node doesn't know, is now judged by none. Review finding 6.
-rw-r--r--docs/access.md4
-rw-r--r--src/access/rules.rs37
-rw-r--r--src/access/screen.rs5
3 files changed, 43 insertions, 3 deletions
diff --git a/docs/access.md b/docs/access.md
index 8443394..f1ed1c3 100644
--- a/docs/access.md
+++ b/docs/access.md
@@ -215,7 +215,9 @@ curl -X PUT localhost:3000/store/_continuum/access/policy \
A change is judged by what it touches, by the node that committed it, and by the user
it names, as the document stood when it was written (at its dependencies): a
-revocation since refuses nothing it allowed then, and every node judges alike.
+revocation since refuses nothing it allowed then, and every node judges alike. One
+written before the policy (at its dependencies) is judged by none: history from before
+stays as it is, wherever it comes from later (a joining node catching up, say).
* A member's entry (`_continuum/members/<id>`) and its votes are written by that member
only; removing an entry, also by a user who may write there.
diff --git a/src/access/rules.rs b/src/access/rules.rs
index 96ee9d4..185996a 100644
--- a/src/access/rules.rs
+++ b/src/access/rules.rs
@@ -24,7 +24,8 @@
//! declared (`sealed_values`).
//!
//! In a swarm without a user registry (`access::registry`), whatever a user would need,
-//! any member may.
+//! any member may. A change written while the document had no policy (at its
+//! dependencies) is judged by none: the rules hold from the policy on.
use std::collections::BTreeSet;
@@ -92,6 +93,12 @@ pub(crate) fn judge(doc: &Document, epoch: Epoch, change: &Change<'_>) -> Result
};
}
let at = At::new(doc, change.seen).ok_or(Refusal::Unready)?;
+ // Written while the document had no policy (or one this node doesn't know): judged
+ // as it was then, by none. History from before the policy stays as it is.
+ let policy = super::policy::of(&at);
+ if !policy.enforced() {
+ return Ok(());
+ }
let refuse = |why: String| Err(Refusal::Rule(why));
let Some(node) = change.committer else {
return refuse("it names no node".to_owned());
@@ -161,7 +168,7 @@ pub(crate) fn judge(doc: &Document, epoch: Epoch, change: &Change<'_>) -> Result
return refuse(format!("{} may not write {}", who(change), shown(path)));
}
}
- if super::policy::of(&at).strict() {
+ if policy.strict() {
signed_values(&at, change.ops).or_else(refuse)?;
}
let declared = super::sealed::declared(&at);
@@ -332,6 +339,7 @@ mod tests {
fn swarm(users: bool) -> Document {
let mut doc = Document::new();
write(&mut doc, "_continuum/swarm", json!("s"));
+ write(&mut doc, "_continuum/access/policy", json!({"version": 1}));
write(&mut doc, "_continuum/members/a", json!({"since": 1}));
write(&mut doc, "_continuum/members/b", json!({"since": 1}));
write(
@@ -708,6 +716,31 @@ mod tests {
assert!(judged(vec![op("notes/x", Action::PutString("plain".to_owned()))]).is_ok());
}
+ /// Written before the policy: judged by none, whatever it does; from the policy on,
+ /// by the rules.
+ #[test]
+ fn before_the_policy() {
+ let mut doc = Document::new();
+ write(&mut doc, "_continuum/swarm", json!("s"));
+ write(&mut doc, "_continuum/members/a", json!({"since": 1}));
+ write(
+ &mut doc,
+ "_continuum/users/alice",
+ registry::entry("a", 1, &[]),
+ );
+ assert!(judged(&doc, "stranger", Some("mallory"), &["notes/x"]).is_ok());
+ write(&mut doc, "_continuum/access/policy", json!({"version": 1}));
+ assert!(refused(judged(
+ &doc,
+ "stranger",
+ Some("mallory"),
+ &["notes/x"]
+ )));
+ // A policy this node doesn't know: judged by none either.
+ write(&mut doc, "_continuum/access/policy", json!({"version": 9}));
+ assert!(judged(&doc, "stranger", Some("mallory"), &["notes/x"]).is_ok());
+ }
+
/// Roots: the first change of a swarm's first epoch, a later epoch's snapshot.
#[test]
fn roots() {
diff --git a/src/access/screen.rs b/src/access/screen.rs
index 8d23b47..35a2af7 100644
--- a/src/access/screen.rs
+++ b/src/access/screen.rs
@@ -579,6 +579,11 @@ mod tests {
};
transact(&mut theirs, UnixMillis(1), &writer, |doc| {
store::write_path(doc, &path("_continuum/swarm"), &json!("s"))?;
+ store::write_path(
+ doc,
+ &path("_continuum/access/policy"),
+ &json!({"version": 1}),
+ )?;
store::write_path(doc, &path("_continuum/users/alice"), &alice)
})
.unwrap();