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/critical_profile.rs

//! Fail-closed validation of the `Critical` security profile. These exercise the
//! pure `Holger::validate_profile` seam (no listeners) via the RON config path.

use holger_server_lib::read_ron_config;

fn load(ron: &str, tmp: &std::path::Path) -> holger_server_lib::Holger {
    let path = tmp.join("h.ron");
    std::fs::write(&path, ron).unwrap();
    read_ron_config(&path).expect("RON must parse")
}

/// A Critical config that satisfies every requirement (dummy TLS paths — the
/// validator checks presence/shape, not that the files load).
fn base_critical() -> String {
    r#"(
    repositories: [],
    exposed_endpoints: [
        (
            ron_name: "grpc",
            ron_url: "127.0.0.1:50051",
            ron_tls: Some((cert: "cert.pem", key: "key.pem", client_ca: Some("ca.pem"), crl: Some("crl.pem"))),
        ),
    ],
    storage_endpoints: [],
    auth: Some((
        methods: [ Mtls(ca_cert: "ca.pem") ],
        require_auth_for_reads: true,
        default_role: Some(Reader),
    )),
    audit: (dir: "audit", enabled: true),
    security_profile: Critical,
)"#
    .to_string()
}

#[test]
fn standard_profile_needs_nothing() {
    let tmp = tempfile::tempdir().unwrap();
    // No auth, no TLS on loopback — fine under Standard (the default). The audit log
    // stays on in every profile (HOL-NCSC, `holger-audit-13`).
    let ron = r#"(
        repositories: [],
        exposed_endpoints: [ (ron_name: "g", ron_url: "127.0.0.1:1") ],
        storage_endpoints: [],
        audit: (dir: "a", enabled: true),
    )"#;
    let h = load(ron, tmp.path());
    h.validate_profile()
        .expect("standard profile always validates");
}

#[test]
fn full_critical_config_validates() {
    let tmp = tempfile::tempdir().unwrap();
    let h = load(&base_critical(), tmp.path());
    h.validate_profile()
        .expect("a fully-configured critical profile validates");
}

fn assert_critical_rejects(ron: &str, needle: &str) {
    let tmp = tempfile::tempdir().unwrap();
    let h = load(ron, tmp.path());
    let err = h
        .validate_profile()
        .expect_err("critical profile must reject this config");
    let msg = format!("{err:#}");
    assert!(
        msg.contains(needle),
        "expected error to mention {needle:?}, got: {msg}"
    );
}

#[test]
fn critical_rejects_audit_disabled() {
    let ron = base_critical().replace("enabled: true", "enabled: false");
    assert_critical_rejects(&ron, "audit.enabled");
}

#[test]
fn critical_rejects_no_auth() {
    let ron = base_critical().replace(
        "auth: Some((\n        methods: [ Mtls(ca_cert: \"ca.pem\") ],\n        require_auth_for_reads: true,\n        default_role: Some(Reader),\n    )),",
        "auth: None,",
    );
    assert_critical_rejects(&ron, "auth configuration");
}

#[test]
fn critical_rejects_read_gate_off() {
    // HCON-5: an estate build gates reads by default; the declaration opens them by name.
    let ron = base_critical()
        .replace(
            "require_auth_for_reads: true",
            "require_auth_for_reads: false",
        )
        .replace(
            "    auth: Some((",
            "    access: (reads: Open, front: Open),\n    auth: Some((",
        );
    assert_critical_rejects(&ron, "require_auth_for_reads");
}

#[test]
fn critical_rejects_no_tls() {
    let ron = base_critical().replace(
        "ron_tls: Some((cert: \"cert.pem\", key: \"key.pem\", client_ca: Some(\"ca.pem\"), crl: Some(\"crl.pem\"))),",
        "",
    );
    assert_critical_rejects(&ron, "must configure TLS");
}

/// HOL-NCSC (NCSC P3): a Critical endpoint that verifies client certificates names their
/// revocation list.
#[test]
fn critical_rejects_mtls_without_a_crl() {
    let ron = base_critical().replace(", crl: Some(\"crl.pem\")", "");
    assert_critical_rejects(&ron, "ron_tls.crl");
}

#[test]
fn critical_rejects_no_client_ca() {
    let ron = base_critical().replace("client_ca: Some(\"ca.pem\")", "client_ca: None");
    assert_critical_rejects(&ron, "client_ca");
}

#[test]
fn critical_rejects_static_bearer_only() {
    let ron = base_critical().replace(
        "methods: [ Mtls(ca_cert: \"ca.pem\") ],",
        "methods: [ StaticToken(subject: \"ci\", token: Some(\"secret\")) ],",
    );
    assert_critical_rejects(&ron, "static bearer must not be the only");
}

