diff options
| author | tv <tv@krebsco.de> | 2026-11-08 13:52:35 +0000 |
|---|---|---|
| committer | tv <tv@krebsco.de> | 2026-11-08 15:04:01 +0100 |
| commit | 1769f30038c62c999cfb91be0868c740fb1c0eba (patch) | |
| tree | 50a6e4aa4d660b4f8cdc8821ad6b27caceece2bb | |
| parent | b3526df29412400cfaa8c3a8d8692e37fc44d19b (diff) | |
rules: protocol maps judged by what is done to them
Review finding 7, first part: the protocol-map rules looked at paths,
not actions. Any member could delete _continuum/members, /votes or
/remakes whole, or rewrite another remake's proposal (its coordinator)
and then write its decision.
What holds the members' entries, votes and remake records, and a
remake's record itself, are now made by a member only where there is
none; a proposal is written only where there is none. Replacing or
removing any of them is left to a user who may write there (the
protocol writers). An abort asked for stays any member's.
| -rw-r--r-- | docs/access.md | 9 | ||||
| -rw-r--r-- | src/access/rules.rs | 48 |
2 files changed, 53 insertions, 4 deletions
diff --git a/docs/access.md b/docs/access.md index f7546aa..eb9efc9 100644 --- a/docs/access.md +++ b/docs/access.md @@ -224,8 +224,13 @@ stays as it is, wherever it comes from later (a joining node catching up, say). member in the same change (`DELETE /peers/{pk}`, with `_continuum/forgotten`). * Epochs and the swarm ID (`_continuum/epochs`, `_continuum/swarm`) by an epoch's first change only (the genesis change, a remake's snapshot). -* A remake's decision by the coordinator its proposal names; the rest of a record by - any member. +* A remake's record and its proposal by whichever member proposes, where there are + none; its decision by the coordinator its proposal names; an abort asked for by any + member. Replacing or removing a record or its proposal, by a user who may write + there. +* What holds the members' entries, votes and remake records (`_continuum/members`, + `_continuum/votes`, `_continuum/remakes`) made by whichever member first writes in + it, where there is none; replaced or removed by a user who may write there only. * A user's tokens revoked by that user too (logging out), and by one who manages users. * A member's forget record cancelled by that member too (forgotten while alive). * Everything else, protocol data or not, by a user the registry lets write there. diff --git a/src/access/rules.rs b/src/access/rules.rs index 320ccd8..2746cb3 100644 --- a/src/access/rules.rs +++ b/src/access/rules.rs @@ -141,6 +141,15 @@ pub(crate) fn judge(doc: &Document, epoch: Epoch, change: &Change<'_>) -> Result && store::read_path(&at, path).ok().flatten().is_none() }) .collect(); + // Whether the change only makes a map at `path`, where there is none. + let made = |path: &Path| { + change + .ops + .iter() + .filter(|op| op.path == *path) + .all(|op| op.action == Action::MakeMap) + && store::read_path(&at, path).ok().flatten().is_none() + }; for path in change.paths { if on_the_way.contains(path) { continue; @@ -168,8 +177,11 @@ pub(crate) fn judge(doc: &Document, epoch: Epoch, change: &Change<'_>) -> Result && user_may(&forgetting)) } (Some(meta::VOTES), Some(owner)) => owner == node, - // What holds them, made by whichever member first writes in it. - (Some(meta::MEMBERS | meta::VOTES | meta::REMAKES), None) => true, + // What holds them: made by whichever member first writes in it, where there + // is none; replaced or removed by one who may write it. + (Some(meta::MEMBERS | meta::VOTES | meta::REMAKES), None) => { + made(path) || user_may(path) + } // Written by a root only. (Some(meta::EPOCHS | meta::SWARM), _) => false, // Logging out: a user revokes its own tokens (`POST /auth/logout`); one who @@ -197,6 +209,18 @@ pub(crate) fn judge(doc: &Document, epoch: Epoch, change: &Change<'_>) -> Result .any(|other| other == &meta::path(&[meta::REMAKES, id])) } }, + // The record, made where there is none, and its proposal, written where + // there is none: by whichever member proposes. Replaced or removed by + // one who may write them. + None => made(path) || user_may(path), + Some("proposal") => { + store::read_path(&at, &meta::path(&[meta::REMAKES, id, "proposal"])) + .ok() + .flatten() + .is_none() + || user_may(path) + } + // The rest (an abort asked for), any member's. _ => true, }, _ => user_may(path), @@ -500,6 +524,26 @@ mod tests { &["_continuum/remakes/r1/decision"] ))); assert!(judged(&doc, "b", None, &["_continuum/remakes/r1/abortRequested"]).is_ok()); + // Its proposal: written where there is none; another's replaced, by one who may. + assert!(judged(&doc, "b", None, &["_continuum/remakes/r3/proposal"]).is_ok()); + assert!(refused(judged( + &doc, + "b", + None, + &["_continuum/remakes/r1/proposal"] + ))); + assert!(refused(judged( + &doc, + "b", + None, + &["_continuum/remakes/r1/proposal/coordinator"] + ))); + // What holds the records, a member's entries, votes: made where there are none, + // never replaced or removed by a member. + assert!(refused(judged(&doc, "b", None, &["_continuum/remakes"]))); + assert!(refused(judged(&doc, "b", None, &["_continuum/members"]))); + assert!(refused(judged(&doc, "b", None, &["_continuum/remakes/r1"]))); + assert!(judged(&doc, "b", None, &["_continuum/votes"]).is_ok()); // A record carried into a new epoch whole: its proposal with its decision. assert!( judged( |
