a73x

tests/delegate_test.rs

Ref:   Size: 18.9 KiB   History

mod common;

use std::process::Output;

use common::ServerHarness;

fn stderr(output: &Output) -> String {
    String::from_utf8_lossy(&output.stderr).into_owned()
}

/// Rules never mention delegates: the person holds the grants, the cert
/// borrows them.
fn access_conf(repo: &str) -> String {
    format!("repo settings\n    RW+ = alex\n\nrepo {repo}\n    RW+ = alex\n    RW+ = bob\n")
}

/// A certificate from an enrolled CA, naming an enrolled person, can read
/// what the person reads.
#[test]
fn a_delegate_certificate_authenticates_and_fetches() {
    let harness = ServerHarness::new("delegate-fetch");
    harness.push_head();
    harness.bootstrap_settings_with_cas(
        &access_conf(harness.repo_name()),
        &[("alex.pub", "alex")],
        &[("mint/alex.pub", "mint")],
    );

    // The delegate's own key is enrolled NOWHERE — that is the point.
    let agent_key = harness.named_key("agent-key");
    let ca = harness.delegate_ca("mint");
    let cert = harness.mint_cert(&ca, &agent_key, "claude-a", "alex", "-1m:+30m");

    let out = harness.ssh_fetch_cert(
        harness.work_repo().dir.path(),
        &agent_key,
        &cert,
        harness.repo_name(),
    );
    assert!(
        out.status.success(),
        "delegate fetch failed: {}",
        stderr(&out)
    );
}

/// The same certificate on an ungoverned server is nothing: there is no
/// roster to tie its principal to.
#[test]
fn a_certificate_is_rejected_on_an_ungoverned_server() {
    let harness = ServerHarness::new("delegate-ungoverned");
    let _ = harness.ssh_client_key(); // authorized_keys exists, server ungoverned
    harness.push_head();

    let agent_key = harness.named_key("agent-key");
    let ca = harness.delegate_ca("mint");
    let cert = harness.mint_cert(&ca, &agent_key, "claude-a", "alex", "-1m:+30m");

    let out = harness.ssh_fetch_cert(
        harness.work_repo().dir.path(),
        &agent_key,
        &cert,
        harness.repo_name(),
    );
    assert!(
        !out.status.success(),
        "an ungoverned server accepted a certificate"
    );
}

/// A cert whose CA is enrolled for someone else cannot act as this person.
#[test]
fn a_ca_enrolled_for_another_name_is_rejected() {
    let harness = ServerHarness::new("delegate-wrong-ca");
    harness.push_head();
    harness.bootstrap_settings_with_cas(
        &access_conf(harness.repo_name()),
        &[("alex.pub", "alex"), ("bob.pub", "bob")],
        &[("mint/bob.pub", "mint")], // mint may act for bob, NOT alex
    );

    let agent_key = harness.named_key("agent-key");
    let ca = harness.delegate_ca("mint");
    let cert = harness.mint_cert(&ca, &agent_key, "claude-a", "alex", "-1m:+30m");

    let out = harness.ssh_fetch_cert(
        harness.work_repo().dir.path(),
        &agent_key,
        &cert,
        harness.repo_name(),
    );
    assert!(
        !out.status.success(),
        "a CA enrolled for bob minted a working delegate of alex"
    );
}

