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. --- src/access/rules.rs | 37 +++++++++++++++++++++++++++++++++++-- src/access/screen.rs | 5 +++++ 2 files changed, 40 insertions(+), 2 deletions(-) (limited to 'src') 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