Every hive is in a swarm and every swarm runs matrix, so every swarm has a swarm-controller, and since #4810 its hive_sender pass mints each hive's @hive-<hive>: sender token into the store every five minutes. The two other minters of that token go: - swarm-matrix-ctl mint: the systemd.services.swarm-matrix-ctl unit in the hive-matrix container, Command::Mint and src/mint.rs. The binary, its appservice render/publish verbs, ctlPackage, ctlActive and the ctl cert role stay. bao-matrix-reader's checks on the deleted unit are removed; the leaf-identity and no-token-in-env checks now look at swarm-matrix-appservice-publish, which runs under the same identity. - the hive-side mint ladder in hive-c0re's ensure_hive_user (register/appservice-login/password-login with the local as_token), with read_appservice_token, paths::matrix_appservice_token and the helpers only it used. ensure_hive_user now takes the store's token, keeps the file when the store has none or can't be reached, and fails otherwise. - hivectl matrix sync-admin: the verb, HostRequest::MatrixSyncAdmin and handle_matrix_sync_admin. The periodic MatrixSweep (ensure_all) is unchanged apart from no longer reading the local as_token. This removes the double-mint race #4810's review flagged: two minters logging in on one pinned device could leave a dead token in the store until the next pass. Closes #4813 Closes #4814
195 lines
7.2 KiB
Rust
195 lines
7.2 KiB
Rust
//! 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"
|
|
);
|
|
}
|
|
}
|