crates/gunnar-server/tests/heimdall_push.rs
//! **A pushed pack goes through the Heimdall gate before the ingest**
//! (Heimdall milestone 5, step c; feature `heimdall`).
//!
//! The push is driven through the real `WireTransport` (the `GitTransport`
//! seam `gunnar-ssh` hands an authenticated session to): advertisement,
//! command list, pack, report-status. The gate's runner plays Heimdall
//! in-process: it runs the real checker on the spooled pack and signs, or
//! refuses every pack.
#![cfg(feature = "heimdall")]
use std::sync::{Arc, Mutex};
use gunnar_core::{Level, NewRepo, Principal, PrincipalKind, RepoRef};
use gunnar_heimdall::{checker, PackGate, CHECKER_PATH, PACK, REPORT};
use gunnar_server::stores::ServerStores;
use gunnar_server::transport::WireTransport;
use gunnar_ssh::seam::{Delivery, GitTransport, ServiceRequest};
use gunnar_ssh::GitService;
use gunnar_store::conformance::pack::PackBuilder;
use gunnar_store::git_storage_trait::GitOps;
use heimdall_core::*;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
struct Fake {
key: BootKey,
refuse: bool,
jobs: Mutex<u32>,
}
fn features() -> Features {
Features {
runner: "fake".into(),
container: true,
seccomp: Protection::Default,
landlock: true,
broker: false,
wasm: false,
image_verified_each_start: true,
}
}
impl JobRunner for Fake {
fn run(&self, job: Job) -> Result<SignedResult, RunError> {
*self.jobs.lock().unwrap() += 1;
let JobKind::Native { argv, .. } = &job.kind else {
panic!("a native job")
};
assert_eq!(argv[0], CHECKER_PATH);
let mut outputs = vec![];
let verdict = if self.refuse {
Verdict::Refused {
code: "heimdall-exit-1".into(),
reason: "the checker said no".into(),
}
} else {
let kind = checker::parse_kind(&argv[1]).unwrap();
let r = checker::check_pack_file(&job.input_dir.join(PACK), kind).unwrap();
let b = serde_json::to_vec(&r).unwrap();
std::fs::write(job.output_dir.join(REPORT), &b).unwrap();
outputs.push(Output::of(REPORT, &b));
Verdict::Accepted
};
let result = JobResult {
job_id: job.id,
nonce: job.nonce,
verdict,
outputs,
exit: ExitInfo::exited(0),
started_unix: 1,
finished_unix: 2,
runner: features(),
events: vec![],
kernel: None,
};
Ok(self.key.sign(result).unwrap())
}
fn verifying_key(&self) -> VerifyingKey {
self.key.verifying_key()
}
fn features(&self) -> Features {
features()
}
}
struct Rig {
_dir: tempfile::TempDir,
stores: Arc<ServerStores>,
path: RepoRef,
fake: Arc<Fake>,
}
fn rig(refuse: bool) -> Rig {
let dir = tempfile::tempdir().unwrap();
let stores = Arc::new(ServerStores::open(dir.path()).unwrap());
stores.catalog().create_namespace("team").unwrap();
let path = RepoRef::new("team", "code").unwrap();
stores
.create_repo(NewRepo::new(path.clone()), false)
.unwrap();
let fake = Arc::new(Fake {
key: BootKey::generate(),
refuse,
jobs: Mutex::new(0),
});
let image = ImageRef {
name: "gunnar/pack-check".into(),
digest: format!("sha256:{}", "ab".repeat(32)),
};
let gate = PackGate::new(
fake.clone(),
image,
dir.path().join("heimdall/spool"),
gix_hash::Kind::Sha1,
);
assert!(stores.set_pack_gate(Arc::new(gate)));
Rig {
_dir: dir,
stores,
path,
fake,
}
}
/// A one-commit pack: README blob, tree, commit.
fn commit_pack(kind: gix_hash::Kind) -> (Vec<u8>, gix_hash::ObjectId) {
let blob = b"pushed through the gate\n".to_vec();
let blob_id = gix_object::compute_hash(kind, gix_object::Kind::Blob, &blob).unwrap();
let mut tree = b"100644 README\0".to_vec();
tree.extend_from_slice(blob_id.as_slice());
let tree_id = gix_object::compute_hash(kind, gix_object::Kind::Tree, &tree).unwrap();
let commit = format!(
"tree {tree_id}\nauthor A <a@example.invalid> 1700000000 +0000\ncommitter A \
<a@example.invalid> 1700000000 +0000\n\nfirst\n"
)
.into_bytes();
let commit_id = gix_object::compute_hash(kind, gix_object::Kind::Commit, &commit).unwrap();
let mut pb = PackBuilder::new(kind);
pb.object(gix_object::Kind::Commit, commit);
pb.object(gix_object::Kind::Tree, tree);
pb.object(gix_object::Kind::Blob, blob);
(pb.build().unwrap().bytes, commit_id)
}
impl Rig {
fn kind(&self) -> gix_hash::Kind {
let row = self.stores.catalog().get(&self.path).unwrap().unwrap();
self.stores.repo_store(&row).unwrap().hash_kind()
}
fn tip(&self) -> Option<Vec<u8>> {
let row = self.stores.catalog().get(&self.path).unwrap().unwrap();
let store = self.stores.repo_store(&row).unwrap();
GitOps::refs(store.serve())
.unwrap()
.into_iter()
.find(|r| r.name == "refs/heads/main")
.and_then(|r| r.oid)
}
/// Push `pack` creating refs/heads/main at `tip`; the server's answer.
fn push(&self, pack: &[u8], tip: gix_hash::ObjectId) -> String {
let kind = self.kind();
let format = if kind == gix_hash::Kind::Sha1 {
"sha1"
} else {
"sha256"
};
let line = format!(
"{} {tip} refs/heads/main\0report-status object-format={format}\n",
kind.null()
);
let mut input = format!("{:04x}", line.len() + 4).into_bytes();
input.extend_from_slice(line.as_bytes());
input.extend_from_slice(b"0000");
input.extend_from_slice(pack);
let row = self.stores.catalog().get(&self.path).unwrap().unwrap();
let request = ServiceRequest {
service: GitService::ReceivePack,
repo: row,
prospective: false,
principal: Principal::new("alice", "alice", PrincipalKind::User).unwrap(),
access: Level::Admin,
key_fingerprint: None,
git_protocol: None,
delivery: Delivery::Session,
credential: None,
plan_notice: None,
pack_ceiling: None,
pack_mark: None,
lfs_allowance: None,
cancel: gunnar_ssh::channel::Cancel::new(),
};
let transport = WireTransport::new(self.stores.clone());
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let (mut client, server) = tokio::io::duplex(1 << 20);
let serve = transport.serve(request, Box::new(server));
let talk = async {
client.write_all(&input).await.unwrap();
client.shutdown().await.unwrap();
let mut out = Vec::new();
let _ = client.read_to_end(&mut out).await;
out
};
let (served, out) = tokio::join!(serve, talk);
format!("{served:?} {}", String::from_utf8_lossy(&out))
})
}
}
/// ★ A push whose pack Heimdall refuses lands nothing: the reference does not
/// move and the client hears the push was refused.
#[test]
fn a_push_whose_pack_heimdall_refuses_lands_nothing() {
let r = rig(true);
let (pack, tip) = commit_pack(r.kind());
let said = r.push(&pack, tip);
assert_eq!(r.tip(), None, "{said}");
// The client hears that the pack was refused; the wire never tells a
// pusher the server-side reason (gunnar-wire's unpack status).
assert!(said.contains("ng refs/heads/main"), "{said}");
assert!(!said.contains("unpack ok"), "{said}");
assert_eq!(*r.fake.jobs.lock().unwrap(), 1);
}
/// The same push through a gate whose check passes lands exactly.
#[test]
fn a_push_through_a_passing_gate_lands() {
let r = rig(false);
let (pack, tip) = commit_pack(r.kind());
let said = r.push(&pack, tip);
assert_eq!(r.tip().as_deref(), Some(tip.as_slice()), "{said}");
assert_eq!(*r.fake.jobs.lock().unwrap(), 1, "{said}");
}