/// The ceiling: a delegate writes collab refs with the person's authority —
/// and cannot move a branch the person holds RW+ on.
#[test]
fn a_delegate_writes_collab_refs_and_may_not_write_branches() {
    let harness = ServerHarness::new("delegate-ceiling");
    harness.push_head();
    harness.bootstrap_settings_with_cas(
        &access_conf(harness.repo_name()),
        &[("alex.pub", "alex")],
        &[("mint/alex.pub", "mint")],
    );

    let agent_key = harness.named_key("agent-key");
    let ca = harness.delegate_ca("mint");
    let cert = harness.mint_cert(&ca, &agent_key, "claude-a", "alex", "-1m:+30m");

    harness.work_repo().issue_open("Filed by a delegate");
    let push = harness.ssh_push_from_cert(
        harness.work_repo().dir.path(),
        &agent_key,
        &cert,
        harness.repo_name(),
        "refs/collab/*:refs/collab/*",
    );
    assert!(
        push.status.success(),
        "delegate collab push failed: {}",
        stderr(&push)
    );
    // Attribution has to survive past the door. The connection-level
    // "Certificate auth accepted" line names the delegate once and would
    // satisfy an assertion over the whole log on its own, so look only at
    // what follows it, and at a line that names the command being run: that
    // line can carry an identity only if the command itself is attributed.
    let log = harness.server_log();
    let after_auth = log
        .split_once("Certificate auth accepted")
        .expect("the certificate should have authenticated")
        .1;
    let exec_line = after_auth
        .lines()
        .find(|line| line.contains("Exec request"))
        .unwrap_or_else(|| panic!("the delegate's command should be logged, got: {log}"));
    assert!(
        exec_line.contains("alex (via claude-a)") && exec_line.contains("git-receive-pack"),
        "the delegate's own command should name who is running it, got: {exec_line}"
    );

    harness
        .work_repo()
        .commit_file("d.txt", "delegate", "delegate commit");
    let push = harness.ssh_push_from_cert(
        harness.work_repo().dir.path(),
        &agent_key,
        &cert,
        harness.repo_name(),
        "main:main",
    );
    assert!(
        !push.status.success(),
        "a delegate moved a branch; alex holds RW+ but the cert must not inherit it"
    );
    assert!(
        stderr(&push).contains("refs/collab"),
        "the refusal should name the ceiling, got: {}",
        stderr(&push)
    );
}

/// The person's own key is untouched by the ceiling.
#[test]
fn the_person_still_writes_branches_directly() {
    let harness = ServerHarness::new("delegate-person-unaffected");
    harness.push_head();
    harness.bootstrap_settings_with_cas(
        &access_conf(harness.repo_name()),
        &[("alex.pub", "alex")],
        &[("mint/alex.pub", "mint")],
    );
    harness
        .work_repo()
        .commit_file("p.txt", "person", "person commit");
    let push = harness.ssh_push(&harness.named_key("alex"), "main:main");
    assert!(
        push.status.success(),
        "the person's own push failed: {}",
        stderr(&push)
    );
}

/// Creation is a permission delegates never hold, so CREATOR can never
/// resolve to one.
#[test]
fn a_delegate_may_not_create_a_repository_its_person_could() {
    let harness = ServerHarness::new("delegate-create");
    harness.push_head();
    let conf = format!(
        "repo settings\n    RW+ = alex\n\nrepo {}\n    RW+ = alex\n\nrepo agents/[a-z-]+\n    C = alex\n    RW+ = CREATOR\n",
        harness.repo_name()
    );
    harness.bootstrap_settings_with_cas(
        &conf,
        &[("alex.pub", "alex")],
        &[("mint/alex.pub", "mint")],
    );

    let agent_key = harness.named_key("agent-key");
    let ca = harness.delegate_ca("mint");
    let cert = harness.mint_cert(&ca, &agent_key, "claude-a", "alex", "-1m:+30m");

    harness.work_repo().commit_file("n.txt", "new", "seed");
    let push = harness.ssh_push_from_cert(
        harness.work_repo().dir.path(),
        &agent_key,
        &cert,
        "agents/claude-a",
        "main:main",
    );
    assert!(!push.status.success(), "a delegate created a repository");
    assert!(
        !harness.repos_dir().join("agents/claude-a.git").exists(),
        "the repository must not exist after a refused create"
    );
}

