//! 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 + use<> { let map: HashMap = 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, 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}"); }