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

//! ★ HCON-5 (owner 2026-10-27): **what is open on a Holger is configured in its RON.** One
//! `access` block (`reads: Gated | Open`, `front: Open | Gated`) and each repository's
//! `visibility: Public`; one gate for every read door.
//!
//! * Gated, a public repository is read with no sign-in; a private one is refused by name
//!   (`[read-gated]` and the repository), and read with a token.
//! * The catalog an anonymous caller is shown lists the public repository only.
//! * The front page stays open on a gated box (`front: Open`).
//! * A bad `access` block is refused at load, by name.

use std::io::Write;

use holger_server_lib::{read_ron_config, wire_holger, Holger};
use traits::test_support::hold_port;

const TOKEN: &str = "a-token-of-sufficient-length-0123456789";

fn ron(data: &std::path::Path, grpc: u16, http: u16, access: &str) -> String {
    let d = data.display();
    format!(
        r#"(
    repositories: [
        (ron_name: "crates-nordisk", ron_repo_type: "raw", ron_upstreams: [], ron_in: None,
         ron_out: Some((ron_storage_endpoint: "storage", ron_exposed_endpoint: "default")),
         ron_data_dir: Some("{d}/crates-nordisk"), visibility: Public),
        (ron_name: "pypi-internal", ron_repo_type: "raw", ron_upstreams: [], ron_in: None,
         ron_out: Some((ron_storage_endpoint: "storage", ron_exposed_endpoint: "default")),
         ron_data_dir: Some("{d}/pypi-internal")),
    ],
    exposed_endpoints: [
        (ron_name: "default", 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}") ],
    auth: Some(( methods: [ StaticToken(subject: "ci", token: Some("{TOKEN}")) ], roles: {{"ci": Admin}}, require_auth_for_reads: false )),
    {access}
)"#
    )
}

fn load(text: &str) -> anyhow::Result<(Holger, tempfile::NamedTempFile)> {
    let mut cfg = tempfile::NamedTempFile::new().unwrap();
    cfg.write_all(text.as_bytes()).unwrap();
    let holger = read_ron_config(cfg.path())?;
    Ok((holger, cfg))
}

async fn wait_healthy(c: &reqwest::Client, base: &str) {
    for _ in 0..200 {
        if let Ok(r) = c.get(format!("{base}/healthz")).send().await {
            if r.status() == 200 {
                return;
            }
        }
        tokio::time::sleep(std::time::Duration::from_millis(20)).await;
    }
    panic!("holger never became healthy at {base}");
}

#[tokio::test(flavor = "multi_thread")]
async fn a_gated_holger_serves_its_public_repository_and_refuses_the_rest_by_name() {
    let data = tempfile::tempdir().unwrap();
    let (grpc, http) = (hold_port(), hold_port());
    let (mut holger, _cfg) = load(&ron(
        data.path(),
        grpc,
        http,
        "access: (reads: Gated, front: Open),",
    ))
    .expect("the declaration loads");
    holger.instantiate_backends().unwrap();
    wire_holger(&mut holger).unwrap();
    holger.start().unwrap();
    let base = format!("http://127.0.0.1:{http}");
    let _ = rustls::crypto::ring::default_provider().install_default();
    let c = reqwest::Client::new();
    wait_healthy(&c, &base).await;

    for repo in ["crates-nordisk", "pypi-internal"] {
        let put = c
            .put(format!("{base}/{repo}/pkg-1.0.tar"))
            .bearer_auth(TOKEN)
            .body(format!("{repo} bytes"))
            .send()
            .await
            .unwrap();
        assert!(
            put.status().is_success(),
            "{repo}: the admin writes: {}",
            put.status()
        );
    }

    let public = c
        .get(format!("{base}/crates-nordisk/pkg-1.0.tar"))
        .send()
        .await
        .unwrap();
    assert_eq!(
        public.status(),
        200,
        "a public repository is read with no sign-in on a gated box"
    );
    assert_eq!(public.text().await.unwrap(), "crates-nordisk bytes");

    let private = c
        .get(format!("{base}/pypi-internal/pkg-1.0.tar"))
        .send()
        .await
        .unwrap();
    assert_eq!(
        private.status(),
        401,
        "a private repository needs a sign-in"
    );
    let words = private.text().await.unwrap();
    assert!(
        words.starts_with("[read-gated]") && words.contains("pypi-internal"),
        "refused by name, with the repository: {words}"
    );

    let signed_in = c
        .get(format!("{base}/pypi-internal/pkg-1.0.tar"))
        .bearer_auth(TOKEN)
        .send()
        .await
        .unwrap();
    assert_eq!(
        signed_in.status(),
        200,
        "a token reads the private repository"
    );

    // The page itself always carries the words it prints on its own (`page_refusals`, among
    // them `holger-front-3`), so the gate is read where it answers: the `/-/front` door.
    #[cfg(feature = "front")]
    {
        let page = c.get(format!("{base}/")).send().await.unwrap();
        assert_eq!(
            page.status(),
            200,
            "the front page is served on a gated box (front: Open)"
        );
        let door = c.get(format!("{base}/-/front")).send().await.unwrap();
        assert_eq!(
            door.status(),
            200,
            "the front door stays open on a gated box (front: Open)"
        );
        let words = door.text().await.unwrap();
        assert!(
            !words.contains("holger-front-3"),
            "not refused as gated: {words}"
        );
    }
}

