crates/gunnar-grpc/src/attest.rs
//! Push attestation, rendered as one JSON line.
//!
//! At receive-pack time gunnar already knows who pushed, with which key, into
//! which repo, which refs moved and which objects are new. That is a
//! provenance record for free, and gunnar does exactly two things with it:
//!
//! 1. emits a `PushAttestation` on the event stream ([`crate::bus`]), and
//! 2. appends **one JSON line** (this function) to a log in its own store.
//!
//! It does **not** link the Build Thing, nornir, skade, arrow or parquet.
//! Anything that wants a warehouse row subscribes to the event and writes the
//! row itself. The coupling is a proto message, not a dependency edge. That is
//! the only shape that survives the appliance case, where none of those crates
//! could be linked at all.
use gunnar_proto::PushAttestation;
/// Render an attestation as a single JSONL record: no trailing newline, and no
/// embedded one either, so a caller can append it with exactly one `\n` and a
/// reader can split on `\n` without a parser.
pub fn attestation_json_line(attestation: &PushAttestation) -> Result<String, serde_json::Error> {
serde_json::to_string(attestation)
}
#[cfg(test)]
mod tests {
use super::*;
use gunnar_proto::{HashKind, RefUpdate, RepoPath};
fn sample() -> PushAttestation {
PushAttestation {
attestation_id: "att-1".into(),
repo: Some(RepoPath::new("nordisk", "gunnar")),
principal_id: "principal-00000001".into(),
principal_name: "alice".into(),
key_fingerprint: "SHA256:abc".into(),
credential: "key:SHA256:abc".into(),
hash_kind: HashKind::Sha256 as i32,
ref_updates: vec![RefUpdate {
ref_name: "refs/heads/main".into(),
old_oid: "a".repeat(64),
new_oid: "b".repeat(64),
}],
new_object_oids: vec!["c".repeat(64)],
new_object_count: 1,
// Empty: this sample's object set is known.
new_object_oids_unknown: String::new(),
new_object_bytes: 4096,
pack_hash: "d".repeat(64),
received_at_unix_ms: 1_700_000_000_000,
server_agent: gunnar_proto::AGENT.into(),
transport: "ssh".into(),
}
}
#[test]
fn renders_exactly_one_line() {
let line = attestation_json_line(&sample()).expect("serialises");
assert!(
!line.contains('\n'),
"a JSONL record must not contain a newline: {line}"
);
assert_eq!(line.lines().count(), 1);
}
#[test]
fn carries_the_facts_receive_pack_already_had() {
let line = attestation_json_line(&sample()).expect("serialises");
// Assert the APPLIED output (the actual bytes a subscriber would
// read), not that serialisation returned Ok.
for expected in [
"\"namespace\":\"nordisk\"",
"\"repo\":\"gunnar\"",
"\"principal_name\":\"alice\"",
"\"key_fingerprint\":\"SHA256:abc\"",
"\"ref_name\":\"refs/heads/main\"",
"\"new_object_count\":1",
"\"transport\":\"ssh\"",
] {
assert!(line.contains(expected), "missing {expected} in {line}");
}
}
/// **An empty object list carries the reason it is empty, on the durable
/// line too.**
///
/// The JSONL record is the copy that survives, and it is read by things
/// that have no gunnar to ask. A record whose `new_object_oids` is `[]`
/// with nothing beside it says "this push introduced no objects", which is
/// a different fact from "nobody worked out which objects these were" —
/// and on the engine the appliance ships the second is the ordinary case.
#[test]
fn an_unenumerated_record_says_so_on_the_line_rather_than_showing_an_empty_list() {
let mut att = sample();
att.new_object_oids.clear();
att.new_object_oids_unknown =
"the policy has no gate over objects, so the object set was not enumerated".to_owned();
att.new_object_count = 3;
let line = attestation_json_line(&att).expect("serialises");
assert!(
line.contains("\"new_object_oids\":[]")
&& line.contains("\"new_object_count\":3")
&& line.contains("was not enumerated"),
"the line must carry the empty list, the count that is still true and the reason \
the two disagree: {line}"
);
// The other direction, or the assertion above would pass on a writer
// that always emitted the sentence.
let enumerated = attestation_json_line(&sample()).expect("serialises");
assert!(
enumerated.contains("\"new_object_oids_unknown\":\"\""),
"an enumerated set must leave the reason EMPTY, or every reader is told the list \
is untrustworthy: {enumerated}"
);
}
}