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