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