hyperhive/hive-forge/tests/credential_helper.rs
atlas e0673b6192 tests: cover the three security boundaries that had none (#3950)
- hive-c0re::webhook_secret::verify_signature — the HMAC comparison
  verify_hmac (the only gate on the public webhook endpoint) delegates
  to. Correct-signature and mismatched-signature (wrong secret, tampered
  body) cases.
- hive-forge credential-helper get (host= check) — subprocess
  integration tests since the check is inlined in run(), which reads
  real stdin/env and prints real stdout. Host mismatch (error, token
  withheld), host match (credentials printed), and no host= line
  (backwards compat) cases.
- hive-priv::{validate_credential_name, validate_snapshot_name,
  ensure_plain_filename} — the only gate on the root-privileged socket.
  Empty/charset/dot/slash rejection, hive- prefix requirement, and
  ./../slash rejection respectively.
2026-09-23 14:37:54 +02:00

113 lines
4.4 KiB
Rust

//! Integration coverage for `hive-forge credential-helper get` — the
//! `host=` check in `verbs::credential_helper::run` exists to close a
//! reported credential leak, and git invokes the helper fresh over
//! stdin/env on every fetch/push. The check is inlined in `run()`
//! (real stdin, real env, real stdout), so it can only be driven the way
//! git actually drives it: run the compiled binary.
use std::io::Write as _;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
const TOKEN: &str = "test-token-value";
/// A unique scratch `HYPERHIVE_STATE_DIR`, seeded with a `forge-token`
/// file. Matches the manual-tmpdir pattern `hive-c0re`'s
/// `dashboard::state_files` tests use — this crate carries no `tempfile`
/// dependency to reach for instead.
fn scratch_state_dir(tag: &str) -> PathBuf {
let ts = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or(0, |d| d.as_nanos());
let dir = std::env::temp_dir().join(format!("hive-forge-cred-test-{tag}-{ts}"));
std::fs::create_dir_all(&dir).expect("create scratch state dir");
std::fs::write(dir.join("forge-token"), format!("{TOKEN}\n")).expect("seed forge-token");
dir
}
/// Run `hive-forge credential-helper get` against `base_url` +
/// `state_dir`, feeding `request` as the git credential-protocol
/// request body on stdin. Returns `(exit success, stdout, stderr)`.
fn run_get(base_url: &str, state_dir: &Path, request: &str) -> (bool, String, String) {
let mut child = Command::new(env!("CARGO_BIN_EXE_hive-forge"))
.arg("credential-helper")
.arg("get")
.env("HIVE_FORGE_URL", base_url)
.env("HYPERHIVE_STATE_DIR", state_dir)
.env_remove("HIVE_LABEL")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("spawn hive-forge credential-helper get");
child
.stdin
.take()
.expect("piped stdin")
.write_all(request.as_bytes())
.expect("write credential request to stdin");
let out = child.wait_with_output().expect("wait for hive-forge");
(
out.status.success(),
String::from_utf8_lossy(&out.stdout).into_owned(),
String::from_utf8_lossy(&out.stderr).into_owned(),
)
}
/// This is the check the module doc says exists to close a reported
/// credential leak: refusing to hand the forge token to a host git
/// didn't ask this invocation to serve. If the host comparison were ever
/// inverted, a mismatched host would get the token instead of being
/// refused.
#[test]
fn credential_helper_get_refuses_a_mismatched_host_and_withholds_the_token() {
let dir = scratch_state_dir("mismatch");
let (ok, stdout, stderr) = run_get(
"http://forge.internal.example",
&dir,
"protocol=https\nhost=not-the-forge.example\n",
);
assert!(!ok, "a mismatched host= must fail, stderr={stderr}");
assert!(
!stdout.contains(TOKEN),
"the token must never reach stdout on a host mismatch: stdout={stdout}"
);
let _ = std::fs::remove_dir_all(&dir);
}
/// The other side of the same check: a `host=` that matches the
/// configured forge must still print the credentials, or every real git
/// fetch/push would break.
#[test]
fn credential_helper_get_answers_when_host_matches() {
let dir = scratch_state_dir("match");
let (ok, stdout, stderr) = run_get(
"http://forge.internal.example",
&dir,
"protocol=https\nhost=forge.internal.example\n",
);
assert!(ok, "a matching host= must succeed, stderr={stderr}");
assert!(stdout.contains("username="), "stdout={stdout}");
assert!(
stdout.contains(&format!("password={TOKEN}")),
"stdout={stdout}"
);
let _ = std::fs::remove_dir_all(&dir);
}
/// Backwards compatibility: the request shape git sent before this check
/// existed — no `host=` line at all — must still work.
#[test]
fn credential_helper_get_still_works_with_no_host_line() {
let dir = scratch_state_dir("nohost");
let (ok, stdout, stderr) = run_get("http://forge.internal.example", &dir, "protocol=https\n");
assert!(
ok,
"a request with no host= line must not be treated as a failure, stderr={stderr}"
);
assert!(
stdout.contains(&format!("password={TOKEN}")),
"stdout={stdout}"
);
let _ = std::fs::remove_dir_all(&dir);
}