//! The keys of `memoryd`'s Honcho clients (M5c spec, section 10): each read with //! `bxcore::secrets`, sent as a bearer token to its own workspace only, and never shown by a //! client, an error or a warning; a key that cannot be read is an error naming it. Against a fake //! Honcho. Do not edit. #[path = "support/honcho.rs"] mod honcho; #[path = "support/tmp.rs"] mod tmp; use std::collections::BTreeMap; use std::ffi::OsString; use std::os::unix::fs::PermissionsExt; use std::process::{Command, Stdio}; use std::time::{Duration, Instant}; use bxcore::secrets::SecretSource; use honcho::{FakeHoncho, Mode}; use memoryd::honcho::client::{Clients, Timeouts}; use memoryd::honcho::config::{FLEET_KEY, HonchoConfig, TRUSTED_KEY}; use memoryd::honcho::ledger::Workspace; use tmp::TempDir; const FAST: Timeouts = Timeouts { connect: Duration::from_secs(5), read: Duration::from_millis(300), }; fn config(url: &str) -> HonchoConfig { HonchoConfig { url: url.to_string(), agent: "boxmaker0".to_string(), owner: "kyle".to_string(), trusted: "boxmaker0-trusted".to_string(), untrusted: "boxmaker0-untrusted".to_string(), fleet: "fleet".to_string(), read: Vec::new(), keys: BTreeMap::new(), } } fn no_env(_: &str) -> Option { None } /// Key values built from pieces, so that no file holds one whole. fn env_key() -> String { ["hk", "trusted", "5e1f0a"].join("-") } fn file_key() -> String { ["hk", "fleet", "c27d94"].join("-") } #[test] fn each_key_goes_to_its_own_workspace_and_is_never_shown() { let dir = TempDir::new("honcho-keys"); let file = dir.write("fleet.key", &format!("{}\n", file_key())); std::fs::set_permissions(&file, std::fs::Permissions::from_mode(0o600)).unwrap(); let fake = FakeHoncho::start(); let mut config = config(&fake.url); config.keys.insert( TRUSTED_KEY.to_string(), SecretSource::Env("HONCHO_TRUSTED".to_string()), ); config .keys .insert(FLEET_KEY.to_string(), SecretSource::File(file.clone())); let env = |name: &str| (name == "HONCHO_TRUSTED").then(|| OsString::from(env_key())); let (mut c, warnings) = Clients::new(&config, &env, FAST).unwrap(); assert_eq!(warnings.len(), 1, "{warnings:?}"); assert!(warnings[0].contains("honcho_fleet"), "{}", warnings[0]); c.ensure_workspace(Workspace::Trusted).unwrap(); c.ensure_workspace(Workspace::Untrusted).unwrap(); c.ensure_workspace(Workspace::Fleet).unwrap(); let auth: Vec> = fake .requests() .iter() .map(|r| r.header("authorization").map(str::to_string)) .collect(); assert_eq!( auth, vec![ Some(format!("Bearer {}", env_key())), None, Some(format!("Bearer {}", file_key())), ] ); // Nothing shows a key: the clients, their errors, the warnings. fake.set(Mode::Status(401)); let err = c.ensure_peer(Workspace::Trusted, "kyle").unwrap_err(); let shown = [ format!("{c:?}"), format!("{err:?}"), err.to_string(), warnings.join("\n"), ]; for text in shown { assert!(!text.contains(&env_key()), "{text}"); assert!(!text.contains(&file_key()), "{text}"); } } #[test] fn a_key_that_cannot_be_read_is_an_error_naming_it() { let fake = FakeHoncho::start(); let mut config = config(&fake.url); config.keys.insert( TRUSTED_KEY.to_string(), SecretSource::Env("HONCHO_TRUSTED".to_string()), ); let err = Clients::new(&config, &no_env, FAST).unwrap_err(); assert!(err.contains("honcho_trusted"), "{err}"); assert!( err.contains("see docs/runbook.md#secret-unavailable"), "{err}" ); } /// Set in the process `a_roncho_toml_in_home_is_not_read` starts, and only there. const CHILD: &str = "MEMORYD_TEST_RONCHO_HOME"; #[test] fn a_roncho_toml_in_home_is_not_read() { // `roncho` reads `$HOME/.config/roncho/roncho.toml` unless told not to, and takes its // `api_key`. HOME is the process's, so the check runs in a process of its own: this test // binary, running only the test below. let dir = TempDir::new("honcho-home"); let key = ["hk", "from", "roncho", "toml"].join("-"); dir.write( ".config/roncho/roncho.toml", &format!("api_key = \"{key}\"\n"), ); let mut child = Command::new(std::env::current_exe().unwrap()) .args(["--exact", "in_a_home_with_a_roncho_toml", "--ignored"]) .env("HOME", dir.path()) .env_remove("XDG_CONFIG_HOME") .env(CHILD, "1") .stdin(Stdio::null()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .spawn() .unwrap(); let deadline = Instant::now() + Duration::from_secs(20); let mut status = None; while Instant::now() < deadline { if let Ok(Some(done)) = child.try_wait() { status = Some(done); break; } std::thread::sleep(Duration::from_millis(20)); } let _ = child.kill(); let output = child.wait_with_output().unwrap(); let stdout = String::from_utf8_lossy(&output.stdout); assert!(status.is_some_and(|s| s.success()), "{stdout}"); assert!(stdout.contains("1 passed"), "{stdout}"); } #[test] #[ignore = "run by a_roncho_toml_in_home_is_not_read, in a process of its own"] fn in_a_home_with_a_roncho_toml() { if std::env::var_os(CHILD).is_none() { return; } let fake = FakeHoncho::start(); let (mut c, _) = Clients::new(&config(&fake.url), &no_env, FAST).unwrap(); c.ensure_workspace(Workspace::Trusted).unwrap(); let requests = fake.requests(); assert_eq!(requests.len(), 1); assert_eq!(requests[0].header("authorization"), None); }