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

nordisk/gunnar

crates/gunnar-grpc/tests/wire.rs

//! **The wiring in [`router`], asserted over a real socket.**
//!
//! `tests/auth.rs` is thorough about the two things it tests — the gate's
//! logic, and every handler's own deny-by-default — and it tests both by
//! calling the service traits DIRECTLY. Its own header says so: the 56-RPC
//! sweep runs "with no interceptor in the way". Nothing in it, and nothing
//! anywhere else in `crates/gunnar-grpc/tests/`, ever constructs a tonic
//! server. The single production assembly function
//! [`gunnar_grpc::router_with_trust`] has exactly one non-test caller
//! (`gunnar-server/src/serve.rs`) and, until this file, was executed by no test
//! at all.
//!
//! So the mapping from `gate` to `add_service(…, gate.clone())` — nine lines in
//! `lib.rs` — was asserted by prose and by nothing else. `tests/auth.rs:1013`
//! says out loud that it is *asserting in a comment* that `MirrorServer` is
//! mounted with the interceptor.
//!
//! ## Why this is not the same test twice
//!
//! Because the two layers are indistinguishable from the client's chair, and
//! deliberately so. `ServiceContext::caller` returns
//! `AuthFailure::Missing.status()` when there is no `Caller` extension, which is
//! byte-for-byte the status the gate returns when there is no credential. A
//! wire test that only asserted `Code::Unauthenticated` would therefore stay
//! green with every `with_interceptor` deleted — a hollow guard, and exactly
//! the shape a red-first test exists to catch. Measured: see
//! `an_unauthenticated_call_is_refused_BY_THE_GATE`, whose two assertions were
//! run against a deliberately unmounted `AdminServer`; the status assertion
//! stayed green and the audit assertion went red.
//!
//! The distinguishing signal is the audit record, and `auth.rs` says it is
//! there for this purpose: the gate's refusal emits `grpc.auth.refused` and
//! "names no principal because there is none, which is the difference a test
//! can assert on between this and a handler-level denial". A handler-level
//! refusal emits no such record. So:
//!
//! * `Code::Unauthenticated` says *somebody* refused;
//! * `grpc.auth.refused` says **the interceptor** refused, i.e. the service is
//!   genuinely mounted behind the gate.
//!
//! ## The red
//!
//! Every claim here is asserted in both directions. `AuthMode::Required`
//! refuses; the SAME calls over the SAME wire against a listener built with
//! `insecure_allow_all_for_local_development()` are served. Without that half,
//! a router that refused everything — a dead port, a broken client, a codec
//! mismatch — would pass this file perfectly.
//!
//! `Bootstrap` is the control: it is mounted OUTSIDE the gate on purpose, and
//! it must answer an uncredentialled caller in both modes. That is what
//! separates "the gate is on" from "the port is dead".
//!
//! ## What this file does NOT prove
//!
//! It drives a plaintext loopback listener, so it says nothing about the mutual
//! TLS path in `router_with_trust(.., Some(trust))`. `lib.rs`'s own
//! `trust_tests` module explains why that one needs three live handshakes.

use std::net::{SocketAddr, TcpListener};
use std::sync::{Arc, Mutex, MutexGuard, OnceLock};
use std::time::{Duration, Instant};

use gunnar_grpc::{
    router, ControlPlane, EventBus, MemoryControlPlane, ServerConfig, ServiceContext, AUDIT_TARGET,
};
use gunnar_proto::*;
use tonic::transport::Channel;
use tonic::{Code, Request};
use tracing_subscriber::layer::SubscriberExt;

const NS: &str = "nordisk";
const REPO: &str = "gunnar";

// ───────────────────────────────────────────────────────────────────────────
// The audit trail, captured globally
// ───────────────────────────────────────────────────────────────────────────
//
// `tests/auth.rs` uses `with_default`, which is THREAD-LOCAL. A tonic server
// answers on the runtime's worker threads, so a thread-local subscriber sees
// none of its events — a capture that quietly recorded nothing would make every
// audit assertion here vacuously... false, and then vacuously green the moment
// someone "fixed" it by asserting emptiness. It is a global subscriber for that
// reason, and the tests take a lock so one test's records are not another's.

