diff options
| -rw-r--r-- | docs/access.md | 11 | ||||
| -rw-r--r-- | src/access/screen.rs | 44 | ||||
| -rw-r--r-- | src/document/changes.rs | 4 | ||||
| -rw-r--r-- | src/document/inspect.rs | 11 | ||||
| -rw-r--r-- | src/state/host.rs | 9 | ||||
| -rw-r--r-- | src/swarm/sync.rs | 14 |
6 files changed, 59 insertions, 34 deletions
diff --git a/docs/access.md b/docs/access.md index b853b5a..c5365b7 100644 --- a/docs/access.md +++ b/docs/access.md @@ -312,15 +312,16 @@ saying the same; and never makes a node drop what its own rules take. If its rul take the change, the two policies disagree, which it logs. A change whose dependencies haven't come yet waits for them. +A node judges what it receives whether its document has a policy yet or not, each +change by the policy it was written under: a joining node judges its first sync, the +policy's own change and everything after it, as every other node does. While neither +its document nor what it receives holds a policy, it judges only that a root is the +epoch's, and nothing costs more than applying the changes. + What it doesn't do yet: * **Prove the user.** A change is its node's (with signatures required, proven) and its claimed user's: a member could claim a user its operator doesn't have. -* **Judge a joining node's first sync.** An empty document has no policy: what it gets - from the node it joins through is taken as it is, as that node judged it. -* **Agree at once.** The policy applies as each node gets it: a change received between - the policy's arrival on one node and on another may be refused by one and kept by - the other. A node that doesn't know the policy (another version, or a setting it lacks) takes no writes through the API (`409`) and receives without judging, until it is upgraded. diff --git a/src/access/screen.rs b/src/access/screen.rs index 162528b..758c83b 100644 --- a/src/access/screen.rs +++ b/src/access/screen.rs @@ -288,7 +288,15 @@ pub(crate) fn apply( let decoded: Vec<crate::document::ExpandedChange> = ready.iter().map(|change| change.decode()).collect(); - let ops: Vec<Vec<Op>> = Inspector::new(doc).ops(&decoded); + // Without a policy here, nor one among these: none judged but an epoch's root, and + // nothing placed. A change writing the policy writes at its key. + let judging = super::policy::of(&*doc).enforced() + || crate::document::inspect::touch_key(&decoded, "policy"); + let ops: Vec<Vec<Op>> = if judging { + Inspector::new(doc).ops(&decoded) + } else { + vec![Vec::new(); decoded.len()] + }; let paths: Vec<BTreeSet<Path>> = ops .iter() .map(|ops| ops.iter().map(|op| op.path.clone()).collect()) @@ -431,26 +439,32 @@ pub(crate) fn apply( } } let seen_at: Vec<ChangeHash> = seen_at.into_iter().collect(); - let judged = rules::judge( - doc, - epoch, - &rules::Change { - hash, - deps: change.deps(), - seen: &seen_at, - committer, - user: user.as_deref(), - paths: &paths[index], - ops: &ops[index], - }, - ); + let root = change.deps().is_empty(); + let judged = if judging || root { + rules::judge( + doc, + epoch, + &rules::Change { + hash, + deps: change.deps(), + seen: &seen_at, + committer, + user: user.as_deref(), + paths: &paths[index], + ops: &ops[index], + }, + ) + } else { + Ok(()) + }; match judged { Ok(()) => { accepted.push(change.clone()); if let Some(committer) = committer { screened.taken_from.insert(committer.to_owned()); } - if relevant(&paths[index]) || reaches_signed(doc, &paths[index]) { + // A root too: whether another may be one is read in the document. + if root || relevant(&paths[index]) || reaches_signed(doc, &paths[index]) { // What rules read changed: in before the next is judged. screened.applied += accepted.len(); apply_taken(doc, std::mem::take(&mut accepted), log)?; diff --git a/src/document/changes.rs b/src/document/changes.rs index eef7dbc..44d30bd 100644 --- a/src/document/changes.rs +++ b/src/document/changes.rs @@ -156,7 +156,9 @@ pub(crate) fn delta_bytes( Some(bytes) } -/// Loads a delta (`delta_bytes`) into `doc`. +/// Loads a delta (`delta_bytes`) into `doc`, unjudged: for tests; a node screens what it +/// downloads (`access::screen::apply_bytes`). +#[cfg(test)] pub(crate) fn load_delta(doc: &mut Document, bytes: &[u8]) -> Result<()> { if let Some(misplaced) = misplaced_author(bytes) { bail!("Refused the downloaded changes: {misplaced}"); diff --git a/src/document/inspect.rs b/src/document/inspect.rs index c42de82..f0356de 100644 --- a/src/document/inspect.rs +++ b/src/document/inspect.rs @@ -86,6 +86,17 @@ impl Place { } } +/// Whether any of `changes` does anything at a map key `key`: a cheap look, without +/// placing anything, for whether they may write the policy (`access::screen`). +pub(crate) fn touch_key(changes: &[ExpandedChange], key: &str) -> bool { + changes.iter().any(|change| { + change + .operations + .iter() + .any(|op| matches!(&op.key, Key::Map(name) if name.as_str() == key)) + }) +} + /// 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 diff --git a/src/state/host.rs b/src/state/host.rs index e7fc61a..01cbaa2 100644 --- a/src/state/host.rs +++ b/src/state/host.rs @@ -571,8 +571,9 @@ impl Host for AppState { let result = { let mut replica = state.doc.blocking_write(); let policy = crate::access::policy::of(&replica.doc); - let result = if policy.enforced() { - // Judged first (`access::screen`). + // Judged first (`access::screen`), whether this document has a policy + // yet or not. + let result = { let (epoch, epoch_seq) = (replica.epoch, replica.epoch_seq); let replica = &mut *replica; crate::access::screen::apply_bytes( @@ -586,10 +587,6 @@ impl Host for AppState { ) .map(|_| ()) .map_err(|error| format!("{error:#}")) - } else { - replica - .apply_changes([change]) - .map_err(|error| error.to_string()) }; replica.refresh_heads(); result diff --git a/src/swarm/sync.rs b/src/swarm/sync.rs index ffdbf07..75c20f1 100644 --- a/src/swarm/sync.rs +++ b/src/swarm/sync.rs @@ -641,10 +641,11 @@ pub(crate) async fn handle_sync( replica .logged_signed(|doc, known, log| { let result = (|| { - // Judged first, as the policy has it (`access::screen`); with - // signatures required, by those it sent too. + // Judged first, each as the policy it was written under has it + // (`access::screen`), whether this document has one yet or not; + // with signatures required, by those it sent too. let policy = crate::access::policy::of(&*doc); - if policy.enforced() { + { let bytes = message.take_changes(); let required = policy.required(); if required { @@ -955,8 +956,9 @@ async fn download_and_apply_delta( } let heads_before = replica.heads().to_vec(); let policy = crate::access::policy::of(&replica.doc); - if policy.enforced() { - // Judged first (`access::screen`). + { + // Judged first (`access::screen`), whether this document has a policy yet or + // not. let epoch_seq = replica.epoch_seq; let replica = &mut *replica; crate::access::screen::apply_bytes( @@ -968,8 +970,6 @@ async fn download_and_apply_delta( policy.required().then_some(&mut replica.signatures), &mut crate::document::PatchLog::inactive(), )?; - } else { - crate::document::changes::load_delta(&mut replica.doc, &bytes)?; } replica.refresh_heads(); // Signatures waiting for what it brought (`Signatures::offer`). |
