Watch
0
0
Fork
You've already forked hyperhive
0
hyperhive/hive-forge/src/verbs/credential_helper.rs

80 lines
3.1 KiB
Rust

//! `credential-helper <get|store|erase>` — implements git's
//! credential-helper protocol (`gitcredentials(7)`) so a cloned repo's
//! `.git/config` only ever holds a *reference* to this command, never
//! the forge token itself. `clone` configures the new repo's
//! `credential.helper` to invoke this subcommand; git then calls it
//! fresh on every fetch/push, reading the token from its usual
//! on-disk location (the per-agent `forge-token` file, or the
//! `--forge <label>` sidecar) at auth time rather than the token
//! having been baked into the remote URL and left durably in
//! `.git/config`. A real leak of that shape, found and reported by
//! atlas, is what this verb exists to close.
//!
//! Deliberately bypasses the normal `Client::from_env` construction in
//! `main.rs` (see the special-case there): that path requires resolving
//! an active repo, which can fail depending on where git happens to run
//! this helper from (e.g. mid-`clone`, before the destination checkout
//! exists) — and a credential lookup has no need for a repo anyway.
//!
//! Only `get` does anything: reads the `key=value` lines git sends on
//! stdin, and if a `host=` line is present, refuses to answer unless it
//! matches the resolved forge's own host (defence in depth against this
//! helper ever being invoked outside the specific-URL scope `clone`
//! configures it under). `store`/`erase` drain stdin and no-op — there
//! is nothing durable to store or erase, the token always comes fresh
//! from its file.
use std::io::Read;
use anyhow::{Context, Result, bail};
use clap::{Args as ClapArgs, ValueEnum};
use crate::client::resolve_credentials;
#[derive(ClapArgs)]
pub struct Args {
/// The git credential-protocol operation git invokes this with.
op: Op,
}
#[derive(Clone, Copy, ValueEnum)]
enum Op {
Get,
Store,
Erase,
}
/// # Errors
///
/// Returns an error if credential resolution fails (no token
/// provisioned for the active forge) or the `get` request's stdin
/// can't be read, or if a `host=` line in the request doesn't match
/// the resolved forge.
pub fn run(args: Args, forge_label: Option<&str>) -> Result<()> {
let mut input = String::new();
std::io::stdin()
.read_to_string(&mut input)
.context("read credential request from stdin")?;
let Op::Get = args.op else {
// `store`/`erase`: protocol says read the request, do nothing.
return Ok(());
};
let (base, token) = resolve_credentials(forge_label)?;
if let Some(requested) = input.lines().find_map(|l| l.strip_prefix("host=")) {
let expected = url::Url::parse(&base)
.ok()
.and_then(|u| u.host_str().map(str::to_owned));
if expected.is_some_and(|e| e != requested.trim()) {
bail!(
"hive-forge credential-helper: refusing to hand the forge token to \
host {requested:?} (configured forge is {base:?})"
);
}
}
let user = std::env::var("HIVE_LABEL").unwrap_or_else(|_| "oauth2".to_owned());
println!("username={user}");
println!("password={token}");
Ok(())
}