aboutsummaryrefslogtreecommitdiffstats
path: root/src/simtest
diff options
context:
space:
mode:
Diffstat (limited to 'src/simtest')
-rw-r--r--src/simtest/mod.rs2
-rw-r--r--src/simtest/policy.rs113
2 files changed, 115 insertions, 0 deletions
diff --git a/src/simtest/mod.rs b/src/simtest/mod.rs
index 0ebb0ae..67ddaff 100644
--- a/src/simtest/mod.rs
+++ b/src/simtest/mod.rs
@@ -222,6 +222,8 @@ impl Sim {
session_lifetime: Duration::from_secs(86400),
challenge_lifetime: Duration::from_secs(300),
key_logins_per_user: 100,
+ strict_keys: Vec::new(),
+ follow_strict_remakes: false,
cors_origins: Vec::new(),
files: Arc::new(Files::default()),
state_file: None,
diff --git a/src/simtest/policy.rs b/src/simtest/policy.rs
index 7f32695..0d590d7 100644
--- a/src/simtest/policy.rs
+++ b/src/simtest/policy.rs
@@ -899,3 +899,116 @@ async fn a_write_every_node_would_refuse() {
assert!(!holds(&a, "secrets/x").await);
assert_eq!(refused(&a).await, 0);
}
+
+/// A swarm turning strict through a remake: what a member planted in the registry before
+/// (an entry written plain, as a user who may) is gone unless the operator's manifest
+/// keeps it; the policy is strict after. A node without the operator's key doesn't vote
+/// for it, and follows once the others commit it.
+#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
+async fn a_swarm_turning_strict_through_a_remake() {
+ use crate::access::{sshsig, strict};
+ let key = ed25519_dalek::SigningKey::from_bytes(&[12; 32]);
+ let public = sshsig::PublicKey(key.verifying_key().to_bytes());
+ let line = public.line();
+ let sim = Sim::with_identity(92);
+ let with_key = move |config: &mut crate::node::NodeConfig| config.strict_keys = vec![public];
+ let a = sim.start_with("a", &[], Start::Genesis, {
+ move |config| {
+ config.first_users = [(
+ "alice".to_owned(),
+ [("laptop".to_owned(), line.clone())].into(),
+ )]
+ .into();
+ with_key(config);
+ }
+ });
+ let b = sim.start_with("b", &["a"], Start::Join, with_key);
+ 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/policy",
+ json!({"version": 1, "signatures": "required"}),
+ )
+ .await;
+ // c plants mallory, as alice, who may: taken, the policy not being strict.
+ let mallory = crate::access::registry::entry("alice", 1, &crate::access::registry::all_caps());
+ write_as(&c, "alice", "_continuum/users/mallory", mallory).await;
+ sim.wait_converged(&[&a, &b, &c], SOON).await;
+ assert!(holds(&a, "_continuum/users/mallory").await);
+
+ // The operator's manifest, made on a, keeping everything but mallory.
+ let (text, heads) = {
+ let replica = a.doc.read().await;
+ let policy = json!({"version": 1, "signatures": "required", "values": "signed"});
+ let swarm = store::read_path(&replica.doc, &path("_continuum/swarm"))
+ .unwrap()
+ .unwrap();
+ let made = strict::manifest_of(&replica.doc, swarm.as_str().unwrap(), policy, "alice", 1);
+ let mut manifest: strict::Manifest = serde_json::from_str(&made).unwrap();
+ manifest
+ .keep
+ .retain(|kept| kept.path != path("_continuum/users/mallory"));
+ let line = serde_json::to_string(&manifest).unwrap();
+ let signed = sshsig::sign(&key, strict::NAMESPACE, line.as_bytes());
+ let heads: Vec<String> = replica
+ .doc
+ .get_heads()
+ .iter()
+ .map(ToString::to_string)
+ .collect();
+ (format!("{line}\n{signed}"), heads)
+ };
+ let (status, report) = crate::remake::testing::start(
+ a.clone(),
+ json!({"kind": "strict", "spec": {"manifest": text}, "baseHeads": heads}),
+ 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;
+ for node in [&a, &b, &c] {
+ let replica = node.doc.read().await;
+ assert!(
+ store::read_path(&replica.doc, &path("_continuum/users/mallory"))
+ .unwrap()
+ .is_none(),
+ "mallory planted, kept"
+ );
+ assert!(
+ store::read_path(&replica.doc, &path("_continuum/users/alice"))
+ .unwrap()
+ .is_some()
+ );
+ assert!(crate::access::policy::of(&replica.doc).strict());
+ }
+}