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

nordisk/gunnar

crates/gunnar-ssh/tests/first_push.rs

//! **The first-repository door: a push by a principal who may CREATE here
//! creates the repository instead of being refused.**
//!
//! The gap this closes was measured on an installed appliance (KVM,
//! 2026-08-25, 48/48 guests): the bootstrap admin from `--admin-key`
//! authenticates perfectly and can then do *nothing* — the exec surface is
//! three git verbs, gRPC is off by design, there is no shell, and
//! every verb needs a repository that nobody can ever create. `a8ba0665`
//! fixed "authorizes nobody"; this file is the red proof for "can do
//! nothing".
//!
//! The door is deliberately NOT a new verb and NOT a new permission
//! vocabulary. It fires only on `git-receive-pack` to a repository that does
//! not exist, and it asks exactly the two questions the gRPC control plane
//! asks (one implementation, one answer): `Repos.CreateNamespace` requires
//! `may_server(Admin)`, `Repos.Create` requires `may_namespace(ns, Admin)`.
//! Anyone who fails those questions gets what they always got — the one fused
//! sentence that does not reveal whether the repository exists.

mod common;

use gunnar_core::model::Level;
use gunnar_core::{Grant, HashKind, RepoRef, Visibility};
use gunnar_ssh::server::{Dispatch, DispatchError};
use gunnar_ssh::{Access, GitService};

use common::{authorized_keys_line, mint_key, principal, Plane};

/// The sentence the wire says for "does not exist" and "may not see it", from
/// the one place that renders it, so this file cannot drift from the server.
fn fused_refusal(repo: &str) -> String {
    format!("repository not found, or you do not have access: {repo}")
}

/// **The appliance scenario, exactly.** An empty catalog, no principals, and
/// the one identity `--admin-key` installs — then the admin's first push.
#[test]
fn the_bootstrap_admins_first_push_creates_the_namespace_and_the_repository() {
    let plane = Plane::empty();
    let key = mint_key();
    plane
        .auth()
        .bootstrap_admin(&authorized_keys_line(&key))
        .expect("install the bootstrap admin, as --admin-key does");
    let dispatch = Dispatch::new(plane.control.clone());
    let admin = principal("admin");

    // Red half first: nothing exists.
    assert!(
        plane.catalog().namespaces().expect("list").is_empty(),
        "the fixture must start from the appliance's actual state: an empty catalog"
    );

    let request = dispatch
        .authorise(&admin, None, b"git-receive-pack 'nordisk/first.git'")
        .expect("the bootstrap admin's first push must be served, not refused");
    assert_eq!(request.service, GitService::ReceivePack);
    assert_eq!(request.repo.key(), "nordisk/first");
    assert_eq!(
        request.access,
        Access::Admin,
        "the served level is the admin's own grant, not something the door invented"
    );

    // The door is PROSPECTIVE: the request carries the repository it would
    // create, private, and **the catalog is untouched**. A door that wrote the
    // row here was the 2026-08-30 ghost repository: a sha1 client reads
    // `object-format=sha256` in the advertisement, hangs up without a command,
    // and the empty repository it never pushed to answered `repo get`
    // afterwards.
    //
    // **And that sentence was the bug, not just the ghost.** Removing the row
    // stopped the ghost and left the sha1 client hanging up — which is every
    // client this estate has. MEASURED on gunnar.rs, 2026-09-20, seeding a
    // freshly re-imaged forge with the four repositories `estate.toml`
    // declares:
    //
    //     xtask: REFUSED [seed-push] nordisk/gunnar: protocol error:
    //     local repository is Sha1 but the remote is Sha256;
    //     git cannot push between object formats
    //
    // A prospect is inferred before the client has said one word, and git's v1
    // receive-pack advertises the object format first, so there is nothing to
    // infer it from — sha1 is what an un-negotiated client assumes, and so the
    // only prospect that can be right. `NewRepo::new`'s sha256 default is
    // untouched and still the default for `repo create`, where the hash is a
    // decision somebody made rather than one nobody could.
    let path = RepoRef::new("nordisk", "first").expect("legal");
    assert!(request.prospective, "the door must answer with a prospect");
    assert_eq!(
        request.repo.hash_kind,
        HashKind::Sha1,
        "a first-push prospect is sha1, or no sha1 client can ever use this door"
    );
    assert_eq!(request.repo.visibility, Visibility::Private);
    assert!(
        plane
            .catalog()
            .get(&path)
            .expect("read the catalog")
            .is_none(),
        "authorising a push must not write the catalog; the write waits for the client's commands"
    );
    assert!(
        plane.catalog().namespaces().expect("list").is_empty(),
        "nor may the namespace be conjured before the push is real"
    );

    // The transport, once the client has sent commands that pass the hash
    // check, asks for the write. This is the row and the namespace.
    let (created, conjured) = plane
        .control
        .admit_push_create(&admin, &path)
        .expect("admit")
        .expect("the admin's push is admitted");
    assert!(
        conjured,
        "the namespace did not exist, so this call conjured it"
    );
    // The row matches the prospect that was advertised — one hash, decided in
    // one place. A repository created in a format the client was never told
    // about is a repository it cannot push a second time either.
    assert_eq!(created.hash_kind, HashKind::Sha1);
    assert_eq!(created.visibility, Visibility::Private);
    assert_eq!(
        plane
            .catalog()
            .get(&path)
            .expect("read")
            .expect("created")
            .key(),
        "nordisk/first"
    );

    // The second push is the ordinary path — resolve, grant check — and must
    // agree with the first.
    let second = dispatch
        .authorise(&admin, None, b"git-receive-pack 'nordisk/first.git'")
        .expect("the second push goes through the ordinary resolve");
    assert!(
        !second.prospective,
        "the repository exists now; the door is not involved"
    );

    // And what was pushed can be cloned back.
    dispatch
        .authorise(&admin, None, b"git-upload-pack 'nordisk/first.git'")
        .expect("the created repository serves a clone");
}

