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
109 lines
3.6 KiB
Rust
109 lines
3.6 KiB
Rust
//! `swarm-matrix-ctl` — the rust that runs *inside* `containers.hive-matrix`.
|
|
//!
|
|
//! One binary with subcommands rather than one binary per job. The container
|
|
//! is an awkward place to put code — it needs its own store identity, its own
|
|
//! bind mounts and its own cert role — and all of that is per-*container*, not
|
|
//! per-task. A second single-purpose crate would have had to duplicate the
|
|
//! identity plumbing to add one action, so the next thing that has to run in
|
|
//! here is a verb below, not a new crate.
|
|
//!
|
|
//! [`appservice`] mints the **swarm's** own appservice registration and
|
|
//! publishes its token for `swarm-controller`.
|
|
//!
|
|
//! 🩸 **A secret is a path, never a value.** The only identifier any verb here
|
|
//! logs is the store path; see `swarm_matrix_client`'s module doc for the same rule
|
|
//! applied to error messages.
|
|
|
|
mod appservice;
|
|
mod registration;
|
|
|
|
use anyhow::Result;
|
|
use clap::{Parser, Subcommand};
|
|
|
|
#[derive(Debug, Parser)]
|
|
#[command(
|
|
name = "swarm-matrix-ctl",
|
|
about = "Act on the swarm's matrix homeserver from inside its container"
|
|
)]
|
|
struct Cli {
|
|
#[command(subcommand)]
|
|
command: Command,
|
|
}
|
|
|
|
#[derive(Debug, Subcommand)]
|
|
enum Command {
|
|
/// The swarm's own appservice registration, whose sender is the
|
|
/// homeserver's admin account. Configured by `MATRIX_APPSERVICE_*`.
|
|
#[command(subcommand)]
|
|
Appservice(Appservice),
|
|
}
|
|
|
|
#[derive(Debug, Subcommand)]
|
|
enum Appservice {
|
|
/// Mint the tokens when absent and render the registration tuwunel loads.
|
|
/// Local only: it runs before the homeserver and must not need a network.
|
|
Render,
|
|
/// Write the rendered `as_token` to the swarm secret store when the
|
|
/// store's copy differs.
|
|
Publish,
|
|
}
|
|
|
|
#[tokio::main]
|
|
async fn main() -> Result<()> {
|
|
tracing_subscriber::fmt()
|
|
.with_env_filter(
|
|
tracing_subscriber::EnvFilter::try_from_default_env()
|
|
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")),
|
|
)
|
|
.init();
|
|
|
|
match Cli::parse().command {
|
|
Command::Appservice(Appservice::Render) => appservice::render(),
|
|
Command::Appservice(Appservice::Publish) => appservice::publish().await,
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use clap::CommandFactory;
|
|
|
|
#[test]
|
|
fn the_clap_tree_is_well_formed() {
|
|
Cli::command().debug_assert();
|
|
}
|
|
|
|
/// The two units name these verbs in `ExecStart`, so a rename of one is a
|
|
/// deploy-time failure with no local signal. This is that signal.
|
|
#[test]
|
|
fn the_appservice_verbs_are_spelled_the_way_the_units_invoke_them() {
|
|
let cli =
|
|
Cli::try_parse_from(["swarm-matrix-ctl", "appservice", "render"]).expect("a verb");
|
|
assert!(matches!(
|
|
cli.command,
|
|
Command::Appservice(Appservice::Render)
|
|
));
|
|
let cli =
|
|
Cli::try_parse_from(["swarm-matrix-ctl", "appservice", "publish"]).expect("a verb");
|
|
assert!(matches!(
|
|
cli.command,
|
|
Command::Appservice(Appservice::Publish)
|
|
));
|
|
Cli::try_parse_from(["swarm-matrix-ctl", "appservice"])
|
|
.expect_err("a sub-verb is required");
|
|
}
|
|
|
|
#[test]
|
|
fn an_unknown_verb_is_refused() {
|
|
Cli::try_parse_from(["swarm-matrix-ctl", "conjure"])
|
|
.expect_err("only declared verbs are accepted");
|
|
}
|
|
|
|
/// A bare invocation must not silently do something. `appservice publish`
|
|
/// writes a credential, so "no verb" defaulting to it would make a typo in
|
|
/// the unit write rather than fail.
|
|
#[test]
|
|
fn no_verb_at_all_is_refused() {
|
|
Cli::try_parse_from(["swarm-matrix-ctl"]).expect_err("a verb is required");
|
|
}
|
|
}
|