static AUDIT: OnceLock<Arc<Mutex<Vec<String>>>> = OnceLock::new();
static SERIALIZE: Mutex<()> = Mutex::new(());

fn audit() -> &'static Arc<Mutex<Vec<String>>> {
    AUDIT.get_or_init(|| {
        let log: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new()));
        let subscriber = tracing_subscriber::registry().with(AuditLayer(Arc::clone(&log)));
        tracing::subscriber::set_global_default(subscriber)
            .expect("this test binary installs the audit subscriber exactly once");
        log
    })
}

struct AuditLayer(Arc<Mutex<Vec<String>>>);

impl<S: tracing::Subscriber> tracing_subscriber::Layer<S> for AuditLayer {
    fn on_event(
        &self,
        event: &tracing::Event<'_>,
        _ctx: tracing_subscriber::layer::Context<'_, S>,
    ) {
        if event.metadata().target() != AUDIT_TARGET {
            return;
        }
        struct Visit(String);
        impl tracing::field::Visit for Visit {
            fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) {
                self.0.push_str(&format!("{}={:?} ", field.name(), value));
            }
        }
        let mut visit = Visit(String::new());
        event.record(&mut visit);
        self.0.lock().unwrap().push(visit.0);
    }
}

/// Take the serialization lock and start this test with an empty audit log.
fn fresh_audit() -> MutexGuard<'static, ()> {
    let guard = SERIALIZE.lock().unwrap_or_else(|e| e.into_inner());
    audit().lock().unwrap().clear();
    guard
}

/// The audit records emitted so far.
///
/// The gate warns from the worker thread that answered the call, and the client
/// future can resolve before that `tracing::warn!` has been appended, so this
/// waits briefly for a record rather than sampling once. It returns whatever it
/// has when the deadline passes — an empty answer is a legitimate outcome here
/// (it is exactly what a handler-level refusal produces) and must not be
/// smoothed into a hang or a panic.
fn audit_lines() -> Vec<String> {
    let deadline = Instant::now() + Duration::from_secs(2);
    loop {
        let lines = audit().lock().unwrap().clone();
        if !lines.is_empty() || Instant::now() >= deadline {
            return lines;
        }
        std::thread::sleep(Duration::from_millis(10));
    }
}

