//! The queue agreement: where a hive's agent-container credential lives in the //! store, and what the object at that path holds. //! //! The sibling of [`crate::matrix`], and it differs from it in one way worth //! reading before using either: a matrix credential is keyed per **agent**, //! this one per **hive**. Agents are created at runtime, so the queue //! identity they present is minted once per hive at deploy time and says which //! hive an agent belongs to, never which agent. use serde::{Deserialize, Serialize}; use crate::{ Error, path::{Kind, principal_prefix}, }; /// The path holding the client secret that agent containers on `hive` present /// to the swarm queue. /// /// # Errors /// [`Error::PathSegment`] when `hive` contains anything but `[A-Za-z0-9_-]`, /// which is what keeps one hive's name from addressing another hive's secret. pub fn agent_client_path(hive: &str) -> Result { let prefix = principal_prefix(Kind::Hive, hive)?; Ok(format!("{prefix}/queue/agent")) } /// What the path holds: the client secret, plus the client id it belongs to. /// /// The id rides with the secret for the same reason the homeserver rides with /// a matrix token — a credential has to be reconstructable from the store /// alone. Deriving it on the reading side instead would mean spelling /// `hive--agent` in a second place, and the authelia module's own option /// says what a split spelling costs: every agent is denied as a timeout. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct Credential { /// The secret itself. Named to match [`crate::matrix::Credential::value`] /// so a nix-side reader spells `bao kv get -field=value` for either kind. pub value: String, /// The OIDC client id the secret authenticates. /// /// Required, unlike [`crate::matrix::Credential::homeserver`]: that field /// is optional because it was added to objects already in the store, and /// this path is new. An object without an id is not a usable credential, /// so failing to decode is the wanted behaviour — the alternative is a /// reader holding `None` whose only recourse is to spell the id itself. pub client_id: String, } #[cfg(test)] mod tests { use super::*; #[test] fn a_hive_name_lands_under_its_own_principal_prefix() { assert_eq!( agent_client_path("alpha").expect("a plain name is legal"), "swarm/hives/alpha/queue/agent" ); } #[test] fn a_traversal_in_the_hive_name_is_refused() { let e = agent_client_path("../beta").expect_err("a traversal is not"); assert!(matches!(e, Error::PathSegment { kind: "hive", .. }), "{e}"); } #[test] fn two_hives_never_share_a_path() { assert_ne!( agent_client_path("alpha").expect("legal"), agent_client_path("beta").expect("legal") ); } #[test] fn the_object_round_trips_through_the_store_representation() { let c = Credential { value: "s3cr3t".to_owned(), client_id: "hive-alpha-agent".to_owned(), }; let json = serde_json::to_string(&c).expect("serialises"); assert_eq!( serde_json::from_str::(&json).expect("deserialises"), c ); } #[test] fn the_field_names_the_nix_reader_asks_for_are_the_ones_written() { let json = serde_json::to_value(Credential { value: "s3cr3t".to_owned(), client_id: "hive-alpha-agent".to_owned(), }) .expect("serialises"); assert_eq!(json["value"], "s3cr3t"); assert_eq!(json["client_id"], "hive-alpha-agent"); } }