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

//! **gunnar-server's router serves the identity core's IdentityService**
//! (IDENTITY-HOST): mounted by `router` itself (no second wiring), reachable
//! without a Gunnar credential (sign-up and login come before one), and its
//! refusals carry the core's codes.

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

use gunnar_grpc::identity::{system_clock, GunnarHost, GunnarStore, IdentityTables};
use gunnar_grpc::{
    router, ControlPlane, EventBus, MemoryControlPlane, ServerConfig, ServiceContext,
};
use nornir_identity::conformance::{config, first_admin, Harness, ORIGIN, RP_ID};
use nornir_identity::grpc::pb::identity_service_client::IdentityServiceClient;
use nornir_identity::grpc::{pb, refusal_of};
use nornir_identity::test_host::Kit;
use nornir_identity::{Code, Identity, NoMail, Site};

struct Plain;

impl Harness for Plain {
    type Host = GunnarHost;
    fn host(&self, _: Site, _: &Kit) -> GunnarHost {
        unreachable!("the identity here is built by hand")
    }
    fn admin_groups(&self) -> Vec<String> {
        vec![gunnar_grpc::identity::SERVER_ADMIN.to_owned()]
    }
    fn member_groups(&self) -> Vec<String> {
        Vec::new()
    }
}

#[test]
fn the_router_serves_the_identity_service_outside_the_gate() {
    let addr: SocketAddr = TcpListener::bind("127.0.0.1:0")
        .unwrap()
        .local_addr()
        .unwrap();
    let plane: Arc<dyn ControlPlane> = Arc::new(MemoryControlPlane::new());
    let store = GunnarStore::new(
        Arc::clone(&plane),
        IdentityTables::default(),
        None,
        system_clock(),
    );
    let identity = Arc::new(Identity::new(
        GunnarHost::new(Site::Private, store, Arc::new(NoMail), system_clock()),
        config(),
    ));
    let (admin, _) = first_admin(&Plain, &identity);
    // The default config authenticates every gated call: the identity verbs must not need it.
    let ctx = ServiceContext::new(plane, Arc::new(EventBus::new(16)), ServerConfig::default())
        .with_identity(identity);
    let runtime = tokio::runtime::Builder::new_multi_thread()
        .enable_all()
        .build()
        .unwrap();
    runtime.spawn(async move {
        let _ = router(ctx, || 0).serve(addr).await;
    });
    let deadline = Instant::now() + Duration::from_secs(10);
    while std::net::TcpStream::connect_timeout(&addr, Duration::from_millis(200)).is_err() {
        assert!(Instant::now() < deadline, "the router never bound {addr}");
        std::thread::sleep(Duration::from_millis(20));
    }
    runtime.block_on(async move {
        let mut client = IdentityServiceClient::connect(format!("http://{addr}"))
            .await
            .unwrap();
        let refused = client
            .sign_up(pb::SignUpRequest {
                email: "kim@mock.invalid".into(),
                display: "Kim".into(),
            })
            .await
            .unwrap_err();
        assert_eq!(
            refusal_of(&refused),
            Some(Code::SignUpClosed),
            "{refused:?}"
        );
        let ceremony = client
            .login_start(pb::LoginStartRequest {
                rp_id: RP_ID.into(),
                origin: ORIGIN.into(),
                client: String::new(),
            })
            .await
            .expect("a login opens with no Gunnar credential")
            .into_inner();
        assert!(!ceremony.challenge.is_empty());
        let who = client
            .whoami(pb::WhoamiRequest {
                session: admin.token.clone(),
            })
            .await
            .unwrap()
            .into_inner();
        assert!(who.admin, "the first admin's session, served by gunnar");
        let gone = client
            .whoami(pb::WhoamiRequest {
                session: "nis1.forged".into(),
            })
            .await
            .unwrap_err();
        assert_eq!(refusal_of(&gone), Some(Code::NoSession));
    });
}

/// ★ **RA5-32 follow-up: a login the console relays from off loopback is keyed by its own
/// client.** Gunnar's console runs on the front and reaches this listener over the private
/// network (mTLS); edda's adapter believes the `client` field only from loopback unless the
/// product says its transport authenticates every peer. Without it, every person who logs in
/// through the console would be one client (the console's address): sixteen ceremonies at
/// once for the whole site, then `busy` for everybody.
#[tokio::test]
async fn a_login_the_console_relays_from_off_loopback_is_keyed_by_its_own_client() {
    use nornir_identity::grpc::pb::identity_service_server::IdentityService as _;
    let plane: Arc<dyn ControlPlane> = Arc::new(MemoryControlPlane::new());
    let store = GunnarStore::new(
        Arc::clone(&plane),
        IdentityTables::default(),
        None,
        system_clock(),
    );
    let identity = Arc::new(Identity::new(
        GunnarHost::new(Site::Private, store, Arc::new(NoMail), system_clock()),
        config(),
    ));
    let svc = gunnar_grpc::identity_service(identity);
    for n in 0..nornir_identity::Limits::default().per_client + 4 {
        let mut req = tonic::Request::new(pb::LoginStartRequest {
            rp_id: RP_ID.into(),
            origin: ORIGIN.into(),
            client: format!("198.51.100.{n}"),
        });
        req.extensions_mut()
            .insert(tonic::transport::server::TcpConnectInfo {
                local_addr: None,
                remote_addr: Some("10.13.8.5:41000".parse().unwrap()),
            });
        svc.login_start(req)
            .await
            .unwrap_or_else(|e| panic!("browser {n} through the console: {e:?}"));
    }
}