crates/gunnar-cli/tests/anonymous_access.rs
#![cfg(feature = "server")]
//! **`Repo.visibility` made to mean something: a stranger clones a public
//! repository, over real SSH, with stock `git` and no key at all**.
//!
//! Before this, every operation gunnar served required a registered key.
//! `Repo.visibility` was in the model and nothing honoured it, so "public"
//! meant "readable by anyone who already has an account here". That is the
//! difference between a git host for people with accounts and a place you can
//! publish something.
//!
//! ```text
//! git clone ssh://anonymous@host/ns/repo.git no -i, no agent, no key
//! └─ russh auth_none ── ANONYMOUS_SSH_USER ──▶ Principal::anonymous()
//! └─ exec ── sandbox ──▶ RepoCatalog
//! └─ AuthTable::may(anonymous, repo, Read) ← the ONE ACL
//! └─ Visibility::Public ? serve : refuse-as-if-absent
//! ```
//!
//! # Why these tests and not unit tests of the predicate
//!
//! An authorisation rule that has only ever been unit-tested has never been
//! asked the question by a real client. `gunnar-core`'s `tests/acl.rs` pins the
//! rule and `gunnar-ssh`'s `tests/anonymous.rs` pins the handshake; this file
//! is the only place where **stock `git`**, which nobody here wrote, is the one
//! deciding whether gunnar is a git server a stranger can use.
//!
//! # Measure, never assert, and the bar is higher: this is authorisation code
//!
//! Every claim below is written with its opposite beside it, and the two
//! constants that could hide a dead check are proved dead-or-alive by
//! `breaking_the_visibility_check_in_either_direction_turns_tests_red`, which
//! is not a comment: it drives the real `AuthTable` with visibility pinned to
//! `Public` and then to `Private` and asserts that each pin breaks the half of
//! the suite it should. Neither constant survives.
mod common;
use common::{local_repo, tools_present, Harness, HarnessOptions};
use gunnar_core::{
AuthTable, Grant, HashKind, Level, NewRepo, Principal, PrincipalKind, RepoRef, Visibility,
ANONYMOUS_PRINCIPAL_ID,
};
/// A server with `team/project` **public** and the anonymous door open. This is
/// the shipped default configuration with one repository published.
fn published() -> Harness {
Harness::start_with_options(
"team",
"project",
HashKind::Sha1,
HarnessOptions {
visibility: Visibility::Public,
..HarnessOptions::default()
},
)
}
/// Seed the repository with one commit through the harness's *authenticated*
/// key, so what a stranger reads back was demonstrably put there by somebody
/// who was allowed to.
fn seed(harness: &Harness, content: &str) -> String {
let work = harness.dir.path().join("work");
let head = local_repo(harness, &work, HashKind::Sha1, content);
harness
.git(
&work,
&[
"push",
&harness.url("team", "project"),
"HEAD:refs/heads/main",
],
)
.expect_ok("the seeding push");
head
}
/// What `refs/heads/main` points at on the server right now, or `None`.
///
/// Applied output for the push tests: an anonymous push that was refused must
/// leave the ref where it was, and "the command failed" is not the same claim.
fn stored_head(harness: &Harness) -> Option<String> {
let repo = harness
.stores
.catalog()
.open(&RepoRef::new("team", "project").unwrap())
.unwrap();
let store = harness.stores.repo_store(&repo).expect("open");
// `RefStore::iter` + `Target::Object` became `GitOps::refs` + `RefRow::oid`
// on 2026-08-10. A symbolic row carries `oid: None`, so the `_ => None` arm
// that used to refuse a `Target::Symbolic` is the `and_then` here — same
// answer, and still not a hex string invented from a symref's target.
let rows = gunnar_store::git_storage_trait::GitOps::refs(store.serve()).expect("read the refs");
rows.iter()
.find(|row| row.name == "refs/heads/main")
.and_then(|row| row.oid.as_ref())
.map(|raw| {
gix_hash::ObjectId::try_from(raw.as_slice())
.expect("a well-sized oid")
.to_string()
})
}
// ───────────────────────────────────────────────────────────────────────────
// The prize
// ───────────────────────────────────────────────────────────────────────────
/// **A stranger clones a public repository.** No key, no account, stock `git`.
///
/// Asserted on the **working tree on disk**, not on an exit code: a clone that
/// "succeeded" into an empty repository exits zero and is indistinguishable
/// from this one by status alone.
#[test]
fn an_anonymous_caller_clones_a_public_repository() {
assert!(
tools_present(),
"this test requires `git` and `ssh` on PATH"
);
let harness = published();
let content = "published to the world\n";
let head = seed(&harness, content);
let dest = harness.dir.path().join("stranger-clone");
let out = harness.git_anonymous(
harness.dir.path(),
&[
"clone",
"--quiet",
&harness.anonymous_url("team", "project"),
dest.to_str().unwrap(),
],
);
out.expect_ok("a stranger with no key must be able to clone a PUBLIC repository");
let cloned = std::fs::read_to_string(dest.join("hello.txt"))
.expect("the anonymous clone has a working tree in it");
assert_eq!(cloned, content, "the anonymous clone got the wrong bytes");
let rev = harness.git(&dest, &["rev-parse", "HEAD"]);
rev.expect_ok("git rev-parse in the anonymous clone");
assert_eq!(
rev.stdout.trim(),
head,
"the anonymous clone is not at the commit that was pushed"
);
// The server counted it as a stranger, not as a key that authenticated.
let metrics = harness.server.as_ref().expect("running").metrics();
assert!(
metrics.connections_anonymous >= 1,
"the anonymous clone was not counted as anonymous: {metrics:?}"
);
}
/// **The red half of the same repository.** Made private, the identical clone
/// is refused, so the green above came from `visibility` and not from a server
/// that serves everybody.
#[test]
fn the_same_repository_made_private_refuses_the_same_anonymous_clone() {
assert!(
tools_present(),
"this test requires `git` and `ssh` on PATH"
);
let harness = published();
seed(&harness, "published, then withdrawn\n");
let url = harness.anonymous_url("team", "project");
let allowed = harness.git_anonymous(
harness.dir.path(),
&[
"clone",
"--quiet",
&url,
harness.dir.path().join("while-public").to_str().unwrap(),
],
);
allowed.expect_ok("while public");
harness.set_visibility("team", "project", Visibility::Private);
let refused = harness.git_anonymous(
harness.dir.path(),
&[
"clone",
"--quiet",
&url,
harness.dir.path().join("while-private").to_str().unwrap(),
],
);
refused.expect_failure("an anonymous clone of a PRIVATE repository");
assert!(
refused
.stderr
.contains("repository not found, or you do not have access"),
"the refusal must be gunnar's, and must not say the repository exists: {}",
refused.stderr
);
assert!(
!harness.dir.path().join("while-private").exists()
|| std::fs::read_dir(harness.dir.path().join("while-private"))
.map(|mut d| d.next().is_none())
.unwrap_or(true),
"a refused clone left a populated directory behind"
);
}
/// **Read only.** A stranger cannot push to a public repository, and the ref on
/// the server is asserted **unchanged** afterwards. An exit code is a claim
/// about `git`, not about the server.
#[test]
fn an_anonymous_caller_cannot_push_to_a_public_repository() {
assert!(
tools_present(),
"this test requires `git` and `ssh` on PATH"
);
let harness = published();
let head = seed(&harness, "one\n");
assert_eq!(stored_head(&harness).as_deref(), Some(head.as_str()));
// Clone it as a stranger — which works — then try to push back.
let dest = harness.dir.path().join("stranger");
harness
.git_anonymous(
harness.dir.path(),
&[
"clone",
"--quiet",
&harness.anonymous_url("team", "project"),
dest.to_str().unwrap(),
],
)
.expect_ok("the anonymous clone");
std::fs::write(dest.join("hello.txt"), "vandalised\n").unwrap();
harness
.git_anonymous(&dest, &["add", "hello.txt"])
.expect_ok("add");
harness
.git_anonymous(
&dest,
&[
"-c",
"user.name=nobody",
"-c",
"user.email=nobody@example.invalid",
"commit",
"--quiet",
"-m",
"vandalism",
],
)
.expect_ok("commit");
let out = harness.git_anonymous(&dest, &["push", "origin", "HEAD:refs/heads/main"]);
out.expect_failure("an ANONYMOUS PUSH to a public repository");
assert!(
out.stderr
.contains("repository not found, or you do not have access"),
"the refusal must be gunnar's grant check, not a transport accident: {}",
out.stderr
);
// …and a force push, and a push to a brand-new ref, and a delete. A gate
// that only refused fast-forwards onto an existing branch would be green
// above and open below.
for args in [
&["push", "--force", "origin", "HEAD:refs/heads/main"][..],
&["push", "origin", "HEAD:refs/heads/anonymous-branch"],
&["push", "origin", ":refs/heads/main"],
] {
harness
.git_anonymous(&dest, args)
.expect_failure(&format!("anonymous `git {}`", args.join(" ")));
}
// The applied output: nothing moved.
assert_eq!(
stored_head(&harness).as_deref(),
Some(head.as_str()),
"an anonymous push CHANGED a ref on the server"
);
let refs = {
let repo = harness
.stores
.catalog()
.open(&RepoRef::new("team", "project").unwrap())
.unwrap();
let store = harness.stores.repo_store(&repo).expect("open");
gunnar_store::git_storage_trait::GitOps::refs(store.serve()).expect("read")
};
assert!(
!refs.iter().any(|row| row.name.contains("anonymous-branch")),
"an anonymous push CREATED a ref: {refs:?}"
);
}
/// **A private repository and one that does not exist refuse byte for byte.**
///
/// One name, one server, two states. A comparison across two different names
/// cannot be byte-for-byte, because the pkt-line length prefix follows the name
/// the client itself chose (measured: `0046` against `0047`). Blanking enough
/// of the answer to make two names comparable would blank the thing being
/// measured.
///
/// The third probe is the red half: published, the identical command succeeds.
#[test]
fn a_private_repository_and_an_absent_one_refuse_identically() {
assert!(
tools_present(),
"this test requires `git` and `ssh` on PATH"
);
let harness = published();
let url = harness.anonymous_url("team", "hidden");
let clone = |into: &str| {
harness.git_anonymous(
harness.dir.path(),
&[
"clone",
"--quiet",
&url,
harness.dir.path().join(into).to_str().unwrap(),
],
)
};
// 1. It does not exist.
let absent = clone("absent");
absent.expect_failure("cloning a repository that does not exist");
// 2. It exists, and is private.
harness
.stores
.catalog()
.create(
NewRepo::new(RepoRef::new("team", "hidden").unwrap())
.hash_kind(HashKind::Sha1)
.visibility(Visibility::Private),
)
.expect("create it private");
let private = clone("private");
private.expect_failure("cloning a private repository anonymously");
assert_eq!(
(private.status, private.stderr.as_str()),
(absent.status, absent.stderr.as_str()),
"a private repository answers a stranger differently from an absent one under the \
identical command. That difference is an oracle: every private repository on the \
server can be found by guessing names."
);
assert!(
private
.stderr
.contains("repository not found, or you do not have access"),
"both must be refusals, not two equal successes: {}",
private.stderr
);
// 3. Published, the same command works, so the two equal answers above are
// not simply "gunnar refuses everything".
harness.set_visibility("team", "hidden", Visibility::Public);
harness
.stores
.repo_store(
&harness
.stores
.catalog()
.open(&RepoRef::new("team", "hidden").unwrap())
.unwrap(),
)
.expect("materialise it on disk");
clone("published").expect_ok(
"the identical anonymous clone must succeed once the repository is public, or the \
indistinguishability above means nothing",
);
}
// ───────────────────────────────────────────────────────────────────────────
// No regression for the caller that does have a key
// ───────────────────────────────────────────────────────────────────────────
/// **The regression guard.** A principal with a `Read` grant is unaffected: it
/// clones (public *and* private), it still cannot push, and it is served as
/// **itself** rather than falling through to the anonymous identity.
#[test]
fn an_authenticated_read_grant_is_unaffected() {
assert!(
tools_present(),
"this test requires `git` and `ssh` on PATH"
);
let harness = published();
let content = "authenticated readers still work\n";
seed(&harness, content);
// A second, private repository only a grant can reach.
harness
.stores
.catalog()
.create(
NewRepo::new(RepoRef::new("team", "inner").unwrap())
.hash_kind(HashKind::Sha1)
.visibility(Visibility::Private),
)
.expect("create");
harness
.stores
.repo_store(
&harness
.stores
.catalog()
.open(&RepoRef::new("team", "inner").unwrap())
.unwrap(),
)
.expect("materialise");
// A reader with its own key and a Read grant on both.
let private = gunnar_core::mint_ed25519().expect("mint");
let parsed = gunnar_core::ssh_key::PrivateKey::from_openssh(&private).expect("parse");
let public = parsed.public_key().to_openssh().expect("render");
let key_path = harness.dir.path().join("id_reader");
std::fs::write(&key_path, &private).expect("write");
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&key_path, std::fs::Permissions::from_mode(0o600)).unwrap();
}
harness
.stores
.auth()
.add_principal(Principal::new("reader", "reader", PrincipalKind::User).unwrap())
.expect("add");
harness
.stores
.auth()
.register_key("reader", &public, None)
.expect("register");
for repo in ["project", "inner"] {
harness
.stores
.auth()
.grant(Grant::repo(
"reader",
RepoRef::new("team", repo).unwrap(),
Level::Read,
))
.expect("grant read");
}
let ssh = format!(
"ssh -i {} -o IdentitiesOnly=yes -o IdentityAgent=none -o StrictHostKeyChecking=no \
-o UserKnownHostsFile=/dev/null -o BatchMode=yes -o LogLevel=ERROR",
key_path.display()
);
// It clones the public one…
let dest = harness.dir.path().join("reader-public");
harness
.git_with_ssh(
harness.dir.path(),
&ssh,
&[
"clone",
"--quiet",
&harness.url("team", "project"),
dest.to_str().unwrap(),
],
)
.expect_ok("a Read grant still clones a public repository");
assert_eq!(
std::fs::read_to_string(dest.join("hello.txt")).expect("worktree"),
content
);
// …and it reaches the PRIVATE one, which no stranger can. This is the half
// that would break if the publickey path had been folded into the anonymous
// one.
let inner = harness.dir.path().join("reader-inner");
harness
.git_with_ssh(
harness.dir.path(),
&ssh,
&[
"clone",
"--quiet",
&harness.url("team", "inner"),
inner.to_str().unwrap(),
],
)
.expect_ok("a Read grant must still reach a PRIVATE repository");
assert!(
harness
.git_anonymous(
harness.dir.path(),
&[
"clone",
"--quiet",
&harness.anonymous_url("team", "inner"),
harness.dir.path().join("stranger-inner").to_str().unwrap(),
],
)
.status
!= 0,
"a stranger reached the private repository the reader's grant is for"
);
// …and Read is still not Write.
std::fs::write(dest.join("more.txt"), "more\n").unwrap();
harness.git_with_ssh(&dest, &ssh, &["add", "more.txt"]);
harness.git_with_ssh(
&dest,
&ssh,
&[
"-c",
"user.name=r",
"-c",
"user.email=r@x",
"commit",
"--quiet",
"-m",
"more",
],
);
harness
.git_with_ssh(&dest, &ssh, &["push", "origin", "HEAD:refs/heads/main"])
.expect_failure("a Read-only principal must still be refused a push");
}
/// An unregistered **key** is still refused, with the anonymous door open
/// beside it. Offering a key is a claim to be somebody; "no key" and "a key
/// nobody knows" must not have collapsed into one another.
#[test]
fn an_unregistered_key_is_still_refused_even_for_a_public_repository() {
assert!(
tools_present(),
"this test requires `git` and `ssh` on PATH"
);
let harness = published();
seed(&harness, "public\n");
let private = gunnar_core::mint_ed25519().expect("mint");
let key_path = harness.dir.path().join("id_stranger");
std::fs::write(&key_path, &private).expect("write");
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&key_path, std::fs::Permissions::from_mode(0o600)).unwrap();
}
let ssh = format!(
"ssh -i {} -o IdentitiesOnly=yes -o IdentityAgent=none -o StrictHostKeyChecking=no \
-o UserKnownHostsFile=/dev/null -o BatchMode=yes -o LogLevel=ERROR \
-o PreferredAuthentications=publickey",
key_path.display()
);
harness
.git_with_ssh(
harness.dir.path(),
&ssh,
&[
"clone",
"--quiet",
&harness.url("team", "project"),
harness.dir.path().join("bad-key").to_str().unwrap(),
],
)
.expect_failure("an unregistered key must be refused even on a public repository");
// The red half: the same repository, no key at all, is served.
harness
.git_anonymous(
harness.dir.path(),
&[
"clone",
"--quiet",
&harness.anonymous_url("team", "project"),
harness.dir.path().join("no-key").to_str().unwrap(),
],
)
.expect_ok("no key at all is admitted; a key nobody registered is not");
}
// ───────────────────────────────────────────────────────────────────────────
// The switch, and the control plane
// ───────────────────────────────────────────────────────────────────────────
/// `--no-anonymous` closes the door at the listener, and the same server with
/// it open is the red half.
#[test]
fn no_anonymous_refuses_a_stranger_even_on_a_public_repository() {
assert!(
tools_present(),
"this test requires `git` and `ssh` on PATH"
);
let closed = Harness::start_with_options(
"team",
"project",
HashKind::Sha1,
HarnessOptions {
visibility: Visibility::Public,
allow_anonymous: false,
..HarnessOptions::default()
},
);
seed(&closed, "not for strangers\n");
closed
.git_anonymous(
closed.dir.path(),
&[
"clone",
"--quiet",
&closed.anonymous_url("team", "project"),
closed.dir.path().join("nope").to_str().unwrap(),
],
)
.expect_failure("`allow_anonymous: false` must refuse a stranger on a PUBLIC repository");
// …and the key still works on that same server: one door closed, not the
// building.
closed
.git(
closed.dir.path(),
&[
"clone",
"--quiet",
&closed.url("team", "project"),
closed.dir.path().join("keyed").to_str().unwrap(),
],
)
.expect_ok("a registered key must still work with anonymous access off");
// The same configuration with the switch ON serves the stranger.
let open = published();
seed(&open, "for strangers\n");
open.git_anonymous(
open.dir.path(),
&[
"clone",
"--quiet",
&open.anonymous_url("team", "project"),
open.dir.path().join("yes").to_str().unwrap(),
],
)
.expect_ok("the same configuration with the switch on must serve the stranger");
}
/// **An anonymous caller reaches no gRPC control-plane call, by any route.**
///
/// Three independent statements, because one alone would be a coincidence:
///
/// 1. the control plane refuses a call with no credential at all, and there is
/// no credential a stranger could produce, because signing one needs a
/// private key whose public half is in the identity table;
/// 2. the anonymous principal cannot *get* into that table, so no key can ever
/// be registered to it. Asserted here against the same live `AuthTable` the
/// running server authenticates against;
/// 3. `gunnar-grpc`'s copy of the reserved id is the same string as
/// `gunnar-core`'s. `gunnar-grpc` deliberately does not link `gunnar-core`;
/// this crate links both, so this is where the copy is stopped from
/// drifting. `gunnar-grpc`'s `tests/auth.rs` proves the gate *refuses* that
/// id; this proves it is refusing the right one.
#[test]
fn an_anonymous_caller_reaches_no_control_plane_call() {
assert!(
tools_present(),
"this test requires `git` and `ssh` on PATH"
);
// (3) the drift guard, first because it costs nothing.
assert_eq!(
gunnar_grpc::ANONYMOUS_PRINCIPAL_ID,
ANONYMOUS_PRINCIPAL_ID,
"`gunnar-grpc` is refusing a different string from the one `gunnar-core` reserves, so \
the control plane's anonymous guard is watching a name nobody uses"
);
let harness = Harness::start_with_grpc("team", "project", HashKind::Sha1);
harness.set_visibility("team", "project", Visibility::Public);
seed(&harness, "public\n");
// The stranger can clone, so the anonymous path really is open on this
// server, and everything below is about a door that is not.
harness
.git_anonymous(
harness.dir.path(),
&[
"clone",
"--quiet",
&harness.anonymous_url("team", "project"),
harness.dir.path().join("stranger").to_str().unwrap(),
],
)
.expect_ok("the anonymous git path is open on this server");
// (2) the anonymous principal cannot be given an identity on the LIVE
// table this server authenticates against.
let auth: &AuthTable = harness.stores.auth();
assert!(auth.add_principal(Principal::anonymous()).is_err());
assert!(auth
.register_key(ANONYMOUS_PRINCIPAL_ID, &harness_public_key(&harness), None)
.is_err());
assert!(auth
.principal(ANONYMOUS_PRINCIPAL_ID)
.expect("read")
.is_none());
assert!(!auth
.may_server(ANONYMOUS_PRINCIPAL_ID, Level::Read)
.expect("acl"));
// (1) every RPC, with no credential.
let channel = harness
.runtime
.block_on(
tonic::transport::Endpoint::from_shared(harness.grpc_url())
.unwrap()
.connect(),
)
.expect("connect to the control plane");
let mut repos = gunnar_proto::repos_client::ReposClient::new(channel.clone());
let mut refs = gunnar_proto::refs_client::RefsClient::new(channel.clone());
let mut admin = gunnar_proto::admin_client::AdminClient::new(channel.clone());
let mut access = gunnar_proto::access_client::AccessClient::new(channel);
let path = Some(gunnar_proto::RepoPath {
namespace: "team".into(),
repo: "project".into(),
});
let refused: Vec<(&str, tonic::Code)> = harness.runtime.block_on(async {
vec![
(
"Repos.List",
repos
.list(tonic::Request::new(
gunnar_proto::ListReposRequest::default(),
))
.await
.err()
.map(|s| s.code())
.unwrap_or(tonic::Code::Ok),
),
(
"Repos.Get",
repos
.get(tonic::Request::new(gunnar_proto::GetRepoRequest {
path: path.clone(),
}))
.await
.err()
.map(|s| s.code())
.unwrap_or(tonic::Code::Ok),
),
(
"Refs.List",
refs.list(tonic::Request::new(gunnar_proto::ListRefsRequest {
repo: path.clone(),
..Default::default()
}))
.await
.err()
.map(|s| s.code())
.unwrap_or(tonic::Code::Ok),
),
(
"Admin.Health",
admin
.health(tonic::Request::new(gunnar_proto::HealthRequest::default()))
.await
.err()
.map(|s| s.code())
.unwrap_or(tonic::Code::Ok),
),
(
"Access.ListKeys",
access
.list_keys(tonic::Request::new(gunnar_proto::ListKeysRequest::default()))
.await
.err()
.map(|s| s.code())
.unwrap_or(tonic::Code::Ok),
),
]
});
for (method, code) in &refused {
assert_eq!(
*code,
tonic::Code::Unauthenticated,
"{method} answered a caller with no credential with {code:?}. A stranger who can \
clone this server's public repository must reach NO control-plane call, and \
`Repos.Get`/`Refs.List` on a public repository is exactly the tempting exception."
);
}
// The red half of the loop: the SAME calls with the harness's real
// credential are served. Without this, a control plane that was simply down
// would satisfy every assertion above.
harness.runtime.block_on(async {
let mut repos = gunnar_proto::repos_client::ReposClient::new(
tonic::transport::Endpoint::from_shared(harness.grpc_url())
.unwrap()
.connect()
.await
.expect("connect"),
);
repos
.get(harness.authed("Repos.Get", gunnar_proto::GetRepoRequest { path }))
.await
.expect("an authenticated caller with a server-wide Read must be served");
});
}
/// Read the harness principal's registered `authorized_keys` line back out, so
/// the "cannot register a key to anonymous" assertion uses a key that is
/// genuinely well formed. A malformed one would be refused for the wrong
/// reason, and the guard would be blind.
fn harness_public_key(harness: &Harness) -> String {
harness
.stores
.auth()
.keys_of("tester")
.expect("read")
.first()
.expect("the harness key")
.openssh
.clone()
}
// ───────────────────────────────────────────────────────────────────────────
// Measure, never assert: neither constant survives
// ───────────────────────────────────────────────────────────────────────────
/// **The guard on the guards.** Pin the visibility check to one answer and then
/// to the other, and assert that each pin breaks the half of this file it
/// should.
///
/// This is the identity-value trap in its exact form: `may` reads
/// `repo.visibility == Public`. A version that always said `Public` serves
/// every private repository to the internet and passes every *positive* test
/// above; a version that never said `Public` serves nobody and passes every
/// *negative* one. Only running both breaks tells them apart.
///
/// The break is applied to the **repository record the ACL reads**, not to a
/// copy of the predicate, because a rewritten predicate here would be a
/// different function from the one that ships.
#[test]
fn breaking_the_visibility_check_in_either_direction_turns_tests_red() {
let store = gunnar_core::memory_control_store().expect("control store");
let auth = AuthTable::new(std::sync::Arc::clone(&store)).expect("table");
let catalog = gunnar_core::RepoCatalog::new(store).expect("catalog");
catalog.create_namespace("team").expect("namespace");
let open = catalog
.create(NewRepo::new(RepoRef::new("team", "open").unwrap()).visibility(Visibility::Public))
.expect("create");
let shut = catalog
.create(NewRepo::new(RepoRef::new("team", "shut").unwrap()).visibility(Visibility::Private))
.expect("create");
// The shipped behaviour, both halves. This is the pair the two breaks below
// each destroy one of.
assert!(auth
.may(ANONYMOUS_PRINCIPAL_ID, &open, Level::Read)
.expect("acl"));
assert!(!auth
.may(ANONYMOUS_PRINCIPAL_ID, &shut, Level::Read)
.expect("acl"));
// ── break 1: "everything is public" ────────────────────────────────────
//
// The private repository is now readable by a stranger. Every positive test
// in this file stays green; this is the assertion that goes red.
let always_public = Repo {
visibility: Visibility::Public,
..shut.clone()
};
assert!(
auth.may(ANONYMOUS_PRINCIPAL_ID, &always_public, Level::Read)
.expect("acl"),
"with visibility pinned to Public the private repository must become readable. If it \
does not, `may` is not reading the field at all and the whole feature is decorative"
);
// …and that is exactly what the suite must refuse. Stated as the inverse of
// the shipped answer, so this line IS the red demonstration:
assert_ne!(
auth.may(ANONYMOUS_PRINCIPAL_ID, &always_public, Level::Read)
.expect("acl"),
auth.may(ANONYMOUS_PRINCIPAL_ID, &shut, Level::Read)
.expect("acl"),
"pinning visibility to Public changed nothing, so the private-repository tests in this \
file could not have been testing the visibility check"
);
// ── break 2: "nothing is public" ───────────────────────────────────────
let never_public = Repo {
visibility: Visibility::Private,
..open.clone()
};
assert!(
!auth
.may(ANONYMOUS_PRINCIPAL_ID, &never_public, Level::Read)
.expect("acl"),
"with visibility pinned to Private the public repository must stop being readable"
);
assert_ne!(
auth.may(ANONYMOUS_PRINCIPAL_ID, &never_public, Level::Read)
.expect("acl"),
auth.may(ANONYMOUS_PRINCIPAL_ID, &open, Level::Read)
.expect("acl"),
"pinning visibility to Private changed nothing, so the public-clone tests in this file \
could not have been testing the visibility check"
);
// Neither break touches the level: write is refused under both pins, so
// "public" never becomes "writable" by any route through this field.
for repo in [&open, &shut, &always_public, &never_public] {
for level in [Level::Write, Level::Admin] {
assert!(!auth.may(ANONYMOUS_PRINCIPAL_ID, repo, level).expect("acl"));
}
}
}
use gunnar_core::Repo;