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

//! **Safe defaults** (lane HOL-NCSC, NCSC cross-domain gap list P1, owner 2026-10-30).
//!
//! The open defaults are gone: a Holger that a network can reach authenticates, speaks
//! TLS unless the file says `insecure: true` for that endpoint, and gives a signed-in
//! identity the least privilege (`Reader`) unless the policy names more. Each test drives
//! the real `read_ron_config` path, the one a `holger-server start` takes.
//!
//! * `holger-listen-1`: no authentication method and a listener off loopback.
//! * `holger-listen-2`: a listener off loopback without TLS and without `insecure: true`.
//! * configured authentication with no role policy resolves to `default_role: Reader`.
//! * `security_profile: Critical` requires a role policy written in the file.
//! * `steps:` asks for two approvers unless it names another number.
//! * `holger-audit-13`: no profile runs without its audit log (`audit: (mode: Off)` or the
//!   legacy `enabled: false` refuse the start; Standard included).

use holger_server_lib::read_ron_config;

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

fn config(endpoint: &str, auth: &str) -> String {
    format!(
        r#"(
    repositories: [],
    exposed_endpoints: [ {endpoint} ],
    storage_endpoints: [],
    {auth}
    audit: (dir: "a", enabled: true),
)"#
    )
}

const TOKEN: &str = r#"auth: Some(( methods: [ StaticToken(subject: "ci", token: Some("a-token-of-sufficient-length-0123456789")) ] )),"#;
const TLS: &str = r#"ron_tls: Some((cert: "c.pem", key: "k.pem"))"#;

fn refused(ron: &str) -> String {
    let (_tmp, h) = load(ron);
    match h.validate_listeners() {
        Ok(()) => panic!("this config must be refused:\n{ron}"),
        Err(e) => format!("{e:#}"),
    }
}

fn accepted(ron: &str) {
    let (_tmp, h) = load(ron);
    if let Err(e) = h.validate_listeners() {
        panic!("this config must start: {e:#}\n{ron}");
    }
}

