From 47c3c7842f6c31af18cfaa2d84ada5dc83b9831b Mon Sep 17 00:00:00 2001 From: tv Date: Sun, 8 Nov 2026 09:33:41 +0000 Subject: 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. --- docs/access.md | 4 +++- src/access/rules.rs | 37 +++++++++++++++++++++++++++++++++++-- src/access/screen.rs | 5 +++++ 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/`) 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(); -- cgit v1.3.1