Files
boxmaker/crates/memoryd/tests/honcho_keys.rs
T

172 lines
5.7 KiB
Rust

//! 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<OsString> {
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<Option<String>> = 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);
}