172 lines
5.7 KiB
Rust
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);
|
|
}
|