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

nordisk/gunnar

crates/gunnar-grpc/tests/identity_conformance.rs

//! **The identity conformance suite against Gunnar** (IDENTITY-HOST): the same
//! cases edda's TestHost passes, over Gunnar's control plane and tables, and
//! the reference transcript, so Gunnar answers as every other product does.

use std::sync::{Arc, Mutex};

use gunnar_grpc::identity::{
    Clock, GunnarHost, GunnarStore, IdentityTables, SERVER_ADMIN, SERVER_WRITE,
};
use gunnar_grpc::MemoryControlPlane;
use nornir_identity::conformance::{self, Harness};
use nornir_identity::test_host::Kit;
use nornir_identity::Site;

struct GunnarHarness;

fn kit_clock(kit: &Kit) -> Clock {
    let clock = Arc::clone(&kit.clock);
    Arc::new(move || clock.now_ms())
}

impl Harness for GunnarHarness {
    type Host = GunnarHost;
    fn host(&self, site: Site, kit: &Kit) -> GunnarHost {
        let plane = Arc::new(MemoryControlPlane::new());
        // Every change is persisted and must read back as what was written.
        let persist: gunnar_grpc::identity::Persist =
            Box::new(|json: &str| GunnarStore::read_tables(json).map(|_| ()));
        let store = GunnarStore::new(
            plane,
            IdentityTables::default(),
            Some(persist),
            kit_clock(kit),
        );
        GunnarHost::new(site, store, kit.mail.clone(), kit_clock(kit))
    }
    fn admin_groups(&self) -> Vec<String> {
        vec![SERVER_ADMIN.to_owned()]
    }
    fn member_groups(&self) -> Vec<String> {
        vec![SERVER_WRITE.to_owned()]
    }
}

nornir_identity::conformance_suite!(GunnarHarness);

/// A session, an invite and a floor survive a restart: the server reads the
/// tables it persisted, over the same control plane.
#[test]
fn the_tables_survive_a_restart() {
    let kit = Kit::default();
    let plane = Arc::new(MemoryControlPlane::new());
    let saved = Arc::new(Mutex::new(String::new()));
    let keep = Arc::clone(&saved);
    let persist: gunnar_grpc::identity::Persist = Box::new(move |json: &str| {
        *keep.lock().unwrap() = json.to_owned();
        Ok(())
    });
    let store = GunnarStore::new(
        plane.clone(),
        IdentityTables::default(),
        Some(persist),
        kit_clock(&kit),
    );
    let id = nornir_identity::Identity::new(
        GunnarHost::new(Site::Private, store, kit.mail.clone(), kit_clock(&kit)),
        conformance::config(),
    );
    let (admin, _) = conformance::first_admin(&GunnarHarness, &id);
    let json = saved.lock().unwrap().clone();
    assert!(
        !json.contains(&admin.token),
        "the token is never persisted, only its hash"
    );
    let tables = GunnarStore::read_tables(&json).unwrap();
    let again = GunnarStore::new(plane, tables, None, kit_clock(&kit));
    let id = nornir_identity::Identity::new(
        GunnarHost::new(Site::Private, again, kit.mail.clone(), kit_clock(&kit)),
        conformance::config(),
    );
    let who = id
        .session(&admin.token)
        .expect("the session after a restart");
    assert!(
        who.admin,
        "the first admin holds Gunnar's server-wide Admin grant"
    );
}

/// ★ **RA5-36: a change whose persist fails changes nothing.** `GunnarStore::change` changed
/// the tables in memory before `identity.json` was written and kept that on a failed write: a
/// logout (or an end-all) answered Err while memory had already dropped the session, and the
/// disk copy brought it back after a restart (a stolen token alive again). Memory and disk
/// now agree: the failed logout ended nothing anywhere, and the person can log out again.
#[test]
fn a_change_whose_persist_fails_changes_nothing() {
    use std::sync::atomic::{AtomicBool, Ordering};
    let kit = Kit::default();
    let plane = Arc::new(MemoryControlPlane::new());
    let saved = Arc::new(Mutex::new(String::new()));
    let fail = Arc::new(AtomicBool::new(false));
    let (keep, failing) = (Arc::clone(&saved), Arc::clone(&fail));
    let persist: gunnar_grpc::identity::Persist = Box::new(move |json: &str| {
        if failing.load(Ordering::SeqCst) {
            return Err("the disk is full".to_owned());
        }
        *keep.lock().unwrap() = json.to_owned();
        Ok(())
    });
    let store = GunnarStore::new(
        plane,
        IdentityTables::default(),
        Some(persist),
        kit_clock(&kit),
    );
    let id = nornir_identity::Identity::new(
        GunnarHost::new(Site::Private, store, kit.mail.clone(), kit_clock(&kit)),
        conformance::config(),
    );
    let (admin, _) = conformance::first_admin(&GunnarHarness, &id);
    fail.store(true, Ordering::SeqCst);
    assert!(
        id.logout(&admin.token, false).is_err(),
        "the logout's write failed"
    );
    let on_disk = GunnarStore::read_tables(&saved.lock().unwrap()).unwrap();
    let in_memory = id.host().gunnar_store().tables();
    assert_eq!(
        in_memory.sessions.len(),
        on_disk.sessions.len(),
        "memory and identity.json agree after a failed write"
    );
    fail.store(false, Ordering::SeqCst);
    id.logout(&admin.token, false).expect("the logout, again");
    assert!(id.session(&admin.token).is_err());
}