diff options
Diffstat (limited to 'src/access/rules.rs')
| -rw-r--r-- | src/access/rules.rs | 103 |
1 files changed, 79 insertions, 24 deletions
diff --git a/src/access/rules.rs b/src/access/rules.rs index 450addb..4e4e6e9 100644 --- a/src/access/rules.rs +++ b/src/access/rules.rs @@ -193,12 +193,13 @@ pub(crate) fn judge(doc: &Document, epoch: Epoch, change: &Change<'_>) -> Result || user_may(path) || user_may(&meta::path(&[meta::USERS])) } - // A user's own keys (`/auth/keys`), whatever else it may do. + // A user's own keys (`/auth/keys`), whatever else it may do, one left at + // least. (Some(meta::USERS), Some(owner)) if path.get(3).map(String::as_str) == Some("keys") && change.user == Some(owner) => { - true + keys_left(&at, owner, change.ops) } // A node forgotten while alive cancels its own record (`swarm::peer::forget`). (Some(meta::FORGOTTEN), Some(owner)) => owner == node || user_may(path), @@ -479,6 +480,30 @@ fn aborts_expired(at: &At<'_>, id: &str, change: &Change<'_>) -> bool { change.time >= settle_until.saturating_add(grace.saturating_mul(2)) } +/// Whether `owner` has a key left after `ops`, its keys at `at` as they change them. +fn keys_left(at: &At<'_>, owner: &str, ops: &[Op]) -> bool { + let keys = meta::path(&[meta::USERS, owner, "keys"]); + let mut left: BTreeSet<String> = store::keys_at(at, &keys) + .ok() + .flatten() + .unwrap_or_default() + .into_iter() + .collect(); + for op in ops.iter().filter(|op| op.path.starts_with(&keys)) { + match (op.path.get(keys.len()), &op.action) { + (None, _) => left.clear(), + (Some(id), Action::Delete) => { + left.remove(id); + } + (Some(id), _) if op.path.len() == keys.len() + 1 => { + left.insert(id.clone()); + } + _ => {} + } + } + !left.is_empty() +} + /// The coordinator remake `id`'s proposal names, at `at`. fn coordinator(at: &At<'_>, id: &str) -> Option<String> { let proposal = store::read_path(at, &meta::path(&[meta::REMAKES, id, "proposal"])) @@ -795,38 +820,68 @@ mod tests { assert!(decide(Some("carol"), "committed", 0).is_ok()); } - /// A user's own keys: its own to change, whatever else it may do; another's, or the - /// rest of its own entry, not. + /// A user's own keys: its own to change, whatever else it may do, one left at least; + /// another's, or the rest of its own entry, not. #[test] fn users_change_their_own_keys() { - let doc = swarm(true); - assert!( - judged( - &doc, - "a", - Some("bob"), - &["_continuum/users/bob/keys/laptop"] + let mut doc = swarm(true); + let judged_ops = |doc: &Document, user: Option<&str>, ops: Vec<Op>| { + let deps = doc.get_heads(); + let touched: BTreeSet<Path> = ops.iter().map(|op| op.path.clone()).collect(); + judge( + doc, + Epoch::GENESIS, + &Change { + hash: ChangeHash([9; 32]), + deps: &deps, + seen: &deps, + committer: Some("a"), + user, + paths: &touched, + ops: &ops, + time: 0, + }, ) - .is_ok() - ); - assert!(judged(&doc, "a", Some("bob"), &["_continuum/users/bob/keys"]).is_ok()); - assert!(refused(judged( + }; + let op = |at: &str, action: Action| Op { + path: path(at), + action, + }; + let put = |at: &str| op(at, Action::PutString("ssh-ed25519 AAAA".to_owned())); + let bobs = "_continuum/users/bob/keys/laptop"; + assert!(judged_ops(&doc, Some("bob"), vec![put(bobs)]).is_ok()); + assert!(refused(judged_ops( &doc, - "a", Some("bob"), - &["_continuum/users/alice/keys/laptop"] + vec![put("_continuum/users/alice/keys/laptop")] ))); - assert!(refused(judged( + assert!(refused(judged_ops( &doc, - "a", Some("bob"), - &["_continuum/users/bob/caps"] + vec![op("_continuum/users/bob/caps", Action::MakeMap)] ))); - assert!(refused(judged( + assert!(refused(judged_ops(&doc, None, vec![put(bobs)]))); + // With one key: another added, one of two removed; not the last. + write(&mut doc, bobs, json!("ssh-ed25519 AAAA")); + let phone = "_continuum/users/bob/keys/phone"; + assert!(judged_ops(&doc, Some("bob"), vec![put(phone)]).is_ok()); + assert!(refused(judged_ops( &doc, - "a", - None, - &["_continuum/users/bob/keys/laptop"] + Some("bob"), + vec![op(bobs, Action::Delete)] + ))); + assert!( + judged_ops( + &doc, + Some("bob"), + vec![put(phone), op(bobs, Action::Delete)] + ) + .is_ok() + ); + assert!(refused(judged_ops( + &doc, + Some("bob"), + vec![op("_continuum/users/bob/keys", Action::Delete)] ))); } |
