diff options
Diffstat (limited to 'src/access/envelope.rs')
| -rw-r--r-- | src/access/envelope.rs | 52 |
1 files changed, 51 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(); |