/// A registered principal with Write somewhere — but no creation rights — is
/// refused a push to a missing repository with EXACTLY the measured sentence,
/// and nothing is created. The door must be invisible to everyone it does not
/// admit: this is the "repository not found" the installed appliance answered
/// on 2026-08-25, and it must not move.
#[test]
fn a_non_admins_push_to_a_missing_repository_is_still_not_found_and_creates_nothing() {
    let plane = Plane::empty()
        .with_the_repo()
        .with_principal("principal-1")
        .with_grant("principal-1", Access::Write);
    let dispatch = Dispatch::new(plane.control.clone());
    let who = principal("principal-1");

    for raw in [
        &b"git-receive-pack 'nordisk/new.git'"[..], // namespace exists
        b"git-receive-pack 'elsewhere/new.git'",    // namespace does not
    ] {
        let err = dispatch
            .authorise(&who, None, raw)
            .expect_err("a non-admin must not create by pushing");
        assert!(
            matches!(&err, DispatchError::NoSuchRepo(_)),
            "the refusal must be the not-found arm, not a new one: {err}"
        );
    }
    let err = dispatch
        .authorise(&who, None, b"git-receive-pack 'nordisk/new.git'")
        .expect_err("still refused");
    assert_eq!(
        err.client_message().expect("a git client gets a sentence"),
        fused_refusal("nordisk/new"),
        "the wire sentence must stay exactly as measured on the appliance"
    );

    assert!(
        plane
            .catalog()
            .get(&RepoRef::new("nordisk", "new").expect("legal"))
            .expect("read")
            .is_none(),
        "the refused push must not have created the repository"
    );
    assert!(
        plane
            .catalog()
            .namespace("elsewhere")
            .expect("read")
            .is_none(),
        "the refused push must not have conjured a namespace"
    );
}

/// Reading never creates, for anyone. A clone (or an LFS download) of a
/// missing repository stays not-found even for the admin: the door is the
/// *write*, because a write is the act that puts content somewhere, and a
/// `git clone` of a typo must stay an error rather than mint an empty
/// repository.
#[test]
fn a_read_of_a_missing_repository_never_creates_even_for_the_admin() {
    let plane = Plane::empty();
    let key = mint_key();
    plane
        .auth()
        .bootstrap_admin(&authorized_keys_line(&key))
        .expect("bootstrap");
    let dispatch = Dispatch::new(plane.control.clone());
    let admin = principal("admin");

    for raw in [
        &b"git-upload-pack 'nordisk/first.git'"[..],
        b"git-lfs-transfer nordisk/first.git download",
    ] {
        let err = dispatch
            .authorise(&admin, None, raw)
            .expect_err("only a write opens the door");
        assert!(
            matches!(&err, DispatchError::NoSuchRepo(_)),
            "expected not-found, got: {err}"
        );
    }
    assert!(
        plane.catalog().namespaces().expect("list").is_empty(),
        "no read may leave a repository behind"
    );
}

