//! 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//archive/`: the REST password the //! agent's hive logs in to the archive with, and the restic repository //! password. The hive's own `swarm/hives//*` stanza reads it, and only //! the hive that places the agent holds one. //! - [`user_path`], `swarm/services/archive/users/`: the htpasswd line //! the archive host renders its users file from. A hash, never the password. //! - [`restic_path`], `swarm/services/archive/restic/`: 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 { 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 { 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 { 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 { 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 { 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 { 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()); } }