aboutsummaryrefslogtreecommitdiffstats
path: root/src/access
diff options
context:
space:
mode:
Diffstat (limited to 'src/access')
-rw-r--r--src/access/mod.rs5
-rw-r--r--src/access/policy.rs93
-rw-r--r--src/access/screen.rs75
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 {