/// **An LFS upload opens the same door as the push** (2026-09-28). git-lfs's
/// pre-push hook asks `git-lfs-transfer <repo> upload` before git's
/// receive-pack sends a command; refused as not-found, the hook died
/// (`batch request: exec request failed on channel 0: exit status 255`) and
/// the push never ran. The upload is admitted to exactly whom the push is —
/// a prospective request, nothing written — and refused to everyone else with
/// the same fused not-found, in the same sentence.
#[test]
fn an_lfs_upload_to_a_missing_repository_opens_the_push_door_for_exactly_whom_a_push_does() {
    let plane = Plane::empty();
    let key = mint_key();
    plane
        .auth()
        .bootstrap_admin(&authorized_keys_line(&key))
        .expect("bootstrap");
    let dispatch = Dispatch::new(plane.control.clone());

    let request = dispatch
        .authorise(
            &principal("admin"),
            None,
            b"git-lfs-transfer nordisk/first.git upload",
        )
        .expect("the admin's LFS upload opens the door, as the admin's push does");
    assert!(
        request.prospective,
        "the door is open and nothing is written yet"
    );
    assert_eq!(
        request.repo.hash_kind,
        HashKind::Sha1,
        "the push-create prospect: sha1"
    );
    assert!(
        plane.catalog().namespaces().expect("list").is_empty(),
        "admission writes nothing: the transport creates on the first object"
    );

    // A registered principal with Write somewhere but no create grant:
    // refused exactly as its push would be, in the measured sentence.
    let others = Plane::empty()
        .with_the_repo()
        .with_principal("principal-1")
        .with_grant("principal-1", Access::Write);
    let err = Dispatch::new(others.control.clone())
        .authorise(
            &principal("principal-1"),
            None,
            b"git-lfs-transfer nordisk/new.git upload",
        )
        .expect_err("a principal a push would not admit");
    assert!(
        matches!(&err, DispatchError::NoSuchRepo(_)),
        "the fused not-found: {err}"
    );
    assert_eq!(
        err.client_message().expect("a sentence"),
        fused_refusal("nordisk/new"),
        "the door stays invisible to whom it does not admit"
    );
    assert!(others
        .catalog()
        .get(&RepoRef::new("nordisk", "new").expect("legal"))
        .expect("read")
        .is_none());
}

/// The door asks gRPC's questions, so it admits exactly who `Repos.Create`
/// admits: an account owner in their OWN existing namespace (intrinsic
/// admin), and nobody else's.
#[test]
fn an_account_owner_creates_in_its_own_existing_namespace_and_only_there() {
    let plane = Plane::empty().with_principal("alice").with_principal("bob");
    // The namespaces exist, as `add_principal` on the control plane creates
    // them with the principal.
    plane.catalog().create_namespace("alice").expect("alice");
    plane.catalog().create_namespace("bob").expect("bob");
    let dispatch = Dispatch::new(plane.control.clone());
    let alice = principal("alice");

    dispatch
        .authorise(&alice, None, b"git-receive-pack 'alice/site.git'")
        .expect("an account owner is intrinsically admin of their own namespace");

    for (raw, why) in [
        (
            &b"git-receive-pack 'bob/site.git'"[..],
            "someone else's namespace",
        ),
        (
            b"git-receive-pack 'carol/site.git'",
            "a namespace that does not exist needs a server admin",
        ),
    ] {
        let err = dispatch.authorise(&alice, None, raw).expect_err(why);
        assert!(matches!(&err, DispatchError::NoSuchRepo(_)), "{why}: {err}");
    }
    assert!(plane.catalog().namespace("carol").expect("read").is_none());
}

