gunnar-server 3.0.0

The gunnar git server: SSH and smart-HTTP git transports and the gRPC control plane. The appliance half, extracted from gunnar-cli so the server code lives in its own crate.
Documentation
//! ★ UW-5 / UW-4 (5.4, ONE monetize, G1 pull): **"Add users" and queued room on an attached
//! estate.** Monetize never dials an estate, so a paid "Add users" leaves monetize as a signed
//! `add_users` order the main pulls, and room sold past the set as a `grow_room` order. The
//! main grants the users as the seller (the `Accounts.EnrollEmployee` rules, under the order
//! reference) and acks the product's `UsersAnswer` as JSON (monetize-product
//! `pull::order::ADD_USERS`); it keeps a room want and says it to its operator. Its report
//! carries each account's room, so monetize answers "would they fit" without asking.
//!
//! Driven through `gunnar_repair::exec::execute`, the one dispatch a pulled order takes, on the
//! server's own [`ServerEstate`].
#![cfg(feature = "replication")]

use std::sync::Arc;

use gunnar_core::ssh_key::PrivateKey;
use gunnar_core::{Principal, PrincipalKind};
use gunnar_repair::exec::execute;
use gunnar_repair::{Order, OrderAck};
use gunnar_server::control_plane::CoreControlPlane;
use gunnar_server::repair::{Entitlements, ServerEstate};
use gunnar_server::stores::ServerStores;

const GIB: u64 = 1 << 30;

struct Rig {
    _dir: tempfile::TempDir,
    stores: Arc<ServerStores>,
    /// The box's identity core: the stores hold it weakly, the server (here: the rig) owns it.
    _identity: Arc<gunnar_grpc::identity::GunnarIdentity>,
}

impl Rig {
    /// A main that serves, with `acme`'s account (a person and its namespace).
    fn serving() -> Rig {
        let dir = tempfile::tempdir().expect("tempdir");
        let stores = Arc::new(ServerStores::open(dir.path()).expect("stores"));
        stores
            .auth()
            .add_principal(Principal {
                id: "acme".into(),
                name: "acme".into(),
                kind: PrincipalKind::User,
                created: 0,
            })
            .expect("acme");
        stores
            .catalog()
            .create_namespace("acme")
            .expect("namespace");
        // The box's identity core (IDENTITY-HOST): a new named user's first-login link is its.
        let control: Arc<dyn gunnar_grpc::ControlPlane> = Arc::new(
            gunnar_server::control_plane::CoreControlPlane::new(Arc::clone(&stores)),
        );
        let identity = gunnar_server::identity::build(
            dir.path(),
            control,
            &gunnar_grpc::ServerConfig::default(),
            false,
        );
        stores.set_identity(&identity);
        Rig {
            _dir: dir,
            stores,
            _identity: identity,
        }
    }

    fn entitlements(&self) -> Entitlements {
        Entitlements::of(Arc::clone(&self.stores))
            .with_control(Arc::new(CoreControlPlane::new(Arc::clone(&self.stores))))
    }

    fn estate(&self) -> ServerEstate {
        let pem = gunnar_core::mint_ed25519().expect("mint");
        let key = PrivateKey::from_openssh(std::str::from_utf8(&pem).unwrap()).expect("parse");
        ServerEstate {
            key,
            own: "127.0.0.1:1".into(),
            entitlements: self.entitlements(),
        }
    }

    async fn run(&self, seq: u64, order: Order) -> OrderAck {
        execute(&self.estate(), seq, &order, &|| false).await
    }
}

fn add_users(ask: serde_json::Value, order: &str) -> Order {
    Order::AddUsers {
        tenant: "acme".into(),
        ask,
        order: order.into(),
    }
}

