gunnar-server 2.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>,
}

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");
        Rig { _dir: dir, stores }
    }

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