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

nordisk/gunnar

crates/gunnar-server/tests/unverified_refusal_said_once.rs

//! **A refusal a retrying caller repeats is WARN once a minute** (release-3 test2 with
//! dev-narrate: main logged `grpc.auth.refused reason="not-verified"` and `a control-plane
//! call was refused … did not verify` at WARN 7/min — the front's certify thread and its
//! console calling with keys main had not installed yet). Seven calls with a key nobody
//! registered: each is refused, ONE of each line is a WARN, the rest are INFO.

use std::net::SocketAddr;
use std::sync::Arc;
use std::time::{Duration, Instant};

use gunnar_core::ssh_key::PrivateKey;
use gunnar_grpc::{EventBus, ServerConfig, ServiceContext};
use gunnar_server::stores::ServerStores;
use gunnar_server::twin;
use gunnar_server::CoreControlPlane;

fn free_addr() -> SocketAddr {
    let l = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
    l.local_addr().expect("addr")
}

fn wait<T>(what: &str, within: Duration, mut probe: impl FnMut() -> Option<T>) -> T {
    let t0 = Instant::now();
    loop {
        if let Some(v) = probe() {
            return v;
        }
        assert!(t0.elapsed() < within, "{what}: not within {within:?}");
        std::thread::sleep(Duration::from_millis(25));
    }
}

struct Served {
    rt: tokio::runtime::Runtime,
    addr: SocketAddr,
}

/// The fresh box's control plane as `serve` serves it: the real router,
/// authentication REQUIRED (the default), Bootstrap outside the gate.
fn serve(stores: &Arc<ServerStores>) -> Served {
    let plane: Arc<dyn gunnar_grpc::ControlPlane> =
        Arc::new(CoreControlPlane::new(Arc::clone(stores)));
    let ctx = ServiceContext::new(plane, Arc::new(EventBus::new(16)), ServerConfig::default());
    let rt = tokio::runtime::Builder::new_multi_thread()
        .worker_threads(2)
        .enable_all()
        .build()
        .expect("rt");
    let addr = free_addr();
    rt.spawn(async move {
        let _ = gunnar_grpc::router(ctx, || 0).serve(addr).await;
    });
    wait("the box listens", Duration::from_secs(10), || {
        std::net::TcpStream::connect(addr).ok()
    });
    Served { rt, addr }
}

fn with_credential<T>(message: T, credential: Option<String>) -> tonic::Request<T> {
    let mut request = tonic::Request::new(message);
    if let Some(value) = credential {
        request.metadata_mut().insert(
            gunnar_grpc::CREDENTIAL_METADATA_KEY,
            value.parse().expect("a legal header"),
        );
    }
    request
}

fn key() -> (PrivateKey, String) {
    let pem = gunnar_core::mint_ed25519().expect("mint");
    let key = PrivateKey::from_openssh(std::str::from_utf8(&pem).unwrap()).expect("parse");
    let line = key.public_key().to_openssh().expect("public line");
    (key, line)
}

fn credential(key: &PrivateKey, method: &str) -> String {
    use std::sync::atomic::{AtomicU64, Ordering};
    static N: AtomicU64 = AtomicU64::new(0);
    let now = twin::now_ms();
    let nonce = format!(
        "bootstrap-credential-test-{}-{}-{now}",
        std::process::id(),
        N.fetch_add(1, Ordering::SeqCst)
    );
    gunnar_grpc::sign_credential_for(key, now, &nonce, method)
        .expect("sign")
        .to_header_value()
}

/// What the installer's door leaves on a box it fed: `admin_pubkey` is the operator.
fn feed(root: &std::path::Path, admin: &str) {
    std::fs::create_dir_all(root.join("pairing")).unwrap();
    let text = format!(
        "estate = \"bootstrap-credential-test\"\nrole = \"twin\"\npeers = []\n\n[me]\nname = \"twin\"\noverlay = \"10.13.0.3\"\n\n\
         [feed]\nadmin_pubkey = \"{admin}\"\n"
    );
    std::fs::write(root.join("pairing/joined.toml"), text).unwrap();
}

fn fresh_fed() -> (tempfile::TempDir, Arc<ServerStores>, PrivateKey) {
    let fd = tempfile::tempdir().unwrap();
    let stores = Arc::new(ServerStores::open(fd.path()).unwrap());
    let (operator, line) = key();
    feed(fd.path(), &line);
    (fd, stores, operator)
}

#[derive(Clone, Default)]
struct Heard(Arc<std::sync::Mutex<Vec<u8>>>);
impl std::io::Write for Heard {
    fn write(&mut self, b: &[u8]) -> std::io::Result<usize> {
        self.0.lock().unwrap().extend_from_slice(b);
        Ok(b.len())
    }
    fn flush(&mut self) -> std::io::Result<()> {
        Ok(())
    }
}

#[test]
fn seven_calls_with_a_key_nobody_registered_are_refused_and_said_at_warn_once() {
    let heard = Heard::default();
    let h = heard.clone();
    tracing::subscriber::set_global_default(
        tracing_subscriber::fmt()
            .with_writer(move || h.clone())
            .with_ansi(false)
            .finish(),
    )
    .expect("the only subscriber in this binary");
    let (_fd, stores, _operator) = fresh_fed();
    // An admin exists, so the box authenticates every call.
    let (admin, admin_line) = key();
    let _ = admin;
    stores.auth().bootstrap_admin(&admin_line).unwrap();
    let served = serve(&stores);
    let (stranger, _) = key();
    for _ in 0..7 {
        let _in = served.rt.enter();
        let channel = tonic::transport::Channel::from_shared(format!("http://{}", served.addr))
            .unwrap()
            .connect_lazy();
        let mut c = gunnar_proto::access_client::AccessClient::new(channel);
        let req = with_credential(
            gunnar_proto::ListKeysRequest {
                principal_id: String::new(),
            },
            Some(credential(&stranger, "Access.ListKeys")),
        );
        let r = served.rt.block_on(async move { c.list_keys(req).await });
        assert_eq!(
            r.expect_err("a key nobody registered").code(),
            tonic::Code::Unauthenticated
        );
    }
    let said = String::from_utf8(heard.0.lock().unwrap().clone()).unwrap();
    let warn = |what: &str| {
        said.lines()
            .filter(|l| l.split_whitespace().nth(1) == Some("WARN") && l.contains(what))
            .count()
    };
    let all = |what: &str| said.lines().filter(|l| l.contains(what)).count();
    assert_eq!(
        all("grpc.auth.refused"),
        7,
        "every refusal is still recorded:\n{said}"
    );
    assert_eq!(
        warn("gRPC call refused before reaching the control plane"),
        1,
        "once at WARN:\n{said}"
    );
    assert_eq!(
        warn("a control-plane call was refused"),
        1,
        "once at WARN:\n{said}"
    );
}