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"
);
});
}