#[test]
fn no_authentication_refuses_a_listener_off_loopback() {
    for ep in [
        format!(r#"(ron_name: "g", ron_url: "0.0.0.0:50051", {TLS})"#),
        format!(
            r#"(ron_name: "g", ron_url: "127.0.0.1:50051", ron_http_url: Some("10.0.0.7:8443"), {TLS})"#
        ),
        format!(r#"(ron_name: "g", ron_url: "[::]:50051", {TLS})"#),
        format!(r#"(ron_name: "g", ron_url: "holger.example:50051", {TLS})"#),
    ] {
        for auth in ["", "auth: Some(( methods: [] )),"] {
            let e = refused(&config(&ep, auth));
            assert!(e.contains("[holger-listen-1]"), "{e}");
            assert!(e.contains("'g'"), "the refusal names the endpoint: {e}");
        }
    }
}

#[test]
fn no_authentication_is_fine_on_loopback_only() {
    accepted(&config(
        r#"(ron_name: "g", ron_url: "127.0.0.1:50051", ron_http_url: Some("127.0.0.1:8443"))"#,
        "",
    ));
    accepted(&config(
        r#"(ron_name: "g", ron_url: "[::1]:50051", ron_http_url: Some("localhost:8443"))"#,
        "",
    ));
}

#[test]
fn a_cleartext_listener_off_loopback_needs_insecure_true() {
    let e = refused(&config(
        r#"(ron_name: "doors", ron_url: "0.0.0.0:50051")"#,
        TOKEN,
    ));
    assert!(
        e.contains("[holger-listen-2]") && e.contains("'doors'") && e.contains("insecure: true"),
        "{e}"
    );
    let e = refused(&config(
        r#"(ron_name: "doors", ron_url: "127.0.0.1:50051", ron_http_url: Some("0.0.0.0:8443"))"#,
        TOKEN,
    ));
    assert!(
        e.contains("[holger-listen-2]") && e.contains("0.0.0.0:8443"),
        "{e}"
    );
    // The explicit opt-out, and TLS, both start.
    accepted(&config(
        r#"(ron_name: "doors", ron_url: "0.0.0.0:50051", ron_http_url: Some("0.0.0.0:8443"), insecure: true)"#,
        TOKEN,
    ));
    accepted(&config(
        &format!(
            r#"(ron_name: "doors", ron_url: "0.0.0.0:50051", ron_http_url: Some("0.0.0.0:8443"), {TLS})"#
        ),
        TOKEN,
    ));
}

#[test]
fn insecure_true_does_not_excuse_a_missing_authentication() {
    let e = refused(&config(
        r#"(ron_name: "doors", ron_url: "0.0.0.0:50051", insecure: true)"#,
        "",
    ));
    assert!(e.contains("[holger-listen-1]"), "{e}");
}

#[test]
fn configured_authentication_makes_every_unnamed_identity_a_reader() {
    let (_tmp, h) = load(&config(r#"(ron_name: "g", ron_url: "127.0.0.1:1")"#, TOKEN));
    let auth = h.serving_auth();
    assert!(
        auth.rbac_enabled(),
        "configured authentication turns RBAC on"
    );
    assert_eq!(
        auth.default_role,
        Some(holger_server_lib::auth::Role::Reader)
    );
    assert_eq!(
        auth.role_for("ci"),
        holger_server_lib::auth::Role::Reader,
        "a signed-in identity is no admin"
    );
    // A policy that names roles is kept as written.
    let (_tmp, h) = load(&config(
        r#"(ron_name: "g", ron_url: "127.0.0.1:1")"#,
        r#"auth: Some(( methods: [ StaticToken(subject: "ci", token: Some("a-token-of-sufficient-length-0123456789")) ], roles: {"ci": Admin} )),"#,
    ));
    let auth = h.serving_auth();
    assert_eq!(auth.role_for("ci"), holger_server_lib::auth::Role::Admin);
    assert_eq!(
        auth.role_for("someone-else"),
        holger_server_lib::auth::Role::Reader
    );
    // No authentication at all: nothing to resolve, RBAC stays off.
    let (_tmp, h) = load(&config(r#"(ron_name: "g", ron_url: "127.0.0.1:1")"#, ""));
    assert!(!h.serving_auth().rbac_enabled());
}

#[test]
fn critical_requires_a_role_policy_in_the_file() {
    let ron = 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,
        ROLES
    )),
    audit: (dir: "audit", enabled: true),
    security_profile: Critical,
)"#;
    let (_tmp, h) = load(&ron.replace("ROLES", ""));
    let e = format!(
        "{:#}",
        h.validate_profile()
            .expect_err("critical without a role policy")
    );
    assert!(e.contains("role policy"), "{e}");
    let (_tmp, h) = load(&ron.replace("ROLES", r#"roles: {"ops": Admin},"#));
    h.validate_profile().expect("critical with a role policy");
    let (_tmp, h) = load(&ron.replace("ROLES", "default_role: Some(Reader),"));
    h.validate_profile()
        .expect("critical with an explicit default role");
}

#[cfg(feature = "intake")]
#[test]
fn steps_ask_for_two_approvers_by_default() {
    let ron = r#"(
    repositories: [],
    exposed_endpoints: [ (ron_name: "g", ron_url: "127.0.0.1:1") ],
    storage_endpoints: [],
    audit: (dir: "a", enabled: true),
    sources: [ (name: "team", kind: upload) ],
    steps: (),
)"#;
    let (_tmp, h) = load(ron);
    let intake = h
        .promotion
        .as_ref()
        .and_then(|p| p.intake.as_ref())
        .expect("steps lower onto the intake chain");
    assert_eq!(intake.approvals, 2, "two-person approval is the default");
    let (_tmp, h) = load(&ron.replace("steps: ()", "steps: (approvals: 1)"));
    let intake = h
        .promotion
        .as_ref()
        .and_then(|p| p.intake.as_ref())
        .unwrap();
    assert_eq!(intake.approvals, 1, "a number the file names is kept");
}

#[test]
fn no_profile_runs_without_its_audit_log() {
    for off in [
        r#"audit: (dir: "a", enabled: false),"#,
        r#"audit: (dir: "a", mode: Off),"#,
    ] {
        for profile in ["", "security_profile: Standard,"] {
            let ron = format!(
                r#"(
    repositories: [],
    exposed_endpoints: [ (ron_name: "g", ron_url: "127.0.0.1:1") ],
    storage_endpoints: [],
    {off}
    {profile}
)"#
            );
            let (_tmp, h) = load(&ron);
            let e = match h.validate_profile() {
                Ok(()) => panic!("a Holger with its audit log off must not start:\n{ron}"),
                Err(e) => format!("{e:#}"),
            };
            assert!(e.contains("[holger-audit-13]"), "{e}");
        }
    }
    // The default (omitted) is the append-only log, and it starts.
    let (_tmp, h) = load(
        r#"(repositories: [], exposed_endpoints: [ (ron_name: "g", ron_url: "127.0.0.1:1") ], storage_endpoints: [])"#,
    );
    h.validate_profile().expect("the default audit log");
}