Files
boxmaker/crates/gatewayd/tests/secrets.rs
T
kyle 608d426f95 gatewayd: secrets from a credential, the environment or a file; runbook entries
Implemented-By: OpenCode session (model recorded in docs/implementer-log.md)
2026-09-23 19:37:12 -07:00

201 lines
6.0 KiB
Rust

//! The secret store (M4a spec, section 4). The environment is passed in as a function, so no test
//! changes the process's environment. Do not edit.
#[path = "support/tmp.rs"]
mod tmp;
use std::collections::HashMap;
use std::ffi::OsString;
use std::os::unix::fs::PermissionsExt;
use std::path::PathBuf;
use gatewayd::config::SecretSource;
use gatewayd::secrets::{RUNBOOK, RUNBOOK_FILE, load};
use tmp::TempDir;
const TOKEN: &str = "s3cret-t0ken-value";
fn env_of(pairs: &[(&str, &str)]) -> impl Fn(&str) -> Option<OsString> + use<> {
let map: HashMap<String, OsString> = pairs
.iter()
.map(|(k, v)| (k.to_string(), OsString::from(v)))
.collect();
move |k| map.get(k).cloned()
}
fn owner_file(dir: &TempDir, name: &str, text: &str, mode: u32) -> PathBuf {
let path = dir.write(name, text);
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(mode)).unwrap();
path
}
fn refused(source: &SecretSource, env: &dyn Fn(&str) -> Option<OsString>, word: &str) {
let e = load("mattermost_token", source, env).expect_err(word);
let text = e.to_string();
assert!(text.contains(word), "{word}: {text}");
assert!(text.starts_with("secret mattermost_token: "), "{text}");
assert!(text.ends_with(RUNBOOK), "{text}");
assert!(
!text.contains(TOKEN),
"a refusal never shows the value: {text}"
);
}
#[test]
fn a_systemd_credential() {
let dir = TempDir::new("cred");
dir.write("creds/mattermost-token", &format!("{TOKEN}\n"));
let creds = dir.path().join("creds");
let env = env_of(&[("CREDENTIALS_DIRECTORY", creds.to_str().unwrap())]);
let got = load(
"mattermost_token",
&SecretSource::Credential("mattermost-token".into()),
&env,
)
.unwrap();
assert_eq!(
got.secret.expose(),
TOKEN,
"one trailing newline is removed"
);
assert_eq!(got.warning, None);
}
#[test]
fn a_credential_outside_systemd_or_missing_is_refused() {
let src = SecretSource::Credential("mattermost-token".into());
refused(&src, &env_of(&[]), "CREDENTIALS_DIRECTORY is not set");
let dir = TempDir::new("cred-missing");
refused(
&src,
&env_of(&[("CREDENTIALS_DIRECTORY", dir.path().to_str().unwrap())]),
"cannot read the credential",
);
}
#[test]
fn an_environment_variable() {
let got = load(
"mattermost_token",
&SecretSource::Env("MM".into()),
&env_of(&[("MM", TOKEN)]),
)
.unwrap();
assert_eq!(got.secret.expose(), TOKEN);
assert_eq!(got.warning, None, "only a file warns");
refused(&SecretSource::Env("MM".into()), &env_of(&[]), "is not set");
refused(
&SecretSource::Env("MM".into()),
&env_of(&[("MM", "")]),
"empty",
);
}
#[test]
fn an_owner_only_file_is_read_with_a_warning() {
let dir = TempDir::new("file");
for mode in [0o600, 0o400] {
let path = owner_file(
&dir,
&format!("token-{mode:o}"),
&format!("{TOKEN}\n"),
mode,
);
let got = load(
"mattermost_token",
&SecretSource::File(path.clone()),
&env_of(&[]),
)
.unwrap();
assert_eq!(got.secret.expose(), TOKEN);
let warning = got.warning.expect("a file secret warns");
assert!(
warning.starts_with(
"gatewayd: warning: secret mattermost_token is read in plaintext from "
),
"{warning}"
);
assert!(warning.contains(path.to_str().unwrap()), "{warning}");
assert!(warning.contains(RUNBOOK_FILE), "{warning}");
assert!(!warning.contains(TOKEN));
}
}
#[test]
fn a_file_anyone_else_can_read_or_that_is_not_a_plain_file_is_refused() {
let dir = TempDir::new("file-bad");
for (mode, _) in [
(0o640, "group"),
(0o604, "other"),
(0o644, "both"),
(0o660, "group write"),
] {
let path = owner_file(&dir, &format!("t-{mode:o}"), TOKEN, mode);
refused(
&SecretSource::File(path),
&env_of(&[]),
"only the owner may read it",
);
}
let target = owner_file(&dir, "real", TOKEN, 0o600);
let link = dir.path().join("link");
std::os::unix::fs::symlink(&target, &link).unwrap();
refused(&SecretSource::File(link), &env_of(&[]), "symbolic link");
std::fs::create_dir(dir.path().join("adir")).unwrap();
refused(
&SecretSource::File(dir.path().join("adir")),
&env_of(&[]),
"not a regular file",
);
refused(
&SecretSource::File(dir.path().join("missing")),
&env_of(&[]),
"cannot read",
);
refused(
&SecretSource::File(PathBuf::from("relative/token")),
&env_of(&[]),
"absolute",
);
}
#[test]
fn empty_and_non_utf8_values_are_refused() {
let dir = TempDir::new("value");
refused(
&SecretSource::File(owner_file(&dir, "empty", "", 0o600)),
&env_of(&[]),
"empty",
);
refused(
&SecretSource::File(owner_file(&dir, "nl", "\n", 0o600)),
&env_of(&[]),
"empty",
);
let path = dir.path().join("bin");
std::fs::write(&path, [0xff, 0xfe]).unwrap();
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600)).unwrap();
refused(&SecretSource::File(path), &env_of(&[]), "not UTF-8");
}
#[test]
fn only_one_trailing_newline_is_removed_and_spaces_stay() {
let dir = TempDir::new("trim");
let path = owner_file(&dir, "t", " a b \n\n", 0o600);
let got = load("x", &SecretSource::File(path), &env_of(&[])).unwrap();
assert_eq!(got.secret.expose(), " a b \n");
}
#[test]
fn a_secret_prints_nothing_of_itself() {
let got = load(
"mattermost_token",
&SecretSource::Env("MM".into()),
&env_of(&[("MM", TOKEN)]),
)
.unwrap();
let shown = format!("{:?} {:?}", got.secret, got);
assert!(!shown.contains(TOKEN), "{shown}");
assert!(shown.contains("Secret(…)"), "{shown}");
}