diff options
Diffstat (limited to 'src/access')
| -rw-r--r-- | src/access/mod.rs | 5 | ||||
| -rw-r--r-- | src/access/policy.rs | 93 | ||||
| -rw-r--r-- | src/access/screen.rs | 75 |
3 files changed, 169 insertions, 4 deletions
diff --git a/src/access/mod.rs b/src/access/mod.rs index 1109fa6..ea2e65c 100644 --- a/src/access/mod.rs +++ b/src/access/mod.rs @@ -13,12 +13,9 @@ use serde::{Deserialize, Serialize}; use crate::document::meta; +pub(crate) mod policy; pub(crate) mod registry; -// Judging changes on receipt is what uses it; until then, the tests. -#[cfg_attr(not(test), allow(dead_code))] pub(crate) mod rules; -// Judging changes on receipt is what uses it; until then, the tests. -#[cfg_attr(not(test), allow(dead_code))] pub(crate) mod screen; pub(crate) mod token; diff --git a/src/access/policy.rs b/src/access/policy.rs new file mode 100644 index 0000000..7ddef0e --- /dev/null +++ b/src/access/policy.rs @@ -0,0 +1,93 @@ +//! The swarm's write protection policy (`_continuum/access/policy`): whether every node +//! judges received changes by rules by path before applying them (`access::screen`, +//! `access::rules`). Absent, nobody does: what exists keeps working as it did. Written +//! by a user who may write protocol data, as any of it. +//! +//! `{"version": 1, "signatures": "optional"}`: rules enforced; a change is its claimed +//! committer's whether or not a signature proves it (`document::signatures`). A policy +//! this node doesn't know (another version, or a setting it lacks) stops it from taking +//! writes through the API, and it receives without judging: it can't judge as the +//! others do, so it writes nothing they might refuse, until it is upgraded. + +use serde_json::Value as JsonValue; + +use crate::document::{ReadDoc, meta, store}; + +/// The policy versions this node knows. +pub(crate) const VERSION: u64 = 1; + +/// Where the policy is. +pub(crate) fn path() -> Vec<String> { + meta::path(&[meta::ACCESS, "policy"]) +} + +/// What the policy says. +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) enum Policy { + /// None: changes aren't judged. + Open, + /// Rules by path enforced. + Enforced, + /// One this node doesn't know, and why. + Unknown(String), +} + +impl Policy { + pub(crate) fn enforced(&self) -> bool { + matches!(self, Policy::Enforced) + } +} + +/// The policy `doc` holds. +pub(crate) fn of(doc: &impl ReadDoc) -> Policy { + let value = match store::read_path(doc, &path()) { + Ok(Some(value)) => value, + Ok(None) => return Policy::Open, + Err(error) => return Policy::Unknown(format!("it doesn't read: {error}")), + }; + let version = value.get("version").and_then(JsonValue::as_u64); + if version != Some(VERSION) { + return Policy::Unknown(format!( + "version {}, this node knows {VERSION}", + value + .get("version") + .map_or("none".to_owned(), JsonValue::to_string) + )); + } + match value.get("signatures").and_then(JsonValue::as_str) { + None | Some("optional") => Policy::Enforced, + Some(other) => Policy::Unknown(format!("signatures {other:?}")), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::document::Document; + use crate::document::testing::change; + use serde_json::json; + + fn with(policy: JsonValue) -> Policy { + let mut doc = Document::new(); + change(&mut doc, |doc| { + store::write_path(doc, &path(), &policy).unwrap(); + }); + of(&doc) + } + + #[test] + fn what_the_policy_says() { + assert_eq!(of(&Document::new()), Policy::Open); + assert_eq!(with(json!({"version": 1})), Policy::Enforced); + assert_eq!( + with(json!({"version": 1, "signatures": "optional"})), + Policy::Enforced + ); + assert!(matches!(with(json!({"version": 2})), Policy::Unknown(_))); + assert!(matches!( + with(json!({"version": 1, "signatures": "sometimes"})), + Policy::Unknown(_) + )); + assert!(matches!(with(json!("on")), Policy::Unknown(_))); + } +} diff --git a/src/access/screen.rs b/src/access/screen.rs index dd9ffcd..66b7fc8 100644 --- a/src/access/screen.rs +++ b/src/access/screen.rs @@ -36,6 +36,52 @@ pub(crate) struct Refused { pub(crate) why: String, } +/// What screening keeps between batches, for one document (`AppState::screening`): the +/// changes waiting for their dependencies, and those refused, for operators +/// (`GET /admin/refused`). +#[derive(Debug, Default)] +pub(crate) struct Screening { + /// The document's (`Replica::epoch_seq`): another's start empty (`of`). + epoch_seq: u32, + pub(crate) waiting: Waiting, + refused: std::collections::VecDeque<Refused>, +} + +impl Screening { + /// Itself for the document `epoch_seq`: emptied if it was another's. + pub(crate) fn of(&mut self, epoch_seq: u32) -> &mut Self { + if self.epoch_seq != epoch_seq { + *self = Screening { + epoch_seq, + ..Screening::default() + }; + } + self + } + + /// Keeps `refused`, logged, the oldest going past `MAX_REFUSED`. + pub(crate) fn record(&mut self, refused: Vec<Refused>) { + for refused in refused { + tracing::warn!( + change = %refused.change.hash(), + committer = refused.committer.as_deref().unwrap_or("none"), + user = refused.user.as_deref().unwrap_or("none"), + why = %refused.why, + "Refused a change (_continuum/access/policy)" + ); + self.refused.push_back(refused); + if self.refused.len() > MAX_REFUSED { + self.refused.pop_front(); + } + } + } + + /// Those refused, oldest first. + pub(crate) fn refused(&self) -> impl DoubleEndedIterator<Item = &Refused> { + self.refused.iter() + } +} + /// Received changes waiting for their dependencies. #[derive(Debug, Default)] pub(crate) struct Waiting(Vec<Change>); @@ -179,6 +225,35 @@ pub(crate) fn apply( Ok(screened) } +/// [`apply`] of the changes in `bytes`, as sync messages and downloads carry them, with +/// what `screening` keeps for document `epoch_seq` (of `epoch`). Bytes that don't load +/// as changes, or with a misplaced author (`changes::misplaced_author`), are refused +/// whole. +pub(crate) fn apply_bytes( + doc: &mut Document, + epoch: Epoch, + epoch_seq: u32, + bytes: &[u8], + screening: &std::sync::Mutex<Screening>, + log: &mut crate::document::PatchLog, +) -> Result<usize> { + use crate::locks::MutexExt; + if bytes.is_empty() { + return Ok(0); + } + if let Some(misplaced) = crate::document::changes::misplaced_author(bytes) { + anyhow::bail!("Refused changes: {misplaced}"); + } + let changes = crate::document::changes::changes_in(bytes) + .ok_or_else(|| anyhow::anyhow!("Refused changes that don't load"))?; + let mut screening = screening.lock_or_recover(); + let screening = screening.of(epoch_seq); + let screened = apply(doc, epoch, changes, &mut screening.waiting, log)?; + let applied = screened.applied; + screening.record(screened.refused); + Ok(applied) +} + /// Whether a change touching `paths` changes what rules read: the members, the user /// registry, remake records, the protocol data as a whole. fn relevant(paths: &BTreeSet<Path>) -> bool { |
