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.
143 lines
4.8 KiB
Rust
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());
|
|
}
|
|
}
|