From 41657a4d749b1f1d44663ff6504f2e784f091537 Mon Sep 17 00:00:00 2001 From: tv Date: Sat, 7 Nov 2026 22:35:21 +0100 Subject: access: strict policy, signed paths "values": "signed" (strict, requiring signatures too): the registry, _continuum/access, bans and the paths in _continuum/access/userSigned (* for any key) hold values their users signed. Envelopes read as the values they hold where the node reads protocol data. Auth plan M7. --- src/document/store.rs | 53 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) (limited to 'src/document/store.rs') diff --git a/src/document/store.rs b/src/document/store.rs index 778307f..c946fef 100644 --- a/src/document/store.rs +++ b/src/document/store.rs @@ -454,6 +454,30 @@ fn read_path_inner( Ok(Some(resolved_to_json(doc, ¤t, form, log)?)) } +/// [`read_path`] of a value its user may have signed (`access::envelope`): an envelope +/// read as the value it holds, unchecked (the policy checked it before it went in), a +/// signed `null` as nothing. +pub(crate) fn read_plain( + doc: &impl ReadDoc, + path: &[String], +) -> Result, StoreError> { + Ok(match read_path(doc, path)? { + Some(JsonValue::String(text)) if text.starts_with("cv1.") => { + enveloped(&text).filter(|value| !value.is_null()) + } + other => other, + }) +} + +/// The value an envelope (`cv1..`) holds; None if it isn't one. +fn enveloped(text: &str) -> Option { + use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD}; + let payload = text.split('.').nth(1)?; + let bytes = URL_SAFE_NO_PAD.decode(payload).ok()?; + let mut payload: JsonValue = serde_json::from_slice(&bytes).ok()?; + Some(payload.get_mut("value")?.take()) +} + /// Reads a path; the empty path is the whole store. pub(crate) fn read_path( doc: &impl ReadDoc, @@ -1673,6 +1697,35 @@ mod tests { at.split('/').map(str::to_owned).collect() } + /// An envelope reads as the value it holds, a signed `null` as nothing, anything + /// else as it is. + #[test] + fn plain_reads_open_envelopes() { + use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD}; + let sealed = |value: JsonValue| { + let payload = + json!({"path": ["a"], "value": value, "user": "alice", "at": 1, "prev": ""}); + format!("cv1.{}.c2ln", URL_SAFE_NO_PAD.encode(payload.to_string())) + }; + let mut doc = crate::document::Document::new(); + try_change(&mut doc, |doc| { + write_path(doc, &path("a"), &json!(sealed(json!({"mode": "fast"}))))?; + write_path(doc, &path("gone"), &json!(sealed(JsonValue::Null)))?; + write_path(doc, &path("plain"), &json!("cv1-ish")) + }) + .unwrap(); + assert_eq!( + read_plain(&doc, &path("a")).unwrap(), + Some(json!({"mode": "fast"})) + ); + assert_eq!(read_plain(&doc, &path("gone")).unwrap(), None); + assert_eq!( + read_plain(&doc, &path("plain")).unwrap(), + Some(json!("cv1-ish")) + ); + assert_eq!(read_plain(&doc, &path("none")).unwrap(), None); + } + #[test] fn changes_name_the_paths_their_operations_touch() { fn touched( -- cgit v1.3.1