80 lines
3.1 KiB
Rust
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(())
|
|
}
|