crates/gunnar-server/tests/sso_per_client.rs
//! **A34-6: a few addresses cannot lock an account's company sign-in.** Begins and failed
//! finishes are counted per (account, client) — the client the relay names, an IPv4
//! address or an IPv6 /64 — so one address at its limit is held back and every other
//! address of the same account still signs in.
#![cfg(feature = "sso")]
use std::sync::Arc;
use gunnar_grpc::sso::plane::SecretChange;
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::{Code, Request};
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
}
const ORIGIN: &str = "https://console.example";
/// An account with a SAML provider (a SAML begin dials nothing).
fn rig() -> (tempfile::TempDir, SsoService) {
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(),
);
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::Saml as i32,
display_name: "Acme".into(),
saml: Some(SamlSettings {
idp_entity_id: "https://idp.acme.example/saml".into(),
idp_sso_url: "https://idp.acme.example/sso".into(),
idp_certificates: vec!["MIIB".into()],
..Default::default()
}),
console_origin: ORIGIN.into(),
..Default::default()
};
plane
.sso()
.expect("an identity table")
.set_provider(provider, SecretChange::Clear, "acme")
.expect("stored");
(dir, SsoService::new(ctx))
}
async fn begin(sso: &SsoService, client: &str) -> Result<BeginResponse, tonic::Status> {
sso.begin(relay(BeginRequest {
account: "acme".into(),
console_origin: ORIGIN.into(),
client: client.into(),
..Default::default()
}))
.await
.map(tonic::Response::into_inner)
}
#[tokio::test(flavor = "multi_thread")]
async fn one_address_at_its_limit_is_held_back_and_no_other() {
let (_dir, sso) = rig();
let mut held = None;
for _ in 0..200 {
if let Err(status) = begin(&sso, "203.0.113.7").await {
held = Some(status);
break;
}
}
let held = held.expect("one address begins without end");
assert_eq!(held.code(), Code::ResourceExhausted, "{held:?}");
let other = begin(&sso, "198.51.100.9")
.await
.expect("another address of the account still begins");
assert!(!other.state.is_empty());
assert!(
begin(&sso, "203.0.113.7:5555").await.is_err(),
"the same address on another port is the same client"
);
// Failed finishes hold back the failing client only.
let mut failures_held = None;
for _ in 0..200 {
let flow = begin(&sso, "2001:db8:7:7::1").await.ok();
let Some(flow) = flow else { break };
let finished = sso
.finish(relay(FinishRequest {
state: flow.state,
idp_error: "access_denied".into(),
console_origin: ORIGIN.into(),
client: "2001:db8:7:7::2".into(),
..Default::default()
}))
.await;
let status = finished.expect_err("the IdP said no");
if status.code() == Code::ResourceExhausted {
failures_held = Some(status);
break;
}
}
assert!(
failures_held.is_some(),
"a client failing without end was never held back"
);
let flow = begin(&sso, "198.51.100.9")
.await
.expect("another client begins");
let other = sso
.finish(relay(FinishRequest {
state: flow.state,
idp_error: "access_denied".into(),
console_origin: ORIGIN.into(),
client: "198.51.100.9".into(),
..Default::default()
}))
.await
.expect_err("refused, but on its own merits");
assert_eq!(
other.code(),
Code::PermissionDenied,
"another client was held back by one client's failures: {other:?}"
);
}