gunnar-grpc 2.0.0

The tonic service implementations and the event bus behind gunnar's gRPC control plane.
Documentation
//! **A credential refused for skew asks the front for the time — once a minute,
//! in the background, and with nothing from the request.**
//!
//! Its own test binary on purpose: `estate_clock::trigger` is process-global
//! (there is one clock per machine), and the stale credentials `tests/auth.rs`
//! sends would otherwise move the counters this file asserts on.

use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{mpsc, Arc, Mutex};
use std::time::{Duration, Instant};

use gunnar_grpc::auth::{AuthFailure, AuthGate, MAX_CLOCK_SKEW_MS};
use gunnar_grpc::{
    sign_credential_for, AuthMode, ControlPlane, MemoryControlPlane, CREDENTIAL_METADATA_KEY,
};

fn now_ms() -> i64 {
    std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .expect("after 1970")
        .as_millis() as i64
}

fn metadata_for(
    key: &ssh_key::PrivateKey,
    issued_at_ms: i64,
    nonce: &str,
) -> tonic::metadata::MetadataMap {
    let credential = sign_credential_for(key, issued_at_ms, nonce, "Admin.Health").expect("sign");
    let mut metadata = tonic::metadata::MetadataMap::new();
    metadata.insert(
        CREDENTIAL_METADATA_KEY,
        credential.to_header_value().parse().unwrap(),
    );
    metadata
}

#[test]
fn stale_refusals_start_one_background_sync_a_minute_carry_nothing_and_are_not_delayed() {
    let plane: Arc<dyn ControlPlane> = Arc::new(MemoryControlPlane::with_repo("nordisk", "gunnar"));
    let gate = AuthGate::new(plane, AuthMode::Required);
    // NOT enrolled anywhere: a stranger's key. Staleness is checked before the
    // key and the signature, so a stranger can trigger — which is exactly why
    // the rate limit exists.
    let stranger = ssh_key::PrivateKey::random(&mut rand::rand_core::UnwrapErr(rand::rngs::SysRng), ssh_key::Algorithm::Ed25519)
        .expect("mint");

    // The handler takes NO arguments: that is the "carries nothing" rule, held
    // by the type. It blocks until released, standing in for a slow front.
    let syncs = Arc::new(AtomicUsize::new(0));
    let (release, gate_rx) = mpsc::channel::<()>();
    let gate_rx = Mutex::new(gate_rx);
    let counter = Arc::clone(&syncs);
    estate_clock::trigger::install_skew_handler(move || {
        counter.fetch_add(1, Ordering::SeqCst);
        let _ = gate_rx
            .lock()
            .unwrap()
            .recv_timeout(Duration::from_secs(10));
    });
    let suppressed_before = estate_clock::trigger::suppressed();

    // 1. One stale refusal (two hours fast, the measured shape): refused as
    //    before, promptly, and exactly one sync starts.
    let started = Instant::now();
    let refused = gate.authenticate_bound(
        &metadata_for(&stranger, now_ms() + 7_200_000, "nonce-clock-00001"),
        now_ms(),
        Some("Admin.Health"),
    );
    let took = started.elapsed();
    assert!(
        matches!(refused, Err(AuthFailure::Stale { .. })),
        "{refused:?}"
    );
    assert!(
        took < Duration::from_millis(500),
        "the refusal waited {took:?} — the sync (still blocked) must not delay it"
    );
    let deadline = Instant::now() + Duration::from_secs(5);
    while syncs.load(Ordering::SeqCst) == 0 && Instant::now() < deadline {
        std::thread::sleep(Duration::from_millis(5));
    }
    assert_eq!(
        syncs.load(Ordering::SeqCst),
        1,
        "a stale refusal starts exactly one sync"
    );

    // 2. A burst inside the minute — forged, garbage-dated, both directions —
    //    starts nothing more, and every one is counted.
    for (i, issued) in [
        i64::MIN / 2,
        0,
        now_ms() - MAX_CLOCK_SKEW_MS - 1,
        now_ms() + 86_400_000,
    ]
    .into_iter()
    .cycle()
    .take(12)
    .enumerate()
    {
        let refused = gate.authenticate_bound(
            &metadata_for(&stranger, issued, &format!("nonce-burst-{i:06}")),
            now_ms(),
            Some("Admin.Health"),
        );
        assert!(
            matches!(refused, Err(AuthFailure::Stale { .. })),
            "{refused:?}"
        );
    }
    std::thread::sleep(Duration::from_millis(50));
    assert_eq!(
        syncs.load(Ordering::SeqCst),
        1,
        "a burst inside 60 s starts no second sync"
    );
    assert_eq!(estate_clock::trigger::suppressed() - suppressed_before, 12);

    // 3. A credential inside the window is not a skew refusal and triggers nothing.
    let _ = gate.authenticate_bound(
        &metadata_for(&stranger, now_ms(), "nonce-fresh-00001"),
        now_ms(),
        Some("Admin.Health"),
    );
    assert_eq!(estate_clock::trigger::suppressed() - suppressed_before, 12);

    let _ = release.send(());
}