Watch
0
0
Fork
You've already forked hyperhive
0
hyperhive/swarm-matrix-ctl/src/registration.rs
atlas 48067d07e8 matrix: drop the per-hive appservice and its unused token
Every hive's homeserver loaded a per-hive `hyperhive` appservice
registration, and the swarm minted and delivered an as_token for it
through bao. No client presented that token: swarm-controller creates
every `@hive-<hive>` sender and every agent account with the swarm
appservice token (hive_sender.rs, agent_token.rs), and no Rust code read
the per-hive one.

Removed, with everything that existed only to serve it:
- hive-matrix.nix: the `hyperhive` registration, its activation-time
  render, the host bind mount of /var/lib/hyperhive/matrix-appservice,
  its LoadCredential entry, and the internal options
  `deploy.matrix.appserviceTokenFile` / `appserviceRegistrationScript`.
  tuwunel now loads only the swarm registration.
- glue-matrix-bao-token.nix and its `swarm-bao-matrix-token` unit.
- glue-bao-tls.nix: the matrix-token leaf and its defaults.
- swarm-bao.nix: the `swarm-matrix-token-<hive>` roles and policies
  (`swarm-bao-matrix-token-policy`), and the options
  `deploy.bao.matrixTokenClient{Cert,Key}File` and
  `matrixTokenCommonNamePrefix`, now mkRemovedOptionModule.
- swarm-secret-publisher.nix: the per-hive token mint and its state dir.
- swarm.nix: the reserved `<matrixTokenCommonNamePrefix>-<hive>` CN.
- swarm-secret-client: `matrix::appservice_token_path`.

The swarm registration (swarm-matrix-ctl, swarm-controller) is untouched.
Comments and docs that pointed at the removed glue file now point at
glue-queue-agent-credential.nix, which has the same reader shape.

Refs #4428
2026-10-05 07:48:17 +02:00

102 lines
4 KiB
Rust

//! Reading the `as_token` out of the appservice registration the matrix
//! container already has.
//!
//! No new credential is delivered for this. The registration **is** the
//! `as_token`, and `appservice render` writes it into this container's own
//! state dir — so the file this module opens is one this process could already
//! read, and one the homeserver beside it reads too.
//!
//! Scanned line-by-line rather than parsed as YAML. The file has exactly one
//! renderer (`registration_yaml` in `appservice.rs`, a `format!` of
//! `as_token: <hex>`), so a parser would be a second, looser reading of a shape
//! this repo writes itself — and it would pull a YAML crate into a binary whose
//! only other input is JSON.
use anyhow::{Context, Result, bail};
/// The key the token is stored under, and the whole of the agreement with the
/// renderer.
const KEY: &str = "as_token:";
/// Read the registration at `path` and return its `as_token`.
///
/// # Errors
/// When the file cannot be read, or holds no `as_token` with a value — which is
/// what a registration rendered by something other than this repo looks like
/// from here.
pub fn as_token(path: &str) -> Result<String> {
let text = std::fs::read_to_string(path)
.with_context(|| format!("reading the appservice registration at {path}"))?;
// The path, not the file's contents: every line of it is either a secret or
// a shape this module already knows.
extract(&text).with_context(|| format!("no `as_token` in the registration at {path}"))
}
/// [`as_token`] over text already in hand, so the agreement with the renderer
/// can be tested without a file.
fn extract(text: &str) -> Result<String> {
for line in text.lines() {
if let Some(rest) = line.strip_prefix(KEY) {
let token = rest.trim();
if token.is_empty() {
bail!("the registration's `as_token` is empty");
}
return Ok(token.to_owned());
}
}
bail!("the registration carries no `as_token` line")
}
#[cfg(test)]
mod tests {
use super::*;
/// The registration in the shape `registration_yaml` renders it, tokens
/// last. Reproduced rather than rendered so this module's tests do not
/// depend on the renderer's.
const RENDERED: &str = "id: swarm\n\
url: null\n\
sender_localpart: swarm\n\
rate_limited: false\n\
namespaces:\n \
users:\n \
- exclusive: false\n \
regex: '@[a-z0-9._=/+-]+:example\\.test$'\n \
aliases: []\n \
rooms: []\n\
as_token: deadbeef\n\
hs_token: cafebabe\n";
#[test]
fn the_token_is_taken_from_the_registration_this_repo_renders() {
assert_eq!(
extract(RENDERED).expect("the rendered shape parses"),
"deadbeef"
);
}
#[test]
fn the_homeservers_own_token_is_not_mistaken_for_the_appservices() {
// `hs_token` authenticates the homeserver TO the appservice and is a
// different secret with a confusingly similar name; a substring search
// would find it inside neither, but a `contains("s_token")`-shaped one
// would. The control is that the line order in `RENDERED` puts
// `as_token` first, so this arm needs the reverse to mean anything.
let reversed = "hs_token: cafebabe\nas_token: deadbeef\n";
assert_eq!(
extract(reversed).expect("order does not matter"),
"deadbeef"
);
}
#[test]
fn a_registration_with_no_token_is_an_error_rather_than_an_empty_string() {
// An empty token authenticates nothing, and a homeserver answers a
// request carrying one with a 403 that names the account rather than
// the credential — so failing here is the only report an operator can
// act on.
for bad in ["id: swarm\n", "as_token:\n", "as_token: \n"] {
assert!(extract(bad).is_err(), "{bad:?} must not yield a token");
}
}
}