/// ★ HCON-5: `front: Gated` gates the front door too: an anonymous caller is refused by name
/// (`holger-front-3`), a signed-in one reads it.
#[cfg(feature = "front")]
#[tokio::test(flavor = "multi_thread")]
async fn a_gated_front_is_refused_by_name_and_read_signed_in() {
    let data = tempfile::tempdir().unwrap();
    let (grpc, http) = (hold_port(), hold_port());
    let (mut holger, _cfg) = load(&ron(
        data.path(),
        grpc,
        http,
        "access: (reads: Gated, front: Gated),",
    ))
    .expect("the declaration loads");
    holger.instantiate_backends().unwrap();
    wire_holger(&mut holger).unwrap();
    holger.start().unwrap();
    let base = format!("http://127.0.0.1:{http}");
    let _ = rustls::crypto::ring::default_provider().install_default();
    let c = reqwest::Client::new();
    wait_healthy(&c, &base).await;

    let anon = c.get(format!("{base}/-/front")).send().await.unwrap();
    let words = anon.text().await.unwrap();
    assert!(
        words.contains("holger-front-3"),
        "an anonymous caller is refused by name on a gated front: {words}"
    );
    let signed_in = c
        .get(format!("{base}/-/front"))
        .bearer_auth(TOKEN)
        .send()
        .await
        .unwrap();
    assert_eq!(signed_in.status(), 200, "a token reads the gated front");
    assert!(!signed_in.text().await.unwrap().contains("holger-front-3"));
}

#[test]
fn the_access_block_and_the_build_say_what_is_open() {
    let data = tempfile::tempdir().unwrap();
    let (holger, _cfg) = load(&ron(
        data.path(),
        1,
        2,
        "access: (reads: Gated, front: Open),",
    ))
    .unwrap();
    assert!(
        holger.auth.as_ref().unwrap().require_auth_for_reads,
        "Gated gates every read door"
    );
    assert_eq!(
        holger.access_words(),
        "Reading packages needs a sign-in. Public, readable with no sign-in: crates-nordisk. The front page and the sign-up are open."
    );
    let (open, _cfg) = load(&ron(
        data.path(),
        1,
        2,
        "access: (reads: Open, front: Open),",
    ))
    .unwrap();
    assert!(
        !open.auth.as_ref().unwrap().require_auth_for_reads,
        "Open reads openly, on any build"
    );
    let (default, _cfg) = load(&ron(data.path(), 1, 2, "")).unwrap();
    assert_eq!(
        default.auth.as_ref().unwrap().require_auth_for_reads,
        cfg!(feature = "estate"),
        "no access block: the build decides (an estate gates, a downstream reads openly)"
    );
}

#[test]
fn a_bad_access_block_is_refused_at_load_by_name() {
    let data = tempfile::tempdir().unwrap();
    let contradiction = ron(data.path(), 1, 2, "access: (reads: Open, front: Open),").replace(
        "require_auth_for_reads: false",
        "require_auth_for_reads: true",
    );
    let e = load(&contradiction).err().expect("refused").to_string();
    assert!(e.contains("[access-refused]"), "{e}");
    let e = load(&ron(data.path(), 1, 2, "access: (reads: Closed),"))
        .err()
        .expect("refused")
        .to_string();
    assert!(e.contains("Closed"), "an unknown word is named: {e}");
    let e = load(&ron(
        data.path(),
        1,
        2,
        "access: (reads: Gated, frontpage: Open),",
    ))
    .err()
    .expect("refused")
    .to_string();
    assert!(e.contains("frontpage"), "an unknown key is named: {e}");
    let gated_public = ron(data.path(), 1, 2, "").replace(
        "visibility: Public)",
        "visibility: Public, ron_require_auth: true)",
    );
    let e = load(&gated_public).err().expect("refused").to_string();
    assert!(
        e.contains("[access-refused]") && e.contains("crates-nordisk"),
        "{e}"
    );
}