aboutsummaryrefslogtreecommitdiffstats
path: root/src/state
diff options
context:
space:
mode:
authortv <tv@krebsco.de>2026-11-07 22:35:21 +0100
committertv <tv@krebsco.de>2026-11-07 22:35:21 +0100
commitf44fbf5b93269bb3573e8a2701e70ab0d7613153 (patch)
tree32915998f6e797afcc13f485ccde66bac0e94150 /src/state
parent389b11b597b6bf60f120b190002bbb76fff28dbf (diff)
access: the policy enforced
_continuum/access/policy ({"version": 1}) has every node screen every change it receives, on every path changes come in. A policy a node doesn't know stops its writes through the API. GET /admin/refused. Auth plan M6.
Diffstat (limited to 'src/state')
-rw-r--r--src/state/host.rs21
-rw-r--r--src/state/mod.rs11
2 files changed, 29 insertions, 3 deletions
diff --git a/src/state/host.rs b/src/state/host.rs
index a48a6fb..e401509 100644
--- a/src/state/host.rs
+++ b/src/state/host.rs
@@ -403,9 +403,24 @@ impl Host for AppState {
tokio::task::spawn_blocking(move || {
let result = {
let mut replica = state.doc.blocking_write();
- let result = replica
- .apply_changes([change])
- .map_err(|error| error.to_string());
+ let result = if crate::access::policy::of(&replica.doc).enforced() {
+ // Judged first (`access::screen`).
+ let (epoch, epoch_seq) = (replica.epoch, replica.epoch_seq);
+ crate::access::screen::apply_bytes(
+ &mut replica.doc,
+ epoch,
+ epoch_seq,
+ change.raw_bytes(),
+ &state.screening,
+ &mut crate::document::PatchLog::inactive(),
+ )
+ .map(|_| ())
+ .map_err(|error| format!("{error:#}"))
+ } else {
+ replica
+ .apply_changes([change])
+ .map_err(|error| error.to_string())
+ };
replica.refresh_heads();
result
}; // lock released before notifying
diff --git a/src/state/mod.rs b/src/state/mod.rs
index ff19124..049c9e6 100644
--- a/src/state/mod.rs
+++ b/src/state/mod.rs
@@ -150,6 +150,9 @@ pub(crate) struct AppState {
/// Which of its changes this node signs besides those a write's `Sign` says
/// (`--sign-every`).
pub(crate) sign_every: SignEvery,
+ /// Received changes waiting for their dependencies, and those refused, while the
+ /// policy has them judged (`access::screen`). Locked after the document.
+ pub(crate) screening: Arc<std::sync::Mutex<crate::access::screen::Screening>>,
pub(crate) token_lifetime: Duration,
pub(crate) session_lifetime: Duration,
/// Origins whose pages may call this node's open endpoints (`api::cors`).
@@ -490,6 +493,14 @@ impl AppState {
tokio::task::spawn_blocking(move || {
let result = {
let mut replica = state.doc.blocking_write();
+ // A write through the API under a policy this node doesn't know: what
+ // the others would refuse it can't tell (`access::policy`).
+ if user.is_some()
+ && let crate::access::policy::Policy::Unknown(why) =
+ crate::access::policy::of(&replica.doc)
+ {
+ return Err(E::from(StoreError::UnknownPolicy(why)));
+ }
// Checked here, under the lock: the caller's readiness check (if any)
// was before a rejoin could swap in a fresh document.
if !replica.writable {