aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/simtest/policy.rs155
1 files changed, 154 insertions, 1 deletions
diff --git a/src/simtest/policy.rs b/src/simtest/policy.rs
index d425ecf..60a9f6b 100644
--- a/src/simtest/policy.rs
+++ b/src/simtest/policy.rs
@@ -2,7 +2,8 @@
//! `access::screen`), with signatures required: honest writes go everywhere, a joining
//! node registers, a write a node lets through for a user the registry doesn't have
//! stays where it was made, and a remake carries the swarm over without refusing
-//! anything.
+//! anything. A strict one (`"values": "signed"`) takes the values its users sign, and
+//! nothing else there.
use std::time::Duration;
@@ -162,3 +163,155 @@ async fn a_remake_under_the_policy() {
assert_eq!(refused(node).await, 0, "an honest change was refused");
}
}
+
+/// Writes `value` at `at` on `node`, signed by `user` with `key` (`access::envelope`),
+/// replacing the envelope there on `node`, as the store API writes one.
+async fn write_signed(
+ node: &AppState,
+ user: &str,
+ key: &ed25519_dalek::SigningKey,
+ at: &str,
+ value: serde_json::Value,
+) {
+ use crate::access::{envelope, sshsig};
+ use crate::document::ScalarValue;
+ use crate::document::typed::{TimestampFormat, Val};
+ let at = path(at);
+ let previous = match store::read_path(&node.doc.read().await.doc, &at) {
+ Ok(Some(serde_json::Value::String(text))) if text.starts_with("cv1.") => Some(text),
+ _ => None,
+ };
+ let payload = envelope::Payload {
+ path: at.clone(),
+ value,
+ user: user.to_owned(),
+ at: 1,
+ prev: envelope::digest(previous.as_deref()),
+ };
+ let bytes = serde_json::to_vec(&payload).unwrap();
+ let sealed = envelope::seal(&bytes, &sshsig::sign(key, envelope::NAMESPACE, &bytes)).unwrap();
+ let sign = Sign::Write {
+ path: at.clone(),
+ asked: false,
+ };
+ node.mutate_doc_for(Some(user.to_owned()), sign, move |doc| {
+ let writing = store::Writing::new(store::Sticky::No, TimestampFormat::default(), true);
+ store::write_val_path(
+ doc,
+ &at,
+ &Val::Scalar(ScalarValue::Str(sealed.into())),
+ writing,
+ )
+ .map(|_| ())
+ })
+ .await
+ .expect("signed write");
+}
+
+/// What `node` reads at `at`, envelopes opened (`store::read_plain`).
+async fn plain(node: &AppState, at: &str) -> Option<serde_json::Value> {
+ store::read_plain(&node.doc.read().await.doc, &path(at))
+ .ok()
+ .flatten()
+}
+
+/// A strict swarm (`"values": "signed"`), its first user's key given at genesis: values
+/// signed by their user go everywhere, before and after a remake, which carries them
+/// over; a node writing the registry plain, for a user that may, gets nothing past the
+/// others.
+#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
+async fn a_strict_swarm() {
+ let key = ed25519_dalek::SigningKey::from_bytes(&[11; 32]);
+ let line = crate::access::sshsig::PublicKey(key.verifying_key().to_bytes()).line();
+ let sim = Sim::with_identity(73);
+ let a = sim.start_with("a", &[], Start::Genesis, move |config| {
+ config.first_users = [(
+ "alice".to_owned(),
+ [("laptop".to_owned(), line.clone())].into(),
+ )]
+ .into();
+ });
+ let b = sim.start("b", &["a"], Start::Join);
+ let c = sim.start("c", &["a"], Start::Join);
+ sim.wait_ready(&[&a, &b, &c], SOON).await;
+ let nodes = [a.clone(), b.clone(), c.clone()];
+ sim.wait_until("every node is registered", SOON, || {
+ let nodes = nodes.clone();
+ async move {
+ for node in &nodes {
+ if crate::swarm::members::members(&node.doc.read().await.doc).len() < 3 {
+ return false;
+ }
+ }
+ true
+ }
+ })
+ .await;
+ write_as(
+ &a,
+ "alice",
+ "_continuum/access/userSigned",
+ json!([["config"]]),
+ )
+ .await;
+ write_as(
+ &a,
+ "alice",
+ "_continuum/access/policy",
+ json!({"version": 1, "values": "signed"}),
+ )
+ .await;
+ sim.wait_converged(&[&a, &b, &c], SOON).await;
+
+ // Signed, on any node: everywhere.
+ write_signed(&b, "alice", &key, "config", json!({"mode": "fast"})).await;
+ for node in [&a, &b, &c] {
+ sim.wait_until("the signed value is everywhere", SOON, move || async move {
+ plain(node, "config").await == Some(json!({"mode": "fast"}))
+ })
+ .await;
+ }
+
+ // A remake carries it over; replacing it afterwards names it.
+ let (status, report) =
+ crate::remake::testing::start(a.clone(), json!({ "kind": "compact" }), None).await;
+ assert!(status.is_success(), "proposal refused: {status} {report}");
+ sim.wait_until(
+ "every node is on generation 1",
+ Duration::from_secs(60),
+ || {
+ let nodes = nodes.clone();
+ async move {
+ for node in &nodes {
+ if node.local_epoch().generation != 1 || !node.is_ready().await {
+ return false;
+ }
+ }
+ true
+ }
+ },
+ )
+ .await;
+ write_signed(&c, "alice", &key, "config", json!({"mode": "slow"})).await;
+ for node in [&a, &b, &c] {
+ sim.wait_until("the replacement is everywhere", SOON, move || async move {
+ plain(node, "config").await == Some(json!({"mode": "slow"}))
+ })
+ .await;
+ assert_eq!(refused(node).await, 0, "an honest change was refused");
+ }
+
+ // c writes the registry plain, as a user who may manage users: refused by the
+ // others, and so is a plain write over the signed value.
+ let mallory = crate::access::registry::entry("alice", 1, &crate::access::registry::all_caps());
+ write_as(&c, "alice", "_continuum/users/mallory", mallory).await;
+ write_as(&c, "alice", "config", json!({"mode": "forged"})).await;
+ for node in [&a, &b] {
+ sim.wait_until("the plain writes are refused", SOON, move || async move {
+ refused(node).await > 0
+ })
+ .await;
+ assert!(plain(node, "_continuum/users/mallory").await.is_none());
+ assert_eq!(plain(node, "config").await, Some(json!({"mode": "slow"})));
+ }
+}