a73x

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