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