tests/governance_test.rs
Ref: Size: 32.2 KiB History
//! Governance by `settings.git`, end to end through the real server.
//!
//! Every test here drives a live `git-collab-server` over SSH with real
//! OpenSSH clients, because the parts most worth checking — that a rejection
//! reaches the pushing client, that a rejected config leaves the previous one
//! live — are properties of the whole path, not of the rule engine. The rule
//! engine's own semantics are unit-tested in `src/server/governance/`.
mod common;
use common::ServerHarness;
use std::process::{Command, Output};
/// The configuration most of these tests run under.
///
/// The shape is the point of the design: an agent's entire grant is one
/// prefix, and `refs/heads/` never appears in it.
const ACCESS_CONF: &str = "\
@admins = alex
@agents = claude-a claude-b
repo settings
RW+ = @admins
repo governed
RW+ = @admins
RW refs/collab/ = @agents
R = @all
repo agents/[a-z-]+
C = @agents
RW+ = CREATOR
";
/// `ACCESS_CONF` with extra lines appended to the `governed` block, which is
/// where every exposure rule in these tests goes.
fn access_conf_with(governed_extra: &str) -> String {
ACCESS_CONF.replace("\nrepo agents/", &format!("{governed_extra}\nrepo agents/"))
}
/// The two enrolled keys every exposure test uses.
const KEYS: &[(&str, &str)] = &[("alex.pub", "alex"), ("claude-a.pub", "claude-a")];
/// The two exposure lines, spelled the way the spec's example spells them.
const ANON_READ: &str = " R = @anonymous\n";
const LISTED: &str = " option listed = yes\n";
fn stderr(output: &Output) -> String {
String::from_utf8_lossy(&output.stderr).to_string()
}
fn assert_refused(output: &Output, context: &str) {
assert!(
!output.status.success(),
"{context}: expected the push to be refused, but it succeeded\n{}",
stderr(output)
);
}
fn assert_accepted(output: &Output, context: &str) {
assert!(
output.status.success(),
"{context}: expected the push to be accepted, but it failed\n{}",
stderr(output)
);
}
/// A server with no `settings.git` is not governed, and nothing about it
/// changes: `authorized_keys` still authenticates, `server.toml` still
/// authorizes, pushes still land, and no hook is installed anywhere.
#[test]
fn a_server_with_no_settings_repository_behaves_exactly_as_before() {
let harness = ServerHarness::new("ungoverned");
let key = harness.ssh_client_key();
harness.work_repo().commit_file("a.txt", "one", "first");
assert_accepted(
&harness.ssh_push(&key, "main:main"),
"a branch push on an ungoverned server",
);
harness.work_repo().issue_open("An issue");
assert_accepted(
&harness.ssh_push(&key, "refs/collab/*:refs/collab/*"),
"a collab push on an ungoverned server",
);
// Force-push, which under governance would need RW+.
harness.work_repo().commit_file("a.txt", "two", "second");
harness.work_repo().git(&["reset", "--hard", "HEAD~1"]);
assert_accepted(
&harness.ssh_push(&key, "+main:main"),
"a force push on an ungoverned server",
);
// No hook is installed on a repository the server does not govern.
let hook = harness
.repos_dir()
.join("ungoverned.git")
.join("hooks")
.join("update");
assert!(
!hook.exists(),
"an ungoverned server must not install hooks; found {}",
hook.display()
);
// And the HTTP surface is untouched.
let page = harness.get_ok("/ungoverned");
assert!(page.body.contains("ungoverned"), "got {}", page.body);
}
/// The consequence the revision-refs work bought: a contributor needs write
/// access to `refs/collab/*` and to nothing else, and a compromised agent
/// credential cannot move canonical state.
#[test]
fn an_agent_may_write_collab_refs_and_may_not_write_branches() {
let harness = ServerHarness::new("governed");
harness.push_head();
harness.bootstrap_settings(
ACCESS_CONF,
&[("alex.pub", "alex"), ("claude-a.pub", "claude-a")],
);
let agent = harness.named_key("claude-a");
harness.work_repo().issue_open("Found a bug");
assert_accepted(
&harness.ssh_push(&agent, "refs/collab/*:refs/collab/*"),
"an agent pushing collab refs",
);
harness.work_repo().commit_file("evil.txt", "x", "sneak");
let refused = harness.ssh_push(&agent, "main:main");
assert_refused(&refused, "an agent pushing a branch");
// The hook's reason has to reach the person pushing, or the refusal is
// indistinguishable from a broken server.
let message = stderr(&refused);
assert!(
message.contains("claude-a may not write refs/heads/main"),
"the hook's message did not reach the client; got:\n{message}"
);
// The branch really did not move.
let admin = harness.named_key("alex");
let listing = harness.ssh_fetch(harness.work_repo().dir.path(), &admin, "governed");
assert!(
!String::from_utf8_lossy(&listing.stdout).contains("sneak"),
"the refused commit must not be reachable"
);
}
/// `RW` is not `RW+`: an agent may add to the refs it owns but not rewrite
/// them, which is what stops a compromised credential erasing review history.
#[test]
fn an_agent_may_not_rewind_the_refs_it_may_write() {
let harness = ServerHarness::new("governed");
harness.push_head();
harness.bootstrap_settings(
ACCESS_CONF,
&[("alex.pub", "alex"), ("claude-a.pub", "claude-a")],
);
let agent = harness.named_key("claude-a");
harness.work_repo().issue_open("First");
assert_accepted(
&harness.ssh_push(&agent, "refs/collab/*:refs/collab/*"),
"the initial collab push",
);
// Delete a collab ref: a rewind, and RW does not grant it.
let refs = harness
.work_repo()
.git(&["for-each-ref", "--format=%(refname)", "refs/collab/"]);
let victim = refs.lines().next().expect("a collab ref to delete").trim();
let refused = harness.ssh_push(&agent, &format!(":{victim}"));
assert_refused(&refused, "an agent deleting a collab ref");
assert!(
stderr(&refused).contains("may not rewind or delete"),
"got:\n{}",
stderr(&refused)
);
}
/// The whole point of validating on push: an unusable config never becomes the
/// live one, so there is no malformed-config state for the request path to
/// handle.
#[test]
fn a_config_that_does_not_parse_is_rejected_and_the_previous_one_stays_live() {
let harness = ServerHarness::new("governed");
harness.push_head();
harness.bootstrap_settings(
ACCESS_CONF,
&[("alex.pub", "alex"), ("claude-a.pub", "claude-a")],
);
let admin = harness.named_key("alex");
let agent = harness.named_key("claude-a");
// A valid config push over SSH lands, proving the path works before we
// check that a broken one does not.
harness.stage_settings(
&format!("{ACCESS_CONF}\nrepo scratch\n RW+ = @admins\n"),
&[("alex.pub", "alex"), ("claude-a.pub", "claude-a")],
);
assert_accepted(
&harness.push_settings_over_ssh(&admin),
"an admin pushing a valid config",
);
assert!(harness.live_access_conf().contains("repo scratch"));
// Now a config that does not parse.
harness.stage_raw_access_conf("repo governed\n RWD = @agents\n");
let refused = harness.push_settings_over_ssh(&admin);
assert_refused(&refused, "an admin pushing a config that does not parse");
let message = stderr(&refused);
assert!(
message.contains("the previous one stays live"),
"the rejection must say what happened; got:\n{message}"
);
assert!(
message.contains("line 2") && message.contains("RWD"),
"the rejection must point at the offending line; got:\n{message}"
);
// The live config is still the previous one, byte for byte...
let live = harness.live_access_conf();
assert!(live.contains("repo scratch"), "got:\n{live}");
assert!(!live.contains("RWD"), "got:\n{live}");
// ...and, more to the point, it is still the config actually in force.
harness.work_repo().issue_open("Still governed");
assert_accepted(
&harness.ssh_push(&agent, "refs/collab/*:refs/collab/*"),
"the previous config still granting the agent its collab refs",
);
harness.work_repo().commit_file("evil.txt", "x", "sneak");
assert_refused(
&harness.ssh_push(&agent, "main:main"),
"the previous config still denying the agent branches",
);
}
/// The other half of validation: a config that parses but locks everyone out
/// is just as unusable, and would need `kubectl exec` to undo.
#[test]
fn a_config_that_would_lock_everyone_out_is_rejected() {
let harness = ServerHarness::new("governed");
harness.bootstrap_settings(
ACCESS_CONF,
&[("alex.pub", "alex"), ("claude-a.pub", "claude-a")],
);
let admin = harness.named_key("alex");
// Parses cleanly; nobody retains RW+ on settings.
harness.stage_raw_access_conf("repo governed\n RW+ = alex\n");
let refused = harness.push_settings_over_ssh(&admin);
assert_refused(&refused, "a config that locks everyone out");
assert!(
stderr(&refused).contains("lock everyone out"),
"got:\n{}",
stderr(&refused)
);
assert!(harness.live_access_conf().contains("repo settings"));
}
/// Adding a second machine is adding a file. Two keys whose paths differ only
/// by directory are one principal, and the rules never mention the directory.
#[test]
fn two_keys_in_different_keydir_directories_are_the_same_principal() {
let harness = ServerHarness::new("governed");
harness.push_head();
harness.bootstrap_settings(
ACCESS_CONF,
&[
("laptop/alex.pub", "alex-laptop"),
("desktop/alex.pub", "alex-desktop"),
("claude-a.pub", "claude-a"),
],
);
// Two distinct keypairs. `conf/access.conf` names `alex` once.
let laptop = harness.named_key("alex-laptop");
let desktop = harness.named_key("alex-desktop");
harness
.work_repo()
.commit_file("from-laptop.txt", "1", "laptop");
assert_accepted(
&harness.ssh_push(&laptop, "main:main"),
"the laptop key acting as alex",
);
harness
.work_repo()
.commit_file("from-desktop.txt", "2", "desktop");
assert_accepted(
&harness.ssh_push(&desktop, "main:main"),
"the desktop key acting as the same alex",
);
// A third key, generated the same way but never enrolled, is not a
// principal at all — it cannot even authenticate.
let stranger = harness.named_key("mallory");
let probe = harness.ssh_fetch(harness.work_repo().dir.path(), &stranger, "governed");
assert!(
!probe.status.success(),
"an unenrolled key must not authenticate\n{}",
String::from_utf8_lossy(&probe.stdout)
);
}
/// Wild repos: an agent allocates its own namespace, with no central
/// allocator to be a bottleneck or a privilege.
#[test]
fn an_agent_creates_its_own_wild_repo_and_another_agent_cannot_write_it() {
let harness = ServerHarness::new("governed");
harness.bootstrap_settings(
ACCESS_CONF,
&[
("alex.pub", "alex"),
("claude-a.pub", "claude-a"),
("claude-b.pub", "claude-b"),
],
);
let first = harness.named_key("claude-a");
let second = harness.named_key("claude-b");
harness.work_repo().commit_file("mine.txt", "1", "mine");
assert_accepted(
&harness.ssh_push_from(
harness.work_repo().dir.path(),
&first,
"agents/claude-a",
"main:main",
),
"an agent creating its own wild repo",
);
assert!(harness
.repos_dir()
.join("agents")
.join("claude-a.git")
.exists());
harness.work_repo().commit_file("yours.txt", "2", "yours");
assert_refused(
&harness.ssh_push_from(
harness.work_repo().dir.path(),
&second,
"agents/claude-a",
"main:main",
),
"a second agent writing someone else's wild repo",
);
// But it can create its own.
assert_accepted(
&harness.ssh_push_from(
harness.work_repo().dir.path(),
&second,
"agents/claude-b",
"main:main",
),
"the second agent creating its own",
);
}
// ---- Exposure: unlisted by default, published by rule -------------------
/// Whether an anonymous git client can clone over HTTP, asked of the real
/// smart-HTTP endpoint rather than of a rendered page.
fn clonable_anonymously(harness: &ServerHarness, repo: &str) -> bool {
harness
.get(&format!("/{repo}.git/info/refs?service=git-upload-pack"))
.status_line
.contains("200")
}
fn readable_anonymously(harness: &ServerHarness, repo: &str) -> bool {
harness.get(&format!("/{repo}")).status_line.contains("200")
}
fn listed_anonymously(harness: &ServerHarness, repo: &str) -> bool {
harness.get_ok("/").body.contains(repo)
}
/// The model, end to end: a governed repository is unlisted and unreadable
/// without authentication, `R = @anonymous` makes it reachable by name, and
/// `option listed = yes` advertises it. The two are separate states because
/// "not in the list" and "404 to a direct URL" are different things.
#[test]
fn a_governed_repository_is_unlisted_and_unreadable_until_a_rule_says_so() {
let harness = ServerHarness::new("governed");
harness.push_head();
harness.bootstrap_settings(ACCESS_CONF, KEYS);
// The config grants `R = @all` — every enrolled key — and that is not
// the same set as "anybody at all", so nothing is open to HTTP.
assert!(
!listed_anonymously(&harness, "governed"),
"a repository with no anonymous grant must not be advertised"
);
assert!(
!readable_anonymously(&harness, "governed"),
"nor reachable by name"
);
assert!(
!clonable_anonymously(&harness, "governed"),
"nor clonable over HTTP"
);
// `R = @anonymous`: reachable by name, still not advertised.
harness.bootstrap_settings(&access_conf_with(ANON_READ), KEYS);
assert!(
readable_anonymously(&harness, "governed"),
"an anonymous read grant must make the repository reachable"
);
assert!(
clonable_anonymously(&harness, "governed"),
"and clonable: a clone is the same read"
);
assert!(
!listed_anonymously(&harness, "governed"),
"but reachable by name is not advertised"
);
// And the option that advertises it.
harness.bootstrap_settings(&access_conf_with(&format!("{ANON_READ}{LISTED}")), KEYS);
assert!(
listed_anonymously(&harness, "governed"),
"option listed = yes must put the repository in the list"
);
assert!(readable_anonymously(&harness, "governed"));
}
/// The inverted default is a property of the governed world, not of the
/// binary. A server with no `settings.git` keeps today's behaviour exactly —
/// otherwise every existing deployment would hide every repository it has the
/// moment it upgraded.
#[test]
fn an_ungoverned_server_still_lists_and_serves_every_repository() {
let harness = ServerHarness::new("ungoverned");
harness.push_head();
assert!(
listed_anonymously(&harness, "ungoverned"),
"an ungoverned server must go on listing its repositories"
);
assert!(readable_anonymously(&harness, "ungoverned"));
assert!(clonable_anonymously(&harness, "ungoverned"));
}
/// Web UI authentication is out of scope, so an HTTP request has no identity:
/// a listed repository nobody may read would advertise a name that 404s. The
/// pair is rejected on push, and the config that was live stays live.
#[test]
fn listing_a_repository_nobody_may_read_is_rejected_on_push() {
let harness = ServerHarness::new("governed");
harness.push_head();
harness.bootstrap_settings(&access_conf_with(ANON_READ), KEYS);
let admin = harness.named_key("alex");
// Keep the option, drop the grant.
harness.stage_settings(&access_conf_with(LISTED), KEYS);
let refused = harness.push_settings_over_ssh(&admin);
assert_refused(&refused, "listed without an anonymous read grant");
let message = stderr(&refused);
assert!(
message.contains("@anonymous") && message.contains("the previous one stays live"),
"the rejection must say what is wrong and what happened; got:\n{message}"
);
// The previously-live config is still the one in force.
assert!(harness.live_access_conf().contains("@anonymous"));
assert!(readable_anonymously(&harness, "governed"));
assert!(!listed_anonymously(&harness, "governed"));
}
/// An unauthenticated request cannot be held responsible for a write, so a
/// permission that grants it one is a mistake — rejected, not ignored.
#[test]
fn a_write_grant_to_the_anonymous_reader_is_rejected_on_push() {
let harness = ServerHarness::new("governed");
harness.push_head();
harness.bootstrap_settings(&access_conf_with(ANON_READ), KEYS);
let admin = harness.named_key("alex");
harness.stage_settings(&access_conf_with(" RW = @anonymous\n"), KEYS);
let refused = harness.push_settings_over_ssh(&admin);
assert_refused(&refused, "a write grant to @anonymous");
assert!(
stderr(&refused).contains("@anonymous"),
"got:\n{}",
stderr(&refused)
);
// Still governed by the config that was live before the attempt.
assert!(!harness.live_access_conf().contains("RW = @anonymous"));
assert!(readable_anonymously(&harness, "governed"));
let agent = harness.named_key("claude-a");
harness.work_repo().issue_open("Still governed");
assert_accepted(
&harness.ssh_push(&agent, "refs/collab/*:refs/collab/*"),
"the previous config still granting the agent its collab refs",
);
}
/// A wild repo is governed like any other, and `agents/settings` is a
/// repository whose name happens to end in `settings` — not the repository
/// that governs the server.
#[test]
fn a_wild_repo_obeys_the_exposure_rules_and_is_not_the_governance_repo() {
let harness = ServerHarness::new("governed");
harness.bootstrap_settings(ACCESS_CONF, KEYS);
let agent = harness.named_key("claude-a");
harness.work_repo().commit_file("mine.txt", "1", "mine");
assert_accepted(
&harness.ssh_push_from(
harness.work_repo().dir.path(),
&agent,
"agents/settings",
"main:main",
),
"an agent creating a wild repo whose last segment is `settings`",
);
// Created, and closed by default like everything else.
assert!(harness
.repos_dir()
.join("agents")
.join("settings.git")
.exists());
assert!(!listed_anonymously(&harness, "agents/settings"));
assert!(!readable_anonymously(&harness, "agents/settings"));
// Publish the wild namespace. The governance repository is a different
// repository and stays shut.
harness.bootstrap_settings(
&ACCESS_CONF.replace(
" RW+ = CREATOR\n",
" RW+ = CREATOR\n R = @anonymous\n option listed = yes\n",
),
KEYS,
);
assert!(
readable_anonymously(&harness, "agents/settings"),
"a wild repo follows the rule that matches it"
);
assert!(listed_anonymously(&harness, "agents/settings"));
assert!(
!readable_anonymously(&harness, "settings"),
"the governance repository is not what `agents/settings` names"
);
}
/// Downloads are data distribution, like a clone, so they follow the same
/// grant. Publishing stays `RW+` and SSH-only.
#[test]
fn release_downloads_follow_the_anonymous_read_grant() {
let harness = ServerHarness::new("governed");
harness.push_head();
harness.bootstrap_settings(ACCESS_CONF, KEYS);
let admin = harness.named_key("alex");
let upload = harness.ssh_exec_as(
&admin,
"collab-release upload 'governed.git' 'v1' 'a.tar.gz'",
b"payload",
);
assert!(
upload.status.success(),
"an admin with RW+ must be able to publish: {}",
stderr(&upload)
);
let (head, _) = harness.get_bytes("/governed/releases/v1/a.tar.gz");
assert!(
head.contains("404"),
"a repository with no anonymous grant must not serve its artifacts: {head}"
);
harness.bootstrap_settings(&access_conf_with(ANON_READ), KEYS);
let (head, body) = harness.get_bytes("/governed/releases/v1/a.tar.gz");
assert!(head.contains("200"), "got {head}");
assert_eq!(body, b"payload");
}
// ---- The bootstrap half-state -------------------------------------------
//
// The closed-by-default posture is a property of the *governed* world, and a
// `settings.git` holding `keydir/` but no `conf/access.conf` governs nothing:
// it is an ordinary repository on an ungoverned server, listed and clonable,
// publishing who has access to the forge and how many of them there are. That
// window is closed at the source — the push that would create it is refused —
// rather than by special-casing the repository on the way out.
/// The mistake a first-time operator makes, because `keydir/` is the thing you
/// have before you have rules.
#[test]
fn a_roster_pushed_without_rules_is_refused_and_nothing_lands() {
let harness = ServerHarness::new("alpha");
let key = harness.ssh_client_key();
harness.stage_keydir_only(&[("alex.pub", "alex")]);
let refused = harness.push_settings_over_ssh(&key);
assert_refused(&refused, "a keydir with no access.conf");
// The operator is mid-bootstrap and has no idea why this bounced, so the
// message has to name the fix.
let message = stderr(&refused);
assert!(
message.contains("conf/access.conf") && message.contains("keydir/"),
"the refusal must name both halves; got:\n{message}"
);
assert!(
message.contains("push both") || message.contains("first"),
"the refusal must say what to do instead; got:\n{message}"
);
// Nothing landed, so there is no roster on the server to publish.
assert!(
!harness.settings_has_content(),
"the refused push must not have updated the settings repository"
);
// And the server is still ungoverned: the repository it hosts is
// untouched by any of this.
assert!(readable_anonymously(&harness, "alpha"));
}
/// The same exposure by the other route: the resulting tree is what matters,
/// not the diff that produced it.
#[test]
fn a_push_that_deletes_the_rules_and_keeps_the_roster_is_refused() {
let harness = ServerHarness::new("governed");
harness.bootstrap_settings(ACCESS_CONF, KEYS);
let admin = harness.named_key("alex");
harness.stage_keydir_only(KEYS);
let refused = harness.push_settings_over_ssh(&admin);
assert_refused(&refused, "a push that removes access.conf but keeps keydir");
// Specifically the roster check, not the generic "you removed the rules"
// one: both routes leave the same tree and must say the same thing.
assert!(
stderr(&refused).contains("keydir/ without conf/access.conf"),
"got:\n{}",
stderr(&refused)
);
// The previously-live config still governs.
assert!(harness.live_access_conf().contains("repo settings"));
assert!(!readable_anonymously(&harness, "settings"));
}
/// The fix the message tells the operator to apply: push both together.
#[test]
fn pushing_the_roster_and_the_rules_together_is_accepted() {
let harness = ServerHarness::new("alpha");
let key = harness.ssh_client_key();
harness.stage_settings(ACCESS_CONF, KEYS);
assert_accepted(
&harness.push_settings_over_ssh(&key),
"the first config push, roster and rules together",
);
assert!(harness.live_access_conf().contains("repo settings"));
// And from that moment the server is governed, so the repository holding
// the roster is closed by the default.
assert!(!readable_anonymously(&harness, "settings"));
assert!(!listed_anonymously(&harness, "settings"));
}
/// Rule 3, at the boundary: a `settings.git` with neither file governs
/// nothing and is refused nothing. Only a roster *without* rules is.
#[test]
fn a_settings_repo_with_neither_file_is_an_ordinary_repository() {
let harness = ServerHarness::new("alpha");
let key = harness.ssh_client_key();
harness
.work_repo()
.commit_file("readme.md", "hello", "docs");
let pushed = harness.ssh_push_from(
harness.work_repo().dir.path(),
&key,
"settings",
"main:main",
);
assert_accepted(
&pushed,
"an ordinary push to a repository that happens to be called settings",
);
assert!(harness.settings_has_content());
assert!(readable_anonymously(&harness, "settings"));
}
/// Creating `settings.git` must not silently publish the key roster and the
/// access rules to an internet-facing web UI. This used to be a hand-coded
/// special case; it is now just the default with no rule to lift it.
#[test]
fn the_settings_repository_is_not_on_the_anonymous_http_surface_by_default() {
let harness = ServerHarness::new("governed");
harness.bootstrap_settings(
ACCESS_CONF,
&[("alex.pub", "alex"), ("claude-a.pub", "claude-a")],
);
let listing = harness.get_ok("/");
assert!(
!listing.body.contains("settings"),
"the governance repo must not be listed anonymously; got:\n{}",
listing.body
);
for path in ["/settings", "/settings/tree", "/settings/commits"] {
let page = harness.get(path);
assert!(
!page.status_line.contains("200"),
"{path} must not be anonymously readable; got {}",
page.status_line
);
}
// An enrolled principal still reads it over SSH.
let admin = harness.named_key("alex");
let listed = harness.ssh_fetch(harness.work_repo().dir.path(), &admin, "settings");
assert!(
listed.status.success(),
"an admin must still be able to read the rules: {}",
stderr(&listed)
);
}
/// The documented split, asserted rather than left to the doc comment: where
/// governance is in force, `settings.git` supersedes `server.toml` on *every*
/// axis, the anonymous surface included. Exactly one file answers the
/// question, so a `server.toml` nobody remembers editing can neither subtract
/// access that `access.conf` grants nor add exposure it withholds.
#[test]
fn settings_supersedes_server_toml_on_the_anonymous_axis_in_both_directions() {
let harness = ServerHarness::new("governed");
harness.push_head();
harness.bootstrap_settings(ACCESS_CONF, KEYS);
// A server.toml that, under the old regime, would deny every principal
// and hide the repository from anonymous HTTP.
harness.write_repo_server_policy(
"visibility = \"private\"\n\
[ui]\nanonymous = false\n\
[http]\nanonymous_clone = false\n\
[access]\nread = []\nwrite = []\n",
);
// The authenticated axis is access.conf's alone: the empty lists above are
// not consulted, so the admin still has RW+.
let admin = harness.named_key("alex");
harness
.work_repo()
.commit_file("b.txt", "1", "still allowed");
assert_accepted(
&harness.ssh_push(&admin, "main:main"),
"access.conf superseding an empty server.toml access list",
);
// The anonymous axis is access.conf's too, and it grants nothing here.
assert!(!readable_anonymously(&harness, "governed"));
// Now the direction that proves supersession rather than agreement: the
// same private server.toml, and a rule that publishes the repository.
harness.bootstrap_settings(&access_conf_with(ANON_READ), KEYS);
assert!(
readable_anonymously(&harness, "governed"),
"a private server.toml must not subtract what access.conf grants"
);
// And the reverse: the most permissive server.toml there is cannot
// publish a repository access.conf has not published.
harness.write_repo_server_policy(
"visibility = \"public\"\n[ui]\nanonymous = true\n[http]\nanonymous_clone = true\n",
);
harness.bootstrap_settings(ACCESS_CONF, KEYS);
assert!(
!readable_anonymously(&harness, "governed"),
"server.toml must not be able to publish what access.conf withholds"
);
}
// ---- Log identity: rejections name the person, not just the key --------
/// The `key:SHA256:...` principal string OpenSSH's own `ssh-keygen -lf`
/// would print for `pubkey_path` — an external oracle, not the server's own
/// fingerprint code, so this proves agreement with an independent source
/// rather than self-consistency.
fn fingerprint_oracle(pubkey_path: &std::path::Path) -> String {
let out = Command::new("ssh-keygen")
.args(["-lf"])
.arg(pubkey_path)
.output()
.expect("failed to run ssh-keygen -lf");
assert!(
out.status.success(),
"ssh-keygen -lf failed: {}",
String::from_utf8_lossy(&out.stderr)
);
let stdout = String::from_utf8_lossy(&out.stdout);
// "256 SHA256:Gzb/mmBlyDmoWq3hMyPjb3SRWklPJ/yDAoD1K+ATvJA name (ED25519)"
let fp = stdout
.split_whitespace()
.nth(1)
.expect("ssh-keygen -lf output missing fingerprint field");
format!("key:{fp}")
}
/// A refused create names the person `settings.git` resolved the key to,
/// with the fingerprint kept alongside for the surprising-resolution case —
/// not just the bare fingerprint an operator has to map back to someone by
/// hand.
#[test]
fn a_refused_create_names_the_resolved_person_and_the_fingerprint() {
let harness = ServerHarness::new("governed-create-identity");
harness.bootstrap_settings(ACCESS_CONF, KEYS);
let admin = harness.named_key("alex");
let fingerprint = fingerprint_oracle(&admin.with_extension("pub"));
// "eitri" matches no `repo` pattern in ACCESS_CONF (not "settings", not
// "governed", not "agents/[a-z-]+"), so no rule grants alex `C` on it.
harness.work_repo().commit_file("e.txt", "1", "seed");
let push = harness.ssh_push_from(harness.work_repo().dir.path(), &admin, "eitri", "main:main");
assert_refused(&push, "a push creating a repo no rule grants C on");
assert!(
!harness.repos_dir().join("eitri.git").exists(),
"the repository must not exist after a refused create"
);
let log = harness.server_log();
let rejection = log
.lines()
.find(|line| line.contains("Rejected exec request") && line.contains("eitri"))
.unwrap_or_else(|| panic!("expected a create-rejection line, got: {log}"));
assert!(
rejection.contains("alex") && rejection.contains(&fingerprint),
"rejection should name both the resolved person and the fingerprint, got: {rejection}"
);
assert!(
rejection.contains("no such repository") && rejection.contains("may not create"),
"a denied create should say so, not just \"may not create\", got: {rejection}"
);
}
/// The common case behind "no such repository" is a typo or a repo never
/// pushed here — not a permission gap. A fetch of an absent repository can
/// never create anything either way, so it must not be phrased as a denied
/// create, contrasting with the ACL-denial case above.
#[test]
fn a_fetch_of_an_absent_repo_is_not_phrased_as_a_denied_create() {
let harness = ServerHarness::new("governed-fetch-missing");
harness.bootstrap_settings(ACCESS_CONF, KEYS);
let admin = harness.named_key("alex");
let fetch = harness.ssh_fetch(harness.work_repo().dir.path(), &admin, "nonexistent");
assert!(
!fetch.status.success(),
"fetching an absent repository should fail"
);
let log = harness.server_log();
let rejection = log
.lines()
.find(|line| line.contains("Rejected exec request") && line.contains("nonexistent"))
.unwrap_or_else(|| panic!("expected a missing-repo rejection line, got: {log}"));
assert!(rejection.contains("no such repository"), "got: {rejection}");
assert!(
!rejection.contains("may not create"),
"a fetch was never going to create anything, so it must not be phrased as a denied \
create, got: {rejection}"
);
}