Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/holger

server/lib/tests/a_promotion_gate_refuses_over_grpc.rs

//! ★ **The promotion gate refuses over the real gRPC door** (lane OX-WIRED2): a running holger
//! with a two-rung ladder (`mirror` → `release`, the `osv` gate over an advisory feed in the
//! config) is asked `PromotionService.PromoteArtifact` by a real tonic client:
//!
//! * the vulnerable `serde@1.0.0` is refused by the gate with the frozen `holger-promote-2`,
//!   and the release repository serves nothing of it;
//! * the clean `serde@1.0.1` is promoted and sealed into the PROD store (its ref names the
//!   content address, the blob is the bytes);
//! * a Reader is refused the promotion.
//!
//! Before, the gate's refusal was driven only by the local `holger-server promote` verb
//! (server/cli tests/promote_cli.rs) and the gRPC door only for a missing source
//! (grpc_error_codes_on_the_wire.rs).

use std::io::Write;

use holger_server_lib::grpc::holger_proto::promotion_service_client::PromotionServiceClient;
use holger_server_lib::grpc::holger_proto::repository_service_client::RepositoryServiceClient;
use holger_server_lib::grpc::holger_proto::{
    ArtifactId as ProtoId, FetchArtifactRequest, PromoteArtifactRequest,
};
use holger_server_lib::{read_ron_config, wire_holger, ArtifactId, Holger};
use traits::test_support::hold_port;

const ADMIN: &str = "rel-admin-token-of-sufficient-length-for-holger";
const READER: &str = "rita-token-of-sufficient-length-for-holger";

const OSV_FEED: &str = r#"{
  "id": "OSV-TEST-0001",
  "summary": "test advisory that must block the promotion",
  "affected": [ { "package": { "ecosystem": "crates.io", "name": "serde" }, "versions": ["1.0.0"] } ],
  "severity": [ { "type": "CVSS_V3", "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H" } ]
}"#;

struct Running {
    _data: tempfile::TempDir,
    _cfg: tempfile::NamedTempFile,
    _holger: Holger,
    grpc: String,
}

fn ron(d: &str, grpc: u16, http: u16) -> String {
    let repo = |name: &str| {
        format!(
            r#"(ron_name: "{name}", ron_repo_type: "rust", ron_data_dir: Some("{d}/{name}"), ron_base_url: Some("http://x"), ron_upstreams: [], ron_in: None,
         ron_out: Some((ron_storage_endpoint: "storage", ron_exposed_endpoint: "door"))),"#
        )
    };
    format!(
        r#"(
    repositories: [ {mirror} {release} ],
    exposed_endpoints: [ (ron_name: "door", ron_url: "127.0.0.1:{grpc}", ron_http_url: Some("127.0.0.1:{http}")) ],
    storage_endpoints: [ (ron_name: "storage", ron_storage_type: "znippy", ron_path: "{d}") ],
    audit: (dir: "{d}/audit", enabled: true),
    auth: Some((
        methods: [ StaticToken(subject: "rel", token: Some("{ADMIN}")), StaticToken(subject: "rita", token: Some("{READER}")) ],
        roles: {{"rel": Admin, "rita": Reader}},
    )),
    promotion: Some((
        ladder_name: "rust",
        stages: [ (name: "dev", repo: "mirror"), (name: "prod", repo: "release", gated: true, gates: ["osv"]) ],
        advisory_db: Some("{d}/advisories.json"),
        // The seeded crates are bare bytes with no licence; the gate under test is the CVE's.
        unknown_license: Pass,
    )),
)"#,
        mirror = repo("mirror"),
        release = repo("release"),
    )
}

fn start() -> Running {
    let mut last = String::new();
    for _ in 0..8 {
        let (grpc, http) = (hold_port(), hold_port());
        let data = tempfile::tempdir().unwrap();
        let d = data.path().display().to_string();
        std::fs::write(data.path().join("advisories.json"), OSV_FEED).unwrap();
        let mut cfg = tempfile::NamedTempFile::new().unwrap();
        cfg.write_all(ron(&d, grpc, http).as_bytes()).unwrap();
        let go = || -> Result<Holger, String> {
            let mut h = read_ron_config(cfg.path()).map_err(|e| format!("config: {e:#}"))?;
            h.instantiate_backends()
                .map_err(|e| format!("backends: {e:#}"))?;
            for v in ["1.0.0", "1.0.1"] {
                let id = ArtifactId {
                    namespace: None,
                    name: "serde".into(),
                    version: v.into(),
                };
                h.put("mirror", &id, format!("serde-{v}-bytes").as_bytes())
                    .map_err(|e| format!("seed: {e:#}"))?;
            }
            wire_holger(&mut h).map_err(|e| format!("wire: {e:#}"))?;
            h.start().map_err(|e| format!("start: {e:#}"))?;
            Ok(h)
        };
        match go() {
            Ok(h) => {
                return Running {
                    _data: data,
                    _cfg: cfg,
                    _holger: h,
                    grpc: format!("http://127.0.0.1:{grpc}"),
                }
            }
            Err(e) if e.starts_with("config") => panic!("{e}"),
            Err(e) => last = e,
        }
    }
    panic!("server never started; last error: {last}");
}

