crates/gunnar-server/tests/sso_without_an_idp_dials_nothing.rs
//! **AIRGAP B4: company SSO dials an identity provider only once one is configured** (owner
//! rule 2026-10-25: outbound calls need an explicit switch; for SSO the switch is an account
//! admin configuring an IdP). With none configured, the visitor's doors (`Sso.Discover`,
//! `Sso.Begin`) answer without the egress being asked at all, so nothing is dialled: no
//! discovery document, no keys, no token endpoint.
use std::sync::{Arc, Mutex};
use gunnar_grpc::sso::Egress;
use gunnar_grpc::{Caller, ControlPlane, EventBus, ServerConfig, ServiceContext, SsoService};
use gunnar_proto::sso_server::Sso as _;
use gunnar_proto::*;
use gunnar_server::control_plane::CoreControlPlane;
use gunnar_server::stores::ServerStores;
use tonic::Request;
/// An egress that records every call and dials nothing.
#[derive(Default)]
struct Counting(Mutex<Vec<String>>);
impl Counting {
fn ask(&self, url: &str) -> Result<(u16, String), String> {
self.0.lock().unwrap().push(url.to_owned());
Err("this egress dials nothing".to_owned())
}
}
impl Egress for Counting {
fn get(&self, url: &str) -> Result<(u16, String), String> {
self.ask(url)
}
fn post_form(
&self,
url: &str,
_authorization: &str,
_body: &str,
) -> Result<(u16, String), String> {
self.ask(url)
}
fn get_trusting(&self, url: &str, _ca: &[Vec<u8>]) -> Result<(u16, String), String> {
self.ask(url)
}
fn post_form_trusting(
&self,
url: &str,
_authorization: &str,
_body: &str,
_ca: &[Vec<u8>],
) -> Result<(u16, String), String> {
self.ask(url)
}
}
fn relay<T>(body: T) -> Request<T> {
let mut r = Request::new(body);
r.extensions_mut().insert(Caller {
principal_id: "gunnar-ui".to_owned(),
fingerprint: String::new(),
machine: true,
authenticated: true,
});
r
}
#[tokio::test(flavor = "multi_thread")]
async fn with_no_idp_configured_the_sign_in_doors_dial_nothing() {
let dir = tempfile::tempdir().expect("tempdir");
let stores = Arc::new(ServerStores::open(dir.path()).expect("stores"));
let plane = Arc::new(CoreControlPlane::new(Arc::clone(&stores)));
let control: Arc<dyn ControlPlane> = plane.clone();
let ctx = ServiceContext::new(
control,
Arc::new(EventBus::new(16)),
ServerConfig::default(),
);
let egress = Arc::new(Counting::default());
ctx.sso.set_egress(egress.clone());
ctx.sso.set_egress_on(true);
plane
.add_principal("gunnar-ui", "gunnar-ui", PrincipalKind::Machine)
.expect("console");
plane
.grant("gunnar-ui", "", "", GrantLevel::Admin)
.expect("the relay is a server admin");
plane
.add_principal("acme", "acme", PrincipalKind::User)
.expect("an account with no IdP");
let sso = SsoService::new(ctx.clone());
for hint in ["", "bob@acme.example", "acme", "acme.example"] {
let found = sso
.discover(relay(DiscoverRequest { hint: hint.into() }))
.await
.expect("discover answers")
.into_inner();
assert!(!found.found && !found.any_provider, "{hint:?}: {found:?}");
}
let begun = sso
.begin(relay(BeginRequest {
account: "acme".into(),
login_hint: "bob@acme.example".into(),
console_origin: "http://localhost:7000".into(),
client: String::new(),
}))
.await;
assert!(
begun.is_err(),
"a sign-in with no IdP configured is refused: {begun:?}"
);
assert!(
egress.0.lock().unwrap().is_empty(),
"the egress was asked: {:?}",
egress.0.lock().unwrap()
);
}
/// ★ **A configured IdP is dialled only while the operator's switch is on** (A34-16,
/// default = AIRGAP): with it off, an account admin's issuer is never read and the sign-in
/// is refused by name (`[sso-egress-off]`); switched on, the same sign-in reads the IdP when
/// `sso` is compiled in, and is refused by name with nothing dialled when it is not (default).
#[tokio::test(flavor = "multi_thread")]
async fn with_the_operators_switch_off_a_configured_idp_is_never_dialled() {
let dir = tempfile::tempdir().expect("tempdir");
let stores = Arc::new(ServerStores::open(dir.path()).expect("stores"));
let plane = Arc::new(CoreControlPlane::new(Arc::clone(&stores)));
let control: Arc<dyn ControlPlane> = plane.clone();
let ctx = ServiceContext::new(
control,
Arc::new(EventBus::new(16)),
ServerConfig::default(),
);
let egress = Arc::new(Counting::default());
ctx.sso.set_egress(egress.clone());
plane
.add_principal("gunnar-ui", "gunnar-ui", PrincipalKind::Machine)
.expect("console");
plane
.grant("gunnar-ui", "", "", GrantLevel::Admin)
.expect("the relay is a server admin");
plane
.add_principal("acme", "acme", PrincipalKind::User)
.expect("an account");
let provider = SsoProvider {
account: "acme".into(),
protocol: SsoProtocol::Oidc as i32,
display_name: "Acme".into(),
oidc: Some(OidcSettings {
issuer: "https://idp.acme.example".into(),
client_id: "c".into(),
..Default::default()
}),
console_origin: "https://console.example".into(),
..Default::default()
};
plane
.sso()
.expect("an identity table")
.set_provider(
provider,
gunnar_grpc::sso::plane::SecretChange::Keep,
"acme",
)
.expect("stored");
let sso = SsoService::new(ctx.clone());
let ask = || {
relay(BeginRequest {
account: "acme".into(),
login_hint: String::new(),
console_origin: "https://console.example".into(),
client: String::new(),
})
};
let refused = sso
.begin(ask())
.await
.expect_err("refused while the switch is off");
assert!(
refused.message().contains("[sso-egress-off]"),
"{refused:?}"
);
assert!(
egress.0.lock().unwrap().is_empty(),
"the egress was asked: {:?}",
egress.0.lock().unwrap()
);
ctx.sso.set_egress_on(true);
let begun = sso.begin(ask()).await;
// With `sso` compiled in, the switch is what stands between the issuer and the network.
#[cfg(feature = "sso")]
{
let _ = begun;
assert!(
!egress.0.lock().unwrap().is_empty(),
"switched on, the sign-in reads the IdP"
);
}
// The DEFAULT build (no `sso`, AIRGAP) links no protocol at all: even switched on, the
// sign-in is refused by name and still nothing is dialled. This half used to assert the
// `sso` build's dial in every build, so a default `cargo test` failed on the one build
// that is right not to dial (gunnar-grpc `src/sso/oidc.rs` `off::begin`).
#[cfg(not(feature = "sso"))]
{
let refused = begun.expect_err("no protocol is compiled into the default build");
assert!(
refused.message().contains("not compiled into this build"),
"{refused:?}"
);
assert!(
egress.0.lock().unwrap().is_empty(),
"the default build asked the egress: {:?}",
egress.0.lock().unwrap()
);
}
}