swarm-controller: stop minting the per-hive matrix sender account and token nothing reads
This commit is contained in:
parent
cb5fb3c730
commit
c66832febf
1 changed files with 40 additions and 556 deletions
|
|
@ -1,195 +0,0 @@
|
|||
//! Each hive's sender account, `@hive-<hive>:`, on the swarm's homeserver:
|
||||
//! created here with the **swarm's** appservice token and stored at
|
||||
//! `swarm/hives/<hive>/matrix/sender-token`, where hive-c0re's matrix sweep
|
||||
//! reads it under the hive's own store identity.
|
||||
//!
|
||||
//! Runs for every hive in the directory, local or remote, and decides the
|
||||
//! same way [`super::agent_token`] does: a stored token that `whoami`
|
||||
//! confirms as `@hive-<hive>:` is kept, so this writes only when the path is
|
||||
//! empty or its token is dead.
|
||||
//!
|
||||
//! This is the only writer of that path: hive-c0re never mints, it takes
|
||||
//! whatever the store holds.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use swarm_matrix_client as homeserver;
|
||||
use swarm_secret_client::matrix;
|
||||
|
||||
use super::agent_token::{Decision, Observed, appservice_token, classify, mint_at, plan, probe_at};
|
||||
|
||||
/// How often [`spawn`] re-checks every hive's sender token.
|
||||
const RECONCILE_INTERVAL: std::time::Duration = std::time::Duration::from_mins(5);
|
||||
|
||||
/// Decide what to do about `hive`'s sender token from what the stored one is.
|
||||
fn classify_hive(hive: &str, probe: &super::agent_token::Probe) -> Decision {
|
||||
classify(&matrix::hive_localpart(hive), probe)
|
||||
}
|
||||
|
||||
/// Make sure `hive`'s sender account holds a live token in the store,
|
||||
/// creating the account or logging in to it when it does not. The whole job
|
||||
/// of the `MintHiveSenderToken` node.
|
||||
///
|
||||
/// # Errors
|
||||
/// When the store or the homeserver refuses a step, the swarm appservice
|
||||
/// token has not been published, or the new token authenticates as someone
|
||||
/// else.
|
||||
pub async fn ensure_hive_sender_token(base: &str, hive: &str) -> Result<()> {
|
||||
let store = crate::store::connect()
|
||||
.await
|
||||
.context("logging in to the swarm secret store")?;
|
||||
let as_token = appservice_token(&store).await?;
|
||||
let http = homeserver::client()?;
|
||||
let path = matrix::sender_token_path(hive)?;
|
||||
let reason = match classify_hive(hive, &probe_at(&store, &http, base, &path).await?) {
|
||||
Decision::Keep => {
|
||||
tracing::debug!(hive, "hive sender token is current; left as it is");
|
||||
return Ok(());
|
||||
}
|
||||
Decision::Mint(reason) => reason,
|
||||
};
|
||||
let localpart = matrix::hive_localpart(hive);
|
||||
mint_at(&store, &http, base, &localpart, &as_token, &path).await?;
|
||||
tracing::info!(hive, ?reason, %path, "hive sender token minted and stored");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// One pass: every hive in `hives`, observed.
|
||||
///
|
||||
/// Fails as a whole when the swarm appservice token is not published, for
|
||||
/// the reason `agent_token`'s pass does.
|
||||
async fn observe_all(base: &str, hives: &[String]) -> Result<Vec<(String, Observed)>> {
|
||||
let store = crate::store::connect()
|
||||
.await
|
||||
.context("logging in to the swarm secret store")?;
|
||||
appservice_token(&store).await?;
|
||||
let http = homeserver::client()?;
|
||||
let mut observed = Vec::with_capacity(hives.len());
|
||||
for hive in hives {
|
||||
let o = match matrix::sender_token_path(hive) {
|
||||
Ok(path) => match probe_at(&store, &http, base, &path).await {
|
||||
Ok(p) => Observed::Decided(classify_hive(hive, &p)),
|
||||
Err(e) => {
|
||||
tracing::warn!(hive, error = %format!("{e:#}"), "hive sender token: read failed");
|
||||
Observed::Unknown
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
tracing::warn!(hive, error = %e, "hive sender token: the hive name forms no store path");
|
||||
Observed::Unknown
|
||||
}
|
||||
};
|
||||
observed.push((hive.clone(), o));
|
||||
}
|
||||
Ok(observed)
|
||||
}
|
||||
|
||||
/// Check every hive's sender token now and every [`RECONCILE_INTERVAL`]
|
||||
/// after, and hand the hives that need one to `enqueue`, which inserts a
|
||||
/// `MintHiveSenderToken` node for each.
|
||||
///
|
||||
/// The roster is the controller's hive directory. A pass that fails is
|
||||
/// logged and retried on the next tick; it never stops the daemon.
|
||||
pub fn spawn(base: Arc<str>, hives: Vec<String>, enqueue: impl Fn(Vec<String>) + Send + 'static) {
|
||||
tokio::spawn(async move {
|
||||
let mut ticker = tokio::time::interval(RECONCILE_INTERVAL);
|
||||
loop {
|
||||
ticker.tick().await;
|
||||
match observe_all(&base, &hives).await {
|
||||
Ok(observed) => {
|
||||
let minting = plan(&observed);
|
||||
if minting.is_empty() {
|
||||
tracing::debug!(
|
||||
checked = observed.len(),
|
||||
"hive sender tokens: all current"
|
||||
);
|
||||
} else {
|
||||
tracing::info!(
|
||||
checked = observed.len(),
|
||||
minting = minting.len(),
|
||||
"hive sender tokens: queueing mints"
|
||||
);
|
||||
enqueue(minting);
|
||||
}
|
||||
}
|
||||
Err(e) => tracing::warn!(
|
||||
error = %format!("{e:#}"),
|
||||
retry_in_s = RECONCILE_INTERVAL.as_secs(),
|
||||
"hive sender tokens: pass failed; retrying next tick"
|
||||
),
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::super::agent_token::{MintReason, Probe};
|
||||
use super::*;
|
||||
use swarm_matrix_client::Whoami;
|
||||
|
||||
fn user(id: &str) -> Probe {
|
||||
Probe::Whoami(Whoami::User(id.to_owned()))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_live_token_for_the_hive_account_is_kept() {
|
||||
assert_eq!(
|
||||
classify_hive("pr1ma", &user("@hive-pr1ma:t.local")),
|
||||
Decision::Keep
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nothing_stored_mints() {
|
||||
// A hive whose homeserver is remote: nothing has ever written its path.
|
||||
assert_eq!(
|
||||
classify_hive("pr1ma", &Probe::NotStored),
|
||||
Decision::Mint(MintReason::NotStored)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_dead_token_mints() {
|
||||
// A token the homeserver revoked, or a device a manual login replaced.
|
||||
assert_eq!(
|
||||
classify_hive("pr1ma", &Probe::Whoami(Whoami::UnknownToken)),
|
||||
Decision::Mint(MintReason::Revoked)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_token_for_another_account_mints() {
|
||||
// The bare hive name is an agent's localpart, not the hive's account,
|
||||
// and another hive's account is not this one's.
|
||||
for other in ["@pr1ma:t.local", "@hive-beta:t.local", "@hive:t.local"] {
|
||||
assert_eq!(
|
||||
classify_hive("pr1ma", &user(other)),
|
||||
Decision::Mint(MintReason::OtherUser),
|
||||
"{other}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_outage_plans_nothing() {
|
||||
// A mint replaces the token the hive is running on, so a pass that
|
||||
// could not read must not mint for every hive.
|
||||
let observed = [
|
||||
("alpha".to_owned(), Observed::Unknown),
|
||||
("beta".to_owned(), Observed::Unknown),
|
||||
];
|
||||
assert!(plan(&observed).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_published_path_is_the_one_the_hive_reads() {
|
||||
// hive-c0re's `stored_sender_token` resolves this path; the literal is
|
||||
// what the bao grant names.
|
||||
assert_eq!(
|
||||
matrix::sender_token_path("pr1ma").expect("a plain name is legal"),
|
||||
"swarm/hives/pr1ma/matrix/sender-token"
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue