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_session_auth.rs

//! **A person's identity-core session is a Gunnar credential** (IDENTITY-HOST,
//! A34-18): a console carries the opaque session token in
//! `nornir-identity-session` and every gated verb acts as that person; the
//! server verifies it (the core), the console decides nothing.

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

use gunnar_grpc::auth::SESSION_METADATA_KEY;
use gunnar_grpc::identity::{system_clock, GunnarHost, GunnarStore, IdentityTables, SERVER_ADMIN};
use gunnar_grpc::{
    router, ControlPlane, EventBus, MemoryControlPlane, ServerConfig, ServiceContext,
    CREDENTIAL_METADATA_KEY,
};
use gunnar_proto::access_client::AccessClient;
use gunnar_proto::WhoAmIRequest;
use nornir_identity::conformance::{config, first_admin, Harness};
use nornir_identity::test_host::Kit;
use nornir_identity::{Identity, NoMail, Site};
use tonic::Request;

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![SERVER_ADMIN.to_owned()]
    }
    fn member_groups(&self) -> Vec<String> {
        Vec::new()
    }
}

fn with_session<T>(message: T, token: &str) -> Request<T> {
    let mut request = Request::new(message);
    request
        .metadata_mut()
        .insert(SESSION_METADATA_KEY, token.parse().unwrap());
    request
}

#[test]
fn a_session_token_is_the_persons_credential_on_every_gated_verb() {
    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);
    let ctx = ServiceContext::new(plane, Arc::new(EventBus::new(16)), ServerConfig::default())
        .with_identity(Arc::clone(&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 access = AccessClient::connect(format!("http://{addr}"))
            .await
            .unwrap();
        let me = access
            .who_am_i(with_session(WhoAmIRequest::default(), &admin.token))
            .await
            .expect("the session acts as its person")
            .into_inner();
        assert_eq!(
            me.principal.map(|p| p.id),
            Some(admin.session.user_id.clone())
        );
        assert_eq!(me.server_level, gunnar_proto::GrantLevel::Admin as i32);

        let forged = access
            .who_am_i(with_session(WhoAmIRequest::default(), "nis1.forged"))
            .await
            .unwrap_err();
        assert_eq!(forged.code(), tonic::Code::Unauthenticated, "{forged:?}");

        let mut both = with_session(WhoAmIRequest::default(), &admin.token);
        both.metadata_mut().insert(
            CREDENTIAL_METADATA_KEY,
            "gunnar-sshsig-v2 x 0 n m a".parse().unwrap(),
        );
        let refused = access.who_am_i(both).await.unwrap_err();
        assert_eq!(
            refused.code(),
            tonic::Code::Unauthenticated,
            "a session and a signed credential at once: {refused:?}"
        );

        identity.logout(&admin.token, false).unwrap();
        let ended = access
            .who_am_i(with_session(WhoAmIRequest::default(), &admin.token))
            .await
            .unwrap_err();
        assert_eq!(
            ended.code(),
            tonic::Code::Unauthenticated,
            "a logged-out session is no credential"
        );
    });
}