/// Removing the CA enrolment kills the delegates it minted, on their next
/// command — no restart, no KRL, and the cert itself is still inside its
/// validity window.
#[test]
fn removing_the_cadir_entry_cuts_the_delegate_off() {
    let harness = ServerHarness::new("delegate-revoke-ca");
    harness.push_head();
    harness.bootstrap_settings_with_cas(
        &access_conf(harness.repo_name()),
        &[("alex.pub", "alex")],
        &[("mint/alex.pub", "mint")],
    );

    let agent_key = harness.named_key("agent-key");
    let ca = harness.delegate_ca("mint");
    let cert = harness.mint_cert(&ca, &agent_key, "claude-a", "alex", "-1m:+30m");

    let before = harness.ssh_fetch_cert(
        harness.work_repo().dir.path(),
        &agent_key,
        &cert,
        harness.repo_name(),
    );
    assert!(
        before.status.success(),
        "delegate should work before revocation"
    );

    // Re-bootstrap with the cadir entry gone: same conf, same keys, no CAs.
    harness.bootstrap_settings_with_cas(
        &access_conf(harness.repo_name()),
        &[("alex.pub", "alex")],
        &[],
    );

    let after = harness.ssh_fetch_cert(
        harness.work_repo().dir.path(),
        &agent_key,
        &cert,
        harness.repo_name(),
    );
    assert!(
        !after.status.success(),
        "the delegate outlived its CA enrolment"
    );
}

/// Removing the person kills the person's delegates: cadir/ lends identity,
/// keydir/ is what makes it exist.
#[test]
fn removing_the_person_kills_their_delegates() {
    let harness = ServerHarness::new("delegate-revoke-person");
    harness.push_head();
    // Two people, so removing bob leaves a valid config (alex retains RW+ on
    // settings — the lockout check requires someone does).
    let conf = format!(
        "repo settings\n    RW+ = alex\n\nrepo {}\n    RW+ = alex\n    RW+ = bob\n",
        harness.repo_name()
    );
    harness.bootstrap_settings_with_cas(
        &conf,
        &[("alex.pub", "alex"), ("bob.pub", "bob")],
        &[("mint/bob.pub", "mint")],
    );

    let agent_key = harness.named_key("agent-key");
    let ca = harness.delegate_ca("mint");
    let cert = harness.mint_cert(&ca, &agent_key, "bob-agent", "bob", "-1m:+30m");

    let before = harness.ssh_fetch_cert(
        harness.work_repo().dir.path(),
        &agent_key,
        &cert,
        harness.repo_name(),
    );
    assert!(
        before.status.success(),
        "bob's delegate should work while bob exists"
    );

    // bob leaves; his cadir entry remains — and must grant nothing.
    harness.bootstrap_settings_with_cas(
        &conf,
        &[("alex.pub", "alex")],
        &[("mint/bob.pub", "mint")],
    );

    let after = harness.ssh_fetch_cert(
        harness.work_repo().dir.path(),
        &agent_key,
        &cert,
        harness.repo_name(),
    );
    assert!(!after.status.success(), "a delegate outlived its person");
}

/// An expired certificate is rejected at the door.
#[test]
fn an_expired_certificate_does_not_authenticate() {
    let harness = ServerHarness::new("delegate-expired");
    harness.push_head();
    harness.bootstrap_settings_with_cas(
        &access_conf(harness.repo_name()),
        &[("alex.pub", "alex")],
        &[("mint/alex.pub", "mint")],
    );

    let agent_key = harness.named_key("agent-key");
    let ca = harness.delegate_ca("mint");
    let cert = harness.mint_cert(&ca, &agent_key, "claude-a", "alex", "-30m:-1m");

    let out = harness.ssh_fetch_cert(
        harness.work_repo().dir.path(),
        &agent_key,
        &cert,
        harness.repo_name(),
    );
    assert!(
        !out.status.success(),
        "an expired certificate authenticated"
    );
}

/// A settings push carrying a malformed cadir/ file is refused whole; the
/// previous config keeps governing.
#[test]
fn a_malformed_cadir_file_rejects_the_settings_push() {
    let harness = ServerHarness::new("delegate-bad-cadir");
    harness.push_head();
    harness.bootstrap_settings_with_cas(
        &access_conf(harness.repo_name()),
        &[("alex.pub", "alex")],
        &[("mint/alex.pub", "mint")],
    );

    // Stage a broken CA file in the settings work tree and push over SSH.
    let work = harness.settings_work();
    std::fs::write(work.join("cadir").join("junk.pub"), "not a key").unwrap();
    common::git_cmd(&work, &["add", "-A"]);
    common::git_cmd(&work, &["commit", "-q", "-m", "break cadir"]);

    let push = harness.push_settings_over_ssh(&harness.named_key("alex"));
    assert!(
        !push.status.success(),
        "a malformed cadir file was accepted"
    );
    assert!(
        stderr(&push).contains("junk.pub"),
        "the refusal should name the file, got: {}",
        stderr(&push)
    );
}

