Watch
0
0
Fork
You've already forked hyperhive
0
hyperhive/swarm-secret-client/src/archive.rs
atlas 91f36f39d2 swarm-archive: grant only once an archive is up; keep logins across a gap in placements
The archive host creates a ready marker in the store and swarm-controller
writes no credentials before it exists, so a swarm without an archive, and a
mixed-version swarm, leave every hive backup a logged no-op. A hive with no
declaration no longer counts as placing nothing: its copies and its agents'
user lines stay. The restic password is pinned by tests across a revoke, a
re-placement and a move.
2026-11-10 08:18:45 +01:00

143 lines
4.8 KiB
Rust

//! The archive agreement: where `swarm-controller` keeps each agent's archive
//! credentials, and where its hive and the archive host read them.
//!
//! Three paths per agent, all written by the controller:
//!
//! - [`grant_path`], `swarm/hives/<hive>/archive/<agent>`: the REST password the
//! agent's hive logs in to the archive with, and the restic repository
//! password. The hive's own `swarm/hives/<hive>/*` stanza reads it, and only
//! the hive that places the agent holds one.
//! - [`user_path`], `swarm/services/archive/users/<agent>`: the htpasswd line
//! the archive host renders its users file from. A hash, never the password.
//! - [`restic_path`], `swarm/services/archive/restic/<agent>`: the same restic
//! password, kept where it outlives a move. The archive host reads it to
//! prune the repository.
//!
//! The nix half is `swarm-bao.nix`'s archive grants and `swarm-archive.nix`.
use crate::{
Error,
path::{Kind, checked_segment, principal_prefix},
};
/// The service segment under `swarm/services/`.
pub const SERVICE: &str = "archive";
/// The field of [`grant_path`] holding the REST password.
pub const REST_PASSWORD: &str = "rest-password";
/// The field of [`grant_path`] holding the restic repository password.
pub const RESTIC_PASSWORD: &str = "restic-password";
/// The field of [`restic_path`] holding the restic repository password.
pub const PASSWORD: &str = "password";
/// The field of [`user_path`] holding the htpasswd line.
pub const HTPASSWD: &str = "htpasswd";
/// The field of [`ready_path`].
pub const READY: &str = "ready";
/// Where the archive host announces itself. `swarm-controller` writes no
/// credentials until this exists, so a swarm without an archive gets none.
///
/// # Errors
/// Never in practice: the segments are constants.
pub fn ready_path() -> Result<String, Error> {
Ok(format!(
"{}/ready",
principal_prefix(Kind::Service, SERVICE)?
))
}
/// The directory holding every [`user_path`], to list.
///
/// # Errors
/// Never in practice: the segments are constants.
pub fn users_prefix() -> Result<String, Error> {
Ok(format!(
"{}/users",
principal_prefix(Kind::Service, SERVICE)?
))
}
/// Where the archive host reads `agent`'s htpasswd line.
///
/// # Errors
/// [`Error::PathSegment`] when `agent` contains anything but `[A-Za-z0-9_-]`.
pub fn user_path(agent: &str) -> Result<String, Error> {
checked_segment(Kind::Agent.label(), agent)?;
Ok(format!("{}/{agent}", users_prefix()?))
}
/// Where the archive host reads `agent`'s restic password.
///
/// # Errors
/// [`Error::PathSegment`] when `agent` contains anything but `[A-Za-z0-9_-]`.
pub fn restic_path(agent: &str) -> Result<String, Error> {
checked_segment(Kind::Agent.label(), agent)?;
Ok(format!(
"{}/restic/{agent}",
principal_prefix(Kind::Service, SERVICE)?
))
}
/// The directory holding every [`grant_path`] of `hive`, to list.
///
/// # Errors
/// [`Error::PathSegment`] when `hive` contains anything but `[A-Za-z0-9_-]`.
pub fn grants_prefix(hive: &str) -> Result<String, Error> {
Ok(format!("{}/archive", principal_prefix(Kind::Hive, hive)?))
}
/// Where `hive` reads the credentials of `agent`, which it places.
///
/// # Errors
/// [`Error::PathSegment`] when `hive` or `agent` contains anything but
/// `[A-Za-z0-9_-]`, which keeps one name from addressing another's secret.
pub fn grant_path(hive: &str, agent: &str) -> Result<String, Error> {
checked_segment(Kind::Agent.label(), agent)?;
Ok(format!("{}/{agent}", grants_prefix(hive)?))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_paths_are_the_ones_the_store_grants() {
// Spelled out: `swarm-bao.nix` grants each pattern.
assert_eq!(
user_path("atlas").expect("a plain name"),
"swarm/services/archive/users/atlas"
);
assert_eq!(
restic_path("atlas").expect("a plain name"),
"swarm/services/archive/restic/atlas"
);
assert_eq!(
grant_path("alpha", "atlas").expect("plain names"),
"swarm/hives/alpha/archive/atlas"
);
assert_eq!(
ready_path().expect("constant"),
"swarm/services/archive/ready"
);
assert_eq!(
users_prefix().expect("constant"),
"swarm/services/archive/users"
);
assert_eq!(
grants_prefix("alpha").expect("a plain name"),
"swarm/hives/alpha/archive"
);
}
#[test]
fn a_name_cannot_address_another_principals_credentials() {
assert!(grant_path("alpha", "atlas/../argus").is_err());
assert!(grant_path("alpha/../beta", "atlas").is_err());
assert!(user_path("../restic/atlas").is_err());
assert!(restic_path("").is_err());
}
}