From 645bae8fb8065a353237d24aff274009f37d694e Mon Sep 17 00:00:00 2001 From: tv Date: Sat, 7 Nov 2026 22:35:21 +0100 Subject: access: a node registers before it writes Under a policy, a change by a node that isn't a member is refused with all built on it: a node's registration is written before any write of its own or through the API, not after (the registration itself exempt). Auth plan M6 (fix). --- src/state/mod.rs | 102 ++++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 83 insertions(+), 19 deletions(-) (limited to 'src/state') diff --git a/src/state/mod.rs b/src/state/mod.rs index 294b446..7ee0acb 100644 --- a/src/state/mod.rs +++ b/src/state/mod.rs @@ -474,6 +474,21 @@ impl AppState { .map(|(out, _)| out) } + /// [`mutate_doc`](Self::mutate_doc) of this node's registration in the member list + /// (`swarm::members::ensure_registered`): the one write not preceded by it. + pub(crate) async fn mutate_doc_registering( + &self, + f: impl FnOnce(&mut Transaction<'_>) -> Result + Send + 'static, + ) -> Result + where + T: Send + 'static, + E: From + Send + 'static, + { + self.mutate_doc_as(None, Sign::Protocol, false, f) + .await + .map(|(out, _)| out) + } + /// [`mutate_doc`](Self::mutate_doc) for `user`, who wrote through the API: the /// change names them (`replica::Writer`), and is signed as `sign` says, with this /// node's key. With the heads of the document written to after the @@ -484,6 +499,22 @@ impl AppState { sign: Sign, f: impl FnOnce(&mut Transaction<'_>) -> Result + Send + 'static, ) -> Result<(T, Vec), E> + where + T: Send + 'static, + E: From + Send + 'static, + { + self.mutate_doc_as(user, sign, true, f).await + } + + /// [`mutate_doc_for`](Self::mutate_doc_for); with `registering_first`, after this + /// node's registration if the policy needs it first. + async fn mutate_doc_as( + &self, + user: Option, + sign: Sign, + registering_first: bool, + f: impl FnOnce(&mut Transaction<'_>) -> Result + Send + 'static, + ) -> Result<(T, Vec), E> where T: Send + 'static, E: From + Send + 'static, @@ -525,6 +556,29 @@ impl AppState { heads.sort_unstable(); return Ok((out, heads)); } + // Under a policy judging every change, a change by a node that isn't a + // member is refused everywhere, and so is every change built on it: a + // write before this node registered (`ensure_registered`), through the API + // or its own, comes after its registration. + if registering_first + && crate::access::policy::of(&replica.doc).enforced() + && !crate::swarm::members::registered(&replica.doc, state.local_id()) + { + let path = crate::swarm::members::member_path(state.local_id()); + let entry = crate::swarm::members::entry(&state); + let writer = crate::document::replica::Writer { + node: state.local_id(), + user: None, + }; + let before = replica.doc.get_heads(); + replica + .transact(state.clock.now_millis(), &writer, |doc| { + crate::document::store::write_path(doc, &path, &entry) + }) + .map_err(E::from)?; + state.sign_made(&mut replica, &before, &Sign::Protocol); + tracing::info!("Registered in the swarm's member list, before writing"); + } // One API write = one change; on error, its partial writes (e.g. the old // value deleted, the new one not written) are dropped. let writer = crate::document::replica::Writer { @@ -533,25 +587,8 @@ impl AppState { }; let before = replica.doc.get_heads(); let result = replica.transact(state.clock.now_millis(), &writer, f); - if result.is_ok() - && let Some(key) = &state.node_key - { - // The change made, if any: the one head after it, as it builds on - // every head before. - let after = replica.doc.get_heads(); - if after != before - && let [hash] = after[..] - { - if state.sign_every == SignEvery::Change - || sign.wanted(&replica.doc) - || crate::access::policy::of(&replica.doc).required() - { - let replica = &mut *replica; - replica.signatures.sign(&replica.doc, hash, key); - } else { - replica.signatures.left_unsigned(hash); - } - } + if result.is_ok() { + state.sign_made(&mut replica, &before, &sign); } replica.refresh_heads(); result.map(|out| (out, replica.heads().to_vec())) @@ -566,6 +603,33 @@ impl AppState { .expect("document write task panicked") } + /// Signs the change just made in `replica`, if any (the one head after `before`, as + /// it builds on every head before), as `sign` says, or as `--sign-every` or the + /// policy has it; or keeps it as left unsigned. + fn sign_made( + &self, + replica: &mut crate::document::replica::Replica, + before: &[crate::document::ChangeHash], + sign: &Sign, + ) { + let Some(key) = &self.node_key else { + return; + }; + let after = replica.doc.get_heads(); + if after != before + && let [hash] = after[..] + { + if self.sign_every == SignEvery::Change + || sign.wanted(&replica.doc) + || crate::access::policy::of(&replica.doc).required() + { + replica.signatures.sign(&replica.doc, hash, key); + } else { + replica.signatures.left_unsigned(hash); + } + } + } + /// This node's latest change signed, if no signature vouches for it yet, every /// `every` (`--sign-every`): every change it makes is vouched for within that. pub(crate) async fn sign_latest_every(self, every: Duration) { -- cgit v1.3.1