//! The rules every path into the store obeys, whatever kind of secret it //! addresses. //! //! The controller writes and a hive reads, and neither is senior to the other, //! so what they must agree on is stated once here rather than at each call //! site. A *particular* kind of secret builds its path from these pieces in its //! own module — [`crate::matrix`] is the one that exists. use crate::Error; /// The KV v2 mount every swarm secret lives under. /// /// A literal because the store's existing reader already hardcodes the same /// one (`secret/swarm/matrix/registration-token`); an option nobody sets would /// be two ways to say one thing. pub const MOUNT: &str = "secret"; /// The prefix under [`MOUNT`] owned by per-agent credentials. pub const AGENT_PREFIX: &str = "swarm/agents"; /// A path segment that cannot change the path's shape. /// /// The charset is deliberately narrower than what the store accepts: a `/` /// turns one agent's segment into another agent's directory, and `..` walks /// out of the prefix entirely. Both are names this crate receives from /// elsewhere — an agent name from the topology, an account name from an /// agent's own config — so neither is trusted to be well-formed here. /// /// Public because the same name is also used to build a path **on disk**, and /// that guard must accept exactly what this one does. Two copies of a charset /// are two charsets: they agree until one is edited, and the day they diverge /// is the day a name is legal in the store and not on the filesystem, or the /// reverse. /// /// # Errors /// [`Error::PathSegment`] when `value` is empty or holds anything outside /// `[A-Za-z0-9_-]`. pub fn checked_segment(kind: &'static str, value: &str) -> Result<(), Error> { if value.is_empty() { return Err(Error::PathSegment { kind, value: value.to_owned(), }); } if !value .bytes() .all(|b| b.is_ascii_alphanumeric() || b == b'-' || b == b'_') { return Err(Error::PathSegment { kind, value: value.to_owned(), }); } Ok(()) } #[cfg(test)] mod tests { use super::*; #[test] fn a_segment_that_could_change_a_paths_shape_is_refused() { // Each of these is a *different* way to reach outside the segment the // caller meant, and the last two are the ones a charset check catches // but a `contains("..")` check does not. for bad in [ "../argus", "atlas/../argus", "atlas/matrix", "a b", "a.b", "", ] { assert!( checked_segment("agent", bad).is_err(), "segment {bad:?} must be refused" ); } } #[test] fn the_legal_charset_is_actually_reachable() { // The control for the test above: if `checked_segment` rejected // everything, the escape cases would pass for the wrong reason. The // charset is the one `hive-priv` accepts for the same names on disk, // so uppercase and underscore have to stay legal here. assert!(checked_segment("agent", "a-b_C9").is_ok()); assert!(checked_segment("account", "d-e_F0").is_ok()); } #[test] fn the_rejected_segment_is_named_in_the_error() { // The caller usually got the name from config and needs to see which // of the two it was. let e = checked_segment("account", "a b").expect_err("a space is not legal"); assert!( matches!(e, Error::PathSegment { kind, ref value } if kind == "account" && value == "a b"), "got {e:?}" ); } #[test] fn the_bao_policy_grants_exactly_these_two_values() { // Renaming either constant is a silent 403 at provision time, not a // compile error: the controller's grant spells them out in // `nix/host-modules/swarm-bao.nix` (`controllerPolicyText`), which no // Rust change can reach. Editing here means editing there, and // `nix/module-eval.nix` asserts the other side of the same pair. assert_eq!(MOUNT, "secret"); assert_eq!(AGENT_PREFIX, "swarm/agents"); } }