Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/gunnar

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