fn refusals(lines: &[String]) -> Vec<&String> {
    lines
        .iter()
        .filter(|line| line.contains(r#"event="grpc.auth.refused""#))
        .collect()
}

// ───────────────────────────────────────────────────────────────────────────
// A real listener, serving the real router
// ───────────────────────────────────────────────────────────────────────────

/// The production assembly, on a real TCP port.
///
/// [`router`] and nothing else: a fixture that hand-assembled a tonic server
/// here would be a SECOND wiring, and the drift between it and `lib.rs` is the
/// entire thing this file exists to detect.
struct Wire {
    addr: SocketAddr,
    runtime: tokio::runtime::Runtime,
}

impl Wire {
    fn serve(config: ServerConfig) -> Wire {
        // Bind to :0 to learn a free port, then release it. tonic's `serve`
        // binds the address itself, so the port has to be named before it is
        // handed over.
        let addr = TcpListener::bind("127.0.0.1:0")
            .expect("a loopback port")
            .local_addr()
            .expect("its address");

        let plane = Arc::new(MemoryControlPlane::with_repo(NS, REPO));
        let control: Arc<dyn ControlPlane> = plane;
        let ctx = ServiceContext::new(control, Arc::new(EventBus::new(64)), config);

        let runtime = tokio::runtime::Builder::new_multi_thread()
            .enable_all()
            .build()
            .expect("runtime");
        runtime.spawn(async move {
            let _ = router(ctx, || 0).serve(addr).await;
        });

        let wire = Wire { addr, runtime };
        wire.await_listening();
        wire
    }

    /// Block until the listener answers, and fail loudly if it never does.
    ///
    /// A test that raced the bind would report `Unauthenticated`-shaped
    /// failures that are really connection failures, which is the same green
    /// as a working gate read backwards.
    fn await_listening(&self) {
        let deadline = Instant::now() + Duration::from_secs(10);
        while Instant::now() < deadline {
            if std::net::TcpStream::connect_timeout(&self.addr, Duration::from_millis(200)).is_ok()
            {
                return;
            }
            std::thread::sleep(Duration::from_millis(20));
        }
        panic!("the router never bound {}", self.addr);
    }

    fn channel(&self) -> Channel {
        let url = format!("http://{}", self.addr);
        self.runtime.block_on(async move {
            Channel::from_shared(url)
                .expect("a valid endpoint")
                .connect()
                .await
                .expect("connect to the router")
        })
    }
}

/// The gate ON — the shipped default, and the fixture asserts that rather than
/// trusting it.
fn required() -> Wire {
    let config = ServerConfig::default();
    assert_eq!(
        config.auth,
        gunnar_grpc::AuthMode::Required,
        "the fixture must serve the SHIPPED default; a wire test that quietly ran the escape \
         hatch would prove nothing"
    );
    Wire::serve(config)
}

/// The gate OFF — the escape hatch, used here as the red half and nowhere else.
fn allow_all() -> Wire {
    Wire::serve(ServerConfig::default().insecure_allow_all_for_local_development())
}

/// Drive one uncredentialled RPC on each of the nine gated services, over the
/// wire, and hand back `(service.method, outcome)`.
///
/// One list, used by both the refusal test and the allow-all test, so the two
/// halves cannot drift into asking different questions (one implementation).
fn sweep(wire: &Wire) -> Vec<(&'static str, Result<(), Code>)> {
    let channel = wire.channel();
    let mut out = Vec::new();
    wire.runtime.block_on(async {
        macro_rules! call {
            ($name:expr, $client:path, $method:ident, $request:expr) => {{
                let mut client = <$client>::new(channel.clone());
                let outcome = client
                    .$method(Request::new($request))
                    .await
                    .map(|_| ())
                    .map_err(|status| status.code());
                out.push(($name, outcome));
            }};
        }

        call!(
            "Repos.List",
            repos_client::ReposClient<Channel>,
            list,
            ListReposRequest::default()
        );
        call!(
            "Refs.List",
            refs_client::RefsClient<Channel>,
            list,
            ListRefsRequest::default()
        );
        call!(
            "Access.ListPrincipals",
            access_client::AccessClient<Channel>,
            list_principals,
            ListPrincipalsRequest::default()
        );
        call!(
            "Policy.ListGates",
            policy_client::PolicyClient<Channel>,
            list_gates,
            ListGatesRequest::default()
        );
        call!(
            "Events.Watch",
            events_client::EventsClient<Channel>,
            watch,
            WatchRequest::default()
        );
        call!(
            "Admin.Health",
            admin_client::AdminClient<Channel>,
            health,
            HealthRequest::default()
        );
        call!(
            "Mirror.Status",
            mirror_client::MirrorClient<Channel>,
            status,
            MirrorStatusRequest::default()
        );
        call!(
            "PullRequests.List",
            pull_requests_client::PullRequestsClient<Channel>,
            list,
            ListPullRequestsRequest::default()
        );
        call!(
            "Forge.ListReviews",
            forge_client::ForgeClient<Channel>,
            list_reviews,
            ListReviewsRequest::default()
        );
    });
    out
}

// ───────────────────────────────────────────────────────────────────────────
// The claims
// ───────────────────────────────────────────────────────────────────────────

/// **Every gated service refuses a caller who sent no credential — and it is
/// the GATE that refuses.**
///
/// The second half is the load-bearing one. See the module header: the status
/// alone cannot tell a mounted interceptor from a handler catching the same
/// request behind an unmounted one.
#[test]
fn an_unauthenticated_call_is_refused_by_the_gate() {
    let _guard = fresh_audit();
    let wire = required();
    let outcomes = sweep(&wire);

    assert_eq!(outcomes.len(), 9, "one call per gated service");
    for (name, outcome) in &outcomes {
        assert_eq!(
            *outcome,
            Err(Code::Unauthenticated),
            "{name} answered an uncredentialled caller over the wire"
        );
    }

    // And the interceptor is what answered. A handler-level denial emits no
    // `grpc.auth.refused` record at all.
    let lines = audit_lines();
    let refused = refusals(&lines);
    assert_eq!(
        refused.len(),
        outcomes.len(),
        "expected one gate refusal per call; the gate is not mounted in front of every service. \
         records: {lines:#?}"
    );
    for line in &refused {
        assert!(
            line.contains(r#"reason="missing""#),
            "a call with no credential must be refused as `missing`, not {line}"
        );
    }
}

/// **`Bootstrap` answers without a credential — in the gated mode too.**
///
/// The control. It is mounted outside the gate deliberately (`lib.rs`: a gated
/// `Bootstrap` on a server with an empty key table could be reached by nobody),
/// and it is what proves the nine refusals above are a gate and not a dead
/// port, a bad codec or a client that cannot connect.
#[test]
fn bootstrap_is_served_without_a_credential() {
    let _guard = fresh_audit();
    let wire = required();
    let channel = wire.channel();
    let answered = wire.runtime.block_on(async {
        bootstrap_client::BootstrapClient::new(channel)
            .status(Request::new(BootstrapStatusRequest::default()))
            .await
    });
    assert!(
        answered.is_ok(),
        "Bootstrap.Status is the ONE unauthenticated service and must answer: {answered:?}"
    );
    assert!(
        refusals(&audit_lines()).is_empty(),
        "the gate refused a service it is not mounted in front of"
    );
}

/// **THE RED: the same nine calls, on the same wire, are SERVED when the gate
/// is off.**
///
/// Without this, a router that refused every request for any reason would pass
/// the test above and prove nothing. `Repos.List` is asserted as an outright
/// success; the others are only asserted to be no longer refused *for want of a
/// credential*, because several of them name a repository this fixture does not
/// have and are entitled to fail on their own merits.
#[test]
fn the_gate_is_what_refuses_and_not_the_wire() {
    let _guard = fresh_audit();
    let wire = allow_all();
    let outcomes = sweep(&wire);

    let repos = outcomes
        .iter()
        .find(|(name, _)| *name == "Repos.List")
        .expect("Repos.List is in the sweep");
    assert_eq!(
        repos.1,
        Ok(()),
        "with the gate off, an uncredentialled Repos.List must be served — if this is \
         UNAUTHENTICATED the refusal above was never the gate's"
    );

    for (name, outcome) in &outcomes {
        assert_ne!(
            *outcome,
            Err(Code::Unauthenticated),
            "{name} was refused for want of a credential on a listener that requires none"
        );
    }

    // **The predicate `gunnar control status` reports on.** That verb calls
    // `Admin.Health` with no credential and prints `authentication REQUIRED`
    // when it is refused; its other branch — the ⚠ one that says the control
    // plane is serving uncredentialled admin calls — cannot be reached through
    // any shipped path, because nothing in production calls
    // `insecure_allow_all_for_local_development`. This is the assertion that
    // keeps that warning from being fiction: on a listener with the gate off,
    // an uncredentialled `Admin.Health` really is SERVED, so the verb's two
    // branches describe two states that both exist.
    let health = outcomes
        .iter()
        .find(|(name, _)| *name == "Admin.Health")
        .expect("Admin.Health is in the sweep");
    assert_eq!(
        health.1,
        Ok(()),
        "with the gate off an uncredentialled Admin.Health must be served — this is the exact \
         call `gunnar control status` uses to decide which of its two verdicts to print"
    );

    assert!(
        refusals(&audit_lines()).is_empty(),
        "the gate refused nothing here, so it must have recorded nothing"
    );
}