#[tokio::test]
async fn a_paid_named_user_is_granted_on_the_account_and_the_ack_is_the_answer() {
    let r = Rig::serving();
    let a = r
        .run(
            1,
            add_users(
                serde_json::json!({"count": 1, "room_bytes": 0, "name": "eve"}),
                "gunnar/acme/users-1",
            ),
        )
        .await;
    assert!(a.ok, "{a:?}");
    let answer: serde_json::Value =
        serde_json::from_str(&a.detail).expect("the UsersAnswer as JSON");
    assert_eq!(answer["principal"], "eve", "{answer}");
    assert_eq!(
        answer["room_bytes"],
        10 * GIB,
        "the product's default room per user: {answer}"
    );
    assert_eq!(answer["unchanged"], false, "{answer}");
    assert!(
        answer["invite_secret"]
            .as_str()
            .is_some_and(|s| s.len() >= 32),
        "a new named user's invite rides the first answer: {answer}"
    );
    assert!(
        r.stores.auth().principal("eve").unwrap().is_some(),
        "eve is made"
    );

    let again = r
        .run(
            2,
            add_users(
                serde_json::json!({"count": 1, "room_bytes": 0, "name": "eve"}),
                "gunnar/acme/users-1",
            ),
        )
        .await;
    assert!(again.ok, "monetize lost the ack and asks again: {again:?}");
    let again: serde_json::Value = serde_json::from_str(&again.detail).unwrap();
    assert_eq!(
        again["unchanged"], true,
        "the retry answers the first grant: {again}"
    );
    assert_eq!(
        again["invite_secret"].as_str().unwrap_or_default(),
        "",
        "no second invite: {again}"
    );
}

#[tokio::test]
async fn seats_are_granted_unnamed_and_a_bad_ask_is_refused_finally() {
    let r = Rig::serving();
    let a = r
        .run(
            1,
            add_users(
                serde_json::json!({"count": 3, "room_bytes": 0}),
                "gunnar/acme/seats-1",
            ),
        )
        .await;
    assert!(a.ok, "{a:?}");
    let answer: serde_json::Value = serde_json::from_str(&a.detail).unwrap();
    assert_eq!(answer["seats"], 3, "{answer}");
    let a = r
        .run(
            2,
            add_users(
                serde_json::json!({"count": 1, "room_bytes": 0, "name": "console-x"}),
                "gunnar/acme/users-2",
            ),
        )
        .await;
    assert!(
        !a.ok && a.detail.contains("console-x"),
        "a reserved name is refused, final: {a:?}"
    );
    let a = r
        .run(
            3,
            add_users(
                serde_json::json!({"count": 1, "room_bytes": GIB, "name": "zed"}),
                "gunnar/acme/users-3",
            ),
        )
        .await;
    assert!(!a.ok, "below the room's floor: {a:?}");
}

#[tokio::test]
async fn room_sold_past_the_set_is_kept_and_said_to_the_operator() {
    let r = Rig::serving();
    let a = r
        .run(
            1,
            Order::GrowRoom {
                need_bytes: 30 * GIB,
                reason: "acme: needs 30 GiB past the 10 the main has".into(),
            },
        )
        .await;
    assert!(a.ok, "{a:?}");
    assert!(
        a.detail.contains("30.0 GiB") || a.detail.contains("32212254720"),
        "{}",
        a.detail
    );
    let alarms = gunnar_server::feed::alarms();
    assert!(
        alarms
            .iter()
            .any(|al| al.event == "grow.room_wanted" && al.detail.contains("acme")),
        "the operator is told on Admin.Health: {alarms:?}"
    );
}

#[tokio::test]
async fn the_mains_report_carries_each_accounts_room_for_add_users() {
    let r = Rig::serving();
    let text = r
        .entitlements()
        .product(1_900_000_000_000)
        .expect("a serving main reports its product");
    let report: serde_json::Value = serde_json::from_str(&text).unwrap();
    let acme = &report["accounts"]["acme"];
    assert_eq!(
        acme["room_bytes"],
        10 * GIB,
        "the room one user holds: {report}"
    );
    assert_eq!(acme["seats"], 0, "{report}");
}