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 | f44fbf5b93269bb3573e8a2701e70ab0d7613153 (patch) | |
| tree | 32915998f6e797afcc13f485ccde66bac0e94150 /src/api | |
| parent | 389b11b597b6bf60f120b190002bbb76fff28dbf (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/api')
| -rw-r--r-- | src/api/changes.rs | 40 | ||||
| -rw-r--r-- | src/api/mod.rs | 2 |
2 files changed, 42 insertions, 0 deletions
diff --git a/src/api/changes.rs b/src/api/changes.rs index c3a847d..13c8dd3 100644 --- a/src/api/changes.rs +++ b/src/api/changes.rs @@ -139,3 +139,43 @@ fn changes( } 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<String>, + #[serde(skip_serializing_if = "Option::is_none")] + user: Option<String>, + paths: Vec<String>, + 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<AppState>) -> Json<Vec<RefusedReport>> { + 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(), + ) +} diff --git a/src/api/mod.rs b/src/api/mod.rs index 38868e2..a12e219 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -62,6 +62,7 @@ pub(crate) fn router(state: AppState, auth_state: AuthState, debug_api: bool) -> .route("/admin/stats", get(stats::get_stats)) .route("/admin/document", get(document::get_document)) .route("/admin/changes", get(changes::get_changes)) + .route("/admin/refused", get(changes::get_refused)) .route("/admin/changes/{*key}", get(changes::get_changes_at)) .route("/admin/config", get(config::get_config)) .route("/auth/login", axum::routing::post(login::login)) @@ -311,6 +312,7 @@ pub(crate) fn store_status(error: StoreError, context: &str) -> ApiError { | StoreError::NotCastable { .. } => StatusCode::CONFLICT, StoreError::NotFound | StoreError::UnknownHeads => StatusCode::NOT_FOUND, StoreError::Rejoining => StatusCode::SERVICE_UNAVAILABLE, + StoreError::UnknownPolicy(_) => StatusCode::CONFLICT, other => { error!(error = %format!("{other:#}"), "{context}"); return StatusCode::INTERNAL_SERVER_ERROR.into(); |