/// A namespace-scoped Admin grant opens the door inside that namespace,
/// exactly (no inheritance into nested namespaces), and a namespace
/// Write grant does not open it at all — `Repos.Create` requires Admin, and
/// the door must require what the RPC requires or the two doors disagree.
#[test]
fn a_namespace_admin_creates_inside_the_namespace_and_write_does_not() {
    let plane = Plane::empty()
        .with_principal("lead")
        .with_principal("dev")
        .with_namespace_grant("lead", "team", Level::Admin)
        .with_namespace_grant("dev", "team", Level::Write);
    plane.catalog().create_namespace("team").expect("team");
    let dispatch = Dispatch::new(plane.control.clone());

    dispatch
        .authorise(&principal("lead"), None, b"git-receive-pack 'team/new.git'")
        .expect("a namespace admin may create inside the namespace");

    for (who, raw, why) in [
        (
            "lead",
            &b"git-receive-pack 'team/sub/deep.git'"[..],
            "a grant on `team` does not reach `team/sub`",
        ),
        (
            "lead",
            b"git-receive-pack 'other/new.git'",
            "a namespace admin has nothing outside the namespace",
        ),
        (
            "dev",
            b"git-receive-pack 'team/also.git'",
            "namespace Write is not creation; Repos.Create requires Admin",
        ),
    ] {
        let err = dispatch
            .authorise(&principal(who), None, raw)
            .expect_err(why);
        assert!(matches!(&err, DispatchError::NoSuchRepo(_)), "{why}: {err}");
    }
}

/// Anonymous never creates: the predicates refuse the anonymous
/// principal by construction, so the door cannot be opened by a keyless
/// caller no matter what the grant table holds.
#[test]
fn anonymous_never_creates() {
    let plane = Plane::empty();
    let dispatch = Dispatch::new(plane.control.clone());
    let err = dispatch
        .authorise(
            &gunnar_core::Principal::anonymous(),
            None,
            b"git-receive-pack 'nordisk/first.git'",
        )
        .expect_err("anonymous must not create");
    assert!(matches!(&err, DispatchError::NoSuchRepo(_)), "{err}");
    assert!(plane.catalog().namespaces().expect("list").is_empty());
}

/// A storage fault while the door decides is a FAULT, not a refusal — the
/// same rule `DispatchError::Store` exists for. An identity table that cannot
/// be read must never come back as "repository not found": that would send
/// the admin chasing permissions while the control plane is broken.
#[test]
fn a_broken_identity_table_is_a_fault_not_a_not_found() {
    let plane = Plane::empty();
    let key = mint_key();
    plane
        .auth()
        .bootstrap_admin(&authorized_keys_line(&key))
        .expect("bootstrap");
    plane.corrupt_the_auth_table();
    let dispatch = Dispatch::new(plane.control.clone());

    let err = dispatch
        .authorise(
            &principal("admin"),
            None,
            b"git-receive-pack 'nordisk/first.git'",
        )
        .expect_err("a corrupt table cannot authorise anything");
    assert!(
        matches!(&err, DispatchError::Store(_)),
        "a storage fault must surface as one: {err}"
    );
}

/// The server-scoped Admin grant — the row bootstrap writes, and the row
/// `Access.Grant --server … admin` writes for a SECOND admin — is what opens
/// the door, not the literal principal id "admin". A later gunnar-admin
/// enrolled by the first one holds the same power through the same row.
#[test]
fn any_server_admin_grant_opens_the_door_not_the_name_admin() {
    let plane = Plane::empty().with_principal("second-admin");
    plane
        .auth()
        .grant(Grant::server("second-admin", Level::Admin))
        .expect("grant server admin");
    let dispatch = Dispatch::new(plane.control.clone());

    dispatch
        .authorise(
            &principal("second-admin"),
            None,
            b"git-receive-pack 'nordisk/first.git'",
        )
        .expect("a server Admin grant is the credential, whatever the id");
}

