//! `GET /admin/changes[/]`: who wrote what, newest first, of the whole document //! or of what is at a path (as `/store/` names it). Every change names the node that //! committed it (its author) and, for a write through the API, the user who wrote it (in //! its message, `User:`), with when (its writer's clock) and the store paths it touched //! (`document::replica::Writer`), and whether a signature by that node vouches for it //! (`document::signatures`). Every node holds every change, so any node answers for the //! whole swarm, as far as it has synced. use axum::{ Json, extract::{Path, Query, State}, http::StatusCode, }; use serde::{Deserialize, Serialize}; use crate::document::store::{self, InLists}; use crate::state::AppState; /// Changes listed at most. const MAX_LIMIT: usize = 1000; /// Changes looked at at most, newest first, for those touching `path`: each costs /// decoding it, under the document's read lock (`store::changed_by`). const MAX_SCANNED: usize = 10_000; #[derive(Debug, Deserialize)] pub(crate) struct ChangesQuery { /// How many, at most (100 by default, up to `MAX_LIMIT`). limit: Option, } #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub(crate) struct ChangeReport { hash: String, /// When, by its writer's clock (Unix milliseconds); 0 for a remake's snapshot. at: i64, /// The node that made it (its node ID), if it says. #[serde(skip_serializing_if = "Option::is_none")] node: Option, /// The user it was written for through the API, if any. #[serde(skip_serializing_if = "Option::is_none")] user: Option, /// Whether a signature by its node vouches for it, as far as this node has the /// signatures (`document::signatures`). verified: bool, /// The store paths it touched (a list's items by index). paths: Vec, } #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub(crate) struct ChangesReport { changes: Vec, /// Older ones weren't looked at (`MAX_SCANNED`, or `limit` reached). more: bool, } /// `GET /admin/changes`: all of them. pub(crate) async fn get_changes( State(state): State, Query(query): Query, ) -> Result, StatusCode> { report(state, query, None).await } /// `GET /admin/changes/{key}`: those that touched what is at the path, inside it, or /// above it. pub(crate) async fn get_changes_at( State(state): State, Path(key): Path, Query(query): Query, ) -> Result, StatusCode> { let segments: Vec = key.split('/').map(str::to_owned).collect(); if segments.iter().any(String::is_empty) { return Err(StatusCode::BAD_REQUEST); } report(state, query, Some(segments)).await } async fn report( state: AppState, query: ChangesQuery, path: Option>, ) -> Result, StatusCode> { if !state.is_ready().await { return Err(StatusCode::SERVICE_UNAVAILABLE); } let limit = query.limit.unwrap_or(100).clamp(1, MAX_LIMIT); let doc = std::sync::Arc::clone(&state.doc); let report = tokio::task::spawn_blocking(move || { let replica = doc.blocking_read(); changes(&replica.doc, &replica.signatures, limit, path.as_deref()) }) .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; Ok(Json(report)) } fn changes( doc: &crate::document::Document, signatures: &crate::document::signatures::Signatures, limit: usize, path: Option<&[String]>, ) -> ChangesReport { let all = doc.get_changes_meta(&[]); let mut changes = Vec::new(); let mut scanned = 0; let mut more = false; for meta in all.iter().rev() { if changes.len() == limit || scanned == MAX_SCANNED { more = true; break; } scanned += 1; // What this change did, from its operations. let paths = store::changed_by(doc, &meta.hash, InLists::Index).unwrap_or_default(); if let Some(path) = path && !paths .iter() .any(|touched| touched.starts_with(path) || path.starts_with(touched)) { continue; } changes.push(ChangeReport { hash: meta.hash.to_string(), at: meta.timestamp, node: meta .author .as_ref() .and_then(|author| String::from_utf8(author.as_bytes().to_vec()).ok()), user: meta .message .as_deref() .and_then(crate::document::replica::user_of) .map(str::to_owned), verified: signatures.verified(&meta.actor, meta.seq), paths: paths.iter().map(|path| path.join("/")).collect(), }); } ChangesReport { changes, more } } /// A change the policy refused (`access::screen`): its hash, when its writer made it /// (by its clock), who made it and for whom, as it says, what it touched, and why. #[derive(Serialize)] #[serde(rename_all = "camelCase")] pub(crate) struct RefusedReport { hash: String, at: i64, #[serde(skip_serializing_if = "Option::is_none")] node: Option, #[serde(skip_serializing_if = "Option::is_none")] user: Option, paths: Vec, why: String, } /// `GET /admin/refused`: the changes this node refused under the swarm's policy /// (`_continuum/access/policy`), newest first, the last thousand at most, since it /// started or switched epochs. pub(crate) async fn get_refused(State(state): State) -> Json> { use crate::locks::MutexExt; let screening = state.screening.lock_or_recover(); Json( screening .refused() .rev() .map(|refused| RefusedReport { hash: refused.change.hash().to_string(), at: refused.change.timestamp(), node: refused.committer.clone(), user: refused.user.clone(), paths: refused .paths .iter() .map(|path| format!("/{}", path.join("/"))) .collect(), why: refused.why.clone(), }) .collect(), ) }