/// The PROD store's ref for `serde@version` in `release` (`<data root>/holger-prod-store`).
fn sealed_ref(r: &Running, version: &str) -> std::path::PathBuf {
    r._data
        .path()
        .join("holger-prod-store/refs/release")
        .join(format!("__serde__{version}.ref"))
}

fn as_<T>(token: &str, body: T) -> tonic::Request<T> {
    let mut r = tonic::Request::new(body);
    r.metadata_mut()
        .insert("authorization", format!("Bearer {token}").parse().unwrap());
    r
}

fn serde(version: &str) -> Option<ProtoId> {
    Some(ProtoId {
        namespace: String::new(),
        name: "serde".into(),
        version: version.into(),
    })
}

fn promote(version: &str) -> PromoteArtifactRequest {
    PromoteArtifactRequest {
        from_repo: "mirror".into(),
        to_repo: "release".into(),
        artifact: serde(version),
        ..Default::default()
    }
}

async fn clients(
    r: &Running,
) -> (
    PromotionServiceClient<tonic::transport::Channel>,
    RepositoryServiceClient<tonic::transport::Channel>,
) {
    for _ in 0..200 {
        if let (Ok(p), Ok(c)) = (
            PromotionServiceClient::connect(r.grpc.clone()).await,
            RepositoryServiceClient::connect(r.grpc.clone()).await,
        ) {
            return (p, c);
        }
        tokio::time::sleep(std::time::Duration::from_millis(20)).await;
    }
    panic!("holger never answered at {}", r.grpc);
}

#[tokio::test(flavor = "multi_thread")]
async fn the_osv_gate_refuses_a_vulnerable_promotion_over_grpc_and_passes_a_clean_one() {
    let r = start();
    let (mut p, mut c) = clients(&r).await;

    let st = p
        .promote_artifact(as_(READER, promote("1.0.1")))
        .await
        .expect_err("a Reader does not promote");
    assert!(
        matches!(
            st.code(),
            tonic::Code::PermissionDenied | tonic::Code::Unauthenticated
        ),
        "{st:?}"
    );

    let st = p
        .promote_artifact(as_(ADMIN, promote("1.0.0")))
        .await
        .expect_err("the osv gate refuses serde@1.0.0");
    assert!(
        st.message()
            .contains(holger_server_lib::errcode::PROMOTE_POLICY_BLOCKED.code),
        "refused by the frozen code: {st:?}"
    );
    assert!(
        st.message().contains("OSV-TEST-0001"),
        "refused by the advisory, named: {st:?}"
    );
    assert!(
        !sealed_ref(&r, "1.0.0").exists(),
        "nothing of the refused version is sealed"
    );
    let served = c
        .fetch_artifact(as_(
            ADMIN,
            FetchArtifactRequest {
                repository: "release".into(),
                id: serde("1.0.0"),
            },
        ))
        .await;
    assert!(
        served.is_err(),
        "release serves nothing of the refused version: {served:?}"
    );

    let ok = p
        .promote_artifact(as_(ADMIN, promote("1.0.1")))
        .await
        .expect("the clean version is promoted")
        .into_inner();
    assert!(ok.success && !ok.content_address.is_empty(), "{ok:?}");
    // The seal: the ref names the content address, and the blob is the promoted bytes.
    let record = std::fs::read_to_string(sealed_ref(&r, "1.0.1")).expect("the sealed ref");
    assert!(record.contains(&ok.content_address), "{record}");
    let blob = r
        ._data
        .path()
        .join("holger-prod-store/blobs")
        .join(&ok.content_address);
    assert_eq!(
        std::fs::read(&blob).expect("the sealed blob"),
        b"serde-1.0.1-bytes",
        "the promoted bytes, sealed"
    );
}