/// **A refused create leaves no trace** — the write side of the door undone.
/// The transport calls this when the store could not be made after the row
/// was written; afterwards nothing answers `repo get`, and a namespace that
/// this push conjured is gone with it.
#[test]
fn a_retracted_push_create_leaves_neither_row_nor_conjured_namespace() {
    let plane = Plane::empty();
    let key = mint_key();
    plane
        .auth()
        .bootstrap_admin(&authorized_keys_line(&key))
        .expect("install the bootstrap admin");
    let admin = principal("admin");
    let path = RepoRef::new("ghost", "x").expect("legal");

    let (_, conjured) = plane
        .control
        .admit_push_create(&admin, &path)
        .expect("admit")
        .expect("admitted");
    assert!(conjured);
    plane
        .control
        .retract_push_create(&path, conjured)
        .expect("retract");
    assert!(
        plane.catalog().get(&path).expect("read").is_none(),
        "the row is still there"
    );
    assert!(
        plane.catalog().namespace("ghost").expect("read").is_none(),
        "the conjured namespace is still there"
    );

    // A namespace that existed before the push is NOT taken away by a
    // retraction: it was not this push's to take.
    plane.catalog().create_namespace("kept").expect("namespace");
    let path = RepoRef::new("kept", "x").expect("legal");
    let (_, conjured) = plane
        .control
        .admit_push_create(&admin, &path)
        .expect("admit")
        .expect("admitted");
    assert!(!conjured);
    plane
        .control
        .retract_push_create(&path, conjured)
        .expect("retract");
    assert!(plane.catalog().get(&path).expect("read").is_none());
    assert!(
        plane.catalog().namespace("kept").expect("read").is_some(),
        "kept must be kept"
    );
}

/// ★ **A clan's member makes a PRIVATE repository in the clan by pushing it**
/// (clans 2026-10-05: members Write and create private repositories; cov-04,
/// 2026-10-06: on an installed estate a member has the forge's SSH door and
/// the console, and the first push is how a git user makes a repository). The
/// same rule `Repos.Create` keeps: the clan's member creates PRIVATE ones with
/// the Write its membership gives; its admin, as an account admin, anything;
/// an outsider nothing, told what a stranger is told; an INVITED account that
/// has not accepted is an outsider.
#[test]
fn a_clan_member_creates_a_private_repository_in_the_clan_by_pushing_it() {
    let plane = Plane::empty()
        .with_principal("alice")
        .with_principal("bob")
        .with_principal("carol")
        .with_principal("dave");
    let open = gunnar_core::AccountRoom {
        cap: None,
        stored: 0,
    };
    let auth = plane.auth();
    plane
        .catalog()
        .create_namespace("fellows")
        .expect("the clan's namespace");
    auth.create_clan("fellows", "alice", 0, open)
        .expect("alice makes the clan");
    auth.invite_to_clan("fellows", "alice", "bob")
        .expect("invite bob");
    auth.answer_clan_invite("fellows", "bob", true, 0, open)
        .expect("bob joins");
    auth.invite_to_clan("fellows", "alice", "dave")
        .expect("invite dave (he does not answer)");
    let dispatch = Dispatch::new(plane.control.clone());
    let path = RepoRef::new("fellows", "app").unwrap();

    // bob, a member: the prospect is served and the push admitted — private.
    let request = dispatch
        .authorise(
            &principal("bob"),
            None,
            b"git-receive-pack 'fellows/app.git'",
        )
        .expect("a member's first push into the clan is served");
    assert_eq!(request.service, GitService::ReceivePack);
    let (created, conjured) = plane
        .control
        .admit_push_create(&principal("bob"), &path)
        .expect("admit")
        .expect("the member's first push is admitted");
    assert!(!conjured, "the clan's namespace already existed");
    assert_eq!(
        created.visibility,
        Visibility::Private,
        "a member's new clan repository is private"
    );

    // carol (an outsider) and dave (invited, not a member): the stranger's sentence.
    for who in ["carol", "dave"] {
        let err = dispatch
            .authorise(
                &principal(who),
                None,
                b"git-receive-pack 'fellows/other.git'",
            )
            .expect_err("an outsider makes nothing in the clan");
        assert_eq!(
            err.client_message(),
            Some(fused_refusal("fellows/other")),
            "{who}"
        );
        assert!(
            plane
                .control
                .admit_push_create(&principal(who), &RepoRef::new("fellows", "other").unwrap())
                .expect("admit")
                .is_none(),
            "{who}"
        );
    }
    // A member's Write reaches no other namespace's door.
    assert!(dispatch
        .authorise(&principal("bob"), None, b"git-receive-pack 'alice/new.git'")
        .is_err());
}