aboutsummaryrefslogtreecommitdiffstats
path: root/src/swarm
diff options
context:
space:
mode:
authortv <tv@krebsco.de>2026-11-08 09:44:28 +0000
committertv <tv@krebsco.de>2026-11-08 14:29:01 +0100
commite63984e99bef66cb05d7bcf781812ea5e6276fa8 (patch)
treeaad499e46a8b63ebc2c329e5984ef48e45dc6a0a /src/swarm
parent4ecb4ee5c0a49fc6d78ae06e21f6bb1571e290cb (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/swarm')
-rw-r--r--src/swarm/refusals.rs53
1 files changed, 1 insertions, 52 deletions
diff --git a/src/swarm/refusals.rs b/src/swarm/refusals.rs
index fd857fa..30facb4 100644
--- a/src/swarm/refusals.rs
+++ b/src/swarm/refusals.rs
@@ -66,7 +66,7 @@ pub(crate) async fn received(state: &AppState, peer: &str, message: RefusalsMess
let mut refused = None;
let mut taken = Vec::new();
for hash in changes {
- match judge_held(&replica.doc, replica.epoch, &hash) {
+ match crate::access::rules::judge_held(&replica.doc, replica.epoch, &hash) {
Some(Err(why)) => {
refused = Some((hash, why));
break;
@@ -90,54 +90,3 @@ pub(crate) async fn received(state: &AppState, peer: &str, message: RefusalsMess
warn!(peer = %peer, changes = ?taken, "A peer refuses changes our rules take: its policy or ours is off");
}
}
-
-/// `hash`, which `doc` (of `epoch`) holds, judged by its rules now: why they refuse it,
-/// if they do; None if `doc` doesn't hold it.
-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 = crate::access::rules::judge(
- doc,
- epoch,
- &crate::access::rules::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(crate::access::rules::Refusal::Rule(why)) => Err(why),
- // Its dependencies are in the document: it is.
- Err(crate::access::rules::Refusal::Unready) => Ok(()),
- })
-}