diff options
Diffstat (limited to 'src/access/rules.rs')
| -rw-r--r-- | src/access/rules.rs | 93 |
1 files changed, 93 insertions, 0 deletions
diff --git a/src/access/rules.rs b/src/access/rules.rs index 1ce22ee..f285328 100644 --- a/src/access/rules.rs +++ b/src/access/rules.rs @@ -19,6 +19,8 @@ //! * User data: by a user who may write there. //! * Under a strict policy (`access::policy`), the values their users sign //! (`access::envelope`) only as they signed them (`signed_values`). +//! * Where the swarm declares values sealed (`access::sealed`), only those sealed as +//! declared (`sealed_values`). //! //! In a swarm without a user registry (`access::registry`), whatever a user would need, //! any member may. @@ -157,6 +159,40 @@ pub(crate) fn judge(doc: &Document, epoch: Epoch, change: &Change<'_>) -> Result if super::policy::of(&at).strict() { signed_values(&at, change.ops).or_else(refuse)?; } + let declared = super::sealed::declared(&at); + if !declared.is_empty() { + sealed_values(&at, &declared, change.ops).or_else(refuse)?; + } + Ok(()) +} + +/// Where values are declared sealed (`declared`), `ops` write only sealed values as +/// declared, judged at `at`, as their writer saw it (`sealed::check`): at such a place, a +/// string put there that is one (or deleting what is there); nothing within one; nothing +/// in an object nowhere to be found, which might be one. +fn sealed_values( + at: &At<'_>, + declared: &[super::sealed::Declared], + ops: &[Op], +) -> Result<(), String> { + for op in ops { + let path = &op.path; + if op.action == Action::Nowhere { + return Err("it writes in an object nowhere to be found".to_owned()); + } + if let Some(place) = super::sealed::place(declared, path) { + match &op.action { + Action::PutString(text) => { + super::sealed::check(at, place, &serde_json::Value::String(text.clone())) + .map_err(|why| format!("{}: {why}", shown(path)))?; + } + Action::Delete => {} + _ => return Err(format!("{} holds a sealed value", shown(path))), + } + } else if super::sealed::within(declared, path) { + return Err(format!("{} is within a sealed value", shown(path))); + } + } Ok(()) } @@ -606,6 +642,63 @@ mod tests { assert!(refused(judged(&doc, vec![op("", Action::Nowhere)]))); } + /// Where values are declared sealed: only sealed ones, as declared, with or without + /// a strict policy. + #[test] + fn sealed_values_as_declared() { + use crate::access::sealed::testing::sealed_for; + use crate::access::sshsig::PublicKey; + let key = PublicKey([8; 32]); + let mut doc = swarm(true); + let mut alice = registry::entry("a", 2, &[(Vec::new(), &[Right::Write][..])]); + alice["keys"] = json!({ "k": key.line() }); + write(&mut doc, "_continuum/users/alice", alice); + write( + &mut doc, + "_continuum/access/sealed", + json!([{"path": ["secrets", "*"], "readers": ["alice"]}]), + ); + let judged = |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: Some("alice"), + paths: &touched, + ops: &ops, + }, + ) + }; + let op = |at: &str, action: Action| Op { + path: path(at), + action, + }; + let sealed = sealed_for(&[key]); + assert!(judged(vec![op("secrets/db", Action::PutString(sealed))]).is_ok()); + assert!(judged(vec![op("secrets/db", Action::Delete)]).is_ok()); + assert!(refused(judged(vec![op( + "secrets/db", + Action::PutString("hunter2".to_owned()) + )]))); + let stray = sealed_for(&[key, PublicKey([9; 32])]); + assert!(refused(judged(vec![op( + "secrets/db", + Action::PutString(stray) + )]))); + assert!(refused(judged(vec![op("secrets/db", Action::MakeMap)]))); + assert!(refused(judged(vec![op( + "secrets/db/password", + Action::Other + )]))); + assert!(judged(vec![op("notes/x", Action::PutString("plain".to_owned()))]).is_ok()); + } + /// Roots: the first change of a swarm's first epoch, a later epoch's snapshot. #[test] fn roots() { |