/// The threat-model case by name: a delegate must not rewrite `settings`
/// itself, the config that governs it — even though the person it acts for
/// holds RW+ there.
#[test]
fn a_delegate_may_not_push_settings_itself() {
    let harness = ServerHarness::new("delegate-settings-ceiling");
    harness.push_head();
    harness.bootstrap_settings_with_cas(
        &access_conf(harness.repo_name()),
        &[("alex.pub", "alex")],
        &[("mint/alex.pub", "mint")],
    );

    let agent_key = harness.named_key("agent-key");
    let ca = harness.delegate_ca("mint");
    let cert = harness.mint_cert(&ca, &agent_key, "claude-a", "alex", "-1m:+30m");

    // A harmless change in the settings work tree — the push itself, not its
    // content, is what must be refused.
    let work = harness.settings_work();
    std::fs::write(work.join("note.txt"), "n/a").unwrap();
    common::git_cmd(&work, &["add", "-A"]);
    common::git_cmd(&work, &["commit", "-q", "-m", "settings tweak"]);

    let push = harness.ssh_push_from_cert(&work, &agent_key, &cert, "settings", "main:main");
    assert!(
        !push.status.success(),
        "a delegate rewrote settings, the config that governs it"
    );
    assert!(
        stderr(&push).contains("refs/collab"),
        "the refusal should name the ceiling, got: {}",
        stderr(&push)
    );
}

/// Release upload/delete are outside the ceiling even though the person a
/// delegate acts for may hold RW+ on the repo; List is Read and stays open.
#[test]
fn a_delegate_may_list_releases_but_not_publish_them() {
    let harness = ServerHarness::new("delegate-release-ceiling");
    harness.push_head();
    harness.bootstrap_settings_with_cas(
        &access_conf(harness.repo_name()),
        &[("alex.pub", "alex")],
        &[("mint/alex.pub", "mint")],
    );

    let agent_key = harness.named_key("agent-key");
    let ca = harness.delegate_ca("mint");
    let cert = harness.mint_cert(&ca, &agent_key, "claude-a", "alex", "-1m:+30m");

    let upload = harness.ssh_exec_as_cert(
        &agent_key,
        &cert,
        &format!(
            "collab-release upload '{}.git' 'v1' 'a.tar.gz'",
            harness.repo_name()
        ),
        b"fake tarball bytes",
    );
    assert!(
        !upload.status.success(),
        "a delegate published a release: {}",
        stderr(&upload)
    );

    let list = harness.ssh_exec_as_cert(
        &agent_key,
        &cert,
        &format!("collab-release list '{}.git'", harness.repo_name()),
        b"",
    );
    assert!(
        list.status.success(),
        "a delegate could not list releases: {}",
        stderr(&list)
    );
}

/// Mint a delegate cert of `alex` for `harness`, having bootstrapped settings
/// with `access_conf`. Shared by the RW/RW+ collab-ref pair below, which
/// differ only in that one line of config.
fn delegate_of_alex(
    harness: &ServerHarness,
    access_conf: &str,
) -> (std::path::PathBuf, std::path::PathBuf) {
    harness.bootstrap_settings_with_cas(
        access_conf,
        &[("alex.pub", "alex")],
        &[("mint/alex.pub", "mint")],
    );
    let agent_key = harness.named_key("agent-key");
    let ca = harness.delegate_ca("mint");
    let cert = harness.mint_cert(&ca, &agent_key, "claude-a", "alex", "-1m:+30m");
    (agent_key, cert)
}

