aboutsummaryrefslogtreecommitdiffstats
path: root/src/access/envelope.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/access/envelope.rs')
-rw-r--r--src/access/envelope.rs52
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: &registry::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, &registry::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();