git-ents.gitmain
⌘K
foforge
commit b096979
refactor!: rename the trust ref refs/meta/auth to refs/meta/members

The push trust root is the repository’s member set. Rename the ref, the Auth/Members document and its signers/members/ subtree, and the git ents auth subcommand group to git ents members. Hard break: the live trust list is discarded and re-pushed after redeploy, no fallback.

refactor: rename signers AUTH_REF/Auth to MEMBERS_REF/Members refactor: rename git ents auth subcommand group to git ents members Assisted-by: Claude:claude-opus-4-8

Joseph D. Carpinelli · 1 month ago

Reviews

No reviews of this commit yet — record a verdict below.

Start a review

verdict

crates/git-ents-server/src/verify.rs @@ -1,9 +1,9 @@ //! The `pre-receive` verifier: a git hook that gates pushes on a signature from -//! an authorized signer. +//! a member. //! -//! When the trust list at `refs/meta/auth` is empty the server is still in its -//! open bootstrap window and every push is allowed, so the first signer can be -//! pushed in. Once any signer is listed, a push must carry a signed-push +//! When the trust list at `refs/meta/members` is empty the server is still in +//! its open bootstrap window and every push is allowed, so the first member can +//! be pushed in. Once any member is listed, a push must carry a signed-push //! certificate (`git push --signed`) whose anti-replay nonce git accepted and //! whose signature verifies against one of those keys.
crates/git-ents-server/tests/pre_receive.rs @@ -95,10 +95,10 @@ let oid = hash_object(&repo, key.as_bytes()); tree_entries.push_str(&format!("100644 blob {oid}\tkey-{index}\n")); } - let signers_tree = mktree(&repo, &tree_entries); - let root_tree = mktree(&repo, &format!("040000 tree {signers_tree}\tsigners\n")); - let commit = ok(&repo, "git", &["commit-tree", &root_tree, "-m", "auth"]); - ok(&repo, "git", &["update-ref", "refs/meta/auth", &commit]); + let members_tree = mktree(&repo, &tree_entries); + let root_tree = mktree(&repo, &format!("040000 tree {members_tree}\tmembers\n")); + let commit = ok(&repo, "git", &["commit-tree", &root_tree, "-m", "members"]); + ok(&repo, "git", &["update-ref", "refs/meta/members", &commit]); } repo }
crates/git-ents/src/main.rs @@ -1,18 +1,17 @@ //! `git ents` — the git-ents command-line porcelain. //! -//! Today it carries a single command, `git ents auth`, for managing the -//! authorized push signers recorded at `refs/meta/auth` and for configuring -//! this client to produce the signed pushes the server requires. The signer -//! commands read and write a remote's set by fetching `refs/meta/auth` into the -//! local repository, editing it through [`git_ents::signers`], and pushing it -//! back. +//! Today it carries a single command, `git ents members`, for managing the +//! repository members recorded at `refs/meta/members` and for configuring this +//! client to produce the signed pushes the server requires. The member commands +//! read and write a remote's set by fetching `refs/meta/members` into the local +//! repository, editing it through [`git_ents::signers`], and pushing it back. use std::path::{Path, PathBuf}; use std::process::{Command, ExitCode, Stdio}; use clap::{Parser, Subcommand}; use git_ents::checks::{self, CHECKS_REF, Check}; -use git_ents::signers::{self, AUTH_REF, Signer}; +use git_ents::signers::{self, MEMBERS_REF, Signer}; #[derive(Parser)] #[command(name = "git-ents", about = "Helpful guardians of your git trees.")] @@ -23,8 +22,8 @@ #[derive(Subcommand)] enum Top { - /// Manage the authorized push signers at `refs/meta/auth`. - Auth { + /// Manage the repository members at `refs/meta/members`. + Members { #[command(subcommand)] action: Action, }, @@ -47,32 +46,32 @@ #[arg(long)] local: bool, }, - /// List the authorized signers on a remote. + /// List the members on a remote. List { - /// Remote to read `refs/meta/auth` from. + /// Remote to read `refs/meta/members` from. #[arg(default_value = "origin")] remote: String, }, - /// Add a signer to a remote's set and push the update. + /// Add a member to a remote's set and push the update. Add { - /// Remote whose `refs/meta/auth` to update. + /// Remote whose `refs/meta/members` to update. #[arg(default_value = "origin")] remote: String, /// Key to authorize; defaults to `user.signingkey`. #[arg(long)] key: Option<PathBuf>, }, - /// Remove a signer from a remote's set and push the update. + /// Remove a member from a remote's set and push the update. Remove { - /// Fingerprint (`signers/<name>`) to drop. + /// Fingerprint (`members/<name>`) to drop. fingerprint: String, - /// Remote whose `refs/meta/auth` to update. + /// Remote whose `refs/meta/members` to update. #[arg(default_value = "origin")] remote: String, }, - /// Report whether a key is authorized and the client is configured. + /// Report whether a key is a member and the client is configured. Check { - /// Remote to read `refs/meta/auth` from. + /// Remote to read `refs/meta/members` from. #[arg(default_value = "origin")] remote: String, /// Key to look for; defaults to `user.signingkey`. @@ -112,7 +111,7 @@ fn main() -> ExitCode { let cli = Cli::parse(); let result = match cli.command { - Top::Auth { action } => run_auth(action), + Top::Members { action } => run_members(action), Top::Checks { action } => run_checks(action), }; match result { @@ -124,7 +123,7 @@ } } -fn run_auth(action: Action) -> Result<(), String> { +fn run_members(action: Action) -> Result<(), String> { match action { Action::Setup { key, local } => setup(key.as_deref(), local), Action::List { remote } => list::<Signers>(&remote), @@ -171,12 +170,12 @@ fn row_value(key: &str, value: &str) -> String; } -/// The authorized signer set at `refs/meta/auth`. +/// The repository member set at `refs/meta/members`. struct Signers; impl Set for Signers { - const REF: &'static str = AUTH_REF; - const NOUN: &'static str = "signer"; + const REF: &'static str = MEMBERS_REF; + const NOUN: &'static str = "member"; fn load(repo: &Path) -> Result<Vec<(String, String)>, String> { Ok(signers::load(repo) @@ -198,7 +197,7 @@ } fn empty_listing(remote: &str) -> String { - format!("no authorized signers on {remote} (open bootstrap window)") + format!("no members on {remote} (open bootstrap window)") } fn row_value(_key: &str, value: &str) -> String { @@ -314,7 +313,7 @@ scope.trim_start_matches('-') ); println!("signing key: {signing_key} ({fingerprint})"); - println!("authorize it on a server with `git ents auth add <remote>`"); + println!("authorize it on a server with `git ents members add <remote>`"); Ok(()) } @@ -440,13 +439,13 @@ let repo = repo()?; let public_key = public_key(key)?; let fingerprint = fingerprint(&public_key)?; - let expected = sync(remote, AUTH_REF)?; + let expected = sync(remote, MEMBERS_REF)?; let mut signers = signers::load(&repo).map_err(|error| error.to_string())?; if signers .iter() .any(|signer| same_key(&signer.key, &public_key)) { - println!("{fingerprint} is already authorized"); + println!("{fingerprint} is already a member"); return Ok(()); } signers.push(Signer { @@ -454,7 +453,7 @@ key: public_key, }); signers::store(&repo, &signers).map_err(|error| error.to_string())?; - push_signed(remote, AUTH_REF, expected.as_deref())?; + push_signed(remote, MEMBERS_REF, expected.as_deref())?; println!("authorized {fingerprint}"); Ok(()) } @@ -464,17 +463,17 @@ let repo = repo()?; let public_key = public_key(key)?; let fingerprint = fingerprint(&public_key)?; - sync(remote, AUTH_REF)?; + sync(remote, MEMBERS_REF)?; let signers = signers::load(&repo).map_err(|error| error.to_string())?; if signers.is_empty() { - println!("{remote}: open bootstrap window (no signers yet)"); + println!("{remote}: open bootstrap window (no members yet)"); } else if signers .iter() .any(|signer| same_key(&signer.key, &public_key)) { - println!("{remote}: {fingerprint} is authorized"); + println!("{remote}: {fingerprint} is a member"); } else { - println!("{remote}: {fingerprint} is NOT authorized"); + println!("{remote}: {fingerprint} is NOT a member"); } println!( "client: gpg.format={}, user.signingkey={}, push.gpgSign={}", @@ -587,7 +586,7 @@ /// The key's MD5 fingerprint in colon form (`aa:bb:…`). Colon-separated pairs /// are filesystem-safe, unlike the slashes in a base64 SHA256 fingerprint that -/// would split the `signers/<name>` entry into a subtree. +/// would split the `members/<name>` entry into a subtree. fn fingerprint(public_key: &str) -> Result<String, String> { let scratch = tempfile::tempdir().map_err(|error| format!("could not create temp dir: {error}"))?;
crates/git-ents/src/signers.rs @@ -1,37 +1,37 @@ -//! The authorized signer set, sourced from the `refs/meta/auth` ref. +//! The repository's members, sourced from the `refs/meta/members` ref. //! -//! Push authentication trusts exactly one place: the `refs/meta/auth` ref. Its -//! tree is an [`Auth`] document mapping each fingerprint to its OpenSSH public -//! key. The document is read and written through [`git_store`], so the trust -//! list is a typed value that lives in git — versioned, auditable, and itself -//! pushable. +//! Push authentication trusts exactly one place: the `refs/meta/members` ref. +//! Its tree is a [`Members`] document mapping each fingerprint to its OpenSSH +//! public key. A member *is* one or more keys whose signed pushes are accepted. +//! The document is read and written through [`git_store`], so the trust list is +//! a typed value that lives in git — versioned, auditable, and itself pushable. use std::collections::BTreeMap; use std::path::Path; use facet::Facet; -/// The ref whose tree holds the authorized signer set. -pub const AUTH_REF: &str = "refs/meta/auth"; +/// The ref whose tree holds the member set — the push trust root. +pub const MEMBERS_REF: &str = "refs/meta/members"; -/// The authorization document stored at [`AUTH_REF`]: its `signers/` subtree +/// The membership document stored at [`MEMBERS_REF`]: its `members/` subtree /// maps each fingerprint to the OpenSSH public key held there. #[derive(Debug, Clone, PartialEq, Eq, Facet)] -struct Auth { - signers: BTreeMap<String, String>, +struct Members { + members: BTreeMap<String, String>, } -impl git_store::MapDoc for Auth { +impl git_store::MapDoc for Members { fn from_entries(entries: BTreeMap<String, String>) -> Self { - Self { signers: entries } + Self { members: entries } } fn into_entries(self) -> BTreeMap<String, String> { - self.signers + self.members } } -/// One authorized signer recorded in [`AUTH_REF`]. +/// One member's authorized signing key recorded in [`MEMBERS_REF`]. #[derive(Debug, Clone, PartialEq, Eq, Facet)] pub struct Signer { /// The key it is stored under — its fingerprint. @@ -40,22 +40,22 @@ pub key: String, } -/// A failure reading or writing the signer set. +/// A failure reading or writing the member set. #[derive(Debug, thiserror::Error)] pub enum Error { - /// The signer set could not be read from or written to its ref. + /// The member set could not be read from or written to its ref. #[error(transparent)] Store(#[from] git_store::Error), } -/// Load the authorized signers recorded at [`AUTH_REF`] in `repo`. +/// Load the members recorded at [`MEMBERS_REF`] in `repo`. /// /// An absent ref yields an empty set, as on a fresh server whose trust list has /// not been pushed yet. A present but unreadable ref is an error so callers can -/// fail closed rather than mistake corruption for "no signers". +/// fail closed rather than mistake corruption for "no members". pub fn load(repo: &Path) -> Result<Vec<Signer>, Error> { Ok(git_store::Store::open(repo)? - .load_entries::<Auth>(AUTH_REF)? + .load_entries::<Members>(MEMBERS_REF)? .into_iter() .map(|(fingerprint, key)| Signer { fingerprint, @@ -64,16 +64,17 @@ .collect()) } -/// Write `signers` to [`AUTH_REF`], replacing any existing set, as a new commit. +/// Write `signers` to [`MEMBERS_REF`], replacing any existing set, as a new +/// commit. pub fn store(repo: &Path, signers: &[Signer]) -> Result<(), Error> { let entries = signers .iter() .map(|signer| (signer.fingerprint.clone(), signer.key.clone())) .collect(); - git_store::Store::open(repo)?.store_entries::<Auth>( - AUTH_REF, + git_store::Store::open(repo)?.store_entries::<Members>( + MEMBERS_REF, entries, - "Update authorized signers", + "Update members", )?; Ok(()) } @@ -142,22 +143,22 @@ } #[test] - fn empty_when_the_auth_ref_is_absent() { + fn empty_when_the_members_ref_is_absent() { let repo = unique_repo(); assert!(load(&repo).unwrap().is_empty()); let _ = std::fs::remove_dir_all(&repo); } #[test] - fn loads_the_on_disk_signers_format() { - // A fixture written as the real `signers/<fingerprint>` blob layout must - // keep loading; this fails if the Auth document's shape changes + fn loads_the_on_disk_members_format() { + // A fixture written as the real `members/<fingerprint>` blob layout must + // keep loading; this fails if the Members document's shape changes // incompatibly with data already on a ref. let repo = unique_repo(); write_meta_doc( &repo, - AUTH_REF, - "signers", + MEMBERS_REF, + "members", &[("aa:bb:cc", KEY_A), ("dd:ee:ff", KEY_B)], ); let mut loaded = load(&repo).unwrap();
crates/git-ents-server/src/web/pages.rs @@ -814,18 +814,18 @@ div.card { div.card-header { - "Authorized signers" + "Members" @if let Ok(signers) = &signers { span.count { (signers.len()) } } } p.shell-note { - "Keys on " code { "refs/meta/auth" } " whose signed pushes are accepted " - "(" code { "git ents auth list" } ")." + "Keys on " code { "refs/meta/members" } " whose signed pushes are accepted " + "(" code { "git ents members list" } ")." } @match &signers { - Err(err) => div.card-row.muted { "Could not read signers: " (err) } + Err(err) => div.card-row.muted { "Could not read members: " (err) } Ok(signers) if signers.is_empty() => { div.card-row.muted { - "No authorized signers — pushes are open until the first key is added." + "No members — pushes are open until the first key is added." } } Ok(signers) => {