aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/access/screen.rs75
1 files changed, 73 insertions, 2 deletions
diff --git a/src/access/screen.rs b/src/access/screen.rs
index 35a2af7..44d96ec 100644
--- a/src/access/screen.rs
+++ b/src/access/screen.rs
@@ -367,7 +367,7 @@ pub(crate) fn apply(
if let Some(committer) = committer {
screened.taken_from.insert(committer.to_owned());
}
- if relevant(&paths[index]) {
+ if relevant(&paths[index]) || reaches_signed(doc, &paths[index]) {
// What rules read changed: in before the next is judged.
screened.applied += accepted.len();
doc.apply_changes_log_patches(std::mem::take(&mut accepted), log)?;
@@ -470,7 +470,9 @@ pub(crate) fn apply_bytes(
/// Whether a change touching `paths` changes what rules read: the members, the user
/// registry, remake records, the policy and the rest of `_continuum/access`, bans, the
-/// protocol data as a whole.
+/// protocol data as a whole; and, under a strict policy, values their users sign
+/// (`reaches_signed`). A rule that reads anything else (`rules::judge`) must add it
+/// here, or a batch judged at its frontier is judged at heads it doesn't hold.
fn relevant(paths: &BTreeSet<Path>) -> bool {
paths.iter().any(|path| match path.as_slice() {
[] => true,
@@ -487,6 +489,20 @@ fn relevant(paths: &BTreeSet<Path>) -> bool {
})
}
+/// Whether a change touching `paths` changes values their users sign, under a strict
+/// policy, at, within or above one (`envelope::reaches_signed`): the rules read the
+/// value at a signed place (`prev`) and what holds one, so it goes in before the next
+/// is judged.
+fn reaches_signed(doc: &Document, paths: &BTreeSet<Path>) -> bool {
+ if !super::policy::of(doc).strict() {
+ return false;
+ }
+ let signed = super::envelope::signed_paths(doc);
+ paths
+ .iter()
+ .any(|path| super::envelope::reaches_signed(&signed, path))
+}
+
/// `candidates` in an order where every change comes after its dependencies among them
/// (ready), and those with a dependency neither in `doc` nor among them, nor ready
/// (unready).
@@ -645,6 +661,61 @@ mod tests {
assert!(screening.refusing("b").is_none());
}
+ /// Under a strict policy, two signed values written one after the other at the same
+ /// path, received in one batch: both taken, the second judged with the first in.
+ #[test]
+ fn successive_signed_values_in_one_batch() {
+ use crate::access::{envelope, sshsig};
+ let key = ed25519_dalek::SigningKey::from_bytes(&[7; 32]);
+ let mut theirs = Document::new();
+ let mut alice = registry::entry("a", 1, &[(path("config"), &[Right::Write][..])]);
+ alice["keys"] = json!({ "k": sshsig::PublicKey(key.verifying_key().to_bytes()).line() });
+ let writer = Writer {
+ node: "a",
+ user: None,
+ };
+ 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, "values": "signed"}),
+ )?;
+ store::write_path(
+ doc,
+ &path("_continuum/access/userSigned"),
+ &json!([["config"]]),
+ )?;
+ store::write_path(doc, &path("_continuum/users/alice"), &alice)
+ })
+ .unwrap();
+ write(&mut theirs, "a", None, "_continuum/members/a", json!({}));
+ let sealed = |prev: &str, value: serde_json::Value| {
+ let payload = envelope::Payload {
+ path: path("config"),
+ value,
+ user: "alice".to_owned(),
+ at: 1,
+ prev: prev.to_owned(),
+ };
+ let bytes = serde_json::to_vec(&payload).unwrap();
+ envelope::seal(&bytes, &sshsig::sign(&key, envelope::NAMESPACE, &bytes)).unwrap()
+ };
+ let first = sealed("", json!(1));
+ let second = sealed(&envelope::digest(Some(&first)), json!(2));
+ write(&mut theirs, "a", Some("alice"), "config", json!(first));
+ write(&mut theirs, "a", Some("alice"), "config", json!(second));
+
+ let mut ours = Document::new();
+ let screened = screen(&mut ours, received(&theirs, &[]), &mut Waiting::default());
+ let why: Vec<&str> = screened.refused.iter().map(|r| r.why.as_str()).collect();
+ assert!(why.is_empty(), "{why:?}");
+ assert_eq!(
+ store::read_path(&ours, &path("config")).unwrap(),
+ Some(json!(second))
+ );
+ }
+
/// A refused change offered again: refused by its hash, not judged or kept again,
/// and so is a new one building on it.
#[test]