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");
}