#![cfg(target_os = "macos")]
use std::io::Read as _;
use std::process::{Command, Stdio};
use std::time::{Duration, Instant};
use runner_manager_platform::secrets::{
PlatformSecretStore, ROOTED_KEYCHAIN_PASSWORD, SecretScope, SecretStore as _,
};
use secrecy::SecretString;
const SERVICE: &str = "io.github.IvanMurzak.runner-manager";
const ACCOUNT: &str = "user-access-token";
const DEADLINE: Duration = Duration::from_secs(30);
fn fixture_token() -> String {
format!("{}{}", "ghu_", "d2GrantFixtureNotARealOne0000000")
}
#[test]
fn a_program_that_did_not_write_the_item_can_still_read_it() {
let root = tempfile::tempdir().expect("a temporary directory");
let store = PlatformSecretStore::rooted_at(SecretScope::Machine, root.path())
.expect("a rooted machine-scoped store resolves");
let token = fixture_token();
store
.store(&SecretString::from(token.clone()))
.expect("the token is stored");
let keychain = store.guard();
let unlock = run_with_deadline(
Command::new("/usr/bin/security")
.arg("unlock-keychain")
.arg("-p")
.arg(ROOTED_KEYCHAIN_PASSWORD)
.arg(&keychain),
);
assert!(
unlock.timed_out.not_reached(),
"`security unlock-keychain` did not finish within {DEADLINE:?}"
);
assert!(
unlock.status_success,
"`security unlock-keychain` failed: {}",
unlock.stderr
);
let read = run_with_deadline(
Command::new("/usr/bin/security")
.arg("find-generic-password")
.arg("-w")
.arg("-s")
.arg(SERVICE)
.arg("-a")
.arg(ACCOUNT)
.arg(&keychain),
);
assert!(
read.timed_out.not_reached(),
"`security find-generic-password` did not finish within {DEADLINE:?}. That is what a \
per-application grant looks like from the outside: the keychain is waiting for somebody \
to approve a read by a program the item does not name, which on a daemon's host is \
nobody. See `grants_every_application` in secrets.rs."
);
assert!(
read.status_success,
"another program was refused the stored token: {}",
read.stderr
);
assert_eq!(
read.stdout.trim_end_matches('\n'),
token,
"the value another program read back is not the one that was stored"
);
}
struct Deadline(bool);
impl Deadline {
fn not_reached(&self) -> bool {
!self.0
}
}
struct Finished {
timed_out: Deadline,
status_success: bool,
stdout: String,
stderr: String,
}
fn run_with_deadline(command: &mut Command) -> Finished {
let mut child = command
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("security(1) is present on every macOS host");
let started = Instant::now();
let status = loop {
match child.try_wait().expect("the child can be waited on") {
Some(status) => break Some(status),
None if started.elapsed() >= DEADLINE => {
let _ = child.kill();
let _ = child.wait();
break None;
}
None => std::thread::sleep(Duration::from_millis(50)),
}
};
let mut stdout = String::new();
let mut stderr = String::new();
if let Some(mut pipe) = child.stdout.take() {
let _ = pipe.read_to_string(&mut stdout);
}
if let Some(mut pipe) = child.stderr.take() {
let _ = pipe.read_to_string(&mut stderr);
}
Finished {
timed_out: Deadline(status.is_none()),
status_success: status.is_some_and(|status| status.success()),
stdout,
stderr,
}
}