#[test]
fn critical_with_a_failing_repo_hard_fails_instantiate() {
    // Passes validate_profile (valid TLS/auth/audit) but has a znippy repo whose
    // archive does not exist — in Critical, a single failing repo fails startup.
    let tmp = tempfile::tempdir().unwrap();
    let ron = r#"(
        repositories: [
            (
                ron_name: "fw",
                ron_repo_type: "rust",
                ron_upstreams: [],
                ron_in: None,
                ron_out: None,
                ron_archive_path: Some("/nonexistent/does-not-exist.znippy"),
            ),
        ],
        exposed_endpoints: [
            (
                ron_name: "grpc",
                ron_url: "127.0.0.1:50051",
                ron_tls: Some((cert: "cert.pem", key: "key.pem", client_ca: Some("ca.pem"), crl: Some("crl.pem"))),
            ),
        ],
        storage_endpoints: [],
        auth: Some(( methods: [ Mtls(ca_cert: "ca.pem") ], require_auth_for_reads: true, default_role: Some(Reader) )),
        audit: (dir: "audit", enabled: true),
        security_profile: Critical,
    )"#;
    let mut h = load(ron, tmp.path());
    let err = h
        .instantiate_backends()
        .expect_err("critical profile must hard-fail on a disabled repository");
    let msg = format!("{err:#}");
    assert!(
        msg.contains("critical profile") && msg.contains("failed to initialize"),
        "got: {msg}"
    );
}

#[test]
fn standard_with_a_failing_repo_disables_only_that_repo() {
    // Contrast: under Standard the same failing repo is disabled, not fatal.
    let tmp = tempfile::tempdir().unwrap();
    let ron = r#"(
        repositories: [
            (ron_name: "ok", ron_repo_type: "raw", ron_upstreams: [], ron_in: None, ron_out: None, ron_data_dir: Some("DATA")),
            (ron_name: "bad", ron_repo_type: "rust", ron_upstreams: [], ron_in: None, ron_out: None, ron_archive_path: Some("/nonexistent/x.znippy")),
        ],
        exposed_endpoints: [ (ron_name: "g", ron_url: "127.0.0.1:1") ],
        storage_endpoints: [],
    )"#
    .replace("DATA", &tmp.path().join("raw").to_string_lossy());
    let mut h = load(&ron, tmp.path());
    // Not all repos fail (raw is fine), so Standard serves on.
    h.instantiate_backends()
        .expect("standard profile disables only the failing repo");
}

/// **Critical without an intake chain refuses a writable or proxy repository** (SEC-REVIEW
/// 2026-11-01 P6). `scan_required_why` reads `promotion.intake`, so a Critical config with no
/// chain booted with every writable repository open: no side door was refused and nothing
/// scanned what they served. A repository that only serves a sealed archive is no door.
#[test]
fn critical_without_an_intake_chain_refuses_a_writable_or_proxy_repository() {
    for repo in [
        r#"(ron_name: "bundles", ron_repo_type: "raw", ron_upstreams: [], ron_in: None, ron_out: None, ron_data_dir: Some("d"))"#,
        r#"(ron_name: "bundles", ron_repo_type: "rust", ron_upstreams: [], ron_in: None, ron_out: None, ron_writable_archive: Some("w.znippy"))"#,
        r#"(ron_name: "bundles", ron_repo_type: "rust-remote", ron_base_url: Some("https://index.crates.io"), ron_upstreams: [], ron_in: None, ron_out: None)"#,
    ] {
        let ron = base_critical().replace("repositories: [],", &format!("repositories: [{repo}],"));
        let tmp = tempfile::tempdir().unwrap();
        let err = load(&ron, tmp.path())
            .validate_profile()
            .expect_err("critical without a chain refuses it");
        let msg = format!("{err:#}");
        assert!(
            msg.contains("holger-intake-18")
                && msg.contains("bundles")
                && msg.contains("promotion.intake"),
            "{repo}: {msg}"
        );
    }
    let sealed = r#"(ron_name: "fw", ron_repo_type: "rust", ron_upstreams: [], ron_in: None, ron_out: None, ron_archive_path: Some("x.znippy"))"#;
    let ron = base_critical().replace("repositories: [],", &format!("repositories: [{sealed}],"));
    let tmp = tempfile::tempdir().unwrap();
    load(&ron, tmp.path())
        .validate_profile()
        .expect("a sealed archive takes no writes");
}