//! The credential this hive reads the forge with: its own read-only account, //! whose token `swarm-controller` mints into the store at //! `swarm/hives//forge/token`, and the core token file where the store //! holds none. //! //! The file is the fallback so a hive deployed before the controller mints its //! token keeps the reads it had: the forge host holds the file, no other hive //! does. use super::{basic_auth_header, core_token}; /// The basic-auth user [`ReaderToken::auth_header`] names for the core token. const CORE_LOGIN: &str = "core"; /// A forge token and the account it belongs to. /// /// No `Debug` derive: `value` is a live forge credential. pub struct ReaderToken { login: String, value: String, } impl std::fmt::Debug for ReaderToken { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("ReaderToken") .field("login", &self.login) .field("value", &"") .finish() } } impl ReaderToken { /// The token itself, for an API client. pub fn value(&self) -> &str { &self.value } /// The `http.extraHeader` value for git; see [`basic_auth_header`]. pub fn auth_header(&self) -> String { basic_auth_header(&self.login, &self.value) } } /// The token to read the forge with, or `None` when this hive has neither. /// /// 🩸 Logs the store path and which source won, never a value. pub async fn reader_token() -> Option { pick(stored_reader_token().await, core_token()) } /// The store's token wins over the core file's. fn pick(stored: Option, core: Option) -> Option { let picked = stored.or_else(|| { core.map(|value| ReaderToken { login: CORE_LOGIN.to_owned(), value, }) }); if let Some(t) = &picked { tracing::debug!(login = %t.login, "forge: reading as this account"); } else { tracing::debug!("forge: no read token in the store or the core token file"); } picked } /// This hive's token from the store, under the hive's own identity. /// /// `None`, never an error, for every way it can come up empty — no hive name, /// no `BAO_*` identity, an unreachable store, nothing at the path — the same /// as `matrix::stored_sender_token`, and for the same reason: each sends the /// caller to the fallback. async fn stored_reader_token() -> Option { let hive = std::env::var("HYPERHIVE_HIVE_NAME") .ok() .filter(|h| !h.is_empty())?; let path = match swarm_secret_client::forge::hive_token_path(&hive) { Ok(path) => path, Err(e) => { tracing::warn!(error = %e, "forge: this hive's name does not form a store path"); return None; } }; let store = match swarm_secret_client::SecretStore::from_env(&hive).await { Ok(store) => store, Err(e) => { tracing::debug!(error = %e, "forge: no swarm secret store to read the hive token from"); return None; } }; match store .read::(&path) .await { Ok(credential) if !credential.value.trim().is_empty() => Some(ReaderToken { login: swarm_secret_client::forge::hive_login(&hive), value: credential.value.trim().to_owned(), }), Ok(_) => { tracing::warn!(%path, "forge: the stored hive token is empty"); None } Err(e) => { tracing::debug!(%path, error = %e, "forge: no hive token in the store"); None } } } #[cfg(test)] mod tests { use super::*; fn stored() -> ReaderToken { ReaderToken { login: "hive-alpha".to_owned(), value: "from-the-store".to_owned(), } } #[test] fn the_store_wins_over_the_core_file() { let t = pick(Some(stored()), Some("from-the-file".to_owned())).expect("a token"); assert_eq!(t.value(), "from-the-store"); assert_eq!(t.login, "hive-alpha"); } #[test] fn the_core_file_is_the_fallback() { let t = pick(None, Some("from-the-file".to_owned())).expect("a token"); assert_eq!(t.value(), "from-the-file"); assert_eq!(t.login, CORE_LOGIN); } #[test] fn neither_is_none() { assert!(pick(None, None).is_none()); } #[test] fn the_header_names_the_tokens_own_account() { use base64::Engine as _; let header = stored().auth_header(); let encoded = header .strip_prefix("Authorization: Basic ") .expect("a basic auth header"); let decoded = base64::engine::general_purpose::STANDARD .decode(encoded) .expect("base64"); assert_eq!(decoded, b"hive-alpha:from-the-store"); } #[test] fn debug_never_prints_the_value() { let shown = format!("{:?}", stored()); assert!(!shown.contains("from-the-store"), "{shown}"); assert!(shown.contains("hive-alpha"), "{shown}"); } }