/// The README's claim that `RW refs/collab/` (as opposed to `RW+`) keeps
/// rewind and delete of collab refs out of a delegate's reach, even though
/// the delegate's write into that namespace is otherwise unrestricted.
#[test]
fn a_delegate_under_rw_may_write_but_not_rewind_or_delete_collab_refs() {
    let harness = ServerHarness::new("delegate-rw-ceiling");
    harness.push_head();
    let conf = format!(
        "repo settings\n    RW+ = alex\n\nrepo {}\n    RW refs/collab/ = alex\n",
        harness.repo_name()
    );
    let (agent_key, cert) = delegate_of_alex(&harness, &conf);

    harness.work_repo().issue_open("RW ceiling");
    let push = harness.ssh_push_from_cert(
        harness.work_repo().dir.path(),
        &agent_key,
        &cert,
        harness.repo_name(),
        "refs/collab/*:refs/collab/*",
    );
    assert!(
        push.status.success(),
        "a delegate under RW could not write a collab ref: {}",
        stderr(&push)
    );

    let victim = harness
        .work_repo()
        .git(&["for-each-ref", "--format=%(refname)", "refs/collab/"])
        .lines()
        .next()
        .expect("a collab ref to target")
        .trim()
        .to_string();

    // Rewind: force-push an unrelated commit onto the same ref name, which is
    // not a fast-forward of it.
    harness
        .work_repo()
        .commit_file("unrelated.txt", "x", "unrelated commit");
    let rewind = harness.ssh_push_from_cert(
        harness.work_repo().dir.path(),
        &agent_key,
        &cert,
        harness.repo_name(),
        &format!("+HEAD:{victim}"),
    );
    assert!(
        !rewind.status.success(),
        "a delegate under RW rewound a collab ref"
    );

    let delete = harness.ssh_push_from_cert(
        harness.work_repo().dir.path(),
        &agent_key,
        &cert,
        harness.repo_name(),
        &format!(":{victim}"),
    );
    assert!(
        !delete.status.success(),
        "a delegate under RW deleted a collab ref"
    );
}

/// The contrast: under `RW+`, the same operations succeed — a delegate
/// inherits rewind and delete inside `refs/collab/*` along with everything
/// else the person holds there.
#[test]
fn a_delegate_under_rw_plus_may_rewind_and_delete_collab_refs() {
    let harness = ServerHarness::new("delegate-rw-plus-ceiling");
    harness.push_head();
    let conf = format!(
        "repo settings\n    RW+ = alex\n\nrepo {}\n    RW+ refs/collab/ = alex\n",
        harness.repo_name()
    );
    let (agent_key, cert) = delegate_of_alex(&harness, &conf);

    harness.work_repo().issue_open("RW+ ceiling");
    let push = harness.ssh_push_from_cert(
        harness.work_repo().dir.path(),
        &agent_key,
        &cert,
        harness.repo_name(),
        "refs/collab/*:refs/collab/*",
    );
    assert!(
        push.status.success(),
        "a delegate under RW+ could not write a collab ref: {}",
        stderr(&push)
    );

    let victim = harness
        .work_repo()
        .git(&["for-each-ref", "--format=%(refname)", "refs/collab/"])
        .lines()
        .next()
        .expect("a collab ref to target")
        .trim()
        .to_string();

    harness
        .work_repo()
        .commit_file("unrelated.txt", "x", "unrelated commit");
    let rewind = harness.ssh_push_from_cert(
        harness.work_repo().dir.path(),
        &agent_key,
        &cert,
        harness.repo_name(),
        &format!("+HEAD:{victim}"),
    );
    assert!(
        rewind.status.success(),
        "a delegate under RW+ could not rewind a collab ref: {}",
        stderr(&rewind)
    );

    let delete = harness.ssh_push_from_cert(
        harness.work_repo().dir.path(),
        &agent_key,
        &cert,
        harness.repo_name(),
        &format!(":{victim}"),
    );
    assert!(
        delete.status.success(),
        "a delegate under RW+ could not delete a collab ref: {}",
        stderr(&delete)
    );
}