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};
fn fused_refusal(repo: &str) -> String {
format!("repository not found, or you do not have access: {repo}")
}
#[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");
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"
);
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"
);
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"
);
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"
);
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"
);
dispatch
.authorise(&admin, None, b"git-upload-pack 'nordisk/first.git'")
.expect("the created repository serves a clone");
}
#[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'"[..], b"git-receive-pack 'elsewhere/new.git'", ] {
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"
);
}
#[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"
);
}
#[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"
);
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());
}
#[test]
fn an_account_owner_creates_in_its_own_existing_namespace_and_only_there() {
let plane = Plane::empty().with_principal("alice").with_principal("bob");
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());
}
#[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}");
}
}
#[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());
}
#[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}"
);
}
#[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");
}
#[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"
);
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"
);
}
#[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();
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"
);
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}"
);
}
assert!(dispatch
.authorise(&principal("bob"), None, b"git-receive-pack 'alice/new.git'")
.is_err());
}