diff options
| author | tv <tv@krebsco.de> | 2026-11-07 22:35:21 +0100 |
|---|---|---|
| committer | tv <tv@krebsco.de> | 2026-11-07 22:35:21 +0100 |
| commit | 5471e5285e94ce7576e6c86f260fea15ed9503d1 (patch) | |
| tree | 310be18d26611d6aaf6d8b02a118ba2fb6ef4aa1 | |
| parent | c3fde0fdd410902125f006f0a7d77ca030cb9061 (diff) | |
signatures: verified changes
GET /admin/changes shows whether a signature by its node vouches for
each change.
Auth plan M5.
| -rw-r--r-- | docs/api.md | 3 | ||||
| -rw-r--r-- | docs/openapi.yaml | 10 | ||||
| -rw-r--r-- | docs/trust.md | 8 | ||||
| -rw-r--r-- | src/api/changes.rs | 26 | ||||
| -rw-r--r-- | src/document/signatures.rs | 1 |
5 files changed, 32 insertions, 16 deletions
diff --git a/docs/api.md b/docs/api.md index a02d3d5..89da7ba 100644 --- a/docs/api.md +++ b/docs/api.md @@ -237,7 +237,8 @@ signs it, so a member could name another. In a swarm without a registry, user na are each node's own (`--htpasswd`, or its proxy's): `alice` on two nodes is one person only where their operators make it so, which nothing checks. `GET /admin/changes` lists them, -newest first, with the paths each touched; `GET /admin/changes/settings` those that +newest first, with the paths each touched and whether a signature by its node vouches +for it (`verified`, Signed changes); `GET /admin/changes/settings` those that touched what is at `settings`. Every node holds every change, so user names are known to the whole swarm, and so is who wrote what, until a remake drops the history ([`remakes.md`](remakes.md)). diff --git a/docs/openapi.yaml b/docs/openapi.yaml index 7aba2fe..d067f4f 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -601,7 +601,7 @@ paths: type: array items: type: object - required: [hash, at, paths] + required: [hash, at, verified, paths] properties: hash: type: string @@ -611,6 +611,9 @@ paths: type: string user: type: string + verified: + type: boolean + description: A signature by its node vouches for it, as far as this node has the signatures (`api.md`, Signed changes). paths: type: array items: @@ -652,7 +655,7 @@ paths: type: array items: type: object - required: [hash, at, paths] + required: [hash, at, verified, paths] properties: hash: type: string @@ -662,6 +665,9 @@ paths: type: string user: type: string + verified: + type: boolean + description: A signature by its node vouches for it, as far as this node has the signatures (`api.md`, Signed changes). paths: type: array items: diff --git a/docs/trust.md b/docs/trust.md index 8b0f72a..70369e2 100644 --- a/docs/trust.md +++ b/docs/trust.md @@ -61,8 +61,12 @@ node acts on what the document says: * **User data**: any member can write or delete any path. * **Who wrote what** (`GET /admin/changes`): each change names its node and API user - as the writing node says; nothing signs that, so a member can name another. The - names of the users who write are in the document, and so on every node. + as the writing node says. Nodes sign their changes ([`api.md`](api.md), Signed + changes), so a change a signature vouches for shows `verified`: its node made it. + One that shows unverified may yet be vouched for by its node's next signature, or + may be another member's claiming that node; nothing is refused for it yet. The user a + change names is its node's claim, signed or not. The names of the users who write + are in the document, and so on every node. * **The member registry**: each node writes its own entry, but nothing stops a member from writing others'. The registry decides the topology and, through the alive stamps, the electorate of a remake. diff --git a/src/api/changes.rs b/src/api/changes.rs index 419f5ed..c3a847d 100644 --- a/src/api/changes.rs +++ b/src/api/changes.rs @@ -2,8 +2,9 @@ //! or of what is at a path (as `/store/<path>` 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`). Every node holds every change, so any node answers -//! for the whole swarm, as far as it has synced. +//! (`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, @@ -39,6 +40,9 @@ pub(crate) struct ChangeReport { /// The user it was written for through the API, if any. #[serde(skip_serializing_if = "Option::is_none")] user: Option<String>, + /// 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<String>, } @@ -82,18 +86,19 @@ async fn report( return Err(StatusCode::SERVICE_UNAVAILABLE); } let limit = query.limit.unwrap_or(100).clamp(1, MAX_LIMIT); - Ok(Json( - state - .render( - move |doc| changes(doc, limit, path.as_deref()), - |report| report, - ) - .await, - )) + 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 { @@ -128,6 +133,7 @@ fn changes( .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(), }); } diff --git a/src/document/signatures.rs b/src/document/signatures.rs index a6a30f7..d347e10 100644 --- a/src/document/signatures.rs +++ b/src/document/signatures.rs @@ -341,7 +341,6 @@ impl Signatures { } /// Whether a signature vouches for `actor`'s change `seq`. - #[cfg_attr(not(test), allow(dead_code))] pub(crate) fn verified(&self, actor: &super::ActorId, seq: u64) -> bool { self.latest .get(actor) |
