diff options
| author | tv <tv@krebsco.de> | 2026-11-08 09:44:28 +0000 |
|---|---|---|
| committer | tv <tv@krebsco.de> | 2026-11-08 14:29:01 +0100 |
| commit | e63984e99bef66cb05d7bcf781812ea5e6276fa8 (patch) | |
| tree | aad499e46a8b63ebc2c329e5984ef48e45dc6a0a /src/access/rules.rs | |
| parent | 4ecb4ee5c0a49fc6d78ae06e21f6bb1571e290cb (diff) | |
access: a node judges its own changes before they leave
The API checks the path a request asks for; the rules judge everything
the change does. Where they disagreed, the honest node's change was
refused by every other node, and with signatures required the node was
refused as a whole. The plan had both gates run the same judgement.
Under a policy, each change a node makes (its own writes and those
through the API) is now judged by rules::judge as receivers will judge
it, before it is signed or sent; one every node would refuse is rolled
back (the document forked at the heads before it, same actor: the
change never left) and the request refused with 403 (StoreError::
Refused). rules::judge_held, judging a change the document holds,
moves from swarm::refusals to access::rules for both.
The simulations that play a member past its own checks write with a
helper that bypasses them (forge).
Review finding 3 (the same judgement at both gates).
Diffstat (limited to 'src/access/rules.rs')
| -rw-r--r-- | src/access/rules.rs | 53 |
1 files changed, 53 insertions, 0 deletions
diff --git a/src/access/rules.rs b/src/access/rules.rs index 0de901a..320ccd8 100644 --- a/src/access/rules.rs +++ b/src/access/rules.rs @@ -215,6 +215,59 @@ pub(crate) fn judge(doc: &Document, epoch: Epoch, change: &Change<'_>) -> Result Ok(()) } +/// `hash`, which `doc` (of `epoch`) holds, judged by its rules now, as received: why +/// they refuse it, if they do; None if `doc` doesn't hold it. A change this node just +/// made, before it leaves (`AppState::mutate_doc_for`), or one a peer says it refused +/// (`swarm::refusals`). +pub(crate) fn judge_held( + doc: &crate::document::Document, + epoch: Epoch, + hash: &ChangeHash, +) -> Option<Result<(), String>> { + let change = doc.get_change_by_hash(hash)?; + // An epoch's root is what the document is: never refused, whatever a peer says. + if change.deps().is_empty() { + return Some(Ok(())); + } + let decoded = change.decode(); + let ops = crate::document::inspect::Inspector::new(doc) + .ops(std::slice::from_ref(&decoded)) + .pop() + .unwrap_or_default(); + let paths = ops.iter().map(|op| op.path.clone()).collect(); + let committer = doc + .get_author_for_actor(change.actor_id()) + .and_then(|author| { + std::str::from_utf8(author.as_bytes()) + .ok() + .map(str::to_owned) + }); + let user = change + .message() + .and_then(crate::document::replica::user_of) + .map(str::to_owned); + let deps = change.deps().to_vec(); + let judged = judge( + doc, + epoch, + &Change { + hash: *hash, + deps: &deps, + seen: &deps, + committer: committer.as_deref(), + user: user.as_deref(), + paths: &paths, + ops: &ops, + }, + ); + Some(match judged { + Ok(()) => Ok(()), + Err(Refusal::Rule(why)) => Err(why), + // Its dependencies are in the document: it is. + Err(Refusal::Unready) => 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 |
