diff options
Diffstat (limited to 'src/access')
| -rw-r--r-- | src/access/envelope.rs | 52 | ||||
| -rw-r--r-- | src/access/rules.rs | 42 |
2 files changed, 93 insertions, 1 deletions
diff --git a/src/access/envelope.rs b/src/access/envelope.rs index e0c8909..7cd0083 100644 --- a/src/access/envelope.rs +++ b/src/access/envelope.rs @@ -207,7 +207,7 @@ pub(crate) fn check( if !user.keys.values().any(|known| *known == key) { return Err(format!("{} has no key {}", payload.user, key.fingerprint())); } - if !user.caps().allows(Right::Write, path) { + if !user.caps().allows(Right::Write, path) && !own_keys_only(doc, &payload, &user, path) { return Err(format!( "{} may not write /{}", payload.user, @@ -217,6 +217,33 @@ pub(crate) fn check( Ok(Signed { payload, key }) } +/// Whether `payload`, signed by its user (`user`, enabled), is that user's own entry +/// with only its keys changed from the one at `path` in `doc`: a user manages its own +/// keys, whatever else it may do. +fn own_keys_only( + doc: &impl ReadDoc, + payload: &Payload, + user: ®istry::User, + path: &[String], +) -> bool { + if user.disabled || path != registry::user_path(&payload.user) { + return false; + } + let Ok(Some(mut before)) = store::read_plain(doc, path) else { + return false; + }; + let mut after = payload.value.clone(); + for entry in [&mut before, &mut after] { + match entry.as_object_mut() { + Some(entry) => { + entry.remove("keys"); + } + None => return false, + } + } + before == after +} + #[cfg(test)] mod tests { use super::*; @@ -307,6 +334,29 @@ mod tests { assert!(check(&doc, &elsewhere, &path("config"), None).is_err()); } + /// A user signs its own entry with its keys changed, though it may not write the + /// registry; not with anything else changed, nor another's. + #[test] + fn users_sign_their_own_keys() { + let key = SigningKey::from_bytes(&[5; 32]); + let doc = registry_with(&key); + let entry = store::read_plain(&doc, ®istry::user_path("alice")) + .unwrap() + .unwrap(); + let line = + sshsig::PublicKey(SigningKey::from_bytes(&[8; 32]).verifying_key().to_bytes()).line(); + let mut rekeyed = entry.clone(); + rekeyed["keys"]["phone"] = json!(line); + let at = "_continuum/users/alice"; + let signed_as = |user: &str, value: JsonValue| signed(&key, &payload(user, at, value, "")); + assert!(check(&doc, &signed_as("alice", rekeyed.clone()), &path(at), None).is_ok()); + let mut more = rekeyed.clone(); + more["caps"] = json!({"x": {"path": [], "rights": ["write"]}}); + assert!(check(&doc, &signed_as("alice", more), &path(at), None).is_err()); + // bob, holding the same key, for alice: not his entry. + assert!(check(&doc, &signed_as("bob", rekeyed), &path(at), None).is_err()); + } + #[test] fn listed_paths() { let mut doc = Document::new(); diff --git a/src/access/rules.rs b/src/access/rules.rs index 3eca1c9..450addb 100644 --- a/src/access/rules.rs +++ b/src/access/rules.rs @@ -193,6 +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. + (Some(meta::USERS), Some(owner)) + if path.get(3).map(String::as_str) == Some("keys") + && change.user == Some(owner) => + { + true + } // A node forgotten while alive cancels its own record (`swarm::peer::forget`). (Some(meta::FORGOTTEN), Some(owner)) => owner == node || user_may(path), (Some(meta::REMAKES), Some(id)) => match path.get(3).map(String::as_str) { @@ -788,6 +795,41 @@ 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. + #[test] + fn users_change_their_own_keys() { + let doc = swarm(true); + assert!( + judged( + &doc, + "a", + Some("bob"), + &["_continuum/users/bob/keys/laptop"] + ) + .is_ok() + ); + assert!(judged(&doc, "a", Some("bob"), &["_continuum/users/bob/keys"]).is_ok()); + assert!(refused(judged( + &doc, + "a", + Some("bob"), + &["_continuum/users/alice/keys/laptop"] + ))); + assert!(refused(judged( + &doc, + "a", + Some("bob"), + &["_continuum/users/bob/caps"] + ))); + assert!(refused(judged( + &doc, + "a", + None, + &["_continuum/users/bob/keys/laptop"] + ))); + } + /// A node that isn't a member may only register itself. #[test] fn strangers_register_themselves_only() { |
